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中的應用有一些瞭解。若是文中有什麼錯漏之處,抑或大夥有什麼想法,都請在評論中提出來。