AutomaticKeepAliveClientMixin doesn't work in Flutter - flutter

`` I am making an app which should be able to open multiple tabs. I am saving tabs as widgets in Map<Item, Widget>, where Item represents tab info, and the widget is a tab widget. When Navigator is called to show specific tab, it searches in map by item and returns tab widget. If there is no such tab, it creates new tab widget, saves it in map and returns tab. My tab widget is using AutomaticKeepAliveClientMixin with wantKeepAlive => true and super.build(context), it is all ok in widget code.
The problem is that state is not saving. I debugged it and figured out, that when widget is firstly created and super.build(context) is called, it does not call _ensureKeepAlive because the condition is not met (code from super.build):
#mustCallSuper
#override
Widget build(BuildContext context) {
if (wantKeepAlive && _keepAliveHandle == null) {
_ensureKeepAlive();
}
return const _NullWidget();
}
}
wantKeepAlive is true in this condition, but _keepAliveHandle somewhy is not null already, so _ensureKeepAlive() is not running.
When I open this tab later, _ensureKeepAlive() is not called for the same reason, and then my widget builder is being executed, which rebuilds widget from scratch. So _ensureKeepAlive() will be never called. What's wrong with my code?
Here is a part of my code where Navigator is called:
final Map<Item, Widget> pages = {};
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) {
Item pageId = (settings.arguments ??
Item(type: ItemTypes.boardList, tag: "", name: "Doski"))
as Item;
Widget? page = pages[pageId];
// if page with that id not exist yet, then create it
if (page == null) {
page = BoardListScreen(
key: ValueKey(pageId),
title: "Doski",
pages[pageId] = page;
}
}
return MaterialPageRoute(builder: (context) {
return pages[pageId]!;
});
}),
drawer: Drawer(...));
}
I tried to put a unique key to a new widget, nothing has changed.

Related

Navigation inside NavigationBody fluent_ui Flutter

I'm writing a flutter windows application using fluent_ui package.
I have NavigationView with NavigationPage and NavigationBody items.
On some NavigationBody item, I want to navigate to the other page (for example from page app/cars/ to app/cars/id) but leave the same NavigationPane state (which mean changing only a NavigationBody widget).
How can I achieve this? Only by using some kind of SetState() which totally changes the content of the widget or some solutions with using Navigator?
Basic page structure:
- NavigationView
- NavigationPane
- NavigationBody
So. I found a solution to this problem. My solution is based on using Navigator as a base widget. In NavigationView as a NavigationBody item used not concrete page (widget), but Navigator which takes care of inner navigation. So Push() method changes only the NavigationBody of NavigationView and not the entire window of the application.
Here is Code example for this. Hope someone will find this helpful:
nav_view_page.dart
NavigationView(
appBar: //some app bar
pane: // your NavigationPane
content: NavigationBody(
index: _selectedIndex,
children: [
//some other body items
CarsPageNavigator(
navigatorKey: GlobalKey<NavigatorState>(),
carsController: widget.carsController,
), //this is our navigator
],
),
);
custom_navigator.dart
class CarsPageNavigator extends StatelessWidget {
const CarsPageNavigator(
{Key? key, required this.navigatorKey, required this.carsController})
: super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final CarsController carsController;
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/car',
onGenerateRoute: (RouteSettings routeSettings) {
return FluentPageRoute(
settings: routeSettings,
builder: (context) {
return getPage(routeSettings.name);
});
});
}
Widget getPage(String? url) {
switch (url) {
case CarsManagementPage.routeName:
{
return CarsManagementPage(carsController: carsController);
}
case CarManagementPage.routeName:
return CarManagementPage(controller: carsController);
default:
return CarsManagementPage(carsController: carsController);
}
}
}
In this code in OnGenerateRoute method returns page that you want to navigate to.
To navigate to other page from some inner page use pushNamed(or other named push methods of Navigator):
Navigator.pushNamed(context, CarManagementPage.routeName, arguments: car);
small remark: some "basic" code is skipped to make answer smaller

Flutter persistent sidebar

