Flutter Navigator 2.0:带抽屉导航的命名路由

Flutter Navigator 2.0: Named routes with Drawer Navigation

我正在一个遵循 Flutter Navigator 2.0 的应用程序上开发一个简单的导航,以便更好地支持路线。 Navigator 2.0 的一个优点是您可以对屏幕导航进行精细控制,并更好地支持网络。

这是我要实现的目标的图表。

我可以毫无问题地从登录屏幕导航到“主页”。主页有一个 Drawer,默认显示 HomeScreen。我的问题是我不确定如何使用 Navigator 2.0 从抽屉中正确显示 HomeScreen 和 ProfileScreen。在此之后 guide 演示了将屏幕推送到导航器堆栈并使用 RouterDelegate 跟踪路线。这样做会将新屏幕推送到堆栈。

该应用似乎运行良好,但我只是删除了过渡动画。您会注意到,甚至在 Drawer 完成其关闭动画之前,就绘制了一个新屏幕。由于主页面被推入堆栈,整个抽屉小部件被再次绘制。

这个 guide 在抽屉上显示屏幕只替换同一屏幕上的小部件。我目前正在做的是每次导航到 HomeScreen 和 ProfileScreen 时我都在重建主页

这是主页的样子。 currentPage 更新为显示 HomeScreen 和 ProfileScreen 的 Widget。

late Widget currentPage;

@override
Widget build(BuildContext context) {
  currentPage = HomeScreen();
  return Scaffold(
    appBar: AppBar(title: Text(title)),
    body: currentPage,
    drawer: Drawer(
      child: ListView(
        children: <Widget>[
          // ...
          ListTile(
            title: Text('Home'),
            onTap: () {
              widget.navHome();
              Navigator.pop(context);
           
            },
          ),
          ListTile(
            title: Text('Profile'),
            // ...
          ),
        ],
      ),
    ),
  );
}

我的 RouterDelegate 上的 Navigator 有这个设置。

Navigator(
  key: navigatorKey,
  transitionDelegate: NoAnimationTransitionDelegate(),
  pages: [
    if (show404)
      MaterialPage(
        key: ValueKey('UnknownPage'),
        child: UnknownScreen(),
      )
    else if (page == Pages.home)
      MaterialPage(
        key: ValueKey('HomePage'),
        child: HomePage(
          title: 'Home',
          handleLogout: _logOut,
          navHome: _navHome,
          navProfile: _navProfile,
          currentScreen: HomeScreen(username: username),
        ),
      )
    else if (page == Pages.profile)
      MaterialPage(
        key: ValueKey('ProfilePage'),
        child: HomePage(
          title: 'Profile',
          handleLogout: _logOut,
          navHome: _navHome,
          navProfile: _navProfile,
          currentScreen: ProfileScreen(),
        ),
      )
    else // username is null, no user logged in
      MaterialPage(
        key: ValueKey('LoginPage'),
        child: LoginPage(
          title: 'Login',
          onTapped: _handleLogin,
        ),
      ),
  ]
)

函数作为参数传递以更新 RouterDelegate 中的路由。

enum Pages { login, home, profile }

class PageRouterDelegate extends RouterDelegate<PageRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<PageRoutePath> {

  // ... Other lines omitted for simplicity

  void _handleLogin(String username, String password) {
    // TODO Auth
    this.page = Pages.home;
    notifyListeners();
  }

  void _navHome() {
    this.page = Pages.home;
    notifyListeners();
  }

  void _navProfile() {
    this.page = Pages.profile;
    notifyListeners();
  }

  void _logOut() {
    this.page = Pages.login;
    notifyListeners();
  }
}

有什么方法可以显示抽屉中的不同屏幕并使用 Navigator 2.0 跟踪它们的路线有什么建议吗?

更新:

我找到了这个问题的解决方案,但它需要使用两个导航器。将键分配给导航器有助于我们管理它们。

第二个导航器在主页上有一个抽屉。这使我们能够在不重建整个屏幕的情况下浏览不同的页面。这种方法的注意事项是只有 mainNavigator 上的路由才会显示在 Web 上的地址栏上。

如果您对如何解决这个问题有其他建议,请告诉我。

这不是一个直接的答案,只是一个如何解决这个问题的参考。

Flutter 最近更新了 routing example and in that example, they use InheritedWidget with ChangeNotifier to push new route to route delegate like RouteStateScope.of(context)!.go('/book/${book.id}');,同时保持路由状态处于活动状态。

由于需要编写大量样板代码,我暂时放弃了使用 Navigator 2.0。我退后一步,使用带有命名路由的 Navigator 1.0。 Navigator 2.0仍然可以应用同样的原理

我现在所做的不是在主导航器上构建不同的屏幕,而是重建相同的屏幕 (NavigatorPage) 以节省资源并为我想导航到的屏幕传递参数。

