Flutter - Page with SingleTickerProviderStateMixin cause unnecessary build - flutter

I am having this issue github link. What happens is if a widget uses TickerProviderStateMixin then it gets rebuilt when a page navigation occurs. I have a very complex page and rebuilding the whole page causes a UI jank on page navigation. If I do not rebuild then everything is fine no janks. Is there a workaround for this? It seems to me that this is some sort of an internal flutter bug or unexpected behaviour?
Example:
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: PageA(title: 'Flutter Demo Home Page'),
);
}
}
class PageA extends StatefulWidget {
PageA({Key key, this.title}) : super(key: key);
final String title;
#override
_PageAState createState() => _PageAState();
}
class _PageAState extends State<PageA>
with SingleTickerProviderStateMixin {
TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
}
void toPageB() {
//tabController.animateTo(1);
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
return PageB();
}));
}
#override
Widget build(BuildContext context) {
print("Page A");
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: TabBar(
tabs: [
Text(
"Tab A",
style: Theme.of(context).textTheme.bodyText1,
),
Text(
"Tab B",
style: Theme.of(context).textTheme.bodyText1,
)
],
controller: tabController,
),
),
floatingActionButton: FloatingActionButton(
onPressed: toPageB,
child: Icon(Icons.add),
),
);
}
}
class PageB extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("Page B");
return Scaffold(
appBar: AppBar(
title: Text("Page A"),
),
body: Container(
child: Center(
child: Text("Page A"),
),
));
}
}

#override
// ignore: must_call_super
void didChangeDependencies() {}
just add the code to prevent the rebuild, I dont know the side effect, but this walk around works for my app.

This is the solution I used before.
Change your Page A like
class PageA extends StatefulWidget {
PageA({Key key, this.title}) : super(key: key);
final String title;
#override
_PageAState createState() => _PageAState();
}
class _PageAState extends State<PageA> {
TabController tabController;
// #override
// void initState() {
// super.initState();
// tabController = TabController(length: 2, vsync: this);
// }
void toPageB() {
tabController.animateTo(1);
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
return PageB();
}));
}
#override
Widget build(BuildContext context) {
print("Page A");
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: CustomTabBar(
tabs: [
Text(
"Tab A",
style: Theme.of(context).textTheme.bodyText1,
),
Text(
"Tab B",
style: Theme.of(context).textTheme.bodyText1,
)
],
controller: (controller) {
tabController = controller;
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: toPageB,
child: Icon(Icons.add),
),
);
}
}
And add a new class CustomTabBar
class CustomTabBar extends StatefulWidget {
const CustomTabBar({
this.controller,
this.tabs,
Key? key,
}) : super(key: key);
final Function(TabController)? controller;
final List<Widget>? tabs;
#override
_CustomTabBarState createState() => _CustomTabBarState();
}
class _CustomTabBarState extends State<CustomTabBar>
with SingleTickerProviderStateMixin {
late TabController tabController;
#override
void initState() {
super.initState();
tabController =
TabController(length: widget.tabs?.length ?? 0, vsync: this);
if (widget.controller != null) {
widget.controller!(tabController);
}
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TabBar(
tabs: widget.tabs ?? [],
controller: tabController,
);
}
}
It should fix the issue that Page A rebuild

Related

Why setState is not rebuilding my child widget?

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),
),
);
}
}

Skip a tab while switching among tabs in TabBarView

