Çevrimdışı kalıcılık (deneysel)
Çevrimdışı kalıcılık, Provider'lar durumunu kullanıcının cihazında saklayarak kullanıcı çevrimdışıyken ya da uygulama yeniden başlatıldığında bile bu duruma erişilebilmesidir.
Riverpod, verinin saklandığı veritabanından veya kullanılan protokolden bağımsızdır. Ancak varsayılan olarak Riverpod, temel JSON serileştirmesinin yanı sıra riverpod_sqflite paketini sunar.
Riverpod'un çevrimdışı kalıcılığı, veritabanlarının etrafında basit bir sarmalayıcı olacak şekilde tasarlanmıştır. Bir veritabanıyla etkileşim kuran kodun tamamının yerini alması amaçlanmamıştır.
Şu durumlar için hâlâ veritabanıyla elle etkileşim kurmanız gerekebilir:
- İleri düzey veritabanı geçişleri (migration)
- Daha optimize edilmiş depolama stratejileri
- Sıra dışı kullanım senaryoları
Çevrimdışı kalıcılık iki parçadan oluşur:
- Veritabanınızla etkileşim kurmaya yarayan bir arayüz olan Storage. Bu genellikle bir paket tarafından uygulanır (örneğin riverpod_sqflite).
- Kalıcılığı devreye almak için notifier'ların içinde kullanılan AnyNotifier.persist fonksiyonu.
Bir Storage oluşturmak
Notifier'ları kalıcı hale getirmeye başlamadan önce, Storage arayüzünü uygulayan bir nesne oluşturmamız gerekir. Bu nesne, Riverpod'u veritabanınıza bağlamaktan sorumlu olacaktır.
Şunlardan birini yapmanız gerekir:
- Riverpod'u tercih ettiğiniz veritabanına bağlamanın bir yolunu sunan bir paket kurmak.
- Storage'ı elle uygulamak
SQFlite kullanıyorsanız riverpod_sqflite paketini kullanabilirsiniz:
dart pub add riverpod_sqflite sqflite
Ardından JsonSqFliteStorage örneği oluşturarak bir Storage yaratabilirsiniz:
- riverpod
- riverpod_generator
import 'package:flutter_riverpod/experimental/persist.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart';
import 'package:riverpod_sqflite/riverpod_sqflite.dart';
import 'package:sqflite/sqflite.dart';
final storageProvider = FutureProvider<Storage<String, String>>((ref) async {
// Initialize SQFlite. We should share the Storage instance between providers.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
});
import 'package:flutter_riverpod/experimental/persist.dart';
import 'package:path/path.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_sqflite/riverpod_sqflite.dart';
import 'package:sqflite/sqflite.dart';
part 'codegen.g.dart';
Future<Storage<String, String>> storage(Ref ref) async {
// Initialize SQFlite. We should share the Storage instance between providers.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
}
Bir provider'ın durumunu kalıcı hale getirmek
Bir Storage oluşturduktan sonra provider'ların durumunu kalıcı hale getirmeye başlayabiliriz.
Şu anda yalnızca "Notifier"lar kalıcı hale getirilebilir. Bunlar hakkında daha fazla bilgi için Provider'lar sayfasına bakın.
Bir notifier'ın durumunu kalıcı hale getirmek için genellikle notifier'ınızın build metodu içinde AnyNotifier.persist çağırmanız gerekir.
- riverpod
- riverpod_generator
class Todo {
Todo({required this.task});
final String task;
}
final todoListProvider = AsyncNotifierProvider<TodoList, List<Todo>>(
TodoList.new,
);
class TodoList extends AsyncNotifier<List<Todo>> {
Future<List<Todo>> build() async {
persist(
// We pass in the previously created Storage.
// Do not "await" this. Riverpod will handle it for you.
ref.watch(storageProvider.future),
// A unique identifier for this state.
// If your provider receives parameters, make sure to encode those
// in the key as well.
key: 'todo_list',
// Encode/decode the state. Here, we're using a basic JSON encoding.
// You can use any encoding you want, as long as your Storage supports it.
encode: (todos) => todos.map((todo) => {'task': todo.task}).toList(),
decode:
(json) =>
(json as List)
.map((todo) => Todo(task: todo['task'] as String))
.toList(),
);
// Regardless of whether some state was restored or not, we fetch the list of
// todos from the server.
return fetchTodosFromServer();
}
}
class Todo {
Todo({required this.task});
final String task;
}
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
persist(
// We pass in the previously created Storage.
// Do not "await" this. Riverpod will handle it for you.
ref.watch(storageProvider.future),
// A unique identifier for this state.
// If your provider receives parameters, make sure to encode those
// in the key as well.
key: 'todo_list',
// Encode/decode the state. Here, we're using a basic JSON encoding.
// You can use any encoding you want, as long as your Storage supports it.
encode: (todos) => todos.map((todo) => {'task': todo.task}).toList(),
decode:
(json) =>
(json as List)
.map((todo) => Todo(task: todo['task'] as String))
.toList(),
);
// Regardless of whether some state was restored or not, we fetch the list of
// todos from the server.
return fetchTodosFromServer();
}
}
Basitleştirilmiş JSON serileştirmesini kullanmak (kod üretimi)
riverpod_sqflite ile birlikte kod üretimi kullanıyorsanız, JsonPersist anotasyonunu
kullanarak persist çağrısını basitleştirebilirsiniz:
// Using freezed or json_serializable to generate from/toJson for your objects
abstract class Todo with _$Todo {
const factory Todo({required String task}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
// Specify @JsonPersist. This will provide a custom "persist" method for your notifier
()
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
persist(
// We pass in the previously created Storage.
// Do not "await" this. Riverpod will handle it for you.
ref.watch(storageProvider.future),
// No need to specify key/encode/decode functions.
);
// Initialize the notifier as usual.
return fetchTodosFromServer();
}
}
Persist anahtarlarını anlamak
Önceki kod parçalarının bazılarında AnyNotifier.persist metoduna bir key parametresi geçtik.
Bu anahtar, veritabanınızın bir provider'ın durumunu veritabanında nerede saklayacağını bilmesini sağlar.
Veritabanına bağlı olarak bu anahtar benzersiz bir satır kimliği olabilir.
key belirtirken şunlardan emin olmanız kritik önemdedir:
- Anahtar, kalıcı hale getirdiğiniz tüm provider'lar arasında benzersiz olmalıdır.
Aksi takdirde iki provider veritabanındaki aynı satıra yazmaya çalışabileceği için veri bozulmasına yol açabilirsiniz. Riverpod aynı anahtarı kullanan iki provider tespit ederse bir assertion fırlatılır. - Anahtar, uygulama yeniden başlatıldığında değişmemelidir. Anahtar değişirse Riverpod, uygulama yeniden başlatıldığında provider'ın durumunu geri yükleyemez ve provider hiç kalıcı hale getirilmemiş gibi başlatılır
- Anahtar, provider'ın aldığı tüm parametreleri içermelidir. "Family" kullanırken (bkz. Family) anahtarın family parametresini içermesi gerekir.
Önbellek süresini değiştirmek
Varsayılan olarak durum yalnızca 2 gün boyunca önbellekte tutulur. Bu varsayılan, sızıntı olmamasını ve silinen provider'ların veritabanında süresiz kalmamasını sağlar
Riverpod öncelikli olarak IO işlemleri (ağ istekleri, veritabanı sorguları vb.) için bir önbellek olarak kullanılmak üzere tasarlandığından bu genellikle güvenlidir. Ancak bu varsayılan, örneğin kullanıcı tercihlerini saklamak istediğinizde olduğu gibi her kullanım senaryosu için uygun olmayacaktır.
Bu varsayılanı değiştirmek için options parametresini şu şekilde belirtin:
Future<List<Todo>> build() async {
persist(
ref.watch(storageProvider.future),
// We tell Riverpod to forever persist the state of this provider.
options: const StorageOptions(
// Instead of "unsafe_forever", you can alternatively specify a Duration.
cacheTime: StorageCacheTime.unsafe_forever,
),
// ...
);
return fetchTodosFromServer();
}
Önbellek süresini sonsuz olarak ayarlarsanız, provider'ı bir gün silmeniz durumunda kalıcı hale getirilmiş durumu veritabanından elle silmeyi unutmayın.
Bunun için veritabanınızın dokümantasyonuna bakın.
Basit veri geçişleri için "destroy key" kullanmak
Veriyi kalıcı hale getirirken sık karşılaşılan bir zorluk, veri yapısı değiştiğinde bunun nasıl ele alınacağıdır. Bir nesnenin serileştirilme biçimini değiştirirseniz, veritabanında saklanan veriyi taşımanız gerekebilir.
Riverpod tam anlamıyla bir veri geçişi yapmanın yolunu sunmasa da, eski kalıcı durumu yepyeni bir durumla kolayca değiştirmenin bir yolunu sunar: Destroy key'ler.
- riverpod
- riverpod_generator
class TodoList extends AsyncNotifier<List<Todo>> {
Future<List<Todo>> build() async {
persist(
ref.watch(storageProvider.future),
// We can optionally pass a "destroyKey". When a new version of the application
// is release with a different destroyKey, the old persisted state will be
// deleted, and a brand new state will be created.
options: const StorageOptions(destroyKey: '1.0'),
// Persist as usual
key: 'todo_list',
encode: (todos) => todos.map((todo) => {'task': todo.task}).toList(),
decode:
(json) =>
(json as List)
.map((todo) => Todo(task: todo['task'] as String))
.toList(),
);
return fetchTodosFromServer();
}
}
()
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
persist(
ref.watch(storageProvider.future),
// We can optionally pass a "destroyKey". When a new version of the application
// is release with a different destroyKey, the old persisted state will be
// deleted, and a brand new state will be created.
options: const StorageOptions(destroyKey: '1.0'),
);
return fetchTodosFromServer();
}
}
Destroy key'ler, eski kalıcı durumun ne zaman atılması gerektiğini Riverpod'un bilmesini sağlayarak basit veri geçişlerine yardımcı olur. Uygulamanın farklı bir destroyKey ile yeni bir sürümü yayınlandığında eski kalıcı durum atılır ve provider hiç kalıcı hale getirilmemiş gibi başlatılır.
Kalıcılık çözümlemesini beklemek
Şimdiye kadar AnyNotifier.persist metodunun tamamlanmasını hiç beklemedik.
Bu bilinçli bir tercihtir; çünkü bu sayede provider ağ isteklerine mümkün olan en kısa sürede başlayabilir.
Ancak bu, provider'ın persist çağrısının hemen ardından kalıcı duruma kolayca
erişemeyeceği anlamına gelir.
Bazı durumlarda provider'ı bir ağ isteğiyle başlatmak yerine kalıcı durumla başlatmak isteyebilirsiniz.
Bu durumda persist sonucunu şu şekilde bekleyebilirsiniz:
await persist(...).future;
Bu, build içinde this.state üzerinden kalıcı duruma erişmenizi sağlar:
Future<List<Todo>> build() async {
// Wait for decoding to complete
await persist(
ref.watch(storageProvider.future),
// ...
).future;
// If any state has been decoded, initialize the provider with it.
// Otherwise provide a default value.
return state.value ?? <Todo>[];
}
Kalıcılığı test etmek
Uygulamanızı test ederken gerçek bir veritabanı kullanmak zahmetli olabilir. Özellikle birim testleri ve widget testleri bir cihaza erişemeyeceğinden veritabanı kullanamaz.
Bu nedenle Riverpod, Storage.inMemory ile bellek içi bir veritabanı kullanmanın yolunu sunar.
Testinizin bu bellek içi veritabanını kullanması için Provider'ları geçersiz kılma kullanabilirsiniz:
testWidgets('Widget test example', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Override the `storageProvider` so that our application
// uses an in-memory storage.
storageProvider.overrideWith((ref) {
// Create an in-memory storage.
final storage = Storage<String, String>.inMemory();
// Initialize it with some data.
storage.write(
'todo_list',
'{"task": "Eat a cookie"}',
const StorageOptions(),
);
return storage;
}),
],
child: const MyApp(),
),
);
});
test('Pure dart example', () {
final container = ProviderContainer.test(
// Same as above, we override the `storageProvider`
overrides: [
storageProvider.overrideWith(
(ref) => Storage<String, String>.inMemory(),
),
],
);
// TODO use container to interact with providers by hand.
});