Skip to content

Instantly share code, notes, and snippets.

@tolo
Last active November 22, 2022 03:17
Show Gist options
  • Save tolo/cfff35673064571285470a7552740eed to your computer and use it in GitHub Desktop.
Save tolo/cfff35673064571285470a7552740eed to your computer and use it in GitHub Desktop.
WIP. Example showing how to use go_router to build stateful dynamic navigation (i.e. varying number of nested route branches) with a BottomNavigationBar. Requires https://github.com/flutter/packages/pull/2650
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// A quick sample showcasing dynamic number of sections in a stateful navigation
// with a bottom navigation bar. Based on stateful_nested_navigation.dart from
// https://github.com/flutter/packages/pull/2650
void main() {
runApp(TopStateWidget());
}
class TopStateWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() => TopState();
}
class TopState extends State<TopStateWidget> {
bool _loggedIn = false;
int _sections = 1;
void login(int sections) => setState(() {
_sections = sections;
_loggedIn = true;
});
void logout() => setState(() {
_loggedIn = false;
});
@override
Widget build(BuildContext context) {
return InheritedTopState(
topState: this,
child: NestedTabNavigationExampleApp(
loggedIn: _loggedIn, sections: _sections),
);
}
}
class InheritedTopState extends InheritedWidget {
const InheritedTopState({
required super.child,
required this.topState,
super.key,
}) : super();
final TopState topState;
@override
bool updateShouldNotify(covariant InheritedTopState oldWidget) {
return topState != oldWidget.topState;
}
}
/// An example demonstrating how to use nested navigators
class NestedTabNavigationExampleApp extends StatelessWidget {
/// Creates a NestedTabNavigationExampleApp
NestedTabNavigationExampleApp(
{required this.loggedIn, required this.sections, Key? key})
: super(key: key);
final bool loggedIn;
final int sections;
GoRouter get _router => GoRouter(
initialLocation: loggedIn ? '/a0' : '/',
routes: <RouteBase>[
GoRoute(path: '/', builder: (context, state) => const LoginScreen()),
if (loggedIn)
StatefulShellRoute(
/// To enable preloading of the root routes of the branches, pass true
/// for the parameter preloadBranches.
// preloadBranches: true,
branches: List<ShellRouteBranch>.generate(
sections,
(index) =>
/// The route branch for the first tab of the bottom navigation bar.
ShellRouteBranch(
routes: <RouteBase>[
GoRoute(
/// The screen to display as the root in the first tab of the
/// bottom navigation bar.
path: '/a$index',
builder: (BuildContext context, GoRouterState state) =>
RootScreen(
label: 'A$index',
detailsPath: '/a$index/details'),
routes: <RouteBase>[
/// The details screen to display stacked on navigator of the
/// first tab. This will cover screen A but not the application
/// shell (bottom navigation bar).
GoRoute(
path: 'details',
builder:
(BuildContext context, GoRouterState state) =>
DetailsScreen(label: 'A$index'),
),
],
),
],
),
),
builder:
(BuildContext context, GoRouterState state, Widget child) {
return ScaffoldWithNavBar(body: child, sections: sections);
},
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Login screen', style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
final InheritedTopState? top =
context.dependOnInheritedWidgetOfExactType();
top!.topState.login(2 + Random().nextInt(9));
},
child: const Text('Login'),
),
],
),
);
}
}
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.body,
required this.sections,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
/// Body, i.e. the index stack
final Widget body;
final int sections;
@override
Widget build(BuildContext context) {
final StatefulShellRouteState shellState = StatefulShellRoute.of(context);
return Scaffold(
body: body,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: List<BottomNavigationBarItem>.generate(
sections,
(index) => BottomNavigationBarItem(
icon: index % 2 == 0
? const Icon(Icons.home)
: const Icon(Icons.work),
label: 'Section $index')),
currentIndex: shellState.index,
onTap: (int tappedIndex) => shellState.goBranch(index: tappedIndex),
),
);
}
}
/// Widget for the root/initial pages in the bottom navigation bar.
class RootScreen extends StatelessWidget {
/// Creates a RootScreen
const RootScreen({
required this.label,
required this.detailsPath,
Key? key,
}) : super(key: key);
/// The label
final String label;
/// The path to the detail page
final String detailsPath;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tab root - $label'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Screen $label',
style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
GoRouter.of(context).go(detailsPath, extra: '$label-XYZ');
},
child: const Text('View details'),
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
final InheritedTopState? top =
context.dependOnInheritedWidgetOfExactType();
top!.topState.logout();
},
child: const Text('Logout'),
),
],
),
),
);
}
}
/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
/// Constructs a [DetailsScreen].
const DetailsScreen({
required this.label,
this.param,
Key? key,
}) : super(key: key);
/// The label to display in the center of the screen.
final String label;
/// Optional param
final String? param;
@override
State<StatefulWidget> createState() => DetailsScreenState();
}
/// The state for DetailsScreen
class DetailsScreenState extends State<DetailsScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details Screen - ${widget.label}'),
),
body: _build(context),
);
}
Widget _build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Details for ${widget.label} - Counter: $_counter',
style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment counter'),
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.param != null)
Text('Parameter: ${widget.param!}',
style: Theme.of(context).textTheme.titleMedium),
const Padding(padding: EdgeInsets.all(8)),
],
),
);
}
}
@hmbenhaim
Copy link

looks great! I wish it will be in the package already!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment