mvc_pattern 1.2.3

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 86

mvc_pattern

Build Status 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. It is expected the user of this package to be knowledgeable with such framework architectures.

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 that extends the Class, ControllerMVC, and a static instance of that Class is made available to the build() function. Done!

With that, there is now a separation of ‘the Interface’ and ‘the data’ as it’s intended with the MVC architecture. The build() function (the View) is concerned solely with the ‘look and feel’ of the app’s interface—‘how’ things are displayed. While, in this case, it is the Controller that’s concerned with ‘what’ is displayed.

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 case, it’s a title and a counter. When a button is pressed, the View again ‘talks to’ the Controller to address the event (i.e. It calls one of the Controller’s public functions, incrementCounter()). myhomepage

mvc pattern

In this arrangement, the Controller is ‘talking back’ to the View by calling the View’s function, setState(), to tell it to rebuild.

Maybe we don’t want that. Maybe we want the View to be solely concern with the interface and only determine when to rebuild or not. It’s a simple change.

view talks to contoller only

myhomepage 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. Also, notice what I did with the app’s title? I created a static String field in the MyApp class called, title. It’s named ‘MyApp’ after all—It should know its own title.

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: controller I decided to make the Model’s API a little cleaner with the use of a static members. As you can deduce, the changes were just made in the Controller. 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.’

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 then calls the Model’s properties and functions directly.

mvc pattern classic

myhomepagestate Not particularly pretty. I would have just kept the Static members in the Model, and have the View call them instead (or not do that at all frankly), but I’m merely demonstrating the possibilities. With this MVC implementation, you have options, and developers love options.

The Counter App

Below is the full Counter App with the MVC implementation.

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(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or press Run > Flutter Hot Reload in IntelliJ). Notice that the
        // counter didn't reset back to zero; the application is not restarted.
        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 StateMVC {

  _MyHomePageState():super(Controller()){

    _con = Controller.con;
  }
  Controller _con;

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return new Scaffold(
      appBar: new 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: new Text(MyHomePage.title),
      ),
      body: new Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: new Column(
          // Column is also layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug paint" (press "p" in the console where you ran
          // "flutter run", or select "Toggle Debug Paint" from the Flutter tool
          // window in IntelliJ) to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(MyHomePage.title,
            ),
            new Text(
              '${_con.displayThis}',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: (){
          setState(
            _con.whatever
          );
        },
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
class Controller extends ControllerMVC{

  Controller(){
    con = this;
  }
  static Controller con;

  @override
  initState(){
    /// Demonstrating how the 'initState()' is easily implemented.
    _counter = Model.counter;
  }

  int get displayThis => _counter;
  int _counter;

  void whatever(){
    /// The Controller knows how to 'talk to' the Model. It knows the name, but Model does the work.
    _counter = Model._incrementCounter();
  }
}
class Model{

  static int get counter => _counter;
  static int _counter = 0;

  static int _incrementCounter(){
    return ++_counter;
  }
}

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 {
  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, ‘Flutter + MVC at Last!’ online article Repository (GitHub) API Docs

MVC Pattern

[0.0.1] - Oct. 13, 2018. Initial Release
[0.0.3] - Oct. 18, 2018. fix on AppMVC and StateEvents
[0.0.4] - Oct. 18, 2018. fix on StateMVC.add()
[0.0.5] - Oct. 18, 2018. fix on StatedWidget
[0.0.6] - Oct. 18, 2018. fix on StateMVC.con()
[0.0.7] - Oct. 19, 2018. Provide example/main.dart
[0.0.8] - Oct. 20, 2018. Updated the README.md
[0.0.9] - Oct. 21, 2018. fix on ViewMVC & StateViewMVC
[0.0.10] - Oct. 22, 2018. include travis.yml
[0.0.11] - Oct. 23, 2018. class StateMVC with _ControllerListing, _StateEventList
[0.0.12] - Oct. 23, 2018. class ViewMVC with _ControllerListing
[0.0.13] - Oct. 23, 2018. fix on _ControllerListing
[0.0.14] - Oct. 24, 2018. fix on StateViewMVC & AppMVC & @protected
[1.0.0] - Oct. 24, 2018. Official Production Release
[1.1.0] - Oct. 25, 2018. keyId in StateEvents
[1.1.1] - Oct. 27, 2018. StatefulWidgetMVC deemed deprecated
[1.2.0] - Oct. 29, 2018. enhance AppMVC
[1.2.1] - Nov. 02, 2018. fix on StateViewMVC
[1.2.2] - Nov. 02, 2018. fix on StatedWidget
[1.2.3] - Nov. 13, 2018. test for TestWidgetsFlutterBinding

example/lib/main.dart

/// Note: This license has also been called the "Simplified BSD License" and the "FreeBSD License".
/// See also the 2-clause BSD License.
///
/// Copyright 2018 www.andrioussolutions.com
///
/// Redistribution and use in source and binary forms, with or without modification,
/// are permitted provided that the following conditions are met:
///
/// 1. Redistributions of source code must retain the above copyright notice,
/// this list of conditions and the following disclaimer.
///
/// 2. Redistributions in binary form must reproduce the above copyright notice,
/// this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
///
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
/// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
/// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
/// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
/// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
/// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
/// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
/// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
/// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import 'package:flutter/material.dart';

/// To try this example,
/// Uncomment the import statement the line in the function, makeWordPairs(), below.
/// Of course, that means you'll have to modify your pubspec.yaml file.
//import 'package:english_words/english_words.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(),
    );
  }
}

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

