水曜日, 9月 3, 2025
水曜日, 9月 3, 2025
- Advertisment -
ホームニューステックニュース【Flutter】React Hooks 風の状態管理パッケージ「ReArch」の紹介

【Flutter】React Hooks 風の状態管理パッケージ「ReArch」の紹介



最近は Claude Code を使って、Flutter アプリを一から作成してもらうことが多いのですが、AI は一つのファイルで完結しない状態管理を苦手としているように感じます。
具体的には、親からDIで受け渡されるものや、使用されなくなったタイミングで自動的に解放されるものです。

そこで、AI にとって扱いやすい状態管理パッケージを探したところ、「ReArch」に辿り着きました。

https://pub.dev/packages/flutter_rearch

https://rearch.gsconrad.com

ReArchは Flutter Hooks や React Hooks と書き方が非常に似ているため、どちらかを使ったことのある方ならすんなり習得できるかと思います。

セットアップ

flutter pub add rearch flutter_rearch

ReArchConsumer

Riverpodの ProviderScope に相当するものですね。runAppの直後にこれを設置してやることでReArchが使えるようになります。

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_rearch/flutter_rearch.dart';
import 'package:rearch/rearch.dart';

void main() {
  runApp(RearchBootstrapper(child: Myapp()));
}

RearchConsumer

RiverpodのConsumerWidgethooks_riverpodHookConsumerWidgetに相当するものです。

class MainPage extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {

StatefulRearchConsumerはありません。RearchBuilderというRearchConsumerをラップしただけのWidgetがあるので、StatefulWidget内ではそれを使います。

class MainPage extends StatefulWidget {
  
  StateMainPage> createState() => _MainPageState();
}

class _MainPageState extends StateMainPage> {
  
  Widget build(BuildContext context) {
    return RearchBuilder(builder:(context, use) {

use.state

React Hooks や Flutter Hooks の useState に相当する機能です。

Flutter Hooks と違い、本家の React Hooks のように (値, 値を更新するメソッド) がセットで返されます。

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final (count, setCount) = use.state(0);
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$count'),
        
        ElevatedButton(
          onPressed: () {
            setCount(count + 1);
          },
          child: const Text('+1'),
        ),
      ],
    );
  }
}

use.memo

React Hook の useMemo に相当する機能です。

Flutter Hooks における useMemoized に相当するほか、 useScrollControllerやuseAnimationController などの代わりにもなります。

また、dependenciesという引数が付いていて、ここに監視する変数を渡してやると、その値が変わった際に再生成されるようになります。

例えば特定の変数が変化した場合、use.memo側で再計算・再取得させたり出来ます。

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final (count, setCount) = use.state(0);
    
    final textController = use.memo(
      () => TextEditingController(text: '$count'),
      [count],
    );
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(controller: textController),
        
        ElevatedButton(
          onPressed: () {
            setCount(count + 1);
          },
          child: const Text('+1'),
        ),
      ],
    );
  }
}

use.effect

React Hooks や Flutter Hooks の useEffect に相当する機能です。use.memoは何らかの値を返しますが、use.effectは返しません。

use.memoと同様に、dependenciesに監視する変数を渡してやると、その値が変化した際に再実行されます。

また、returnでメソッドを返せるのですが、これはクリーンナップ (Clean Up) と言って、dependenciesが変化した際や、Widgetが使われなくなり破棄される際に行いたい処理を記述します。

class CountUpTimer extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final (seconds, setSeconds) = use.state(0);
    final (isRunning, setIsRunning) = use.state(false);

    use.effect(() {
      Timer? timer;

      if (isRunning) {
        timer = Timer.periodic(Duration(seconds: 1), (timer) {
          setSeconds(seconds + timer.tick);
        });
      }

      
      return () {
        
        
        if (isRunning) {
          print('タイマーストップ');
          timer?.cancel();
        }
      };
    }, [isRunning]);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('秒数: $seconds'),
        ElevatedButton(
          onPressed: () => setIsRunning(!isRunning),
          child: Text(isRunning ? '停止' : '開始'),
        ),
      ],
    );
  }
}

dependenciesに何も指定しない、つまり未指定の場合、最初に実行された後は二度と呼ばれることがないため、StatefulWidgetinitStateメソッドのような使い方が出来ます。

dependenciesが未指定の場合のクリーンナップはWidgetが使われなくなり破棄される時、つまりStatefulWidgetdisposeメソッドに相当します。

