最近発表されたRiverpod3.0の新機能として、オフライン対応が強化されました。これは、アプリがインターネットに接続されていない状態でも状態を保持できるようにするもので、これまでは、別のパッケージを利用してバックエンドサービスと手動で状態の永続化を実装する必要がありました。
今回、riverpodのデフォルトの機能として、オフラインサポートが追加されたことで、いちいち面倒なロジックを書かなくても、簡単にオフライン対応ができるようになりました。
しかし、デフォルトでは sqflite
をオフライン用のデータベース(DB)に使用しています。そこで、よりシンプルで馴染みのある SharedPreferences
を使ってオフラインサポートを実装してみました。
※ 基本的に、デフォルトのsqflite
の利用で問題ないのですが、特別な事情があって他のDBを使いたい人向けの記事です。
また、SharedPreferences
側が今後のアップデートで対応してくれる可能性があります
※ この機能は実験的な要素であり、本番環境などへの導入は慎重に行う必要があります。
🚀 GitHubで開くまずは、デフォルトの使い方を紹介します。詳しくは、公式ドキュメントを参照してください。
基本的には、コードの通りですが、オフライン対応のためのDBとして、riverpod_sqflite
パッケージで提供されているメソッドを利用してstorage
(オフライン用のDB)を定義します
オフライン対応させたいデータのモデルクラスを今まで通りに定義します。ここでは、freezed
パッケージを使って、Todo
モデルを定義します。
オフライン対応のためのRiverpodのNotifierを定義します。ここでは、TodosNotifier
を定義しています。ここで、@JsonPersist()
アノテーションを付けることを忘れないでください。
TodosNotifier
の中で、persist
メソッドを呼び出すことで、オフライン対応のDBにデータを保存します。これにより、アプリがオフラインになっても状態が保持されます。また、persist
メソッドのoptions
引数で、キャッシュ時間を設定できます。ここでは、無制限に設定していますが、特に設定しなければ、デフォルトで2日間のキャッシュ時間が設定されます。
// オフラインサポート用のDBを提供するプロバイダ
Future<JsonSqFliteStorage> storage(Ref ref) async {
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
}
/// オフライン対応させたいデータのモデルクラス
abstract class Todo with _$Todo {
const factory Todo({
required int id,
required String description,
required bool completed,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
// オフライン対応のためのRiverpodのNotifier
()
class TodosNotifier extends _$TodosNotifier {
FutureOr<List<Todo>> build() async {
await persist(
// オフライン用のDBを設定
ref.watch(storageProvider.future),
// オフライン対応の設定
// ここでは、キャッシュ時間を無制限に設定しています
options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
);
// オフラインに保存されたデータがあればそれを返す
return state.value ?? [];
}
// ここはこれまで通りの実装で自動的にオフラインに保存してくれる
Future<void> add(Todo todo) async {
state = const AsyncLoading();
final repository = ref.watch(todoRepositoryProvider);
state = await AsyncValue.guard(() => repository.addTodo(todo));
}
}
以上の設定で、アプリがオフラインになっても、TodosNotifier
の状態が保持されるようになります。非常に簡単ですね!
UI部分のコードと実際の画面は次のようになります。
class TodosPage extends ConsumerWidget {
const TodosPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final todosAsync = ref.watch(todosNotifierProvider);
final notifier = ref.watch(todosNotifierProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: switch (todosAsync) {
AsyncData(value: final todos) => Center(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
trailing: Checkbox(
value: todo.isCompleted,
onChanged: (value) {
notifier.toggleCompletion(todo.id);
},
),
);
},
),
),
AsyncLoading() => const Center(child: CircularProgressIndicator()),
AsyncError(error: final error, stackTrace: _) => Center(
child: Text('Error: $error'),
),
},
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: 'New Todo',
isCompleted: false,
);
await notifier.add(newTodo);
},
child: const Icon(Icons.add),
),
);
}
}
画面は次のようになります。ホットリロードを行っても状態が保持されていることが確認できます。
ここからは、SharedPreferences
を使ったオフラインサポートの実装方法を紹介します。基本的な流れは、デフォルトのsqflite
を使った方法と同じですが、SharedPreferences
を使うためのカスタムのStorage
クラスを作成します。手順を説明します。
PersistedData
というオフラインサポート用に使われる内部のデータクラスのFreezedクラスFreezedPersistedData
を用意します
1.で定義したFreezedPersistedData
を使って、SharedPreferences
にデータを保存するためのカスタムのStorage
クラスを作成します
TodosNotifier
の中で、SharedPreferences
を使ったカスタムのStorage
クラスを使うように変更します
具体的なコードは次のようになります(コピペで動きます)。
PersistedData
のFreezedクラスabstract class FreezedPersistedData with _$FreezedPersistedData {
const factory FreezedPersistedData({
required String value,
required String? destroyKey,
required DateTime? expireAt,
}) = _FreezedPersistedData;
factory FreezedPersistedData.fromJson(Map<String, dynamic> json) =>
_$FreezedPersistedDataFromJson(json);
}
SharedPreferences
を使ったカスタムのStorage
クラスclass SharedPrefsStorage implements Storage<String, String> {
final SharedPreferences _prefs;
SharedPrefsStorage(this._prefs);
FutureOr<void> delete(String key) async {
final prefs = _prefs;
await prefs.remove(key);
}
FutureOr<PersistedData<String>?> read(String key) async {
final prefs = _prefs;
final result = prefs.getString(key);
if (result == null) return null;
final data = FreezedPersistedData.fromJson(jsonDecode(result));
return PersistedData<String>(
data.value,
destroyKey: data.destroyKey,
expireAt: data.expireAt,
);
}
FutureOr<void> write(String key, String value, StorageOptions options) async {
final prefs = _prefs;
final duration = options.cacheTime.duration;
final json = jsonEncode(
FreezedPersistedData(
value: value,
destroyKey: options.destroyKey,
expireAt: duration == null ? null : DateTime.now().add(duration),
).toJson(),
);
await prefs.setString(key, json);
}
}
SharedPrefsStorage sharedPrefsStorage(Ref ref) {
final sharedPrefs = ref.watch(sharedPreferencesProvider);
return SharedPrefsStorage(sharedPrefs);
}
TodosNotifier
の中で、SharedPreferences
を使ったカスタムのStorage
クラスを使うように変更 <List<Todo>> build() async {
// [追加] SharedPreferencesを使ったカスタムのStorageを取得
final sharedPrefStorage = ref.watch(sharedPrefsStorageProvider);
await persist(
// [変更] SharedPreferencesを使ったカスタムのStorageを利用
storage: sharedPrefStorage,
options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
);
// オフラインに保存されたデータがあればそれを返す
return state.value ?? [];
}
FutureOr
Riverpod3.0の新機能であるオフラインサポートの実装方法を紹介しました。これにより、よりシンプルで馴染みのある方法でオフライン対応が可能になります。今まで面倒だからという理由でオフライン対応を避けていた方も、これなら簡単に実装できるのではないでしょうか。
また、SharedPreferences
を使ったカスタムのStorage
クラスを作成することで、より柔軟なオフライン対応が可能になります。これにより、アプリのアーキテクチャや要件に応じて、適切な永続化方法を選択できるようになります。
ただしこのオフラインサポートの機能ですが、現状は、Notifier
クラスにしか対応していないそうです。個人的には、streamProvider
やfutureProvider
などにも対応してくれるとアーキテクチャの面でもありがたいです。
可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More