Using the Provider Package for MVVM in Flutter

Flutter is a versatile framework for building cross-platform applications, and effective state management is crucial for any robust application. The Model-View-ViewModel (MVVM) architectural pattern is a popular choice for structuring Flutter apps. Combining MVVM with the provider package from pub.dev can lead to cleaner, more maintainable code. This article explores how to leverage the provider package to implement the MVVM pattern in a Flutter application.

Understanding MVVM Architecture

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

  • Model: Represents the data and business logic.
  • View: The user interface, responsible for displaying data and forwarding user actions to the ViewModel.
  • ViewModel: Acts as an intermediary between the Model and the View, preparing data for the View and handling user input.

Why Use Provider for MVVM?

The provider package simplifies state management by providing a way to access and update data from anywhere in the application. It works well with MVVM because:

  • It allows ViewModels to be easily provided to the View.
  • It facilitates rebuilding of UI components when the ViewModel’s state changes.
  • It offers a simple, declarative approach to state management.

Implementing MVVM with Provider

Let’s walk through the steps to implement MVVM using the provider package in Flutter:

Step 1: Add the Provider Dependency

First, add the provider package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.0

Then, run flutter pub get to install the package.

Step 2: Define the Model

Create the Model class representing your data. For example, if you’re building a simple counter app, the model might look like this:

class CounterModel {
  int value;

  CounterModel({this.value = 0});
}

Step 3: Create the ViewModel

The ViewModel is where the logic for the UI resides. This class will extend ChangeNotifier, which is provided by the provider package. ChangeNotifier allows the ViewModel to notify listeners (the Views) when its state changes.

import 'package:flutter/foundation.dart';

class CounterViewModel extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }
}

Explanation:

  • _counter: A private field to store the counter value.
  • counter: A getter method to expose the counter value to the View.
  • incrementCounter(): A method to increment the counter and notify listeners using notifyListeners().

Step 4: Create the View

The View is responsible for displaying the data and allowing the user to interact with the application. It will use the Consumer widget to access the ViewModel and rebuild when necessary.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_viewmodel.dart';

class CounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer<CounterViewModel>(
              builder: (context, viewModel, child) {
                return Text(
                  '${viewModel.counter}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<CounterViewModel>(context, listen: false).incrementCounter();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Explanation:

  • Consumer<CounterViewModel>: This widget rebuilds its child whenever notifyListeners() is called in the CounterViewModel.
  • The builder parameter of the Consumer widget provides access to the viewModel, allowing you to access its properties and methods.
  • The FloatingActionButton uses Provider.of<CounterViewModel>(context, listen: false).incrementCounter() to access the CounterViewModel and call the incrementCounter() method. Setting listen: false ensures that the widget does not rebuild when the counter changes, as only the Consumer needs to rebuild.

Step 5: Provide the ViewModel

Wrap your root widget (typically MyApp) with a ChangeNotifierProvider to make the ViewModel available to the entire application.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_viewmodel.dart';
import 'counter_view.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterViewModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM with Provider',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CounterView(),
    );
  }
}

Explanation:

  • ChangeNotifierProvider: Provides an instance of CounterViewModel to all descendant widgets.
  • create: (context) => CounterViewModel(): Creates an instance of CounterViewModel when the app starts.
  • child: MyApp(): The MyApp widget is the child of the ChangeNotifierProvider, meaning it and all its descendants can access the CounterViewModel.

Advanced Usage: Multiple Providers and Models

In a larger application, you might have multiple ViewModels and Models. The provider package allows you to provide multiple providers using MultiProvider.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_viewmodel.dart';
import 'settings_viewmodel.dart';
import 'home_view.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterViewModel()),
        ChangeNotifierProvider(create: (context) => SettingsViewModel()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MultiProvider Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomeView(),
    );
  }
}

In this example, both CounterViewModel and SettingsViewModel are provided using MultiProvider.

Benefits of Using Provider with MVVM

  • Simplified State Management: The provider package abstracts away much of the complexity of state management.
  • Testability: MVVM promotes testable code by separating the UI logic from the UI elements.
  • Reusability: ViewModels can be reused across different parts of the application.
  • Maintainability: The separation of concerns makes the codebase easier to maintain and scale.

Conclusion

By combining the MVVM architectural pattern with the provider package, you can create a Flutter application that is well-structured, testable, and maintainable. The provider package simplifies state management and makes it easy to access and update data from anywhere in your application. This approach not only improves code quality but also enhances the development experience.