Skip to main content

第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 默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置 maintainStatefalse .
  • fullscreenDialog 表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果 fullscreenDialogtrue ,新页面将会从屏幕底部滑入(而不是水平方向).

如果想自定义路由切换动画,可以自己继承 PageRoute 来实现

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).

路由传值

ChildPage.dart
Navigator.pop(context, "我是返回值");
ParentPage.dart
// 父级页面打开 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(首页)
)

通过路由名称来打开新路由, 可以使用 NavigatorpushNamed 方法:

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 打开未注册的命令路由时, 会调用 MaterialApponGenerateRoute 属性, 利用该特性可以实现权限控制:

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 不会,它们都是单线程模型,运行机制很相似,但有区别:

run

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.onErrorrunZoned, 我们就可以捕获我们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');
},
),
);
}