flutter_hooks

React hooks 的 Flutter 版本
授权协议 MIT
开发语言 Dart
所属分类 手机/移动开发、 手机开发工具
软件类型 开源软件
地区 不详
投 递 者 燕凯旋
操作系统 Android
开源组织
适用人群 未知
 软件概览

基于 React hooks 实现的 Flutter hooks。Flutter hooks 用于管理 Flutter Widgeet。有利于增加小部件之间的代码共享,可以代替 StatefulWidget 。

Flutter Hooks

A flutter implementation of React hooks: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889

Hooks are a new kind of object that manages a Widget life-cycles. They exist for one reason: increase the code sharing between widgets and as a complete replacement for StatefulWidget.

Motivation

StatefulWidget suffer from a big problem: it is very difficult to reuse the logic of say initState or dispose. An obvious example is AnimationController:

class Example extends StatefulWidget {
  final Duration duration;

  const Example({Key key, @required this.duration})
      : assert(duration != null),
        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() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

All widgets that desire to use an AnimationController will have to reimplement almost of all this from scratch, which is of course undesired.

Dart mixins can partially solve this issue, but they suffer from other problems:

  • A given mixin can only be used once per class.

  • Mixins and the class shares the same object. This means that if two mixins define a variable under the same name, the end result may vary between compilation fail to unknown behavior.


This library propose a third solution:

class Example extends HookWidget {
  final Duration duration;

  const Example({Key key, @required this.duration})
      : assert(duration != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: duration);
    return Container();
  }
}

This code is strictly equivalent to the previous example. It still disposes the AnimationController and still updates its duration when Example.duration changes. But you're probably thinking:

Where did all the logic go?

That logic moved into useAnimationController, a function included directly in this library (see https://github.com/rrousselGit/flutter_hooks#existing-hooks). It is what we call a Hook.

Hooks are a new kind of objects with some specificities:

  • They can only be used in the build method of a HookWidget.

  • The same hook is reusable an infinite number of times The following code defines two independent AnimationController, and they are correctly preserved when the widget rebuild.

Widget build(BuildContext context) {
  final controller = useAnimationController();
  final controller2 = useAnimationController();
  return Container();
}
  • Hooks are entirely independent of each other and from the widget. Which means they can easily be extracted into a package and published on pub for others to use.

Principle

Similarily to State, hooks are stored on the Element of a Widget. But instead of having one State, the Element stores a List<Hook>. Then to use a Hook, one must call Hook.use.

The hook returned by use is based on the number of times it has been called. The first call returns the first hook; the second call returns the second hook, the third returns the third hook, ...

If this is still unclear, a naive implementation of hooks is the following:

class HookElement extends Element {
  List<HookState> _hooks;
  int _hookIndex;

  T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this);

  @override
  performRebuild() {
    _hookIndex = 0;
    super.performRebuild();
  }
}

For more explanation of how they are implemented, here's a great article about how they did it in React: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

Rules

Due to hooks being obtained from their index, there are some rules that must be respected:

DO call use unconditionally

Widget build(BuildContext context) {
  Hook.use(MyHook());
  // ....
}

DON'T wrap use into a condition

Widget build(BuildContext context) {
  if (condition) {
    Hook.use(MyHook());
  }
  // ....
}

DO always call all the hooks:

Widget build(BuildContext context) {
  Hook.use(Hook1());
  Hook.use(Hook2());
  // ....
}

DON'T aborts build method before all hooks have been called:

Widget build(BuildContext context) {
  Hook.use(Hook1());
  if (condition) {
    return Container();
  }
  Hook.use(Hook2());
  // ....
}

About hot-reload

Since hooks are obtained from their index, one may think that hot-reload while refactoring will break the application.

But worry not, HookWidget overrides the default hot-reload behavior to work with hooks. Still, there are some situations in which the state of a Hook may get reset.

Consider the following list of hooks:

Hook.use(HookA());
Hook.use(HookB(0));
Hook.use(HookC(0));

Then consider that after a hot-reload, we edited the parameter of HookB:

Hook.use(HookA());
Hook.use(HookB(42));
Hook.use(HookC());

Here everything works fine; all hooks keep their states.

Now consider that we removed HookB. We now have:

Hook.use(HookA());
Hook.use(HookC());

