Riverpod Simplified Part II: Lessons Learned From 4 Years of Development


Introduction
The response to my "Riverpod Simplified" article caught me off guard. What started as a collection of personal insights from 4 years of Riverpod work sparked conversations across Twitter, Reddit, and LinkedIn that I never expected.
Between coffee breaks and late-night coding sessions, I've had time to reflect on all your questions. Two topics kept surfacing: annotations (which promised to make our lives easier) and that mysterious .family
modifier that seems to cause more headaches than solutions.
So let's talk. Not as a tutorial writer showing off perfect code, but as one developer to another who's battled the same frustrations you have. I've made every mistake possible with Riverpod- deployed broken code, debugged weird state issues at 2 AM, and questioned my life choices while staring at provider chains that made no sense the next morning.
The truth about annotations
Riverpod annotations were introduced with promises of simplified code and reduced boilerplate. After extensive use, here's my candid assessment:
→ they are mostly useless.
There are a couple of reasons for this:
Double effort: The amount of code for generating a Provider is the exact same as just writing it by hand. Add waiting for
build_runner
to generate the same thing again and you have gained absolutely nothing but a headache.//no annotation final userRepositoryProvider = Provider<UserRepository>((ref) { return UserRepository(userApi: ref.read(userApiProvider)); } //annotation @riverpod(keepAlive: true) UserRepository userRepository(Ref ref) { return UserRepository(userApi: ref.read(userApiProvider)); }
Macros cancellation: When Riverpod annotations were introduced, the Flutter team was working on macros that would have made them more powerful. However, now that macros are scrapped, annotations offer minimal value.
AI: I don’t know anybody who doesn’t use Github Copilot, Cursor, Windsurf, or any other AI helper in their IDE. Given their speed and accuracy, it’s often better to let it write the boilerplate than generate it.
Modifiers
Riverpod offers two powerful modifiers: .autoDispose
and .family
. Used correctly, they solve specific problems; used incorrectly, they create confusion in the best case, and bugs in the worst case.
.autoDispose
The .autoDispose
modifier ensures your provider is disposed of when no longer used.
Notifiers often manage a state that doesn't need to be kept indefinitely. Using autoDispose
on *NotifierProviders
prevents memory leaks and ensures a fresh state when the provider is used again.
The exceptions to the rule are global singletons or persistent states (like authentication) that should live for the app's entire lifecycle.
.family
The .family
modifier allows passing parameters to providers but is often misused because people don’t read the documentation.
Because of the way this modifier works, the parameters need to satisfy two conditions: immutability and comparability.
Primitive types like int
, String
, or bool
satisfy both conditions.
Classes must implement ==
and hashCode
correctly and have all properties marked as final
. If any property is var
, it means that ==
will return false, and a different Provider instance will be created. You can generate the methods using packages like freezed
or equatable
, write them on your own or use AI.
I’ve also seen some people use collections (lists, sets, maps), which means they lack understanding of these conditions. Consider the following code:
void main() {
final list1 = [1,2,3];
final list2 = [1,2,3];
print(list1==list2); // false
}
Why would this code print false? Because collections are complex and are compared by reference, not by value. As such, they cannot be used as parameters on their own, but they can be used as an object property.
Conclusion
After working extensively with Riverpod, I've found that simplicity often yields the best results. While annotations may seem appealing initially, they add complexity without significant benefits, especially with modern AI coding assistants handling boilerplate efficiently.
Regarding modifiers, use .autoDispose
for the temporary state but avoid it for persistent application state. Be extremely cautious with .family
modifiers—ensure your parameters are immutable and properly comparable, or you'll introduce subtle bugs that are difficult to trace.
If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter and LinkedIn.
Subscribe to my newsletter
Read articles from Dinko Marinac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dinko Marinac
Dinko Marinac
Mobile app developer and consultant. CEO @ MOBILAPP Solutions. Passionate about the Dart & Flutter ecosystem.