📝
SharedPreferences + Riverpod Generator サンプル
Remiさんのポストを見かけて、Riverpod Generator をそろそろ採用しても良さそうに思えてきた.
↑の例でCounterRef と書かなくて良くなっただけでもかなり嬉しい.
試す
カウントアップしてSharedPreferenceに記録するサンプルをRiverpod Generator を採用/非採用でそれぞれ実装してみる
動作環境
- Flutter 3.24.4
- flutter_riverpod: 2.6.1
- riverpod_generator: 2.6.2
- riverpod_annotation: 2.6.1
- shared_preferences: 2.3.2
Riverpod Generator を採用しない場合
Provider: SharedPreferencesWithCache
FutureProvider で SharedPreferencesWithCache のインスタンスを返す:
final prefWithCacheProvider = FutureProvider<SharedPreferencesWithCache>((ref) {
return SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: {'counter'},
),
);
});
Provider: Counter
AutoDisposeAsyncNotifier でカウントアップして記録する処理を実装:
final counterProvider = AsyncNotifierProvider.autoDispose<CounterController, int>(
CounterController.new,
);
class CounterController extends AutoDisposeAsyncNotifier<int> {
FutureOr<int> build() async {
final pref = await ref.read(prefWithCacheProvider.future);
return pref.getInt('counter') ?? 0;
}
Future increment() async {
if (state.isLoading || state.hasError) return;
final pref = await ref.read(prefWithCacheProvider.future);
await pref.setInt('counter', state.requireValue + 1);
ref.invalidateSelf();
}
}
Riverpod Generator を採用する場合
パッケージ
riverpod_annotation と riverpod_generator を追加する:
dependencies:
flutter_riverpod:
# @riverpod を宣言するためのアノテーションパッケージ
riverpod_annotation:
dev_dependencies:
# コードジェネレータを実行するためのツール
build_runner:
# コードジェネレータ
riverpod_generator:
コードジェネレータの起動
% dart run build_runner watch
Provider: SharedPreferencesWithCache
AutoDisposeさせない場合は keepAlive を設定する:
(keepAlive: true)
Future<SharedPreferencesWithCache> prefWithCache(Ref ref) async {
return SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: {'counter'},
),
);
}
- Suffix はデフォルトで
Provider
になるため、prefWithCacheProvider が自動生成されることになる(変更可能)
Provider: Counter
同様にカウンターロジック実装:
class Counter extends _$Counter {
Future<int> build() async {
final pref = await ref.read(prefWithCacheProvider.future);
return pref.getInt('counter') ?? 0;
}
Future increment() async {
if (state.isLoading || state.hasError) return;
final pref = await ref.read(prefWithCacheProvider.future);
await pref.setInt('counter', state.requireValue + 1);
ref.invalidateSelf();
}
}
試してみた感想
諸々メリットがあるため採用してみても良さそう:
- ボイラープレートコードの削減
- (という目的でいうとそこまででもないかも)
- FutureProvider/Notifier/AsyncNotifier のどれにすべきかを特に意識せずによくなりそう
- build_runner watch による差分コード生成なら、待機時間も生じない
- 部分的に採用できる
- Suffixの統一がとれる(xxxControllerとかxxxNotifierとかにならない)
ただし、規模が大きいプロジェクトでどうなるかは分かっていない
また、family の使い勝手や自動テストへの影響も分かっていないため、別途サンプルで確認したい
全サンプル
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'main.g.dart';
(keepAlive: true)
Future<SharedPreferencesWithCache> prefWithCache(Ref ref) async {
return SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: {'counter'},
),
);
}
class Counter extends _$Counter {
Future<int> build() async {
final pref = await ref.read(prefWithCacheProvider.future);
return pref.getInt('counter') ?? 0;
}
Future increment() async {
if (state.isLoading || state.hasError) return;
final pref = await ref.read(prefWithCacheProvider.future);
await pref.setInt('counter', state.requireValue + 1);
ref.invalidateSelf();
}
}
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
title: 'SharedPreferencesWithCache Demo',
home: SharedPreferencesDemo(),
);
}
}
class SharedPreferencesDemo extends ConsumerWidget {
const SharedPreferencesDemo({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('SharedPreferencesWithCache Demo'),
),
body: Center(
child: switch (counter) {
AsyncData(:final value) => Text('$value'),
AsyncError(:final error) => Text('Error: $error'),
_ => const CircularProgressIndicator(),
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Discussion