flutter 是否可以使用GoRouter包实现导航栏?

mspsb9vt  于 2023-01-02  发布在  Flutter
关注(0)|答案(2)|浏览(283)

我正在尝试使用新的Material You API实现一个导航栏。
https://api.flutter.dev/flutter/material/NavigationBar-class.html
我只是好奇地想知道我们是否可以使用Go_Router包实现相同的功能。

a8jjtwal

a8jjtwal1#

更新答案(v6.0.0)

我最初的答案是使用GoRouter v3创建的,当时不可能将导航栏保留在子屏幕中。
目前,在版本6中,GoRouter允许使用ShellRoute,您可以使用builder属性构建带有导航栏的Scaffold。
您可以看到the oficial live example here
我正在使用GoRouter v6.0.0重写下面过时的答案,我将保留原来的答案,以防有人发现它有用。

更新代码

1.我们需要创建一些基本模型来存储数据:

/// Just a generic model that will be used to present some data on the screen.
class Person {
  final String id;
  final String name;

  Person({required this.id, required this.name});
}

/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
  final String id;
  final String name;
  final List<Person> people;
  final Icon icon;

  Family({
    required this.id,
    required this.name,
    required this.people,
    required this.icon,
  });
}

/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
  static const List<Icon> icons = [
    Icon(Icons.looks_one),
    Icon(Icons.looks_two),
    Icon(Icons.looks_3)
  ];

  static final List<Family> data = List.generate(
    3,
    (fid) => Family(
      id: '$fid',
      name: 'Family $fid',
      people: List.generate(
        10,
        (id) => Person(id: '$id', name: 'Family $fid Person $id'),
      ),
      icon: icons[fid],
    ),
  );
}

1.现在我们将创建呈现模型数据的基本视图:

/// Used to present Person's data.
class PersonView extends StatelessWidget {
  const PersonView({required this.person, Key? key}) : super(key: key);
  final Person person;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text(person.name),
      ),
    );
  }
}

/// This is the view that will be used by each application's tab.
class FamilyView extends StatelessWidget {
  const FamilyView({super.key, required this.family});
  final Family family;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(family.name),
      ),
      body: ListView(
        children: [
          for (final p in family.people)
            ListTile(
              title: Text(p.name),
              onTap: () => context.go('/family/${family.id}/person/${p.id}'),
            ),
        ],
      ),
    );
  }
}

1.现在,让我们最后创建一个小部件,它将显示NavigationBar

/// Widget responsible to render the actual page and the navigation bar.
class ShellScreen extends StatelessWidget {
  final Widget child;
  final int index;

  const ShellScreen({super.key, required this.child, required this.index});

  @override
  Widget build(BuildContext context) {
    if (index < 0 || index >= Families.data.length) {
      // Just in case someone tries to pass an invalid index in the url.
      GoRouter.of(context).go('/');
      return const SizedBox.shrink();
    }
    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        destinations: [
          for (final f in Families.data)
            NavigationDestination(
              icon: f.icon,
              label: f.name,
            )
        ],
        onDestinationSelected: (index) => context.go(
          '/family/${Families.data[index].id}',
        ),
        selectedIndex: index,
      ),
    );
  }
}

1.最后,只有在使用StackRouter定义应用的路线并将GoRouter设置为应用的导航器时,这才有效:

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routeInformationProvider: router.routeInformationProvider,
      routeInformationParser: router.routeInformationParser,
      routerDelegate: router.routerDelegate,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_, __) => '/family/${Families.data[0].id}',
    ),
    ShellRoute(
      // The ShellRoute is what make it possible to wrap the subroutes in a common widget using the `builder`
      builder: (BuildContext context, GoRouterState state, Widget child) {
        int index = int.tryParse(state.params['fid'] ?? '0') ?? 0;
        return ShellScreen(index: index, child: child);
      },
      routes: [
        GoRoute(
          path: '/family/:fid',
          builder: (context, state) {
            final fid = state.params['fid']!;
            final family = Families.data.firstWhere((f) => f.id == fid,
                orElse: () => throw Exception('family not found: $fid'));
            return FamilyView(
              key: state.pageKey,
              family: family,
            );
          },
          routes: [
            GoRoute(
              path: 'person/:id',
              builder: (context, state) {
                final fid = state.params['fid']!;
                final id = state.params['id'];

                final person = Families.data
                    .firstWhere((f) => f.id == fid,
                        orElse: () => throw Exception('family not found: $fid'))
                    .people
                    .firstWhere(
                      ((p) => p.id == id),
                      orElse: () => throw Exception('person not found: $id'),
                    );
                return PersonView(key: state.pageKey, person: person);
              },
            ),
          ],
        ),
      ],
    ),
  ],
);

解决我们需求的重要部分是ShellRouter,它是一个用于显示任何匹配子路由的路由,而不是将它们放在根Navigator中。
匹配到的子路径构建的widget成为构建器的子参数,ShellScreen可以渲染子路径widget呈现导航条。
通过所有这些步骤,您将得到:

