Implementing MVVM (Model-View-ViewModel) in Flutter

The Model-View-ViewModel (MVVM) architectural pattern is widely used in modern app development for creating scalable, testable, and maintainable applications. In Flutter, MVVM can help you structure your code, separate concerns, and manage the UI state effectively. This comprehensive guide explores how to implement MVVM in Flutter with best practices, detailed examples, and explanations.

What is MVVM?

MVVM is a design pattern that separates an application into three interconnected parts:

  • Model: The data layer that handles the business logic and data.
  • View: The UI layer that displays data to the user and relays user actions to the ViewModel.
  • ViewModel: A mediator between the Model and View, exposing data required by the View and handling user commands.

Why Use MVVM in Flutter?

  • Separation of Concerns: Divides the app into distinct layers, making it easier to manage and understand.
  • Testability: ViewModel is easily testable since it is independent of the UI.
  • Maintainability: Clear separation makes it easier to update and maintain each layer.
  • Reusability: ViewModel can be reused across multiple views.
  • Scalability: Simplifies the addition of new features and scaling of the app.

Implementing MVVM in Flutter: A Step-by-Step Guide

Let’s walk through implementing MVVM in a simple Flutter app that displays a list of users fetched from an API.

Step 1: Project Setup

First, create a new Flutter project:

flutter create flutter_mvvm_example

Add necessary dependencies to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.5 # For making HTTP requests
  provider: ^6.0.5 # For state management

Run flutter pub get to install the dependencies.

Step 2: Create the Model

Define the data model for a user. Create a file named user.dart in the lib/models directory:

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

Step 3: Create the ViewModel

Create a user_viewmodel.dart file in the lib/viewmodels directory. The ViewModel will handle fetching the users and notifying the View when the data is ready.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/user.dart';

class UserViewModel with ChangeNotifier {
  List _users = [];
  bool _loading = false;
  String _errorMessage = '';

  List get users => _users;
  bool get loading => _loading;
  String get errorMessage => _errorMessage;

  Future fetchUsers() async {
    _loading = true;
    _errorMessage = '';
    notifyListeners();

    try {
      final response =
          await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
      if (response.statusCode == 200) {
        final List userList = json.decode(response.body);
        _users = userList.map((json) => User.fromJson(json)).toList();
      } else {
        _errorMessage = 'Failed to load users';
      }
    } catch (e) {
      _errorMessage = 'An error occurred: $e';
    } finally {
      _loading = false;
      notifyListeners();
    }
  }
}

Explanation:

  • The UserViewModel class extends ChangeNotifier, which is part of Flutter’s foundation library.
  • It holds the _users list, _loading flag, and _errorMessage string as private variables.
  • It provides getter methods for accessing these variables from the View.
  • The fetchUsers method fetches user data from the API, updates the _users list, and uses notifyListeners to notify the View about the changes.

Step 4: Create the View

Create the UI using Flutter widgets in the lib/views directory. The user_view.dart file will display the list of users.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/user_viewmodel.dart';

class UserView extends StatefulWidget {
  @override
  _UserViewState createState() => _UserViewState();
}

class _UserViewState extends State {
  @override
  void initState() {
    super.initState();
    Provider.of(context, listen: false).fetchUsers();
  }

  @override
  Widget build(BuildContext context) {
    final userViewModel = Provider.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: userViewModel.loading
          ? const Center(child: CircularProgressIndicator())
          : userViewModel.errorMessage.isNotEmpty
              ? Center(child: Text(userViewModel.errorMessage))
              : ListView.builder(
                  itemCount: userViewModel.users.length,
                  itemBuilder: (context, index) {
                    final user = userViewModel.users[index];
                    return ListTile(
                      title: Text(user.name),
                      subtitle: Text(user.email),
                    );
                  },
                ),
    );
  }
}

Explanation:

  • The UserView widget is a StatefulWidget that displays the list of users.
  • It uses Provider.of to access the UserViewModel instance.
  • In the initState method, it calls fetchUsers to fetch the data when the widget is first created.
  • The build method checks the loading flag and errorMessage to display a loading indicator or error message accordingly.
  • If the data is loaded successfully, it uses ListView.builder to display the list of users.

Step 5: Set up Provider

In your main.dart file, set up the Provider to make the UserViewModel available to the UserView.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'views/user_view.dart';
import 'viewmodels/user_viewmodel.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserViewModel()),
      ],
      child: MaterialApp(
        title: 'MVVM Example',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: UserView(),
      ),
    );
  }
}

Explanation:

  • The MyApp widget is wrapped with MultiProvider to provide the UserViewModel.
  • ChangeNotifierProvider creates an instance of UserViewModel and makes it available to its descendants.

Complete Code

Here is the complete code structure:

  • main.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'views/user_view.dart';
import 'viewmodels/user_viewmodel.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserViewModel()),
      ],
      child: MaterialApp(
        title: 'MVVM Example',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: UserView(),
      ),
    );
  }
}
  • lib/models/user.dart:
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}
  • lib/viewmodels/user_viewmodel.dart:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/user.dart';

class UserViewModel with ChangeNotifier {
  List _users = [];
  bool _loading = false;
  String _errorMessage = '';

  List get users => _users;
  bool get loading => _loading;
  String get errorMessage => _errorMessage;

  Future fetchUsers() async {
    _loading = true;
    _errorMessage = '';
    notifyListeners();

    try {
      final response =
          await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
      if (response.statusCode == 200) {
        final List userList = json.decode(response.body);
        _users = userList.map((json) => User.fromJson(json)).toList();
      } else {
        _errorMessage = 'Failed to load users';
      }
    } catch (e) {
      _errorMessage = 'An error occurred: $e';
    } finally {
      _loading = false;
      notifyListeners();
    }
  }
}
  • lib/views/user_view.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/user_viewmodel.dart';

class UserView extends StatefulWidget {
  @override
  _UserViewState createState() => _UserViewState();
}

class _UserViewState extends State {
  @override
  void initState() {
    super.initState();
    Provider.of(context, listen: false).fetchUsers();
  }

  @override
  Widget build(BuildContext context) {
    final userViewModel = Provider.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: userViewModel.loading
          ? const Center(child: CircularProgressIndicator())
          : userViewModel.errorMessage.isNotEmpty
              ? Center(child: Text(userViewModel.errorMessage))
              : ListView.builder(
                  itemCount: userViewModel.users.length,
                  itemBuilder: (context, index) {
                    final user = userViewModel.users[index];
                    return ListTile(
                      title: Text(user.name),
                      subtitle: Text(user.email),
                    );
                  },
                ),
    );
  }
}

Advantages and Considerations

  • Advantages of MVVM:
  • Testability: ViewModels are testable in isolation.
  • Maintainability: Clear separation of UI and business logic.
  • Reusability: ViewModels can be shared among different views.
  • Considerations:
  • Complexity: MVVM can add complexity to small apps.
  • Learning Curve: Requires understanding of both architectural patterns and state management techniques.

Conclusion

Implementing MVVM in Flutter is an effective way to build scalable, testable, and maintainable applications. By separating the concerns into distinct layers—Model, View, and ViewModel—you can manage UI state effectively and improve the overall architecture of your Flutter apps. This comprehensive guide provided a step-by-step approach to implementing MVVM with practical examples and best practices, enabling you to create robust and well-structured Flutter applications.