'How to dispose of and recreate some state
Please, consider the below code.
A textController is created during initState. If a button is pressed, another textController is created, inside of setState:
import 'package:flutter/material.dart';
void main() { runApp(Test()); }
class Test extends StatefulWidget {
TestState createState() => TestState();
}
class TestState extends State<Test> {
TextEditingController textController;
void initState() {
print("initState");
super.initState();
textController = TextEditingController(text: "1st textController");
}
void dispose() {
print("dispose");
textController.dispose();
super.dispose();
}
void onPressed() {
print("onPressed");
setState(() {
print("setState");
// It breaks if this line is uncommented.
if (textController != null) textController.dispose();
textController = TextEditingController(text: "2nd textController");
});
}
Widget build(BuildContext context) {
print("build");
var button = MaterialButton(onPressed: onPressed, child: const Text("Click Me"));
var textField = TextField(keyboardType: TextInputType.number, controller: textController);
return MaterialApp(
home: Material(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(children: [button, textField]),
),
),
);
}
}
It works. However, I've never disposed of the old textController. I can do that inside of setState, before creating the new textController:
setState(() {
print("setState");
if (textController != null) textController.dispose();
textController = TextEditingController(text: "2nd textController");
});
However, then, I get an error:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞══
I/flutter ( 4645): The following assertion was thrown building
InputDecorator(decoration: InputDecoration(), isFocused:
I/flutter ( 4645): false, isEmpty: false, state:
_InputDecoratorState#8195a(tickers: tracking 2 tickers)):
I/flutter ( 4645): A TextEditingController was used after being disposed.
I/flutter ( 4645): Once you have called dispose() on a TextEditingController, it can no longer be used.
My questions:
1) Why am I getting this error? Is the textControlled still being used? Where?
2) How to fix this?
Solution 1:[1]
I've got a couple of observations for you that might help, plus, I've prepared a code sample.
First, there is no need to recreate the TextEditingController
, as usually there's going to be one for each TextField
, or TextFormField
(depending on the implementation). You could also declare it as final
without the need for the use of initState()
.
Second, remember to dispose of the TextEditingController inside dispose()
when it is no longer needed. This will ensure we discard any resources used by the object. There is no need to dispose of it while in use. Remember: the controller is not there to notify listeners of the changes inside the text input field.
import 'package:flutter/material.dart';
// Main method
void main() {
runApp(App());
}
// I've made a separate App class that returns MaterialApp
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
);
}
}
// HomeScreen replaces your Test
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// TextEditingController could have been declared final here
// as well, for example:
//
// final _controller = TextEditingController(text: 'default');
//
// but take that as a suggestion for future ;)
TextEditingController _controller;
// String we'll be changing
String _mutableTextString = '';
@override
void initState() {
super.initState();
// Simple declarations
_controller = TextEditingController(text: 'default');
_mutableTextString = _controller.text;
}
@override
void dispose() {
// Call the dispose() method of the TextEditingController
// here, and remember to do it before the super call, as
// per official documentation:
// https://api.flutter.dev/flutter/widgets/TextEditingController-class.html
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
minimum: const EdgeInsets.all(30.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
keyboardType: TextInputType.text,
),
MaterialButton(
child: Text('CLICK ME'),
onPressed: _handleOnPressed,
),
Text(_mutableTextString),
],
),
),
),
);
}
// This method handles actions when the button is pressed
// and updates the UI
void _handleOnPressed() {
setState(() {
_mutableTextString = _controller.text;
});
}
}
I've got a short demo for you as well, hopefully, it helps!
Solution 2:[2]
- Why am I getting this error? Is the textControlled still being used? Where?
From what I've seen in your implementation, you are creating the TextController
in a setState
call. It should be created once in initState and reusing it during the lifetime of your stateful widget. Here is the part where the error occurs:
void onPressed() {
print("onPressed");
setState(() {
print("setState");
// It breaks if this line is uncommented.
if (textController != null) textController.dispose();
textController = TextEditingController(text: "2nd textController");
});
}
Since the textController
has a String
value of "1st textController" that was set here:
void initState() {
print("initState");
super.initState();
textController = TextEditingController(text: "1st textController");
}
Every time you call the onPressed()
, this line:
if (textController != null) textController.dispose();
checks the value of textController
. Since it is not null
, the textController
was disposed by the disposed()
method. There is no way that this next line will run successfully:
textController = TextEditingController(text: "2nd textController");
- How to fix this?
Currently, you have implemented the dispose() inside onPressed()
method. If you'll check closely the documentation for dispose() method, it states that:
Called when this object is removed from the tree permanently.
The framework calls this method when this State object will never build again. After the framework calls dispose, the State object is considered unmounted and the mounted property is false. It is an error to call setState at this point. This stage of the lifecycle is terminal: there is no way to remount a State object that has been disposed.
Re using your code, what I did is implement the dispose()
in the dispose()
method as described in the documentation:
void dispose() {
print("dispose");
// This is the line that breaks when called inside setState() of onPressed() method
if (textController != null) textController.dispose();
super.dispose();
}
So your code will look like this way:
import 'package:flutter/material.dart';
void main() {
runApp(Test());
}
class Test extends StatefulWidget {
TestState createState() => TestState();
}
class TestState extends State<Test> {
TextEditingController textController;
void initState() {
print("initState");
super.initState();
textController = TextEditingController(text: "1st textController");
}
void onPressed() {
print("onPressed");
setState(() {
print("setState");
print(textController);
// It breaks if this line is uncommented.
// if (textController != null) textController.dispose();
textController = TextEditingController(text: "2nd textController");
});
}
void dispose() {
print("dispose");
// This is the line that breaks when called inside setState() of onPressed() method
if (textController != null) textController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
print("build");
var button =
MaterialButton(onPressed: onPressed, child: const Text("Click Me"));
var textField = TextField(
keyboardType: TextInputType.number, controller: textController);
return MaterialApp(
home: Material(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(children: [button, textField]),
),
),
);
}
}
And it's working like this:
Solution 3:[3]
To answer why
Here is what's happening:
- You dispose the
TextEditingController
. It's OK by itself. - The app is rebuilt.
- The
TextField
sees its controller changed. - It stops listening to the old controller by calling
removeListener
on it. - The controller checks if it is disposed and breaks:
So the rule is to never dispose controllers until they stop being listened to. And this can only happen after everything is rebuilt with new controllers.
To answer how to fix
You say that
In the real code I need to substitute a subclass of TextEditingController with another different subclass.
I see the following options:
Ideally,
TextEditingController
(as anyValueNotifier
) should go hand-in-hand with its widget. So you may have your own stateful widget instead of thatTextField
, create its controller there and dispose it there. When you need to replace the controller, replace the whole widget. This way, Flutter framework takes care of disposal.Make your own super-
ValueNotifier
that aggregates your different subclasses, exposes differentTextEditingController
instances in different states of your screen, and disposes them all in itsdispose()
. Create it once in yourinitState()
, dispose it once indispose()
.Keep a list of old controllers and dispose them all in your state's
dispose()
method as the last resort. But it makes it all too messy.
TextEditingController
is something that maintains the value, allows to change it and to know that it is changed. That's it. If you want to replace it while the screen is still shown, it means you are treating it as something else which can bring problems.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | MαπμQμαπkγVπ.0 |
Solution 3 | Alexey Inkin |