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
Related
I am using FutureBuilder in one of my widgets and it requires a future. I pass the future to the widget through its constructor. The problem is that while passing the future to the widget it gets automatically executed. Since the FutureBuilder accepts only a Future and not a Future Function() i am forced to initialize a variable which in turn calls the async function. But i don't know how to pass the Future without it getting executed.
Here is the complete working example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final icecreamSource = DataService.getIcecream();
final pizzaSource = DataService.getPizza();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
MenuButton(label: 'Ice Cream', dataSource: icecreamSource),
MenuButton(label: 'Pizza', dataSource: pizzaSource),
]
),
),
),
);
}
}
class MenuButton extends StatelessWidget {
final String label;
final Future<String> dataSource;
const MenuButton({required this.label, required this.dataSource});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
child: Text(label),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => AnotherPage(label: label, dataSource: dataSource)))
),
);
}
}
// Mock service to simulate async data sources
class DataService {
static Future<String> getIcecream() async {
print('Trying to get ice cream...');
return await Future.delayed(const Duration(seconds: 3), () => 'You got Ice Cream!');
}
static Future<String> getPizza() async {
print('Trying to get pizza...');
return await Future.delayed(const Duration(seconds: 2), () => 'Yay! You got Pizza!');
}
}
class AnotherPage extends StatefulWidget {
final String label;
final Future<String> dataSource;
const AnotherPage({required this.label, required this.dataSource});
#override
State<AnotherPage> createState() => _AnotherPageState();
}
class _AnotherPageState extends State<AnotherPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.label)),
body: Center(
child: FutureBuilder<String>(
future: widget.dataSource,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return Text('${snapshot.data}');
} else if(snapshot.hasError) {
return Text('Error occurred ${snapshot.error}');
} else {
return Text('Fetching ${widget.label}, please wait...');
}
}
),
),
);
}
}
The intended behaviour is that when i press the "Ice Cream" or "Pizza" button on the main page, the widget/screen named "Another Page" should appear and the async request should get executed during which the loading message should be displayed. However, what is happening is that on loading the homepage, even before pressing any of the buttons, both the async requests are getting executed. On pressing any of the buttons, the loading message does not appear as the request is already completed so it directly shows the result, which is totally undesirable. I am now totally confused about Futures and Future Functions. Someone please help me out.
Instead of passing the Future you could pass the function itself which returns the Future. You can try this example here on DartPad.
You have to modify MyApp like this:
final icecreamSource = DataService.getIcecream; // No () as we want to store the function
final pizzaSource = DataService.getPizza; // Here aswell
In MenuButton and in AnotherPage we need:
final Future<String> Function() dataSource; // Instead of Future<String> dataSource
No we could pass the future directly to the FutureBuilder but it's bad practice to let the FutureBuilder execute the future directly as the build method gets called multiple times. Instead we have this:
class _AnotherPageState extends State<AnotherPage> {
late final Future<String> dataSource = widget.dataSource(); // Gets executed right here
...
}
Now we can pass this future to the future builder.
instead passing Future function, why you dont try pass a string ?
Remove all final Future<String> dataSource;. You dont need it.
you can use the label only.
.....
body: Center(
child: FutureBuilder<String>(
future: widget.label == 'Pizza'
? DataService.getPizza()
: DataService.getIcecream(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
....
i just test it in https://dartpad.dev . its work fine.
you dont have to make complex, if you can achive with simple way.
I have a screen that is using values from a ChangeNotifierProvider to display the content of the screen. As expected when these values change the screen build method is triggered and the screen content is updated.
I would like that if one of those values changes from false to true, a Dialog is opened on that screen.
The only way I figured out to open this Dialog is by launching it from the build method when the value has changed and a new build is called.
I know that the showDialog is an async method while build is sync and it is an antipattern to manage these side effects from inside the build method. I cannot know when the build method will be called and this could lead to having several dialogs opened everytime the build is called.
So, so far I can only open the dialog from the build method and using a boolean flag in memory to control if the Dialog is opened or not. Like this:
class MyScreen extends StatefulWidget {
const MyScreen();
#override
State<StatefulWidget> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
late final myModel = Provider.of<MyModel?>(context);
bool _isDialogShowing = false;
#override
void initState() {
super.initState();
...
}
void _showMyDialog(BuildContext ctx) {
showDialog(
context: ctx,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: const Text("Hello I am a dialog"),
),
).then((val) {
// The dialog has been closed
_isDialogShowing = true;
});
}
#override
Widget build(BuildContext context) {
// Open dialog if value from `ChangeNotifierProvider` has changed
if (myModel?.hasToShowDialog == true && _isDialogShowing == false) {
_isDialogShowing = true;
Future.delayed(
Duration.zero,
() {
_showMyDialog(context);
},
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
.....
],
),
],
);
}
}
Do you know how to properly trigger the dialog opening event when a value changes in the ChangeNotifierProvider? Thank you very much!!!
What you can do is to add a listener to your ChangeNotifier:
late final myModel = Provider.of<MyModel?>(context);
bool _isDialogShowing = false;
#override
void initState() {
super.initState();
myModel.addListener(_showDialog);
}
#override
void dipose() {
myModel.removeListener(_showDialog);
super.dispose();
}
Future<void> _showDialog() async {
if (_isDialogShowing || !mounted) return;
// `hasToShowDialog` could be a getter and not a variable.
if (myModel?.hasToShowDialog != true) return;
_isDialogShowing = true;
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: const Text("Hello I am a dialog"),
),
);
_isDialogShowing = false;
}
I don't know what your MyModel looks like so it is hard to know what else you could fo. hasToShowDialog could be a getter and not a variable as you suggested in your question.
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
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.
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;
});
},