零膠水程式碼。無串流反應性。純 Hook 組合。

如果你曾使用 flutter_hooks 建置 Flutter 應用程式,你應該了解基於 hook 的組合有多麼強大。透過以宣告式 hooks(如 useStateuseMemoizeduseEffect)取代 boilerplate StatefulWidget 生命週期,Flutter Hooks 讓 widget 程式碼變得格外簡潔。

然而,當開發者嘗試將傳統的 flutter_bloc 橋接至 HookWidget 時,很快就會遇到摩擦。因為傳統 BLoC 狀態會透過 Stream 管道以非同步方式發布,因此要消費狀態或對副作用做出反應,需要:

  1. HookWidgetbuild() 方法中巢狀使用沉重的 widget 包裝器(BlocBuilderBlocListenerBlocSelector)。
  2. 依賴第三方專用膠水程式碼套件(如 flutter_hooks_bloc),將 BLoC 串流包裝成自訂 hooks,如 useBlocuseBlocBuilderuseBlocListener

有了 BlocSignal,整個典範就徹底改變了。


💡 核心架構洞察

與傳統 BLoC 不同,傳統 BLoC 的狀態更新會透過 microtask 事件佇列以非同步方式串流,而 BlocSignal 則建立在 Rody Davis 的 signals.dart 基本型別之上。

BlocSignal 中:

  • bloc.state 原生就是一個 ReadonlySignal<StateType>
  • 狀態轉換會在發出的同一幀中同步傳播。
  • 使用 == 等式自動對相同狀態進行去重複。

因為 bloc.state 已經是原生的 ReadonlySignal,所以 BlocSignal 不需要 自訂的 flutter_hooks_bloc_signals 轉接套件!

取而代之,任何 HookWidget 都可以直接使用官方 signals_hooks 套件來消費、衍生或對 BlocSignal 狀態做出反應。


🪝 主要功能與模式

讓我帶你了解 BlocSignalsignals_hooks 如何簡化 HookWidget 實作中的狀態管理。

1. 直接狀態消費(useSignalValue / useWatch

在傳統 BLoC 搭配 Flutter Hooks 的情境中,訂閱 BLoC 需要 useBlocBuilder(bloc) hook 或巢狀 BlocBuilder widget。

有了 BlocSignal + signals_hooksbloc.state 就是一個 signal。你可以使用 useSignalValue(bloc.state)useWatch(bloc.state) 來讀取並訂閱它:

final count = useSignalValue(bloc.state);

進入全螢幕模式 離開全螢幕模式

每當 bloc.emit() 更新狀態時,useSignalValue 就會在 hook element 上註冊依賴關係並觸發同步重建。

2. 無串流副作用(useSignalEffect

在傳統 BLoC 中,執行一次性副作用(例如顯示 SnackBar、觸發導航或記錄分析)需要將 UI 包裝在 BlocListener 中,或呼叫 useBlocListener

因為 BlocSignal 更新是同步的,你可以使用 useSignalEffect 撰寫內嵌反應式副作用:

useSignalEffect(() {
  // 在回呼內讀取 `bloc.stateValue`(或 `bloc.state.value`)
  // 會自動將 `bloc.state` 註冊為反應式依賴關係!
  if (bloc.stateValue > 10) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已達到門檻!')),
    );
  }
});

進入全螢幕模式 離開全螢幕模式

不需要 widget 樹巢狀結構。效果回呼會在轉換時同步執行,並在 HookWidget 卸載時自動取消訂閱。

💡 小提示BlocSignalBase 提供方便的 .stateValue getter(直接委派至 bloc.state.value)。你可以在 useSignalEffectuseComputed 或像 emit(stateValue + 1) 這類事件處理程式中使用 bloc.stateValue,以獲得乾淨、簡潔的狀態存取!

