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

  • 本篇参考资料《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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容