How to dispose of and recreate some state - widget

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?

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!

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:

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 any ValueNotifier) should go hand-in-hand with its widget. So you may have your own stateful widget instead of that TextField, 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 different TextEditingController instances in different states of your screen, and disposes them all in its dispose(). Create it once in your initState(), dispose it once in dispose().
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.

Related

Calling a widget with a function as a parameter in Flutter

I am trying to call a custom widget that has a function as a parameter, however I got no clue what parameter I could assign to it. I have tested all the ideas I came up with but with no success. The idea for this, I have gained from a tutorial, however I have done some things differently, as I have different requirements.
This is my code:
import 'package:flutter/material.dart';
class NewTransaction extends StatelessWidget {
final Function addTx;
const NewTransaction({Key? key, required this.addTx}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text('I cant solve this problem');
}
}
========================================================================
import 'package:flutter/material.dart';
import 'package:function_parameter_problem/new_transaction.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
NewTransaction(
addTx:
addTx), // What parameter can/should I pass here? It is crucial for my project
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Edit: Okay, but let's say that the NewTransaction() function returnes a scaffold with appbar, etc and I have to call it again, however in the following scenario:
_wykonajZapytanie() { //function called onPressed in the main window
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewTransaction()),
); /*I have to put a parameter here, but I just want to display another window, I don't know why, but I just can't get any ideas*/
}
IMPORTANT TO NOTICE In the original project, if it comes to the first example I have provided, I do not call NewTransaction(addTx...) in the main class, but in the UserTransaction class.
tldr I need to assemble an expandable list in the main class - which can be done the way below from what I know, but also I need to call this function in the the main class on the onPressed of a button to display all of the textfields, etc.
import 'package:flutter/material.dart';
import './new_transaction.dart';
import './transaction_list.dart';
import '../models/transaction.dart';
class UserTransaction extends StatefulWidget {
#override
_UserTransactionState createState() => _UserTransactionState();
}
class _UserTransactionState extends State<UserTransaction> {
final List<Transaction> _userTransactions = [
Transaction(
id: 1,
date: DateTime.now(),
numTel: 911911911,
// scoring: 'Link wygasł',
user: 'Polizei pau pau'),
Transaction(
id: 2,
date: DateTime.now(),
numTel: 911911911,
// scoring: 'Link wygasł',
user: 'Tha police')
];
void _addNewTransaction(int txNumTel, /* String txScoring,*/ String txUser) {
final newTx = Transaction(
numTel: txNumTel,
/*scoring: txScoring,*/
user: 'PLZ WORK',
date: DateTime.now(),
id: 3,
);
setState(() {
_userTransactions.add(newTx);
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
NewTransaction(_addNewTransaction),
]);
}
}
import 'package:flutter/material.dart';
class NewTransaction extends StatelessWidget {
final VoidCallback function;
//if nothing returns
//else typedef CustomFunc = Function(int param); (replace int with your data type)
const NewTransaction({Key? key, required this.function}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
child: Text('I cant solve this problem'),
onTap:function,
//if custom
//onTap: (){
// function(params);
//}
);
}
}
Call like below...
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
NewTransaction(
function: (){
//todo
},
//if custome function: (params){ todo },
)
],
),
),
If I understood your question correctly, this is the way you can pass a function with parameters:
import 'package:flutter/material.dart';
class NewTransaction extends StatelessWidget {
final Function() addTx;
const NewTransaction({Key? key, required this.addTx}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text('I cant solve this problem');
}
}
When passing this Function, make sure to pass it like this: NewTransaction(addTx: () => addTx());. If you pass it like this: NewTransaction(addTx: addTx()); the function gets called instantly.
When you sayFunction addTx it means any function can be passed.
If Function() addTx it means function with no parameter.
If Function(int) addTx it means function with one required positional parameter integer.
If void Function() addTx it means function with no parameter and return type should be void.
In flutter you can also use
VoidCallback which is basically void Function() written like this
final VoidCallback addTd.
.
or can use ValueChanged<T> which is basically void Function(T value)
More info at:
https://dart.dev/guides/language/language-tour#functions

How am I able to pass setState in a stateless widget from a statefull widget

