Flutter开发经常会遇到一些需要固定初始化、回收的场景,比如AnimationController,看一个官方例子
class Example extends StatefulWidget {
final Duration duration;
const Example({Key? key, required this.duration})
: super(key: key);
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
AnimationController? _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
}
@override
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller!.duration = widget.duration;
}
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
像这样的controller还有很多,如TextEditingController、TabController、ScrollController、PageController、FocusNode、FocusScopeNode,他们都有一个共同点,需要在组件dispose的时候回收,如果一个页面只有一两个controller还好,如果多个页面有多个controller,重复写一样的代码没有意义。
如果使用Hook实现上面案例效果,就要简洁的多
class Example extends HookWidget {
const Example({Key? key, required this.duration})
: super(key: key);
final Duration duration;
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
controller这种类型的Hook相对简单,主要是复写dispose方法完成自动回收
class _TextEditingControllerHookState
extends HookState<TextEditingController, _TextEditingControllerHook> {
late final _controller = hook.initialValue != null
? TextEditingController.fromValue(hook.initialValue)
: TextEditingController(text: hook.initialText);
@override
TextEditingController build(BuildContext context) => _controller;
@override
void dispose() => _controller.dispose();
}
class _PageControllerHookState
extends HookState<PageController, _PageControllerHook> {
late final controller = PageController(
initialPage: hook.initialPage,
keepPage: hook.keepPage,
viewportFraction: hook.viewportFraction,
);
@override
PageController build(BuildContext context) => controller;
@override
void dispose() => controller.dispose();
}
回收的原理也很简单,就是当HookElement走到unmount这个生命周期,依次调用其所有HookState的dispose方法,各自进行回收操作,避免造成泄露
@override
void unmount() {
super.unmount();
if (_hooks.isNotEmpty) {
for (_Entry<HookState<dynamic, Hook<dynamic>>>? hook = _hooks.last;
hook != null;
hook = hook.previous) {
try {
hook.value.dispose();
} catch (exception, stack) {
...
}
}
}
}
我们以TextEditController和FocusScopeNode为例写一个小Demo
class LoginHook extends StatelessWidget {
// GlobalKey 唯一标识了一个表单 Form,在后续的表单验证步骤中,起到了关键的作用
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hook Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: HookBuilder(
builder: (context) {
final nameController = useTextEditingController();
final pswController = useTextEditingController();
// 和FocusScope配合使用管理
var focusScopeNode = useFocusScopeNode();
void onEditComplete() {
focusScopeNode.nextFocus();
}
return FocusScope(// 子控件内的 FocusNode都会被统一维护
node: focusScopeNode,
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
autofocus: true,
onEditingComplete: onEditComplete,// 编辑完成的回调,我们移动到下一个焦点
autovalidateMode: AutovalidateMode.onUserInteraction,// 用户输入的时候会自动验证
controller: nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '请输入用户名'),
validator: (value) {
if (value == null || value.isEmpty) {
// 当验证失败返回提示文字
return '用户名不能为空';
}
return null;
},
),
const SizedBox(
height: 16,
),
TextFormField(
controller: pswController,
autovalidateMode: AutovalidateMode.onUserInteraction,
onEditingComplete: onEditComplete,
decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '请输入密码'),
validator: (value) {
if (value == null || value.isEmpty) {
// 当验证失败返回提示文字
return '密码不能为空';
}
if (value.length < 6) {
return '密码强度太弱';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 表单数据验证成功 提示成功
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("${nameController.text} 登录成功")),
);
}
},
child: const Text('提交'),
),
],
),
));
},
),
),
);
}
}