MVVM Design Pattern using Riverpod in Flutter

Antonio Cranga
4 min readMay 26, 2023

Riverpod is a state management library for the Flutter framework. It is an alternative to Flutter's built-in state management solutions like StatefulWidget and Provider. Riverpod is designed to simplify the process of managing and sharing state in your Flutter application.

At its core, Riverpod revolves around the concept of providers. Providers are objects that hold and expose state or values in your application. They can be simple values, objects, or even complex dependencies. Providers can also be updated and accessed from different parts of your application.

The key features of Riverpod include:

  1. Dependency injection: Riverpod provides a way to inject dependencies into your Flutter widgets and classes. It helps in decoupling dependencies and promoting code reusability.
  2. Scoped and global state management: Riverpod allows you to manage state at different levels of your application hierarchy. You can define providers that are scoped to a specific widget subtree or providers that are accessible globally across your entire application.
  3. Automatic state propagation: When a provider's value changes, Riverpod automatically notifies and updates all the dependent widgets that are using that value. This ensures that your UI remains in sync with the state.
  4. Testability: Riverpod promotes test-driven development by providing an easy way to mock and test your providers. You can use mock providers during testing to simulate different scenarios and validate your application's behavior.

What is the MVVM Design Pattern?

The Model-View-ViewModel (MVVM) architectural pattern is commonly used in software development to separate the concerns of an application into three distinct components: the model, the view, and the view-model.

Figure1. MVVM Design Pattern illustration

How to use it in Flutter?

The next example was made using:

flutter_hooks: ^0.18.6
hooks_riverpod: ^2.3.4

Let’s define our context first, we are going to build a screen that displays a list of items that were asynchronous fetched.

1. Model

Our model is going to be called Event. It's definition is not important for this tutorial, but we will define it anyway.

class Event {
final int? id;
final String? title;
final String? description;

Event(this.id, this.title, this.description);
}

Extra tip: Freezed is by far the most popular code generator for data-classes/unions/pattern-matching/cloning, is not going to be included in this article, but you should have an eye on it.

2. View

Since we would like to see our data displayed, we need a Widget class, let's call it EventsScreen.

EventsScreen.dart

class EventsScreen extends HookConsumerWidget {
const EventsScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold();
}
}

3. ViewModel

We will use AsyncValue approach instead of FutureBuilder for our ViewModel implementation, because it's a more flexible solution.

  1. Simplified state management: With AsyncValue, Riverpod provides a more streamlined and declarative way to manage the state of asynchronous operations. It abstracts away the complexities of manually tracking and handling the different states of a future (e.g., loading, error, data) and provides a unified interface to represent those states.
  2. Better integration with Riverpod: Riverpod is designed to work seamlessly with AsyncValue. When using FutureProvider in Riverpod, the provider's value is automatically wrapped in an AsyncValue type. This allows you to directly access and manipulate the different states of the future (loading, error, data) using convenient methods provided by AsyncValue, such as when, maybeWhen, or fold.
  3. Improved UI updates and error handling: AsyncValue offers additional features for handling UI updates and error scenarios. For example, you can use the when method to conditionally render different UI components based on the state of the asynchronous operation. You can also use maybeWhen to handle specific states while providing fallback behavior for other states. Additionally, AsyncValue provides error handling methods like maybeWhen and maybeMap to handle and react to errors that may occur during the asynchronous operation.
  4. Type-safe and expressive: AsyncValue provides type safety and expressive features that can enhance the development experience. It allows you to define specific types for loading, data, and error states, enabling better type checking and preventing potential mistakes in handling the asynchronous data. The expressive API of AsyncValue makes it easier to work with asynchronous data in a more readable and concise manner.

EventsScreenViewModel.dart

class EventsScreenViewModel extends StateNotifier<AsyncValue<List<Events>>> {
final EventsService eventsService;
EventsScreenViewModel({required this.eventsService})
: super(const AsyncData<List<Events>>([]));
}

Then we can define any custom logic that is going to modify the inner state of the viewmodel.

class EventsScreenViewModel extends StateNotifier<AsyncValue<List<Events>>> {
final EventsService eventsService;
EventsScreenViewModel({required this.eventsService})
: super(const AsyncData<List<Events>>([]));

Future<void> getEvents() async {
state = const AsyncLoading<List<Events>>();
state = await AsyncValue.guard<List<Events>>(
() => eventsService.getEvents());
}
}

In order to use this viewmodel we have to declare it, we can do it in the same file.

final eventsScreenViewModelProvider =
StateNotifierProvider<EventsScreenViewModel, AsyncValue<List<Events>>>(
(ref) {
return EventsScreenViewModel(
eventsService: ref.read(eventsServiceProvider));
});

The last step is by far the most important, now we want to connect the ViewModel to the previous screen that we built.

class EventsScreen extends HookConsumerWidget {
const EventsScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<Events>> state =
ref.watch(eventsScreenViewModelProvider);

useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(eventsScreenControllerProvider.notifier).getEvents();
});
return null;
}, const []);
return Scaffold(
body: state.when(
data: (data) {
//Data type will be List<Events>, iterate over the List
//and return the desired widget.
},
error: (error, stackTrace) {},
loading: () {}));
}
}

That's it, now you can just update EventsScreenViewModel with any logic, without thinking about restructuring your code.

Resources

Riverpod: https://riverpod.dev/
Hooks Riverpod: https://pub.dev/packages/hooks_riverpod/versions/2.3.4
Flutter Hooks: https://pub.dev/packages/flutter_hooks/versions/0.18.6

Need help with your project? Feel free to contact me anytime: LinkedIn, GitHub

Are you passionate about UI/UX Design? Join our community for tips&tricks, inspiration and much more. UI Global

--

--

Antonio Cranga

Full Stack Mobile Developer | UI/UX passionate | Founder of @ui.global