I have a question about when and how a const widget will rebuild.
For example, I have a demo project like this:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
const child = ChildWidget();
return OrientationBuilder(
builder: (context, orientation) {
debugPrint('$orientation');
final isPortrait = orientation == Orientation.portrait;
return Container(
alignment: Alignment.topCenter,
child: isPortrait
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SizedBox(height: 200, child: child),
Text('Bellow text'),
])
: child,
);
},
);
}
}
class ChildWidget extends StatefulWidget {
const ChildWidget({Key? key}) : super(key: key);
#override
State<ChildWidget> createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_counter += 1;
});
},
child: Container(
color: Colors.blue, child: Center(child: Text('$_counter'))),
);
}
}
As you see, I have a const ChildWidget.
const child = ChildWidget();
When I rotate the device, it will trigger the builder function of the OrientationBuilder and return a new Container. But my question is why the child widget is rebuilt again while it is a const.
The reason why I want the child widget is not rebuilt is that I don't want the counter Text to reset to 0 each time I rotate the device.
Please advice.
Thanks a lot.
While the variable and object assigned are constant, the framework will still call the build method on the child widgets. So the actual ChildWidget class is not recreated, but the build will be called.
This is not really a problem. Flutter is really optimized for rebuilding Widgets. If the data has not changed, the actual cost of rebuilding is negligible.
Related
I have a simple flutter application. It's ok, but I'm trying to understand how onHover: (event){...} works, why "event" contains data? How can I make my own widget have function parameters like that?
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
double dx = 0, dy = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Title',
home: Scaffold(
body: MouseRegion(
onHover: (event) {
setState(() {
dx = event.localPosition.dx;
dy = event.localPosition.dy;
});
},
child: Center(
child: Text('$dx'),
),
),
),
);
}
}
To create your own onChange, or the like we can use ValueChanged.
For example, taking a look at the code for a TextButton() we see:
const TextButton({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
the onHover uses a ValueChanged.
You can implement your own valueChanged using this example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Buttons(
onHover: (value) {
// Do something
print(value);
},
),
),
),
);
}
}
class Buttons extends StatelessWidget {
final ValueChanged<String> onHover;
Buttons({Key? key, required this.onHover}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: () {
onHover('Pressed');
},
child: Text("Click me")),
Text('hi')
],
);
}
}
So this how we pass the data from the widget which is at the bottom of the widget tree.
It's more related to passing the value from bottom to top using callback functions.
Below is the simple example to demonstrate this data sharing.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _parentData = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(
"Parent State Value: " + _parentData.toString(),
),
ChildWidgetExample(
callbackFn: (data) {
setState(() {
_parentData = data;
});
},
)
],
);
}
}
class ChildWidgetExample extends StatefulWidget {
final Function(int) callbackFn;
const ChildWidgetExample({
Key? key,
required this.callbackFn,
}) : super(key: key);
#override
State<ChildWidgetExample> createState() => _ChildWidgetExampleState();
}
class _ChildWidgetExampleState extends State<ChildWidgetExample> {
int data = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(
data.toString(),
),
const SizedBox(
height: 30,
),
ElevatedButton(
onPressed: () {
setState(() {
data++;
});
widget.callbackFn(data);
},
child: const Text("Press"),
)
],
);
}
}
In Flutter you can declare Functions with parameters.
void Function(String foo) myFunction;
So you declare in as a variable in your widget component.
MyWidget({required this.myFunction});
Then when you have to call this component you can write :
...
child : MyWidget(myFunction: (String foo) {},),
I am trying to clean up my code and want to place one of my methods from a stateful widget inside of a different class that is also a stateful widget but whenever I try to call the method it does not recognize it unless the class that I am calling it from it a stateless widget. I was wondering what the best way to get around this would be without changing the class?
Here is a simple example of my problem, I am trying to call
exampleStatefulWidget.testWidget() inside of MyHomePage which is a stateful widget.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
ExampleStatefulWidget exampleStatefulWidget = ExampleStatefulWidget();
ExampleStatelessWidget exampleStatelessWidget = ExampleStatelessWidget();
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
exampleStatefulWidget.testWidget(), // Can not call method that is inside of a stateful widget
exampleStatelessWidget.testWidget(); // Will call method but only if inside of a stateless widget
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class ExampleStatefulWidget extends StatefulWidget {
const ExampleStatefulWidget({Key? key}) : super(key: key);
#override
_ExampleStatefulWidgetState createState() => _ExampleStatefulWidgetState();
}
class _ExampleStatefulWidgetState extends State<ExampleStatefulWidget> {
MyHomePage myHomePage = MyHomePage();
Widget testWidget() {
return Container(); // Do Something
}
#override
Widget build(BuildContext context) {
return Container();
}
}
class ExampleStatelessWidget extends StatelessWidget {
const ExampleStatelessWidget({ Key? key }) : super(key: key);
Widget testWidget() {
return Container(); // Do Something
}
#override
Widget build(BuildContext context) {
return Container(
);
}
}
ExampleStatefulWidget and _ExampleStatefulWidgetState are different class, you can create instance of _ExampleStatefulWidgetState instead. And use the method.
_ExampleStatefulWidgetState exampleStatefulWidget =
_ExampleStatefulWidgetState();
In this case, uses will be like
_MyHomePageState
class _MyHomePageState extends State<MyHomePage> {
Widget? widgetFromMethod;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widgetFromMethod != null) widgetFromMethod!,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
exampleStatelessWidget
.testWidget(); // Will call method but only if inside of a stateless widget
final gotWidget = exampleStatefulWidget.testWidget();
setState(() {
widgetFromMethod = gotWidget;
});
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
_ExampleStatefulWidgetState
class _ExampleStatefulWidgetState extends State<ExampleStatefulWidget> {
MyHomePage myHomePage = MyHomePage();
Widget testWidget() {
return Container(
color: Colors.purple,
width: 100,
height: 100,
child: Text("generated method from statefull"),
); // Do Something
}
#override
Widget build(BuildContext context) {
return Container();
}
}
if _ExampleStatefulWidgetState are on separate file, make it public removing _.
it will be ExampleStatefulWidgetState
Calling a printLog method of SecondWidget (Child) from FirstWidget(Parent)
Note: This is not recommended. Use any state management lib to achive this (flutter_bloc, provider etc)
import 'package:flutter/material.dart';
class FirstWidget extends StatefulWidget {
#override
_FirstWidgetState createState() => _FirstWidgetState();
}
class _FirstWidgetState extends State<FirstWidget> {
final key = GlobalKey<_SecondWidgetState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
child: Column(
children: [
ElevatedButton(
onPressed: () {
key.currentState?.printLog();
},
child: Text("Click"),
),
SecondWidget(key)
],
),
),
),
);
}
}
class SecondWidget extends StatefulWidget {
SecondWidget(Key key) : super(key: key);
#override
_SecondWidgetState createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> {
#override
Widget build(BuildContext context) {
return Container(padding: EdgeInsets.all(20), child: Container());
}
void printLog() {
print("I am called");
}
}
In this counter example, reproducible on dart pad, I am expecting the child widget to rebuild when I tap on the increment button because it is in the same tree as a the build tree of CounterParentState.
That or there is something about widget tree or the setState method that I don't understand.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller {
late CounterParentState view;
void attach(CounterParentState v) {
this.view = v;
}
int counter = 0;
void incrementCounter() {
counter++;
this.view.applyState();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Flutter Demo Home Page',
controller: controller,
child: CounterChild(controller: controller),
),
);
}
}
class CounterParent extends StatefulWidget {
final Controller controller;
final String title;
final Widget child;
CounterParent({
Key? key,
required this.title,
required this.controller,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
void applyState() {
setState(() {});
}
#override
void initState() {
widget.controller.attach(this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Why I chose this design ?
I choose this design because I wanted to be able to re-use the CounterParent and freely replace the child of the CounterParent
class CounterStylishChild extends StatelessWidget {
final Controller controller;
const CounterStylishChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times: (And I am stylish too)',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyMoreStylishApp extends StatefulWidget {
const MyMoreStylishApp({Key? key}) : super(key: key);
#override
MyMoreStylishAppState createState() => MyMoreStylishAppState();
}
class MyMoreStylishAppState extends State<MyMoreStylishApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Counter Parent with a better looking child',
controller: controller,
child: CounterStylishChild(controller: controller),
),
);
}
}
setState is working as expected and rebuilding CounterParent, but you passed a Widget instance ("child") that is stored in CounterParent and stays constant as it's constructed in MyAppState, which is never rebuilt in your code. This is the equivalent of "storing" a widget, which some do to prevent unnecessary rebuilds.
For example, the AnimatedBuilder widget does this to prevent unnecessary rebuilds of larger children, while rebuilding the widgets that have changing values that create the animation.
The AnimatedBuilder example code may provide more understanding:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
child: const Center(
child: Text('Whee!'),
),
),
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
}
The "complex" child (the Container) is passed to the AnimatedBuilder, which stores the widget. It can then pass this widget as a parameter in the builder that is called rapidly. The Transform widget gets its new rotation value on every call of the builder, but the potentially "complex" child is never actually rebuilt.
This is identical to the situation you have here, except your situation is unintentional. It's inadvisable to have multiple widgets so dependent on each other and organized in such a complex manner like what you have here.
Though you don't ask for this in your question, I'll provide a fix. This may not be what you want and it may defeat the purpose of this elaborate design, but it's the simple way to make a pattern such as this function. Elaboration as to why you wanted to do this would be helpful if this solution is not satisfactory.
The CounterChild should be instantiated in the CounterParent. This way, when setState is called for the parent, a new instance of the child is create and it is rebuilt.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller {
late CounterParentState view;
void attach(CounterParentState v) {
this.view = v;
}
int counter = 0;
void incrementCounter() {
counter++;
this.view.applyState();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Flutter Demo Home Page',
controller: controller,
),
);
}
}
class CounterParent extends StatefulWidget {
final Controller controller;
final String title;
CounterParent({
Key? key,
required this.title,
required this.controller,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
void applyState() {
setState(() {});
}
#override
void initState() {
widget.controller.attach(this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CounterChild(controller: widget.controller),
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
If you'd like to keep the CounterChild where it is, you might consider using InheritedNotifier:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
class Counter extends InheritedNotifier<Controller> {
const Counter({
Key? key,
required Controller controller,
required Widget child,
}) : super(key: key, child: child, notifier: controller);
static Controller of(BuildContext context) {
final Controller? result = context.dependOnInheritedWidgetOfExactType<Counter>()?.notifier;
assert(result != null, 'No Controller found in context');
return result!;
}
#override
bool updateShouldNotify(Counter old) => notifier?.counter != old.notifier?.counter;
}
class CounterChild extends StatelessWidget {
const CounterChild({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${Counter.of(context).counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final Controller controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Counter(
controller: controller,
child: CounterParent(
title: 'Flutter Demo Home Page',
child: CounterChild(),
),
),
);
}
}
class CounterParent extends StatefulWidget {
final String title;
final Widget child;
CounterParent({
Key? key,
required this.title,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: Counter.of(context).incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Or if you want to preserve more of your original structure, you need to pass your MyApp setState instead of your CounterParent setState so both the parent and child are rebuilt:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
late final Controller controller;
#override
void initState() {
super.initState();
controller = Controller()
..addListener(()=>setState((){}));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
controller: controller,
title: 'Flutter Demo Home Page',
child: CounterChild(controller: controller),
),
);
}
}
class CounterParent extends StatefulWidget {
final String title;
final Widget child;
final Controller controller;
CounterParent({
Key? key,
required this.title,
required this.controller,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I've create simple PageView app to test multiple pages.
import 'package:flutter/material.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: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
final firstPage = FirstPage(key: Key("FirstPage"));
final secondPage = SecondPage(key: Key("SecondPage"));
debugPrint("_MyHomePageState.build");
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView(
children: <Widget>[
firstPage,
secondPage,
],
),
);
}
}
class FirstPage extends StatelessWidget {
FirstPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
debugPrint("FirstPage.build");
return Container(
child: Center(
child: Text("First Page"),
),
);
}
}
class SecondPage extends StatelessWidget {
SecondPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
debugPrint("SecondPage.build");
return Container(
child: Center(
child: Text("Second Page"),
),
);
}
}
Even thought _MyHomePageState.build has been shown only once, FirstPage.build and SecondPage.build were printed on every page changes.
What I'd like to prevent unnecessary page draw, how can I accomplish this?
You can achieve so by using
1. const keyword
Make your widgets accept to be const:
class FirstPage extends StatelessWidget {
const FirstPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
debugPrint("FirstPage.build");
return Container(
child: Center(
child: Text("First Page"),
),
);
}
}
and call it with const keyword:
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView(
children: <Widget>[
const firstPage(),
const secondPage(),
],
),
);
2. AutomaticKeepAliveClientMixin
Convert your StatelessWidget to StatefullWidget.
class FirstPage extends StatefulWidget {
FirstPage({Key key}) : super(key: key);
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
debugPrint("FirstPage.build");
return Container(
child: Center(
child: Text("First Page"),
),
);
}
}
Extends AutomaticKeepAliveClientMixin on StatefullWidget created State.
class _FirstPageState extends State<FirstPage> with AutomaticKeepAliveClientMixin {
Call super on the build method.
#override
Widget build(BuildContext context) {
super.build(context);
debugPrint("FirstPage.build");
return Container(
child: Center(
child: Text("First Page"),
),
);
}
Override wantKeepAlive getter with true returned value.
#override
bool get wantKeepAlive => true;
And then your widget tree won't dispose of this widget so it won't rebuild over and over.
Code Example:
class FirstPage extends StatefulWidget {
FirstPage({Key key}) : super(key: key);
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
debugPrint("FirstPage.build");
return Container(
child: Center(
child: Text("First Page"),
),
);
}
#override
bool get wantKeepAlive => true;
}
3. MVVM Architecture with any State-management solution you like
It will save your state on ViewModel away from the View, so your UI can rebuild itself anytime it wants with no worries about your State because the ViewModel is still the same.
You should always imagine that your build() methods (for both StatefulWidget and StatelessWidget) are being called 60 times per second, so they should be simple and idempotent. Anything else should be moved into a StatefulWidget initState() and friends.
It's easy!
pageController can help you.
Just in your _MyHomePageState
Declare final pageController = PageController(keepPage: false);
And in your PageView
PageView(
controller: pageController,
children: <Widget>[
firstPage,
secondPage,
],
)
Good Luck.
I'm currently reading the example code of the provider package:
// ignore_for_file: public_member_api_docs
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Counter()),
],
child: Consumer<Counter>(
builder: (context, counter, _) {
return MaterialApp(
supportedLocales: const [Locale('en')],
localizationsDelegates: [
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
_ExampleLocalizationsDelegate(counter.count),
],
home: const MyHomePage(),
);
},
),
);
}
}
class ExampleLocalizations {
static ExampleLocalizations of(BuildContext context) =>
Localizations.of<ExampleLocalizations>(context, ExampleLocalizations);
const ExampleLocalizations(this._count);
final int _count;
String get title => 'Tapped $_count times';
}
class _ExampleLocalizationsDelegate
extends LocalizationsDelegate<ExampleLocalizations> {
const _ExampleLocalizationsDelegate(this.count);
final int count;
#override
bool isSupported(Locale locale) => locale.languageCode == 'en';
#override
Future<ExampleLocalizations> load(Locale locale) =>
SynchronousFuture(ExampleLocalizations(count));
#override
bool shouldReload(_ExampleLocalizationsDelegate old) => old.count != count;
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Title()),
body: const Center(child: CounterLabel()),
floatingActionButton: const IncrementCounterButton(),
);
}
}
class IncrementCounterButton extends StatelessWidget {
const IncrementCounterButton({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: Provider.of<Counter>(context).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
}
class CounterLabel extends StatelessWidget {
const CounterLabel({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
],
);
}
}
class Title extends StatelessWidget {
const Title({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text(ExampleLocalizations.of(context).title);
}
}
When the user presses the FloatingRadioButton within IncrementCounterButton, build() is called on CounterLabel and IncrementCounterButton.
They both depend on an inherited widget, which is updated.
How does flutter discover this dependency?
I assume that the BuildContext is modified by the call to Provider.of<>().
Is this why we add the IncrementCounterButton, which has no functionality on its own?
Just to move the call to Provider.of<>() outside of its bigger parent widget, which would be more expensive to rebuild?
The binding widget an InheritedWidget and its consumers is created through BuildContext.
Consider the following InheritedWidget:
class Foo extends InheritedWidget {}
Then the descendants of Foo can subscribe to it by calling:
BuildContext context
context.inheritFromWidgetOfExactType(Foo);
It's worth noting that a widget can obtain the InheritedWidget without subscribing to it, by instead doing:
BuildContext context
context.ancestorInheritedElementForWidgetOfExactType(Foo);
This call is usually performed internally by the .of(context) pattern.
In the case of provider, that subscription is done by calling Provider.of<T>(context).
provider also exposes an optional argument to purposefully not subscribe to the inherited widget:
T value = Provider.of<T>(context, listen: false);