I'm attempting to set a field value from SharedPreferences like so:
FutureBuilder<String>(
future: _getUsername(),
initialData: 'Bruh',
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
print(snapshot);
print(snapshot.data);
print(snapshot.hasData);
return snapshot.hasData ? TextFormField(initialValue: snapshot.data, onChanged: _setUsername) : TextFormField();
}
),
This is the _getUsername() future I'm using:
Future<String> _getUsername() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('username') ?? 'hmm';
return "Fine";
}
And this is the console output:
Reloaded 0 of 567 libraries in 352ms.
I/flutter (28961): AsyncSnapshot<String>(ConnectionState.done, tesy, null)
I/flutter (28961): tesy
I/flutter (28961): true
As you can see, L2 of the output shows 'tesy', which is the value in SharedPreferences, but I only ever see 'Bruh' in the text field (the initial value).
In all the examples I can find, 'Bruh' would be displayed (extremely) briefly (if at all) before 'tesy' is then displayed in the input box. Where am I going wrong?
I guess the problem is in TextFormField itself. When the component is being rebuilt, changed initialValue is not taken into account. If you want to have a "dynamic" initial value, it's better to provide an explicit controller. Something like this:
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = 'Initial';
_initUsername();
}
Future<void> _initUsername() async {
final username = await _getUsername();
_controller.value = _controller.value.copyWith(text: username);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: TextFormField(controller: _controller, onChanged: (_) {}),
),
);
}
Future<String> _getUsername() async {
await Future.delayed(Duration(seconds: 1));
return 'Loaded';
}
}
Try change
TextFormField(initialValue: snapshot.data, onChanged: _setUsername)
To
TextFormField(key: UniqueKey(), initialValue: snapshot.data, onChanged: _setUsername)
This is because TextFormField is "Stateful" and the widget tree doesn't detect the change, puting a unique key, does the job that you want, another option is update value using an TextEditingrController.
Related
I made following simple class to allow entering a string:
import 'package:flutter/material.dart';
class MyDialog {
late TextEditingController _controller;
Future<String> showMyDialog(BuildContext context) async {
_controller = TextEditingController();
final result = await showDialog(
context: context,
builder: (BuildContext context) => _buildMyDialog(context),
);
_controller.dispose();
return result;
}
Widget _buildMyDialog(BuildContext context) {
return AlertDialog(
title: const Text('Enter string: '),
content: TextField(
autofocus: true,
controller: _controller,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(_controller.text);
},
child: const Text('OK'),
),
],
);
}
}
and I call it like this:
final dialog = MyDialog();
final result = await dialog.showMyDialog(context);
Unfortunately I get following error:
A TextEditingController was used after being disposed.
Why does it happen and how to fix it? I don't use the controller anywhere after disposing it after all.
My concern with Yeasin's answer (at time of writing) is that you are not calling dispose() on the controller and I'm not 100% certain what nulling out the instance does in that regard. It may avoid the error, but maybe there is a memory leak or other problem (hypothetically).
Another approach I can suggest would be to turn your AlertDialog into a StatefulWidget so that you can take advantage of the built-in dispose() method for Widgets. This would allow you to avoid micro-managing the controller. Instead, you call the dispose method for the controller inside the dispose method of the Widget in which it is contained.
Here is what your code could look like to take advantage of this feature of Flutter and it does not change how you call the code to bring up the dialog:
class MyDialog {
Future<String> showMyDialog(BuildContext context) async {
final result = await showDialog(
context: context,
builder: (BuildContext context) => MyAlertDialog(),
);
return result;
}
}
class MyAlertDialog extends StatefulWidget {
#override
State<MyAlertDialog> createState() => MyAlertDialogState();
}
class MyAlertDialogState extends State<MyAlertDialog> {
// for this example, it's safe to instantiate the controller inline
TextEditingController _controller = new TextEditingController();
#override
void dispose() {
// attempt to dispose controller when Widget is disposed
try { _controller.dispose(); } catch (e) {}
super.dispose();
}
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Enter string: '),
content: TextField(
autofocus: true,
controller: _controller,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(_controller.text);
},
child: const Text('OK'),
),
],
);
}
}
On showMyDialog it is getting new TextEditingController every time, we can comment _controller.dispose();. Also, it is possible to get null on this method, therefore it will be better using
Future<String?> showMyDialog(...){...}
Another thing we can do making it nullable
class MyDialog {
TextEditingController? _controller;
Future<String?> showMyDialog(BuildContext context) async {
_controller = TextEditingController();
final String? result = await showDialog(
context: context,
builder: (BuildContext context) => _buildMyDialog(context),
);
_controller = null;
return result;
}
//....
You can check about _dependents.isEmpty': is not true
I have a settings page where I'm holding a path for a keyfile in SavedPreferences. It is also possible to redefine the path for keyfile in this settings page.
class Settings extends StatefulWidget {
const Settings({Key? key}) : super(key: key);
#override
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
void initState() {
getSettings();
super.initState();
}
void getSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = prefs.getString('keyPath')!;
_keyFile = _keyPath.split('/').last;
}
String _keyPath = '';
String _keyFile = '';
Future<void> openFile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = (await FlutterFileDialog.pickFile())!;
setState(() {
print(_keyPath);
_keyFile = _keyPath.split('/').last;
prefs.setString('keyPath', _keyPath);
});
}
#override
Widget build(BuildContext context) {
getSettings();
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Column(
children: [
Row(
children: [
Expanded(
child: Column(
children: [
Text('Key File: '),
Text(_keyFile),
],
),
),
Expanded(
child: ElevatedButton(onPressed: openFile, child: Text('Open')),
)
],
)
],
),
);
}
}
This works fine when initializing for first time but when the Widget is already initialized and the navigated back second time I'm having trouble to use the saved key in SharedPreferences when navigating back to this page.
I know I'm getting the value for _keyFile and _keyPath when renavigating in
String _keyPath = '';
String _keyFile = '';
Cant figure out how to call async function when renavigating to widget without initState to use SharedPreferences
I guess this should be done via state and not to query the items from SharedPreferences but I'm little clueless how to do this exactly.
I would suggest you to use a FutureBuilder instead of getting SharedPreferences in InitState:
FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// set keys and show your Column widget
} else if (snapshot.hasError) {
// Show error widget
} else {
// Show loading Widget
}
},
),
);
Like this you will get the saved value in your SharedPreferences everytime you navigate to this widget. For more information, you can check the doc link.
For example you can use GlobalKey to store the scaffold state in use to show snackbar and etc...
I have a Listview.builder inside a FutureBuilder which taking some data from an http request.i have a bool closed i want to prevent some items from refreshing if status bool is true
how can I do that
You can achieve this by placing your call in initState. In this way you can make sure that it will get the data only once.
example:
class FutureSample extends StatefulWidget {
// Create instance variable
#override
_FutureSampleState createState() => _FutureSampleState();
}
class _FutureSampleState extends State<FutureSample> {
Future myFuture;
Future<String> _fetchData() async {
await Future.delayed(Duration(seconds: 10));
return 'DATA';
}
#override
void initState() {
// assign this variable your Future
myFuture = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FutureBuilder(
future: myFuture,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
}
return CircularProgressIndicator();
},
),
),
);
}
}
In that way you don't need a bool value. There are also different ways to achieve or extend your request. You can check this article for more informations: https://medium.com/flutterworld/why-future-builder-called-multiple-times-9efeeaf38ba2
I have a page that dynamically accepts a future list and a callback to get the future list to receive data and be able to refresh it through on refresh. a simplified version looks like this:
class ListSearchPage<T> extends StatefulWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
_ListSearchPageState createState() => _ListSearchPageState();
}
class _ListSearchPageState<T> extends State<ListSearchPage> {
Future<List<T>> itemsFuture;
TextEditingController _controller;
#override
void initState() {
itemsFuture = widget.itemsFuture;
_controller = TextEditingController();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:
itemsFuture != null ? itemsFuture : widget.getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
setState(() {
itemsFuture = null;
_controller.text = '';
});
},
child: ...
);
});
}
}
So the first time, the page loads with the future already loaded. when the user refreshes, I mark the future as null so the callback gets called and the data can be re-fetched.
I'm trying to implement flutter_hooks throughout the app now and I've refactored this widget to be like this (simplified version):
class ListSearchPage<T> extends HookWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
Widget build(BuildContext context) {
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
}
}
This works the first time, however after that the value keeps on getting assigned to null, and therefore the value notifier does not get notified about the change. How can I force the widget to rebuild in this case like before? and as a bonus, do you see a better solution for this?
Thanks in advance.
update
This is itemsFuture
final future = useMemoized(() => repository.fetchData());
This is getItemsFuture
() => repository.fetchData()
The idea behind it is to fetch the data before the search page is opened. In my use case works.
I've found a solution to my problem, but I won't post it as an answer because I don't believe is clean and I rather see if someone finds the proper way of doing it.
current solution
#override
Widget build(BuildContext context) {
// feels like a dirty solution for rebuilding on refresh
final counterNotifier = useState(0);
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
counterNotifier.value++;
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
});
As you can see I now have a counter notifier that will actually rebuild the ValueListenableBuilder and will make the FutureBuilder fetch the data
I think itemsFuture is not necessary to set to null (because it can be a initial statement inside useState).
#override
Widget build(BuildContext context) {
final fetchData = useState(itemsFuture ?? getItemsFuture());
return Scaffold(
body: FutureBuilder(
future: fetchData.value,
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
fetchData.value = getItemsFuture();
},
child: ...
);
},
),
);
}
The _futureData is to used for the FutureBuilder after retrieving value from the _loadPhobias() function.
entry_screen.dart
Future _futureData;
final TextEditingController _textEditingController = TextEditingController();
_loadPhobias() function does not seem to have any problem.
entry_screen.dart
Future<List<String>> _loadPhobias() async =>
await rootBundle.loadString('assets/phobias.txt').then((phobias) {
List _listOfAllPhobias = [];
List<String> _listOfSortedPhobias = [];
_textEditingController.addListener(() {
...
});
return _listOfSortedPhobias;
});
#override
void initState() {
super.initState();
_futureData = _loadPhobias();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
// When the value is changed, the value returned from the _loadPhobias will also change. So I want the FutureBuilder to be rebuilt.
onChanged: (text) { setState(() => _futureData = _loadPhobias()) },
),
),
body: FutureBuilder(
future: _futureData,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) => Column(
children: <Widget>[
PhobiasCard(sentence: snapshot.data[index]),
)
],
))
: Center(
child: CircularProgressIndicator(),
);
},
),
),
);
}
This is the error that I got:
FlutterError (setState() callback argument returned a Future.
The setState() method on _EntryScreenState#51168 was called with a closure or method that returned a Future. Maybe it is marked as "async".
Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().)
The first thing to note, you mentioned that you want to rebuild your app every time there's a change in the text. For that to happen, you should use StreamBuilder instead. FutureBuilder is meant to be consumed once, it's like a fire and forget event or Promise in JavaScript.
Here's to a good comparison betweenStreamBuilder vs FutureBuilder.
This is how you would refactor your code to use StreamBuilder.
main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyAppScreen(),
);
}
}
class MyAppScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppScreenState();
}
}
class MyAppScreenState extends State<MyAppScreen> {
StreamController<List<String>> _phobiasStream;
final TextEditingController _textEditingController = TextEditingController();
void _loadPhobias() async =>
await rootBundle.loadString('lib/phobia.txt').then((phobias) {
List<String> _listOfSortedPhobias = [];
for (String i in LineSplitter().convert(phobias)) {
for (String t in _textEditingController.text.split('')) {
if (i.split('-').first.toString().contains(t)) {
_listOfSortedPhobias.add(i);
}
}
}
_phobiasStream.add(_listOfSortedPhobias);
});
#override
void initState() {
super.initState();
_phobiasStream = StreamController<List<String>>();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _textEditingController,
onChanged: (text) {
print("Text $text");
_loadPhobias();
},
),
),
body: StreamBuilder(
stream: _phobiasStream.stream,
builder: (context, snapshot) {
return snapshot.hasData
? Container(
height: 300,
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
print("Data ${snapshot.data[index]}");
return Text(snapshot.data[index]);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}
As seen in the code above, I eliminated unnecessary text change callbacks inside the for a loop.
lib/phobia.txt
test1-test2-test3-test4-test5
Let me know if this is the expected scenario.
Hope this helps.
The solution can be inferred in the third line of the error message:
Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().)
So this means you'll have to perform the operation before refreshing the widget. You can have a temporary variable to hold the result of the asynchronous work and use that in your setState method:
onChanged: (text) {
setState(() => _futureData = _loadPhobias())
},
Could be written as:
onChanged: (text) async {
var phobias = _loadPhobias();
setState(() {
_futureData = phobias;
});
},