Instantly share code, notes, and snippets.
Last active
November 22, 2022 03:17
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)), | |
], | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
looks great! I wish it will be in the package already!