Back to Riverpod

Offline persistence (experimental)

website/docs/concepts2/offline.mdx

2.0.0-dev.96.8 KB
Original Source

import { Link } from "/src/components/Link"; import { AutoSnippet, } from "/src/components/CodeSnippet"; import storage from './offline/storage' import manualPersist from './offline/manual_persist' import destroyKey from './offline/destroy_key'; import waitPersist from 'raw-loader!./offline/wait_persist.dart'; import jsonPersist from 'raw-loader!./offline/json_persist.dart' import customDuration from 'raw-loader!./offline/custom_duration.dart'; import inMemoryTest from 'raw-loader!./offline/in_memory_test.dart';

Offline persistence is the ability to store the state of <Link documentID="concepts2/providers" /> on the user's device, so that it can be accessed even when the user is offline or when the app is restarted.

Riverpod is independent from the underlying database or the protocol used to store the data. But by default, Riverpod provides riverpod_sqflite alongside basic JSON serialization.

:::caution Riverpod's offline persistence is designed to be a simple wrapper around databases. It is not designed to fully replace code for interacting with a database.

You may still need to manually interact with a database for:

  • Advanced Database migrations
  • More optimized storage strategies
  • Unusual use-cases :::

Offline persistence works using two parts:

  1. Storage, an interface to interact with your database. This is typically implemented by a package (such as riverpod_sqflite).
  2. AnyNotifier.persist, a function used inside notifiers to opt-in to persistence.

Creating a Storage

Before we start persisting notifiers, we need to instantiate an object that implements the Storage interface. This object will be responsible for connecting Riverpod with your database.

You need have to either:

  • Download a package that provides a way to connect Riverpod with your Database of choice.
  • Manually implement Storage

If using SQFlite, you can use riverpod_sqflite:

sh
dart pub add riverpod_sqflite sqflite

Then, you can create a Storage by instantiating JsonSqFliteStorage:

<AutoSnippet {...storage} />

Persisting the state of a provider

Once we've created a Storage, we can start persisting the state of providers.
Currently, only "Notifiers" can be persisted. See <Link documentID="concepts2/providers" /> for more information about them.

To persist the state of a notifier, you will typically need to call AnyNotifier.persist inside the build method of your notifier.

<AutoSnippet {...manualPersist} />

Using simplified JSON serialization (code-generation)

If you are using riverpod_sqflite and code-generation, you can simplify the persist call by using the JsonPersist annotation:

<AutoSnippet raw={jsonPersist} />

Understanding persist keys

In some of the previous snippets, we've passed a key parameter to AnyNotifier.persist. That key is there to enable your database to know where to store the state of a provider in the Database. Depending on the database, this key may be a unique row ID.

When specifying key, it is critical to ensure that:

  • The key is unique across all providers that you persist.
    Failing to do so could cause data corruption, as two providers could be trying to write to the same row in the database. If Riverpod detects two providers using the same key, an assertion will be thrown.
  • The key is stable across app restarts. If the key changes, Riverpod will not be able to restore the state of the provider when the app is restarted, and the provider will be initialized as if it was never persisted
  • The key needs to include any parameter that the provider takes. When using "families" (cf <Link documentID="concepts2/family" />), the key needs to include the family parameter.

Changing the cache duration

By default, state is only cached for 2 days. This default ensures that no leak happens and deleted providers stay in the database indefinitely

This is generally safe, as Riverpod is designed to be used primarily as a cache for IO operations (network requests, database queries, etc). But such default will not be suitable for all use-cases, such as if you want to store user preferences.

To change this default, specify options like so:

<AutoSnippet raw={customDuration} />

:::caution If you set the cache duration to infinite, make sure to manually delete the persisted state from the database if you ever delete the provider.

For this, refer to your database's documentation. :::

Using "destroy key" for simple data-migration

A common challenge when persisting data is handling when the data structure changes. If you change how an object is serialized, you may need to migrate the data stored in the database.

While Riverpod does not provide a way to do proper data migration, it does provide a way to easily replace the old persisted state with a brand new one: Destroy keys.

<AutoSnippet {...destroyKey} />

Destroy keys help doing simple data migrations by enabling Riverpod to know when the old persisted state should be discarded. When a new version of the application is released with a different destroyKey, the old persisted state will be discarded, and the provider will be initialized as if it was never persisted.

Waiting for persistence decoding

Until now, we've never waited for AnyNotifier.persist to complete.
This is voluntary, as this allows the provider to start its network requests as soon as possible. However, it means that the provider cannot easily access the persisted state right after calling persist.

In some cases, instead of initializing the provider with a network request, you may want to initialize it with the persisted state.

In that case, you can await the result of persist as follows:

dart
await persist(...).future;

This enables accessing the persisted state within build using this.state:

<AutoSnippet raw={waitPersist} />

Testing persistence

When testing your application, it may be inconvenient to use a real database. In particular, unit and widget tests will not have access to a device, and thus cannot use a database.

For this reason, Riverpod provides a way to use an in-memory database using Storage.inMemory.
To have your test use this in-memory database, you can use <Link documentID="concepts2/overrides" />:

<AutoSnippet raw={inMemoryTest} />