In my flutter app, I use a simple tab-bar. I used the code from the flutter website and updated to make sure that I can keep the state of each tab using AutomaticKeepAliveClientMixin.
I have 3 tabs and each tab is fetching a list of data (why I need to use AutomaticKeepAliveClientMixin) from my backend API.
The problem is that when I switch between first and 3rd tabs (Page1 and Page3), the middle tab keeps rebuilding over and over again until I switch to that tab (Page2) and only at that point it doesn't get rebuilt anymore.
Every rebuild results in fetching data from API and that's not desirable.
Below, i have included a simplified code to reproduce this issue.
You can see in the debug console once switching between 1st and 3rd tab (without switching to 2nd tab) that it keeps printing "p2" (in my real app, it keeps fetching data for the 2nd tab).
Is there a way to switch between tabs without other tabs in between being built/rebuilt?
This is my code.
import 'package:flutter/material.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Page1(),
Page2(),
Page3(),
],
),
),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1>
with AutomaticKeepAliveClientMixin<Page1> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p1');
return Container(
child: Center(
child: Icon(Icons.directions_car),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2>
with AutomaticKeepAliveClientMixin<Page2> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p2');
return Container(
child: Center(
child: Icon(Icons.directions_transit),
),
);
}
}
class Page3 extends StatefulWidget {
#override
_Page3State createState() => _Page3State();
}
class _Page3State extends State<Page3>
with AutomaticKeepAliveClientMixin<Page3> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p3');
return Container(
child: Center(
child: Icon(Icons.directions_bike),
),
);
}
}
I believe this isn't a bug with flutter, but ultimately comes down to your implementation.
Please take a look at the code I wrote for you.
import 'package:flutter/material.dart';
import 'dart:async';
class FakeApi {
Future<List<int>> call() async {
print('calling api');
await Future.delayed(const Duration(seconds: 3));
return <int>[for (var i = 0; i < 100; ++i) i];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
static const _icons = [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
];
#override
Widget build(BuildContext context) => DefaultTabController(
length: _icons.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [for (final icon in _icons) Tab(icon: icon)],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Center(child: _icons[0]),
StaggeredWidget(_icons[1]),
Center(child: _icons[2]),
],
),
),
);
}
class StaggeredWidget extends StatefulWidget {
const StaggeredWidget(this.icon)
: super(key: const ValueKey('StaggeredWidget'));
final Icon icon;
#override
_StaggeredWidgetState createState() => _StaggeredWidgetState();
}
class _StaggeredWidgetState extends State<StaggeredWidget> {
Widget _child;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => _child = MyApiWidget(widget.icon));
}
});
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) => _child ?? widget.icon;
}
class MyApiWidget extends StatefulWidget {
const MyApiWidget(this.icon, [Key key]) : super(key: key);
final Icon icon;
#override
_MyApiWidgetState createState() => _MyApiWidgetState();
}
class _MyApiWidgetState extends State<MyApiWidget>
with AutomaticKeepAliveClientMixin {
final _api = FakeApi();
#override
Widget build(BuildContext context) {
print('building `MyApiWidget`');
super.build(context);
return FutureBuilder<List<int>>(
future: _api(),
builder: (context, snapshot) => !snapshot.hasData
? const Center(child: CircularProgressIndicator())
: snapshot.hasError
? const Center(child: Icon(Icons.error))
: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
),
),
);
}
#override
bool get wantKeepAlive => true;
}

Flutter force reformat input

By setting _mytexteditingcontroller.value , We can update value of TextField, But inputFormatters is not running
How can I force inputFormatters to reformat value?
Here is a minimal example , I use LengthLimitingTextInputFormatter(3) to limit length of input, by running _controller.text = '12345678' I want to tell flutter to reformat input again
CONSIDER THAT IT IS A MININAL EXAMPLE, DONT TELL ME USE SUBSTRING TO FIX IT
/// Flutter code sample for TextField
// This sample shows how to get a value from a TextField via the [onSubmitted]
// callback.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
TextEditingController _controller;
void initState() {
super.initState();
_controller = TextEditingController();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
RaisedButton(
child: Text('Set Text'),
onPressed: () {
_controller.text = '12345678';
}),
TextField(
controller: _controller,
inputFormatters: [
LengthLimitingTextInputFormatter(3),
],
onSubmitted: (String value) async {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Thanks!'),
content: Text('You typed "$value".'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'),
),
],
);
},
);
},
),
],
),
),
);
}
}
As mentioned above this is a currently open known issue https://github.com/flutter/flutter/issues/30369. However you may try using an extension on TextInputFormatter to achieve a similar result.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
TextEditingController _controller;
void initState() {
super.initState();
_controller = TextEditingController();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
RaisedButton(
child: Text('3 Length Limit'),
onPressed: () {
_controller.text = LengthLimitingTextInputFormatter(3).format('12345678');
}),
RaisedButton(
child: Text('Upper case'),
onPressed: () {
_controller.text = UpperCaseTextFormatter().format('upper');
}),
RaisedButton(
child: Text('Length Limit & Upper chained'),
onPressed: () {
_controller.text = LengthLimitingTextInputFormatter(3).format(UpperCaseTextFormatter().format('upper'));
}),
TextField(
controller: _controller,
inputFormatters: [
LengthLimitingTextInputFormatter(3),
UpperCaseTextFormatter(),
],
),
],
),
),
);
}
}
extension on TextInputFormatter {
String format(String text) {
return formatEditUpdate(
const TextEditingValue(),
TextEditingValue(
text: text,
selection: TextSelection(
extentOffset: text.length,
baseOffset: text.length,
),
),
).text;
}
}
class UpperCaseTextFormatter extends TextInputFormatter {
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
return TextEditingValue(
text: newValue.text?.toUpperCase(),
selection: newValue.selection,
);
}
}

