Flutter Hooks 使用及原理

前言

Hooks,直譯過來就是"鉤子",是前端React框架加入的特性,用來分離狀態邏輯和視圖邏輯。如今這個特性並不僅侷限在於React框架中,其它前端框架也在借鑑。一樣的,咱們也能夠在Flutter中使用Hooks。Hooks對於從事Native開發的開發者可能比較陌生。但Flutter的一大優點就是綜合了H5,Native等開發平臺的優點,對Native開發者和對H5開發者都比較友好。因此經過這篇文章來介紹Hooks,但願你們能對這一特性有所瞭解。前端

爲何引入Hooks

咱們都知道在FLutter開發中的一大痛點就是業務邏輯和視圖邏輯的耦合。這一痛點也是前端各個框架都有的痛點。因此你們就像出來各類辦法來分離業務邏輯和視圖邏輯,有MVP,MVVM,React中的Mixin,高階組件(HOC),直到Hooks。Flutter中你們可能對Mixin比較熟悉,我以前寫過一篇文章介紹使用Mixin這種方式來分離業務邏輯和視圖邏輯。git

Mixin的方式在實踐中也會遇到一些限制:github

  • Mixin之間可能會互相依賴。
  • Mixin之間可能存在衝突。

所以咱們引入Hooks來看看能不能避免Mixin的這些限制。數組

Flutter Hooks使用

引入Hooks須要在pubspec.yaml加入如下內容緩存

flutter_hooks: ^0.12.0
複製代碼

Hooks函數通常以use開頭,格式爲useXXX。React定義了一些經常使用的Hooks函數,如useState,useEffect等等。bash

useState

useState咱們可能會比較經常使用,用來獲取當前Widget所須要的狀態。 咱們以Flutter的計數器例子來介紹一下如何使用Hooks,代碼以下:前端框架

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(MaterialApp(
    home: HooksExample(),
  ));
}

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('useState example'),
      ),
      body: Center(
        child: Text('Button tapped ${counter.value} times'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed:() => counter.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

複製代碼

咱們來看一下使用Hooks的計數器和原生的計數器例子源碼有什麼樣的區別。markdown

  • 首先原生的計數器由於要保存counter這個狀態,因此使用的是一個StatefulWidgetcounter保存在對應的State中。而使用Hooks改造過的計數器卻沒有使用StatefulWidget,而是繼承自HookWidget, 它實際上是一個StatelessWidget
class HooksExample extends HookWidget {
複製代碼
  • 其次咱們看到計數器的狀態counter是經過調用函數useState()獲取到的。入參0表明初始值。
final counter = useState(0);
複製代碼
  • 最後就是在點擊事件的處理上,咱們只是把計數器數值+1。並無去調用setState(),計數器就會自動刷新。
onPressed:() => counter.value++
複製代碼

可見相比於原生Flutter的模式,一樣作到了將業務邏輯和視圖邏輯分離。不須要再使用StatefulWidget,就能夠作到對狀態的訪問和維護。app

咱們也能夠在同一個Widget下引入多個Hooks:框架

final counter = useState(0);
final name = useState('張三');
final counter2 = useState(100);

複製代碼

這裏要特別注意的一點是,使用Hooks的時候不能夠在條件語句中調用useXXX,相似如下這樣的代碼要絕對避免。

if(condition) {
        useMyHook();
    }
複製代碼

熟悉Hooks的同窗可能會知道這是爲何。具體緣由我會在下面的Flutter Hooks原理小結中作以說明。

useMemoized

當你使用了BLoC或者MobX,可能須要有一個時機來建立對應的store。這時你可讓useMemoized來爲你完成這項工做。

class MyWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
 
    final store = useMemoized(() => MyStore());

    return Scaffold(...);
  }
}
複製代碼

useMemoized的入參是個函數,這個函數會返回MySotre實例。此函數在MyWidget的生命週期內只會被調用一次,獲得的MySotre實例會被緩存起來,後續再次調用useMemoized會獲得這一緩存的實例。

useEffect

在首次建立MySotre實例以後咱們通常須要作一些初始化工做,例如開始加載數據之類。有時候或許在Widget生命週期結束的時候作一些清理工做。這些事情則會由useEffect這個Hook來作。

class MyWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
 
    final store = useMemoized(() => MyStore());
    useEffect((){
        store.init(); 
        return store.dispose;
    },const []);
    return Scaffold(...);
  }
}
複製代碼

useEffect的入參函數內能夠作一些初始化的工做。若是須要在Widget生命週期結束的時候作一些清理工做,能夠返回一個負責清理的函數,好比代碼裏的store.disposeuseEffect的第二個入參是一個空數組。這樣就保證了初始化和清理函數只會在Widget生命週期開始和結束時各被調用一次。若是不傳這個參數的話則會在每次build的時候都會被調用。

其餘Hooks

除了以上這些Hooks,flutter_hooks還提供了一些能夠節省咱們代碼量的Hooks。如useAnimationController,提供AnimationController直接用而不用去操心初始化以及釋放資源的事情。還有useTabController,useTextEditingController等等,完整Hooks列表你們能夠去flutter_hooks@github查看。