In this situation, HookA keeps its state but HookC gets a hard reset. This happens because when a refactoring is done, all hooks after the first line impacted are disposed. Since HookC was placed after HookB, is got disposed.

How to use

There are two ways to create a hook:

  • A function

Functions is by far the most common way to write a hook. Thanks to hooks being composable by nature, a function will be able to combine other hooks to create a custom hook. By convention these functions will be prefixed by use.

The following defines a custom hook that creates a variable and logs its value on the console whenever the value changes:

ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
  final result = useState<T>(initialData);
  useValueChanged(result.value, (_, __) {
    print(result.value);
  });
  return result;
}
  • A class

When a hook becomes too complex, it is possible to convert it into a class that extends Hook, which can then be used using Hook.use. As a class, the hook will look very similar to a State and have access to life-cycles and methods such as initHook, dispose and setState. It is usually a good practice to hide the class under a function as such:

Result useMyHook(BuildContext context) {
  return Hook.use(_MyHook());
}

The following defines a hook that prints the time a State has been alive.

class _TimeAlive<T> extends Hook<void> {
  const _TimeAlive();

  @override
  _TimeAliveState<T> createState() => _TimeAliveState<T>();
}

class _TimeAliveState<T> extends HookState<void, _TimeAlive<T>> {
  DateTime start;

  @override
  void initHook() {
    super.initHook();
    start = DateTime.now();
  }

  @override
  void build(BuildContext context) {
    // this hook doesn't create anything nor uses other hooks
  }

  @override
  void dispose() {
    print(DateTime.now().difference(start));
    super.dispose();
  }
}

Existing hooks

Flutter_hooks comes with a list of reusable hooks already provided. They are static methods free to use that includes:

  • useEffect

Useful to trigger side effects in a widget and dispose objects. It takes a callback and calls it immediately. That callback may optionally return a function, which will be called when the widget is disposed.

By default, the callback is called on every build, but it is possible to override that behavior by passing a list of objects as the second parameter. The callback will then be called only when something inside the list has changed.

The following call to useEffect subscribes to a Stream and cancel the subscription when the widget is disposed:

Stream stream;
useEffect(() {
    final subscribtion = stream.listen(print);
    // This will cancel the subscription when the widget is disposed
    // or if the callback is called again.
    return subscription.cancel;
  },
  // when the stream change, useEffect will call the callback again.
  [stream],
);
  • useState

Defines + watch a variable and whenever the value change, calls setState.

The following code uses useState to make a counter application:

class Counter extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);

    return GestureDetector(
      // automatically triggers a rebuild of Counter widget
      onTap: () => counter.value++,
      child: Text(counter.value.toString()),
    );
  }
}
  • useReducer

An alternative to useState for more complex states.

useReducer manages an read only state that can be updated by dispatching actions which are interpreted by a Reducer.

The following makes a counter app with both a "+1" and "-1" button:

class Counter extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useReducer(_counterReducer, initialState: 0);

    return Column(
      children: <Widget>[
        Text(counter.state.toString()),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () => counter.dispatch('increment'),
        ),
        IconButton(
          icon: const Icon(Icons.remove),
          onPressed: () => counter.dispatch('decrement'),
        ),
      ],
    );
  }

  int _counterReducer(int state, String action) {
    switch (action) {
      case 'increment':
        return state + 1;
      case 'decrement':
        return state - 1;
      default:
        return state;
    }
  }
}
  • useMemoized

Takes a callback, calls it synchronously and returns its result. The result is then stored to that subsequent calls will return the same result without calling the callback.

By default, the callback is called only on the first build. But it is optionally possible to specify a list of objects as the second parameter. The callback will then be called again whenever something inside the list has changed.

The following sample make an http call and return the created Future. And if userId changes, a new call will be made:

String userId;
final Future<http.Response> response = useMemoized(() {
  return http.get('someUrl/$userId');
}, [userId]);
  • useValueChanged

Takes a value and a callback, and call the callback whenever the value changed. The callback can optionally return an object, which will be stored and returned as the result of useValueChanged.

The following example implicitly starts a tween animation whenever color changes:

AnimationController controller;
Color color;

final colorTween = useValueChanged(
    color,
    (Color oldColor, Animation<Color> oldAnimation) {
      return ColorTween(
        begin: oldAnimation?.value ?? oldColor,
        end: color,
      ).animate(controller..forward(from: 0));
    },
  ) ??
  AlwaysStoppedAnimation(color);
  • useAnimationController, useStreamController, useSingleTickerProvider

