Flutter: Widget State: Is this code safe? - flutter

The code below is an example to illustrate this question. The code below works, however the following line:
class WidgetCustom extends StatefulWidget {
has "WidgetCustom" underlined in green in vsCode, and when the cursor is positioned over it, it shows the message:
"This class (or a class this class inherits from) is marked as #immutable, but one or more of its instance fields are not final".
The code works fine.
Is it safe to use this code?
Is there a way to achieve this without the warning?
import 'package:flutter/material.dart';
class WidgetCustom extends StatefulWidget {
_WidgetCustomState _state;
WidgetCustom({#required int iCount}) {
_state = _WidgetCustomState(iCount);
}
#override
State<StatefulWidget> createState() {
return _state;
}
int get getIcount => _state.iCount;
}
class _WidgetCustomState extends State<WidgetCustom> {
int iCount;
_WidgetCustomState(this.iCount);
#override
Widget build(BuildContext context) {
return Container(
child: Row(children: <Widget>[
Column(
children: <Widget>[
RaisedButton(
child: const Text("Please tap me"),
onPressed: () {
setState(() => iCount = iCount + 1);
}),
SizedBox(height: 40),
Text("Tapped $iCount Times")
],
),
]));
}
}
Edited to add main.dart
import 'package:flutter/material.dart';
import 'widgetCustom.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: 'Custom Widget Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
WidgetCustom _widgetCustom;
String _sMessage = "Fab has not been pressed";
#override
void initState() {
super.initState();
_widgetCustom = WidgetCustom(iCount: 99);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(children: [
_widgetCustom,
SizedBox(height: 40),
Text(_sMessage),
]),
floatingActionButton: FloatingActionButton(
onPressed: _fabPressed,
tooltip: 'Get Value',
child: Icon(Icons.add),
),
);
}
_fabPressed() {
setState(() => _sMessage =
"Value from last button click = ${_widgetCustom.getIcount}");
}
}

Pass the initial value to the constructor when creating the widget as a final value, and then get it from the State class.