class _RandomWordsState extends StateMVC {
  _RandomWordsState() : 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,
            ),
          );
        },
      );
}

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);
}

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) {
  /// Uncomment the import statement above and the line below to try this example.
  /// Of course, that means you'll have to modify your pubspec.yaml
//  return generateWordPairs().take(count).map((pair){return pair.asPascalCase;});
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  mvc_pattern: ^1.2.3

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter packages get

Alternatively, your editor might support flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:mvc_pattern/mvc_pattern.dart';
  
Version Uploaded Documentation Archive
1.2.3 Nov 16, 2018 Go to the documentation of mvc_pattern 1.2.3 Download mvc_pattern 1.2.3 archive
1.2.2 Nov 8, 2018 Go to the documentation of mvc_pattern 1.2.2 Download mvc_pattern 1.2.2 archive
1.2.1 Nov 3, 2018 Go to the documentation of mvc_pattern 1.2.1 Download mvc_pattern 1.2.1 archive
1.2.0 Oct 30, 2018 Go to the documentation of mvc_pattern 1.2.0 Download mvc_pattern 1.2.0 archive
1.1.1 Oct 28, 2018 Go to the documentation of mvc_pattern 1.1.1 Download mvc_pattern 1.1.1 archive
1.1.0 Oct 26, 2018 Go to the documentation of mvc_pattern 1.1.0 Download mvc_pattern 1.1.0 archive
1.0.0 Oct 25, 2018 Go to the documentation of mvc_pattern 1.0.0 Download mvc_pattern 1.0.0 archive
0.0.14 Oct 24, 2018 Go to the documentation of mvc_pattern 0.0.14 Download mvc_pattern 0.0.14 archive
0.0.13 Oct 24, 2018 Go to the documentation of mvc_pattern 0.0.13 Download mvc_pattern 0.0.13 archive
0.0.12 Oct 23, 2018 Go to the documentation of mvc_pattern 0.0.12 Download mvc_pattern 0.0.12 archive

All 19 versions...

Popularity:
Describes how popular the package is relative to other packages. [more]
73
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
86
Learn more about scoring.

We analyzed this package on Nov 16, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0
  • pana: 0.12.6
  • Flutter: 0.11.3

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Health suggestions

Fix lib/mvc_pattern.dart. (-0.50 points)

Analysis of lib/mvc_pattern.dart reported 1 hint:

line 819 col 8: Method doesn't override an inherited method.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=1.19.0 <4.0.0
flutter 0.0.0
flutter_test 0.0.0
Transitive dependencies
async 2.0.8
boolean_selector 1.0.4
charcode 1.1.2
collection 1.14.11
matcher 0.12.3+1 0.12.4
meta 1.1.6
path 1.6.2
quiver 2.0.1
sky_engine 0.0.99
source_span 1.4.1
stack_trace 1.9.3
stream_channel 1.6.8
string_scanner 1.0.4
term_glyph 1.0.1
test_api 0.2.1
typed_data 1.1.6
vector_math 2.0.8