🚣‍♂️

riverpod_cliを利用してriverpod v1.0.0へ移行してみた

2021/11/07に公開

はじめに

v1.0.0リリースについて

riverpodv1.0.0が、遂に 2021-11-05(JST) に公開されました。

Flutterの状態管理戦争に終止符を打つ(?)勢いのriverpodv1.0.0リリースは、下記のCHANGELOGを見ても変更箇所が多めです。

https://github.com/rrousselGit/river_pod/blob/master/packages/riverpod/CHANGELOG.md

又、v1.0.0では、後方互換のない変更が発生している為、そのままバージョンアップを行うとエラーになる可能性があり、公式からもriverpod_cliという移行ツールが公開されています。
https://pub.dev/packages/riverpod_cli

今回はこのriverpod_cliを利用して、どれほどスムーズに v1.0.0への移行が行えるか確認してみました。

方針

移行対象のアプリを作成する

今回は移行対象のサンプルアプリとして、試験的にv0.14.* 系のriverpodを使用したサンプルアプリを作成しました。今回はこちらのサンプルアプリにriverpod_cliを適用し結果を確認したいと思います。

こちら↓がv0.14.* で作成した移行対象の「入力された文字数を右上に表示する」アプリです。

https://github.com/trashfeed/flutter_notepad_hooks_riverpod_sandbox

サンプルでは、状態管理に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);

  
  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 の導入から行います。

https://pub.dev/packages/riverpod_cli#installation

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分もかからずに完了しました。
ツールのみで移行した直後のものは、こちら↓になります。

https://github.com/trashfeed/flutter_notepad_hooks_riverpod_sandbox/commit/2853c86cddada69ea0c79fd72a011178b4a157b2

検証

結果を確認する

移行結果を確認すると、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);

他にも、気になっていたuseProviderref.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と、HookConsumerref)がありエラーが発生しているようです。

 Widget _header(WidgetRef ref) {
    return HookConsumer(builder: (BuildContext context, ref, child) {

ここは _header の引数 ref が重複して不要の為、手動でパラメータ自体を削除しました。

 Widget _header() {
    return HookConsumer(builder: (BuildContext context, ref, child) {

又、pubspecflutter_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 に指定しました。

https://riverpod.dev/docs/getting_started/#installing-the-package

又、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 を使用してはいけない」と書かれています。

https://riverpod.dev/docs/concepts/reading#dont-use-refread-inside-the-build-method

ref.watch のままで、本当にリビルドがされないのか確認してみました。

Widgetのリビルド回数を確認する為には、AndroidStudioを使います。
https://flutter.dev/docs/development/tools/android-studio#show-performance-data

入力文字が変更された場合、stateが変更されリビルドが発生しますが、ref.watch利用しているWidgetのリビルド回数は増えておらず、移行後の結果には問題ありませんでした。

StateNotifierで状態処理が分離しており、ref.watch の対象(****.notifier)には変化する 状態 が含まれていない為、わざわざ ref.read or ref.watch を使い分けなくても ref.watch で統一できるようです。

さいごに

riverpod_cli 実行後にエラーは発生したものの、手動で変更したcommitは下記程度となり、非常に少ない修正でした。

https://github.com/trashfeed/flutter_notepad_hooks_riverpod_sandbox/commit/ff3ec822e7b101beaab397742104b699e25a7901

今回はサンプルアプリを作成し、移行ツールを実行したのですが、フルオートで移行できるというわけではなく、必ず自身での確認・修正が必要だと感じました。

只、riverpod公式のドキュメントは非常に丁寧にv1.0.0に合わせてアップデートされている為、事前に変更内容を把握しておくと、移行ツールの提案内容が理解しやすく、移行作業がスムーズに行えるかと思います!

Discussion