Updated code:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.dark(),
home: MyHomePage(title: 'Custom Widget Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
WidgetCustom _widgetCustom;
String _sMessage = "Fab has not been pressed";
int _value = 99;
#override
void initState() {
super.initState();
_widgetCustom = WidgetCustom(iCount: _value, function: _update);
}
void _update(int value) {
setState(() {
_value = value;
_widgetCustom = WidgetCustom(iCount: _value, function: _update);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Column(
children: [
_widgetCustom,
SizedBox(height: 40),
Text(_sMessage),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _fabPressed,
tooltip: 'Get Value',
child: Icon(Icons.add),
),
);
}
_fabPressed() {
setState(() => _sMessage = "Value from last button click = ${_value}");
}
}
class WidgetCustom extends StatefulWidget {
final int iCount;
final Function function;
WidgetCustom({#required this.iCount, this.function});
#override
State<StatefulWidget> createState() {
return _WidgetCustomState();
}
}
class _WidgetCustomState extends State<WidgetCustom> {
int _iCount;
#override
void initState() {
super.initState();
_iCount = widget.iCount;
}
#override
Widget build(BuildContext context) {
return Container(
child: Row(
children: <Widget>[
Column(
children: <Widget>[
RaisedButton(child: const Text("Please tap me"), onPressed: (){
_iCount = _iCount + 1;
widget.function(_iCount);
}),
SizedBox(height: 40),
Text("Tapped $_iCount Times")
],
),
],
),
);
}
}

Related

set state Badge appbar - flutter

I'm trying to increment the app bar icon by making a custom app bar. But I can't update the value with setState. I tried to update the value by the setAppBarValue function, but it didn't work.
MyAppBar.of(context)?.setAppBarValue(_counter);
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
MyAppBar.of(context)?.setAppBarValue(_counter);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class MyAppBar extends StatefulWidget implements PreferredSizeWidget {
MyAppBar() : super();
#override
Size get preferredSize => const Size.fromHeight(60);
#override
_MyAppBarState createState() => _MyAppBarState();
static _MyAppBarState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppBarState>();
}
class _MyAppBarState extends State<MyAppBar>
with SingleTickerProviderStateMixin {
int _appBarValue = 0;
setAppBarValue(int value) {
setState(() {
_appBarValue = value;
});
}
#override
Widget build(BuildContext context) {
return AppBar(
title: const Text("teste"),
actions: <Widget>[Center(child: _alertBadge(context))]);
}
Widget _alertBadge(BuildContext context) {
return Badge(
position: BadgePosition.topEnd(
top: 0,
end: 3,
),
badgeStyle: BadgeStyle(
badgeColor: Colors.red,
),
badgeContent: Text(
_appBarValue.toString(),
style: TextStyle(color: Colors.white),
),
child:
IconButton(icon: Icon(Icons.shopping_bag_outlined), onPressed: () {}),
);
}
}
You Have to pass the increment variable value to the appbar widget like this:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppBar(
appBarValue: _counter,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
// MyAppBar.of(context)?.setAppBarValue(_counter);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
And use that value in your app bar widget:
class MyAppBar extends StatefulWidget implements PreferredSizeWidget {
const MyAppBar({
Key? key,
required this.appBarValue,
}) : super(key: key);
final int appBarValue;
#override
Size get preferredSize => const Size.fromHeight(60);
#override
_MyAppBarState createState() => _MyAppBarState();
static _MyAppBarState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppBarState>();
}
class _MyAppBarState extends State<MyAppBar>
with SingleTickerProviderStateMixin {
// int _appBarValue = 0;
//
// setAppBarValue(int value) {
// setState(() {
// _appBarValue = value;
// });
// }
#override
Widget build(BuildContext context) {
return AppBar(
title: const Text("teste"),
actions: <Widget>[Center(child: _alertBadge(context))]);
}
Widget _alertBadge(BuildContext context) {
return Badge(
position: BadgePosition.topEnd(
top: 0,
end: 3,
),
badgeStyle: BadgeStyle(
badgeColor: Colors.red,
),
badgeContent: Text(
widget.appBarValue.toString(),
style: TextStyle(color: Colors.white),
),
child:
IconButton(icon: Icon(Icons.shopping_bag_outlined), onPressed: () {}),
);
}
}

How to Make flutter webview load only once?

when I switch from one page to another the state of my WebView isn't remembered and it reloads every time. how can prevent that?
I was expecting the web view to load only for the first time and remember it's state but it gets reloaded every time i navigate to that screen. how to make it remember the state?
You can achieve this with PageView with AutomaticKeepAliveClientMixin.
See this example app:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final bucket = PageStorageBucket();
MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
final PageController _controller = PageController();
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_controller.jumpToPage(index);
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
// selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
BottomNavigationBarItem(icon: Icon(Icons.web), label: 'web'),
]),
body: PageView(
controller: _controller,
children: const [MyDummyPage(), MyWebView()],
),
);
}
}
class MyDummyPage extends StatefulWidget {
const MyDummyPage({super.key});
#override
State<MyDummyPage> createState() => _MyDummyPageState();
}
class _MyDummyPageState extends State<MyDummyPage>
with AutomaticKeepAliveClientMixin {
late int count;
#override
void initState() {
super.initState();
count = 0;
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('$count'),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => setState(() {
count++;
}),
),
);
}
#override
bool get wantKeepAlive => true;
}
class MyWebView extends StatefulWidget {
const MyWebView({super.key});
#override
State<MyWebView> createState() => _MyWebViewState();
}
class _MyWebViewState extends State<MyWebView>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return const WebView(initialUrl: 'https://flutter.dev');
}
#override
bool get wantKeepAlive => true;
}

Show list items in a stateful widget linked to the item

I have a list of integers. Each of this item is displayed in a statefull widget by iterating the list in the build method.
import 'package:flutter/material.dart';
import 'package:widget_list/ItemWidget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Item list state demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Item list state demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static int itemsCount = 0;
final List<int> _items = List.empty(growable: true);
void _add() {
setState(() {
_items.add(itemsCount++);
});
}
void _remove() {
setState(() {
_items.removeAt(0);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () => _add(),
child: const Text('Add item'),
),
TextButton(
onPressed: () => _items.isNotEmpty ? _remove() : null,
child: const Text('Remove item'),
),
],
),
for (var item in _items) ItemWidget(item: item),
],
),
),
);
}
}
Each of this widget, has a statically incremented integer "id" in it's state. Both the item and the widget id are displayed.
import 'package:flutter/material.dart';
var widgetCount = 0;
class ItemWidget extends StatefulWidget {
final int item;
const ItemWidget({
required this.item,
Key? key,
}) : super(key: key);
#override
State<ItemWidget> createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
final int widgetId = widgetCount++;
#override
Widget build(BuildContext context) {
print("Item ${widget.item} / Widget $widgetId");
return Text("Item ${widget.item} / Widget $widgetId");
}
}
When I add an item in the list, it is displayed in a newly generated widget. E.g. first item 0 is displayed in widget 0.
But if I remove an item at the beginning of the list (e.g. item 0), it's not the first widget that is destoyed, but the last one. The item 1 is then displayed in widget 0.
The widget item is final, so it cannot change. The widget ids are still the same, so the states were not rebuild. Then, why are the states no more consistent with the widgets?
This is done in FLutter desktop for Linux, v3.0.1
In the itemWidget you are creating a value from 0 so for each element that is rendered it will start from 0. please check the code below
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Item list state demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Item list state demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static int itemsCount = 0;
final List<ItemInfo> _items = List.empty(growable: true);
void _add() {
setState(() {
itemsCount++;
_items.add(ItemInfo(itemsCount, itemsCount));
});
}
void _remove() {
setState(() {
_items.removeAt(0);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () => _add(),
child: const Text('Add item'),
),
TextButton(
onPressed: () => _items.isNotEmpty ? _remove() : null,
child: const Text('Remove item'),
),
],
),
for (var item in _items) ItemWidget(item: item),
],
),
),
);
}
}
and Itemwidget to be like this
class ItemWidget extends StatefulWidget {
final ItemInfo item;
const ItemWidget({
required this.item,
Key? key,
}) : super(key: key);
#override
State<ItemWidget> createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
#override
Widget build(BuildContext context) {
return Text(
"Item ${widget.item.itemVal} / Widget ${widget.item.itemIndex}");
}
}
also I created a class named ItemInfo which will hold both the value and its index.
class ItemInfo {
int itemVal;
int itemIndex;
ItemInfo(this.itemVal, this.itemIndex);
}

Flutter: scoped model access in StatefulWidget

