How to write Flutter apps faster with Riverpod Lint & Riverpod Snippets

Source code on GitHub

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:

We can declare the part file with the Riverpod Snippets extension
We can declare the part file with the Riverpod Snippets extension

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:

List of available options when creating a provider
List of available options when creating a provider

When deciding which option to choose, we can ask ourselves:

  • will this provider return an object, a Future, a Stream, or a Notifier?
  • 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:

Output from the riverpodKeepAlive snippet
Output from the riverpodKeepAlive snippet

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 and firebaseAuthProvider

So let's use the Quick Fix once again:

Using the Quick Fix option to choose the correct Ref type
Using the Quick Fix option to choose the correct Ref type

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 the firebaseAuthProvider and the authRepositoryProvider, but not for the authStateChangesProvider. 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:

Snippets for creating a Riverpod class type
Snippets for creating a Riverpod class type

By choosing the option above, we end up with this code:

Resulting snippet for the riverpodAsyncClass option
Resulting snippet for the riverpodAsyncClass option

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 to ConsumerWidget or ConsumerStatefulWidget
  • 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:

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.