📝

SharedPreferences + Riverpod Generator サンプル

2024/10/31に公開

https://x.com/remi_rousselet/status/1848068559529808187

Remiさんのポストを見かけて、Riverpod Generator をそろそろ採用しても良さそうに思えてきた.
↑の例でCounterRef と書かなくて良くなっただけでもかなり嬉しい.

試す

カウントアップしてSharedPreferenceに記録するサンプルをRiverpod Generator を採用/非採用でそれぞれ実装してみる

動作環境

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