Implementation of MVU (Model View Update) pattern for Flutter. Inspired by TEA (The Elm Architecture) and Elmish (F# TEA implemetation)
This app architecture is based on three key things:
- Model (App state) must be immutable.
- View and Update fucntions must be pure.
- All side-effects should be separated from the UI logic.
The heart of the dartea application are three yellow boxes on the diagram above. First, the state of the app (Model) is mapped to the widgets tree (View). Second, events from the UI are translated into Messages and go to the Update function (together with current app state). Update function is the brain of the app. It contains all the presentation logic, and it MUST be pure. All the side-effects (such as database queries, http requests and etc) must be isolated using Commands and Subscriptions.
class Model {
final int counter;
Model(this.counter);
}
abstract class Message {}
class Increment implements Message {}
class Decrement implements Message {}
Widget view(BuildContext context, Dispatch<Message> dispatch, Model model) {
return Scaffold(
appBar: AppBar(
title: Text('Simple dartea counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'${model.counter}',
style: Theme.of(context).textTheme.display1,
),
Padding(
child: RaisedButton.icon(
label: Text('Increment'),
icon: Icon(Icons.add),
onPressed:() => dispatch(Increment()),
),
padding: EdgeInsets.all(5.0),
),
RaisedButton.icon(
label: Text('Decrement'),
icon: Icon(Icons.remove),
onPressed: () => dispatch(Decrement()),
),
],
),
),
);
}
Upd<Model, Message> update(Message msg, Model model) {
if (msg is Increment) {
return Upd(Model(model.counter + 1));
}
if (msg is Decrement) {
return Upd(Model(model.counter - 1));
}
return Upd(model);
}
void main() {
final program = Program(
() => Model(0), //create initial state
update,
view);
runApp(MyApp(program));
}
class MyApp extends StatelessWidget {
final Program darteaProgram;
MyApp(this.darteaProgram);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dartea counter example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: darteaProgram.build(key: Key('root_key')),
);
}
}
And that's it. Here is more advanced example.
- Single responsibility. Whole application state is in
model
, all the logic is inupdate
, all the UI is inview
. view
andupdate
are pure functions andmodel
is immutable - easy to test, easy to maintain.- Easy and straitforward composition. You can split youre app into nested components (
model
,view
,update
). - Side-effects are decoupled and isolated. It helps us to write and refactor presentation logic and side-effects separately.
model
andmessagess
describe all possible application's states.
setState()
is called on the root widget and it means that every widget in the tree should be rebuilt. Flutter is smart enough to make just incremental changes, but in general this is not so good.update
function is pure and easy for testing, but testing side-effects (commands
) could be tricky. Although I think in most cases we can avoid it, but it could be necessary sometimes. You can find example of commands testing here. It's a pretty good example of a Todo app.- Even though composition is easy and straightforward, but it requires some boilerplate code.