In the below code I have passed set state as a function call in a stateless widget how is this possible I know there is nothing wrong with flutter, but its something about the functions in general, I am not getting the basics right can someone plz explain me this concept.
Comments are provided for assistance.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget { //STATEFUL WIDGET
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Demo(function: () {
setState(() {}); //PASSING SET STATE
});
},
),
);
}
}
class Demo extends StatelessWidget { //STATE LESS WIDGET
final Function function;
Demo({this.function});
#override
Widget build(BuildContext context) {
return Container();
}
}
setState() is a method inside the State class. In your code you have the following:
onPressed: () {
Demo(function: () {
setState(() {}); //PASSING SET STATE
});
},
Here you create an instance of Demo and give it a callback as an argument. Inside that callback, you are calling setState which is a method inside the State class.
In Dart, methods and functions can be passed around just as though they were normal variables and values. In this case, you have passed the setState method from the stateful widget into the constructor of the stateless widget. (Or, more precisely, you have wrapped a call to the stateful widget's setState in an anonymous function and then passed that function to the stateless widget.)
This can be demonstrated more simply in the following way:
class A {
int x = 0;
void doThing() {
x = 5;
}
}
class B {
Function f;
B(this.f);
void runFunction() {
f();
}
}
// ...
A a = A();
B b = B(a.doThing);
b.runFunction();
print(a.x);
// Prints: 5
In this example, the doThing from a is passed into b, which assigns it to b.f. When b calls f, it is the same as calling a.doThing directly, which can be shown by a.x having the updated value after the fact. This is what is known as passing a callback, which is a function passed to another function or object that can then be called without having to reference the passed-in function directly. i.e. b can call a.doThing by calling the callback f() without having to explicitly call a.doThing(). (Note: this is a very simplistic explanation of a callback, and a more detailed explanation goes into the nuance of how the passed function is actually used.)
This is not the same as saying that a.doThing now belongs to b, and b does not now magically have a doThing method. Likewise, when you pass in setValue to a stateless widget, you are not magically granting that widget a state and you are not breaking the rules of Flutter by suddenly being able to call "setState" from a stateless widget any more than if you had passed an instance of the State<MyApp> itself and called stateObj.setState(...) directly (not that I am recommending you do that, of course).

Flutter: how to keep widget with Google Map unchanged?

I am using Google Maps for Flutter widget.
In my app map is displayed via one of the tabs of BottomNavigationBar.
And I have the following problem:
user is on Map's tab
user changes tab (by tapping on another one)
[PROBLEM] when user returns on Map's tab map redraws.
I would like to keep map as it is when user leaves Map's tab, so he can continue to work with it when he returns to it later on.
Tried to:
use PageStorage - without success.
make something like Singletone of Map's state - without success.
use AutomaticKeepAliveClientMixin (saw here), which looked promising, but still without success.
(I admit that I could have done something wrong)
Code of last attempt:
class MapScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => MapScreenState();
}
class MapScreenState extends State<MapScreen> with AutomaticKeepAliveClientMixin {
GoogleMapController mapController;
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
title: const Text("Map"),
),
body: GoogleMap(
onMapCreated: _onMapCreated,
)
);
}
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
updateKeepAlive();
}
}
So, I just need a way to either keep MapScreen alive and unchanged, or to store its state somehow and restore it when user returns to MapScreen. Or something else which will solve the problem.
Use IndexedStack
For example:
Class _ExamplePageState extends State<ExamplePage> {
int _bottomNavIndex = 0;
final List<Widget> _children = [
WidgetOne(),
WidgetTwo(),
GoogleMap(),
]
#override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _bottomNavIndex,
children: _children,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _bottomNavIndex,
onTap: (index) {
if (_bottomNavIndex == index) return;
setState(() {
_bottomNavIndex = index;
});
}
items: [ ... ]
),
);
}
}
Widget always rebuild after you change from page to page, so, try this , use a variable for GoogleMap and reuse if it's different from null.
GoogleMap _map;
#override
Widget build(BuildContext context) {
if (_map == null){
_map = GoogleMap(
onMapCreated: _onMapCreated,
);
}
return Scaffold(
appBar: AppBar(
title: const Text("Map"),
),
body:_map,
);
}
I was just having this same problem and the mixin solved it. I think you've just missed enter the type of mixin at the end of its declaration.
class MapScreenState extends State<MapScreen> with AutomaticKeepAliveClientMixin<MapScreenState>
see this thread if anything.
AutomaticKeepAliveClientMixin
To
AutomaticKeepAliveClientMixin<MapScreenState>

