Flutter tabview refresh issue - tabs

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.

Related

Flutter : how to change the local value in other function?

I don't know if this is possible but how to change a value in another function ?
for exemple in this function, the function receive the value but it can' return it because it return the widget "Switch":
import 'package:flutter/material.dart';
class CustumSwitch extends StatefulWidget {
bool _switchGD;
CustumSwitch(this._switchGD);
#override
_CustumSwitchState createState() => _CustumSwitchState();
}
class _CustumSwitchState extends State<CustumSwitch> {
#override
Widget build(BuildContext context) {
return Switch(
value: widget._switchGD,
onChanged: (value) {
setState(() {
widget._switchGD = !widget._switchGD;
});
});
}
}
You have to use setState() method in every case where you want to dynamically change values during build.
onChanged: (value) {
setState((){
widget._switchGD = !widget._switchGD;
});
});
It will work.. But I recommend you to learn something about Flutter best practices, because you never want to use non-final fields as Widget parameters.

Function parameter of contructor was null . #Flutter

I have a class to change UI between SignInPage and SignUpPage.
class Authenticate extends StatefulWidget {
#override
_AuthenticateState createState() => _AuthenticateState();
}
class _AuthenticateState extends State<Authenticate> {
bool showSignIn = true;
toggleView() {
setState(() {
showSignIn = !showSignIn;
});
}
#override
Widget build(BuildContext context) {
if (showSignIn) {
return SignIn(
toggleView(),
);
} else
return SignUp(
toggle: toggleView(),
);
;
}
}
and this is my Contructor in SignInPage
Function toogle;
#override
SignIn(Function fun) {
this.toogle = fun;
}
the toogle always is null and i don't know why :(
If you want to pass the function, you should pass it by reference. Now, you are executing the function and passing its result.
Change this:
return SignIn(
toggleView(),
);
to this:
return SignIn(
toggleView,
);
Also, not sure if #override is needed for the constructor, probably you can remove it.

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.

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?
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.