Skip to content

Instantly share code, notes, and snippets.

@tolo
Last active April 6, 2023 17:36
Show Gist options
  • Save tolo/5e235a9a986b45f2e5447a8dab9d5d10 to your computer and use it in GitHub Desktop.
Save tolo/5e235a9a986b45f2e5447a8dab9d5d10 to your computer and use it in GitHub Desktop.
Example showing how to use go_router to build stateful nested navigation, in combination with Hero animations. Requires https://github.com/flutter/packages/pull/2650 (or https://github.com/tolo/flutter_packages/tree/nested-persistent-navigation).
// 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 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _tabANavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
// This example demonstrates how to setup nested navigation using a
// BottomNavigationBar, where each tab uses its own persistent navigator, i.e.
// navigation state is maintained separately for each tab. This setup also
// enables deep linking into nested pages.
//
// This example demonstrates how to display routes within a StackedShellRoute,
// that are places on separate navigators. The example also demonstrates how
// state is maintained when switching between different tabs (and thus branches
// and Navigators).
void main() {
runApp(NestedTabNavigationExampleApp());
}
/// An example demonstrating how to use nested navigators
class NestedTabNavigationExampleApp extends StatelessWidget {
/// Creates a NestedTabNavigationExampleApp
NestedTabNavigationExampleApp({super.key});
final GoRouter _router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/a',
routes: <RouteBase>[
GoRoute(
path: '/hero',
parentNavigatorKey: _rootNavigatorKey,
pageBuilder: (context, state) => CustomTransitionPage<void>(
key: state.pageKey,
child: HeroScreen(),
transitionDuration: Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
),
),
StackedShellRoute(
branches: <StackedShellBranch>[
/// The route branch for the first tab of the bottom navigation bar.
StackedShellBranch(
navigatorKey: _tabANavigatorKey,
routes: <RouteBase>[
GoRoute(
/// The screen to display as the root in the first tab of the
/// bottom navigation bar.
path: '/a',
builder: (BuildContext context, GoRouterState state) =>
const RootScreen(label: 'A', detailsPath: '/a/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', extra: state.extra),
),
],
),
],
),
/// The route branch for the second tab of the bottom navigation bar.
StackedShellBranch(
/// It's not necessary to provide a navigatorKey if it isn't also
/// needed elsewhere. If not provided, a default key will be used.
// navigatorKey: _tabBNavigatorKey,
routes: <RouteBase>[
GoRoute(
/// The screen to display as the root in the second tab of the
/// bottom navigation bar.
path: '/b',
builder: (BuildContext context, GoRouterState state) =>
const RootScreen(
label: 'B',
detailsPath: '/b/details/1',
secondDetailsPath: '/b/details/2',
),
routes: <RouteBase>[
GoRoute(
path: 'details/:param',
builder: (BuildContext context, GoRouterState state) =>
DetailsScreen(
label: 'B',
param: state.params['param'],
extra: state.extra,
),
),
],
),
],
),
],
builder: (BuildContext context, StackedShellRouteState state,
Widget child) {
/// This builder implementation uses the default container for the
/// branch Navigators (provided in through the `child` argument). This
/// is the simplest way to use StackedShellRoute, where the shell is
/// built around the Navigator container (see ScaffoldWithNavBar).
return ScaffoldWithNavBar(shellState: state, body: child);
},
/// If it's necessary to customize the Page for StackedShellRoute,
/// provide a pageBuilder function instead of the builder, for example:
// pageBuilder: (BuildContext context, StackedShellRouteState state,
// Widget child) {
// return NoTransitionPage<dynamic>(
// child: ScaffoldWithNavBar(shellState: state, body: child));
// },
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}
/// 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.shellState,
required this.body,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
/// The current state of the parent StackedShellRoute.
final StackedShellRouteState shellState;
/// Body, i.e. the container for the branch Navigators.
final Widget body;
@override
Widget build(BuildContext context) {
return Scaffold(
body: body,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
],
currentIndex: shellState.currentIndex,
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,
this.secondDetailsPath,
Key? key})
: super(key: key);
/// The label
final String label;
/// The path to the detail page
final String detailsPath;
/// The path to another detail page
final String? secondDetailsPath;
@override
Widget build(BuildContext context) {
debugPrint('Building RootScreen - ${label}');
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)),
if (secondDetailsPath != null)
TextButton(
onPressed: () {
GoRouter.of(context).go(secondDetailsPath!);
},
child: const Text('View more details'),
),
PhotoHero(
photo: 'https://picsum.photos/id/1/500/500',
width: 100.0,
onTap: () {
GoRouter.of(context).push('/hero');
}),
],
),
),
);
}
}
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.width,
});
final String photo;
final VoidCallback? onTap;
final double width;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.network(
photo,
fit: BoxFit.contain,
cacheHeight: 500,
cacheWidth: 500,
),
),
),
),
);
}
}
class HeroScreen extends StatelessWidget {
const HeroScreen({super.key});
@override
Widget build(BuildContext context) {
void pop() {
Navigator.of(context).pop();
}
return Scaffold(
floatingActionButton: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: pop,
child: const Icon(Icons.close),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.startTop,
body: GestureDetector(
onTap: pop,
child: Container(
color: Colors.black87,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: PhotoHero(
photo: 'https://picsum.photos/id/1/500/500',
width: 500.0,
onTap: pop,
),
),
),
);
}
}
/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
/// Constructs a [DetailsScreen].
const DetailsScreen({
required this.label,
this.param,
this.extra,
Key? key,
}) : super(key: key);
/// The label to display in the center of the screen.
final String label;
/// Optional param
final String? param;
/// Optional extra object
final Object? extra;
@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)),
if (widget.extra != null)
Text('Extra: ${widget.extra!}',
style: Theme.of(context).textTheme.titleMedium),
],
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment