美文网首页
《Flutter实战》第二章(上)路由

《Flutter实战》第二章(上)路由

作者: 番茄tomato | 来源:发表于2020-04-28 19:20 被阅读0次
    • 本篇参考资料《Flutter实战》
    • 本篇文章只是本人看书的理解和整理的笔记,更完整的内容还在书上!
    • 电子书链接:https://book.flutterchina.club/
    • Flutter中文社区链接:https://flutterchina.club/
    • 尊重原作者,能支持购买实体书当然最好

    首先 在Flutter中 需要记住一句话:万物皆Widget
    本片参考第二章2.1和2.2内容

    一.关于StatelessWidgetStatefulWidget

    现在刚起步,只是简单的理解
    在第二章开头的计数器例子中可以看到
    MyApp继承于StatelessWidget,是无状态组件,直接在其内部就有build方法构建Widget树

    MyHomePage继承于StatefulWidget,是有状态组件,在其内部有 createState方法来根据状态来创建Weiget状态,并且_MyHomePageState继承于State<MyHomePage>,这里传入范形<MyHomePage>,之前的createState就是创建的_MyHomePageState对象,在_MyHomePageState中才有build创建Weiget树
    并且在_MyHomePageState的中还有setState来根据条件刷新状态,进而刷新界面,可以看到在_incrementCounter方法中调用了setState
    所以相对于StatelessWidget无状态组件不可改变,StatefulWidget是有状态的组件,是可以改变的,我们可以得出以下结论

    Stateful widget至少由两个类组成:
    一个StatefulWidget类。
    一个 State类; StatefulWidget类本身是不变的,但是State类中持有的状态在widget生命周期中可能会发生变化。

    二.路由管理 页面跳转

    路由(Route)在移动开发中通常指页面(Page),这跟web开发中单页应用的Route概念意义是相同的,Route在Android中通常指一个Activity,在iOS中指一个ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter中的路由管理和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈

    关于路由更详细的内容请参考:https://blog.csdn.net/zhongguohaoshaonian/article/details/105389566

    2.1 路由的例子

    先来完成我们的第二个页面NewRoute:内容很简单,就是一个appBar和中间一句话而已

    class NewRoute extends StatefulWidget {
      @override
      NewRouteState createState() {
        return NewRouteState();
      }
    }
    
    class NewRouteState extends State<NewRoute> {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Scaffold(
          appBar: AppBar(
            title: Text("New route"),
          ),
          body: Center(
            child: Text("This is new route"),
          ),
        );
      }
    }
    
    

    在之前的<Widget>[]中加入一个组件FlatButton

    Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
          ... //省略无关代码
          FlatButton(
             child: Text("open new route"),
             textColor: Colors.blue,
             onPressed: () {
              //导航到新路由   
              Navigator.push( context,
               MaterialPageRoute(builder: (context) {
                  return NewRoute();
               }));
              },
             ),
           ],
     )
    

    这样我们就可以点击“open new route”实现页面跳转了


    路由管理.png

    简单分析一下代码:
    首先我们看到FlatButton的onPressde中,我们传入了一个匿名方法执行了以下内容

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

    这里有很多新东西,一个个来,首先是Navigator

    2.2 Navigator路由导航器,

    是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:

    Navigator.push:是跳转到下一个页面,它要接受两个参数一个是上下文context,另一个是要跳转的函数;
    源码分析:Future push(BuildContext context, Route route)
    将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。

    Navigator.pop:是返回到上一个页面,使用时传递一个context(上下文)参数,使用时要注意的是,你必须是有上级页面的,也就是说上级页面使用了bool
    源码分析 :pop(BuildContext context, [ result ])
    将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。

    Navigator还有很多其它方法,如Navigator.replaceNavigator.popUntil等,以后用到再补充

    2.3 MaterialPageRoute页面路由

    MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

    • 对于Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
    • 对于iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
      其余内容参考书上

    补充一点:
    Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route) ,下面命名路由相关的方法也是一样的。

    2.4 路由传值

    界面的跳转我们通常会有参数的传递,在android中,使用的Intent来传递参数,那flutter中呢
    其实在flutter中路由的传递更加简单

    单纯的A-->B
    具体的步骤就是为即将要显示的界面设置构造函数,在 Navigator.push传递路由调用构造函数时,传入值即可
    创建构造函数:

    class TipRoute extends StatelessWidget {
      TipRoute({
        Key key,
        @required this.text,  // 构造函数 接收一个text参数
      }) : super(key: key);
      final String text;
    .......
    }
    

    在 Navigator.push中调用构造函数时传入需要传递的值(可以是任何对象)

                FlatButton(
                  child: Text("打开一个传值的路由"),
                  textColor: Colors.blue,
                  onPressed: () {
                    Navigator.push(context, MaterialPageRoute(builder: (context) {
                      return TipRoute(text: "晚上号呀 sIR");
                    }));
                  },
                )
    

    A<--->B
    但是有时候我们需要B在返回A的时候有一个返回值,类似于startActivityForResult的效果
    这里需要使用到asyncawait关键字这个是flutter中异步编程常用的关键字,这里只是简单说明使用,暂时不深入讲
    先是从A打开B,可以看到和之前的例子大概相同,但是多了一些东西

                FlatButton(
                  child: Text("打开一个传值的路由"),
                  textColor: Colors.blue,
                  onPressed: () async{
                   var result=await Navigator.push(context, MaterialPageRoute(builder: (context) {
                      return TipRoute(text: "晚上好呀 sir");
                    }));
                    print(result);
                  },
                )
    

    首先async修饰函数体,声明让这个函数异步进行,在传值的时候,之前说过Navigator.push会返回一个Future对象,用以接收新路由出栈(即关闭)时的返回数据,这里就用await关键字等待Navigator.push返回,然后打印出结果

    I/flutter (18268): 我是返回值
    

    如果没有await关键字,就是不等待Navigator.push返回,直接打印,则会是一个Future对象:

    I/flutter (18268): Instance of 'Future<dynamic>'
    

    那么看完了A-->B异步方式等待值返回,那么接下来看一下B-->A,值是怎么返回A,其实非常简单,之前介绍过了Navigator.pop,将返回值作为Navigator.pop的一个参数就好

                  RaisedButton(
                    onPressed: (){
                      Navigator.pop(context, "我是返回值")
                    },
                    child: Text("返回"),
                  )
    

    2.5 命名路由

    2.5.1 命名路由和路由表

    所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。
    要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:

    Map<String, WidgetBuilder> routes;
    

    它是一个Map,key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。
    但是光有路由表还不够,我们还需要注册路由表,在MaterialApp中进行注册

    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      //注册路由表
      routes:{
       "new_page":(context) => NewRoute(),
        ... // 省略其它路由注册信息
      } ,
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    
    2.5.2 通过路由名来进行路由

    现在我们路由表有了一个"new_page"路由名,该怎么通过路由名路由呢
    使用Navigator.pushNamed方法

                FlatButton(
                  child: Text("open new route"),
                  textColor: Colors.blue,
                  onPressed: () {
                    Navigator.pushNamed(context, "new_page");
                  },
                )
    

    除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,后续补充

    2.5.2 home也可以是路由

    接下来
    刚刚的例子中我们可以看到
    home: MyHomePage(title: 'Flutter Demo Home Page')
    其实也相当于一个路由,从没有界面到显示第一个界面
    那么我们home是否也可以注册在路由表中呢,当然可以:

    MaterialApp(
          title: 'Flutter Demo',
          theme: new ThemeData(
            primarySwatch: Colors.blue,
          ),
          initialRoute: "MyHome",//注册了home路由表 使用initialRoute初始路由调用
          routes: {
            "new_route_page":(context)=>NewRoute(),
            "MyHome":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
          },
        );
    

    home注册路由表后,就不能再使用home来new一个新界面了,而是使用initialRoute来调用路由表的内容作为启动页

    通常,移动应用管理着大量的路由,并且最容易的是使用名称来引用它们。路由名称通常使用路径结构:“/a/b/c”,主页默认为 “/”。

    2.5.3 通过命名路由来传递参数

    方式一:不修改原路由页
    比如之前的例子,我们打开TipRoute并传递参数:
    在路由表中注册路由名tip,并且调用构造函数的时候,通过RouteSetting对象获取路由参数

          routes: {
            "new_route_page":(context)=>NewRoute(),
            "MyHome":(context) => MyHomePage(title: 'Flutter Demo Home Page'),
            "tip": (context){
              return TipRoute(text: ModalRoute.of(context).settings.arguments);
            }//tip路由名,打开TipRoute并传递text参数
            ,
          }
    

    用法:

               FlatButton(
                  child: Text("打开一个传值的路由"),
                  textColor: Colors.blue,
                  onPressed: () async{
                   var result=await Navigator.pushNamed(context, "tip",arguments:"晚上好 Sir");
                    print(result);
                  },
                )
    

    但为了保持路由表的简洁,我们也可也这么做:
    方式二:修改原路由页
    在路由页build中通过RouteSetting对象获取路由参数
    修改TipRoute代码,删除构造函数

    class TipRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
    //在路由页通过RouteSetting对象获取路由参数
        var text=ModalRoute.of(context).settings.arguments;
        
    
        return Scaffold(....)
    }
    

    然后在路由表中注册路由名 tip:

          routes: {
            "new_route_page":(context)=>NewRoute(),
            "MyHome":(context) => MyHomePage(title: 'Flutter Demo Home Page'),//注册首页路由
    
            "tip": (context){
              return TipRoute();
            },//tip路由名,打开TipRoute并传递text参数
          }
    

    这里用法和方式一没有区别

               FlatButton(
                  child: Text("打开一个传值的路由"),
                  textColor: Colors.blue,
                  onPressed: () async{
    //尝试一下实例方法 和上边完全等价
                  var result=await Navigator.of(context).pushNamed("tip",arguments:"晚上好 Sir");
    
                    print(result);
                  },
                )
    

    2.6 路由钩子 路由拦截器的使用

    2.6.1 onGenerateRoute

    MaterialApp中有一个属性onGenerateRoute这个就是路由拦截器,当使用命名路由时,如果这个命名没有在路由表中注册,就会调用路由拦截器:
    例子:比如说我们这里在路由表里注册了“new1”,但是没有注册“new2”,并且完成了onGenerateRoute的逻辑

          routes: {
            "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'),//注册首页路由
            "/new1": (context)=>NewRoute(),
          },
          onGenerateRoute: (RouteSettings settings){
            String routeName = settings.name;
            print('当前访问路由名:$routeName');
            if (routeName == '/new2') {
              return MaterialPageRoute(builder: (context) {
                return NewRoute();
              });
            } else {
               return   MaterialPageRoute(builder: (context) {
                return ErrorPage();
              });
            }
          },
    

    当我们直接使用"new1"时,不会触发onGenerateRoute的逻辑

    //new1已经注册
    Navigator.of(context).pushNamed("/new1");
    

    但是当我们使用“new2”时则会触发onGenerateRoute的逻辑

    //new2未注册
    Navigator.of(context).pushNamed("/new2");
    

    再来看看onGenerateRoute的逻辑内容:首先获取到路由名称,打印出来,然后判断路由名称如果是“new2”就返回newroute页面,这个和new1的效果完全一样

    通过路由拦截器,我们可以全局进行路由拦截,不将需要拦截的路由名注册在路由表中,然后再在拦截器中单独处理,比如在一个需要登陆权限的界面,判断你是否需要登陆:

          onGenerateRoute: (RouteSettings settings){
            String routeName = settings.name;
            print('当前访问路由名:$routeName');
            // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
            // 引导用户登录;其它情况则正常打开路由。
            if(routeName对应的访问界面需要登陆){
              if(未登陆){
                return MaterialPageRoute(builder: (context) {
                  return LoginPage();
                });
              }else{
                ...正常访问
              }
            }else{...正常访问}
          }
    

    然后就是如果想要在onGenerateRoute路由拦截器中传递参数该怎么做?
    目前我能想到的就是增加构造函数,通过构造函数传值(这个方式感觉不优雅,暂时想不到其他的,以后再改)
    构造函数:

    class TipRoute extends StatelessWidget {
      var text;
    
      TipRoute({
        Key key,
        this.text, // 构造函数 接收一个text参数,和之前不同的是这里不是必须要求的了
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        if (text == null) {
          //没有从构造函数接入
          text = ModalRoute.of(context).settings.arguments;
        }
    
        if (text == null) {
          text = "没有接收到参数";
    ......    
    }
    }
    

    在拦截器中获取本次传递的参数,并传过去

          onGenerateRoute: (RouteSettings settings) {
            String routeName = settings.name;
            var argument = settings.arguments;
    
            print('当前访问路由名: $routeName');
            print("本次传递参数: $argument");
            
            if (routeName == "/tip2") {
              print("调用tip2");
              return MaterialPageRoute(builder: (context) {
                return TipRoute(text: argument);
              });
            } 
    ....
    }
    
    2.6.2 onUnknownRoute

    onUnknownRoute这个就比较简单了,它的用法和onGenerateRoute差不多

          onUnknownRoute: (RouteSettings settings) {
            String routeName = settings.name;
            print('未知路由: $routeName');
            return  MaterialPageRoute(builder: (context) {
              return ErrorPage();
            });
          }
    

    但是它似乎可以被onGenerateRoute完全替代,因为onUnknownRoute表示的未知路由显示错误界面,不就是onGenerateRoute中else的情况嘛,实际表现也一模一样,可能就是不想使用onGenerateRoute时又想拦截错误路由才会使用吧

    最后:
    再啰嗦两句,再flutter中,为了代码的可读性,我们通常为每个界面建立一个dart文件,并且单独为路由表建立一个dart文件,当然,拦截器的逻辑代码页应该单独封装,避免过多的缩进
    比如我们的路由表:

    import 'package:flutter/material.dart';
    
    import '../wedgets/myHome.dart';
    import '../wedgets/routerDeme/page1.dart';
    import '../wedgets/routerDeme/page2.dart';
    import '../wedgets/routerDeme/page3.dart';
    
    final routes = {
      '/': (context) => MyHomePage(),
      '/page1': (context) => PageOne(),
      '/page2': (context) => PageTwo(),
      '/page3': (context) => PageThree(),
    };
    

    本篇完
    下篇:
    《Flutter实战》第二章(下)
    https://www.jianshu.com/p/f16d71808ea3

    相关文章

      网友评论

          本文标题:《Flutter实战》第二章(上)路由

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