Giter Club home page Giter Club logo

dartea's Introduction

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

Update with side-effects:

Upd<Model, Message> update(Message msg, Model model) {  
  final persistCounterCmd = Cmd.ofAsyncAction(()=>Storage.save(model.counter));
  if (msg is Increment) {    
    return Upd(Model(model.counter + 1), effects: persistCounterCmd);
  }
  if (msg is Decrement) {
    return Upd(Model(model.counter - 1), effects: persistCounterCmd);
  }
  return Upd(model);
}

Create 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.

External world and subscriptions

Dartea program is closed loop with unidirectional data-flow. Which means it's closed for all the external sources (like sockets, database and etc). To connect Dartea program to some external events source one should use Subscriptions. Subscription is just a function (like view and update) with signature:

TSubHolder Function(TSubHolder currentSub, Dispatch<TMsg> dispatch, TModel model);

This function is called from Dartea engine right after every model's update. Here is an example of Timer subscription from the counter example.

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

There is a flag autoIncrement in model for controlling state of a subscription. currentTimer parameter is a subscription holder, an object which controls subscription lifetime. It's generic parameter, so it could be anything (for example StreamSubscription in case of dart's built-in Streams). If this parameter is null then it means that there is no active subscription at this moment. Then we could create new Timer subscription (if model's state is satisfied some condition) and return it as a result. Returned currentTimer subscription will be stored inside Dartea engine and passed as a parameter to this function on the next model's update. If we want to cancel current subscription then just call cancel() (or dispose(), or whatever it uses for releasing resources) and return null. Also there is dispatch parameter, which is used for sending messages into the Dartea progam loop (just like in view function). When Dartea program is removed from the widgets tree and getting disposed it calls subscription function last time to prevent memory leak. One should cancel the subscription if it happened.

Full counter example is here

Scaling and composition

First of all we need to say that MVU or TEA is fractal architecture. It means that we can split entire app into small MVU-components and populate some tree from them.

Traditional Elm composition

Traditional MVU fractals

You can see how our application tree could look like. The relations are explicit and very strict. We can describe it in code something like this:

class BlueModel {
  final Object stateField;
  final YellowModel yellow;
  final GreenModel green;
  //constructor, copyWith...
}

abstract class BlueMsg {}
class UpdateFieldBlueMsg implements BlueMsg {
  final Object newField;
}
class YellowModelBlueMsg implements BlueMsg {
  final YellowMsg innerMsg;
}
class GreenModelBlueMsg implements BlueMsg {
  final GreenMsg innerMsg;
}

Upd<BlueModel, BlueMsg> updateBlue(BlueMsg msg, BlueModel model) {
  if (msg is UpdateFieldBlueMsg) {
    return Upd(model.copyWith(stateField: msg.newField));
  }
  if (msg is YellowModelBlueMsg) {
    //update yellow sub-model
    final yellowUpd = yellowUpdate(msg.innerMsg, model.yellow);
    return Upd(model.copyWith(yellow: yellowUpd.model));
  }
  //the same for green model
}

Widget viewBlue(BuildContext ctx, Disptach<BlueMsg> dispatch, BlueModel model) {
  return Column(
    children: [
      viewField(model.stateField),
      //yellow sub-view
      viewYellow((m)=>dispatch(YellowModelBlueMsg(innerMsg: m)), model.yellow),
      //green sub-view
      viewGreen((m)=>dispatch(GreenModelBlueMsg(innerMsg: m)), model.green),
    ],
  );
}

//The same for all other components

As we can see everything is straightforward. Each model holds strong references to its sub-models and responsible for updating and displaying them. It's typical composition of an elm application and it works fine. The main drawback is bunch of boilerplate: fields for all sub-models, messages wrappers for all sub-models, huge update function. Also performance could be an issue in case of huge widgets tree with list view and frequent model updates. I recommend this approach for all screens where components logically strictly connected and no frequent updates of leaves components (white, red and grey on the picture).

Alternative composition

In continue with Flutter's slogan "Everything is a widget!" we could imagine that each MVU-component of our app is a widget. And, fortunately, that is true. Program is like container for core functions (init, update, view, subscribe and etc) and when build() is called new widget is created and could be mounted somewhere in the widgets tree. Moreover Dartea has built-in ProgramWidget for more convinient way putting MVU-component into the widgets tree.

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DarteaMessagesBus(
      child: MaterialApp(        
        home: HomeWidget(),
      ),
    );
  }
}
class HomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProgramWidget(
      key: DarteaStorageKey('home'),
      init: _init,
      update: _update,
      view: _view,
      withDebugTrace: true,
      withMessagesBus: true,
    );
  }
}

First, we added special root widget DarteaMessagesBus. It's used as common messages bus for whole our app and should be only one per application, usually as root widget. It means that one MVU-component (or ProgramWidget) can send to another a message without explicit connections. To enable ability receiving messages from another components we need to set flag withMessagesBus to true, it makes our component open to outside world. There are two ways to send a message from one component to another.

