mvc_pattern

Build Status Medium Pub.dev GitHub stars flutter and mvc

The "Kiss" of Flutter Frameworks

In keeping with the "KISS Principle", this is an attempt to offer the MVC design pattern to Flutter in an intrinsic fashion incorporating much of the Flutter framework itself. All in a standalone Flutter Package.

In truth, this all came about only because I wanted a place to put my 'mutable' code (the business logic for the app) without the compiler complaining about it! Placing such code in a StatefulWidget or a StatelessWidget is discouraged of course--only immutable code should be in those objects. Sure, all that code could go into the State object. That's good since you want access to the State object anyway. After all, it's the main player when it comes to 'State Management' in Flutter. However, it makes for rather big and messy State objects!

Placing the code in separate Dart files would be the solution, but then there would have to be a means to access that ever-important State object. I wanted the separate Dart file or files that had all the functionality and capability of the State object. In other words, that separate Dart file would to have access to a State object!

Now, I had no interest in re-inventing the wheel. I wanted to keep it all Flutter, and so I stopped and looked at Flutter closely to see how to apply some already known design pattern onto it. That's when I saw the State object (its build() function specifically) as 'The View,' and the separate Dart file or files with access to that State object as 'The Controller.'

This package is essentially the result, and it involves just two 'new' classes: StateMVC and ControllerMVC. A StateMVC object is a State object with an explicit life-cycle (Android developers will appreciate that), and a ControllerMVC object can be that separate Dart file with access to the State object (StateMVC in this case). All done with Flutter objects and libraries---no re-inventing here. It looks and tastes like Flutter.

Indeed, it just happens to be named after the 'granddaddy' of design patterns, MVC, but it's actually a bit more like the PAC design pattern. In truth, you could use any other architecture you like with it. By design, you can just use the classes, StateMVC, and ControllerMVC. Heck! You could call objects that extend ControllerMVC, BLoC's for all that matters! Again, all I wanted was some means to bond a State object to separate Dart files containing the 'guts' of the app. I think you'll find it useful.

Note, there is now the 'MVC framework' which wraps around this library package as its core: MVC

Installing

I don't always like the version number suggested in the 'Installing' page. Instead, always go up to the 'major' semantic version number when installing my library packages. This means always entering a version number trailing with two zero, '.0.0'. This allows you to take in any 'minor' versions introducing new features as well as any 'patch' versions that involves bugfixes. Semantic version numbers are always in this format: major.minor.patch.

  1. patch - I've made bugfixes
  2. minor - I've introduced new features
  3. major - I've essentially made a new app. It's broken backwards-compatibility and has a completely new user experience. You won't get this version until you increment the major number in the pubspec.yaml file.

And so, in this case, add this to your package's pubspec.yaml file instead:

dependencies:
  mvc_pattern:^6.0.0

For more information on this topic, read the article, The importance of semantic versioning.

Usage

