零膠水程式碼。無串流反應性。純 Hook 組合。
如果你曾使用 flutter_hooks 建置 Flutter 應用程式,你應該了解基於 hook 的組合有多麼強大。透過以宣告式 hooks(如 useState、useMemoized 和 useEffect)取代 boilerplate StatefulWidget 生命週期,Flutter Hooks 讓 widget 程式碼變得格外簡潔。
然而,當開發者嘗試將傳統的 flutter_bloc 橋接至 HookWidget 時,很快就會遇到摩擦。因為傳統 BLoC 狀態會透過 Stream 管道以非同步方式發布,因此要消費狀態或對副作用做出反應,需要:
- 在
HookWidget的build()方法中巢狀使用沉重的 widget 包裝器(BlocBuilder、BlocListener、BlocSelector)。 - 依賴第三方專用膠水程式碼套件(如
flutter_hooks_bloc),將 BLoC 串流包裝成自訂 hooks,如useBloc、useBlocBuilder和useBlocListener。
有了 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 狀態做出反應。
🪝 主要功能與模式
讓我帶你了解 BlocSignal 與 signals_hooks 如何簡化 HookWidget 實作中的狀態管理。
1. 直接狀態消費(useSignalValue / useWatch)
在傳統 BLoC 搭配 Flutter Hooks 的情境中,訂閱 BLoC 需要 useBlocBuilder(bloc) hook 或巢狀 BlocBuilder widget。
有了 BlocSignal + signals_hooks,bloc.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提供方便的.stateValuegetter(直接委派至bloc.state.value)。你可以在useSignalEffect、useComputed或像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.isEven 在 true 和 false 之間切換時重建——避免在 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 工作流程。
🔗 探索更多
- 📦
bloc_signalson pub.dev: pub.dev/packages/bloc_signals - 🪝
signals_hookson pub.dev: pub.dev/packages/signals_hooks - 🐙 GitHub 儲存庫: github.com/RandalSchwartz/BlocSignal
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.