class MountExampleWidget extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {    
    use.effect(() {
      
      print('マウントされました');
      
      return () {
      	print('アンマウントされます');
      }
    });

ReArchの肝となる機能です。

先ほどのuse.stateuse.memouse.effectをbuildメソッド内で使うだけでも Flutter Hooks と同じことが出来ますが、ReArchはCapsuleを使うことでWidgetの外に持ち出すことが出来ます

Capsuleとは「CapsuleHandleという引数を一つだけ持ったメソッド」です。

最大の特長はbuildメソッド内で使えていたuse.stateやuse.memo、use.effectがそのまま使えるという点です。

なので、buildメソッド内で使用していたuse.stateも、Capsuleメソッドを作ってそこに移すだけで外に出せ、他のWidgetやモデルと共有できます。

先ほどのCounterAndButtonのcountをWidgetの外に出す

(int count, void Function(int) setCount) countCapsule(CapsuleHandle use) {
  final (count, setCount) = use.state(0);
  return (count, setCount);
}

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    
    final (count, setCount) = use(countCapsule);

これがReArchがAI駆動開発と相性が良い最大の理由で、AIがコードを書く際に詰まりにくくなります。

戻り値は何でもOK

use.stateが必ず (値, 値を更新するメソッド) のセットになるのでCapsuleメソッドも同じだと勘違いされがちですが、実は特に制限がありません。

setCountの代わりにincrementとresetを追加する

(int count, ({void Function() increment, void Function() reset})) countCapsule(
  CapsuleHandle use,
) {
  final (count, setCount) = use.state(0);
  return (
    count,
    (increment: () => setCount(count + 1), reset: () => setCount(0)),
  );
}

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final (count, action) = use(countCapsule);

    action.increment();
    action.reset();

値だけを返せばRiverpodのProviderのようになります。

(int count, Function(int) setCount) countCapsule(CapsuleHandle use) {
  final (count, setCount) = use.state(0);
  return (count, setCount);
}

int doubleCountCapsule(CapsuleHandle use) {
  final (count, _) = use(countCapsule);
  return count * 2;
}

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final (count, setCount) = use(countCapsule);
    
    final doubleCount = use(doubleCountCapsule);

逆にメソッドだけを返すことも出来ます。

(int count, Function(int) setCount) countCapsule(CapsuleHandle use) {
  final (count, setCount) = use.state(0);
  return (count, setCount);
}


void Function() resetCountCapsule(CapsuleHandle use) {
  final (_, setCount) = use(countCapsule);
  return () {
    setCount(0);
  };
}

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final countReset = use(resetCountCapsule);
    
    countReset();

さらに、制限が無いと言うことは (値, 値を更新するメソッド) の代わりに「両方を持ったクラス」にすることも可能です。

class Counter {
  final int count;
  final void Function(int) _setState;
  const Counter(this.count, this._setState);

  void increment() {
    _setState(count + 1);
  }
}

Counter counterCapsule(CapsuleHandle use) {
  final (counter, setState) = use.state(0);
  return Counter(counter, setState);
}

class CounterAndButton extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    final counter = use(counterCapsule);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('${counter.count}'),
        ElevatedButton(
          onPressed: () {
            counter.increment();
          },
          child: Text('+1'),
        ),
      ],
    );
  }
}

これを例えば、Capsuleメソッドで返すクラスを abstract interface にすれば、それをimplementsしたクラスなら何でも返せるということになります。

つまりテスト時にはモッククラスを返せるようになりますから、RiverpodのdependencyOverridesと同じことが出来ます。

上記の通り、CapsuleメソッドはRiverpodにおけるProviderに相当し、useはref.watchに相当します。

ちなみに任意のタイミングで解放させることは出来ず、全てAutoDisposeと同じ扱いです。

つまりどこからもuseで使われなくなったら自動で解放され、次にuseを使うと初期化されています。
自動解放ということはAIが苦手なのでは?と思われそうですが、watchしなくても更新できてしまうRiverpodとは違い、必ずuse(≒watch)を使う仕様なので問題が起きにくいようになっています。

最初は「何でメソッドで状態管理できるの?魔法か??」と思いましたが、流石に色々試してみると実装が何となく理解できました。
仕組みとしてはメソッドのhashCodeを利用しています。

普段メソッドは()を使って呼び出すだけの方がほとんどだと思いますが、実際はFunctionというクラスです。

つまり、他の型やクラスと同じように、hashCodeという重複しない値を持っています。

ReArchはこのhashCodeをMapのキーにすることで状態を管理しています。

流れとしては

  1. useの引数にCapsuleメソッドを渡す
  2. 必要になったタイミングでCapsuleメソッドが実行される
  3. メソッド内で呼び出されたuse.stateやuse.memoを、そのCapsuleメソッドのhashCodeをMapのキーにして状態管理

という感じです。

(上記は時々コードを読みつつしばらく使っての推測 + DeepResearchによる調査によるものなので、間違っていたらご指摘ください)

この「CapsuleメソッドのhashCodeをキーにしている」という仕様は重要な点で、ReArchで実装する際に考慮する必要が度々あります。

ここまで書くと「ReArchって Riverpod x Flutter Hooks の完全上位互換なのでは」と思われるかと思います。しかし実際には、実装難易度が高いので必ずしもそうとは限りません。

自由度が高い

ReArchの最大の特長は自由度の高さです。Riverpodが様々な機能をあらかじめ提供しているのに対し、ReArchはあえて最小限の機能を提供することで、開発者に実装の自由を委ねています。

ReArchは色々なデザインパターンを駆使すれば、恐らくRiverpodと全く同じことが出来るようになりますし、Riverpodで悩みの種だった点も解消できる可能性が高いです。

ただその分、実装や設計のセンスが必要になります。チームで開発する場合は結構な時間を議論に割くことになりそうです。

