As a Flutter developer, I've always been on the lookout for efficient ways to manage app state and dependencies. In my journey, I've come across Riverpod, a powerful library that I like to think of as the Swiss Army knife of Flutter development. It offers elegant solutions for both state management and dependency injection, giving you the freedom to "cook" your app architecture just the way you like it.
This flexibility, while powerful, can sometimes be overwhelming. The main challenge often lies in finding the optimal way to use Riverpod for your specific needs. In this post, I want to share with you a production-ready example from my own experience. We'll explore Riverpod's key features and then dive into a real-world example of how to implement a clean, modular dependency injection system.
But before we dive in, let's take a look at the dependencies we'll be using. Here's the pubspec.yaml
file for our project:
name: riverpod_di_example
description: "Example of using Riverpod for managing application dependencies"
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.4.3 <4.0.0"
dependencies:
dio: ^5.5.0+1
flutter:
sdk: flutter
flutter_hooks: ^0.20.5
flutter_riverpod: ^2.5.1
hooks_riverpod: ^2.5.1
very_good_analysis: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
Now that we have our dependencies set up, let's dive into what makes Riverpod such a versatile tool.
What is Riverpod?
Riverpod is a robust state management library for Flutter that offers a declarative approach to programming, easy implementation of complex UI patterns, and enhanced tooling support. Let's look at its key features:
1. Declarative Programming
Riverpod allows developers to write business logic in a manner similar to Stateless widgets. This approach makes the code:
Easily reusable
Highly composable
More maintainable
A standout feature is its ability to automatically recompute network requests when necessary, streamlining data management in your application.
2. Simplified Implementation of Complex UI Patterns
With Riverpod, implementing common yet complex UI patterns becomes significantly easier. Features like:
Pull to refresh
Search as you type
Real-time updates
These can be implemented with just a few lines of code, greatly reducing development time and complexity.
3. Enhanced Tooling Support
Riverpod goes beyond just state management by providing robust tooling support:
It enhances the compiler to catch common mistakes at compile-time, turning them into compilation errors.
Custom lint rules are provided to ensure code quality and consistency.
Built-in refactoring options help maintain clean code as your project evolves.
A command-line interface for generating documentation is included, aiding in project maintenance and onboarding.
4. Comprehensive Feature Set
Riverpod offers a wide array of features that cater to various aspects of modern app development:
Declarative Programming
Compile Safety
Versatility (works in plain Dart environments)
Custom Lint Rules
Logging
Native Network Request Support
Type-safe Query Parameters
Easily Combinable States
Built-in Refactorings
WebSocket Support
Automatic Loading/Error Handling
Test Ready
Built-in Support for Pull-to-Refresh
Hot-Reload Support
Documentation Generator
Now that we've covered the key features of Riverpod, let's dive into a practical example of how to use it in a production environment for dependency injection.
Implementing Dependency Injection with Riverpod
One of the most powerful features of Riverpod is its ability to provide a clean and modular dependency injection system. Let's look at how we can structure our DI setup using Dart's part
directive for better organization and maintainability.
Project Structure
For our example, we'll use a Pokemon viewer app with the following structure for our DI setup:
lib/
└── src/
└── di/
├── di.dart
├── manager_providers.dart
├── repository_providers.dart
└── view_model_providers.dart
Main DI File
First, let's look at the main di.dart
file:
// lib/src/di/di.dart
library di;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_di_example/src/api/api_manager.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/models/pokemon.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/pokemons_feed_view_model.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/pokemons_repository.dart';
part 'manager_providers.dart';
part 'repository_providers.dart';
part 'view_model_providers.dart';
abstract final class Di {
static final viewModel = _ViewModelProviders();
static final manager = _ManagerProviders();
static final repository = _RepositoryProviders();
}
This file serves as the entry point for our DI setup. It imports necessary dependencies and uses the part
directive to include other files that contain specific provider definitions.
Manager Providers
Next, let's look at the manager_providers.dart
file:
// lib/src/di/manager_providers.dart
part of 'di.dart';
final class _ManagerProviders {
late final api = Provider((ref) => ApiManager());
}
This file contains providers for manager classes, such as the ApiManager
.
Repository Providers
The repository_providers.dart
file contains providers for repositories:
// lib/src/di/repository_providers.dart
part of 'di.dart';
final class _RepositoryProviders {
late final pokemons = Provider(
(ref) => PokemonsRepository(
ref.watch(Di.manager.api),
),
);
}
Here, we define a provider for the PokemonsRepository
, which depends on the ApiManager
.
ViewModel Providers
Finally, the view_model_providers.dart
file contains providers for view models:
// lib/src/di/view_model_providers.dart
part of 'di.dart';
final class _ViewModelProviders {
late final feed = StateNotifierProvider<PokemonsFeedViewModel, List<Pokemon>>(
(ref) => PokemonsFeedViewModel(
ref.watch(
Di.repository.pokemons,
),
),
);
}
This file defines a StateNotifierProvider
for the PokemonsFeedViewModel
, which depends on the PokemonsRepository
.
Benefits of This Structure
Modularity: Each type of provider (managers, repositories, view models) is isolated in its own file, making the code easier to navigate and maintain.
Encapsulation: The
part
directive allows us to keep these provider definitions private to thedi.dart
file, preventing unauthorized access from other parts of the app.Scalability: As the app grows, new providers can be easily added to the appropriate file without cluttering the main
di.dart
file.Clear Dependencies: The structure makes it easy to see the dependency chain from view models down to managers.
Easy to Use: In other parts of the app, you can simply import the
di.dart
file and access all providers through theDi
class.
Using the DI Setup in Your App
To use this DI setup in your Flutter app, you would typically do the following:
Wrap your app with a
ProviderScope
in yourmain.dart
file:
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
In your widgets, use the
Consumer
widget or theref
parameter to access your providers:
class PokemonFeedPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final viewModel = ref.watch(Di.viewModel.pokemonsFeed.notifier);
final state = ref.watch(Di.viewModel.pokemonsFeed);
useEffect(
() {
viewModel.fetchPokemons();
return null;
},
[],
);
// Use viewModel and state to build your UI
// See the full code on Github
}
}
Conclusion
In my experience, Riverpod has proven to be an invaluable tool in my Flutter development toolkit. Its flexibility allows for clean, maintainable, and scalable code, especially when it comes to dependency injection and state management.
The approach we've outlined in this post is just one way to leverage Riverpod's power. It's a method that has worked well for me in production environments, but remember - Riverpod is your Swiss Army knife. Feel free to adapt and modify this approach to best fit your project's needs.
If you want to dive deeper into Riverpod, I highly recommend checking out the official documentation at https://riverpod.dev/. It's a goldmine of information and best practices.
For a complete, working example of the code we've discussed in this post, you can visit the GitHub repository I've set up: https://github.com/devasidmi/riverpod_di_example. Feel free to clone, fork, and experiment with it!
Thank you for reading! I hope this post has given you some insights into how you can use Riverpod to streamline your Flutter development process. Happy coding!