mvc_pattern

codecov CI Medium Pub.dev GitHub stars Last Commit likes

Note, mvc_pattern has been rebranded, StateX. This package here is soon deprecated.

StateX

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.

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:
  state_extended:^8.9.0

Documentation

Turn to this free Medium article for a full overview of the package plus examples: FlutterFramework

Example Code

Copy and paste the code below to get started. Examine the paths specified at the start of every code sequence to determine where these files are to be located.

/// example/lib/main.dart

import 'package:example/src/view.dart';

void main() => runApp(MyApp(key: const Key('MyApp')));
/// example/src/app/view/my_app.dart

import 'package:example/src/view.dart';

import 'package:example/src/controller.dart';

class MyApp extends AppStatefulWidgetMVC {
  const MyApp({Key? key}) : super(key: key);

  /// This is the App's State object
  @override
  AppStateMVC createState() => _MyAppState();
}

class _MyAppState extends AppStateMVC<MyApp> {
  factory _MyAppState() => _this ??= _MyAppState._();
  static _MyAppState? _this;

  @override
  Widget buildApp(BuildContext context) => MaterialApp(
        home: FutureBuilder<bool>(
            future: initAsync(),
            builder: (context, snapshot) {
              //
              if (snapshot.hasData) {
                //
                if (snapshot.data!) {
                  /// Key identifies the widget. New key? New widget!
                  /// Demonstrates how to explicitly 're-create' a State object
                  return MyHomePage(key: UniqueKey());
                } else {
                  //
                  return const Text('Failed to startup');
                }
              } else if (snapshot.hasError) {
                //
                return Text('${snapshot.error}');
              }
              // By default, show a loading spinner.
              return const Center(child: CircularProgressIndicator());
            }),
      );
}
/// example/src/app/controller/app_controller.dart

import 'package:example/src/view.dart';

class AppController extends ControllerMVC with AppControllerMVC {
  factory AppController() => _this ??= AppController._();
  AppController._();
  static AppController? _this;

  /// Initialize any 'time-consuming' operations at the beginning.
  /// Initialize asynchronous items essential to the Mobile Applications.
  /// Typically called within a FutureBuilder() widget.
  @override
  Future<bool> initAsync() async {
    // Simply wait for 10 seconds at startup.
    /// In production, this is where databases are opened, logins attempted, etc.
    return Future.delayed(const Duration(seconds: 10), () {
      return true;
    });
  }

  /// Supply an 'error handler' routine if something goes wrong
  /// in the corresponding initAsync() routine.
  /// Returns true if the error was properly handled.
  @override
  bool onAsyncError(FlutterErrorDetails details) {
    return false;
  }
}
/// example/src/home/view/my_home_page.dart

import 'package:example/src/view.dart';

import 'package:example/src/controller.dart';

/// The Home page
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, this.title = 'Flutter Demo'}) : super(key: key);

  // Fields in a StatefulWidget should always be "final".
  final String title;

  @override
  State createState() => _MyHomePageState();
}

/// This 'MVC version' is a subclass of the State class.
/// This version is linked to the App's lifecycle using [WidgetsBindingObserver]
class _MyHomePageState extends StateMVC<MyHomePage> {
  /// Let the 'business logic' run in a Controller
  _MyHomePageState() : super(Controller()) {
    /// Acquire a reference to the passed Controller.
    con = controller as Controller;
  }
  late Controller con;

  @override
  void initState() {
    /// Look inside the parent function and see it calls
    /// all it's Controllers if any.
    super.initState();

    /// Retrieve the 'app level' State object
    appState = rootState!;

    /// You're able to retrieve the Controller(s) from other State objects.
    var con = appState.controller;

    con = appState.controllerByType<AppController>();

    con = appState.controllerById(con?.keyId);
  }

  late AppStateMVC appState;

  /// This is 'the View'; the interface of the home page.
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              /// Display the App's data object if it has something to display
              if (con.dataObject != null && con.dataObject is String)
                Padding(
                  padding: const EdgeInsets.all(30),
                  child: Text(
                    con.dataObject as String,
                    key: const Key('greetings'),
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: Theme.of(context).textTheme.headline4!.fontSize,
                    ),
                  ),
                ),
              Text(
                'You have pushed the button this many times:',
                style: Theme.of(context).textTheme.bodyText2,
              ),
              // Text(
              //   '${con.count}',
              //   style: Theme.of(context).textTheme.headline4,
              // ),
              SetState(
                builder: (context, dataObject) => Text(
                  '${con.count}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          key: const Key('+'),

          /// Refresh only the Text widget containing the counter.
          onPressed: () => con.incrementCounter(),

          /// The traditional approach calling the State object's setState() function.
          // onPressed: () {
          //   setState(con.incrementCounter);
          // },
          /// You can have the Controller called the interface (the View).
//          onPressed: con.onPressed,
          child: const Icon(Icons.add),
        ),
      );

  /// Supply an error handler for Unit Testing.
  @override
  void onError(FlutterErrorDetails details) {
    /// Error is now handled.
    super.onError(details);
  }
}
/// example/src/home/controller/controller.dart

import 'package:example/src/view.dart';

import 'package:example/src/model.dart';

class Controller extends ControllerMVC {
  factory Controller([StateMVC? state]) => _this ??= Controller._(state);
  Controller._(StateMVC? state)
      : _model = Model(),
        super(state);
  static Controller? _this;

  final Model _model;

  /// Note, the count comes from a separate class, _Model.
  int get count => _model.counter;

  // The Controller knows how to 'talk to' the Model and to the View (interface).
  void incrementCounter() {
    //
    _model.incrementCounter();

    /// Only calls only 'SetState' widgets
    /// or widgets that called the inheritWidget(context) function
    inheritBuild();

    /// Retrieve a particular State object.
    final homeState = stateOf<MyHomePage>();

    /// If working with a particular State object and if divisible by 5
    if (homeState != null && _model.counter % 5 == 0) {
      //
      dataObject = _model.sayHello();
      setState(() {});
    }
  }

  /// Call the State object's setState() function to reflect the change.
  void onPressed() => setState(() => _model.incrementCounter());
}
/// example/src/view.dart

export 'package:flutter/material.dart' hide StateSetter;

export 'package:state_extended/state_extended.dart';

export 'package:example/src/app/view/my_app.dart';

export 'package:example/src/home/view/my_home_page.dart';

export 'package:example/src/home/view/page_01.dart';

export 'package:example/src/home/view/page_02.dart';

export 'package:example/src/home/view/page_03.dart';

export 'package:example/src/home/view/common/build_page.dart';
/// example/src/controller.dart

export 'package:example/src/app/controller/app_controller.dart';

export 'package:example/src/home/controller/controller.dart';

export 'package:example/src/home/controller/another_controller.dart';

export 'package:example/src/home/controller/yet_another_controller.dart';
/// example/src/model.dart

export 'package:example/src/home/model/data_source.dart';

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.