關於 Hooks(鉤子)
本頁介紹了什麼是 Hooks 以及它們與 Riverpod 的關係。
"Hooks" 是獨立於 Riverpod 的單獨包中常見的實用程式:flutter_hooks。
雖然 flutter_hooks 是一個完全獨立的包,
並且與 Riverpod 沒有任何關係(至少沒有直接關係),
但通常將 Riverpod 和 flutter_hooks 配對在一起。
你應該使用 hooks 嗎?
Hooks 是一個強大的工具,但並不適合所有人。
如果您是 Riverpod 的新手,您可能應該避免使用 hooks。
雖然 hooks 很有用,但對於 Riverpod 來說並不是必需的。
您不應該為了 Riverpod 開始使用 hooks。
相反,您開始使用 hooks,是因為您想使用 hooks。
使用 hooks 是一種權衡。它們非常適合生成健壯且可重用的程式碼, 但它們也是一個需要學習的新概念,一開始可能會令人困惑。 Hooks 不是 Flutter 的核心概念。因此,它們在 Flutter/Dart 中會感覺格格不入。
什麼是 Hooks?
Hooks 是小部件內部使用的函式。它們被設計為 StatefulWidget 的替代品, 以使邏輯更加可重用和可組合。
Hooks 是來自 React 的一個概念,flutter_hooks
只是 React 實現到 Flutter 的一個埠。
因此,是的,hooks 在 Flutter 中可能感覺有點不合適。理想情況下,
未來我們會有一個專門為 Flutter 設計的 Hooks 解決問題的解決方案。
如果 Riverpod 的提供者程式用於“全域性”應用程式狀態,則 Hooks 用於本地小部件狀態。
Hooks 通常用於處理有狀態的 UI 物件,例如 TextEditingController、
AnimationController。
它們還可以作為“構建器”模式的替代品,用不涉及“巢狀”的替代方案替換諸如
FutureBuilder/TweenAnimatedBuilder
之類的小部件,從而大大提高可讀性。
一般來說,鉤子有助於:
- 表單
- 動畫
- 對使用者事件做出反應
- ……
例如,我們可以使用鉤子手動實現淡入動畫,其中小部件開始不可見並慢慢出現。
如果我們使用 StatefulWidget,程式碼將如下所示:
class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
State<FadeIn> createState() => _FadeInState();
}
class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
void initState() {
super.initState();
animationController.forward();
}
void dispose() {
animationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}
使用 hooks,相當於:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
// 建立一個 AnimationController。
// 解除安裝 widget 時,控制器將自動處置。
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
// useEffect 相當於 initState + didUpdateWidget + dispose。
// 傳給 useEffect 的回撥會在第一次呼叫鉤子時執行,
// 然後每當作為第二個引數傳遞的列表發生變化時也會執行。
// 由於我們在這裡傳遞的是一個空的常量列表,
// 因此嚴格意義上等同於 `initState`。
useEffect(() {
// 在首次呈現 widget 時啟動動畫。
animationController.forward();
// 我們可以選擇在這裡返回一些“處置”邏輯
return null;
}, const []);
// 告訴 Flutter 在動畫更新時重建此部件。
// 這相當於 AnimatedBuilder
useAnimation(animationController);
return Opacity(
opacity: animationController.value,
child: child,
);
}
}
這段程式碼中有一些有趣的事情需要注意:
- 不存在記憶體洩漏。每當小部件重建時,此程式碼都不會重新建立新的
AnimationController
, 並且在解除安裝小部件時正確處置控制器。
在同一個小部件中可以根據需要多次使用鉤子。 因此,如果我們願意,我們可以建立多個
AnimationController
:
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
final anotherController = useAnimationController(
duration: const Duration(seconds: 2),
);
...
}這會建立兩個控制器,不會產生任何負面後果。
如果我們願意,我們可以將此邏輯重構為一個單獨的可重用函式:
double useFadeIn() {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
useEffect(() {
animationController.forward();
return null;
}, const []);
useAnimation(animationController);
return animationController.value;
}然後我們可以在我們的小部件中使用這個函式,只要該小部件是 HookWidget:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
final fade = useFadeIn();
return Opacity(opacity: fade, child: child);
}
}請注意我們的
useFadeIn
函式是如何完全獨立於我們的FadeIn
小部件的。
如果我們願意,我們可以在完全不同的小部件中使用該useFadeIn
函式,並且它仍然可以工作!
hooks 的規則
Hooks 具有獨特的約束:
它們只能在擴充套件 HookWidget 的小部件的
build
方法中使用:好:
class Example extends HookWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}壞:
// 不是 HookWidget
class Example extends StatelessWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}壞:
class Example extends HookWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// _實際上_不是在 "build" 方法中,
// 而是在使用者互動生命週期中(這裡是 "按下時")。
final controller = useAnimationController();
},
child: Text('click me'),
);
}
}
它們不能在條件語句或在迴圈語句中使用。
壞:
class Example extends HookWidget {
const Example({required this.condition, super.key});
final bool condition;
Widget build(BuildContext context) {
if (condition) {
// 不應該在 "if"/"for"/... 中使用 Hooks
final controller = useAnimationController();
}
...
}
}
有關鉤子的更多資訊,請參閱 flutter_hooks。
Hooks 和 Riverpod
安裝
由於 Hooks 與 Riverpod 是獨立的,因此需要單獨安裝 Hooks。 如果你想使用它們,安裝 hooks_riverpod 是不夠的。 您仍然需要將 flutter_hooks 新增到您的依賴項中。 請參閱 入門指南 瞭解更多資訊。
用途
在某些情況下,您可能想要編寫一個同時使用 hooks 和 Riverpod 的 Widget。
但您可能已經注意到,Hooks 和 Riverpod 都提供了自己的
自定義小部件基本型別:HookWidget 和 ConsumerWidget。
但類一次只能擴充套件一個父類。
為了解決這個問題,你可以使用 hooks_riverpod 包。
該包提供了一個 HookConsumerWidget 類,
它將 HookWidget 和 ConsumerWidget 組合成一個型別。
因此,您可以繼承 HookConsumerWidget 而不是 HookWidget:
// We extend HookConsumerWidget instead of HookWidget
class Example extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// We can use both hooks and providers here
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
}
}
或者,您可以使用兩個包提供的“構建器 builder”。
例如,我們可以堅持使用 StatelessWidget
,
並同時使用 HookBuilder
和 Consumer
。
class Example extends StatelessWidget {
Widget build(BuildContext context) {
// We can use the builders provided by both packages
return Consumer(
builder: (context, ref, child) {
return HookBuilder(builder: (context) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
});
},
);
}
}
這種方法無需使用 hooks_riverpod
即可工作。只需要 flutter_riverpod
。
如果您喜歡這種方法,hooks_riverpod 透過提供 HookConsumer 來簡化它, 它是兩個構建器的組合:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
// Equivalent to using both Consumer and HookBuilder.
return HookConsumer(
builder: (context, ref, child) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
},
);
}
}