Dependency Injection in Flutter using Riverpod
Learn a simple method to apply dependency injection in Flutter using the popular state management framework Riverpod.

In this post, I will talk about the use of the Riverpod package as a dependency injection framework in Flutter.
Managing dependencies is important when writing a scalable project. A component that depends on another dependency should be able to obtain them without needing to know how to create them. One approach to achieve this is through dependency injection.
What is Dependency injection?
Dependency injection is a concept for managing dependencies and separating the concerns of the different blocks of your code.
Let's say that I would need a Professor
to learn from when attending a lecture. I do not care who the assigned Professor
is in this particular class. I just need someone to teach the lecture that I am attending. Dependency injection would be the school providing a professor that I can learn from without me choosing who that person would be.
Why do we need to use Dependency Injection?
The main reason behind using a dependency injection framework is to make our coding experience smoother.
The component that we are building should be able to locate and use another component easily. Our component should also be testable in a way that it can easily get a stub or fake version of its dependency when we run tests.
//BAD Example
class Lecture {
const Lecture();
final Professor _professor = const Professor('Proton');
void participate(Student student) {
student.enterClassroom();
student.sit();
student.listenTo(_professor);
}
}
To be consistent with our previous example, look at the example code above. The participate()
method will first make a student enter the classroom, sit on a chair, and listen to the _professor
.
This will work but what if we want to test this Lecture
component? Since the professor was defined internally and was not injected, we cannot mock or stub this dependency. It is always set to be the same value; Professor Proton
.
// BETTER example
class Lecture {
const Lecture({
required Professor professor,
}) : _professor = professor;
final Professor _professor;
void participate(Student student) {
student.enterClassroom();
student.sit();
student.listenTo(_professor);
}
}
A better example would be to use the class constructor
as a means of injecting the dependency. As we create an instance of a Lecture
, we can easily pass a fake professor as a parameter argument when running tests.
But what happens then when we start having more than one dependency? Using constructor injection is tedious as your app grows. This is the problem we want to solve by using a dependency injection framework.
What is Riverpod?

