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

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 详解)