使用flutter和NavBar在子选项卡中保留历史导航,并执行路由器

qgzx9mmu  于 2023-06-24  发布在  Flutter
关注(0)|答案(1)|浏览(225)

我想知道我如何能保持历史导航在孩子的标签从导航栏。
目前,我遵循这篇文章来创建一个NavBar导航。
当我在选项卡中导航时,我想保留历史记录(Home => FeedPeople => FeedPeople => FeedPeople...):

  • 主页(一般饲料)(从链接访问FeedPeople)
  • FeedPeople => FeedPeople => FeedPeople...
  • 发现
  • 产品展示
  • 我的

问题:当我点击主页上的链接来推送FeedPeople并看到配置文件/:uuid,然后再次是FeedPeople,再次是FeedPeople,等等...当我点击导航栏上的标签,比如购物,然后再次点击主页,我看不到最后一个FeedPeople(最后一个配置文件),而是直接看到主页屏幕。

main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_app/feed.dart';
import 'package:my_app/feed_people.dart';
import 'package:my_app/route_names.dart';

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routerConfig: router,
    );
  }

  final router = GoRouter(
    initialLocation: '/',
    navigatorKey: _rootNavigatorKey,
    routes: [
      ShellRoute(
        navigatorKey: _shellNavigatorKey,
        pageBuilder: (context, state, child) {
          print(state.location);
          return NoTransitionPage(
              child: ScaffoldWithNavBar(
            location: state.location,
            child: child,
          ));
        },
        routes: [
          GoRoute(
              path: '/',
              parentNavigatorKey: _shellNavigatorKey,
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Feed(uuid: 'a78987-hiIh!ç767897'),
                );
              },
              routes: [
                GoRoute(
                    name: RoutesNames.feedPeople,
                    path: 'profil/:uuid',
                    builder: (context, state) =>
                        FeedPeople(uuid: state.pathParameters['uuid']!))
              ]),
          GoRoute(
            path: '/discover',
            parentNavigatorKey: _shellNavigatorKey,
            pageBuilder: (context, state) {
              return const NoTransitionPage(
                child: Scaffold(
                  body: Center(child: Text("Discover")),
                ),
              );
            },
          ),
          GoRoute(
              parentNavigatorKey: _shellNavigatorKey,
              path: '/shop',
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Scaffold(
                    body: Center(child: Text("Shop")),
                  ),
                );
              }),
        ],
      ),
      GoRoute(
        parentNavigatorKey: _rootNavigatorKey,
        path: '/login',
        pageBuilder: (context, state) {
          return NoTransitionPage(
            key: UniqueKey(),
            child: Scaffold(
              appBar: AppBar(),
              body: const Center(
                child: Text("Login"),
              ),
            ),
          );
        },
      ),
    ],
  );
}

// ignore: must_be_immutable
class ScaffoldWithNavBar extends StatefulWidget {
  String location;
  ScaffoldWithNavBar({super.key, required this.child, required this.location});

  final Widget child;

  @override
  State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}

class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
  int _currentIndex = 0;

  static const List<MyCustomBottomNavBarItem> tabs = [
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home),
      label: 'HOME',
      initialLocation: '/',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.explore_outlined),
      activeIcon: Icon(Icons.explore),
      label: 'DISCOVER',
      initialLocation: '/discover',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.storefront_outlined),
      activeIcon: Icon(Icons.storefront),
      label: 'SHOP',
      initialLocation: '/shop',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.account_circle_outlined),
      activeIcon: Icon(Icons.account_circle),
      label: 'MY',
      initialLocation: '/login',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(fontFamily: 'Roboto');
    return Scaffold(
      body: SafeArea(child: widget.child),
      bottomNavigationBar: BottomNavigationBar(
        selectedLabelStyle: labelStyle,
        unselectedLabelStyle: labelStyle,
        selectedItemColor: const Color(0xFF434343),
        selectedFontSize: 12,
        unselectedItemColor: const Color(0xFF838383),
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        onTap: (int index) {
          _goOtherTab(context, index);
        },
        currentIndex: widget.location == '/'
            ? 0
            : widget.location == '/discover'
                ? 1
                : widget.location == '/shop'
                    ? 2
                    : 3,
        items: tabs,
      ),
    );
  }

  void _goOtherTab(BuildContext context, int index) {
    if (index == _currentIndex) return;
    GoRouter router = GoRouter.of(context);
    String location = tabs[index].initialLocation;

    setState(() {
      _currentIndex = index;
    });
    if (index == 3) {
      context.push('/login');
    } else {
      router.go(location);
    }
  }
}

