前言
Hooks,直譯過來就是"鉤子",是前端React框架加入的特性,用來分離狀態邏輯和檢視邏輯。現在這個特性並不只侷限在於React框架中,其它前端框架也在借鑑。同樣的,我們也可以在Flutter中使用Hooks。Hooks對於從事Native開發的開發者可能比較陌生。但Flutter的一大優勢就是綜合了H5,Native等開發平臺的優勢,對Native開發者和對H5開發者都比較友好。所以通過這篇文章來介紹Hooks,希望大家能對這一特性有所瞭解。
為什麼引入Hooks
我們都知道在FLutter開發中的一大痛點就是業務邏輯和檢視邏輯的耦合。這一痛點也是前端各個框架都有的痛點。所以大家就像出來各種辦法來分離業務邏輯和檢視邏輯,有MVP,MVVM,React中的Mixin,高階元件(HOC),直到Hooks。Flutter中大家可能對Mixin比較熟悉,我之前寫過一篇文章介紹使用Mixin這種方式來分離業務邏輯和檢視邏輯。
Mixin的方式在實踐中也會遇到一些限制:
- Mixin之間可能會互相依賴。
- Mixin之間可能存在衝突。
因此我們引入Hooks來看看能不能避免Mixin的這些限制。
Flutter Hooks使用
引入Hooks需要在pubspec.yaml
加入以下內容
flutter_hooks: ^0.12.0
複製程式碼
Hooks函式一般以use
開頭,格式為useXXX
。React定義了一些常用的Hooks函式,如useState
,useEffect
等等。
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的計數器和原生的計數器例子原始碼有什麼樣的區別。
- 首先原生的計數器因為要儲存
counter
這個狀態,所以使用的是一個StatefulWidget
。counter
儲存在對應的State
中。而使用Hooks改造過的計數器卻沒有使用StatefulWidget
,而是繼承自HookWidget
, 它其實是一個StatelessWidget
。
class HooksExample extends HookWidget {
複製程式碼
- 其次我們看到計數器的狀態
counter
是通過呼叫函式useState()
獲取到的。入參0代表初始值。
final counter = useState(0);
複製程式碼
- 最後就是在點選事件的處理上,我們只是把計數器數值+1。並沒有去呼叫
setState()
,計數器就會自動重新整理。
onPressed:() => counter.value++
複製程式碼
可見相比於原生Flutter的模式,同樣做到了將業務邏輯和檢視邏輯分離。不需要再使用StatefulWidget
,就可以做到對狀態的訪問和維護。
我們也可以在同一個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.dispose
。useEffect
的第二個入參是一個空陣列。這樣就保證了初始化和清理函式只會在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
並且加上了HookElement
的mixin
。所以關鍵的東西應該都是在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
函式。在每次HookElement
做build
的時候都會把_currentHookState
指向_hooks
連結串列的第一個元素。然後才走Widget
的build
函式。也就是說,每次重建Widget
的時候都會重置_currentHookState
。記住這一點。
另一個問題。我們不是在討論Hooks嗎?那這裡的HookState
和Hook
又是什麼關係呢?
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
一毛一樣。我們可以直接拿StatefulWidget
和State
的關係來理解Hook
和HookState
的聯絡了。有一點區別是State.build
返回值是個Widget
。而HookState.build
的返回值則是狀態值。
另外,一個StatefulElement
只會持有一個State
。而HookElement
則可能持有多個HookState
,並且把這些HookState
都放在_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();
複製程式碼
像上述程式碼。如果第一次呼叫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中的應用有一些瞭解。如果文中有什麼錯漏之處,抑或大夥有什麼想法,都請在評論中提出來。