Hooks,直譯過來就是"鉤子",是前端React框架加入的特性,用來分離狀態邏輯和視圖邏輯。如今這個特性並不僅侷限在於React框架中,其它前端框架也在借鑑。一樣的,咱們也能夠在Flutter中使用Hooks。Hooks對於從事Native開發的開發者可能比較陌生。但Flutter的一大優點就是綜合了H5,Native等開發平臺的優點,對Native開發者和對H5開發者都比較友好。因此經過這篇文章來介紹Hooks,但願你們能對這一特性有所瞭解。前端
咱們都知道在FLutter開發中的一大痛點就是業務邏輯和視圖邏輯的耦合。這一痛點也是前端各個框架都有的痛點。因此你們就像出來各類辦法來分離業務邏輯和視圖邏輯,有MVP,MVVM,React中的Mixin,高階組件(HOC),直到Hooks。Flutter中你們可能對Mixin比較熟悉,我以前寫過一篇文章介紹使用Mixin這種方式來分離業務邏輯和視圖邏輯。git
Mixin的方式在實踐中也會遇到一些限制:github
所以咱們引入Hooks來看看能不能避免Mixin的這些限制。數組
引入Hooks須要在pubspec.yaml
加入如下內容緩存
flutter_hooks: ^0.12.0
複製代碼
Hooks函數通常以use
開頭,格式爲useXXX
。React定義了一些經常使用的Hooks函數,如useState
,useEffect
等等。bash
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
這個狀態,因此使用的是一個StatefulWidget
。counter
保存在對應的State
中。而使用Hooks改造過的計數器卻沒有使用StatefulWidget
,而是繼承自HookWidget
, 它實際上是一個StatelessWidget
。class HooksExample extends HookWidget { 複製代碼
counter
是經過調用函數useState()
獲取到的。入參0表明初始值。final counter = useState(0); 複製代碼
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原理小結中作以說明。
當你使用了BLoC或者MobX,可能須要有一個時機來建立對應的store。這時你可讓useMemoized
來爲你完成這項工做。
class MyWidget extends HookWidget { @override Widget build(BuildContext context) { final store = useMemoized(() => MyStore()); return Scaffold(...); } } 複製代碼
useMemoized
的入參是個函數,這個函數會返回MySotre
實例。此函數在MyWidget
的生命週期內只會被調用一次,獲得的MySotre
實例會被緩存起來,後續再次調用useMemoized
會獲得這一緩存的實例。
在首次建立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.dispose
。useEffect
的第二個入參是一個空數組。這樣就保證了初始化和清理函數只會在Widget
生命週期開始和結束時各被調用一次。若是不傳這個參數的話則會在每次build
的時候都會被調用。
除了以上這些Hooks,flutter_hooks
還提供了一些能夠節省咱們代碼量的Hooks。如useAnimationController
,提供AnimationController
直接用而不用去操心初始化以及釋放資源的事情。還有useTabController
,useTextEditingController
等等,完整Hooks列表你們能夠去flutter_hooks@github查看。
當以上Hooks不能知足需求時,咱們也能夠自定義Hooks。自定義Hooks有兩種方式,一種是用函數來自定義自定義Hooks,若是需求比較複雜的話還能夠用類來自定義Hooks。
這種方式通常來說就是用咱們自定義的函數來包裹組合原生的Hooks。好比對前面的計數器那個例子。咱們想在技術器增長的時候除了界面上有顯示,還須要在日誌裏打出來。那麼就能夠這樣來自定義一個Hook:
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) { final result = useState<T>(initialData); useValueChanged(result.value, (_, __) { print(result.value); }); return result; } 複製代碼
若是需求比較複雜,須要在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原理以前咱們要先提幾個問題。在用Hooks改造計數器以後,就沒有了StatefulWidget
。那麼計數器的狀態放在哪裏了呢?在狀態發生變化以後界面又是如何響應的呢?帶着這些問題讓咱們來探索Flutter Hooks的世界
首先來看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
而且加上了HookElement
的mixin
。因此關鍵的東西應該都是在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
函數。在每次HookElement
作build
的時候都會把_currentHookState
指向_hooks
鏈表的第一個元素。而後才走Widget
的build
函數。也就是說,每次重建Widget
的時候都會重置_currentHookState
。記住這一點。
另外一個問題。咱們不是在討論Hooks嗎?那這裏的HookState
和Hook
又是什麼關係呢?
abstract class Hook<R> { const Hook({this.keys}); @protected HookState<R, Hook<R>> createState(); } 複製代碼
Hook
這個類就很簡單了,並且看起來很像一個StatefulWidget
。那麼對應的State
就是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
一毛同樣。咱們能夠直接拿StatefulWidget
和State
的關係來理解Hook
和HookState
的聯繫了。有一點區別是State.build
返回值是個Widget
。而HookState.build
的返回值則是狀態值。
另外,一個StatefulElement
只會持有一個State
。而HookElement
則可能持有多個HookState
,而且把這些HookState
都放在_hooks
這個鏈表裏。以下圖所示:
至此咱們知道了引入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(); 複製代碼
像上述代碼。若是第一次調用condition
爲true
。那麼此後_hooks
鏈表就按順序保存着HookState1
,HookState2
,HookState3
。那若是再次調用的時候condition
爲false
。useHook2()
被跳過,useHook3()
被調用,但此時_currentHookState
卻指向HookState2
,這就出問題了。若是Hook2
和Hook3
類型不一致則會拋異常,若是不幸它們類型一致則取到了錯誤的狀態,致使不易察覺的問題。因此咱們必定要保證每次調用useXXX
都是一致的。
從以上對flutter_hooks的介紹能夠看出,使用Hooks能夠大大簡化咱們的開發工做,可是要注意一點,flutter_hooks並不能處理在Widget
之間傳遞狀態這種狀況,這時就須要將Hooks和Provider等狀態管理工具結合使用。
flutter_hooks將React中火爆的Hooks移植到Flutter。使廣大Flutter開發者也能體會到Hooks概念的強大。大前端的趨勢就是各個框架的技術理念相互融合,我但願經過閱讀本文也能使你們對Hooks技術在Flutter中的應用有一些瞭解。若是文中有什麼錯漏之處,抑或大夥有什麼想法,都請在評論中提出來。