Riverpod is a reactive caching and data-binding framework built by Remi Rousselet. It is a popular solution built for doing state management, dependency injection and caching data which the Flutter framework does not provide natively.
While it's mainly used for state management, Riverpod can be used for dependency injection alone if your project is using another library for managing states such as BLoC.
Each dependency that we want to inject is wrapped by an object called Provider
. In Riverpod, a Provider
is an immutable object that is used to obtain a dependency. It wraps the dependency that you need so that it can be accessed anywhere in your app that is within a ProviderScope
.
Installation
Set up your project by including the Flutter Riverpod dependency.
$ flutter pub add flutter_riverpod
How do we get dependencies with Riverpod?
Let's use a more realistic example (a simple weather app) to demonstrate how to use Riverpod for dependency injection.
There are three main blocks in Riverpod
that we need to be familiar with; Provider
, Ref
(or WidgetRef
in Flutter widgets), and the ProviderScope. Each of these blocks is important to get Riverpod
running.
1. Provider
The first block that we already know is the Provider
which can give us the dependency we need. For example, if we have a WeatherRepository
to get today's weather, we can build a provider for this dependency as such:
// Weather Repository to get today's weather
class WeatherRepository {
final WeatherDataSource _dataSource = OpenWeatherApiDataSource();
Future<String> todaysWeather() {
return _dataSource.getWeatherToday();
}
}
// Weather Repository provider
final Provider<WeatherRepository> weatherRepositoryProvider =
Provider<WeatherRepository>(
(ref) {
return WeatherRepository();
},
);
The weatherRepositoryProvider
can be accessed and used across our app to obtain a WeatherRepository
dependency.
2. ProviderScope
Each dependency that is wrapped by a Provider
is only accessible within a ProviderScope
. The ProviderScope
sets the environment of our app so that the state of our dependencies is contained within this scope.
void main() {
runApp(
const ProviderScope(
child: WeatherApp(),
),
);
}
The best approach to define the scope of providers is to wrap the whole app in a ProviderScope
. It is usually done in the top-most parent widget of the app so that all the dependencies can be accessed anywhere within that scope.
3a. Dependency injection in providers using Ref
The Ref
object is the key to locating our providers in the defined scope. If we want to inject a weather data source for our WeatherRepository
, we can do so by first creating a Provider
for this data source.
// Weather data source interface
abstract class WeatherDataSource {
Future<String> getWeatherToday();
}
// Dummy OpenWeatherAPI Weather data source
class OpenWeatherApiDataSource extends WeatherDataSource {
@override
Future<String> getWeatherToday() async {
await Future.delayed(const Duration(seconds: 2));
return "10C";
}
}
// Weather Data Source provider
final Provider<WeatherDataSource> openWeatherDataSourceProvider =
Provider<WeatherDataSource>(
(ref) {
return OpenWeatherApiDataSource();
},
);
Now that we have a Provider
for our weather data source, we can inject this into our WeatherRepository
. First, we refactor the repository to accept a data source when constructing the object. Then, we use the watch
method of the Ref
object in the weather repository provider to obtain the data source.
The watch
method locates the latest state of the provider being watched. In our example, it will be the OpenWeatherApiDataSource
instance.
//Using ref to find a weather data source
final Provider<WeatherRepository> weatherRepositoryProvider =
Provider<WeatherRepository>(
(ref) {
final weatherDataSource = ref.watch(openWeatherDataSourceProvider);
return WeatherRepository(weatherDataSource: weatherDataSource);
},
);
Instead of tightly coupling the open weather data source with the WeatherRepository
, we have improved the code by injecting this dependency instead.
Now that we know how to obtain dependencies using the Ref
object, let's continue with WidgetRef
.
3b. Dependency injection in widgets using WidgetRef
The Ref
is used to obtain provider dependencies within other providers. On the other hand, WidgetRef
is used to locate providers inside a Flutter widget.
The most common approach to get a WidgetRef
is to make your widget extend from the ConsumerWidget
class if it is a StatelessWidget
, or from ConsumerStatefulWidget
if it is a StatefulWidget
.
Here's an example of a stateless widget using ConsumerWidget
.
//Using widget ref to find a weather data source
class WeatherScreen extends ConsumerWidget {
const WeatherScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final weatherRepository = ref.watch(weatherRepositoryProvider);
return FutureBuilder(
future: weatherRepository.todaysWeather(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!);
} else {
return const CircularProgressIndicator();
}
},
);
}
}
We use the WidgetRef
instance in the widget's build function to obtain the dependency that we need. In the WeatherScreen
example, we get the WeatherRepository
dependency to obtain today's weather.
Any widgets that extend either ConsumerWidget
or ConsumerStatefulWidget
that are within the ProviderScope
can access the WeatherRepository
using ref.watch()
.
Is defining providers as global variables a bad thing?
There is a bad connotation with using global variables in your code due to the dangers of mutability, testability, and scalability. Global variables tend to be not testable due to their global scope and the ability to be modified anywhere in the code.
Providers in Riverpod are safe in this manner because the states that they hold are contained within the ProviderScope
. They are also always declared as final
in which variables pointing to them cannot be updated or replaced.
How do we replace dependencies when running tests?
We should be able to constrain the environment and scope of our app when running tests. Dependencies such as data repositories should be easy to replace with mocked or stubbed instances as you write your tests. Here is how you do it with Riverpod.
Let's have all our providers in a separate file called providers.dart
which will look something like this:
//providers.dart
final Provider<WeatherDataSource> openWeatherDataSourceProvider =
Provider<WeatherDataSource>(
(ref) {
return OpenWeatherApiDataSource();
},
);
final Provider<WeatherRepository> weatherRepositoryProvider =
Provider<WeatherRepository>(
(ref) {
final weatherDataSource = ref.watch(openWeatherDataSourceProvider);
return WeatherRepository(weatherDataSource: weatherDataSource);
},
);
If we want to run our app, we usually run the main.dart
which should look like this:
// Production code
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'weather_app.dart';
void main() {
runApp(
const ProviderScope(
child: WeatherApp(),
),
);
}
However, if we want to inject fake versions of our dependencies in tests, we can override them by using the overrides
argument in the ProviderScope
of our test file.
//Test code
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_di/app.dart';
import 'package:flutter_riverpod_di/providers.dart';
import 'package:flutter_riverpod_di/services.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeWeatherDataSource extends WeatherDataSource {
@override
Future<String> getWeatherToday() {
return Future.value('Fake value');
}
}
void main() {
testWidgets(
'Use a fake weather data source',
(tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
openWeatherDataSourceProvider.overrideWithValue(
FakeWeatherDataSource(),
),
],
child: const WeatherApp(),
),
);
},
);
}
In the example code above, we replace the open weather data source with a fake one whilst using the real WeatherRepository
. To do so, we need to use the provider's method overrideWithValue
and supply your desired instance.
The variables being global did not cause any issue because the scope is still controlled when running our widget tests and was overridden using the overrides parameter of the ProviderScope
. We keep our production code as is and the same time able to stub our dependencies with fake ones.
Weather app example
Putting everything that we learn into practice, let's implement the complete weather app.

