Riverpod Generator's next version V3 will support Generics
Tags: flutter
Published: 2024-04-29 10:36:07

Riverpod's next version V3 will [support Generics](https://github.com/rrousselGit/riverpod/blob/riverpod_generator-v3.0.0-dev.11/packages/riverpod_generator/CHANGELOG.md#300-dev7---2023-10-29
).

Here is the code presented as a sample code, which makes it possible to specify a type parameter (`T` in the example below).

```dart
@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](https://pub.dev/packages/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``.

```dart
// 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.

```dart
@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.

```dart
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.

```dart
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>`.

```dart
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.

```dart
@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.

```dart
@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`.
- 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.

```dart
@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

```dart
final shouldShowWalkthrough = ref.watch(boolPreferenceProvider(Preferepnce.shouldShowWalkthrough));
```

Code to update the bool value.

```dart
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.

```dart
  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 <T> as follows.

```dart
@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.

```dart
// 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.

```dart
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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0xph375s5lfv7eytbrjv.png)

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.