推奨状態管理 - 1m-llc/Flutter-KtoK GitHub Wiki

Riverpod + Flutter Hooks + freezed + StateNotifier

こちらの状態管理パターンについて整理してます。

Riverpod 公式サイト

2021/12/28時点

  • hooks_riverpod 1.0.3
  • flutter_hooks 0.18.1
  • freezed 1.1.0




Riverpod

  • hooks_reverpod v1.0.0からの変更点
対象 変更内容
useProvider 以前は、 HookWidget と useProvider の組み合わせにより、各Providerを読み取ることができていました。v1.0.0 で ref.xxx を使用した記法に統一されたことにより、 useProvider は廃止となります。代わりに、 HookConsumerWidget と ref.watch を使用しましょう。
  • HookWidget -> HookConsumerWidget
  • Widget build(BuildContext context) -> Widget build(BuildContext context, WidgetRef ref)
  • useProvider -> ref.watch
class Example extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
  }
}
対象 変更内容
build,builder引数が変更 ConsumerWidget と Consumer の build , builder メソッド引数が変更ScopedReader watch が WidgetRef ref に変更されました。
class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
  }
}
対象 変更内容
ProviderListenerが非推奨 代わりに ref.listen を使いましょう。
  • listen とは: Providerが変更されたときに、Provider/Widget内で任意のアクションを実行できます。 (再構築させずに 各Provider の値の変更を購読できる)
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<int>(counterProvider, (count) {
      // `counterProvider` の値が変更されたときに実行される処理
      print('The new count is $count');
    });
    return Text('Example Widget...');
  }
}
その他変更内容まとめ
StatefulWidgetからWidgetRefを利用するための ConsumerStatefulWidget と ConsumerState が追加されました
すべての watch 関数で select ( myProvider.select((value) => ...) ) が使用できるようになりました
Provider/FutureProvider/StreamProvider 内で ref.state を使うことで現在公開されている値を参照できるようになりました
StateProvider内で ref.controller にアクセスできるようになりました
ProviderReference.mounted が削除されました。onDispose を使用しましょう。
context.read が廃止、 ref.read を使いましょう。context を使ったProviderへのアクセスが廃止されました。代わりに build メソッドの引数である WidgetRef ref を使用しましょう。
context.refresh が廃止、 ref.refresh を使いましょう
StateProviderを読み込んだ時の戻り値がStateNotifierProviderと統一されました。
StateController.update が追加され、以前の状態を使った更新が簡単になりました。
ScopedProvider が廃止され、全てのProviderで、範囲を絞った override ができるようになった。従来は、 ProviderScope を使った各Providerの値の上書きはルートの ProviderScope で行うか、ScopedProvider を対象にしてしか使うことができませんでした。今回の修正で、全ての XxxProvider で ScopedProvider と同様に、任意の箇所での ProviderScope/overrides による値の上書きが可能になりました。
AsyncValueを出力する全てのProviderで await を使用できるようになりました(stable~)
最小要求 Dart SDKバージョンが 2.14.0 になりました。

引用:Riverpod v1.0.0 (stable)の変更点(v0.14.0との比較)





Flutter Hooks

PrimitivesなHooks 説明
useContext HookWidgetのbuild内でBuildContextを取得できます
Widget _buildBody() {
  // [BuildContext]を引数でバケツリレーしなくてもよくなる。
  // ただし、buildの外で`useContext`を実行しても[BuildContext]は取得できません
  final context = useContext();
  // 途中略

  return SomeWidget();
}
PrimitivesなHooks 説明
useState 使い方はuseState(0)のように書くだけです。引数の初期値は省略も可能です。省略する場合はuseState()のように型を指定する必要があります。また、その場合の初期値はnullになるため、扱いに注意してください。また、初期値による更新通知は発生しません。
Widget build(BuildContext context) {
  final counter = useState(0);

  return Scaffold(
    body: Container(),
    floatingActionButton: FloatingActionButton(
      onPressed: () => counter.value++,
      child: const Icon(Icons.add),
    ),
  );
}
PrimitivesなHooks 説明
useEffect 初期化処理など一度のみ実行したい場合や、開放処理を書きたい場合に便利な仕組みです。ここで注意すべきは、useEffectの戻り値は関数のため、store.dispose()ではなく、store.disposeである必要があります。
Widget build(BuildContext context) {
  final store = useMemoized(() => MyStore());

  useEffect(() {
    // 初期表示時にデータのロードを実行
    store.loadData();
    // 関数(Function())を返却しておくと、Widgetのライフサイクルに合わせてWidgetのdisposeのタイミングで関数を実行してくれます(不要であればnullでOK)
    return store.dispose;
  }, // [keys]は空配列でも問題ない
  const []);
  return SomeWidget();
}
PrimitivesなHooks 説明
useMemoized 値をキャッシュしてくれるため、API通信などで取得したデータの管理に便利です。具体的にはStreamやFutureの管理を想定しています。
useFuture / useStream FutureやStreamを扱うもので、FutureBuilderやStreamBuilderを簡単にかけるようになります。
class UseFutureSample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final packageInfo = useMemoized(PackageInfo.fromPlatform);
    final snapshot = useFuture(packageInfo);
    if (snapshot.hasData) {
      return Text('appName = ${snapshot.data.appName}');
    } else {
      return Container();
    }
  }
}




freezed

  • user.dart ファイルに User というクラスを定義する場合、 part 'user.freezed.dart'; の一行が必要。

  • toJson, fromJsonを定義する場合は part 'user.g.dart';factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);を追加。

  • コード生成コマンドを実行すると user.freezed.dart という名前のファイルが同階層のフォルダに生成される。

  • 詳しいsyntax(構文)

https://pub.dev/packages/freezed#the-syntax

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
abstract class User with _$User {
 const factory User({String name, int age}) = _User;
 factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
  • コマンド
flutter pub run build_runner build --delete-conflicting-outputs
⚠️ **GitHub.com Fallback** ⚠️