mvc_pattern
Note, mvc_pattern has been rebranded, StateX. This package here is soon deprecated.
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.
- patch - I've made bugfixes
- minor - I've introduced new features
- 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:
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’
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.