Simple Flutter app architecture: the Provider way

Simple Flutter app architecture: the Provider way

You are reading about Flutter. You get excited that you can express yourself and create so much so quickly if what it promises is true. You give it a try. It's indeed a pleasure to develop and you can see your changes without a reload! You think you must create an app to give it a go.

But wait. You've only read about widgets and how to draw things on the screen. Where is your business logic going to live? Next to the UI building logic?

Flutter is quite opinionated on how to draw things on the screen but leaves how to organize state management and business logic to you. The Flutter community came up with various ways to do state management. I will focus on the most simple yet scalable way (that is officially recommended): the provider way.

There are plenty of great resources about the Provider state management. This post aims to give you an intro into the state management method as quickly as possible. It's a bit more concise than a tutorial and a bit more verbose than a cheat sheet.

High-level overview

To make Flutter redraw the screen you need to call setState(). But this is not very convenient for a sizable app, especially when changes happen deep down in the widget hierarchy.

Provider offers a way for (stateful) widgets to get notified when a change happens in the view model that requires a call to build() to redraw the UI.

Set up

Head to your pubspec.yaml and add a dependency to the provider package (check pub.dev for the latest version).

dependencies:
  provider: ^4.3.2+2
  [...]

View Model

This is where your "business logic" will live. Whatever is not related to how things will be drawn on the screen, should live here.

This class will extend ChangeNotifier. It's a kind of an Observable interface if you are familiar with the term. This allows the view model to notify the view when things have changed and a redraw is needed. This "notification" happens with the notifyListeners() call.

import 'package:flutter/foundation.dart';

class MyHomePageViewModel extends ChangeNotifier {
    String text = 'Initial text';
    
    void onClicked() {
        text = 'Something was clicked';
        notifyListeners();
    }
}
my_home_page_view_model.dart

View

ChangeNotifierProvider

Now that we have a "view model" we need to bind it with the "view". ChangeNotifierProvider is the widget that initially creates the view model and provides it to whatever descendant requires to consume it. This wighet comes from the provider package we installed in the setup phase.

This should be just above the higher widget that needs to consume the view model. In a simple screen-based app, each view has a single view model. So for each screen, you have  a ChangeNotifierProvider just above the screen widget.

import 'package:provider/provider.dart';

[...]

class MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => MyHomePageViewModel(),
        child: Row(children: <Widget>[
          Text('This is always the same'),
          FloatingActionButton(
            child: Icon(Icons.create),
          )
        ]));
  }
}
my_home_page.dart

Consumer

This is the widget that asks to "consume" a "view model". Any widget that needs data from the model to set its state should be wrapped in a Consumer widget. In the above view, we want the Text widget to have the text value found in our model.

[...]
    return ChangeNotifierProvider(
        create: (context) => MyHomePageViewModel(),
        child: Row(children: <Widget>[
        
          // This part was updated
          Consumer<MyHomePageViewModel>(
            builder: (context, viewModel, child) {
              return Text(viewModel.text);
            },
          ),
        
          FloatingActionButton(
            child: Icon(Icons.create),
          )
        ]));
[...]
my_home_page.dart

Provider.of

Now we have the view bound to our view model. All we have to do is somehow call the model, which will make some "business logic" and trigger an update.

[...]
    return ChangeNotifierProvider(
        create: (context) => MyHomePageViewModel(),
        child: Row(children: <Widget>[
          Consumer<MyHomePageViewModel>(
            builder: (context, viewModel, child) {
              return Text(viewModel.text);
            },
          ),        
          FloatingActionButton(
            child: Icon(Icons.create),
            
            // This part was updated
            onPressed: () =>
                Provider.of<MainViewModel>(context, listen: false).onClicked(),
                
          )
        ]));
[...]
my_home_page.dart

Here we didn't use a Consumer to get on hold of a model reference. We didn't want any property of the model to draw our widget on the screen. We just wanted to call a method on our model. Provider.of will get you that reference.

Hopefully, by now you have a grasp of the basics of the Provider pattern for managing state in a Flutter app. Happy coding!

Show Comments