Back
Using MVVM Architecture in Flutter
Model-View-ViewModel (MVVM) Architecture is a software pattern that supports separating the UI (which is View) from the backend (Model).
What is Model-View-ViewModel?
Model-View-ViewModel (MVVM) Architecture is a unique combination of software architecture patterns that supports the separation of the UI (which is View) from the development of the business logic or the backend logic (Model). The view model inside MVVM is the bridge responsible for the conversion of data in a way that behaves in accordance with the changes happening in the UI.
In addition, to know about the responsibilities of the three components, it’s also important to understand how the components interact with each other. At the highest level, the view “knows about” the view model, and the view model “knows about” the model, but the model is unaware of the view model, and the view model is unaware of the view.
Advantages and Disadvantages of MVVM
There are several advantages of MVVM Architecture:
Separation of Concerns: This is a design principle for separating a computer program into distinct sections such that each section addresses a separate concern. A concern is anything that matters to provide a solution to a problem.
Improved testability
Defined project structure
Parallel development of UI
Abstracting the view, thus reducing the quantity of business logic required in the code behind it
Some disadvantages of MVVM:
It has a somewhat steep learning curve. How all the layers work together may take some time to understand.
It adds a lot of extra classes, so it’s not ideal for low-complexity projects.
Since architectural or MVVM design patterns are platform-agnostic, they can be used with any framework MVVM framework; in our case, Flutter. If you prefer visual learning, follow along with a video version of this tutorial.
MVVM Components
Model: This is basically the domain model, or the model which represents the data from your backend (i.e., the data access layer). Models hold information but typically don’t handle behavior. They don’t format information or influence how data appears. The Model in the MVVM pattern in Flutter represents the actual data that will be used in application development.
View: This is basically the only part of the application users actually interact with. For instance, the user presses the button, scrolls the list, edits the settings, etc. These events are then forwarded to the view model, which does the processing and returns the expected user response (which is some form of UI). It’s important to remember the View isn’t responsible here for handling the state.
A View should be as dumb as possible. Never put your business logic in a View.
ViewModel: The ViewModel acts as an intermediary between the View and the Model, in such a way that it provides data to the UI. The ViewModel may also expose methods for helping to maintain the View’s state, update the model based on the actions on a View, and trigger events on the View. In Flutter, we have a listener called ChangeNotifier
that allows the ViewModel to inform or update the View whenever the data is updated.
The ViewModel has basically two responsibilities:
It reacts to user inputs (e.g., by changing the model, initiating network requests, or routing to different screens)
It offers output data that the View can subscribe to
In summary, the ViewModel sits behind the UI layer which is a key user interface principle. It exposes data needed by a View and can be viewed as the source our Views go to for both data and actions.
What is Flutter ChangeNotifier?
ChangeNotifier
is a class that provides change notifications to its listeners.
As per the official documentation, a ChangeNotifier
is:
A class that can be extended or mixed in that provides a change notification API using VoidCallback
for notifications.
It is O(1) for adding listeners and O(N) for removing listeners and dispatching notifications (where N is the number of listeners).
There are two ways to consume the ChangeNotifier
in Flutter:
Using the
.addListener
method, as theChangeNotifier
is a type of Listenable.Using a combination of
ChangeNotifierProvider
,Consumer
, andProvider
. These capabilities are provided to us by theProvider
package.
We will use approach 2 in the following flutter ChangeNotifier
example.
In the real world, other classes can listen
to a ChangeNotifier
object. When the change notifier gets updated values, it can call a method called notifyListeners
, and then any of its listeners will receive the updated values.
Inside the app, any class that listens to this Person
will be notified in case the age changes. Internally, notifyListeners
calls the registered listeners.
Flutter MVVM Example
Flutter is declarative in nature. This means that Flutter builds UI by overriding your build methods to reflect the current state of your app:
According to the Flutter documentation, the state is described as “the data you need to rebuild your UI at any point in time.”
A state can be contained in a single widget, known as a local state. Flutter provides inbuilt classes and methods to deal with self-contained states like StatefulWidget
and setState
.
However, a state that has to be shared across different widgets is known as an app state. It is at this point that we introduce state management tools.
We will be using Provider
for state management.
Let’s say you were architecting an application that includes only the screen below. How would you do it?
Hint: Using Flutter MVVM
Each screen should comprise its own flutter MVVM folder structure. Create a folder called home which contains a view called home_view.
Naming convention for View:
Each screen is called “view” and the file is suffixed with _view
. The view will be listening to the changes happening on the ViewModel Flutter, using the Consumer
.
Each view should have a ViewModel associated with it. Create a file called
home_view_model
which will be responsible for accepting the user interactions, processing them by running some business logic, and finally responding back.
Naming convention for ViewModel:
Each screen has a ViewModel associated with it and the file is suffixed with _view_model
. The ViewModel notifies the changes to the UI (if any) using notifyListeners
.
Let’s assume the button calls some API (more on that later) and responds back with some response. This response should be converted as a model suffixed with
_model
and returned from the ViewModel to the view.
These are the basics of MVVM architecture, as we can see in the screenshot above. This can be replicated for all of your app's screens. You can read more about The Best File Formats for Web Development for a better understanding. Now, let’s see a slight addition on top of this structure.
Extending the MVVM toolkit with Repository and Services
In the real world, our app needs to interact with APIs or third-party integrations. So, we introduce something called a Repository.
A repository pattern provides an abstraction of data so that your application can work with a simple abstraction that has an interface. Using this pattern can help to achieve loose coupling. If implemented correctly, the Repository pattern can be a great way to ensure you follow the Single Responsibility Principle for your data access code.
Some benefits of using the Repository pattern:
It separates the business logic for accessing external services
It makes mocking easier and allows to do unit tests
It easily switches data sources without doing time-consuming code changes
Some disadvantages of using the Repository pattern:
It adds another layer of abstraction, which adds a certain level of complexity, making it overkill for small applications.
Continuing with the previous example, let’s say our button needs to call an API. Let’s implement it using the Repository pattern.
Dart has no interfaces, like Java, but we can create one with an abstract class. We begin by creating an abstract class that defines the interface for our home_repo.
This abstract class helps to create a boundary, and we are free to work on either side of that boundary. We could work on implementing the home repository (recommended), or we could just use the implementation directly in our app (not recommended).
Here, the HomeRepository
has only one method, fetchData
. This method returns the response as a model called CarouselModel
.
Next, let’s implement the HomeRepository
:
Inside the method fetchData
, we introduce a delay and then load the data from the assets, which is a JSON file. This delay is basically a substitute for calling the API, but I hope I am able to convey my thoughts to the reader.
As your application grows, you may find yourself adding more and more methods to a given repository. In this scenario, consider creating multiple repositories and keeping related methods together.
Now, we have
carousel_model
representing the Model (M)home_view
representing the View (V)home_view_model
representing the View Model (VM)home_repo
representing the Repository
Register the Repository
Since our repository is ready, now we need to figure out how to register it and make it available inside our app. This is when we introduce another concept called Dependency Injection (DI). We make use of the package get_it. As per the documentation:
This is a simple Service Locator for Dart and Flutter projects with some additional goodies highly inspired by Splat. It can be used instead of InheritedWidget
or Provider
to access objects, e.g., from your UI.
GetIt is super fast because it uses just a Map
, which makes access to it O(1). GetIt itself is a singleton, so you can access it from everywhere using its instance property (see below).
We install get_it
it by including it inside the pubspec.yaml
as:
Typically at the start of your app, you register the types that you want to later access from anywhere. After that, you can access instances of the registered types by calling the locator again.
The nice thing is you can register an interface or abstract class together with a concrete implementation. When accessing the instance, you always ask for the interface/abstract class type. This makes it easy to switch the implementation by just switching the concrete type at registration time.
We create a file called locator.dart
inside which we will instantiate the object of get_it
:
As Dart supports global variables, we assign the GetIt
instance to a global variable to make access to it as easy as possible.
Although GetIt
is a singleton, we will assign its instance to a global variable locator
to minimize the code for accessing GetIt
. Any call to locator
in any package of a project will get the same instance of GetIt
.
Next, we use the locator
and use the registerFactory
to register our HomeRepository
.
Provider as an alternative to GetIt
Provider is a powerful alternative to GetIt. But there are some reasons why people use GetIt for Dependency injection:
Provider needs a
BuildContext
to access the registered objects, so you can’t use it inside business objects outside of the widget tree or in a pure Dart MVVM package.Provider adds its own widget classes to the widget tree that are no GUI elements, but are needed to access the in Provider registered objects.
Testing Repository
You can implement unit testing for different elements of your Flutter applications, such as widgets, controllers, models, services, and repositories. It’s possible to unit test repository-based Flutter codebases with the following strategies:
Implement a mock repository class and test the logic
You don’t need to implement mock classes by yourself — the Mockito package helps you to generate them quickly and automatically.
Integrate Repository in ViewModel
Now comes the time to use the Dependency Injection (DI). But before that, let’s see what it is.
When class A uses some functionality of class B, then it’s said that class A has a dependency of class B.
Before we can use methods of other classes, we first need to create the object of that class (i.e., class A needs to create an instance of class B).
Dependency injection is transferring the task of creating the object to someone else and directly using the dependency.
Benefits of using DI
Supports unit testing
Boilerplate code is reduced, as the initializing of dependencies is done by another component (locator in our case)
Enables loose coupling
Drawbacks of using DI
It’s complex to learn, and if overused, can lead to management issues and other problems
Many compile time errors are pushed to runtime
Coming back to our application, let’s see how we integrate it:
Here, we create a constructor inside our HomeViewModel and specify the homeRepo as our required parameter. This way, we direct whomever needs access to our ViewModel first through the homeRepo.
Initialize the service locator
You need to register the services on app startup, so you can do that in main.dart.
Replace the standard:
With the following:
This will register any services you have with GetIt before the widget tree is built.
If we recall, our homeRepo was registered inside the locator, so in order to declare our ViewModel, we follow this:
Inside our main
, we call the setupLocator
, which is the method that comprises all the registered dependencies under locator.dart
.
Next, inside our MultiProvider, we specify the HomeViewModel under the ChangeNotifierProvider.
ChangeNotifierProvider creates a ChangeNotifier using create and automatically disposes it when it is removed from the widget tree.
Using ViewModel inside the View
We have our repository registered and passed as a required parameter to our ViewModel. Let’s see how to use the ViewModel inside our View.
There are two ways to access the ViewModel inside the View:
Using the Consumer widget
Using Provider.of(context)
We instantiate the viewModel
using Provider.of inside the home_view
.
Provider.of(context)
is used when you need to access the dependency, but you don’t want to make any changes to the UI. We simply set listen: false
, signifying that we don’t need to listen to updates from the ChangeNotifier. The listen: false
parameter is used to specify when you're using Provider to fetch an instance and call a method on that instance.
Note: We can also use the below:
For reacting to the changes that happen to the viewModel, we use Consumer when we want to rebuild the widgets when a value changes. It is a must to provide the type so that the Provider can understand which dependency you are referring to.
The Consumer widget doesn’t do any fancy work. It just calls Provider.of in a new widget and delegates its build implementation to the builder.
The Consumer widget takes two parameters, the builder
parameter and the child
parameter (optional). The child parameter widget is not affected by any change in the ChangeNotifier.
This builder can be called multiple times (such as when the provided value changes), and that is where we can rebuild our UI. The Consumer widget has two main purposes:
It allows us to obtain a value from a provider when we don’t have a BuildContext that is a descendant of said provider and therefore cannot use Provider.of.
It helps with performance optimization by providing more granular rebuilds.
Unit tests for the ViewModel (Optional)
You can mock dependencies by creating an alternative implementation of a class by making use of the Mockito package as a shortcut.
What are Services?
Services are normal Dart classes that are written to do some specialized task in your app. The purpose of a service is to isolate a task, especially volatile third-party packages, and hide its implementation details from the rest of the app.
Some common examples you might create a service to handle:
Using a third-party package, for instance, read and write to local storage (shared preferences)
Using Cloud Providers like Firebase or some other third-party package
Let’s say you’re using package_info to get the package details of your app.
You use the package directly inside the app, but after some time, you found an even better package. You go through and replace all the references to package_info
with the new package some_great_package
. This was surely a waste of your time and effort.
Let’s say the product owners found that no user was using this feature. Instead, they requested a new feature. You go through and remove all the references to some_great_package
. This was, again, a waste of your time and effort.
The point is, when you have tight coupling to some function scattered around your code, it makes it error-prone and difficult to change.
Clean coding takes time and effort up front, but will save you more time and effort in the long run.
This is where services come in. You make a new class and call it something like PackageInfoService
. The rest of the classes in the app don’t know how it works internally, they just call methods on the service to get the result.
This makes it easy to change. If you want to switch
package_info
tosome_great_package
, just alter the code inside the service class. Updating the service code automatically affects everywhere the service is used inside the app.This supports swapping around implementations. You can create a “fake” implementation that just returns hard-coded data while another team is finalizing/developing the service implementation.
Sometimes, the implementation may rely on other services. For example, your
xyzService
might use a service for making a network call to get other types of data.
Registering your Service
Using a service locator like GetIt is a convenient way to provide services throughout your app.
We use the locator to register our
PackageInfoService
We will be registering
PackageInfoService
as a lazy singleton. It only gets initialized when it’s first used. If you want it to be initialized on app startup, then useregisterSingleton()
instead. Since it’s a singleton, you’ll always have the same instance of your service.
Using the Service
Since we registered the service using the GetIt, we can get a reference to the service from anywhere in the code.
Then you can use it within that class like this:
packageService.getSomeValue()
packageService.doSomething(someValue)
Unit tests for the service (Optional)
You can mock dependencies by creating an alternative implementation of the service class by making use of the Mockito package.
Use Pieces to Store Your Flutter Snippets
When developing Flutter web applications, you may have tons of widgets you save that you want to reuse later, but you just don't have them in a safe place where you can access them. There also may be the scenario where you are combing through Flutter and Dart documentation, and you want to save examples that come in handy when implementing a new feature or figuring out which widget to use for different circumstances.
Pieces helps you save all your useful code snippets efficiently through a desktop application and integrations. Using Pieces, you can save any code snippets from StackOverflow with the click of a button using the chrome extension, have your code autosaved from locally-hosted ML algorithms that recognize your code patterns, auto-classify snippets by language, share code with others using generated links, and more! The Pieces’ suite is continuously being developed, and there’s some groundbreaking stuff that is being put together to share, reuse, and store code snippets to 10x your developer productivity.