Riverpodのように機能が豊富なことは、議論の余地が少なくなるという利点があります。

一方でReArchの自由度の高さは、プロジェクトにとって最適な設計に出来る可能性を秘めています。

どちらを選ぶべきかはチームやプロジェクトによるかと思います。

ドキュメントや知見が少ない

ReArchがpub.devに登場したのは2024年の10月です。まだ一年も経っていません。

また、Riverpodは業界標準となる状態管理パッケージが決まっていなかった頃に登場したため、当時はFlutterにおける救世主のような存在でした。なので非常に活発に議論や知見の共有が行われていた記憶があります。

今はもうFlutterは成熟期に近いので、ReArchが普及するまでに時間はかかるかと思います。

Familyに相当する書き方

RiverpodにはFamilyという機能があります。

これは、基本的に一つの値を状態管理するだけのProviderに対し、追加で引数を受け取れるようにする機能です。

同じProviderであっても、引数が異なると別のProviderとして扱われるようになります。

例えばアカウント情報を状態管理するaccountProviderというNotifierProviderがあった場合、FamilyでuserIdを渡すようにすれば、userIdごとに異なるアカウント情報が管理できるようになります。

さらに、 Riverpod Generator を使えば二つ以上の引数を受け取ることも可能です。

ReArchはpub.devのトップページ(Why not Riverpod? の所)などで明確にFamilyを批判していて、代わりにFactoryパターンを使うべきだと書いています。実際に、ReArchはFactoryパターンなどを用いることでFamilyと同じことが出来ます。

が、しかし、これが中々難しい…。

Capsuleメソッドに追加で引数を持たせれば良いのでは」と最初は思いましたが、CapsuleメソッドはCapsuleHandle一つしか引数を持てません。
仮に持たせられたとしても、引数に何を渡してもメソッドのhashCodeは変わらないので、引数に応じて状態管理を分けることは出来ません。

解決策は、Capsuleメソッドを関数内関数にすることです。

ただし、関数内関数は親のメソッドが呼ばれた際に新しく作られるので、hashCodeが毎回変わってしまいます。そのため、引数をキーにするなどの方法でMapにキャッシュする必要があります。


typedef AccountCapsuleReturn = (Account, void Function(Account));
typedef AccountCapsuleCallback = AccountCapsuleReturn Function(CapsuleHandle);


final _accountCapsuleCache = String, AccountCapsuleCallback>{};

class Account {
  final String userName;
  const Account({required this.userName});
}

AccountCapsuleCallback accountCapsuleFactory({required String userId}) {
  AccountCapsuleReturn accountCapsule(CapsuleHandle use) {
    final (account, updateAccount) = use.state(
      Account(userName: 'none'),
    );
    return (account, updateAccount);
  }

  
  
  return (_accountCapsuleCache[userId] ??= accountCapsule);
}

class MyPage extends RearchConsumer {
  
  Widget build(BuildContext context, WidgetHandle use) {
    
    final (user1, updateUser1) = use(accountCapsuleFactory(userId: "user001"));
    final (user2, updateUser2) = use(accountCapsuleFactory(userId: "user002"));

ちなみに、このCapsuleメソッドがどこからも使われなくなり、解放されても_accountCapsuleCacheにメソッドは残っていますが、特に動作に問題ありません。状態管理しているのはReArch側なので、次回呼んだ際には初期化されています。

とはいえこの実装だと残ったメソッドによってメモリー消費が増えるので、本格的に管理したい場合は専用のクラスを作り、Capsuleメソッドもクラスのインスタンスのメソッドから返すなどの設計にする必要があるかと思います。

Flutter Hooks でも時々起きる現象ですが、以下のようなエラーが出ることがあります。

The following assertion was thrown building CounterAndButton(dirty, dependencies:
[CapsuleContainerProvider]):
setState() or markNeedsBuild() called during build.
This CounterAndButton widget cannot be marked as needing to build because the framework is already
in the process of building widgets. A widget can be marked as needing to be built during the build
phase only if one of its ancestors is currently building. This exception is allowed because the
framework builds parent widgets before children, which means a dirty descendant will always be
built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  CounterAndButton
The widget which was currently being built when the offending call was made was:
  CounterAndButton

Widgetが描画される前にuse.effectでCapsuleメソッドを更新した際に起こるエラーです。
修正も Flutter Hooks と同じで、WidgetsFlutterBinding.ensureInitialized().addPostFrameCallbackuse.effectの中で使います。

use.effect( () {
  WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) {
    
  });
});

細々と運営しているFlutterアプリ開発者専用のコミュニティーです。
ちょいちょい知見共有などを行なっているのでお気軽にご参加ください😊

https://discord.gg/C4bxjk56UQ

本記事は以下の環境で検証しました

rearch: 1.16.1
flutter_rearch: 1.7.2
Flutter 3.35.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 05db968908 (7 days ago) • 2025-08-25 10:21:35 -0700
Engine • hash abb725c9a5211af2a862b83f74b7eaf2652db083 (revision a8bfdfc394) (9
days ago) • 2025-08-22 23:51:12.000Z
Tools • Dart 3.9.0 • DevTools 2.48.0



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -