routemaster
routemaster copied to clipboard
Store page state
Add an option to never have pages created, no matter how the user navigates around.
The widgets stay in memory and get restored to their previous state so things like scroll position and entered text remain the same.
I used the approach below, for pages that should be stored and never recreated. It is just copy of the same private class that the CupertinoTabScaffold in the SDK uses, with things renamed a bit.
/// A widget laying out multiple pages with only one active page being built
/// at a time and on stage. Off stage pages' animations are stopped.
class PageSwitchingView extends StatefulWidget {
const PageSwitchingView({
required this.currentPageIndex,
required this.pageCount,
required this.pageBuilder,
}) : assert(currentPageIndex != null),
assert(pageCount != null && pageCount > 0),
assert(pageBuilder != null);
final int currentPageIndex;
final int pageCount;
final IndexedWidgetBuilder pageBuilder;
@override
_PageSwitchingViewState createState() => _PageSwitchingViewState();
}
class _PageSwitchingViewState extends State<PageSwitchingView> {
final List<bool> shouldBuildPage = <bool>[];
final List<FocusScopeNode> pageFocusNodes = <FocusScopeNode>[];
// When focus nodes are no longer needed, we need to dispose of them, but we
// can't be sure that nothing else is listening to them until this widget is
// disposed of, so when they are no longer needed, we move them to this list,
// and dispose of them when we dispose of this widget.
final List<FocusScopeNode> discardedNodes = <FocusScopeNode>[];
@override
void initState() {
super.initState();
shouldBuildPage.addAll(List<bool>.filled(widget.pageCount, false));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_focusActivePage();
}
@override
void didUpdateWidget(PageSwitchingView oldWidget) {
super.didUpdateWidget(oldWidget);
// Only partially invalidate the page cache to avoid breaking the current
// behavior. We assume that the only possible change is either:
// - new pages are appended to the tab list, or
// - some trailing pages are removed.
// If the above assumption is not true, some pages may lose their state.
final int lengthDiff = widget.pageCount - shouldBuildPage.length;
if (lengthDiff > 0) {
shouldBuildPage.addAll(List<bool>.filled(lengthDiff, false));
} else if (lengthDiff < 0) {
shouldBuildPage.removeRange(widget.pageCount, shouldBuildPage.length);
}
_focusActivePage();
}
// Will focus the active tab if the FocusScope above it has focus already. If
// not, then it will just mark it as the preferred focus for that scope.
void _focusActivePage() {
if (pageFocusNodes.length != widget.pageCount) {
if (pageFocusNodes.length > widget.pageCount) {
discardedNodes.addAll(pageFocusNodes.sublist(widget.pageCount));
pageFocusNodes.removeRange(widget.pageCount, pageFocusNodes.length);
} else {
pageFocusNodes.addAll(
List<FocusScopeNode>.generate(
widget.pageCount - pageFocusNodes.length,
(int index) => FocusScopeNode(
debugLabel: 'Page ${index + pageFocusNodes.length}'),
),
);
}
}
FocusScope.of(context)
.setFirstFocus(pageFocusNodes[widget.currentPageIndex]);
}
@override
void dispose() {
for (final FocusScopeNode focusScopeNode in pageFocusNodes) {
focusScopeNode.dispose();
}
for (final FocusScopeNode focusScopeNode in discardedNodes) {
focusScopeNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: List<Widget>.generate(widget.pageCount, (int index) {
final bool active = index == widget.currentPageIndex;
shouldBuildPage[index] = active || shouldBuildPage[index];
return HeroMode(
enabled: active,
child: Offstage(
offstage: !active,
child: TickerMode(
enabled: active,
child: FocusScope(
node: pageFocusNodes[index],
child: Builder(builder: (BuildContext context) {
return shouldBuildPage[index]
? widget.pageBuilder(context, index)
: Container();
}),
),
),
),
);
}),
);
}
}
Would this include keeping indexed pages alive in the background? I'm trying to keep certain tabs alive so they don't reload when navigating between them.
@RedTech64 It does indeed do so.
However, if you are using the Routemaster setup for CupertinoTabBar as a top level navigator, as shown eg in its bundled mobile_app demo, those pages then already keep state, then again and if it if for a TabBar view and navigator, you can use this lighter approach mentioned by @tomgilder here https://github.com/tomgilder/routemaster/issues/87
But if it is for some other type of indexed navigator, some custom one, or why not eg Flutter SDK NavigationRail
, then you might indeed need to consider the above approach, at least until Tom builds in support for it in Routemaster, if he plans to do so.
I don't have public repo showing how to use the above thing yet, but I might add it to this modified version of the mobile_app demo and use the NavigationRail
for a mock adaptive setup where it switches from bottom navbar to rail/menu. I have a bunch of question and potential issues I want to demonstrate to Tom concerning its (and similar index navigators) usage as well.
@rydmike Thanks
I'm using a NavigationRail so a modified version of mobile_app with an example would be great.
Why have the pages created in memory? That sounds like a performance loss and can be potentially heavy. Also, it makes the library scope creep.
Data can be cached (up to the developer to do this), for quick view restoration, but view caching is a dangerous (gets in the way of lifecycle dependent calls, init etc.) and as stated, heavy.
@sp00ne I agree, I don't want to add lots of scope creep and complexity here, and there are performance issues to consider.
Flutter already has state restoration built-in which might be the best way to tackle this issue.
I'm not sure how well it would work with Routemaster though, I haven't tried.
Flutter already has state restoration built-in which might be the best way to tackle this issue.
it doesn't work with routemaster, i've tried