How can I update some properties of the roots scaffold in a child widget(page).
Here is a snippet from my root scaffold.
CupertinoPageScaffold(
resizeToAvoidBottomInset:
state.resizeToAvoidBottomInsets, //update this here
child: CupertinoTabScaffold(
controller: _tabController,
tabBar: CupertinoTabBar(
onTap: onTap,
items: widget.items,
),
tabBuilder: (BuildContext context, index) {
return StatusBarPadding(child: _tabs[index]);
}),
),
The docs say, I should add a listener to avoid a nested scaffold (e.g. to update resizeToAvoidBottomInset).
However, this does only work for one page per tab. When I nest tabs, I can't access them directly anymore.
I tried two solutions which I will explain in the follow (+problems):
Solution 1: Provider
I used a Provider to keep track of a global Navbar State:
class NavbarState extends ChangeNotifier {
bool _resizeBottom;
NavbarState(this._resizeBottom);
get resizeBottom => _resizeBottom;
void setResizeBottom(bool state) {
_resizeBottom = state;
notifyListeners();
}
}
Then In my Pages I set the state in the initState-Method with BlocProvider.of<NavbarState>(context).setResizeBottom(val) (respective for dispose).
This has 2 problems:
Calling notifyListeners triggers a setState in the consumer and you can't call setState in the initState method.
I have to declare this in every initState and dispose method.
Solution 2: Bloc
Once again I have a global state, but it does not have to inherit from `ChangeNotifier`. I track the state with a `NavbarBloc`-class.
Then I can add an event in the onGenerateRoute method. This is more handy then the provider approach, because there is just one place where I manage this state.
However, there is still a big problem:
When I navigate back, the onGenerateRoute Method does not get called and hence the state is not getting updated.
What the easiest solution would be
At least from an app-developer perspective it would be nice if I could just ask for the the current widget which is sitting in the active navigator.
Example of a Navbar
Here is an illustration of 3 navigators for the given cupertinotabscaffold.
The middle "stack" is active and the topmost widget is seen on the screen. Thus, currently the resize param should be false. On navigating between the stacks (tapping navigation icon), the resize parameter should adjust. Furthermore, on navigating in between a single stack (push, pop) should also adjust the resize param (E.g. on a pop the param should be set to true).
I couldn't find anything like that. Thus I need your help.
For setting the state how about a setter as a callback to onTap?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var values = [
[true, false],
[false, true],
];
int stackIndex = 0;
bool _resizeToAvoidBottomInsets = true;
set _resizeToAvoidButtomInsets(bool value) => setState(() {
print("set _resizeToAvoidBottomInsets = $value");
_resizeToAvoidBottomInsets = value;
});
void handleTap(int i) {
print("tapped: $i");
_resizeToAvoidBottomInsets = values[stackIndex][i];
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
resizeToAvoidBottomInset: _resizeToAvoidBottomInsets,
child: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.ac_unit)),
BottomNavigationBarItem(icon: Icon(Icons.wb_sunny)),
],
onTap: handleTap,
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 1 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: Text(
'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
),
onPressed: () {
// set state and increment stack index before push
stackIndex++;
_resizeToAvoidButtomInsets = values[stackIndex][0];
Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 2 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: Text(
'resizeToAvoidBottomInsets: $_resizeToAvoidBottomInsets',
),
onPressed: () {
// set state and decrement stack index before pop
stackIndex--;
_resizeToAvoidButtomInsets =
values[stackIndex][0];
Navigator.of(context).pop();
}),
),
);
},
),
);
},
),
),
);
},
);
},
),
);
}
}
So I implemented a solution for the given problem. I don't think this is very smooth, but at least one can abstract the scaffold-properties from each single page.
I use a bloc for that as already mentioned in the question-solution-try-2.
The bloc emits a state MyScaffoldState which contains a ScaffoldProperties attribute. For this toy example I implemented it as follows:
class ScaffoldProperties {
bool resizeBottom;
String title;
ScaffoldProperties({this.resizeBottom = false, required this.title});
ScaffoldProperties copyWith({ScaffoldProperties? state}) {
return ScaffoldProperties(
resizeBottom: state?.resizeBottom ?? resizeBottom,
title: state?.title ?? title,
);
}
}
You could add any scaffold-property to this class.
The main work happens in my MultiTabScaffold class. This class takes the parameters
List<Widget> pages;
List<BottomNavigationBarItem> items;
Route<dynamic>? Function(RouteSettings)? onGenerateRoute;
List<NavigatorObserver> navigatorObservers
And most importantly
Function<ScaffoldProperties> getScaffoldPropertiesFromRouteName(String? route);
The class wraps the necessary Scaffolds around the pages. Additionally, it uses a BlocBuilder and thus can use the ScaffoldProperties of the current state.
The getScaffoldPropertiesFromRouteName Method takes a route and returns a ScaffoldProperties-Instance. Thus all title, resizeProperties,... have to be collected here.
To update the current state (or emit events to the bloc), I had to modify a few points.
onGenerateRoute: After generating the route (pushing to navigator), the ScaffoldProperties of the current page have to be emitted (with usage of the getScaffoldPropertiesFromRouteName Function)
WillPopScope: Also after popping the page, I emit an event with the properties of the page below.
Initial Value: Initially, when you open the app, neither onGenerateRoute nor willpop will be called. Thus, you have to have a different handling for the initial route '/' of every Navigator.
I have a toy example fully implemented.
This feels like a lot of work for such a simple use case, but I couldn't find a simpler way to do this. If there is one, please let me know.
Related
I don't know if I used correct terms in the title. I meant share by being displayed in diffrent pages with the same state, so that even if I push a new page, the “shared” widget will stay the same.
I'm trying to share the same widget across several pages, like the navigation bar of facebook.com.
As I know, Navigator widget allows to build up a seperate route. I've attempted to use the widget here, and it works quite well.
...
Scaffold(
body: Stack(
children: [
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) => MainPage());
},
// observers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
),
Positioned(
bottom: 0,
child: BottomBarWithRecord(),
)
],
));
...
To summarize the situation, there used to be only one root Navigator (I guess it's provided in MaterialApp, but anyway), and I added another Navigator in the route under a Stack (which always display BottomBarWithRecord).
This code works perfect as I expected, that BottomBarWithRecord stays the same even if I open a new page in that new Navigator. I can also open a new page without BottomBarWithRecord by pushing the page in the root Navigator: Navigator.of(context, rootNavigator: true).push(smthsmth)
However, I couldn't find a way to change BottomBarWithRecord() as the route changes, like the appbar of facebook.com.
What I've tried
Subscribe to route using navigator key
As I know, to define a navigator key, I have to write final navigatorKey = GlobalObjectKey<NavigatorState>(context);. This doesn't seem to have addListener thing, so I couldn't find a solution here
Subscribe to route using navigator observer
It was quite complicated. Normally, a super complicated solutions works quite well, but it didn't. By putting with RouteAware after class ClassName, I could use some functions like void didPush() {} didPop() didPushNext to subscribe to the route. However, it was not actually "subscribing" to the route change; it was just checking if user opened this page / opened a new page from this page / ... , which would be complicated to deal with in my situation.
React.js?
When I learned a bit of js with React, I remember that this was done quite easily; I just had to put something like
...
const [appBarIndex, setAppBarIndex] = useState(0);
//0 --> highlight Home icon, 1 --> highlight Chats icon, 2 --> highlight nothing
...
window.addEventListener("locationChange", () => {
//location is the thing like "/post/postID/..."
if (window.location == "/chats") {
setAppBarIndex(1);
} else if (window.location == "/") {
setAppBarIndex(0);
} else {
setAppBarIndex(2);
}
})
Obviously I cannot use React in flutter, so I was finding for a similar easy way to do it on flutter.
How can I make the shared BottomBarWithRecord widget change as the route changes?
Oh man it's already 2AM ahhhh
Thx for reading this till here, and I gotta go sleep rn
If I've mad e any typo, just ignore them
You can define a root widget from which you'll control what screen should be displayed and position the screen and the BottomBar accordingly. So instead of having a Navigator() and BottomBar() inside your Stack, you'll have YourScreen() and BottomBar().
Scaffold(
body: SafeArea(
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: _buildScreen(screenIndex),
),
Align(
alignment: Alignment.bottomCenter,
child: BottomBar(
screenIndex,
onChange: (newIndex) {
setState(() {
screenIndex = newIndex;
});
},
),
),
],
),
),
)
BotttomBar will use the screenIndex passed to it to do what you had in mind and highlight the selected item.
_buildScreen will display the corresponding screen based on screenIndex and you pass the onChange to your BottomBar so that it can update the screen if another item was selected. You won't be using Navigator.of(context).push() in this case unless you want to route to a screen without the BottomBar. Otherwise the onChange passed to BottomBar will be responsible for updating the index and building the new screen.
This is how you could go about it if you wanted to implement it yourself. This package can do what you want as well. Here is a simple example:
class Dashboard extends StatefulWidget {
const Dashboard({Key? key}) : super(key: key);
#override
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
final PersistentTabController _controller = PersistentTabController(initialIndex: 0);
#override
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: _controller,
screens: _buildScreens(),
items: _navBarsItems(),
);
}
List<Widget> _buildScreens() {
return [
const FirstScreen(),
const SecondScreen(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
PersistentBottomNavBarItem(
icon: const Icon(Icons.home),
title: ('First Screen'),
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.edit),
title: ('Second Screen'),
),
];
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('First Screen'),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('Second Screen'),
);
}
}
One of the main mecanism of Flutter Navigator 2.0 it the function onPopPage inside RouterDelegate > build > Navigator. However, I do not understand when route.didPop(result) returns false.
We can use the John Ryan's famous example to show my question. His demo code.
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
show404 = false;
notifyListeners();
return true;
},
On all of my tests, using AppBar autogenerated back button, route.didPop(result) returns true.
The doc stays :
bool didPop(dynamic result)
package:flutter/src/widgets/navigator.dart
A request was made to pop this route. If the route can handle it internally (e.g. because it has its own stack of internal state) then return false, otherwise return true (by returning the value of calling super.didPop). Returning false will prevent the default behavior of [NavigatorState.pop].
When this function returns true, the navigator removes this route from the history but does not yet call [dispose]. Instead, it is the route's responsibility to call [NavigatorState.finalizeRoute], which will in turn call [dispose] on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
This method should call [didComplete] to resolve the [popped] future (and this is all that the default implementation does); routes should not wait for their exit animation to complete before doing so.
See [popped], [didComplete], and [currentResult] for a discussion of the result argument.
But was does "If the route can handle it internally (e.g. because it has its own stack of internal state) then return false" mean ? The route has its own stack of internal state ? How to produce this result ?
Thank you, stay safe
After some research to fully understand the Navigator 2.0, I think this might be the answer to the question:
route.didPop(result) will return false, when the Route, which are asked to pop, keeps local history entries and they have to be removed before popping the complete Route.
So what are local history entries (the stack of internal states)?
Local history entries are a way to implement local navigation within a page. You can do so using the method addLocalHistoryEntry. To understand this better, take a look at the official Flutter Docs sample:
The following example is an app with 2 pages: HomePage and SecondPage.
The HomePage can navigate to the SecondPage. The SecondPage uses a
LocalHistoryEntry to implement local navigation within that page.
Pressing 'show rectangle' displays a red rectangle and adds a local
history entry. At that point, pressing the '< back' button pops the
latest route, which is the local history entry, and the red rectangle
disappears. Pressing the '< back' button a second time once again pops
the latest route, which is the SecondPage, itself. Therefore, the
second press navigates back to the HomePage.
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (BuildContext context) => HomePage(),
'/second_page': (BuildContext context) => SecondPage(),
},
);
}
}
class HomePage extends StatefulWidget {
HomePage();
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('HomePage'),
// Press this button to open the SecondPage.
ElevatedButton(
child: Text('Second Page >'),
onPressed: () {
Navigator.pushNamed(context, '/second_page');
},
),
],
),
),
);
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
bool _showRectangle = false;
void _navigateLocallyToShowRectangle() async {
// This local history entry essentially represents the display of the red
// rectangle. When this local history entry is removed, we hide the red
// rectangle.
setState(() => _showRectangle = true);
ModalRoute.of(context).addLocalHistoryEntry(
LocalHistoryEntry(
onRemove: () {
// Hide the red rectangle.
setState(() => _showRectangle = false);
}
)
);
}
#override
Widget build(BuildContext context) {
final localNavContent = _showRectangle
? Container(
width: 100.0,
height: 100.0,
color: Colors.red,
)
: ElevatedButton(
child: Text('Show Rectangle'),
onPressed: _navigateLocallyToShowRectangle,
);
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
localNavContent,
ElevatedButton(
child: Text('< Back'),
onPressed: () {
// Pop a route. If this is pressed while the red rectangle is
// visible then it will will pop our local history entry, which
// will hide the red rectangle. Otherwise, the SecondPage will
// navigate back to the HomePage.
Navigator.of(context).pop();
},
),
],
),
),
);
}
}
To see the sample in the docs, click here.
I hope I answered the question in an understandable way.
There's a similar unanswered question here: FullscreenDialog screen not covering bottomnavigationbar
but I want to provide more context, to see if that helps find a solution.
We'll start at the top with my main.dart, I am building a MaterialApp that builds a custom DynamicApp. Here's the important stuff:
Widget build(BuildContext context) {
var _rootScreenSwitcher = RootScreenSwitcher(key: switcherKey);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
builder: (context, child) {
return DynamicApp(
navigator: locator<NavigationService>().navigatorKey,
child: child,
switcher: _rootScreenSwitcher,
);
},
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
},
home: _rootScreenSwitcher,
);
}
My DynamicApp sets up the root Scaffold like so:
Widget build(BuildContext context) {
return Scaffold(
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
drawerItems: widget.drawerItems,
headerView: Container(
child: Text('Drawer Header'),
decoration: BoxDecoration(color: Colors.blue),
),
onNavigationItemSelect: (index) {
onTapped(index);
return true; // true means that drawer must close and false is Vice versa
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
onTapped(index);
},
currentIndex: _selectedIndex,
items: bottomNavBarItems,
showUnselectedLabels: false,
),
body: widget.child,
);
}
The child of the DynamicApp is a widget called RootScreenSwitcher which is an IndexedStack and controls the switching of screens from my BottomNavigationBar and also when items are selected in the Drawer. Here's the RootScreenSwitcher:
class RootScreenSwitcherState extends State<RootScreenSwitcher> {
int _currentIndex = 0;
int get currentIndex => _currentIndex;
set currentIndex(index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentIndex,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
),
);
}
void switchScreen(int index) {
setState(() {
_currentIndex = index;
});
}
}
Each of the main section of the app has its own Navigator and root screen. This all works fine, and I'm happy with the overall navigation structure. Each root screen has its own AppBar but the global Drawer and BottomNavigationBar are handled in the DynamicApp so I don't have to keep setting them in the other Scaffold screens.
So, then it came to start to introduce other sections of the app that are not serviced by the bottom tab bar, and can be presented from the Drawer or from other action buttons. Each of these new sections would have to be modal fullscreenDialog screens so they slide up from the bottom, but have their own navigation and scaffold.
My issue is that when I navigate to my SettingsNavigator screen it slides up from behind the BottomNavigationBar, and not on top of everything. Here's the onGenerateRoute method of the MaterialApp:
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
}
I'm new to Flutter and don't quite understand how routing works with contexts, so I am wondering whether the context of the screen that calls the navigateTo method of the Navigator becomes the main build context, and is therefore not on top of the widget tree?
gif here: https://www.dropbox.com/s/nwgzo0q28cqk61p/FlutteRModalProb.gif?dl=0
Here's the tree structure that shows that the Scaffold for the Settings screen has been placed inside the DynamicApp Scaffold. The modal needs to sit above DynamicApp.
Can anyone shed some light on this?
UPDATE: I have tried creating and sharing a unique ScaffoldState key for the tab bar screens, and then the Settings page has a different key. It made no difference. I wonder now if it is the BuildContext having the same parent.
UPDATE UPDATE:
I had a breakthrough last night which has made me realise that it just isn't going to be possible to use embedded Scaffolds in the way I have them at the moment. The problem is that i have a root scaffold called DynamicApp which persists my Drawer and BottomNavigationBar, but loading in other Scaffold pages into the body means the modals are slotting into that body and behind the BottomNavigationBar. To solve the problem you have to subclass BottomNavigationBar and reference it in every Scaffold; which means encapsulating all of the business logic so it uses ChangeNotifier to change state when the nav is interacted with. Basically, Flutter forces a separation of concerns on your architecture, which I guess is a good thing. I'll compose a better answer when I've done all the extra work.
After many hours tearing my hair out trying to pass ScaffoldState keys around, and Navigators the answer was to build the BottomNavigationBar into every Scaffold. But to do this I've had to change how my architecture works ... for the better! I now have a BottomNavigationBar and RootScreenSwitcher that listens for updates from an AppState ChangeNotifier and rebuilds itself when the page index changes. So, I only have to change state in one place for the app to adapt automatically. This is the AppState class:
import 'package:flutter/material.dart';
class AppState extends ChangeNotifier {
int _pageIndex = 0;
int get pageIndex {
return _pageIndex;
}
set setpageIndex(int index) {
_pageIndex = index;
notifyListeners();
}
}
and this is my custom BottomNavigationBar called AppBottomNavigationBar:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBottomNavigationBar extends StatefulWidget {
AppBottomNavigationBar({Key key}) : super(key: key);
#override
_AppBottomNavigationBarState createState() => _AppBottomNavigationBarState();
}
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex ?? 0,
items: bottomNavBarItems,
showUnselectedLabels: false,
onTap: (int index) {
setState(() {
state.setpageIndex = index;
});
},
);
}
}
So, now in my other Scaffold pages I only need to include this line to make sure the BottomNavigationBar is in the Scaffold`:
bottomNavigationBar: AppBottomNavigationBar(),
Which means absolute minimal boilerplate.
I changed the name of the DynamicApp class to AppRootScaffold and this now contains a Scaffold, Drawer, and then set the body of the Scaffold as the RootScreenSwitcher:
class RootScreenSwitcher extends StatelessWidget {
RootScreenSwitcher({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return SafeArea(
top: false,
child: IndexedStack(
index: currentIndex ?? 0,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
);
}
}
I still have a lot to do to streamline this architecture, but it is definitely the better way to go.
UPDATE:
Can you spot the problem with the new Scaffold architecture?
This is still not great. Ironically, I need the BottomNavigationBar back in the root Scaffold for this to work as expected. But then the modals wont appear over the top of the bar again.
For various reasons, sometimes the build method of my widgets is called again.
I know that it happens because a parent updated. But this causes undesired effects.
A typical situation where it causes problems is when using FutureBuilder this way:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: httpCall(),
builder: (context, snapshot) {
// create some layout here
},
);
}
In this example, if the build method were to be called again, it would trigger another HTTP request. Which is undesired.
Considering this, how to deal with the unwanted build? Is there any way to prevent a build call?
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as:
Route pop/push
Screen resize, usually due to keyboard appearance or orientation change
The parent widget recreated its child
An InheritedWidget the widget depends on (Class.of(context) pattern) change
This means that the build method should not trigger an http call or modify any state.
How is this related to the question?
The problem you are facing is that your build method has side effects/is not pure, making extraneous build calls troublesome.
Instead of preventing build calls, you should make your build method pure, so that it can be called anytime without impact.
In the case of your example, you'd transform your widget into a StatefulWidget then extract that HTTP call to the initState of your State:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
#override
void initState() {
future = Future.value(42);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
I know this already. I came here because I really want to optimize rebuilds
It is also possible to make a widget capable of rebuilding without forcing its children to build too.
When the instance of a widget stays the same; Flutter purposefully won't rebuild children. It implies that you can cache parts of your widget tree to prevent unnecessary rebuilds.
The easiest way is to use dart const constructors:
#override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(),
child: Text("Hello World"),
);
}
Thanks to that const keyword, the instance of DecoratedBox will stay the same even if the build was called hundreds of times.
But you can achieve the same result manually:
#override
Widget build(BuildContext context) {
final subtree = MyWidget(
child: Text("Hello World")
);
return StreamBuilder<String>(
stream: stream,
initialData: "Foo",
builder: (context, snapshot) {
return Column(
children: <Widget>[
Text(snapshot.data),
subtree,
],
);
},
);
}
In this example when StreamBuilder is notified of new values, subtree won't rebuild even if the StreamBuilder/Column does.
It happens because, thanks to the closure, the instance of MyWidget didn't change.
This pattern is used a lot in animations. Typical uses are AnimatedBuilder and all transitions such as AlignTransition.
You could also store subtree into a field of your class, although less recommended as it breaks the hot-reload feature.
You can prevent unwanted build calling, using these way
Create child Statefull class for individual small part of UI
Use Provider library, so using it you can stop unwanted build method calling
In these below situation build method call
After calling initState
After calling didUpdateWidget
when setState() is called.
when keyboard is open
when screen orientation changed
If Parent widget is build then child widget also rebuild
Flutter also has ValueListenableBuilder<T> class . It allows you to rebuild only some of the widgets necessary for your purpose and skip the expensive widgets.
you can see the documents here ValueListenableBuilder flutter docs
or just the sample code below:
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:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
One of the easiest ways to avoid unwanted reBuilds that are caused usually by calling setState() in order to update only a specific Widget and not refreshing the whole page, is to cut that part of your code and wrap it as an independent Widget in another Stateful class.
For example in following code, Build method of parent page is called over and over by pressing the FAB button:
import 'package:flutter/material.dart';
void main() {
runApp(TestApp());
}
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
int c = 0;
#override
Widget build(BuildContext context) {
print('build is called');
return MaterialApp(home: Scaffold(
appBar: AppBar(
title: Text('my test app'),
),
body: Center(child:Text('this is a test page')),
floatingActionButton: FloatingActionButton(
onPressed: (){
setState(() {
c++;
});
},
tooltip: 'Increment',
child: Icon(Icons.wb_incandescent_outlined, color: (c % 2) == 0 ? Colors.white : Colors.black)
)
));
}
}
But if you separate the FloatingActionButton widget in another class with its own life cycle, setState() method does not cause the parent class Build method to re-run:
import 'package:flutter/material.dart';
import 'package:flutter_app_mohsen/widgets/my_widget.dart';
void main() {
runApp(TestApp());
}
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
int c = 0;
#override
Widget build(BuildContext context) {
print('build is called');
return MaterialApp(home: Scaffold(
appBar: AppBar(
title: Text('my test app'),
),
body: Center(child:Text('this is a test page')),
floatingActionButton: MyWidget(number: c)
));
}
}
and the MyWidget class:
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
int number;
MyWidget({this.number});
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: (){
setState(() {
widget.number++;
});
},
tooltip: 'Increment',
child: Icon(Icons.wb_incandescent_outlined, color: (widget.number % 2) == 0 ? Colors.white : Colors.black)
);
}
}
I just want to share my experience of unwanted widget build mainly due to context but I found a way that is very effective for
Route pop/push
So you need to use Navigator.pushReplacement() so that the context of the previous page has no relation with the upcoming page
Use Navigator.pushReplacement() for navigating from the first page to Second
In second page again we need to use Navigator.pushReplacement()
In appBar we add -
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pushReplacement(
context,
RightToLeft(page: MyHomePage()),
);
},
)
In this way we can optimize our app
You can do something like this:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
#override
void initState() {
future = httpCall();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
void refresh(){
setState((){
future = httpCall();
});
}
}
For various reasons, sometimes the build method of my widgets is called again.
I know that it happens because a parent updated. But this causes undesired effects.
A typical situation where it causes problems is when using FutureBuilder this way:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: httpCall(),
builder: (context, snapshot) {
// create some layout here
},
);
}
In this example, if the build method were to be called again, it would trigger another HTTP request. Which is undesired.
Considering this, how to deal with the unwanted build? Is there any way to prevent a build call?
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as:
Route pop/push
Screen resize, usually due to keyboard appearance or orientation change
The parent widget recreated its child
An InheritedWidget the widget depends on (Class.of(context) pattern) change
This means that the build method should not trigger an http call or modify any state.
How is this related to the question?
The problem you are facing is that your build method has side effects/is not pure, making extraneous build calls troublesome.
Instead of preventing build calls, you should make your build method pure, so that it can be called anytime without impact.
In the case of your example, you'd transform your widget into a StatefulWidget then extract that HTTP call to the initState of your State:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
#override
void initState() {
future = Future.value(42);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
I know this already. I came here because I really want to optimize rebuilds
It is also possible to make a widget capable of rebuilding without forcing its children to build too.
When the instance of a widget stays the same; Flutter purposefully won't rebuild children. It implies that you can cache parts of your widget tree to prevent unnecessary rebuilds.
The easiest way is to use dart const constructors:
#override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(),
child: Text("Hello World"),
);
}
Thanks to that const keyword, the instance of DecoratedBox will stay the same even if the build was called hundreds of times.
But you can achieve the same result manually:
#override
Widget build(BuildContext context) {
final subtree = MyWidget(
child: Text("Hello World")
);
return StreamBuilder<String>(
stream: stream,
initialData: "Foo",
builder: (context, snapshot) {
return Column(
children: <Widget>[
Text(snapshot.data),
subtree,
],
);
},
);
}
In this example when StreamBuilder is notified of new values, subtree won't rebuild even if the StreamBuilder/Column does.
It happens because, thanks to the closure, the instance of MyWidget didn't change.
This pattern is used a lot in animations. Typical uses are AnimatedBuilder and all transitions such as AlignTransition.
You could also store subtree into a field of your class, although less recommended as it breaks the hot-reload feature.
You can prevent unwanted build calling, using these way
Create child Statefull class for individual small part of UI
Use Provider library, so using it you can stop unwanted build method calling
In these below situation build method call
After calling initState
After calling didUpdateWidget
when setState() is called.
when keyboard is open
when screen orientation changed
If Parent widget is build then child widget also rebuild
Flutter also has ValueListenableBuilder<T> class . It allows you to rebuild only some of the widgets necessary for your purpose and skip the expensive widgets.
you can see the documents here ValueListenableBuilder flutter docs
or just the sample code below:
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:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
One of the easiest ways to avoid unwanted reBuilds that are caused usually by calling setState() in order to update only a specific Widget and not refreshing the whole page, is to cut that part of your code and wrap it as an independent Widget in another Stateful class.
For example in following code, Build method of parent page is called over and over by pressing the FAB button:
import 'package:flutter/material.dart';
void main() {
runApp(TestApp());
}
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
int c = 0;
#override
Widget build(BuildContext context) {
print('build is called');
return MaterialApp(home: Scaffold(
appBar: AppBar(
title: Text('my test app'),
),
body: Center(child:Text('this is a test page')),
floatingActionButton: FloatingActionButton(
onPressed: (){
setState(() {
c++;
});
},
tooltip: 'Increment',
child: Icon(Icons.wb_incandescent_outlined, color: (c % 2) == 0 ? Colors.white : Colors.black)
)
));
}
}
But if you separate the FloatingActionButton widget in another class with its own life cycle, setState() method does not cause the parent class Build method to re-run:
import 'package:flutter/material.dart';
import 'package:flutter_app_mohsen/widgets/my_widget.dart';
void main() {
runApp(TestApp());
}
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
int c = 0;
#override
Widget build(BuildContext context) {
print('build is called');
return MaterialApp(home: Scaffold(
appBar: AppBar(
title: Text('my test app'),
),
body: Center(child:Text('this is a test page')),
floatingActionButton: MyWidget(number: c)
));
}
}
and the MyWidget class:
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
int number;
MyWidget({this.number});
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: (){
setState(() {
widget.number++;
});
},
tooltip: 'Increment',
child: Icon(Icons.wb_incandescent_outlined, color: (widget.number % 2) == 0 ? Colors.white : Colors.black)
);
}
}
I just want to share my experience of unwanted widget build mainly due to context but I found a way that is very effective for
Route pop/push
So you need to use Navigator.pushReplacement() so that the context of the previous page has no relation with the upcoming page
Use Navigator.pushReplacement() for navigating from the first page to Second
In second page again we need to use Navigator.pushReplacement()
In appBar we add -
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pushReplacement(
context,
RightToLeft(page: MyHomePage()),
);
},
)
In this way we can optimize our app
You can do something like this:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
#override
void initState() {
future = httpCall();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
void refresh(){
setState((){
future = httpCall();
});
}
}