StatefulWidget
是 Flutter 默认的状态管理方式,它只适合一些功能单一的小组件的内部状态管理,一旦组件中状态多了起来,页面稍微复杂一点,setState
就是高射炮打蚊子,只想更改页面中很小的一部分,整个页面却都被重新 build 了,随之带来的就是页面卡顿。而用 Provider
就可以解决这种问题,有了它,大多数情况下我们都可以不使用 StatefulWidget
,所以本文将记录 Provider
在我实际项目中的应用。
使用 ChangeNotifierProvider.value 管理状态
一开始看了官方文档和一些资料,我发现 Provider 类型是真的花里胡哨,我人直接给看晕了,有 Provider
、ListenableProvider
、StreamProvider
等等,但其实我发现在我的项目中(一个视频 App),只需要使用 ChangeNotifierProvider
就可以满足日常开发中遇到的各种需求,下面先讲讲如何使用。
创建模型类
模型类,无论是前端还是后端,都有这种类似的概念,例如在 Vue
中,数据会放在 VueX
中进行管理,在 Laravel
中,数据会放在 Eloquent
中进行管理,在 Provider
中,我们同样需要创建一个模型类来管理数据。
首先通过混入 ChangeNotifier
抽象类,让模型类具有管理收听者的能力:
class HomeModel with ChangeNotifier {
var opacity = .5;
void setOpacity(double value) {
opacity = value;
notifyListeners();
}
}
ChangeNotifier
继承自 Listenable
类,实现了addListener
、removeListener
等操作,这是一个典型的观察者模式,它可以自动管理收听者及通知收听者。
上面的代码中,模型类 HomeModel
对外暴露了一个数据 opacity
,我们在构建 Widget
时可以使用该数据,并且可以调用 setOpacity
方法更新该数据,并由 notifyListeners
通知 Widget
刷新状态。
创建 Provider
首先创建一个 HomeModel
的实例 model
,然后将 model
传递给 ChangeNotifierProvider.value
,完成 Provider
的创建:
final model = HomeModel();
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ChangeNotifierProvider.value(
value: model,
child: Opacity(
opacity: model.opacity,
child: Container(color: Colors.blue)
),
),
),
);
}
上面的代码中 Opacity
组件使用了 HomeModel
中的 opacity
值,最终的构建效果是一个半透明的蓝色容器。
使用 Selector 更新数据并刷新 UI
Consumer
及 Selector
组件都可以与模型类建立依赖关系,当我们在模型类中调用 notifyListeners
时,便会通知到 Consumer
及 Selector
进行更新。
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
不过它们之间当然是有区别的,Selector
组件可以只依赖模型中的具体某个数据,仅当依赖的那个数据发生变化时才会重新构建,而 Consumer
则是一把梭,整个模型类只要有任何数据发生改变,就会刷新子组件,这就有点 setState
内味儿了,使用 Provider
本来就是为了优化性能的,减少构建范围的,我在项目中没有用过该组件。
使用 ChangeNotifierProvider 销毁状态
ChangeNotifierProvider
除了 .value
命名构造函数以外,还可以使用默认的构造函数进行创建,例如:
final model = HomeModel();
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ChangeNotifierProvider(
create: (_) => model,
child: Opacity(
opacity: model.opacity,
child: Container(color: Colors.blue)
),
),
),
);
}
默认构造函数和 .value
命名构造函数有什么区别?
经过我的实践,我发现 .value
创建的 Provider
不会在页面销毁时调用模型类的 dispose
方法,而使用默认的构造函数创建的 Provider
则会在页面销毁时自动调用模型类的 dispose
方法,例如:
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoModel extends ChangeNotifier {
VideoModel() {
_initController();
}
var duration = const Duration(milliseconds: 1);
VideoPlayerController controller;
void _handleVideoChange() {
// 播放器状态改变时的回调处理
}
Future<void> _initController() async {
videoFile = await pickedVideo.file;
controller = VideoPlayerController.file(videoFile)
..initialize().then((e) {
duration = controller.value.duration;
notifyListeners();
})
..addListener(_handleVideoChange);
}
// 页面销毁
void dispose() {
controller.dispose();
controller.removeListener(_handleVideoChange);
super.dispose();
}
}
上面的 VideoModel
模型类,在初始化时创建了 VideoController
,并添加了 controller
的侦听者,当退出该页面时,我需要销毁 controller
并释放资源,默认的构造函数就非常适合干这个:
class VideoPage extends StatelessWidget {
final model = VideoModel();
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ChangeNotifierProvider<VideoModel>(
create: (_) => model,
child: body,
),
),
);
}
}
模型类的 dispose
方法还是挺有用的,由于我使用 Provider
,大多数页面不会使用 StatefulWidget
,而 StatelessWidget
不带有 dispose
钩子,这种情况就非常适合使用默认的构造函数来创建 Provider
,而非 .value
构造函数。
这里有个知识点,dispose
钩子是在路由 pop
完成后才会执行,例如,当前页面 pop
回上一页后,上一页会重新执行 build
函数,执行完 build
函数后才会执行 dispose
。
