41.5K Views
September 29, 23
スライド概要
「YOUTRUST x ゆめみ Flutter LT会@渋谷 #3」に登壇時の発表資料となります。
https://yumemi.connpass.com/event/294763/
目次
• Riverpod 3 で変更されそうなこと
• AsyncValueがsealed classになる
• Genericsなプロバイダを作成可能になる
• 2時点でDeprecatedなものは削除される
• Riverpod Tips 4選
• ref.invalidateとref.refreshの使い分け
• Functional providerとClass based providerの使い分け
• アプリルートのProviderScope.overrides
• BuildContext無しでダイアログやバナー表示
Altive株式会社 代表取締役 Flutterアプリ開発者
Riverpod 3とTips 4選
Altiveメンバーの働き方 ● ● ● 入社時有給休暇付与 フルリモートワーク スーパーフレックス Altive株式会社 代表取締役 村松 龍之介 𝕏 @riscait Flutter x Riverpod でアプリ開 発!実践入門 をZennで公開中 Altive株式会社では、 現在 iOSネイティブ出身の2人で、 Flutterアプリの受託・自社開発を行なっていま す。 主な技術スタック: Firebase / Go / SwiftUI ※ 開発アプリ一例
目次 ● Riverpod 3 で変更されそうなこと ○ AsyncValueがsealed classになる ○ Genericsなプロバイダを作成可能になる ○ 2時点でDeprecatedなものは削除される ● Riverpod Tips 4選 ○ ref.invalidateとref.refreshの使い分け ○ Functional providerとClass based providerの使い分 ○ アプリルートのProviderScope.overrides ○ BuildContext無しでダイアログやバナー表示
Riverpod 3で変更されそうなこと 主にRiverpod作者が作成中のPull-requestを読んだ内容になります ※ https://github.com/rrousselGit/riverpod/pull/2487
① AsyncValue が sealed class になる
AsyncValue が sealed class になる Riverpod 2以下は abstract class + @sealed となっている。 sealedクラスになることで、 Dart 3 のパターンマッチングが適切に使えるようになる。 Riverpod 3以降では、 AsyncValue.map/when ではなく、 switch-case の使用が推奨される。 手動マイグレーションのドキュメントページが追加される予定。 自動マイグレーションツールが提供されるかも?
AsyncValue.when と switch-case の対照表
// Before
asyncValue.when(
data: (value) => print(value),
error: (error, stack) => print('Error $error'),
loading: () => print('loading'),
);
// After
switch (asyncValue) {
case AsyncData(:final value): print(data);
case AsyncError(:final error): print('Error $error');
case _: print('loading');
}
// Before
asyncValue.when(
skipLoadingOnReload: true,
data: (value) => print(value),
error: (error, stack) => print('Error $error'),
loading: () => print('loading'),
);
// After
switch (asyncValue) {
case AsyncValue(:final error?): print('Error $error');
case AsyncValue(:final value, hasData: true): print(data);
case _: print('loading');
}
https://github.com/rrousselGit/riverpod/issues/2715
AsyncValue.when と switch-case の対照表
// Before
asyncValue.when(
data: (value) => print(value),
error: (error, stack) => print('Error $error'),
loading: () => print('loading'),
);
// After
switch (asyncValue) {
case AsyncData(:final value): print(data);
case AsyncError(:final error): print('Error $error');
case _: print('loading');
}
// Before
asyncValue.when(
skipLoadingOnReload: true,
data: (value) => print(value),
error: (error, stack) => print('Error $error'),
loading: () => print('loading'),
);
// After
switch (asyncValue) {
case AsyncValue(:final error?): print('Error $error');
case AsyncValue(:final value, hasData: true): print(data);
case _: print('loading');
}
https://github.com/rrousselGit/riverpod/issues/2715
② Genericsなプロバイダが作成可能に
Genericsなプロバイダが作成可能になる
// intやdoubleの可能性のあるプロバイダを定義できる。
@riverpod
List<T> example<T extends num>(ExampleRef<T> ref) {
return <T>[];
}
// 呼び出し側では、引数がなくても関数として使用する。
ref.watch(exampleProvider<int>());
https://github.com/rrousselGit/riverpod/pull/2487/files
Genericsなプロバイダが作成可能になる
// StateにGenericsを持つ、NotifierProviderを定義できる。
@riverpod
class ClassExample<T> extends _$ClassExample<T> {
@override
List<T> build() => <T>[];
}
https://github.com/rrousselGit/riverpod/pull/2487/files
③ Riverpod 2 時点で Deprecatedと されているものは削除される
Riverpod 2 時点で Deprecatedなものは削除される ● AutoDisposeプロバイダでの `maintainState` ○ keepAlive() を使用する。 ● Streamプロバイダの `.stream` ○ プロバイダかプロバイダ.futureを listen/watch する。 など… 「Riverpod 3で変更されそうなこと」終わり
Riverpod Tips 4選 1) 2) 3) 4) ref.invalidate と ref.refresh の使い分け Functional providerとClass based providerの使い分け アプリルートのProviderScope.overrides BuildContext無しでダイアログやバナー表示
① ref.invalidate と ref.refresh の使い分け
ref.invalidate と ref.refresh の使い分け 違い: ● refresh()は更新後の値を返却する。invalidate() は戻り値の型が `void` ● refresh() はプロバイダに `.future` が指定できる refresh()の内部実装を見ると… // riverpod/lib/src/framework/container.dart State refresh<State>(Refreshable<State> provider) { invalidate(provider._origin); return read(provider); } invalidate()した後にread()を行なっている👀
ref.invalidate と ref.refresh の使い分け つまり… ● 更新が完了するのを待ってから行いたい処理がある場合 → `.future` と `await` が使用できる refresh() を使う。 ● 更新後、その場で更新後の値を使用したい場合 → 更新後の値を返してくれる refresh() を使う。 final result = await ref.refresh(someProvider.future); それ以外の場合は、 プロバイダへの次回アクセスまで値の読み取りを遅延できる invalidate() を使う。
② Functional provider と Class based provider の使い分け
Functional provider と Class based providerの使い分け
Functional provider(関数プロバイダ)
@riverpod
Future<List<Todo>> todoList(TodoListRef ref) async {
final r = await ref.watch(dioProvider).get<List<Map<String, Object?>>>(
'https://example.com/api/todos',
);
return r.data.map(Todo.fromJson).toList();
}
従来の Provider, FutureProvider, StreamProviderの役割を担う。
作成時にFutureProviderかProviderか等を意識する必要はない。
プロバイダの外部から値を直接操作することはできない。( ProviderScope.overridesを除く)
Functional provider と Class based providerの使い分け
Class based provider(クラスベースプロバイダ) By Notifier/AsyncNotifier
@riverpod
class AsyncTodoList extends _$AsyncTodoList {
@override
Future<List<Todo>> build() async {
final r = await ref.watch(dioProvider).get<List<Map<String, Object?>>>(
'https://example.com/api/todos',
);
return r.data.map(Todo.fromJson).toList();
}
従来の StateProvider, StateNotifierProvider, ChangeNotifierProvider の役割を担う。
Notifierクラスにメソッドを定義し、外部から `state` を操作できる。
関連する処理をクラスに集約しやすい。
Functional provider と Class based providerの使い分け Functional provider(関数プロバイダ) ● 外部から値を変更する必要のないもの ● REST APIのGETメソッドの結果や、Firebase Firestoreのsnapshot など Class based provider(クラスベースプロバイダ) ● 外部から `.notifier` を使用して `state` を操作したい ○ ● ページをまたがる複数の入力・選択フォームの状態保持など 関連する処理をクラスに集約したい ○ buildメソッドでGETを行い、 クラスにPOSTやPATCH、DELETEメソッドを集約したい、など
Functional provider と Class based providerの使い分け
AsyncTodoList (Class based provider) にTodoを追加する `add` メソッドを定義する例①
Todo追加完了後に `invalidateSelf()` を使用することで最新の値を再取得して
asyncTodoListProvider を購読している Widgetでは最新のリストが表示される。
// AsyncTodoList
Future<void> add(Todo todo) async {
// POSTメソッドで新しいTODOを追加する。
await ref.read(dioProvider).post<Map<String, Object?>>(
'https://example.com/api/todos',
data: todo.toJson(),
);
// POST完了後にbuildメソッドを再実行させることで最新の値を GETさせる。
ref.invalidateSelf();
}
Functional provider と Class based providerの使い分け
AsyncTodoList (Class based provider) にTodoを追加する `add` メソッドを定義する例②
POSTやPATCHメソッド実行時に GETしたくない場合の一例。
POSTメソッドのレスポンスや引数で受け取った Todoを `state` に代入すれば Widgetは更新される。
Future<void> add(Todo todo) async { // 新しいTODOを追加するメソッド
final todos = state.valueOrNull;
state = const AsyncValue.loading(); // 処理完了までの間はローディング状態にしたい場合
state = await AsyncValue.guard(() async {
final r = await ref.read(dioProvider).post<Map<String, Object?>>(
'https://example.com/api/todos',
data: todo.toJson(),
);
return [Todo.fromJson(r.data), ...?todos];
});
}
③ アプリルートのProviderScope.overrides
アプリルートのProviderScope.overrides
// Dart-defineで渡した `flavor` 文字列をEnumにして提供するプロバイダ。
@Riverpod(keepAlive: true)
Flavor flavor(FlavorRef ref) {
return Flavor.values.byName(const String.fromEnvironment('flavor'));
}
課題:main関数で使いたいけどProviderScopeが無いから使えない🥺
// SharedPreferencesのインスタンスを提供するプロバイダ。
@Riverpod(keepAlive: true)
Future<SharedPreferences> sharedPreferences(SharedPreferencesRef ref) async {
return SharedPreferences.getInstance();
}
課題:FutureProviderになるので、使用時に非同期処理が必要🥺
アプリルートのProviderScope.overrides 準備:例外(UnimplementedError)を投げて、オーバーライド必須にする。 @Riverpod(keepAlive: true) Flavor flavor(FlavorRef ref) { throw UnimplementedError(); } @Riverpod(keepAlive: true) SharedPreferences sharedPreferences(SharedPreferencesRef ref) { throw UnimplementedError(); } この場合、FutureProviderにする必要はない。
アプリルートのProviderScope.overrides
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final flavor = Flavor.values.byName(const String.fromEnvironment('flavor'));
final (_, sharedPreferences) = await (
Firebase.initializeApp(options: firebaseOptionsWithFlavor(flavor)),
SharedPreferences.getInstance(),
).wait;
次ページへ続く
アプリルートのProviderScope.overrides // 前ページの続き runApp( ProviderScope( overrides: [ flavorProvider.overrideWithValue(flavor), sharedPreferencesProvider.overrideWithValue(sharedPreferences), ], child: const MyApp(), ), ); }
④ BuildContext無しで ダイアログやバナー表示
BuildContextが無いところでダイアログやバナー表示
準備:それぞれのGlobalKeyを提供するプロバイダを用意する
@Riverpod(keepAlive: true)
GlobalKey<NavigatorState> navigatorKey(NavigatorKeyRef ref) {
return GlobalKey<NavigatorState>();
}
@Riverpod(keepAlive: true)
GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey(
ScaffoldMessengerKeyRef ref,
){
return GlobalKey<ScaffoldMessengerState>();
}
BuildContextが無いところでダイアログやバナー表示
Refさえあれば、プロバイダから取得したGlobalKeyを使って実現できる🎉
// スナックバー表示
ref.read(scaffoldMessengerKeyProvider).currentState!.showSnackBar(snackBar);
// 画面遷移
ref.read(navigatorKeyProvider).currentState!.push(route);
// ダイアログ表示
await showDialog<void>(
context: ref.read(navigatorKeyProvider).currentState!.overlay!.context;,
builder: (context) {
// return dialog
},
);
BuildContextが無いところで画面遷移(go_router)
@Riverpod(keepAlive: true)
Raw<GoRouter> router(RouterRef ref) {
return GoRouter(routes: $appRoutes);
}
return MaterialApp.router(
routerDelegate: router.routerDelegate,
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
scaffoldMessengerKey: ref.watch(scaffoldMessengerKeyProvider),
);
auto_routeパッケージでも要領は一緒。
ご清聴ありがとうござ いました!