A set of hooks that handles the whole life-cycle of an object. These hooks will take care of both creating, disposing and updating the object.

They are the equivalent of both initState, dispose and didUpdateWidget for that specific object.

Duration duration;
AnimationController controller = useAnimationController(
  // duration is automatically updates when the widget is rebuilt with a different `duration`
  duration: duration,
);
  • useStream, useFuture, useAnimation, useValueListenable, useListenable

A set of hooks that subscribes to an object and calls setState accordingly.

Stream<int> stream;
// automatically rebuild the widget when a new value is pushed to the stream
AsyncSnapshot<int> snapshot = useStream(stream);
  • 原文链接 前言 随着Flutter 2.0的发布,越来越多的公司和个人都在尝试使用Flutter来编写性能优异、跨平台、渲染一致性好的App。当然,在追求效率的同时,也不要忘了给你的代码添加更多严谨的限制,来保证代码的质量。今天我给大家推荐一个插件,用于给提交代码到Github或者GitLab时,在本地使用dart语言,即可限制提交代码的质量。 Git Hooks Git Hooks是什么,Hoo

  • | 作者:Ryan Edge | 原文链接:medium.com/flutter-com… | 公众号链接:mp.weixin.qq.com/s/5ghu1YkFw… 我之前去了 DartConf 2018,最初并没有对 Flutter 的有过多的期待,只是想去了解一下。但在离开时,我非常期望看到它在移动平台之外能蓬勃发展并更加成熟。看看 React Native 在过去几年里的发展,我坚信 Fl

  • flutter 问题集合 一、dio框架问题 1.报错: 500 问题具体描述: AS断点发现值已经传到了后端,后端断点(Spring)发现key "B"的值为空字符串"",flutter传参的数据结构为: {"A":"111", "B":["222","333"] } 后端取值发现 "B"的值取不到,报错显示的是"B[]"的参数问题 。 解决方式: 方式1: 后端,经试验后端取值key 从

  • 报错信息: I/flutter: FlutterBoost#onEvent backPressedCallback I/flutter: FlutterBoost#performBackPressed E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: Bad state: No element

  • React 最新正式版已经支持了 Hooks API,先快速过一下新的 API 和大概的用法。 // useState,简单粗暴,setState可以直接修改整个state const [state,setState] = useState(value); // useEffect,支持生命周期 useEffect(()=>{ // sub return ()=>{

  • Flutter命令行 flutter build ipa 打包报错 cannot load such file — sqlite3 (LoadError) xcode kernel_require rb require usr/bin/ipatool:24 The data couldn’t be read because it isn’t in the correct format   [+14

 相关资料
  • 我有这个错误:

  • 我已经用flutter在play store上发布了一个应用程序,现在我想上传一个新版本的应用程序。我正在尝试使用以下命令更改版本代码: flutter build apk--Build-Name=1.0.2--Build-Number=3 或者像这样更改local.properties

  • 每当我尝试从终端运行任何应用程序或flutter命令时,我都会遇到问题,显示:

  • 我正在制作一个有15个屏幕的非常简单的应用程序。当我运行这个命令时,快跑,放开。我发布的apk大小是26.2mb,非常大。 我的颤动医生: 我的pubspec.yaml文件包含以下依赖项: 这些文件占用了更多的空间。我们有没有办法减少发布apk的大小。对于这样的应用程序,其大小非常大。在我之前的Flatter sdk版本1.17中,30屏应用程序的发布apk大小不超过15 mb。

  • 我得到这个问题与flutter pub get,试图更新dart sdk,重新启动pc添加dart sdk位置回到环境变量,删除。git文件夹。 在计算器中运行“flutter pub get”... 当前Dart SDK版本为2.13.4。 颤医生: 飞镖版:

  • 问题内容: 如何设置Flutter应用程序的版本名称和版本代码,而不必进入Android和iOS设置? 在我的pubspec.yaml中 但我看不到内部编号的地方。 问题答案: 您可以在 pubspec.yaml 中的同一位置更新版本名称和版本代码号。只需将它们与标志分开即可。例如: 这表示 版本名称是 版本代码是 在新项目的文档中对此进行了描述(但是,如果您正在处理旧项目,则可能已将其删除):