class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  final String initialLocation;

  const MyCustomBottomNavBarItem(
      {required this.initialLocation,
      required Widget icon,
      String? label,
      Widget? activeIcon})
      : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}

feed.dart(从导航栏点击主页)

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_app/route_names.dart';

class Feed extends StatelessWidget {
  final String uuid;
  const Feed({super.key, required this.uuid});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Welcome $uuid"),
        ),
        body: Column(
          children: <Widget>[
            ElevatedButton(
                child: const Text("Retour"),
                onPressed: () {
                  context.pop(context);
                }),
            ElevatedButton(
                child: const Text("go to feedPeople"),
                onPressed: () {
                  String uuid = "za987YIU-89798Yu-95fGBU";

                  context.pushNamed(RoutesNames.feedPeople,
                      pathParameters: {"uuid": uuid});
                })
          ],
        ));
  }
}

feedPeople

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_app/route_names.dart';

class FeedPeople extends StatelessWidget {
  final String uuid;
  const FeedPeople({super.key, required this.uuid});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Welcome $uuid"),
        ),
        body: Column(
          children: <Widget>[
            ElevatedButton(
                child: const Text("Retour"),
                onPressed: () {
                  context.pop(context);
                }),
            ElevatedButton(
                child: const Text("Autre profil"),
                onPressed: () {
                  String uuid = "a987YIU-89798Yu-95fGBU";

                  context.pushNamed(RoutesNames.feedPeople,
                      pathParameters: {"uuid": uuid});
                })
          ],
        ));
  }
}

我试着直接在回家路线中设置路线,但同样的问题。历史没有保留,当我从导航栏导航时,我直接看到“第一”屏幕(主页而不是最后一个FeedPeople)。

shyt4zoc

shyt4zoc1#

使用go_router,使用StatefulShellRoute获取选项卡的持久状态。下面是dartpad go_router示例的修改版本,但使用StatefulShellRoute和NavigationBar(而不是BottomNavigationBar)来获得所需的输出。
你可以在dartpart here中使用下面的代码

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:english_words/english_words.dart';
import 'dart:math' as math;

void main() {
  runApp(MusicAppDemo());
}

class MusicAppDemo extends StatelessWidget {
  MusicAppDemo({Key? key}) : super(key: key);

  final MusicDatabase database = MusicDatabase.mock();

  final GoRouter _router = GoRouter(
    initialLocation: '/login',
    routes: <RouteBase>[
      GoRoute(
        path: '/login',
        builder: (BuildContext context, GoRouterState state) {
          return const LoginScreen();
        },
      ),
      StatefulShellRoute.indexedStack(
        builder: (BuildContext context, GoRouterState state,
            StatefulNavigationShell navigationShell) {
          return MusicAppShell(
            navigationShell: navigationShell,
          );
        },
        branches: <StatefulShellBranch>[
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/library',
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const LibraryScreen(),
                    key: state.pageKey,
                  );
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'album/:albumId',
                    builder: (BuildContext context, GoRouterState state) {
                      return AlbumScreen(
                        albumId: state.pathParameters['albumId'],
                      );
                    },
                    routes: [
                      GoRoute(
                        path: 'song/:songId',
                        // Display on the root Navigator
                        builder: (BuildContext context, GoRouterState state) {
                          return SongScreen(
                            songId: state.pathParameters['songId']!,
                          );
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/recents',
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const RecentlyPlayedScreen(),
                    key: state.pageKey,
                  );
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'song/:songId',
                    // Display on the root Navigator
                    builder: (BuildContext context, GoRouterState state) {
                      return SongScreen(
                        songId: state.pathParameters['songId']!,
                      );
                    },
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/search',
                pageBuilder: (context, state) {
                  final query = state.queryParameters['q'] ?? '';
                  return FadeTransitionPage(
                    child: SearchScreen(
                      query: query,
                    ),
                    key: state.pageKey,
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Music app',
      theme: ThemeData(primarySwatch: Colors.pink),
      routerConfig: _router,
      builder: (context, child) {
        return MusicDatabaseScope(
          state: database,
          child: child!,
        );
      },
    );
  }
}

class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Center(child: Text('Welcome!'))),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const FlutterLogo(
              size: 150,
            ),
            const SizedBox(
              height: 50,
            ),
            ElevatedButton(
              child: const Text('Login'),
              onPressed: () {
                context.go('/library');
              },
            ),
          ],
        ),
      ),
    );
  }
}

