- 本篇参考资料《Flutter实战》
- 本篇文章只是本人看书的理解和整理的笔记,更完整的内容还在书上!
- 电子书链接:https://book.flutterchina.club/
- Flutter中文社区链接:https://flutterchina.club/
- 尊重原作者,能支持购买实体书当然最好
首先 在Flutter中 需要记住一句话:万物皆Widget
本片参考第二章2.1和2.2内容
一.关于StatelessWidget
和StatefulWidget
现在刚起步,只是简单的理解
在第二章开头的计数器例子中可以看到
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”实现页面跳转了
简单分析一下代码:
首先我们看到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.replace
、Navigator.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
的效果
这里需要使用到async
和await
关键字这个是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