第2章 - 第一个Flutter应用
2.3 状态管理
状态管理官方原则:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理.
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理.
- 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理.
2.4 路由管理
导航到新路由:
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return NewRoutePage();
}))
}
MaterialPageRoute
PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性.
MaterialPageRoute
继承自 PageRoute
是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder
是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget. 我们通常要实现此回调,返回新路由的实例.settings
包含路由的配置信息,如路由名称、是否初始路由(首页).maintainState
默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState
为false
.fullscreenDialog
表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog
为true
,新页面将会从屏幕底部滑入(而不是水平方向).
如果想自定义路由切换动画,可以自己继承
PageRoute
来实现
Navigator
Navigator
是一个路由管理的组件,提供了打开和退出路由页方法. Navigator
通过一个栈来管理活动路由集合. 通常当前屏幕显示的页面就是栈顶的路由:
Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个 Future
对象,用以接收新路由出栈(即关闭)时的返回数据.
bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result
为页面关闭时返回给上一个页面的数据.
Navigator
类中第一个参数为context
的静态方法都对应一个Navigator
的实例方法, 比如Navigator.push(BuildContext context, Route route)
等价于Navigator.of(context).push(Route route)
.
路由传值
Navigator.pop(context, "我是返回值");
// 父级页面打开 ChildPage.dart 页面, 并子页面返回的值
ElevatedButton(onPressed: () async {
// result 是字面返回的值, 也即上面写的: "我是返回值"
var result = await Navigator.push(context, MaterialPageRoute(builder: (context) {
return ChildPage(
skuId: 123 // 传递参数给子页面
);
}));
});
命名路由
注册路由表:
MaterialApp(
title: 'Flutter Demo',
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
... // 省略其它路由注册信息
},
home: MyHomePage(title: 'Flutter Demo Home Page'),
)
也可以把上面的 MyHomePage
也注册成命名路由:
MaterialApp(
title: 'Flutter Demo',
//注册路由表
routes: {
"/": (context) => MyHomePage(title: 'Flutter Demo Home Page'),
"new_page": (context) => NewRoute(),
},
initialRoute: "/" // 名为"/"的路由作为应用的home(首页)
)
通过路由名称来打开新路由, 可以使用 Navigator
的 pushNamed
方法:
Future pushNamed(BuildContext context, String routeName,{Object arguments});
// 跳转到命名路由 new_page
var newPagePopReturnValue = await Navigator.of(context).pushNamed("new_page", arguments: "hi");
// new_page.dart 获取路由参数
var transferRouteArgs = ModalRoute.of(context)?.settings.arguments; // hi
加入HelpPage.dart
页面接受一个text
参数, 可以按照下面方式适配命令路由:
MaterialApp(
routes: {
"help": (context){
var text = ModalRoute.of(context)!.settings.arguments;
return HelpPage(text: text);
}
}
)
路由生成钩子
当调用 Navigator.pushNamed
打开未注册的命令路由时, 会调用 MaterialApp
的 onGenerateRoute
属性, 利用该特性可以实现权限控制:
MaterialApp(
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其它情况则正常打开路由.
}
);
}
);
onGenerateRoute
只会对命名路由生效, 且跳转到路由表(MaterialApp.routes
)不存在的路由才会被调用.
2.6 资源管理
和包管理一样,Flutter 也使用 pubspec.yaml
文件来管理应用程序所需的资源:
flutter:
assets:
- assets/my_icon.png
- assets/background.png
通常可以使用DefaultAssetBundle.of()
在应用运行时来间接加载 asset(例如JSON文件),而在 widget 上下文之外,或其它 AssetBundle
句柄不可用时,可以使用 rootBundle
直接加载这些 asset.
2.8 Flutter异常捕获
在 Java 和 Objective-C 中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart或JavaScript中则不会!究其原因,这和它们的运行机制有关系. Java 和 OC 都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出. 但 Dart 和 JavaScript 不会,它们都是单线程模型,运行机制很相似,但有区别:
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是微任务队列(microtask queue),另一个叫做事件队列(event queue),微任务队列的执行优先级高于事件队列.
在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长. 我们可以通过Future.microtask(…)方法向微任务队列插入一个任务.
框架异常捕获
Flutter 框架在一些关键的方法进行了异常捕获, 比如布局越界会自动弹出一个错误界面,这是因为Flutter在执行build方法时添加了异常捕获,源码如下:
@override
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
通过跟踪 _debugReportException
方法源码, 我们修改 FlutterError.onError
即可监听这部分异常:
FlutterErrorDetails _debugReportException(
String context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector
}) {
//构建错误详情对象
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
//报告错误
FlutterError.reportError(details);
return details;
}
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //调用了onError回调
}
// 下面我们修改 FluterError 的静态属性 onError 就可提供一个自定义的错误处理:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
其它异常捕获
在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常. try/catch
只能捕获同步异常.
Dart中有一个 runZoned(...)
方法,可以给执行对象指定一个Zone,在Zone中可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,也可以捕获所有未处理的异常:
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
})
zoneValues
Zone 的私有数据,可以通过实例zone[key]
获取,可以理解为每个“沙箱”的私有数据zoneSpecification
Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误等:
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
// 拦截print 蜀西湖
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, "Interceptor: $line");
},
// 拦截未处理的异步错误
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
Object error, StackTrace stackTrace) {
parent.print(zone, '${error.toString()} $stackTrace');
},
),
);
最终捕获代码
结合上面的 FlutterError.onError
和 runZoned
, 我们就可以捕获我们Flutter应用错误了并进行上报了,最终代码如下:
void collectLog(String line){
... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
... //上报错误和日志逻辑
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
...// 构建错误信息
}
void main() {
var onError = FlutterError.onError; //先将 onerror 保存起来
FlutterError.onError = (FlutterErrorDetails details) {
onError?.call(details); //调用默认的onError
reportErrorAndLog(details); //上报
};
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
// 拦截print 蜀西湖
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line);
parent.print(zone, "Interceptor: $line");
},
// 拦截未处理的异步错误
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
Object error, StackTrace stackTrace) {
reportErrorAndLog(details);
parent.print(zone, '${error.toString()} $stackTrace');
},
),
);
}
- 作者: 杜文 wendux
- 原文: https://book.flutterchina.club
- 本文仅用于本人学习和存档, 文章所有权归作者 杜文 wendux 所有, 如有侵权请发送邮件至 me#alanwei.com(#换成@) 删除此文.