flutter drawer to remember the clicked item

I want to remember the item that was clicked in drawer .
I am using the same widget for drawer ( sameDrawerOnly ) in all three widgets ( MyHomePage , FirstPage and SecondPage) and using variable itemClicked to trackthe item that was tapped inside setState . But the conditional formatting is not working.
Here is the code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
DrawerOnly sameDrawerOnly = DrawerOnly();
class MyApp extends StatelessWidget {
final appTitle = 'Drawer Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('My Page!')),
drawer: sameDrawerOnly,
);
}
}
class DrawerOnly extends StatefulWidget {
const DrawerOnly ({
Key key,
}) : super(key: key);
#override
_DrawerOnlyState createState() => _DrawerOnlyState();
}
class _DrawerOnlyState extends State<DrawerOnly > {
int itemClicked = 0;
#override
Widget build(BuildContext ctxt) {
return Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Text("DRAWER HEADER.."),
decoration: new BoxDecoration(
color: Colors.orange
),
),
new ListTile(
title: new Text("Item => A", style: itemClicked==1 ? TextStyle( fontWeight: FontWeight.bold, color: Colors.red.withOpacity(0.6) ) : null),
onTap: () {
Navigator.pop(ctxt);
setState(() {
itemClicked=1;
});
Navigator.push(ctxt,
new MaterialPageRoute(builder: (ctxt) => new FirstPage()));
},
),
new ListTile(
title: new Text("Item => 2", style: itemClicked==2 ? TextStyle( fontWeight: FontWeight.bold , color: Colors.green.withOpacity(0.6) ) : TextStyle()),
onTap: () {
Navigator.pop(ctxt);
setState(() {
itemClicked=2;
});
Navigator.push(ctxt,
new MaterialPageRoute(builder: (ctxt) => new SecondPage()));
},
),
],
)
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
drawer: sameDrawerOnly,
appBar: new AppBar(title: new Text("First Page"),),
body: new Text("I belongs to First Page"),
);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
drawer: sameDrawerOnly,
appBar: new AppBar(title: new Text("Second Page"),),
body: new Text("I belongs to Second Page"),
);
}
}
What went wrong
Although sameDrawerOnly was declared at the top most part of your file. Everytime the widget re-draws your app's screens, eg. opening FirstPage via MaterialPageRoute, the variable in the DrawerOnly widget will always stay to zero. Because it is always re-drawn based on your configuration.
What you can do
Hotfix: Make itemClicked a static variable. (Not Recommended)
// Before
int itemClicked
// After
static int itemClicked
Alternatively, you can refactor your code and use PageView instead of opening a new Scaffold widget every time you switch between drawer items. Then, you can now use currentPageValue to determine what item was selected by the user.
MyHomePage.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final appTitle = 'Drawer Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
PageController _pageController;
double currentPageValue = 0.0;
#override
void initState() {
super.initState();
_pageController = PageController();
_pageController.addListener(() {
setState(() {
currentPageValue = _pageController.page;
// Do whatever you like with the page value
});
});
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: PageView(
controller: _pageController,
children: <Widget>[
FirstPage(),
SecondPage(),
],
),
),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
_pageController.jumpToPage(0);
Navigator.pop(context);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
_pageController.jumpToPage(1);
Navigator.pop(context);
},
),
],
),
),
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(color: Colors.red);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(color: Colors.yellow);
}
}
View on dartpad.dev.
More on:
https://flutter.dev/docs/cookbook/design/drawer

Flutter: Widget State: Is this code safe?

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")
],
),
],
),
);
}
}