riverpod_cliを利用してriverpod v1.0.0へ移行してみた
はじめに
v1.0.0リリースについて
riverpodのv1.0.0が、遂に 2021-11-05(JST) に公開されました。
Flutterの状態管理戦争に終止符を打つ(?)勢いのriverpodのv1.0.0リリースは、下記のCHANGELOGを見ても変更箇所が多めです。

又、v1.0.0では、後方互換のない変更が発生している為、そのままバージョンアップを行うとエラーになる可能性があり、公式からもriverpod_cliという移行ツールが公開されています。
今回はこのriverpod_cliを利用して、どれほどスムーズに v1.0.0への移行が行えるか確認してみました。
方針
移行対象のアプリを作成する
今回は移行対象のサンプルアプリとして、試験的にv0.14.* 系のriverpodを使用したサンプルアプリを作成しました。今回はこちらのサンプルアプリにriverpod_cliを適用し結果を確認したいと思います。
こちら↓がv0.14.* で作成した移行対象の「入力された文字数を右上に表示する」アプリです。

サンプルでは、状態管理にriverpod/state_notifier を利用し、hooksを取り入れた hooks_riverpodを使用しています。
| flutter_hooks | hooks_riverpod |
|---|---|
| ^0.16.0-nullsafety.0 | ^0.14.0+3 |
import 'package:hooks_riverpod/hooks_riverpod.dart';
final memoStateNotifierProvider =
StateNotifierProvider<MemoStateNotifier, Memo>((ref) {
return MemoStateNotifier(Memo.empty());
});
class Memo {
Memo({required this.memo, required this.wordCount});
final String memo;
final String wordCount;
factory Memo.empty() {
return Memo(memo: '', wordCount: '');
}
}
class MemoStateNotifier extends StateNotifier<Memo> {
MemoStateNotifier(Memo state) : super(state);
void update({required String memo}) {
state = Memo(
memo: memo,
wordCount: memo.isEmpty ? '' : 'count:${memo.length}');
}
}
画面側は HookWidgetを継承し、画面右上のcount:12というTextのみ再描画したい為、HookBuilderを利用しています。
class MemoPage extends HookWidget {
const MemoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(appBar: _appBar(), body: _body());
}
PreferredSizeWidget _appBar() {
return AppBar(
title: const Text('MemoPad'),
);
}
Widget _body() {
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
_header(),
_editor(),
],
),
);
}
Widget _header() {
return HookBuilder(builder: (BuildContext context) {
return Align(
alignment: Alignment.centerRight,
child: Text(
useProvider(
memoStateNotifierProvider.select((state) => state.wordCount)),
style: TextStyle(color: Theme.of(useContext()).disabledColor),
),
);
});
}
Widget _editor() {
var notifier = useProvider(memoStateNotifierProvider.notifier);
return Expanded(
child: TextField(
maxLines: null,
textAlignVertical: TextAlignVertical.top,
expands: true,
keyboardType: TextInputType.multiline,
decoration: const InputDecoration(
isDense: true,
border: OutlineInputBorder(),
),
onChanged: (memo) => notifier.update(memo: memo)),
);
}
}
実施
riverpod_cli を実行する
それでは移行を実行します。
移行方法は公式通り、 まずはriverpod_cli の導入から行います。
dart pub global activate riverpod_cli
導入自体は問題なく完了し、次に下記のコマンドよりmigrationを実行します!
riverpod migrate
移行が必要な箇所は、次のように都度、確認してくれます。
- import 'package:riverpod/all.dart';
+ import 'package:riverpod/riverpod.dart';
適用については Accept change(y = yes, n = no [default], A = yes to all, q = quit) の何れかを選択して実行していきます。
今回はサンプルアプリだった事もありますが、移行は1分もかからずに完了しました。
ツールのみで移行した直後のものは、こちら↓になります。
検証
結果を確認する
移行結果を確認すると、v1.0.0 で変更されたコマンド名を単純に置換しているだけでなく、メソッドへのパラメータ追加などややロジカルな移行も行われているようでした。
例えば、今回のサンプルアプリは、v1.0.0では削除されるuseProviderを利用していましたが、buildメソッド直下で使用しているのはなく、少し深い build() -> _body() -> _editor() という孫階層で useProvider を利用していました。
メソッド呼び出し元で ref パラメータの提案が行われ...
- _editor(),
+ _editor(ref),
呼び出し先にもWidgetRef refパラメータの追加が行われていました!
- Widget _editor() {
- var notifier = -useProvider(memoStateNotifierProvider.notifier);
+ Widget _editor(WidgetRef ref) {
+ var notifier = ref.watch(memoStateNotifierProvider.notifier);
他にも、気になっていたuseProviderはref.watchへと変更され、 HookWidget -> HookConsumerWidgetへの移行なども自動で行ってくれており、公式の変更内容にそってmigrateが行われているようです。
手動で移行する
Terminalに下記のログが出力されていました。
The following files were not able to be successfully migrated
/lib/memo_page.dart: had error when visiting a method invocation memoStateNotifierProvider.select((state) => state.wordCount)
Null check operator used on a null value
確認すると、下記のように引数名が重複している箇所(_headrの引数refと、HookConsumerのref)がありエラーが発生しているようです。
Widget _header(WidgetRef ref) {
return HookConsumer(builder: (BuildContext context, ref, child) {
ここは _header の引数 ref が重複して不要の為、手動でパラメータ自体を削除しました。
Widget _header() {
return HookConsumer(builder: (BuildContext context, ref, child) {
又、pubspec の flutter_hooks もバージョンが更新されておらず、下記のエラーが出力されていました。
Because hooks_riverpod >=1.0.0-dev.4 depends on flutter_hooks ^0.18.0 and notepad depends on flutter_hooks ^0.16.0-nullsafety.0, hooks_riverpod >=1.0.0-dev.4 is forbidden.
riverpodは、複数のパッケージ(riverpod flutter_hooks flutter_riverpod)が公開されていますが、インストールするべきパッケージの組み合わせとVersionは公式ドキュメントの下記に記載されているので、flutter_hooks のバージョンを ^0.18.0 に指定しました。

又、useProviderを使用してメソッドを実行していた下記箇所が、ref.watchに移行された点も気になりました。
- Widget _editor() {
- var notifier = useProvider(memoStateNotifierProvider.notifier);
+ Widget _editor(WidgetRef ref) {
+ var notifier = ref.watch(memoStateNotifierProvider.notifier);
状態 ではなく 処理 を参照しているので、監視(ref.watch) だと リビルドが発生してしまうのではと思ったのですが、公式ドキュメントにも DON'T use ref.read inside the build method と「build内では ref.read を使用してはいけない」と書かれています。
ref.watch のままで、本当にリビルドがされないのか確認してみました。
Widgetのリビルド回数を確認する為には、AndroidStudioを使います。
入力文字が変更された場合、stateが変更されリビルドが発生しますが、ref.watch利用しているWidgetのリビルド回数は増えておらず、移行後の結果には問題ありませんでした。

StateNotifierで状態と処理が分離しており、ref.watch の対象(****.notifier)には変化する 状態 が含まれていない為、わざわざ ref.read or ref.watch を使い分けなくても ref.watch で統一できるようです。
さいごに
riverpod_cli 実行後にエラーは発生したものの、手動で変更したcommitは下記程度となり、非常に少ない修正でした。
今回はサンプルアプリを作成し、移行ツールを実行したのですが、フルオートで移行できるというわけではなく、必ず自身での確認・修正が必要だと感じました。
只、riverpod公式のドキュメントは非常に丁寧にv1.0.0に合わせてアップデートされている為、事前に変更内容を把握しておくと、移行ツールの提案内容が理解しやすく、移行作業がスムーズに行えるかと思います!
Discussion