MVC Application

Medium Pub.dev GitHub stars

A Flutter Framework using the MVC Design Pattern

Installing I don't always like the version number suggested in the 'Installing' page. Instead, always go up to the 'minor' semantic version number when installing this library package. This means always trailing with one zero, '.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. I know I should be changing the 'major' instead, but since I chose to release this to production with a lot of changes to come, I don't want to be up to version '27.0.0' before I know it!

  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_application:^5.7.0

For more information on version numbers: The importance of semantic versioning.

Note, in fact, this package serves as a 'wrapper' to the core MVC packeage:

MVC Pattern

Pub.devGitHub stars

Usage

Like many other design patterns and architectures, MVC separates three common 'areas of concern' found in computer programs. Specifically, there's the separation of the program's logic from its inteface and from its data. The many design patterns that have come into vogue since MVC's inception in the early 1970's are, in fact, decendants of MVC.

Note, computer platforms themselves have come full circle in the last 40 years. From mainframes to desktop computers in the first twenty years, and then website applications to mobile apps in the last twenty years. A computer now fits in the palm of your hand, and because of this, MVC again fits as the software architecture for the apps it runs. mvcTypes

Implementing the MVC framework using two common example apps:

The Counter App
import 'package:flutter/material.dart';

import 'package:mvc_application/view.dart' show App, AppView, Colors, StateMVC;

import 'package:mvc_application/controller.dart' show ControllerMVC;

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

class  MyApp extends App{
  // Allow for hot reloads.
  @override
  createView() => View();
}

class View extends AppView {
  factory View() => _this ??= View._();
  View._()
      : super(
    title: 'Flutter Demo',
    home: MyHomePage(),
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
  );
  static View _this;
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);
  // Fields in a StatefulWidget should always be "final".
  final String title = 'Flutter Demo Home Page';
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends StateMVC<MyHomePage> {
  _MyHomePageState(): super(Controller()){
  // Acquire a reference to the particular Controller.
  con = controller;
  }
  Controller con;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        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 {
  factory Controller() => _this ??= Controller._();
  static Controller _this;
  Controller._();

  int get counter => _Model.counter;
  // The Controller knows how to 'talk to' the Model.
  void incrementCounter() => _Model._incrementCounter();
}

class _Model {
  static int get counter => _counter;
  static int _counter = 0;
  static int _incrementCounter() => ++_counter;
}

Name Generator App

import 'package:english_words/english_words.dart' show generateWordPairs;

import 'package:flutter/material.dart';

import 'package:mvc_application/view.dart' show App, AppView, Colors, StateMVC;

import 'package:mvc_application/controller.dart' show ControllerMVC;

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

class NameApp extends App {
  // Allows for hot reloads.
  @override
  createView() => MyApp();
}

class MyApp extends AppView {
  factory MyApp() => _this ??= MyApp._();
  MyApp._()
      : super(
          title: 'Startup Name Generator',
          home: RandomWords(),
          theme: ThemeData(
            primaryColor: Colors.white,
          ),
          debugShowCheckedModeBanner: false,
        );
  static MyApp _this;
}

class RandomWords extends StatefulWidget {
  @override
  State createState() => _RandomWordsState();
}

class _RandomWordsState extends StateMVC<RandomWords> {
  _RandomWordsState() : super(Con()) {
    con = controller;
  }
  Con con;

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

  @override
  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),
      itemBuilder: (context, i) {
        // Add a one-pixel-high divider widget before each row in theListView.
        if (i.isOdd) return Divider();
        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,
            ),
          );
        },
      );
}

class Con extends ControllerMVC {
  // Supply only one instance of this Controller class.
  factory Con() => _this ??= Con._();

  static Con _this;

  Con._() {
    model = _Model();
  }

  _Model model;

  int get length => model.length;

  void addAll(int count) => model.addAll(count);

  String something(int index) => model.wordPair(index);

  bool contains(String something) => model.contains(something);

  void somethingHappens(String something) => model.save(something);

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

class _Model {
  final List<String> _suggestions = [];

  int get length => _suggestions.length;

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

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

  final Set<String> _saved = Set();

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

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

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

  void addAll(int count) => _suggestions.addAll(wordPairs(count));
}

Iterable<String> _makeWordPairs(int count) =>
    generateWordPairs().take(count).map((pair) => pair.asPascalCase);

Additional Documentation

Please begin with the article, ‘Flutter + MVC at Last!’ online article

Follow up with Bazaar in MVC and Shrine in MVC and Weather App in "mvc pattern" bizzarMVC shrineMVC MVCWeatherApp

Optionally, there is the 3-part series beginning with, MVC in Flutter MVCFlutter

Further articles include, A Design Pattern for Flutter. designpattern

Other Dart Packages

packages Other Dart packages from the author can also be found at Pub.dev

Libraries

controller
model
prefs
settings
view