In my application I want to have a sidebar that allows me to have access to specific functions everywhere in my application.
What I want :
That the sidebar remains visible when I push my pages
That I can pushNamed route or open a modal with one of the sidebar functions
That I can not display the sidebar on certain pages
What I do :
In red, the persistent sidebar and in yellow my app content.
If I click on my profil button in the HomeView, the ProfilView is displayed and my sidebar remains visible so it's ok
My AppView :
class AppView extends StatelessWidget {
const AppView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: AppConfig.kAppName,
debugShowCheckedModeBanner: false,
theme: AppTheme().data,
builder: (context, child) => SidebarTemplate(child: child), // => I create a template
onGenerateRoute: RouterClass.generate,
initialRoute: RouterName.kHome,
);
}
My SidebarTemplate : (Display the sidebar and load the page with my router)
class SidebarTemplate extends StatelessWidget {
final Widget? child;
const SidebarTemplate({Key? key, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body : Row(
children: [
SidebarAtom(), // => My sidebar Widget
Expanded(
child: ClipRect(
child: child! // => My view
),
)
],
)
),
);
}
}
My RouterClass :
abstract class RouterClass{
static Route<dynamic> generate(RouteSettings settings){
final args = settings.arguments;
switch(settings.name){
case RouterName.kHome:
return MaterialPageRoute(
builder: (context) => HomeView()
);
case RouterName.kProfil:
return MaterialPageRoute(
builder: (context) => ProfilView(title: "Profil",)
);
default:
return MaterialPageRoute(
builder: (context) => Error404View(title: "Erreur")
);
}
}
}
How to do :
To pushNamed or open a modal with a button from my sidebar because I have an error
The following assertion was thrown while handling a gesture:
I/flutter (28519): Navigator operation requested with a context that does not include a Navigator.
I/flutter (28519): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter (28519): descendant of a Navigator widget.
To hide the sidebar when I want like SplashScreen for example
Any guidance on the best way to accomplish this would be appreciated.
You can use a NavigatorObserver to listen to the changes in the route.
class MyNavObserver with NavigatorObserver {
final StreamController<int> streamController;
MyNavObserver({required this.streamController});
#override
void didPop(Route route, Route? previousRoute) {
if (previousRoute != null) {
if (previousRoute.settings.name == null) {
streamController.add(3);
} else {
streamController
.add(int.parse(previousRoute.settings.name!.split('/').last));
}
}
}
#override
void didPush(Route route, Route? previousRoute) {
if (route.settings.name == null) {
streamController.add(3);
} else {
streamController.add(int.parse(route.settings.name!.split('/').last));
}
}
}
and using StreamController you can make changes to your SidebarTemplate by putting it inside StreamBuilder. This will take care of all the requirements you have mentioned in the question.
Check out the live example here.
As you can see from the Profil screenshot, the sidebar is not part of the widget subtree of the Navigator (the back button is only on the profil widget). This means that you cannot find the Navigator from the context of the sidebar. That is happening because you are using builder in your MaterialApp which inserts widgets above the navigator.
That is also the reason why you cannot hide the sidebar when you want to show a splash screen.
Do you really need to use the builder on MaterialApp? Then you can save the Navigator globally and access it from the sidebar. This is the first article when I search on DuckDuckGo, that you can follow.
To show a SplashScreen you would need to add a state to AppView and change the builder function. Not very nice if you ask me.
I suggest you to re-think your architecture and get rid of the builder in the MaterialApp.

Flutter Navigator (2.0) How can I return control to the Base Router from a Nested Router on a certain Pop event?