final _mainNavigatorKey = GlobalKey<NavigatorState>();
...
MaterialApp(
  title: 'Navigator Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  navigatorKey: _mainNavigatorKey,
  routes: {
    /// [title] updates the title on the main AppBar
    /// [route] NavigatorPage Router depends on route defined on this parameter
    /// [showDrawer] show/hide main AppBar drawer
    Nemo.home: (context) => NavigatorPage(
      title: 'Home',
      route: Nemo.home,
      navigatorKey: _mainNavigatorKey,
      showDrawer: true,
    ),
    Nemo.post: (context) => NavigatorPage(
      title: 'Post',
      route: Nemo.post,
      navigatorKey: _mainNavigatorKey,
      showDrawer: true),
    Nemo.profile: (context) => NavigatorPage(
      title: 'Profile',
      route: Nemo.profile,
      navigatorKey: _mainNavigatorKey,
      showDrawer: true),
    Nemo.settings: (context) => NavigatorPage(
      title: 'Settings',
      route: Nemo.settings,
      navigatorKey: _mainNavigatorKey,
      showDrawer: true),
  },
);

NavigatorPage 包含我们可以更新标题的主 AppBar 和 display/hide LinearProgressBar。从那里,我们可以通过使用 _mainNavigatorKey 作为我们的导航器检查参数中传递的路由来导航到所需的屏幕。

Navigator(
  // key: _navigatorKey,

  onGenerateRoute: (RouteSettings settings) {
    WidgetBuilder builder;
    // Manage your route names here
    // switch (settings.name) {
    switch (widget.route) {

      /// Default page displayed on Home Screen
      case Nemo.home:
        builder = (BuildContext context) => _homePage();
        break;
      case Nemo.post:
        builder = (BuildContext context) => _postPage();
        break;
      case Nemo.profile:
        builder = (BuildContext context) => _profilePage();
        break;
      case Nemo.settings:
        builder = (BuildContext context) => _settingsPage();
        break;
      default:
        builder = (BuildContext context) => const UnknownPage();
    }
    return MaterialPageRoute(
      builder: builder,
      settings: settings,
    );
  },
),

我们仍在重建主导航器上的页面。导航抽屉关闭时卡顿动画的解决方法是添加至少 300 毫秒的延迟,以等待动画完成后再执行导航。您可以根据需要调整延迟。

ListTile(
  title: const Text('Home'),
  onTap: () {
    // Close the drawer
    Navigator.pop(context);

    /// [drawerDelay] gives time to animate the closing of the Drawer
    Timer(Duration(milliseconds: drawerDelay), () async {
      widget.navigatorKey.currentState!.pushNamed(Nemo.home);
    });
  },
),

演示

示例代码

import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);
  final _mainNavigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Navigator Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // home: const NavigatorPage(title: 'Flutter Demo Home Page'),
      navigatorKey: _mainNavigatorKey,
      routes: {
        /// [title] updates the title on the main AppBar
        /// [route] NavigatorPage Router depends on route defined on this parameter
        /// [showDrawer] show/hide main AppBar drawer
        Nemo.home: (context) => NavigatorPage(
              title: 'Home',
              route: Nemo.home,
              navigatorKey: _mainNavigatorKey,
              showDrawer: true,
            ),
        Nemo.post: (context) => NavigatorPage(
            title: 'Post',
            route: Nemo.post,
            navigatorKey: _mainNavigatorKey,
            showDrawer: true),
        Nemo.profile: (context) => NavigatorPage(
            title: 'Profile',
            route: Nemo.profile,
            navigatorKey: _mainNavigatorKey,
            showDrawer: true),
        Nemo.settings: (context) => NavigatorPage(
            title: 'Settings',
            route: Nemo.settings,
            navigatorKey: _mainNavigatorKey,
            showDrawer: true),
      },
    );
  }
}

class NavigatorPage extends StatefulWidget {
  const NavigatorPage(
      {Key? key,
      required this.title,
      required this.route,
      required this.navigatorKey,
      required this.showDrawer})
      : super(key: key);

  final String title;
  final String route;
  final bool showDrawer;
  final GlobalKey<NavigatorState> navigatorKey;

  @override
  State<NavigatorPage> createState() => _NavigatorPageState();
}

