DEV Community

YORIFUJI
YORIFUJI

Posted on

Riverpod's next version V3 will support Generics

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>[];
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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(),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

After this, you can use instances of SharedPreferences synchronously with the following code.

final sharedPreference = ref.watch(sharedPreferencesProvider);

final someBoolValue = sharedPreference.getBool(...)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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) => ...
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

The above code specifies the type in the following places

  • build function
    • Specify bool for the return type
    • Preferepnce<T> type parameter: bool.
  • update function
    • Specify bool for the value argument

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Code to update the bool value.

ref.read(boolPreferenceProvider(Preferepnce.shouldShowWalkthrough).notifier).update(false);
Enter fullscreen mode Exit fullscreen mode

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();
  }
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

read and write would be code like this.

// read
ref.watch(preferenceNotifierProvider(Preference.shouldShowWalkthrough));

// write
ref.read(preferenceNotifierProvider(Preference.shouldShowWalkthrough).notifier).update(false);
Enter fullscreen mode Exit fullscreen mode

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}''',
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

Here is where we are using Riverpod on the Widget side

https://github.com/yorifuji/riverpod_generics_sandbox/blob/756f61e578552790679a10d97d0f84cd1bbc90e0/lib/main.dart#L42-L52

Here is the code for Riverpod and SharedPreferences

https://github.com/yorifuji/riverpod_generics_sandbox/blob/8c47a32d3fe2f55d4b3246712dc1377b74eb3bc4/lib/preference.dart#L6-L73

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)