自定義Hooks

當以上Hooks不能知足需求時,咱們也能夠自定義Hooks。自定義Hooks有兩種方式,一種是用函數來自定義自定義Hooks,若是需求比較複雜的話還能夠用類來自定義Hooks。

Function

這種方式通常來說就是用咱們自定義的函數來包裹組合原生的Hooks。好比對前面的計數器那個例子。咱們想在技術器增長的時候除了界面上有顯示,還須要在日誌裏打出來。那麼就能夠這樣來自定義一個Hook:

ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
  final result = useState<T>(initialData);
  useValueChanged(result.value, (_, __) {
    print(result.value);
  });
  return result;
}
複製代碼

Class

若是需求比較複雜,須要在Widget的各個生命週期作處理,則能夠用類的方式來自定義Hook。這裏咱們來自定義一個Hook,做用是做用是打印出Widget存活的時長。咱們知道Hooks都是以useXXX做爲名字的函數。因此咱們先來給出這樣的函數

Result useTimeAliveHook(BuildContext context) {
  return use(const _TimeAlive());
}
複製代碼

而後就是對應的類:

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

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

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

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

  @override
  void build(BuildContext context) {}

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

看起來是否是有一種很熟悉的感受?這不就是一個StatefulWidget嘛。對的,flutter_hooks其實就是借鑑了Flutter自身的一些機制來達到Hooks的目的。那些自帶的useState也都是這麼寫的。也就是看起來很複雜的須要StatefulWidget來完成的工做如今簡化爲一個useXXX的函數調用實際上是由於flutter_hooks幫你把事情作了。至於這背後是怎樣的一個機制,下一節咱們經過源碼來了解一下Flutter Hooks的原理。

Flutter Hooks原理

在瞭解Flutter Hooks原理以前咱們要先提幾個問題。在用Hooks改造計數器以後,就沒有了StatefulWidget。那麼計數器的狀態放在哪裏了呢?在狀態發生變化以後界面又是如何響應的呢?帶着這些問題讓咱們來探索Flutter Hooks的世界

HookWidget

首先來看HookWidget

abstract class HookWidget extends StatelessWidget {
  const HookWidget({Key key}) : super(key: key);

  @override
  _StatelessHookElement createElement() => _StatelessHookElement(this);
}

class _StatelessHookElement extends StatelessElement with HookElement {
  _StatelessHookElement(HookWidget hooks) : super(hooks);
}
複製代碼

它繼承自StatelessWidget。而且重寫了createElement。其對應的element_StatelessHookElement。而這個element只是繼承了StatelessElement而且加上了HookElementmixin。因此關鍵的東西應該都是在HookElement裏面。

HookElement

看一下HookElement:

mixin HookElement on ComponentElement {
   ...
  _Entry<HookState> _currentHookState;
  final LinkedList<_Entry<HookState>> _hooks = LinkedList();
  ...
  
    @override
  Widget build() {
    ...
    _currentHookState = _hooks.isEmpty ? null : _hooks.first;
    HookElement._currentHookElement = this;
    
    _buildCache = super.build();
    
    return _buildCache;
  }
}
複製代碼

HookElement有一個鏈表,_hooks保存着全部的HookState。還有一個指針_currentHookState指向當前的HookState。咱們看一下build函數。在每次HookElementbuild的時候都會把_currentHookState指向_hooks鏈表的第一個元素。而後才走Widgetbuild函數。也就是說,每次重建Widget的時候都會重置_currentHookState。記住這一點。

另外一個問題。咱們不是在討論Hooks嗎?那這裏的HookStateHook又是什麼關係呢?

Hook

abstract class Hook<R> {
  const Hook({this.keys});

  @protected
  HookState<R, Hook<R>> createState();
}
複製代碼

Hook這個類就很簡單了,並且看起來很像一個StatefulWidget。那麼對應的State就是HookState了。

HookState

abstract class HookState<R, T extends Hook<R>> {
  @protected
  BuildContext get context => _element;
  HookElement _element;

  T get hook => _hook;
  T _hook;

  @protected
  void initHook() {}

  @protected
  void dispose() {}

  @protected
  R build(BuildContext context);

  @protected
  void didUpdateHook(T oldHook) {}

  void reassemble() {}

  /// Equivalent of [State.setState] for [HookState]
  @protected
  void setState(VoidCallback fn) {
    fn();
    _element
      .._isOptionalRebuild = false
      ..markNeedsBuild();
  }
}
複製代碼

簡直和State一毛同樣。咱們能夠直接拿StatefulWidgetState的關係來理解HookHookState的聯繫了。有一點區別是State.build返回值是個Widget。而HookState.build的返回值則是狀態值。

另外,一個StatefulElement只會持有一個State。而HookElement則可能持有多個HookState,而且把這些HookState都放在_hooks這個鏈表裏。以下圖所示:

Hooks

use