//from update function
Upd<Model, Msg> update(Msg msg, Model model) {
  return Upd(model, msgsToBus: [AnotherModelMsg1(), AnotherModelMsg2()]);
}
//from view function
Widget view(BuildContext ctx, Dispatch<Msg> dispatch, Model model) {
  return RaisedButton(
    //...
    onPressed:(){
      final busDispatch = DarteaMessagesBus.dispatchOf(ctx);
      busDispatch?(AnotherModelMsg3());
    },
    //..
  );
}

And if there are any components which set withMessagesBus to true and can handle AnotherModelMsg1, AnotherModelMsg2 or AnotherModelMsg3, then they receive all those messages.

Second, we added special key DarteaStorageKey('home') for ProgramWidget. That means that after every update model is saved in PageStorage using that key. And when ProgramWidget with the same key is removed from the tree and then added again it restores latest model from the PageStorage instead of calling init again. It could be helpfull in many cases, for example when there is BottomNavigationBar.

Widget _view(BuildContext ctx, Dispatch<HomeMsg> dispatch, HomeModel model) {
  return Scaffold(
    //...
    body: model.selectedTab == Tab.trending
        ? ProgramWidget(
            key: DarteaStorageKey('trending_tab_program'),
            //init, update, view
          )
    )
        : ProgramWidget(
            key: DarteaStorageKey('search_tab_program'),
            //init, update, view
          )
    ),
    bottomNavigationBar: _bottomNavigation(ctx, dispatch, model.selectedTab),
    //...
  );
}

Here we create new ProgramWidget when tab is switched, but model for each tab is saved and restored automatically and we do not lose UI state. See full example of this approach in GitHub client example Using common messages bus and auto-save\restore mechanism helps us to compose loosely coupled components ProgramWidget. Communication protocol is described via messages. It reduces boilerplate code, removes strong connections. But at the same time it creates implicit connections between components. I suggest to use this approach when components are not connected logically, for example filter-component and content-component, tabs.

Sample apps

dartea's People

Contributors

jjclane avatar nyck33 avatar p69 avatar rosenstrauch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

dartea's Issues

TextFields are getting rebuild even when I use GlobalKeys.

To reproduce:

Program(
  view: (c,d,m) => TextField(key: GlobalKey())
  init: () => Upd(null),
  update: (msg, m) => Upd(null),
);

Each time you try to focus TextField it rebuilds and thus unfocuses. Without GlobalKey specified that is not a problem.

How to do routing?

Not an issue but a question.

What do you recommend for routing for SPA's using Dartea?
Something like Elm's Navigation module for Dart?

Awesome

Am loving this approach.
The fractal self similar aspect is create.

One concern I have is about performance. When a data change occurs is it updating all of the widgets , even though only one widget is affected by the data change ? I am not sure.

Dartea for a Webapp

Another question please. Is there a Dartea equivalent for Web? If not, do you have a plan to create one?

Wrong information in readme

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.

This isn't true.
First of all, a setState on the root widget doesn't rebuild the whole widget tree.
Secondly, flutter doesn't do any sort of incremental change.

In flutter things are based around the class instance. If you reuse the same class instance as the old build call, flutter will stop any further build.

Which is why the following:

class _State extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    Future.microtask( () => setState(() {}) );

    return widget.child
  }
}

will actually never force its child to rebuild even if it updates every frames.

type 'NoSuchMethodError' is not a subtype of type 'Exception'

Hi, I'm getting the above error as I try to introduce 'effects' to one of my messages for http calls. (The program before this change worked fine just dealing with Model changes.)

The stack trace doesn't refer directly to my codes, so I'm a bit lost what to fix, so your help will be much appreciated.

FYI. Someone on the internet thought the following lines in Dartea's widget.dart might be the issue, because "our onError handler is passed Errors and Exceptions, but you strictly restrict it to Exceptions, that's the problem." but I'm not sure if it's right or not.

    _appLoopSub = updStream
        .handleError((e, st) => program.onError(st, e))