Let’s demonstrate its usage with the ol’ ‘Counter app’ created every time you start a new Flutter project. In the example below, to utilize the package, three things are changed in the Counter app. The Class, _MyHomePageState, is extended with the Class StateMVC, a ‘Controller’ Class is introduced. It extends the Class, ControllerMVC, and a static instance of that Class is made available to the build() function. Done! (Note, you will find the 'source code' for this example in this package's test folder.)

With that, there's now a separation of ‘the Interface’ and ‘the data’ as intended with the MVC architecture. The build() function serves as 'the View.' It's concerned solely with the ‘look and feel’ of the app’s interface—‘how’ things are displayed. While it is the Controller that determines 'what’ is displayed. The Controller is also concerned with 'how' the app interacts with the user, and so it's involved in the app's event handling.

What data does the View display? It doesn’t know nor does it care! It ‘talks to’ the Controller instead. Again, it is the Controller that determines ‘what’ data the View displays. In this example, it’s a title and a counter. And when a button is pressed, the View again ‘talks to’ the Controller to address the event (i.e. calls one of the Controller’s public functions, incrementCounter()). myhomepage In this arrangement, the Controller is ‘talking back’ to the View by calling the View’s function, setState(), telling it to rebuild the widget tree.

viewandcontroller

Maybe we don’t want that. Maybe we want the View to be solely concerned with the interface and solely determine when to rebuild the widget tree (if at all). It’s a simple change. myhomepage2 The View knows how to 'talk to' the Controller, but the Controller doesn't need to know how to 'talk to' the View. Notice what I did to the Controller? Makes the API between the View and the Controller a little clearer.

view talks to contoller only

It does separate the ‘roles of responsibility’ a little more, doesn’t it? After all, it is the View that’s concerned with the interface. It would know best when to rebuild, no? Regardless, with this package, such things are left to the developer. Lastly, notice how I created a static String field in the MyApp class called, title. It’s named ‘MyApp’ after all—It should know its own title.

myapp You see in the 'App' class above, that the Controller is instantiated in the parent class. Doing so allows your Controller to now access the many 'events' fired in a typical Flutter app.

How about Model?

Currently, in this example, it’s the Controller that’s containing all the ‘business logic’ for the application. In some MVC implementations, it’s the Model that contains the business rules for the application. So how would that look? Well, it maybe could look like this: howaboutmodel The Model’s API is also a little cleaner with the use of a static members. The View doesn’t even know the Model exists. It doesn’t need to. It still ‘talks to’ the Controller, but it is now the Model that has all the ‘brains.’

Note above, it's a full screenshot of the counter app example, and there's further changes made compared to the previous examples. Note, the State object again extends StateMVC and the 'MyApp' class is returned to StatelessWidget. Allows the State object to address events instead. Here, the Controller is passed to its parent class to 'plug it into' the event handling of a typical Flutter Widget. Finally, note the Controller's property and function names have been changed. This merely demonstrates that there's no 'hard cold rules' about what the API is between the View, the Controller, and the Model. You merely need to be consistent so they can 'talk to' each other.

pac pattern

However, what if you want the View to talk to the Model? Maybe because the Model has zillions of functions, and you don’t want the Controller there merely ‘to relay’ the Model’s functions and properties over to the View. You could simply provide the Model to the View. The View can then call the Controller's properties and functions as well the Model’s.

viewcantalk viewtomodel Not particularly pretty. I mean, at this point, you don't even need 'the Controller', but it merely demonstrates the possibilities. With this MVC implementation, you have options, and developers love options.

viewcontrollermodel

Below, I've changed it a little bit. The View still has access to the Model, but the Controller is still responsible for any 'event handling' and responds to the user pressing that lone button in the app. viewtomodel2

The Counter App

Below is the Counter App you can copy n' paste to quickly try out:

import 'package:flutter/material.dart';

import 'package:mvc_pattern/mvc_pattern.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  // Fields in a Widget subclass are always marked "final".

  static final String title = 'Flutter Demo Home Page';

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {

  final Controller _con = Controller.con;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              widget.title,
            ),
            Text(
              '${_con.counter}',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(
              _con.incrementCounter
          );
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
class Controller extends ControllerMVC {
  /// Singleton Factory
  factory Controller() {
    if (_this == null) _this = Controller._();
    return _this;
  }
  static Controller _this;

  Controller._();

  /// Allow for easy access to 'the Controller' throughout the application.
  static Controller get con => _this;

  int get counter => _counter;
  int _counter = 0;
  void incrementCounter() => _counter++;
}

Now you can easily introduce a Model:

class Controller extends ControllerMVC {
  factory Controller() {
    if (_this == null) _this = Controller._();
    return _this;
  }
  static Controller _this;

  Controller._();

  /// Allow for easy access to 'the Controller' throughout the application.
  static Controller get con => _this;

  int get counter => Model.counter;
  void incrementCounter() {
    /// The Controller knows how to 'talk to' the Model. It knows the name, but Model does the work.
    Model._incrementCounter();
  }
}
class Model {
  static int get counter => _counter;
  static int _counter = 0;
  static int _incrementCounter() => ++_counter;
}

Of course, you're free to 'switch out' variations of the Controller over time. In this case, you no longer need to assign a local variable, _con, but instead use a static reference: Controller.incrementCounter;

class Controller extends ControllerMVC {
  static int get counter => Model.counter;
  static void incrementCounter() => Model._incrementCounter();
}

Your First Flutter App: startup_namer

This is the application offered in the website, Write Your First Flutter App, when you're first learning Flutter. This version has this MVC implementation.

MyApp.dart

import 'package:flutter/material.dart';

import 'package:mvc_pattern/mvc_pattern.dart';

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

class MyApp extends AppMVC {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      theme: ThemeData(
        primaryColor: Colors.white,
      ),
      home: RandomWords(),
    );
  }
}

