最近は Claude Code を使って、Flutter アプリを一から作成してもらうことが多いのですが、AI は一つのファイルで完結しない状態管理を苦手としているように感じます。
具体的には、親からDIで受け渡されるものや、使用されなくなったタイミングで自動的に解放されるものです。
そこで、AI にとって扱いやすい状態管理パッケージを探したところ、「ReArch」に辿り着きました。
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のConsumerWidget
やhooks_riverpodのHookConsumerWidget
に相当するものです。
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
に何も指定しない、つまり未指定の場合、最初に実行された後は二度と呼ばれることがないため、StatefulWidget
のinitState
メソッドのような使い方が出来ます。
dependencies
が未指定の場合のクリーンナップはWidgetが使われなくなり破棄される時、つまりStatefulWidget
のdispose
メソッドに相当します。
class MountExampleWidget extends RearchConsumer {
Widget build(BuildContext context, WidgetHandle use) {
use.effect(() {
print('マウントされました');
return () {
print('アンマウントされます');
}
});
ReArchの肝となる機能です。
先ほどのuse.state
やuse.memo
、use.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のキーにすることで状態を管理しています。
流れとしては
- useの引数にCapsuleメソッドを渡す
- 必要になったタイミングでCapsuleメソッドが実行される
- メソッド内で呼び出された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().addPostFrameCallback
をuse.effect
の中で使います。
use.effect( () {
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) {
});
});
細々と運営しているFlutterアプリ開発者専用のコミュニティーです。
ちょいちょい知見共有などを行なっているのでお気軽にご参加ください😊
本記事は以下の環境で検証しました
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
Views: 0