Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
Riverpod_lint adds various warnings with quick fixes and refactoring options, such as:
- Warn if
runApp
does not include aProviderScope
at its root - Warn if provider parameters do not respect the rules of
family
- Refactor a widget to a ConsumerWidget/ConsumerStatefulWidget
- ...
Table of content
- Table of content
- Installing riverpod_lint
- Enabling/disabling lints.
- Running riverpod_lint in the terminal/CI
- All the lints
- missing_provider_scope
- provider_dependencies (riverpod_generator only)
- scoped_providers_should_specify_dependencies (generator only)
- avoid_manual_providers_as_generated_provider_dependency
- avoid_build_context_in_providers (riverpod_generator only)
- provider_parameters
- avoid_public_notifier_properties
- unsupported_provider_value (riverpod_generator only)
- functional_ref (riverpod_generator only)
- notifier_extends (riverpod_generator only)
- avoid_ref_inside_state_dispose
- notifier_build (riverpod_generator only)
- async_value_nullable_patttern
- protected_notifier_properties
- All assists
- Upcoming content:
Installing riverpod_lint
Riverpod_lint is implemented using custom_lint. As such, it uses custom_lint's installation logic.
Long story short:
-
Add both riverpod_lint and custom_lint to your
pubspec.yaml
:dev_dependencies: custom_lint: riverpod_lint:
-
Enable
custom_lint
's plugin in youranalysis_options.yaml
:analyzer: plugins: - custom_lint
Enabling/disabling lints.
By default when installing riverpod_lint, most of the lints will be enabled. To change this, you have a few options.
Disable one specific rule
You may dislike one of the various lint rules offered by riverpod_lint.
In that event, you can explicitly disable this lint rule for your project
by modifying the analysis_options.yaml
analyzer:
plugins:
- custom_lint
custom_lint:
rules:
# Explicitly disable one lint rule
- missing_provider_scope: false
Note that you can both enable and disable lint rules at once.
This can be useful if your analysis_options.yaml
includes another one:
include: path/to/another/analysis_options.yaml
analyzer:
plugins:
- custom_lint
custom_lint:
rules:
# Enable one rule
- provider_parameters
# Disable another
- missing_provider_scope: false
Disable all lints by default
Instead of having all lints on by default and manually disabling lints of your choice,
you can switch to the opposite logic:
Have lints off by default, and manually enable lints.
This can be done in your analysis_options.yaml
with the following:
analyzer:
plugins:
- custom_lint
custom_lint:
# Forcibly disable lint rules by default
enable_all_lint_rules: false
rules:
# You can now enable one specific rule in the "rules" list
- missing_provider_scope
Running riverpod_lint in the terminal/CI
Custom lint rules created by riverpod_lint may not show-up in dart analyze
.
To fix this, you can run a custom command line: custom_lint
.
Since your project should already have custom_lint installed (cf installing riverpod_lint), then you should be able to run:
dart run custom_lint
Alternatively, you can globally install custom_lint
:
# Install custom_lint for all projects
dart pub global activate custom_lint
# run custom_lint's command line in a project
custom_lint
All the lints
missing_provider_scope
Flutter applications using Riverpod should have a ProviderScope widget at the top of the widget tree.
Good:
void main() {
runApp(ProviderScope(child: MyApp()));
}
Bad:
void main() {
runApp(MyApp());
}
provider_dependencies (riverpod_generator only)
If a provider depends on providers which specify dependencies
, they should
specify dependencies
and include all the scoped providers.
This lint only works for providers using the @riverpod
annotation.
Consider the following providers:
// A non-scoped provider
@riverpod
int root(Ref ref) => 0;
// A possibly scoped provider
@Riverpod(dependencies: [])
int scoped(Ref ref) => 0;
Good:
// No dependencies used, no need to specify "dependencies"
@riverpod
int example(Ref ref) => 0;
// We can specify an empty "dependencies" list if we wish to.
// This marks the provider as "scoped".
@Riverpod(dependencies: [])
int example(Ref ref) => 0;
@riverpod
void example(Ref ref) {
// rootProvider is not scoped, no need to specify it as "dependencies"
ref.watch(rootProvider);
}
@Riverpod(dependencies: [scoped])
void example(Ref ref) {
// scopedProvider is scoped and as such specifying "dependencies" is required.
ref.watch(scopedProvider);
}
Bad:
// scopedProvider isn't used and should therefore not be listed
@Riverpod(dependencies: [scoped])
int example(Ref ref) => 0;
@Riverpod(dependencies: [])
void example(Ref ref) {
// scopedProvider is used but not present in the list of dependencies
ref.watch(scopedProvider);
}
@Riverpod(dependencies: [root])
void example(Ref ref) {
// rootProvider is not a scoped provider. As such it shouldn't be listed in "dependencies"
ref.watch(rootProvider);
}
scoped_providers_should_specify_dependencies (generator only)
Providers that do not specify "dependencies" shouldn't be overridden in a
ProviderScope
/ProviderContainer
that is possibly not at the root of the tree.
Consider the following providers:
@Riverpod(dependencies: [])
int scoped(Ref ref) => 0;
@riverpod
int root(Ref ref) => 0;
(Providers defined without riverpod_generator
are not supported)
Good
void main() {
runApp(
ProviderScope(
// This is the main ProviderScope. All providers can be overridden there
overrides: [
rootProvider.overrideWith(...),
scopedProvider.overrideWith(...),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
// This ProviderScope is not the root one, so only providers with "dependencies"
// can be specified.
overrides: [
scopedProvider.overrideWith(...),
],
child: Container(),
);
}
}
Bad:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
// This ProviderScope is not the root one, so only providers with "dependencies"
// can be specified.
overrides: [
// rootProvider does not specify "dependencies" and therefore should not
// be overridden here.
rootProvider.overrideWith(...),
],
child: Container(),
);
}
}
avoid_manual_providers_as_generated_provider_dependency
Providers using riverpod_generator should not depend on providers which do not use riverpod_generator. Failing to do so would break the provider_dependencies lint.
Good:
@riverpod
int dep(Ref ref) => 0;
@riverpod
void example(Ref ref) {
/// Generated providers can depend on other generated providers
ref.watch(depProvider);
}
Bad:
final depProvider = Provider((ref) => 0);
@riverpod
void example(Ref ref) {
// Generated providers should not depend on non-generated providers
ref.watch(depProvider);
}
avoid_build_context_in_providers (riverpod_generator only)
Providers should not interact with BuildContext
.
Good:
@riverpod
int fn(Ref ref) => 0;
@riverpod
class MyNotifier extends _$MyNotifier {
int build() => 0;
void event() {}
}
Bad:
// Providers should not receive a BuildContext as a parameter.
int fn(Ref ref, BuildContext context) => 0;
@riverpod
class MyNotifier extends _$MyNotifier {
int build() => 0;
// Notifiers should not have methods that receive a BuildContext as a parameter.
void event(BuildContext context) {}
}
provider_parameters
Providers' parameters should have a consistent ==. Meaning either the values should be cached, or the parameters should override ==.
Good:
// Parameters may override ==
ref.watch(myProvider(ClassThatOverridesEqual()));
// Constant objects are canonalized and therefore have a consistent ==
ref.watch(myProvider(const Object()));
ref.watch(myProvider(const [42]));
// Passing a variable disable the lint, as the variable may be cached.
ref.watch(myProvider(variable));
Bad:
// New instances passed as provider parameter must override ==
ref.watch(myProvider(ClassWithNoCustomEqual()));
// List/map/set litterals do not have a custom ==
ref.watch(myProvider([42]));
avoid_public_notifier_properties
The Notifier
/AsyncNotifier
classes should not have a public state outside
of the state
property.
The reasoning is that all "state" should be accessed through the .state
property.
There should never be a case where you do ref.watch(someProvider.notifier).someState
.
Instead, you should do ref.watch(provider).someState
.
Bad:
@riverpod
class GeneratedNotifier extends _$GeneratedNotifier {
// Notifiers should not have public properties/getters
int b = 0;
@override
int build() => 0;
}
Good:
class Model {
Model(this.a, this.b);
final int a;
final int b;
}
// Notifiers using the code-generator
@riverpod
class MyNotifier extends _$MyNotifier {
// No public getters/fields, this is fine. Instead
// Everything is available in the `state` object.
@override
Model build() => Model(0, 0);
}
@riverpod
class MyNotifier extends _$MyNotifier {
// Alternatively, notifiers are allowed to have properties/getters if they
// are either private or annotated such that using inside widgets would
// trigger a warning.
int _internal = 0;
@protected
int publicButProtected = 0;
@visibleForTesting
int testOnly = 0;
@override
Something build() {...}
}
unsupported_provider_value (riverpod_generator only)
The riverpod_generator package does not support StateNotifier
/ChangeNotifier
and
manually creating a Notifier
/AsyncNotifier
.
This lint warns against unsupported value types.
Note:
In some cases, you may voluntarily want to return a ChangeNotifier
& co, even though
riverpod_generator will neither listen nor disposes of the value.
In that scenario, you may explicitly wrap the value in Raw
:
Good:
@riverpod
int integer(Ref ref) => 0;
@riverpod
class IntegerNotifier extends _$IntegerNotifier {
@override
int build() => 0;
}
// By using "Raw", we can explicitly return a ChangeNotifier in a provider
// without triggering `unsupported_provider_value`.
@riverpod
Raw<GoRouter> myRouter(Ref ref) {
final router = GoRouter(...);
// Riverpod won't dispose the ChangeNotifier for you in this case. Don't forget
// to do it on your own!
ref.onDispose(router.dispose);
return router;
}
Bad:
// KO: riverpod_generator does not support creating StateNotifier/ChangeNotifiers/...
// Instead annotate a class with @riverpod.
@riverpod
MyStateNotifier stateNotifier(Ref ref) => MyStateNotifier();
class MyStateNotifier extends StateNotifier<int> {
MyStateNotifier(): super(0);
}
functional_ref (riverpod_generator only)
Functional providers must receive a ref matching the provider name as their first positional parameter.
Good:
@riverpod
int myProvider(Ref ref) => 0;
Bad:
// No "ref" parameter found
@riverpod
int myProvider() => 0;
// The ref parameter is not correctly typed (int -> Ref)
@riverpod
int myProvider(int ref) => 0;
notifier_extends (riverpod_generator only)
Classes annotated by @riverpod
must extend _$ClassName
Good:
@riverpod
class Example extends _$Example {
int build() => 0;
}
Bad:
// No "extends" found
@riverpod
class Example {
int build() => 0;
}
// An "extends" is present, but is not matching the class name
@riverpod
class Example extends Anything {
int build() => 0;
}
avoid_ref_inside_state_dispose
Avoid using Ref
in the dispose method.
Bad:
class _MyWidgetState extends ConsumerState<MyWidget> {
@override
void dispose() {
// Do not use 'ref' in the dispose method
ref.read(provider).doSomething();
super.dispose();
}
// ...
}
notifier_build (riverpod_generator only)
Classes annotated by @riverpod
must have the build
method.
Good:
@riverpod
class Example extends _$Example {
@override
int build() => 0;
}
Bad:
// No "build" method found
@riverpod
class Example extends _$Example {}
async_value_nullable_patttern
Warn if the pattern AsyncValue(:final value?)
is used when the data
is possibly nullable.
Bad:
switch (...) {
// int? is nullable, therefore ":final value?" should not be used
case AsyncValue<int?>(:final value?):
print('data $value');
}
Good:
switch (...) {
// int is non-nullable, so using ":final value?" is fine.
case AsyncValue<int>(:final value?):
print('data $value');
}
switch (...) {
// int? is nullable, therefore we use "hasValue: true"
case AsyncValue<int?>(:final value, hasValue: true):
print('data $value');
}
protected_notifier_properties
Notifiers should not access the state of other notifiers.
This includes .state
, .future
, and .ref
.
Bad:
@riverpod
class A extends _$A {
@override
int build() => 0;
}
@riverpod
class B extends _$B {
@override
int build() => 0;
void doSomething() {
// KO: Reading protected properties
ref.read(aProvider.notifier).state++;
}
}
Good:
@riverpod
class A extends _$A {
@override
int build() => 0;
void increment() {
state++;
}
}
@riverpod
class B extends _$B {
@override
int build() => 0;
void doSomething() {
// Only access what notifiers explicitly enables us to.
ref.read(aProvider.notifier).increment();
}
}
All assists
Wrap widgets with a Consumer
Wrap widgets with a ProviderScope
Convert widget to ConsumerWidget
Convert widget to ConsumerStatefulWidget
Convert functional @riverpod
to class variant
Convert class @riverpod
to functional variant
Upcoming content:
- Warn if a provider's
dependencies
parameter doesn't match theref.watch/read/listen
usages. - Refactoring to convert AsyncNotifier<>Notifier + autoDispose/family variants
- Warn if an
AsyncNotifierProvider.autoDispose
doesn't use anAutoDisposeAsyncNotifier
and much more