E/flutter ( 541): [ERROR:flutter/shell/common/shell.cc(186)] Dart Error: Unhandled exception:
E/flutter ( 541): type 'NoSuchMethodError' is not a subtype of type 'Exception'
E/flutter ( 541): #0 _DrateaProgramState.initState.
../…/src/widget.dart:68
E/flutter ( 541): #1 _invokeErrorHandler (dart:async/async_error.dart:14:37)
E/flutter ( 541): #2 _HandleErrorStream._handleError (dart:async/stream_pipe.dart:286:9)
E/flutter ( 541): #3 _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:168:13)
E/flutter ( 541): #4 _RootZone.runBinaryGuarded (dart:async/zone.dart:1326:10)
E/flutter ( 541): #5 _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:355:15)
E/flutter ( 541): #6 _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:373:16)
E/flutter ( 541): #7 _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:272:7)
E/flutter ( 541): #8 _SyncStreamController._sendError (dart:async/stream_controller.dart:768:19)
E/flutter ( 541): #9 _StreamController._addError (dart:async/stream_controller.dart:648:7)
E/flutter ( 541): #10 _StreamController.addError (dart:async/stream_controller.dart:600:5)
E/flutter ( 541): #11 _RootZone.runBinaryGuarded (dart:async/zone.dart:1326:10)
E/flutter ( 541): #12 _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:355:15)
E/flutter ( 541): #13 _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:373:16)
E/flutter ( 541): #14 _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:272:7)
E/flutter ( 541): #15 _ForwardingStreamSubscription._addError (dart:async/stream_pipe.dart:137:11)
E/flutter ( 541): #16 _ForwardingStream._handleError (dart:async/stream_pipe.dart:102:10)
E/flutter ( 541): #17 _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:168:13)
E/flutter ( 541): #18 _RootZone.runBinaryGuarded (dart:async/zone.dart:1326:10)
E/flutter ( 541): #19 _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:355:15)
E/flutter ( 541): #20 _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:373:16)
E/flutter ( 541): #21 _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:272:7)
E/flutter ( 541): #22 _ForwardingStreamSubscription._addError (dart:async/stream_pipe.dart:137:11)
E/flutter ( 541): #23 _addErrorWithReplacement (dart:async/stream_pipe.dart:188:8)
E/flutter ( 541): #24 _MapStream._handleData (dart:async/stream_pipe.dart:229:7)
E/flutter ( 541): #25 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:164:13)
E/flutter ( 541): #26 _RootZone.runUnaryGuarded (dart:async/zone.dart:1314:10)
E/flutter ( 541): #27 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)
E/flutter ( 541): #28 _DelayedData.perform (dart:async/stream_impl.dart:591:14)
E/flutter ( 541): #29 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:707:11)
E/flutter ( 541): #30 _PendingEvents.schedule. (dart:async/stream_impl.dart:667:7)
E/flutter ( 541): #31 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
E/flutter ( 541): #32 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)

Here are the relevant parts of my code.

class FetchHttp implements Message {}
...

else if (msg is FetchHttp) {
    final fetchCmd = Cmd.ofAsyncFunc(() => httpFunc("query"),
        onSuccess: (res) => HandleHttpResponse(res),
        onError: (_) => HandleHttpResponse(null));

    return Upd(model.copyWith(todo: model.todo), effects: fetchCmd);
  }

....

Future<String> httpFunc(String query) async {
  final response = await _client.post(_searchUrl(query));
  if (response.statusCode == 200) {
    return response.body;
  }
  throw Exception('failed to load todos');
}

....

DarteaMessageBus not found

With the following dependency setting in pubspec.yaml, Dart complains DarteaMessageBus isn't defined. I tried modifying the yaml file to reference the github repo (git:), and also tried putting a copy of the repo in the root folder (path: ../dartea) of my project to no avail.

dartea: "^0.6.1"

What could I be missing?

AppLifeCycleState, no constant named suspending

There's no constant named 'suspending' in 'AppLifecycleState'.
Try correcting the name to the name of an existing constant, or defining a constant named 'suspending'.

counter example main.dart
line 116

///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:
    //case AppLifecycleState.detached:
      return Upd(model.copyWith(autoIncrement: false));
    case AppLifecycleState.resumed:
    default:
      return Upd(model);
  }
}

I'm going to try replacing suspending with detached but please let me know what you think.

How to get Scaffold context inside of _view?

Hello, i have no idea how to catch Scaffold from context in this situation:

Widget _view(
    BuildContext ctx, Dispatch<CaseEdMsg> dispatch, CaseEdModel model) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Editor'),
    ),
    body: Container(
      padding: EdgeInsets.all(8),
      child: SomeButton(
          onPressed: () {
               Scaffold.of(ctx).showSnackBar(); /// Here
          },
);

Where can i intercept Scaffold?

May I use a single big model object instead of many models?

Could a ProgramWidget bind two or more models?
My view may corresponds to many models, for example, teacher's model, student's model,

If only one model could be bound to a ProgramWidget, although we can send msg(by msgsToBus) to each other, but how can we access other model's fields?

I saw in the examples models and views are one-to-one, if we want to share business code with web code(for example AngularDart), and web client's view is different from mobile client's view(for example search view and detail view are merged into one page), reuse model code will be somehow cumbersome(although possible).

So, IMHO, another way to go would be using only one big model, and bind that model to a root widget(MaterialApp ? ), so that we could access model's fields and dispatch messages from everywhere.

What if one need pass some parameters to _init..?

We have

    return ProgramWidget(
      init: _init,
      update: (msg, model) => _update(msg, model),
      view: _view,
      withMessagesBus: true,
    );

where init has signature without any custom parameters.
What if one need pass some?

copyWith

Have you figured out a way to generate copyWith so it does not have to be created manually by hand? built_value offer this solution but it's pretty ugly to use.

How can i effective forward message to another model?

Wolud you be so kind to give me receipt how can i do next:
When DataModel finishes some work i'd like to instantiate and send Message from another one, i.e. HistoryCreateMsg from HistoryModel.

So which the right approach?

DataModel
-> DataCreateMsg
–> HistoryCreateMsg 

HistoryModel
* HistoryCreateMsg

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.