Functional Model-View-Update Architecture for Flutter

Object orientation has led us to separate the application into easily manageable chunks. We send messages between objects, they update themselves and send more messages. While this premise is sound, you require god-like foresight to ensure this works well over the life of your project. A dependency, cross-cutting concern, or rogue developer is always near by, to ensure your architecture cracks.

As your application becomes bigger, so does the cognitive load and keeping track of all objects, and how they interact. Bring in a new developer and they have a large learning curve, and may be surprised by many different ways objects interact.

Working on some large mobile applications, none have ever needed separating into modules. No one has known enough about the entire app to easily work on each module without knowing how it affects the rest and how each module it interacts with actually works.

The easiest and simplest way I have found to develop an app involves:

  • All context for a method is given at the point of calling the method
  • Using the simplest path forward because in a rush the developers usually do the simplest thing possible.

Model View Update (MVU)

Model-View-Update is incredibly simple and straightforward architecture, and fits in nicely with Flutter. You have a Model, send it to the View to render, and the View calls update methods, that update the Model back to the Model to update.

Simple Weather App

To highlight the approach, this is an example of a very simple weather app.

main.dart

The main.dart file contains the standard setup of your app, where we reference a regular page, which we call ForecastPage here.

import 'package:flutter/material.dart';
import 'ui/forecast.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Weather App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ForecastPage(title: 'Weather App'),
    );
  }
}

forecast.dart

Separated in another file, just for ease of use, is the ForecastPage, which is also a simple StatefulWidget, and nothing more.

import 'package:flutter/material.dart';
import '../io/darksky.dart' as darksky;

class ForecastPage extends StatefulWidget {
  ForecastPage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _ForecastPageState createState() => _ForecastPageState();
}

The State for the page is also out of the box, yet the only difference here, is I separated the building of the UI, out to another function. ForecastPageState, now just holds logic and state.

class _ForecastPageState extends State<ForecastPage> {
  double _temperature = 0;
  bool _loading = false;

  void _updateTemperature() async {

    setState(() { _loading = true; });

    var forecast = await darksky.getForecast();

    setState(() {
      _temperature = forecast;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ForecastPageUI.buildUI(context, 
                                  widget.title,
                                  () => _updateTemperature(),
                                  _loading,
                                  _temperature);
  }
}

buildUI will create the Widget, and all required information will be passed via the parameters. It is a completely functional way to build your Widgets, and a nice way to separate your UI and logic, which may be a benefit for those who are coming from HTML or XAML based frameworks.

class ForecastPageUI
{
  static buildUI(BuildContext context,
                 String title,
                 Function update,
                 bool loading,
                 double temperature)
  {
      return Scaffold(
            appBar: AppBar(
              title: Text(title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: loading ? ( [new CircularProgressIndicator()]) :
                  ([
                    Text(
                      'The current temperature is:',
                    ),
                    Text(
                      '$temperature °F',
                      style: Theme.of(context).textTheme.display1,
                    ),
                  ]),
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: update,
              tooltip: 'Update',
              child: Icon(Icons.update),
            ),
          );
  }
}

DarkSky Weather API

Create the API call to the DarkSky weather API by adding the http package in pubspec.yaml.

dependencies:
  http: "^0.12.0"
  flutter:
    sdk: flutter

darksky.dart

This simple file contains a function to get the current forecast.

import 'package:http/http.dart' as http;
import 'dart:convert';

typedef Future GetHttp(dynamic url, {Map<String, String> headers});
GetHttp get getHttp => http.get;

typedef dynamic JsonDecode(String source);
JsonDecode get jsonDecode => json.decode; 

const String url = "https://api.darksky.net/forecast/<insert-api-key-here>/37.8267,-122.4233";

Future _getForecast(GetHttp getHttp, JsonDecode jsonDecode,
    String url) async {

  var response = await getHttp(url);

  var decode = jsonDecode(response.body);

  return decode["currently"]["temperature"];
}

Future getForecast() => _getForecast(getHttp, jsonDecode, url);

Functional State and Model

Keeping the sample simple, the ForecastPageState was not completely functional. You can place all variables in a Model, and passing all functions as required into each method, to make this functional. Shown below is an updated ForecastPageState, made functional, along with an immutable Model class.

class ForecastModel
{
  const ForecastModel(this.temperature, this.loading);
  final double temperature;
  final bool loading;
}

class _ForecastPageState extends State<ForecastPage> {

  ForecastModel model = ForecastModel(0, false);

  void updateModel(Function update) => setState(() { model = update(); });

  void _updateTemperature(Future<double> getForecast(), Function update) async {

    update(() => ForecastModel(0, true));

    var forecast = await getForecast();

    update(() => ForecastModel(forecast, false));
  }

  @override
  Widget build(BuildContext context) {
    return ForecastPageUI.buildUI(context, 
                                  widget.title,
                                  () => _updateTemperature(darksky.getForecast, (func) => updateModel(func)),
                                  model);
  }
}

Here is a visualization of the Model, View and Update functionality pertaining to the Flutter State and Page.

 

Summary

MVU keeps everything simple and is very similar to how Flutter pages and states work. Combined with functional programming, it keeps all the context for each action in focus, allowing developers to come and go, without having to worry how the entire application works, or inadvertently affecting something else.

See Flutter-MVU for the sample repository of the entire MVU-Functional example.

© 2019 Adam Pedley