Aller au contenu principal

About code generation

Code generation is the idea of using a tool to generate code for us. In Dart, it comes with the downside of requiring an extra step to "compile" an application. Although this problem may be solved in the near future, as the Dart team is working on a potential solution to this problem.

In the context of Riverpod, code generation is about slightly changing the syntax for defining a "provider". For example, instead of:

final fetchUserProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
});

Using code generation, we would write:


Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
}

When using Riverpod, code generation is completely optional. It is entirely possible to use Riverpod without. At the same time, Riverpod embraces code generation and recommends using it.

For information on how to install and use Riverpod's code generator, refer to the Getting started page. Make sure to enable code generation in the documentation's sidebar.

Should I use code generation?

Code generation is optional in Riverpod. With that in mind, you may wonder if you should use it or not.

The answer is: Most likely Yes.
Using code generation is the recommended way to use Riverpod. It is the more future-proof approach and will allow you to use Riverpod to its full potential.
At the same time, many applications already use code generation with packages such as Freezed or json_serializable. In that case, your project probably is already set up for code generation, and using Riverpod should be simple.

Currently, code generation is optional because build_runner is disliked by many. But once Static Metaprogramming is available in Dart, build_runner will no longer be an issue. At that point, the code generation syntax will be the only syntax available in Riverpod.

If using build_runner is a deal-breaker for you, then and only then you should consider not using code generation. But keep in mind that you will be missing out on some features, and that you will have to migrate to code generation in the future.
Although when that happens, Riverpod will provide a migration tool to make the transition as smoothly as possible.

What are the benefits of using code generation?

You may be wondering: "If code generation is optional in Riverpod, why use it?"

As always with packages: To make your life easier. This includes but is not limited to:

  • better syntax, more readable/flexible, and with a reduced learning curve.
    • No need to worry about the type of provider. Write your logic, and Riverpod will pick the most suitable provider for you.
    • The syntax no longer looks like we're defining a "dirty global variable". Instead we are defining a custom function/class.
    • Passing parameters to providers is now unrestricted. Instead of being limited to using .family and passing a single positional parameter, you can now pass any parameter. This includes named parameters, optional ones, and even default values.
  • stateful hot-reload of the code written in Riverpod.
  • better debugging, through the generation of extra metadata that the debugger then picks up.
  • some Riverpod features will be available only with code generation.

The Syntax

Defining a provider:

When defining a provider using code generation, it is helpful to keep in mind the following points:

  • Providers can be defined either as an annotated function or as an annotated class. They are pretty much the same, but Class-based provider has the advantage of including public methods that enable external objects to modify the state of the provider (side-effects). Functional providers are syntax sugar for writing a Class-based provider with nothing but a build method, and as such cannot be modified by the UI.
  • All Dart async primitives (Future, FutureOr, and Stream) are supported.
  • When a function is marked as async, the provider automatically handles errors/loading states and exposes an AsyncValue.
Functional
(Can’t perform side-effects
using public methods)
Class-Based
(Can perform side-effects
using public methods)
Sync

String example(ExampleRef ref) {
return 'foo';
}

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
Async - Future

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
Async - Stream

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}

Enabling/disable autoDispose:

When using code generation, providers are autoDispose by default. That means that they will automatically dispose of themselves when there are no listeners attached to them (ref.watch/ref.listen).
This default setting better aligns with Riverpod's philosophy. Initially with the non-code generation variant, autoDispose was off by default to accommodate users migrating from package:provider.

If you want to disable autoDispose, you can do so by passing keepAlive: true to the annotation.

// AutoDispose provider (keepAlive is false by default)

String example1(Example1Ref ref) => 'foo';

// Non autoDispose provider
(keepAlive: true)
String example2(Example2Ref ref) => 'foo';

Passing parameters to a provider (family):

When using code generation, we no-longer need to rely on the family modifier to pass parameters to a provider. Instead, the main function of our provider can accept any number of parameters, including named, optional, or default values.
Do note however that these parameters should have still have a consistent ==. Meaning either the values should be cached, or the parameters should override ==.

FunctionalClass-Based

String example(
ExampleRef ref,
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

class Example extends _$Example {

String build(
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

// Add methods to mutate the state
}

Migrate from non-code-generation variant:

When using non-code-generation variant, it is necessary to manually determine the type of your provider. The following are the corresponding options for transitioning into code-generation variant:

Provider
Before
final exampleProvider = Provider.autoDispose<String>(
(ref) {
return 'foo';
},
);
After

String example(ExampleRef ref) {
return 'foo';
}
NotifierProvider
Before
final exampleProvider = NotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeNotifier<String> {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
FutureProvider
Before
final exampleProvider =
FutureProvider.autoDispose<String>((ref) async {
return Future.value('foo');
});
After

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}
StreamProvider
Before
final exampleProvider =
StreamProvider.autoDispose<String>((ref) async* {
yield 'foo';
});
After

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}
AsyncNotifierProvider
Before
final exampleProvider =
AsyncNotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeAsyncNotifier<String> {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
StreamNotifierProvider
Before
final exampleProvider =
StreamNotifierProvider.autoDispose<ExampleNotifier, String>(() {
return ExampleNotifier();
});

class ExampleNotifier extends AutoDisposeStreamNotifier<String> {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}
After

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}