class MusicAppShell extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const MusicAppShell({
    Key? key,
    required this.navigationShell,
  }) : super(key: key ?? const ValueKey<String>('MusicAppShell'));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.my_library_music_rounded),
            label: 'Library',
          ),
          NavigationDestination(
            icon: Icon(Icons.timelapse),
            label: 'Recently Played',
          ),
          NavigationDestination(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
        ],
        selectedIndex: navigationShell.currentIndex,
        onDestinationSelected: (int index) {
          navigationShell.goBranch(index);
        },
      ),
    );
  }
}

class LibraryScreen extends StatelessWidget {
  const LibraryScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    return Scaffold(
      appBar: const CustomAppBar(title: 'Library'),
      body: ListView.builder(
        itemBuilder: (context, albumId) {
          final album = database.albums[albumId];
          return AlbumTile(
            album: album,
            onTap: () {
              GoRouter.of(context).go('/library/album/$albumId');
            },
          );
        },
        itemCount: database.albums.length,
      ),
    );
  }
}

class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final double height;

  const CustomAppBar({
    super.key,
    required this.title,
    this.height = kToolbarHeight,
  });

  @override
  Size get preferredSize => Size.fromHeight(height);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(title),
      actions: <Widget>[
        PopupMenuButton(
            icon: const Icon(Icons.settings),
            itemBuilder: (context) {
              return [
                const PopupMenuItem<int>(
                  value: 0,
                  child: Text("Settings"),
                ),
                const PopupMenuItem<int>(
                  value: 1,
                  child: Text("Logout"),
                ),
              ];
            },
            onSelected: (value) {
              if (value == 1) {
                context.go('/login');
              }
            }),
      ],
    );
  }
}

class RecentlyPlayedScreen extends StatelessWidget {
  const RecentlyPlayedScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.recentlyPlayed;
    return Scaffold(
      appBar: const CustomAppBar(title: 'Recently Played'),
      body: ListView.builder(
        itemBuilder: (context, index) {
          final song = songs[index];
          final albumIdInt = int.tryParse(song.albumId)!;
          final album = database.albums[albumIdInt];
          return SongTile(
            album: album,
            song: song,
            onTap: () {
              GoRouter.of(context).go('/recents/song/${song.fullId}');
            },
          );
        },
        itemCount: songs.length,
      ),
    );
  }
}

class SearchScreen extends StatefulWidget {
  final String query;

  const SearchScreen({Key? key, required this.query}) : super(key: key);

  @override
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  String? _currentQuery;

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.search(widget.query);
    return Scaffold(
      appBar: const CustomAppBar(title: 'Search'),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: TextField(
              decoration: const InputDecoration(
                hintText: 'Search...',
                border: OutlineInputBorder(),
              ),
              onChanged: (String? newSearch) {
                _currentQuery = newSearch;
              },
              onEditingComplete: () {
                GoRouter.of(context).go(
                  '/search?q=$_currentQuery',
                );
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemBuilder: (context, index) {
                final song = songs[index];
                return SongTile(
                  album: database.albums[int.tryParse(song.albumId)!],
                  song: song,
                  onTap: () {
                    GoRouter.of(context).go(
                        '/library/album/${song.albumId}/song/${song.fullId}');
                  },
                );
              },
              itemCount: songs.length,
            ),
          ),
        ],
      ),
    );
  }
}

class AlbumScreen extends StatelessWidget {
  final String? albumId;