StateView

Note the two classes below. RandomWords is extended by the StatefulWidgetMVC and the other, RandomWordsState, extended by StateMVC. With the class, RandomWords, the super constructor is passed the 'State Object', RandomWordsState (StateMVC). In turn, the State Object takes in the Controller Class, Con.

RandomWords.dart

import 'package:mvc_pattern/mvc_pattern.dart';

class RandomWords extends StatefulWidgetMVC {
  RandomWords() : super(RandomWordsState(Con()));
}

class RandomWordsState extends StateMVC<RandomWords> {
  RandomWordsState(Con con) : super(con);

  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);

  @override

  /// the View
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Startup Name Generator'),
        actions: <Widget>[
          IconButton(
              icon: const Icon(Icons.list),
              onPressed: () {
                pushSaved(context);
              }),
        ],
      ),
      body: _buildSuggestions(),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // The itemBuilder callback is called once per suggested word pairing,
      // and places each suggestion into a ListTile row.
      // For even rows, the function adds a ListTile row for the word pairing.
      // For odd rows, the function adds a Divider widget to visually
      // separate the entries. Note that the divider may be difficult
      // to see on smaller devices.
      itemBuilder: (context, i) {
        // Add a one-pixel-high divider widget before each row in theListView.
        if (i.isOdd) return Divider();

        // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
        // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
        // This calculates the actual number of word pairings in the ListView,
        // minus the divider widgets.
        final index = i ~/ 2;
        // If you've reached the end of the available word pairings...
        if (index >= Con.length) {
          // ...then generate 10 more and add them to the suggestions list.
          Con.addAll(10);
        }
        return buildRow(index);
      },
    );
  }

  void pushSaved(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final Iterable<ListTile> tiles = this.tiles;

          final List<Widget> divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return Scaffold(
            appBar: AppBar(
              title: const Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }

  Widget buildRow(int index) {
    if (index == null && index < 0) index = 0;

    String something = Con.something(index);

    final alreadySaved = Con.contains(something);

    return ListTile(
      title: Text(
        something,
        style: _biggerFont,
      ),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          Con.somethingHappens(something);
        });
      },
    );
  }

  Iterable<ListTile> get tiles => Con.mapHappens(
        (String something) {
          return ListTile(
            title: Text(
              something,
              style: _biggerFont,
            ),
          );
        },
      );
}

Controller.dart

Note how its all made up of static members and turns to Model for all the data.

import 'package:mvc_pattern/mvc_pattern.dart';

class Con extends ControllerMVC {
  static int get length => Model.length;

  static void addAll(int count) => Model.addAll(count);

  static String something(int index) => Model.wordPair(index);

  static bool contains(String something) => Model.contains(something);

  static void somethingHappens(String something) => Model.save(something);

  static Iterable<ListTile> mapHappens<ListTile>(Function f) => Model.saved(f);
}

Model.dart

This Model works with the third-party library, english_words. The rest of the application has no idea. The Model is solely concern with where the 'words' originate from.

import 'package:english_words/english_words.dart';

class Model {
  static final List<String> _suggestions = [];
  static int get length => _suggestions.length;

  static String wordPair(int index) {
    if (index == null || index < 0) index = 0;
    return _suggestions[index];
  }

  static bool contains(String pair) {
    if (pair == null || pair.isEmpty) return false;
    return _saved.contains(pair);
  }

  static final Set<String> _saved = Set();

  static void save(String pair) {
    if (pair == null || pair.isEmpty) return;
    final alreadySaved = contains(pair);
    if (alreadySaved) {
      _saved.remove(pair);
    } else {
      _saved.add(pair);
    }
  }

  static Iterable<ListTile> saved<ListTile>(Function f) => _saved.map(f);

  static Iterable<String> wordPairs([int count = 10]) => makeWordPairs(count);

  static void addAll(int count) {
    _suggestions.addAll(wordPairs(count));
  }
}

Iterable<String> makeWordPairs(int count) {
  return generateWordPairs().take(count).map((pair){return pair.asPascalCase;});
}

Further information on the MVC package can be found in the article, ‘MVC in Flutter’ online article

Libraries

mvc_pattern
This library contains the classes necessary to develop apps using the MVC design pattern separating the app's 'interface' from its 'business logic' and from its 'data source' if any. [...]