过期答案(v3.0.0)

是的,这是可能的[事实上,这是不可能的,但我当时不明白这个问题]。
让我们使用GoRouter文档中的示例作为起点。
1.我们需要创建一些基本模型来存储数据:

/// Just a generic model that will be used to present some data on the screen.
class Person {
  final String id;
  final String name;

  Person({required this.id, required this.name});
}

/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
  final String id;
  final String name;
  final List<Person> people;
  final Icon icon;

  Family({
    required this.id,
    required this.name,
    required this.people,
    required this.icon,
  });
}

/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
  static const List<Icon> icons = [
    Icon(Icons.looks_one),
    Icon(Icons.looks_two),
    Icon(Icons.looks_3)
  ];

  static final List<Family> data = List.generate(
    3,
    (fid) => Family(
      id: '$fid',
      name: 'Family $fid',
      people: List.generate(
        10,
        (id) => Person(id: '$id', name: 'Family $fid Person $id'),
      ),
      icon: icons[fid],
    ),
  );
}

1.现在我们将创建呈现模型数据的基本视图:

/// Used to present Person's data.
class PersonView extends StatelessWidget {
  const PersonView({required this.person, Key? key}) : super(key: key);
  final Person person;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text(person.name),
      ),
    );
  }
}

/// This is the view that will be used by each application's tab.
class FamilyView extends StatefulWidget {
  const FamilyView({required this.family, Key? key}) : super(key: key);
  final Family family;

  @override
  State<FamilyView> createState() => _FamilyViewState();
}

class _FamilyViewState extends State<FamilyView>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      children: [
        for (final p in widget.family.people)
          ListTile(
            title: Text(p.name),
            onTap: () =>
                context.go('/family/${widget.family.id}/person/${p.id}'),
          ),
      ],
    );
  }
}

1.到目前为止,我们所做的与GoRouter文档相比没有什么不同,所以让我们最后创建一个小部件来显示NavigationBar

class FamilyTabsScreen extends StatefulWidget {
  final int index;
  FamilyTabsScreen({required Family currentFamily, Key? key})
      : index = Families.data.indexWhere((f) => f.id == currentFamily.id),
        super(key: key) {
    assert(index != -1);
  }

  @override
  _FamilyTabsScreenState createState() => _FamilyTabsScreenState();
}

class _FamilyTabsScreenState extends State<FamilyTabsScreen>
    with TickerProviderStateMixin {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(_title(context)),
        ),
        body: FamilyView(family: Families.data[widget.index]),
        bottomNavigationBar: NavigationBar(
          destinations: [
            for (final f in Families.data)
              NavigationDestination(
                icon: f.icon,
                label: f.name,
              )
          ],
          onDestinationSelected: (index) => _tap(context, index),
          selectedIndex: widget.index,
        ),
      );

  void _tap(BuildContext context, int index) =>
      context.go('/family/${Families.data[index].id}');

  String _title(BuildContext context) =>
      (context as Element).findAncestorWidgetOfExactType<MaterialApp>()!.title;
}

这是上面代码的重要部分:

/// [...] suppressed code
bottomNavigationBar: NavigationBar(
  destinations: [
    for (final f in Families.data)
      NavigationDestination(
        icon: f.icon,
        label: f.name,
      )
  ],
  onDestinationSelected: (index) => _tap(context, index),
  selectedIndex: widget.index,
),
/// [...] suppressed code

因此,基本上我们使用的NavigationBar与使用TabBarView几乎完全相同。
1.最后,只有当我们定义应用的路线并将GoRouter设置为应用的导航器时,这才能起作用:

void main() {
  GoRouter.setUrlPathStrategy(UrlPathStrategy.path);
  runApp(const MyApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_) => '/family/${Families.data[0].id}',
    ),
    GoRoute(
        path: '/family/:fid',
        builder: (context, state) {
          final fid = state.params['fid']!;
          final family = Families.data.firstWhere((f) => f.id == fid,
              orElse: () => throw Exception('family not found: $fid'));

          return FamilyTabsScreen(key: state.pageKey, currentFamily: family);
        },
        routes: [
          GoRoute(
            path: 'person/:id',
            builder: (context, state) {
              final fid = state.params['fid']!;
              final id = state.params['id'];

              final person = Families.data
                  .firstWhere((f) => f.id == fid,
                      orElse: () => throw Exception('family not found: $fid'))
                  .people
                  .firstWhere(
                    ((p) => p.id == id),
                    orElse: () => throw Exception('person not found: $id'),
                  );

              return PersonView(key: state.pageKey, person: person);
            },
          ),
        ]),
  ],
);

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routeInformationParser: _router.routeInformationParser,
      routerDelegate: _router.routerDelegate,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

通过所有这些步骤,您将得到:

wxclj1h5

wxclj1h52#

对于任何在所有页面上持久的BottomNavBar上搜索的人来说,这在Github上正在积极讨论,
https://github.com/flutter/packages/pull/2453

相关问题