跳到主要內容

關於 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 物件,例如 TextEditingControllerAnimationController
它們還可以作為“構建器”模式的替代品,用不涉及“巢狀”的替代方案替換諸如 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 都提供了自己的 自定義小部件基本型別:HookWidgetConsumerWidget
但類一次只能擴充套件一個父類。

為了解決這個問題,你可以使用 hooks_riverpod 包。 該包提供了一個 HookConsumerWidget 類, 它將 HookWidgetConsumerWidget 組合成一個型別。
因此,您可以繼承 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, 並同時使用 HookBuilderConsumer


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');
},
);
}
}