至此咱們知道了引入Hooks之後那些狀態都放在哪裏。那麼這些狀態又是什麼時候被添加,什麼時候被使用的呢?這就要說說那些useXXX函數了。從以前咱們說的用類的方式來自定義Hook的時候瞭解到,每次調用useXXX都會新建一個Hooks實例。

Result useTimeAliveHook(BuildContext context) {
  return use(const _TimeAlive());
}
複製代碼

雖然Hook每次都是新的,可是HookState卻仍是原來那個。這個就參照StatefulWidget每次都是新的但State卻不變來理解就是了。這個useXXX最終會調用到HookElement._use:

R _use<R>(Hook<R> hook) {
    
    if (_currentHookState == null) {
      _appendHook(hook);
    } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) {
      ...
      throw StateError(''' Type mismatch between hooks: - previous hook: $previousHookType - new hook: ${hook.runtimeType} ''');
      }
    } else if (hook != _currentHookState.value.hook) {
      final previousHook = _currentHookState.value.hook;
      if (Hook.shouldPreserveState(previousHook, hook)) {
        _currentHookState.value
          .._hook = hook
          ..didUpdateHook(previousHook);
      } else {
        _needDispose ??= LinkedList();
        _needDispose.add(_Entry(_currentHookState.value));
        _currentHookState.value = _createHookState<R>(hook);
      }
    }

    final result = _currentHookState.value.build(this) as R;
    _currentHookState = _currentHookState.next;
    return result;
  }
複製代碼

這個函數也是Hooks運行的核心,須要咱們仔細去理解。

第一個分支,若是_currentHookState爲空,說明此時_hook鏈表爲空或者_currentHookState指向的是鏈表末尾元素的下一個。換而言之,當前調用use對應的HookState還不在鏈表中,那麼就調用_appendHook來將其加入鏈表

void _appendHook<R>(Hook<R> hook) {
    final result = _createHookState<R>(hook);
    _currentHookState = _Entry(result);
    _hooks.add(_currentHookState);
  }
複製代碼

在這裏咱們能夠看到_createHookState被調用,生成的HookState實例被加入鏈表。

第二個分支,若是新Hook的運行時類型與當前Hook的運行時類型不同,此時會拋出異常。

第三個分支,若是新老Hook類型一致,實例不同,那麼就要看是否保留狀態,若是保留的話就先更新Hook,而後調用HookState.didUpdateHook。這個函數由其子類實現;若是不保留狀態,那就調用_createHookState從新獲取一個狀態實例把原來的給替換掉。通常來說咱們都是想保留狀態的,這也是Flutter Hooks的默認行爲,具體判斷呢則是在函數Hook.shouldPreserveState內:

static bool shouldPreserveState(Hook hook1, Hook hook2) {
    final p1 = hook1.keys;
    final p2 = hook2.keys;

    if (p1 == p2) {
      return true;
    }

    if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) {
      return false;
    }

    final i1 = p1.iterator;
    final i2 = p2.iterator;

    while (true) {
      if (!i1.moveNext() || !i2.moveNext()) {
        return true;
      }
      if (i1.current != i2.current) {
        return false;
      }
    }
  }

複製代碼

這個函數就是在比較兩個新老Hooks的keys。若是爲空或者相等,那麼就認爲是要保留狀態,不然不保留。

分支走完了最後就是經過HookState.build拿到狀態值,而後把_currentHookState指向下一個

把整個流程串起來,就是:

  • HookElement.build首先將_currentHookState重置爲指向鏈表第一個。
  • HookElement.build調用到HookWidget.build
  • HookWidget.build內按順序調用useXXX,每調用一次useXXX就把_currentHookState指向下一個。
  • 等待下一次HookElement.build,返回第一條執行。

至此,咱們就明白了爲何前面說不能出現用條件語句包裹的useXXX

useHook1();
if(condition){
   useHook2(); 
}
useHook3();
複製代碼

像上述代碼。若是第一次調用conditiontrue。那麼此後_hooks鏈表就按順序保存着HookState1,HookState2,HookState3。那若是再次調用的時候conditionfalseuseHook2()被跳過,useHook3()被調用,但此時_currentHookState卻指向HookState2,這就出問題了。若是Hook2Hook3類型不一致則會拋異常,若是不幸它們類型一致則取到了錯誤的狀態,致使不易察覺的問題。因此咱們必定要保證每次調用useXXX都是一致的。

總結

從以上對flutter_hooks的介紹能夠看出,使用Hooks能夠大大簡化咱們的開發工做,可是要注意一點,flutter_hooks並不能處理在Widget之間傳遞狀態這種狀況,這時就須要將Hooks和Provider等狀態管理工具結合使用。

flutter_hooks將React中火爆的Hooks移植到Flutter。使廣大Flutter開發者也能體會到Hooks概念的強大。大前端的趨勢就是各個框架的技術理念相互融合,我但願經過閱讀本文也能使你們對Hooks技術在Flutter中的應用有一些瞭解。若是文中有什麼錯漏之處,抑或大夥有什麼想法,都請在評論中提出來。

相關文章
相關標籤/搜索