The first thing to do is make sure that our weather app is wrapped within a ProviderScope
.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'weather_app.dart';
void main() {
runApp(
const ProviderScope(
child: WeatherApp(),
),
);
}
main.dart
We then declare the different services that we will use in the project. We define the WeatherRepository
and `WeatherDataSource` interface and its implementation.
abstract class WeatherDataSource {
Future<String> getWeatherToday();
}
class OpenWeatherApiDataSource extends WeatherDataSource {
@override
Future<String> getWeatherToday() async {
await Future.delayed(const Duration(seconds: 2));
return "10C";
}
}
class WeatherRepository {
const WeatherRepository({required this.weatherDataSource});
final WeatherDataSource weatherDataSource;
Future<String> todaysWeather() {
return weatherDataSource.getWeatherToday();
}
}
services.dart
Next, we define the providers that we will use to get our dependencies in a file called providers.dart
. We need a WeatherRepository
to fetch today's weather using a data source WeatherDataSource
. In our implementation, our data source will be hardcoded to a value including a delay to simulate loading progress.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'services.dart';
final Provider<WeatherDataSource> openWeatherDataSourceProvider =
Provider<WeatherDataSource>(
(ref) {
return OpenWeatherApiDataSource();
},
);
final Provider<WeatherRepository> weatherRepositoryProvider =
Provider<WeatherRepository>(
(ref) {
final weatherDataSource = ref.watch(openWeatherDataSourceProvider);
return WeatherRepository(weatherDataSource: weatherDataSource);
},
);
providers.dart
Finally, all that is left is to write up our widgets to display the weather. Our WeatherApp
extends from ConsumerWidget
so that it can access the dependencies it needs to get the weather data.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers.dart';
import 'services.dart';
class WeatherApp extends ConsumerWidget {
const WeatherApp({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final WeatherRepository weatherRepository =
ref.watch(weatherRepositoryProvider);
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ref.invalidate(weatherRepositoryProvider);
},
label: const Text('Refresh'),
),
body: Center(
child: FutureBuilder<String>(
future: weatherRepository.todaysWeather(),
builder: (context, snapshot) {
if ((snapshot.hasData) &&
(snapshot.connectionState == ConnectionState.done)) {
return Text(
snapshot.data!,
style: Theme.of(context).textTheme.headlineMedium,
);
} else {
return const CircularProgressIndicator();
}
},
),
),
),
);
}
}
weather_app.dart
When the WeatherApp
widget builds, it will obtain a reference of the weather repository and get the Future
value of today's weather. Using a FutureBuilder
widget, it will wait for the call to finish before displaying the result.

I have also added a button to simulate fetching up-to-date weather data. The ref.invalidate(provider)
is the way to clear the data cache stored by Riverpod. When we call this method, the provider will provide a fresh and updated state for our dependents to access. In our example, it will try to fetch new data from the WeatherDataSource
.
FutureProvider
or StreamProvider
to display cached data.Summary
Dependency injection is important in your app's architecture as it keeps your codebase maintainable and scalable. Riverpod is a great framework to use not only for state management but also for implementing dependency injection.
To get the best results from Riverpod, check out https://riverpod.dev/ to get a more detailed explanation about the framework and how it is meant to be used.