How to apply Model-View ViewModel in Flutter

How MVVM helps us to organize our code better, and why it’s one of the easiest patterns to implement in Flutter

Carlos Montes
etermax technology
Published in
5 min readApr 12, 2023

--

What is Model-View ViewModel?

It’s a design pattern that allows you to split the application (client) in 3 basic parts: Model, View and ViewModel. The goal of this separation is to offer a scheme where responsibilities are well defined and communication between the parts is as clean as possible.

View: It represents everything the user can see and interact with. At this stage, we only expect to be able to receive commands from the user and show processed data through visual components.

Model: This part represents information, the data set. It doesn’t have visual representation.

ViewModel: It’s the link between the view and the model. Its main function is to retrieve the necessary information from the Model, apply the necessary operations and expose any relevant data for the Views. It also receives the commands sent by the view and tells the model if it has to update or not any data.

Motivation

Let’s take a look at the following example. We have a StatefullWidget where the subclass extends from a State. A call is made to a service when the screen is initialized. After receiving the information the contact information and the next page are updated.

// ----------------- NO MVVM PATTERN -----------------

class ContactsScreen extends StatefullWidget {
const ContactScreen({Key? key});
}

class _ContactsScreenState extends State<MyScreen> {
List<Contact> contacts = [];
int page = 0;

@override
void initState() {
fetchContacts(page)
.then((result) {
setState(() {
contacts = result.contacts;
page = result.nextPage;
});
});
super.initState();
}

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: viewModel.contacts.length,
itemBuilder: (context, idx) {
final contact = viewModel.contacts[idx];
return ContactListItem(contact: contact);
}
);
}
}

What can we take from this example?

Basically, we mixed concepts and if we were dealing with a much larger component we’d have a case of the classic spaghetti code. Widgets should just be in charge of showing information and components to the user, but if they’re also in charge of obtaining said information and keeping it up to date, too much responsibility is being placed on them.

With this example in mind, let’s recreate it using MVVM this time, and let’s see what advantages we perceive.

Applying MVVM with Provider library

First: the View

In the build method, before adding the ListView we should add the ChangeNotifierProvider and Consumer widgets. The former gives context to the view about who can modify it, and the latter specifies what parts will be affected during each update.

In initState we initialize what would be our ViewModel. With it, we’ll be able to manipulate the data we want to show in the view. Right after initialization, we can see how a call is made to an onScreenInitialized method, which is where the first contacts are sought, and that part of the screen is remade with the results (we’ll see this in detail when we go over the ViewModel).

As you can see, we’re left with a simpler widget, where the most “complex” part was adding the ChangeNotifierProvider and the Consumer, but all logic referring to the domain was removed.

// ----------------- VIEW -------------------

class ContactsScreen extends StatefullWidget {
const ContactScreen({Key? key});
}

class _ContactsScreenState extends State<ContactsScreen> {
late ContactsScreenViewModel _viewModel;

@override
void initState() {
_viewModel = ContactsScreenViewModel(
ActionsFactory.getContacts()
);

_viewModel.onScreenInitialized();
super.initState();
}

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ContactsScreenViewModel>.value(
value: _viewModel,
builder: (context, _) {
return Consumer<ContactsScreenViewModel>(
builder: (context, viewModel, _) =>
ListView.builder(
itemCount: viewModel.contacts.length,
itemBuilder: (context, idx) {
final contact = viewModel.contacts[idx];
return ContactListItem(contact: contact);
}
)
)
}
)
}
}

Second: the ViewModel

As it can be seen, there’s a place in the ViewModel where we can temporarily store the contacts list, which will be exposed to the View for it to be rendered. Having this logic in the ViewModel gives us the advantage of working more comfortably with the View’s behavior.

When working with the ViewModel, interaction can be split into two parts. The first part is data manipulation, i.e. when we modify the models managed by the ViewModel such as the contacts list. The second part is when the View is informed it has to do a re-build because of these changes. This is achieved by making our ViewModel extend its functionalities through the class ChangeNotifier, which gives us the notifyListeners method.

// ----------------- VIEW_MODEL ---------------------

class ContactsScreenViewModel with ChangeNotifier {
final GetContacts _getContacts;

ContactsScreenViewModel(this._getContacts);

List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;

int _page = 0;

void onScreenInitialized() async {
_fetchContacts();
}

void onEndPageReached() async {
_fetchContacts();
}

void _fetchContacts() {
var result = await _getContacts(_page);
_contacts = result.contacts;
_page = result.nextPage;
notifyListeners();
}
}

Third: the Model

Lastly, we have the Model which is no more than the data structure we created to represent the data we want. So far, we only have the contact class.

// --------------- MODEL ------------------
class Contact {
final String id;
final String name;
final String phoneNumber;

Contact(this.id, this.name, this.phoneNumber);
}

// -------------- Result DTO -------------
class ContactsPage {
final List<Contact> contacts;
final int nextPage;

ContactsPage(this.contacts, this.nextPage);
}


// ----- BONUS: Action (for a DDD Architecture) -------

class GetContacts {
final ContactsService _contactsService;
final UserRepository _userRepository;

GetContacts(this._contactsService, this._userRepository);

Future<ContactsPage> call(int page) {
var userId = _userRepository.get().userId;
return _contactsService.getFor(userId, page);
}
}

>> Bonus Part <<

Even if it wasn’t the main point of this post, it’s worth noting that an additional advantage offered by the MVVM pattern is the opportunity to test the behavior of our app. As it can be seen below, the tests are not performed to the widgets, but to the methods they access.

// -------------------- TESTS ----------------------

main() {
late GetContacts getContacts;
late ContactsScreenViewModel viewModel;

setUp(() {
getContacts = MockGetContacts();
viewModel = ContactsScreenViewModel(getContacts);
});

test('on screen initialized get contacts', () {
viewModel.onScreenInitialized();

verify(getContacts(0)).called(1);
});

test('on screen initialized get contacts', () {
viewModel.onScreenInitialized();

viewModel.onEndPageReached();

verify(getContacts(1)).called(1);
});
}

Conclusion

When working with a Front End project, there are plenty of design patterns to choose from. However, I think the Model-View-ViewModel pattern offers a scheme that’s very complete and easy to adopt when working with Flutter. From the start, this scheme allows you to organize your project better, spelling out how the different parts must be separated and how they can or can’t interact with each other.

This said, the MVVM pattern makes your application much more scalable, allowing you to modify any of the three parts without directly affecting the rest.

In addition, the Provider library is very easy to use compared to many others which focus on a reactive scheme. However, in bigger projects, said reactivity can become complex and hard to keep up with.

--

--