Skip to content

Instantly share code, notes, and snippets.

@tolo
Last active November 22, 2022 03:17
Show Gist options
  • Select an option

  • Save tolo/cfff35673064571285470a7552740eed to your computer and use it in GitHub Desktop.

Select an option

Save tolo/cfff35673064571285470a7552740eed to your computer and use it in GitHub Desktop.

Revisions

  1. tolo revised this gist Nov 19, 2022. No changes.
  2. tolo created this gist Nov 15, 2022.
    299 changes: 299 additions & 0 deletions stateful_dynamic_navigation.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,299 @@
    // 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)),
    ],
    ),
    );
    }
    }