Flutter Hook之Controller(二)

566 阅读2分钟

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('提交'),
                      ),
                    ],
                  ),
                ));
          },
        ),
      ),
    );
  }
}