class _NavigatorPageState extends State<NavigatorPage> {
  // final _navigatorKey = GlobalKey<NavigatorState>();
  /// Drawer delay let's us have the Navigation Drawer close first
  /// before the navigating to the next Screen
  int drawerDelay = 300;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      drawer: widget.showDrawer
          ? Drawer(
              /// TODO return null to hide Drawer if in Login/Registration page
              // Add a ListView to the drawer. This ensures the user can scroll
              // through the options in the drawer if there isn't enough vertical
              // space to fit everything.
              child: ListView(
                // Important: Remove any padding from the ListView.
                padding: EdgeInsets.zero,
                children: <Widget>[
                  const DrawerHeader(
                    decoration: BoxDecoration(
                      color: Colors.blue,
                    ),
                    child: Text('Drawer Header'),
                  ),
                  ListTile(
                    title: const Text('Home'),
                    onTap: () {
                      // Close the drawer
                      Navigator.pop(context);

                      /// [drawerDelay] gives time to animate the closing of the Drawer
                      Timer(Duration(milliseconds: drawerDelay), () async {
                        widget.navigatorKey.currentState!.pushNamed(Nemo.home);
                      });
                    },
                  ),
                  ListTile(
                    title: const Text('Profile'),
                    onTap: () {
                      // Close the drawer
                      Navigator.pop(context);

                      Timer(Duration(milliseconds: drawerDelay), () async {
                        widget.navigatorKey.currentState!
                            .pushNamed(Nemo.profile);
                      });
                    },
                  ),
                  ListTile(
                    title: const Text('Settings'),
                    onTap: () {
                      // Close the drawer
                      Navigator.pop(context);

                      Timer(Duration(milliseconds: drawerDelay), () async {
                        widget.navigatorKey.currentState!
                            .pushNamed(Nemo.settings);
                      });
                    },
                  ),
                ],
              ),
            )
          : null,
      body: Navigator(
        // key: _navigatorKey,

        /// initialRoute needs to be set to '/'
        onGenerateRoute: (RouteSettings settings) {
          WidgetBuilder builder;
          // Manage your route names here
          // switch (settings.name) {
          switch (widget.route) {

            /// Default page displayed on Home Screen
            case Nemo.home:
              builder = (BuildContext context) => _homePage();
              break;
            case Nemo.post:
              builder = (BuildContext context) => _postPage();
              break;
            case Nemo.profile:
              builder = (BuildContext context) => _profilePage();
              break;
            case Nemo.settings:
              builder = (BuildContext context) => _settingsPage();
              break;
            default:
              builder = (BuildContext context) => const UnknownPage();
          }
          return MaterialPageRoute(
            builder: builder,
            settings: settings,
          );
        },
      ),
    );
  }

  Widget _homePage() =>
      HomePage(title: 'Home', navigatorKey: widget.navigatorKey);
  Widget _postPage() =>
      PostPage(title: 'Post', navigatorKey: widget.navigatorKey);
  Widget _profilePage() =>
      ProfilePage(title: 'Profile', navigatorKey: widget.navigatorKey);
  Widget _settingsPage() =>
      SettingsPage(title: 'Settings', navigatorKey: widget.navigatorKey);
}

class Nemo {
  static const home = '/';
  static const login = '/login';
  static const register = '/register';
  static const post = '/post';
  static const profile = '/profile';
  static const settings = '/settings';
}

/// Constant values for UI elements
class Constants {
  static const String webVersion = 'web-0.1.9-dev';
  static const double paddingSmall = 8.0;
  static const double paddingNormal = 16.0;

  static const double heightNormal = 64.0;

  static const double heightThreadCard = 72.0;
  static const double heightButtonNormal = 42.0;

  static const double widthButtonNormal = 160.0;
}

class UnknownPage extends StatefulWidget {
  const UnknownPage({Key? key}) : super(key: key);

  @override
  State<UnknownPage> createState() => _UnknownPageState();
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key, required this.title, required this.navigatorKey})
      : super(key: key);
  final String title;
  final GlobalKey<NavigatorState> navigatorKey;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Home',
            ),
            ElevatedButton(
              child: const Text('View Post Page'),
              onPressed: () {
                widget.navigatorKey.currentState!.pushNamed(Nemo.post);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class PostPage extends StatefulWidget {
  const PostPage(
      {Key? key, required this.title, this.id, required this.navigatorKey})
      : super(key: key);

  final String title;
  final String? id;
  final GlobalKey<NavigatorState> navigatorKey;

  @override
  State<PostPage> createState() => _PostPageState();
}

class _PostPageState extends State<PostPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Id from Route: ${widget.id}',
            ),
          ],
        ),
      ),
    );
  }
}

class SettingsPage extends StatefulWidget {
  const SettingsPage(
      {Key? key, required this.title, required this.navigatorKey})
      : super(key: key);
  final String title;
  final GlobalKey<NavigatorState> navigatorKey;

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Settings',
            ),
            ElevatedButton(
              child: const Text('View Details'),
              onPressed: () {
                widget.navigatorKey.currentState!.pushNamed(Nemo.post);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class ProfilePage extends StatefulWidget {
  const ProfilePage({Key? key, required this.title, required this.navigatorKey})
      : super(key: key);
  final String title;
  final GlobalKey<NavigatorState> navigatorKey;

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Profile',
            ),
            ElevatedButton(
              child: const Text('View Details'),
              onPressed: () {
                widget.navigatorKey.currentState!.pushNamed(Nemo.post);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _UnknownPageState extends State<UnknownPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // appBar: AppBar(
      //   title: Text(widget.title),
      // ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text(
              '404',
            ),
          ],
        ),
      ),
    );
  }
}

如果您想要包含基本登录和注册屏幕的样本。我创建了这个模板,您可以查看:https://github.com/omatt/flutter-navdrawer-template