接着コードゼロ。ストリームレスなリアクティブ性。純粋なHookコンポジション。

flutter_hooksを使ってFlutterアプリケーションを構築したことがあるなら、hookベースのコンポジションがいかに強力かを理解しているでしょう。StatefulWidgetのライフサイクルに関する定型コードを、宣言的なuseStateuseMemoizeduseEffectなどのhookに置き換えることで、Flutter Hooksはウィジェットコードを驚くほど簡潔にします。

しかし、開発者が従来のflutter_blocHookWidgetに橋渡ししようとすると、すぐに摩擦が生じました。従来のBLoCの状態はStreamパイプラインを通じて非同期に公開されるため、状態を消費したり副作用に反応したりするには、次のいずれかが必要でした。

  1. HookWidgetbuild()メソッド内に、重いウィジェットラッパー(BlocBuilderBlocListenerBlocSelector)をネストさせる。
  2. useBlocuseBlocBuilderuseBlocListenerなどのカスタムhookにBLoCストリームをラップするflutter_hooks_blocのような、サードパーティの専用接着コードパッケージに依存する。

BlocSignalによって、そのパラダイムは完全に変わります。


💡 コアアーキテクチャの洞察

状態の更新がマイクロタスクのイベントキューを通じて非同期にストリームされる従来のBLoCとは異なり、BlocSignalはRody Davisのsignals.dartプリミティブ上に構築されています。

BlocSignalでは:

  • bloc.stateはネイティブにReadonlySignal<StateType>です。
  • 状態の遷移は、発行されたまさにそのフレームで同期的に伝播します。
  • 等しい状態は==等価性を使用して自動的に重複排除されます。

bloc.stateがすでにネイティブのReadonlySignalであるため、BlocSignalはカスタムのflutter_hooks_bloc_signalsアダプタパッケージを必要としません

代わりに、任意のHookWidgetは公式のsignals_hooksパッケージを使用して、BlocSignalの状態を箱から出してすぐに消費、派生、または反応させることができます。


🪝 主な機能とパターン

BlocSignalsignals_hooksHookWidgetの実装における状態管理をどのように簡素化するかを、順を追って説明します。

1. 直接的な状態の消費(useSignalValue / useWatch

従来のBLoCとFlutter Hooksでは、BLoCを購読するにはuseBlocBuilder(bloc) hookまたはネストされたBlocBuilderウィジェットが必要でした。

BlocSignal + signals_hooksでは、bloc.stateは単なるsignalです。useSignalValue(bloc.state)またはuseWatch(bloc.state)を使用して読み取りと購読が可能です。

final count = useSignalValue(bloc.state);

フルスクリーンモードに入る フルスクリーンモードを終了する

bloc.emit()が状態を更新するたびに、useSignalValueはhook要素に依存関係を登録し、同期的な再構築をトリガーします。

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('しきい値に達しました!')),
    );
  }
});

フルスクリーンモードに入る フルスクリーンモードを終了する

ウィジェットツリーのネストは必要ありません。効果コールバックは遷移時に同期的に実行され、HookWidgetがアンマウントされると自動的にサブスクライブを解除します。

💡 ボーナスヒントBlocSignalBaseは便利な.stateValueゲッター(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を作成します。signalは同一の出力を重複排除するため、ウィジェットはcount.isEventruefalseの間で切り替わるときにのみ再構築され、countが2から4に増加するような不要なビルドを回避します。

4. ライフサイクルのクリーンアップとスコープの安全性

典型的な本番アプリケーションでは、BLoCはBlocSignalProviderを使用してウィジェットツリーの上流で提供されます。HookWidget内で提供されたBLoCを消費する場合、作成やクリーンアップの定型コードは一切必要ありません

// BlocSignalProviderを介して上流で提供済み — プロバイダがライフタイムとclose()を自動的に処理!
final bloc = context.read<CounterBloc>();

フルスクリーンモードに入る フルスクリーンモードを終了する

ウィジェットローカルのBLoCインスタンス

HookWidgetが独自のローカルBLoCを所有する場合、標準的なFlutter Hooksプリミティブを使用してインスタンス化と破棄を行うことができます。

// ウィジェットのライフタイムに一度だけ作成し、アンマウント時にcloseする
final bloc = useMemoized(() => CounterBloc());
useEffect(() => bloc.close, [bloc]);

フルスクリーンモードに入る フルスクリーンモードを終了する

💡 クイックヒント:ウィジェットローカルのBLoCを多く使用するアプリケーションでは、コードベースに一度だけシンプルな3行のヘルパーを定義できます。

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

これにより、サードパーティのパッケージを追加することなく、1行でローカルインスタンス化(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: $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. ウィジェットのライフタイム全体にBLoCをメモ化し、アンマウント時に破棄する
    final bloc = useMemoized(() => CounterBloc());
    useEffect(() => bloc.close, [bloc]);

    // 2. BLoCの状態をsignalとして直接読み取り・監視する
    final count = useSignalValue(bloc.state);

    // 2. インラインのリアクティブな副作用(リスナーウィジェットのラッパーは不要!)
    useSignalEffect(() {
      if (bloc.stateValue > 10) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('カウントが高くなりました!')),
        );
      }
    });

    // 3. インラインの細粒度computedセレクタ(セレクタウィジェットは不要!)
    final isEven = useComputed(() => bloc.stateValue.isEven);

    return Scaffold(
      body: Center(
        child: Text('Count: $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 標準的なプロバイダ読み取りまたはhook。カスタムBLoCライフサイクルパッケージは不要
状態の再構築 useBlocBuilder(bloc) useSignalValue(bloc.state) ネイティブsignalサブスクリプション。任意のReadonlySignal<S>で動作
副作用 useBlocListener(bloc, fn) useSignalEffect(() => ...) インラインのリアクティブ効果。ウィジェットツリーのラッパーは不要
状態の選択 useBlocSelector(bloc, fn) useComputed(() => ...) 自動的な==等価性フィルタリングを備えた関数型signal派生
接着パッケージ flutter_hooks_bloc なしsignals_hooksのみ) カスタムアダプタパッケージは一切不要

🎯 結論

BLoCパターンを同期的でリアクティブなsignalに基づいて構築することで、BlocSignalはFlutterの2つの最も強力な状態管理パラダイム、すなわちBLoCの予測可能なイベント駆動型アーキテクチャとFlutter Hooksのクリーンで関数的なコンポジションを調和させます。

HookWidget内でBLoCを使用するために、専用の接着コードアダプタパッケージや深いウィジェットツリーのネストはもはや必要ありません。signals_hooksを使用すれば、BLoCの状態、computedセレクタ、副作用はすべて標準的なhookワークフローに自然に適合します。

🔗 さらに詳しく