美文网首页flutter相关Flutter 实战
Flutter入门(18):Flutter 组件之 Route

Flutter入门(18):Flutter 组件之 Route

作者: Maojunhao | 来源:发表于2020-09-16 16:53 被阅读0次

    1. 基本介绍

    Route 在 Flutter 里是极其重要的部分,用来处理页面跳转。本文主要普通路由,命名路由,以及自定义路由等。
    如果是简单的页面跳转可以参考Flutter 页面创建与跳转

    2. 示例代码

    代码下载地址。如果对你有帮助的话记得给个关注,代码会根据我的 Flutter 专题不断更新。

    3. 基础功能

    • 命名路由 routes
    • 路由跳转 push、pop
    • 初始路由 initialRoute
    • 路由拦截 onGenerateRoute

    4. 命名 Route 详解

    4.1 容器创建

    优雅的编程,我们创建一个 materialapp.dart 文件。

    import 'package:flutter/material.dart';
    
    class FMMaterialAppVC extends StatelessWidget{
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          home: Scaffold(
            body: AAA(),
          ),
          routes: {
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
        );
      }
    }
    
    class AAA extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('AAA'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往BBB'),
              onPressed: (){
                Navigator.pushNamed(context, '/bbb');
              },
            ),
          ),
        );
      }
    }
    
    class BBB extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('BBB'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往CCC'),
              onPressed: (){
                Navigator.pushNamed(context, '/ccc');
              },
            ),
          ),
        );
      }
    }
    
    class CCC extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('CCC'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往DDD'),
              onPressed: (){
                Navigator.pushNamed(context, '/ddd');
              },
            ),
          ),
        );
      }
    }
    
    class DDD extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('DDD'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击回到AAA'),
              onPressed: (){
                Navigator.popUntil(context, (route) => route.isFirst);
              },
            ),
          ),
        );
      }
    }
    

    我们为 BBB,CCC,DDD 进行了命名,效果如下。


    named route.gif

    4.2 路由跳转方式之 Push 详解

    在上文中,我们使用了 pushNamed 方法,推到一个新的页面。

    class AAA extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('AAA'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往BBB'),
              onPressed: (){
                Navigator.pushNamed(context, '/bbb');
              },
            ),
          ),
        );
      }
    }
    

    路由 push 其实还有很多种写法。

    4.2.1 Navigator.pushNamed

    无参数

    Navigator.pushNamed(context, '/bbb');
    

    有参数

    final datas = {"data": ["1","2","3"]};
    Navigator.pushNamed(context, '/bbb', arguments: datas);
    

    4.2.2 Navigator.push

    无参数

                Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context){
                          return BBB();
                      }
                );
    

    有参数,name 可以用来给路由命名,arguments 用来传递参数

                final datas = {"data": ["1","2","3"]};
    
                Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context){
                          return BBB();
                      },
                      settings: RouteSettings(
                        name: '/bbb',
                        arguments: datas,
                      ),
                    ),
                );
    

    上面代码意思就是跳转到 BBB() 页面,并且给 BBB() 命名为 '/bbb',在路由堆栈中,读取到这个 route 时,route.setting.name 与该命名相同。

    4.2.3 Navigator.of(context).pushNamed

    无参数

                Navigator.of(context).pushNamed('/bbb');
    

    有参数

                final datas = {"data": ["1","2","3"]};
                Navigator.of(context).pushNamed('/bbb',arguments: datas);
    

    4.2.4 Navigator.of(context).push

    无参数

                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context){
                      return BBB();
                    }
                  ),
                );
    

    有参数

                final datas = {"data": ["1","2","3"]};
    
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context){
                      return BBB();
                    },
                    settings: RouteSettings(
                      name: '/bbb',
                      arguments: datas,
                    ),
                  ),
                );
    

    4.3 路由跳转方式之 Push 进阶

    4.2 中是最为常用的页面进出栈,但是在某些场合需要有特殊的处理。
    注意:写法大同小异,后续就不在赘述传参方式,均与4.2相同。

    4.3.1 pushNamedAndRemoveUntil

    例如我们页面从AAA->BBB->CCC->DDD,这样进行页面,但是我们希望 CCC 再使用过后就被销毁。使得页面层级变为 AAA-BBB-DDD。

    • Navigator.pushNamedAndRemoveUntil

    如下方代码,我们在 CCC 中来做处理。
    我们的页面层级为 AAA->BBB->CCC->DDD,我们在 CCC 中 push 到 DDD 页面,然后从栈顶开始删除,直到这个route.setting.name == '/bbb',也就是销毁 BBB 与 DDD 中间所有的页面。

    class CCC extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('CCC'),
          ),
          body: _listView(context),
        );
      }
      
      ListView _listView(BuildContext context){
        return ListView(
          children: [
            ListTile(
              title: Text("Navigator.pushNamed"),
              onTap: (){
                Navigator.pushNamed(context, '/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.pushNamedAndRemoveUntil"),
              onTap: (){
                Navigator.pushNamed(context, '/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.of(context).pushNamedAndRemoveUntil"),
              onTap: (){
                Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.pushNamedAndRemoveUntil - current"),
              onTap: (){
                Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent);
              },
            ),
            ListTile(
              title: Text("Navigator.pushReplacementNamed"),
              onTap: (){
                Navigator.pushReplacementNamed(context, '/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.of(context).pushReplacementNamed"),
              onTap: (){
                Navigator.of(context).pushReplacementNamed('/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.pushReplacement"),
              onTap: (){
                Navigator.pushReplacement(context,
                  MaterialPageRoute(
                    builder: (context){
                      return DDD();
                    },
                    settings: RouteSettings(
                      name: '/ddd',
                    ),
                  ),
                );
              },
            ),
            ListTile(
              title: Text("Navigator.of(context).pushReplacement"),
              onTap: (){
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context){
                      return DDD();
                    },
                    settings: RouteSettings(
                      name: '/ddd',
                    ),
                  ),
                );
              },
            ),
            ListTile(
              title: Text("Navigator.popAndPushNamed"),
              onTap: (){
                Navigator.popAndPushNamed(context, '/ddd');
              },
            ),
            ListTile(
              title: Text("Navigator.of(context).popAndPushNamed"),
              onTap: (){
                Navigator.of(context).popAndPushNamed('/ddd');
              },
            ),
          ],
        );
      }
    }
    

    如gif图,我们push 顺序为 AAA->BBB->CCC->DDD,返回时为 DDD->BBB->AAA,CCC页面销毁。


    route removeUnti.gif
    • Navigator.of(context).pushNamedAndRemoveUntil
                Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/bbb');
    
    route removeUnti.gif
    • Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
      跳转到指定页面,并删除前边所有页面
    Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
    // 等价于下面这行代码
                //Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
    
    route removeUntil current.gif

    4.3.2 pushReplacementNamed

    例如我们页面从AAA->BBB->CCC->DDD,这样进行页面,但是我们希望 CCC 再使用过后就被销毁。使得页面层级变为 AAA-BBB-DDD。

    注意:同4.2.2区别,假设我们的页面为 AAA->BBB->CCC->DDD->EEE->FFF,我们在 EEE 页面跳转到 FFF 并且 removeUntil('/bbb'),那么我们的页面层级则会变成 AAA->BBB->FFF。而使用 pushReplacementNamed('/ddd'),则会变成 AAA->BBB->CCC->DDD->FFF。

    • Navigator.pushReplacementNamed
                Navigator.pushReplacementNamed(context, '/ddd');
    
    route pushReplacement.gif
    •         Navigator.of(context).pushReplacementNamed
      
                Navigator.of(context).pushReplacementNamed('/ddd');
    
    • Navigator.pushReplacement

    具体可以参考 4.2 写法

                Navigator.pushReplacement(context,
                      MaterialPageRoute(
                        builder: (context){
                          return DDD();
                        },
                        settings: RouteSettings(
                          name: '/ddd',
                        ),
                      ),
                );
    
    • Navigator.of(context).pushReplacement
      具体可以参考 4.2 写法
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context){
                      return DDD();
                    },
                    settings: RouteSettings(
                      name: '/ddd',
                    ),
                  ),
                );
    

    4.3.3 popAndPushNamed

    先 pop 一级,然后在 push 到对应页面,效果与4.3.2 相同

    • Navigator.popAndPushNamed
                Navigator.popAndPushNamed(context, '/ddd');
    
    route popAndPushNamed.gif
    • Navigator.of(context).popAndPushNamed
                Navigator.of(context).popAndPushNamed('/ddd');
    

    4.4 路由跳转方式之 Pop 详解

    Pop 是路由返回方式,与 push 正好相反,push 用来控制页面入栈,而 pop 则控制页面出栈。其实左上角的返回按钮会默认执行一次 pop 方法。

    由于 pop 是返回方式,我们在最后一个页面 DDD 中操作。

    class DDD extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('DDD'),
          ),
          body: _listView(context),
        );
      }
    
      ListView _listView(BuildContext context){
        return ListView(
          children: [
            ListTile(
              title: Text('Navigator.pop'),
              onTap: (){
                Navigator.pop(context);
                // final data = {"data":["1","2","3"]};
                // Navigator.pop(context, data);
              },
            ),
            ListTile(
              title: Text('Navigator.pop'),
              onTap: (){
                Navigator.of(context).pop();
              },
            ),
            ListTile(
              title: Text('Navigator.canPop'),
              onTap: (){
                bool canpop = Navigator.canPop(context);
                if (canpop) Navigator.pop(context);
              },
            ),
            ListTile(
              title: Text('Navigator.maybePop'),
              onTap: (){
                Navigator.maybePop(context);
              },
            ),
          ],
        );
      }
    }
    
    route pop.gif

    4.4.1 Navigator.pop

    无参数

                Navigator.pop(context);
    

    有参数

                final data = {"data":["1","2","3"]};
                Navigator.pop(context, data);
    

    4.4.2 Navigator.of(context).pop

    无参数

                Navigator.of(context).pop();
    

    有参数

                final data = {"data":["1","2","3"]};
                Navigator.of(context).pop(data);
    

    4.4.3 Navigator.canPop

    判断是否可以 pop

                bool canpop = Navigator.canPop(context);
                if (canpop) Navigator.pop(context);
    

    4.4.4 Navigator.of(context).canPop

                bool canpop = Navigator.of(context).canPop();
    

    4.4.5 Navigator.maybePop

    先判断是否可以 pop,如果可以,在pop,相当于 4.4.3 中代码

                Navigator.maybePop(context);
    

    4.4.6 Navigator.of(context).maybePop

                Navigator.of(context).maybePop(context);
    

    4.5 路由跳转方式之 Pop 进阶

    4.4 中是最为常用的页面进出栈,但是在某些场合需要有特殊的处理。pop 进阶方法比较少,只有 popUntil 与 popAndPushNamed ,在4.3.3中讲过后者了,这里不重复叙述。

    • 回到栈顶
                Navigator.popUntil(context, (route) => route.isFirst);
    
    • 回到指定页面

    我们使用下方代码回到 BBB 页面

                Navigator.popUntil(context, (route) => route.settings.name == '/bbb');
    
    • 页面堆栈了解
      这里我们自定义一下方法,查看一下当前堆栈的所有 route
                Navigator.popUntil(context, (route) => _lookRoutes(route));
    
      bool _lookRoutes(Route route){
        print(route);
        return route.isFirst;
      }
    
    all routes.png

    我们来看一下上述页面的所有效果。


    route popUntil.gif

    5. 初始路由 initialRoute

    我们注释掉 home 属性,使用 initialRoute 来加载页面

      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          // home: Scaffold(
          //   body: AAA(),
          // ),
          initialRoute: '/ccc',
          routes: {
            '/aaa': (context) => AAA(),
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
        );
      }
    
    initialRoute ccc.png

    5.1 initialRoute 使用了未命名的路由

    报错如下。

    A GlobalKey was used multiple times inside one widget's child list.
    

    注意:initialRoute 需要使用 routes 表中的命名过的路由,否则会报错。

    5.2 解决方案

    • a. 更改 initialRoute 为 routes 表里命名过得路由。

    • b. 设置根路由
      设置路由 '/' ,当初始路由异常时,会停留在根路由。

      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          // home: Scaffold(
          //   body: AAA(),
          // ),
          initialRoute: '/ccc',
          routes: {
            '/': (context) => AAA(),
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
        );
      }
    
    initialRoute plan1.png
    • c. 使用未知路由
      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          // home: Scaffold(
          //   body: AAA(),
          // ),
          initialRoute: '/cc',
          routes: {
            '/aaa': (context) => AAA(),
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
          onUnknownRoute: (setting){
            print(setting);
            return MaterialPageRoute(builder: (context) => AAA());
          },
        );
      }
    
    initialRoute plan2.png

    6. 未知路由 onUnknownRoute

    class FMMaterialAppVC extends StatelessWidget{
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          initialRoute: '/',
          routes: {
            '/': (context) => AAA(),
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
          onUnknownRoute: (setting){
            print(setting);
            return MaterialPageRoute(builder: (context) => AAA());
          },
        );
      }
    }
    
    class BBB extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('BBB'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往CCC'),
              onPressed: (){
                Navigator.pushNamed(context, '/cc');
              },
            ),
          ),
        );
      }
    }
    

    我们在 BBB 页面中 push 一个错误的 '/cc' 路由,按照我们 onUnknownRoute 中的设置,我们返回一个 AAA(),用这个可以做一个统一的报错页面。


    route onUnknownRoute.gif

    7. 路由拦截 onGenerateRoute

    class FMMaterialAppVC extends StatelessWidget{
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          // home: Scaffold(
          //   body: AAA(),
          // ),
          initialRoute: '/',
          routes: {
            '/': (context) => AAA(),
            '/bbb': (context) => BBB(),
            '/ccc': (context) => CCC(),
            '/ddd': (context) => DDD(),
          },
          // onGenerateInitialRoutes: (string){
          //   return [
          //     MaterialPageRoute(builder: (context) => AAA()),
          //   ];
          // },
          onGenerateRoute: (setting){
            print(setting);
            return MaterialPageRoute(builder: (context) => CCC());
          },
          onUnknownRoute: (setting){
            print(setting);
            return MaterialPageRoute(builder: (context) => AAA());
          },
        );
      }
    }
    

    应该有很多小伙伴跟我一样,onGenerateRoute 不执行,明明写了 onGenerateRoute 方法,但是却不响应。查了挺多博客,没有相关描述,然后去翻了官方文档和源码,终于找到问题。

    onGenerateRoute 路由拦截不能与命名路由一起使用,否则会只执行命名路由,不在进行拦截。下面我们注释掉命名路由,然后通过路由拦截,自定义一套路由跳转,从而实现命名路由的功能。

      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          initialRoute: '/',
          // routes: {
          //   '/': (context) => AAA(),
          //   '/bbb': (context) => BBB(),
          //   '/ccc': (context) => CCC(),
          //   '/ddd': (context) => DDD(),
          // },
          // onGenerateInitialRoutes: (string){
          //   return [
          //     MaterialPageRoute(builder: (context) => AAA()),
          //   ];
          // },
          onGenerateRoute: (setting){
            print(setting);
            final isLogin = true;
            
            final routes = {
              '/': (context) => AAA(),
              '/bbb': (context) => BBB(),
              '/ccc': (context) => CCC(),
              '/ddd': (context) => DDD(),
            };
            
            if (!isLogin) {
               return MaterialPageRoute(builder: (context) => AAA());
            }
            
            return MaterialPageRoute(builder: routes[setting.name], settings: setting);
          },
          onUnknownRoute: (setting){
            print(setting);
            return MaterialPageRoute(builder: (context) => AAA());
          },
        );
      }
    

    举一反三,在拦截路由中自定义一张路由表。当未登录时,对页面进行拦截,跳转到登录页面,已登录时,正常加载路由表内的页面,实现路由拦截功能。


    route onGenerateRoute.gif

    8. 路由传值

    在App中,页面之间的通讯和传值是非常重要的,这里只单独介绍一下使用路由时的传值方式,以及反向传值的方法。

    我们在 BBB 页面使用 pushNamed 传值给 CCC,在 CCC 页面介绍如何接收路由传值。然后在 CCC 页面使用 pop 带参数回来,在 BBB 页面介绍如何接收路由反参。

    8.1 路由正向传值以及反参接收

    BBB 页面 push 到 CCC,并等待 CCC 回来时的反参。

    class BBB extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('BBB'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往CCC'),
              onPressed: (){
                var backValueFromDDD = Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB页面传过来的值"});
                backValueFromDDD.then((value){
                  print("CCC 传回来反参了,${value}");
                });
                // Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
              },
            ),
          ),
        );
      }
    }
    

    我们使用 then 属性来接收下一个页面 pop 回来带的参数。

    8.2 路由接收正向传值以及反向传参

    我们在 CCC 类中增加以下代码。

      Widget build(BuildContext context) {
        // TODO: implement build
        var value = ModalRoute.of(context).settings.arguments;
        print("BBB 页面带过来参数了,${value}");
    
        return Scaffold(
          appBar: AppBar(
            title: Text('CCC'),
          ),
          body: _listView(context),
        );
      }
    
            ListTile(
              title: Text("Navigator.of(context).pop 传参"),
              onTap: (){
                Navigator.of(context).pop({"value":"我是CCC页面带回来的值"});
              },
            ),
    

    8.3 打印效果

    route argument.gif

    8.4 其他写法

    在 BBB 中可以尝试以下写法,可以达到相同效果。

    class BBB extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text('BBB'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('点击前往CCC'),
              onPressed: () async {
                var backValueFromDDD = await Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB页面传过来的值"});
                backValueFromDDD;
                print(backValueFromDDD);
                // Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
              },
            ),
          ),
        );
      }
    }
    

    9. 技术小结

    • 路由在App中是一个非常重要的功能,我已经从事App开发多年了,对页面堆栈以及原理比较熟悉,所以上手起来比较容易。初学者需要更多耐心来理解和尝试路由的各个方法。
    • 路由这一篇章花费时间较多,踩坑也比较多,为了尽可能的覆盖到路由的全部用法,本文篇幅也比较长。

    相关文章

      网友评论

        本文标题:Flutter入门(18):Flutter 组件之 Route

        本文链接:https://www.haomeiwen.com/subject/ejffyktx.html