Flutter Appbar (actions) with child tabs

I'm new in Flutter and I wonder if you can help me to find a solution to my problem.
I have a scaffold with a tabview and I want to make a filter (actions button) in the main appbar to manage tabs.
Both main and child are StatefulWidgets
here's my code of the main page :
Widget body = TabBarView(
controller: tabController,
children: <Widget>[
new First(filter: filter),
new Second(filter: filter);
new Third(filter: filter);
],
);
return Scaffold(
appBar: AppBar(
actions:[IconButton(icon: Icon(MdiIcons.filter),onPressed:
() {
// Send info to child
})],
bottom: new TabBar(
controller: tabController,
tabs: <Tab>[
new Tab(text:"Tab1"),
new Tab(text:"Tab2"),
new Tab(text:"Tab3")
],
body: new Builder(builder: (BuildContext context) {
_scaffoldContext = context;
return body;
}));
));
Children page :
Widget body = Text("Filter name here");
return Scaffold(
body:
new Builder(
builder: (context) {
snack = new DisplaySnackBar(context: context);
return body;
}),
);
}
I don't know where to put the actions of appbar to make Text on child's body changes from the main page.
I found the solution by making my parent widget as an InheritedWidget so I can access to it's data from child.
Here's an example :
class MyStatefulWidget extends StatefulWidget{
#override
MyStatefulWidgetState createState() => MyStatefulWidgetState();
static MyStatefulWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInheritedStateContainer) as _MyInheritedStateContainer).data;
}
}
class MyStatefulWidgetState extends State<MyStatefulWidget>{
String variableCalledHello = "Hello World";
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new _MyInheritedStateContainer(data:this,child:/* Body here */);
}
}
class _MyInheritedStateContainer extends InheritedWidget{
_MyInheritedStateContainer({
Key key,
#required Widget child,
#required this.data,
}) : super(key: key, child: child);
final MyStatefulWidgetState data;
#override
bool updateShouldNotify(_MyInheritedStateContainer oldWidget) {
return true;
}
}
and for every child :
you just have to call MyStatefulWidgetState state = MyStatefulWidgetState.of(context);
example :
#override
Widget build(BuildContext context) {
MyStatefulWidgetState state = MyStatefulWidgetState.of(context);
print(state.variableCalledHello);
return /* Container ... */;
}
Hope that will help someone to solve his problem.
Thanks.

Flutter tabview refresh issue

I have a TabBarView in my main.dart and every tab got a class to show the content(it's listview object), when i go between the tabs, the listview page refresh everytime, is it normal for tabbarview? I don't expect it will refresh everytime when i go between the tabs.
is it the problem my class? how to fix this? the code is something like this.
class ListWidget extends StatefulWidget {
final catID;
ListWidget(this.catID);
_ListWidgetState createState() => new _ListWidgetState(catID);
}
class _ListWidgetState extends State<ListWidget> {
var catID;
void initState() {
super.initState();
_fetchListData();
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(.......
}
MahMoos is right, however it's good to have an example here ...
Use AutomaticKeepAliveClientMixin
override wantKeepAlive property and return true
`
class ListWidget extends StatefulWidget {
#override
_ListWidgetState createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> with
AutomaticKeepAliveClientMixin<ListWidget>{ // ** here
#override
Widget build(BuildContext context) {
super.build(context)
return Container();
}
#override
bool get wantKeepAlive => true; // ** and here
}
If I understood you well, you are complaining about the refreshing because you need the views to save their states after moving between tabs. There is an open issue on the subject and there is a way around this problem mentioned in the comments.
Update:
There is a workaround for this issue by using AutomaticKeepAliveClientMixin which you can learn more about in this article.
If you want that your Tab view data does not refresh when you change Tab you should use
AutomaticKeepAliveClientMixin
class BaseScreen extends StatefulWidget {
BaseScreen(this.title, this.listener, {Key key}) : super(key: key);
}
class BaseScreenState extends State<BaseScreen> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
screenHeight = MediaQuery.of(context).size.height;
primaryColor = Theme.of(context).primaryColor;
textTheme = Theme.of(context).textTheme;
return Scaffold(
key: scaffoldKey,
appBar: getAppBar(),
body: Container(),
);
}
#override
bool get wantKeepAlive => true;
}
I was facing the same issue and this tutorial helped me.
Happy Coding.