I have scoped model lib/scoped_models/main.dart:
import 'package:scoped_model/scoped_model.dart';
class MainModel extends Model {
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
notifyListeners();
}
void setCount(int value) {
_count = value;
notifyListeners();
}
And very simple app lib/main.dart:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<MainModel>(
model: MainModel(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
final MainModel _model = MainModel();
void initState() {
super.initState();
// _model.incrementCount(); // <-- doesn't work !!!
}
void _incrementCounter() {
setState(() {
// _model.incrementCount(); // <-- doesn't work !!!
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
}
)
],
),
),
floatingActionButton: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return FloatingActionButton(
onPressed: () {
model.incrementCount(); // <-- only this works !!!
// _incrementCounter(); // <-- doesn't work !!!
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
)
);
}
}
The problem that I can't access MainModel outside of ScopedModelDescendant widget.
How to call MainModel methods at the beginning of _MyHomePageState class?
I believe it is possible because I don't want to keep all logic just in MainModel class and call every method in ScopedModelDescendant widget because it would be very inconvenient if there were many nested widgets.
So, how to get access to scoped model in StatefulWidget?
Use Scoped Model as provider
add ScopedModel just before the widget which use it (MyHomePage)
use ScopedModel.of<MainModel>(context) to control the model
use ScopedModelDescendant<MainModel> to listen the model
The advantage of using this:
You can access the same model in the descendants and share data easily
rebuild widget as small as possible (only ScopedModelDescendant part will be rebuilt)
code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ScopedModel<MainModel>(
model: MainModel(),
child: 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> {
void initState() {
super.initState();
}
void _incrementCounter() {
ScopedModel.of<MainModel>(context).incrementCount();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
ScopedModelDescendant<MainModel>(
builder: (context,child, model){
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Put MainModel as a Singleton
As your solution, you create MainModel once and make it final. This can be more simple like below:
MainModel
final MainModel mainModel = MainModel();
class MainModel{
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
}
void setCount(int value) {
_count = value;
}
}
MyHomePage
MainModel even no need to extend Model or use notifyListeners becaue the widget use setState to rebuild
code:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
}
void _incrementCounter() {
setState(() {
mainModel.incrementCount();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${mainModel.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
After watching into my code for a while I realized how stupid simple it was to fix.
So, obviously there should be just one instance of MainModel() for all widgets and files of the project and for convenience it should be placed in scoped model file lib/scoped_models/main.dart like this:
import 'package:scoped_model/scoped_model.dart';
final MainModel mainModel = MainModel(); // <-- create instance once for all files which require scoped model import
class MainModel extends Model {
int _count = 0;
int get count {
return _count;
}
void incrementCount() {
_count += 1;
notifyListeners();
}
void setCount(int value) {
_count = value;
notifyListeners();
}
And then you can use mainModel instance anywhere you import the model import 'package:<app_name>/scoped_models/main.dart';
So that, this code will be valid lib/main.dart:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<MainModel>(
model: mainModel, // <-- instance of model from 'lib/<app_name>/scoped_models/main.dart'
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
void initState() {
super.initState();
}
void _incrementCounter() {
setState(() {
mainModel.incrementCount(); // <-- now it works !!!
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return Text(
'${model.count}',
style: Theme.of(context).textTheme.headline4,
);
}
)
],
),
),
floatingActionButton: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return FloatingActionButton(
onPressed: () {
// model.incrementCount(); // <-- works !!!
_incrementCounter(); // <-- now it's working too !!!
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
)
);
}
}
Despite that fact that is seems reasonable, it can be overwhelming as well for the first time due to lack of examples.

Flutter: Persisting Page States

Even after reading this and this, I still can't seem to wrap my head around storing page states in Flutter.
I've built a sample app, which has a main page called MyHomePage and a second page called SecondPage. MyHomePage has a floating action button, which displays SecondPage via Navigator.push(...). The second page contains a text field with an assigned controller. I would like to preserve the text field's text after I close and reopen SecondPage.
I've tried all sorts of combinations with setting buckets, page states and keys (inspired by the links above), but I couldn't make it work.
Also I'd like to store the whole page state automatically - without the need to write/retrieve every single value manually (in case I have a lot of text fields on the page).
Here is my code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
PageStorageKey mykey = new PageStorageKey("testkey");
class MyApp extends StatelessWidget {
final PageStorageBucket _bucket = new PageStorageBucket();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PageStorage(
bucket: _bucket,
child: MyHomePage(),
)
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage()));
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
final _aController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
key: mykey,
autofocus: true,
),
)
);
}
}
EDIT:
Based on Ajay's answer, I was able to greatly simplify the working code. Turns out that in order to persist widget states manually, all you need is an instance of PageStorageBucket in combination with ValueKey instances.
Here are the modifications I did to Ajay's code:
Removed the after_layout plugin (initState method is sufficient).
Removed the global PageStorageKey instance (replaced it with a local ValueKey instance in the page that needs to use it).
Removed global instance of PageStorageBucket and replaced it with a final instance in MyApp, which is passed to the pages that need it via constructor attributes.
Removed PageStorage from the component tree.
Here is the resulting code (simplest working form):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final bucket = PageStorageBucket();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(bucket: bucket,),
);
}
}
class MyHomePage extends StatefulWidget {
final PageStorageBucket bucket;
const MyHomePage({Key key, this.bucket}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new SecondPage(bucket: widget.bucket,)));
}
}
class SecondPage extends StatefulWidget {
final PageStorageBucket bucket;
const SecondPage({Key key, this.bucket}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
static const KEY_A = ValueKey("secondPage.A");
final _aController = TextEditingController();
#override
void initState() {
super.initState();
_aController.addListener(_updateValue);
String value = widget.bucket.readState(context, identifier: KEY_A) ?? "";
_aController.text = value;
}
_updateValue() {
widget.bucket.writeState(context, _aController.text, identifier: KEY_A);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
autofocus: true,
),
),
);
}
}
you need to read and write the state as well.
Check out the below code.
Note: I have used after_layout to initialize the text controller.
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
PageStorageKey mykey = new PageStorageKey("testkey");
final PageStorageBucket _bucket = new PageStorageBucket();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PageStorage(
bucket: _bucket,
child: MyHomePage(),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new SecondPage()));
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage>
with AfterLayoutMixin<SecondPage> {
final _aController = TextEditingController();
#override
void initState() {
super.initState();
_aController.addListener(_updateValue);
}
#override
void afterFirstLayout(BuildContext context) {
String value =
_bucket.readState(context, identifier: ValueKey(mykey)) ?? "";
print(value);
_aController.text = value;
}
_updateValue() {
_bucket.writeState(context, _aController.text, identifier: ValueKey(mykey));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
key: mykey,
autofocus: true,
),
),
);
}
}