3. 細粒度衍生選擇器(useComputed

在傳統 BLoC 中過濾重建需要設定 BlocSelector<MyBloc, MyState, SelectedType> 或呼叫 useBlocSelector

有了 BlocSignal + signals_hooks,細粒度選擇器邏輯可以使用 useComputed 乾淨地撰寫:

final isEven = useComputed(() => bloc.stateValue.isEven);

進入全螢幕模式 離開全螢幕模式

useComputed 會建立一個內嵌衍生 signal。因為 signals 會對相同輸出進行去重複,所以你的 widget 只會在 count.isEventruefalse 之間切換時重建——避免在 count 從 2 遞增至 4 時產生不必要的重建。

4. 生命週期清理與範圍安全

在典型的正式應用程式中,BLoCs 是透過 BlocSignalProvider 在 widget 樹上游提供。當在 HookWidget 中消費提供的 BLoC 時,不需要任何建立或清理的 boilerplate

// 透過 BlocSignalProvider 在上游提供 — Provider 會自動處理生命週期與 close()!
final bloc = context.read<CounterBloc>();

進入全螢幕模式 離開全螢幕模式

Widget 本地 BLoC 實例

如果 HookWidget 碰巧擁有自己的本地 BLoC,你可以使用標準 Flutter Hooks 原語來實例化並處置它:

// 為 widget 的生命週期建立一次,並在卸載時關閉
final bloc = useMemoized(() => CounterBloc());
useEffect(() => bloc.close, [bloc]);

進入全螢幕模式 離開全螢幕模式

💡 快速提示:如果你的應用程式使用許多 widget 本地 BLoC,你可以在程式碼庫中定義一個簡單的三行輔助函式:

B useCreateBloc<B extends BlocSignalBase>(B Function() builder) {
  final bloc = useMemoized(builder);
  useEffect(() => bloc.close, [bloc]);
  return bloc;
}

這讓你可以用單行本地實例化(final bloc = useCreateBloc(() => CounterBloc());),而不需要額外的第三方套件!


💻 並排比較

讓我們比較傳統的 flutter_hooks_bloc 與現代的 BlocSignal + signals_hooks

❌ 之前:傳統 flutter_hooks_bloc 膠水程式碼

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks_bloc/flutter_hooks_bloc.dart';

class LegacyCounterView extends HookWidget {
  const LegacyCounterView({super.key});

  @override
  Widget build(BuildContext context) {
    // 需要專用的第三方 useBloc hook
    final bloc = useBloc<CounterBloc, int>();

    // 需要專用 hook 進行狀態重建
    final count = useBlocBuilder(bloc);

    // 需要專用 hook 處理副作用
    useBlocListener<CounterBloc, int>(bloc, (context, state) {
      if (state > 10) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('計數值很高!')),
        );
      }
    });

    return Scaffold(
      body: Center(child: Text('計數: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(Increment()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

進入全螢幕模式 離開全螢幕模式

✅ 之後:現代 BlocSignal + signals_hooks

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:signals_hooks/signals_hooks.dart';
import 'package:bloc_signals/bloc_signals.dart';

class ModernCounterView extends HookWidget {
  const ModernCounterView({super.key});

  @override
  Widget build(BuildContext context) {
    // 1. 為 widget 的整個生命週期 memoize BLoC,並在卸載時 teardown
    final bloc = useMemoized(() => CounterBloc());
    useEffect(() => bloc.close, [bloc]);

    // 2. 直接將 BLoC 狀態作為 signal 讀取與監看
    final count = useSignalValue(bloc.state);

    // 2. 內嵌反應式副作用(不需要 listener widget 包裝器!)
    useSignalEffect(() {
      if (bloc.stateValue > 10) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('計數值很高!')),
        );
      }
    });

    // 3. 內嵌細粒度 computed 選擇器(不需要 selector widget!)
    final isEven = useComputed(() => bloc.stateValue.isEven);

    return Scaffold(
      body: Center(
        child: Text('計數: $count (偶數: ${isEven.value})'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(Increment()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

進入全螢幕模式 離開全螢幕模式


📊 架構對照表

模式/目標 傳統 flutter_hooks_bloc 現代 BlocSignal + signals_hooks 主要優勢
存取 BLoC useBloc<B, S>() context.read<MyBloc>()useMemoized 標準 provider 讀取或 hook;零自訂 BLoC 生命週期套件
狀態重建 useBlocBuilder(bloc) useSignalValue(bloc.state) 原生 signal 訂閱;適用於任何 ReadonlySignal<S>
副作用 useBlocListener(bloc, fn) useSignalEffect(() => ...) 內嵌反應式效果;零 widget 樹包裝器
狀態選擇 useBlocSelector(bloc, fn) useComputed(() => ...) 帶有自動 == 等式過濾的功能式 signal 衍生
膠水套件 flutter_hooks_bloc (僅 signals_hooks 不需要自訂轉接套件

🎯 結論

透過將 BLoC 模式建立在同步、反應式 signals 之上,BlocSignal 將 Flutter 兩個最強大的狀態管理典範和諧地結合在一起:BLoC 可預測的事件驅動架構,以及 Flutter Hooks 乾淨、函數式的組合。

你不再需要專用的膠水程式碼轉接套件或深層 widget 樹巢狀結構,就能在 HookWidget 中使用 BLoC。有了 signals_hooks,你的 BLoC 狀態、computed 選擇器,以及副作用都能自然地融入標準 hook 工作流程。

🔗 探索更多