dartea 0.5.5

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

dartea

Build Status codecov

Implementation of MVU (Model View Update) pattern for Flutter. Inspired by TEA (The Elm Architecture) and Elmish (F# TEA implemetation)

dartea img

Key concepts

This app architecture is based on three key things:

  1. Model (App state) must be immutable.
  2. View and Update fucntions must be pure.
  3. 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.

Simple counter example

Model and Message:

class Model {
  final int counter;
  Model(this.counter);
}

abstract class Message {}
class Increment implements Message {}
class Decrement implements Message {}

View:

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()),
          ),
        ],
      ),
    ),
  );
}

Update:

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

Bootstrap program and run Flutter app

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.

Pros

  1. Single responsibility. Whole application state is in model, all the logic is in update, all the UI is in view.
  2. view and update are pure functions and model is immutable - easy to test, easy to maintain.
  3. Easy and straitforward composition. You can split youre app into nested components (model, view, update).
  4. Side-effects are decoupled and isolated. It helps us to write and refactor presentation logic and side-effects separately.
  5. model and messagess describe all possible application's states.

Cons

  1. 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.
  2. 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.
  3. Even though composition is easy and straightforward, but it requires some boilerplate code.

[0.5.5] - 27.07.2018

  • Support Dart2
  • Upgrade async package version to 2.0.7

[0.5.2] - 24.05.2018

  • Fix dependencies issue

[0.5.0] - 15.05.2018

  • Removed TArg from Prgram and Init. All arguments for init function should be captured in closure.
  • Changed Subscription mechamism.
    • new TSub type-parameter for Program. It's object for holding and managing subscription (like StreamSubscription).
    • subscription function is now called right after every update.
    • subscription is not Cmd anymore.
  • Added new mechamism for reacting on application lifecycle events.

[0.0.2] - 23.04.2018

  • New API. Now every Program is a stateful Widget.
  • Added Multiprogram app support (Programs composition, built-in Flutter navigation)

[0.0.1] - 11.04.2018

  • Initial implementation.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:dartea/dartea.dart';
import 'dart:async';

void main() {
  final program = Program<Model, Message, Timer>(
          init, //create app initial state
          update, //handle messages and returns updated state + side effects
          view, //create UI
          /* optional functions */
          subscription:
              _periodicTimerSubscription, //mange subscription to external source
          lifeCycleUpd:
              lifeCycleUpdate, //handle Flutter lifecycle events and returns updated state + side effects
          onError: (s, e) =>
              debugPrint('Handle app error: $e\n$s')) //handle all errors
      .withDebugTrace(); //Output to the console all the messages, model changes and etc.
  runApp(MyApp(program));
}

class MyApp extends StatelessWidget {
  final Program darteaProgram;

  MyApp(this.darteaProgram);

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter MVU example',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: darteaProgram.build(key: Key('root_key')),
    );
  }
}

///Model - immutable application state
class Model {
  final int counter;
  final bool autoIncrement;
  Model(this.counter, this.autoIncrement);
  Model copyWith({int counter, bool autoIncrement}) =>
      Model(counter ?? this.counter, autoIncrement ?? this.autoIncrement);

  @override
  String toString() => '{counter:$counter, autoIncrement:$autoIncrement}';
}

///Messages - described actions and events, which could affect [Model]
abstract class Message {}

class Increment implements Message {
  @override
  String toString() => 'Increment';
}

class Decrement implements Message {
  @override
  String toString() => 'Decrement';
}

class StartAutoIncrement implements Message {
  @override
  String toString() => 'StartAutoIncrement';
}

class StopAutoIncrement implements Message {
  @override
  String toString() => 'StopAutoIncrement';
}

///create initial [Model] + side-effects (optional)
Upd<Model, Message> init() => Upd(Model(0, false));

///Update - the heart of the [dartea] program. Handle messages and current model, returns updated model + side-effects(optional).
Upd<Model, Message> update(Message msg, Model model) {
  if (msg is Increment) {
    return Upd(model.copyWith(counter: model.counter + 1));
  }
  if (msg is Decrement) {
    return Upd(model.copyWith(counter: model.counter - 1));
  }
  if (msg is StartAutoIncrement) {
    return Upd(model.copyWith(autoIncrement: true));
  }
  if (msg is StopAutoIncrement) {
    return Upd(model.copyWith(autoIncrement: false));
  }
  return Upd(model);
}

///Simple timer for emulating some external events
const _timeout = const Duration(seconds: 1);
Timer _periodicTimerSubscription(
    Timer currentTimer, Dispatch<Message> dispatch, Model model) {
  if (model.autoIncrement) {
    if (currentTimer == null) {
      return Timer.periodic(_timeout, (_) => dispatch(Increment()));
    }
    return currentTimer;
  }
  currentTimer?.cancel();
  return null;
}

///Handle app lifecycle events, almost the same as [update] function
Upd<Model, Message> lifeCycleUpdate(AppLifecycleState appState, Model model) {
  switch (appState) {
    case AppLifecycleState.inactive:
    case AppLifecycleState.paused:
    case AppLifecycleState.suspending:
      return Upd(model.copyWith(autoIncrement: false));
    case AppLifecycleState.resumed:
    default:
      return Upd(model);
  }
}

///View - maps [Model] to the Flutter's Widgets tree
Widget view(BuildContext context, Dispatch<Message> dispatch, Model model) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Flutter MVU example'),
    ),
    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:
                  model.autoIncrement ? null : () => dispatch(Increment()),
            ),
            padding: EdgeInsets.all(5.0),
          ),
          RaisedButton.icon(
            label: Text('Decrement'),
            icon: Icon(Icons.remove),
            onPressed: model.autoIncrement ? null : () => dispatch(Decrement()),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Switch(
                value: model.autoIncrement,
                onChanged: (v) =>
                    dispatch(v ? StartAutoIncrement() : StopAutoIncrement()),
              ),
              Text('Auto increment every second')
            ],
          )
        ],
      ),
    ),
  );
}

Use this package as a library

1. Depend on it

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


dependencies:
  dartea: ^0.5.5

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:dartea/dartea.dart';
  
Version Uploaded Documentation Archive
0.5.5 Jul 27, 2018 Go to the documentation of dartea 0.5.5 Download dartea 0.5.5 archive
0.5.3 May 23, 2018 Go to the documentation of dartea 0.5.3 Download dartea 0.5.3 archive
0.5.2 May 23, 2018 Go to the documentation of dartea 0.5.2 Download dartea 0.5.2 archive
0.5.0 May 15, 2018 Go to the documentation of dartea 0.5.0 Download dartea 0.5.0 archive
0.0.2 Apr 23, 2018 Go to the documentation of dartea 0.0.2 Download dartea 0.0.2 archive
0.0.1 Apr 11, 2018 Go to the documentation of dartea 0.0.1 Download dartea 0.0.1 archive
Popularity:
Describes how popular the package is relative to other packages. [more]
75
Health:
Code health derived from static analysis. [more]
56
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
74
Learn more about scoring.

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

  • Dart: 2.0.0
  • pana: 0.12.4
  • Flutter: 0.9.5

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Health issues and suggestions

Fix lib/src/types.dart. (-43.75 points)

Analysis of lib/src/types.dart failed with 2 errors:

line 38 col 35: Evaluation of this constant expression throws an exception.

line 47 col 23: Evaluation of this constant expression throws an exception.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=1.20.1 <3.0.0
async ^2.0.7 2.0.8
flutter 0.0.0
Transitive dependencies
collection 1.14.11
meta 1.1.6
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test