I would like to use Flutter’s (2.0) navigation for routing on my mobile app. I cannot find cookbook examples and followed the recommended guide, implementing the nested router example.
NOTE ===
If a rendered view has a unique route (uri) within the app I call it a Page.
If it does not have a unique route, I call it a Screen.
========
The base router selects between pages in the app. The nested router in the “Resource Dashboard” uses the resourceViewState object to select the screen to render within the Resource Dashboard” Page. Just by using the selectedIndex as below, I can change the screen depending on which index the user has selected in a Material Design Drawer.
As a result, when the user is on any non-default screen (i.e. Details A, Details B) in the above diagram, and there is a pop event, the user is returned to the default screen. If the user pops from the default screen, they are returned to the “Select Resource” Page outside of the Nested Router. But I have one more sticky case to handle (or maybe I am not handling these cases well :))
#override
Widget build(BuildContext context) {
int selectedIndex = resourceViewState.selectedIndex;
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey(DEFAULT_PAGE),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(‘DEFAULT’),
),
drawer: ResourceDrawer(
resourceViewState: resourceViewState,
navKey: navigatorKey,
),
body: DefaultPage(),
),
),
if (selectedIndex != DEFAULT_PAGE) ...[
MaterialPage(
key: ValueKey(selectedIndex),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(_getTitle(selectedIndex)),
),
drawer: ResourceDrawer(
deviceState: deviceState,
navKey: navigatorKey,
),
body: _getScreen(selectedIndex),
),
),
],
],
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
);
}
I want to navigate the user from an Alert Dialog to the “Select Resource” page when a button is clicked in the dialog. To do so I must (see 1, 2, 3 in diagram).
3 seems to be handled by the base router if the user pops from the default screen, but I also need to do so from an Alert Dialog.
Pop the Alert Dialog
Pop the Material Design Drawer
Pop from the Nested Router to the Base Router.
This has the effect shown by the red arrow in the diagram.
I can simply use Navigator.of(context).pop() for the first 2. I am pretty sure that the Navigator used here is different from that used by the Nested Router (would love some details here). I believe this is the case, because onPopPage is not called for the NestedRouter on these events.
For 3) I have tried this strategy:
a) Call navKey.currentState!.pop(“disconnected”) from the Navigation Drawer. I pass in the navKey of the Nested Router as shown in the code above.
b) Now the onPopPage listener registered with the nested router receives the result from this pop event.
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
When I see result == "return" then I should navigate from the “Resource Dashboard” Page to the “Select Resource” Page. But I am not sure how to do it nor if it is even a good strategy to use different views on the same route (tangent).
This is a working solution. I pass a reference to the parent navigator key, then call pop from it. I would prefer for it to be entirely declarative but I am not sure how to implement with the nested navigator pattern.
class ResourcePage extends StatefulWidget {
ResourcePage({
required this.resourceViewState,
required this.navigatorKey,
});
final ResourceViewState resourceViewState;
final GlobalKey<NavigatorState> navigatorKey;
#override
_ResourcePageState createState() => _ResourcePageState();
}
class _ResourcePageState extends State<DevicePage> {
late DeviceDelegate _routerDelegate;
late ChildBackButtonDispatcher _backButtonDispatcher;
#override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_routerDelegate = InnerRouterDelegate(parentNavigatorKey: widget.navigatorKey, resourceViewState: widget.resourceViewState);
_backButtonDispatcher = Router.of(context).backButtonDispatcher!.createChildBackButtonDispatcher();
_backButtonDispatcher.takePriority();
}
#override
void didUpdateWidget(covariant DevicePage oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.state = widget.resourceViewState;
}
#override
Widget build(BuildContext context) {
return Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
);
}
}
// InnerRouterDelegate Snippets
// Constructor
InnerRouterDelegate({
required this.resourceViewState,
required this.parentNavigatorKey,
}) {
resourceViewState.addListener(notifyListeners); // See Nested Navigation Example
}
// On Pop Page
onPopPage: (route, result) {
if (result == "return") {
parentNavigatorKey.currentState!.pop();
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},

Access a parent class variable in its child in Flutter?

I am trying to use a custom statefull PageWrapper widget to wrap all my pages. The idea is to make it return a Scaffold and use the same menu drawer and bottom navigation bar, and call the appropriate page as page parameter.
My bottomNavigationBar is working well and I am setting the correct selectedIndex, but I can't find a way to access it in the child page (that is in another file), since I don't know how to access the parent's selectedIndex and display the appropriate widget from my page's list.
import 'package:flutter/material.dart';
class PageWrapper extends StatefulWidget {
final Widget page;
final AppBar appBar;
final BottomNavigationBar bottomNav;
final Color bckColor;
PageWrapper({#required this.page, this.appBar, this.bckColor, this.bottomNav});
#override
_PageWrapperState createState() => _PageWrapperState();
}
class _PageWrapperState extends State<PageWrapper> {
int _selectedIndex;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
void initState() {
super.initState();
_selectedIndex = 0;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar,
backgroundColor: widget.bckColor,
bottomNavigationBar: CustomBottomNavigation(selectedIndex: _selectedIndex, onItemTapped: _onItemTapped),
body: widget.page,
drawer: Drawer(...),
);
}
}
Named roots in my main.dart:
home: PageWrapper(page: HomeScreen()),
routes: {
'form': (context) => PageWrapper(page: RoomService()),
},
I would like to access that bottom navigation bar's current index somehow in my HomeScreen and RoomService screen. Is there a way to do it?
You can solve that by using a State Management tool like Provider or Bloc. To keep things simple, lets use Provider to do it.
Wrap MaterialApp with a ChangeNotifierProvider in your main.dart.
return MultiProvider(
providers: [
ChangeNotifierProvider<IndexModel>(
create: (context) => IndexModel()),
],
child: MaterialApp(...)
);
Create a model that will hold your index value:
Also, you have to override the getter and setter of index in order to call notifyListeners after its value is set. Here is an example:
class IndexModel extends ChangeNotifier {
int _index;
get index => _index;
set index(int index) {
_index = index;
notifyListeners(); //Notifies its listeners that the value has changed
}
}
Here is how you can display your data according to its index (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value it is listening to, changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<IndexModel, String>(
selector: (_, model) => model.index,
builder: (_, i, __) {
switch(i){
//do your returning here based on the index
}
},
);
}
)
}
Extra note. Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<IndexModel>(context,listen:false);
int index =model.index; //get index value
model.index=index; //set your index value
You have to pass listen:false when you aren't listening for changes. This is needed when you are accessing it in initState or in onPressed.

Parent's LayoutBuilder is called too often, every time a change in TextField occurs

I'm seeing prints of "called" each time I focus a textfield, input or delete a new character. My real widget tree contains a list with its tiles having animations, that don't work properly due to the whole listview being recreated after scroll position is changed. So how can I still use what LayoutBuilder has to offer, that is constraints data, without having its whole child tree being rebuilt after something changes?
class _AppState extends State<App> with AfterLayoutMixin<App>
{
AppCoordinator appCoordinator;
#override
Widget build(BuildContext context)
{
return Scaffold(
key: Key("sadf"),
body: new LayoutBuilder(
key: Key("sadd"),
builder: (context, constraint)
{
print("called");
return TextField(
key: Key("sad")
);
}),
);
}
}
You can assume that the build methods will be called excessively. If that's a problem, use a stateful widget and cache the widget you return from build.
class _FooState extends State<Foo> {
Widget _child;
Widget build(BuildContext context) {
_child ??= Container();
return _child;
}
}
This can be used together with Layout Builder.