  const AlbumScreen({
    required this.albumId,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final albumIdInt = int.tryParse(albumId ?? '');
    final album = database.albums[albumIdInt!];
    return Scaffold(
      appBar: CustomAppBar(title: 'Album - ${album.title}'),
      body: Center(
        child: Column(
          children: [
            Row(
              children: [
                SizedBox(
                  width: 200,
                  height: 200,
                  child: Container(
                    color: album.color,
                    margin: const EdgeInsets.all(8),
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.headlineMedium,
                    ),
                    Text(
                      album.artist,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              ],
            ),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) {
                  final song = album.songs[index];
                  return ListTile(
                    title: Text(song.title),
                    leading: SizedBox(
                      width: 50,
                      height: 50,
                      child: Container(
                        color: album.color,
                        margin: const EdgeInsets.all(8),
                      ),
                    ),
                    trailing: SongDuration(
                      duration: song.duration,
                    ),
                    onTap: () {
                      GoRouter.of(context)
                          .go('/library/album/$albumId/song/${song.fullId}');
                    },
                  );
                },
                itemCount: album.songs.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SongScreen extends StatelessWidget {
  final String songId;

  const SongScreen({
    Key? key,
    required this.songId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final song = database.getSongById(songId);
    final albumIdInt = int.tryParse(song.albumId);
    final album = database.albums[albumIdInt!];

    return Scaffold(
      appBar: CustomAppBar(title: 'Song - ${song.title}'),
      body: Column(
        children: [
          Row(
            children: [
              SizedBox(
                width: 300,
                height: 300,
                child: Container(
                  color: album.color,
                  margin: const EdgeInsets.all(8),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      song.title,
                      style: Theme.of(context).textTheme.displayMedium,
                    ),
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              )
            ],
          )
        ],
      ),
    );
  }
}

class MusicDatabase {
  final List<Album> albums;
  final List<Song> recentlyPlayed;
  final Map<String, Song> _allSongs = {};

  MusicDatabase(this.albums, this.recentlyPlayed) {
    _populateAllSongs();
  }

  factory MusicDatabase.mock() {
    final albums = _mockAlbums().toList();
    final recentlyPlayed = _mockRecentlyPlayed(albums).toList();
    return MusicDatabase(albums, recentlyPlayed);
  }

  Song getSongById(String songId) {
    if (_allSongs.containsKey(songId)) {
      return _allSongs[songId]!;
    }
    throw ('No song with ID $songId found.');
  }

  List<Song> search(String searchString) {
    final songs = <Song>[];
    for (var song in _allSongs.values) {
      final album = albums[int.tryParse(song.albumId)!];
      if (song.title.contains(searchString) ||
          album.title.contains(searchString)) {
        songs.add(song);
      }
    }
    return songs;
  }

  void _populateAllSongs() {
    for (var album in albums) {
      for (var song in album.songs) {
        _allSongs[song.fullId] = song;
      }
    }
  }

  static MusicDatabase of(BuildContext context) {
    final routeStateScope =
        context.dependOnInheritedWidgetOfExactType<MusicDatabaseScope>();
    if (routeStateScope == null) throw ('No RouteState in scope!');
    return routeStateScope.state;
  }

  static Iterable<Album> _mockAlbums() sync* {
    for (var i = 0; i < Colors.primaries.length; i++) {
      final color = Colors.primaries[i];
      final title = WordPair.random().toString();
      final artist = WordPair.random().toString();
      final songs = <Song>[];
      for (var j = 0; j < 12; j++) {
        final minutes = math.Random().nextInt(3) + 3;
        final seconds = math.Random().nextInt(60);
        final title = WordPair.random();
        final duration = Duration(minutes: minutes, seconds: seconds);
        final song = Song('$j', '$i', '$title', duration);

        songs.add(song);
      }
      yield Album('$i', title, artist, color, songs);
    }
  }

  static Iterable<Song> _mockRecentlyPlayed(List<Album> albums) sync* {
    for (var album in albums) {
      final songIndex = math.Random().nextInt(album.songs.length);
      yield album.songs[songIndex];
    }
  }
}

class MusicDatabaseScope extends InheritedWidget {
  final MusicDatabase state;

  const MusicDatabaseScope({
    required this.state,
    required Widget child,
    Key? key,
  }) : super(child: child, key: key);

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return oldWidget is MusicDatabaseScope && state != oldWidget.state;
  }
}

class Album {
  final String id;
  final String title;
  final String artist;
  final Color color;
  final List<Song> songs;

  Album(this.id, this.title, this.artist, this.color, this.songs);
}

class Song {
  final String id;
  final String albumId;
  final String title;
  final Duration duration;

  Song(this.id, this.albumId, this.title, this.duration);

  String get fullId => '$albumId-$id';
}

class AlbumTile extends StatelessWidget {
  final Album album;
  final VoidCallback? onTap;

  const AlbumTile({Key? key, required this.album, this.onTap})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
        ),
      ),
      title: Text(album.title),
      subtitle: Text(album.artist),
      onTap: onTap,
    );
  }
}

class SongTile extends StatelessWidget {
  final Album album;
  final Song song;
  final VoidCallback? onTap;

  const SongTile(
      {Key? key, required this.album, required this.song, this.onTap})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
          margin: const EdgeInsets.all(8),
        ),
      ),
      title: Text(song.title),
      trailing: SongDuration(
        duration: song.duration,
      ),
      onTap: onTap,
    );
  }
}

class SongDuration extends StatelessWidget {
  final Duration duration;

  const SongDuration({
    required this.duration,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}');
  }
}

/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage<void> {
  /// Creates a [FadeTransitionPage].
  FadeTransitionPage({
    required LocalKey key,
    required Widget child,
  }) : super(
            key: key,
            transitionsBuilder: (BuildContext context,
                    Animation<double> animation,
                    Animation<double> secondaryAnimation,
                    Widget child) =>
                FadeTransition(
                  opacity: animation.drive(_curveTween),
                  child: child,
                ),
            child: child);

  static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
}

相关问题