Riverpod's next version V3 will support Generics.
Here is the code presented as a sample code, which makes it possible to specify a type parameter (T
in the example below).
@riverpod
List<T> example<T extends num>(ExampleRef<T> ref) {
return <T>[];
}
@riverpod
class ClassExample<T> extends _$ClassExample<T> {
@override
List<T> build() => <T>[];
}
Note that according to the author Remi's comment, Generics will be supported by code generation only.
If we want generic providers, it'd most definitely be code-gen only.
https://github.com/rrousselGit/riverpod/issues/2150#issuecomment-1423032267
Not only Generics, but new features may be supported by code-gen only in the future, so if you are using Riverpod in the traditional style (non-code-generated), you may want to consider migrating.
This article used Flutter SDK 3.19.6 and the following versions
flutter pub add \
hooks_riverpod:3.0.0-dev.3 \
riverpod_annotation:3.0.0-dev.3 \
dev:riverpod_generator:3.0.0-dev.11 \
dev:riverpod_lint:3.0.0-dev.4
As of 4/30/2024, Riverpod V3 is still under development and may change in the future.
Specific use cases
I implemented a Provider that uses shared_preferences using Generics to actually try out Genericis.
Since shared_preferences supports multiple types such as int, double, bool, String, List<String>
, I will try to use Generics to reduce code duplication and use type safety.
shared_preferences
The characteristics of shared_preferences are
- Instance acquisition is asynchronous (requires await)
- Separate read/write functions for each type
- read returns null if the value is not stored.
These features must be taken into account when implementing the system.
V2: Without Generics
First, for comparison, here is an example implementation using shared_preference in the current version of Riverpod (V2); those who want to know the code using generics in V3 can skip this section.
Initializing shared_preference
This is not related to Generics, but initializes an instance of shared_preference using Provider.
Getting an instance of shared_preferences is asynchronous and requires await
.
// Obtain shared preferences.
final SharedPreferences prefs = await SharedPreferences.getInstance();
// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = prefs.getInt('counter');
If an instance is obtained each time, it is difficult to handle because it requires an await even when reading.
First, define an empty Provider that manages the SharedPreferences instance.
@riverpod
SharedPreferences sharedPreferences(SharedPreferencesRef ref) =>
throw UnimplementedError();
Next, call SharedPreferences.getInstance()
in the main function to get an instance of SharedPreferences. Override the entity of the retrieved instance with overrideWithValue()
against the Provider defined earlier.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
child: const MyApp(),
),
);
}
After this, you can use instances of SharedPreferences synchronously with the following code.
final sharedPreference = ref.watch(sharedPreferencesProvider);
final someBoolValue = sharedPreference.getBool(...)
key and default value
shared_preferences uses key to read and write values, but returns null if no value is stored in the specified key. In my own implementation, I often assign a default value if no value is stored in the specified key to reduce the use of null.
Since it is preferable to manage key and default value in pairs, I define the following enum Preferepnce<T>
.
enum Preferepnce<T> {
// bool
shouldShowWalkthrough('should_show_walkthrough', true),
// int
themeMode('theme_mode', 0),
// String
appColor('app_color', ''),
;
const Preferepnce(this.key, this.defaultValue);
final String key;
final T defaultValue;
}
The above example shouldShowWalkthrough
defines a key should_show_walkthrough
that controls the display of the walkthrough, and a default value true
if the value was not saved.
It is a bit complicated, but enum Preferepnce<T>
using Generics can also be used with V2 Riverpod (actual usage examples are described later).
Define Provider to Read/Write SharedPreferences
Define a Notifier to read/write values from/to SharedPreferences.
Normally, you would like to use a type parameter (<T>
) in the class declaration of the Notifier as shown below, but this is not supported in Riverpod Generator V2.
@riverpod
class Preference<T> extends _$Preference<T> {
@override
T build(Preferepnce<T> pref) => ...
So how is it done? For example, the Provider that reads and writes Bool implements the following.
@riverpod
class BoolPreference extends _$BoolPreference {
@override
bool build(Preferepnce<bool> pref) =>
ref.read(sharedPreferencesProvider).getBool(pref.key) ??
pref.defaultValue;
// ignore: avoid_positional_boolean_parameters
Future<void> update(bool value) async {
await ref.read(sharedPreferencesProvider).setBool(pref.key, value);
ref.invalidateSelf();
}
}
The above code specifies the type in the following places
- build function
- Specify
bool
for the return type -
Preferepnce<T>
type parameter:bool
.
- Specify
- update function
- Specify
bool
for thevalue
argument
- Specify
As you can see, the implementation of a notifier is necessary for each type handled by SharedPreferences because it is necessary to specify the specific type in the notifier, resulting in duplication of code.
For reference, here are the definitions of Bool, Int, and String notifiers.
@riverpod
class BoolPreference extends _$BoolPreference {
@override
bool build(Preferepnce<bool> pref) =>
ref.read(sharedPreferencesProvider).getBool(pref.key) ??
pref.defaultValue;
// ignore: avoid_positional_boolean_parameters
Future<void> update(bool value) async {
await ref.read(sharedPreferencesProvider).setBool(pref.key, value);
ref.invalidateSelf();
}
}
@riverpod
class IntPreference extends _$IntPreference {
@override
int build(Preferepnce<int> pref) =>
ref.read(sharedPreferencesProvider).getInt(pref.key) ??
pref.defaultValue;
Future<void> update(int value) async {
await ref.read(sharedPreferencesProvider).setInt(pref.key, value);
ref.invalidateSelf();
}
}
@riverpod
class StringPreference extends _$StringPreference {
@override
String build(Preferepnce<String> pref) =>
ref.read(sharedPreferencesProvider).getString(pref.key) ??
pref.defaultValue;
Future<void> update(String value) async {
await ref.read(sharedPreferencesProvider).setString(pref.key, value);
ref.invalidateSelf();
}
}
Code for the caller
The caller's code would look like this
Code referring to a bool value
final shouldShowWalkthrough = ref.watch(boolPreferenceProvider(Preferepnce.shouldShowWalkthrough));
Code to update the bool value.
ref.read(boolPreferenceProvider(Preferepnce.shouldShowWalkthrough).notifier).update(false);
Note that by ref.invalidateSelf();
updating itself in the update()
function, the behavior is such that value updates are reflected reactively.
Future<void> update(String value) async {
await ref.read(sharedPreferencesProvider).setString(pref.key, value);
ref.invalidateSelf();
}
This was an example implementation in V2.
V3: Implementation using Generics
So what is the implementation in Riverpod V3 that supports Generics?
As it turns out, you can declare a Notifier for multiple types by defining a Notifier with the type parameter as follows.
@riverpod
class PreferenceNotifier<T> extends _$PreferenceNotifier<T> {
@override
T build(Preference<T> pref) {
return ref.watch(sharedPreferencesProvider).getValue(pref);
}
Future<void> update(T value) async {
await ref.read(sharedPreferencesProvider).setValue(pref, value);
ref.invalidateSelf();
}
}
read and write would be code like this.
// read
ref.watch(preferenceNotifierProvider(Preference.shouldShowWalkthrough));
// write
ref.read(preferenceNotifierProvider(Preference.shouldShowWalkthrough).notifier).update(false);
Thus, in V2, a Notifier was prepared for each type of variable, but with the support of Generics, it is now possible to implement a common Notifier.
Note that getValue()
and setValue()
are methods created in SharedPreferences using extension. They call getter/setter of SharedPreferences body according to the type, which is complicated, but it is unavoidable because it is a place where abstraction is not possible.
extension on SharedPreferences {
T getValue<T>(Preference<T> prefKey) {
if (prefKey.defaultValue is bool) {
final value = getBool(prefKey.key);
return value == null ? prefKey.defaultValue : value as T;
} else if (prefKey.defaultValue is int) {
final value = getInt(prefKey.key);
return value == null ? prefKey.defaultValue : value as T;
} else if (prefKey.defaultValue is double) {
final value = getDouble(prefKey.key);
return value == null ? prefKey.defaultValue : value as T;
} else if (prefKey.defaultValue is String) {
final value = getString(prefKey.key);
return value == null ? prefKey.defaultValue : value as T;
} else if (prefKey.defaultValue is List<String>) {
final value = getStringList(prefKey.key);
return value == null ? prefKey.defaultValue : value as T;
} else {
throw UnimplementedError(
'''SharedPreferencesExt.getValue: unsupported types ${prefKey.defaultValue.runtimeType}''',
);
}
}
Future<void> setValue<T>(Preference<T> prefKey, T value) {
if (value is bool) {
return setBool(prefKey.key, value);
} else if (value is int) {
return setInt(prefKey.key, value);
} else if (value is double) {
return setDouble(prefKey.key, value);
} else if (value is String) {
return setString(prefKey.key, value);
} else if (value is List<String>) {
return setStringList(prefKey.key, value);
} else {
throw UnimplementedError(
'''SharedPreferencesExt.setValue: unsupported types ${prefKey.defaultValue.runtimeType}''',
);
}
}
}
Sample Code.
Sample code for a counter app created with flutter create
and persisted using Riverpod V3 Generics and SharedPreferences. The counter values are saved in SharedPreferences and restored when the app is restarted.
https://github.com/yorifuji/riverpod_generics_sandbox
Here is where we are using Riverpod on the Widget side
Here is the code for Riverpod and SharedPreferences
You can try the actual operation from the following URL (Flutter Web)
https://yorifuji.github.io/riverpod_generics_sandbox/
Conclusion
Although it seemed a bit over speculative to use Generics to share SharedPreferences input/output, I actually wrote it and found it useful, so I thought I would actively use it when it is released.
Top comments (0)