With every new release, Riverpod and the ecosystem around it keep getting better:
- the core packages give us powerful APIs for reactive caching and data binding
- the Riverpod Generator package simplifies the learning curve and brings significant usability improvements (which I have already covered here)
- the Riverpod Snippets extension helps us create providers and consumers with ease
And the new Riverpod Lint package adds many useful lints and refactoring options that make writing Flutter apps a breeze.
But hold on...
It's better than that.
Modern development with Riverpod is a joy, as you can write complex features like search and pagination with little code (and let the tools guide you).
And in this article, I'll show you how I refactored my time tracking app to the new @riverpod
syntax using all the latest goodies, including code generation, code snippets, refactoring options, and the new Riverpod lints.
This article will show how Riverpod Generator, Riverpod Lint, and the Riverpod Snippets extension are a great combo when used together, but it's not intended as a complete resource. Check the documentation of each package for all the use cases.
Adding all the required packages
Since we'll be using Riverpod Generator and Riverpod Lint, we need to add a few packages to our pubspec.yaml
file:
dependencies:
# the main riverpod package for Flutter apps
flutter_riverpod:
# the annotation package containing @riverpod
riverpod_annotation:
dev_dependencies:
# a tool for running code generators
build_runner:
# the code generator
riverpod_generator:
# riverpod_lint makes it easier to work with Riverpod
riverpod_lint:
# import custom_lint too as riverpod_lint depends on it
custom_lint:
And we also need to enable the custom_lint
plugin inside analysis_options.yaml
:
analyzer:
plugins:
- custom_lint
Next up, we need to start build_runner in watch mode:
dart run build_runner watch -d
The
-d
flag is optional and is the same as--delete-conflicting-outputs
. As the name implies, it ensures that we override any conflicting outputs from previous builds (which is normally what we want).
This will watch all the Dart files in our project and automatically update the generated code when we make changes.
And now that the setup is complete, let's look at some code. 👇
Refactoring example with Riverpod Generator and Riverpod Lint
Suppose we have an AuthRepository
class we use as a wrapper for the FirebaseAuth
class:
// firebase_auth_repository.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AuthRepository {
AuthRepository(this._auth);
final FirebaseAuth _auth;
Stream<User?> authStateChanges() => _auth.authStateChanges();
}
And suppose we also have these providers:
// Make the [FirebaseAuth] instance accessible as a provider
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
// Make the [AuthRepository] instance accessible as a provider
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository(ref.watch(firebaseAuthProvider));
});
// A [StreamProvider] for the [authStateChanges] stream
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
return ref.watch(authRepositoryProvider).authStateChanges();
});
How can we convert the providers above the new Riverpod Generator syntax?
Adding a part file
As we've seen in my article about Riverpod Generator, the first step is to add a part file.
And by using the Flutter Riverpod Snippets extension, we can just type a few characters:
And the extension automatically completes this with the correct file name:
part 'firebase_auth_repository.g.dart';
Converting a simple provider
Next, let's see how to convert this provider:
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
Once again, we can start by typing riverpod
and get a list of options:
When deciding which option to choose, we can ask ourselves:
- will this provider return an object, a
Future
, aStream
, or aNotifier
? - should it dispose itself when no longer listened to, or should it keep alive?
Since FirebaseAuth
is a singleton that remains alive for the entire app lifecycle, we can choose the riverpodKeepAlive
option and end up with this:
The next step is to fill in the blanks by adding:
- a return type
- the function name
- any additional arguments (none in this case)
- the provider body
Here's what we end up with:
@Riverpod(keepAlive: true)
FirebaseAuth firebaseAuth(Ref ref) { // Stateless providers must receive a ref matching the provider name as their first positional parameter.dart(stateless_ref)
return FirebaseAuth.instance;
}
This code is almost correct. But Riverpod Lint reminds us that we must use the correct type since the generator creates a specific Ref
type for each provider.
In fact, there's a strict relationship between the name of the function (firebaseAuth
) and the generated Ref
type and provider name:
firebaseAuth()
→FirebaseAuthRef
andfirebaseAuthProvider
So let's use the Quick Fix once again:
And voilà ! The linter warning goes away:
@Riverpod(keepAlive: true)
FirebaseAuth firebaseAuth(FirebaseAuthRef ref) {
return FirebaseAuth.instance;
}
And as long as build_runner
is still running in watch mode, a firebaseAuthProvider
will be generated (inside the part file) and be ready for use in our code.
Refactoring the remaining providers
Next up, we need to refactor the two remaining providers as well:
// Make the [AuthRepository] instance accessible as a provider
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository(ref.watch(firebaseAuthProvider));
});
// A [StreamProvider] for the [authStateChanges] stream
final authStateChangesProvider = StreamProvider<User?>((ref) {
return ref.watch(authRepositoryProvider).authStateChanges();
});
And with the help of Riverpod Snippets and Riverpod Lint, this is easily done:
@Riverpod(keepAlive: true)
AuthRepository authRepository(AuthRepositoryRef ref) {
return AuthRepository(ref.watch(firebaseAuthProvider));
}
@riverpod
Stream<User?> authStateChanges(AuthStateChangesRef ref) {
return ref.watch(authRepositoryProvider).authStateChanges();
}
Note how I've chosen to use
keepAlive
for thefirebaseAuthProvider
and theauthRepositoryProvider
, but not for theauthStateChangesProvider
. This makes sense since the first two providers contain long-lived dependencies, while the third may or may not need to be always listened to.
Example: Generating an AsyncNotifier
In addition to creating providers for objects, futures, and streams, we also want to generate providers for classes like Notifier
and AsyncNotifier
.
For example, here's an AsyncNotifier
subclass I had in my project:
class EditJobScreenController extends AutoDisposeAsyncNotifier<void> {
@override
FutureOr<void> build() {
// omitted
}
// some methods
}
I could have converted this by hand.
But Riverpod Snippets helps us again with handy riverpodAsyncClass
and riverpodClass
options:
By choosing the option above, we end up with this code:
And then, we can just fill in the blanks:
@riverpod
class EditJobScreenController extends _$EditJobScreenController {
@override
FutureOr<void> build() {
// omitted
}
}
What else can Riverpod Lint do?
The examples above show how to convert existing providers or notifiers to the new syntax.
But you can do so much more with Riverpod Lint, including:
- Convert from
StatelessWidget
toConsumerWidget
orConsumerStatefulWidget
- Convert between functional and class variants
Once again, check the official docs for the full list of options.
Conclusion
In the early days of Riverpod, it was hard to choose the correct provider and get the syntax right (especially when dealing with complex providers with arguments).
But as we've seen, Riverpod Generator and Riverpod Lint make our life much easier.
And nowadays, converting any provider to the new @riverpod
syntax is just a case of:
- adding
part
directives using the Riverpod Snippets extension - choosing the right provider (again with Riverpod Snippets)
- filling in the blanks (return type, function name and arguments)
- choosing the correct
Ref
type (Riverpod Lint makes this easier)
And once this is done, we can save the file and build_runner
takes care of the rest.
After using it and watching it evolve for over two years, I feel that Riverpod is solving all the right problems, in the right way.
And I have no doubt that it will have a bright future.
Happy coding!
Flutter Foundations Course Now Available
I launched a brand new course that covers state management with Riverpod in great depth, along with other important topics like app architecture, routing, testing, and much more: