{"id":32269569,"url":"https://github.com/p69/dartea","last_synced_at":"2026-02-23T14:32:59.336Z","repository":{"id":31684929,"uuid":"128607431","full_name":"p69/dartea","owner":"p69","description":"The Elm Architecture (TEA) for Flutter","archived":false,"fork":false,"pushed_at":"2022-12-15T23:05:14.000Z","size":2881,"stargazers_count":132,"open_issues_count":6,"forks_count":15,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-10-22T22:35:17.826Z","etag":null,"topics":["architecture","flutter","mvu","redux","tea","unidirectional-data-flow","unidirectional-dataflow"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/p69.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-08T06:37:13.000Z","updated_at":"2025-04-18T21:53:17.000Z","dependencies_parsed_at":"2023-01-14T19:33:47.269Z","dependency_job_id":null,"html_url":"https://github.com/p69/dartea","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/p69/dartea","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p69%2Fdartea","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p69%2Fdartea/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p69%2Fdartea/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p69%2Fdartea/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/p69","download_url":"https://codeload.github.com/p69/dartea/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p69%2Fdartea/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29745592,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-23T07:44:07.782Z","status":"ssl_error","status_checked_at":"2026-02-23T07:44:07.432Z","response_time":90,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","flutter","mvu","redux","tea","unidirectional-data-flow","unidirectional-dataflow"],"created_at":"2025-10-22T22:27:24.126Z","updated_at":"2026-02-23T14:32:59.330Z","avatar_url":"https://github.com/p69.png","language":"Dart","funding_links":[],"categories":["Frameworks"],"sub_categories":["Redux / ELM / Dependency Injection"],"readme":"# Dartea\n[![Build Status](https://travis-ci.org/p69/dartea.svg?branch=master)](https://travis-ci.org/p69/dartea)  [![codecov](https://codecov.io/gh/p69/dartea/branch/master/graph/badge.svg)](https://codecov.io/gh/p69/dartea)\n\nImplementation of MVU (Model View Update) pattern for Flutter.\nInspired by [TEA (The Elm Architecture)](https://guide.elm-lang.org/architecture/) and [Elmish (F# TEA implemetation)](https://github.com/elmish/elmish)\n\n![dartea img](dartea_mvu_big_picture.png)\n\n## Key concepts\nThis app architecture is based on three key things:\n1. Model (App state) must be immutable.\n2. View and Update functions must be pure.\n3. All side-effects should be separated from the UI logic.\n\nThe 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](https://en.wikipedia.org/wiki/Pure_function). All the side-effects (such as database queries, http requests and etc) must be isolated using `Commands` and `Subscriptions`.\n\n## Simple counter example\n\n### Model and Message:\n```dart\nclass Model {\n  final int counter;\n  Model(this.counter);\n}\n\nabstract class Message {}\nclass Increment implements Message {}\nclass Decrement implements Message {}\n```\n\n### View:\n```dart\nWidget view(BuildContext context, Dispatch\u003cMessage\u003e dispatch, Model model) {\n  return Scaffold(\n    appBar: AppBar(\n      title: Text('Simple dartea counter'),\n    ),\n    body: Center(\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        crossAxisAlignment: CrossAxisAlignment.center,\n        children: \u003cWidget\u003e[\n          Text(\n            '${model.counter}',\n            style: Theme.of(context).textTheme.display1,\n          ),\n          Padding(\n            child: RaisedButton.icon(\n              label: Text('Increment'),\n              icon: Icon(Icons.add),\n              onPressed:() =\u003e dispatch(Increment()),\n            ),\n            padding: EdgeInsets.all(5.0),\n          ),\n          RaisedButton.icon(\n            label: Text('Decrement'),\n            icon: Icon(Icons.remove),\n            onPressed:  () =\u003e dispatch(Decrement()),\n          ),\n        ],\n      ),\n    ),\n  );\n}\n```\n\n### Update:\n```dart\nUpd\u003cModel, Message\u003e update(Message msg, Model model) {\n  if (msg is Increment) {\n    return Upd(Model(model.counter + 1));\n  }\n  if (msg is Decrement) {\n    return Upd(Model(model.counter - 1));\n  }\n  return Upd(model);\n}\n```\n\n### Update with side-effects:\n```dart\nUpd\u003cModel, Message\u003e update(Message msg, Model model) {  \n  final persistCounterCmd = Cmd.ofAsyncAction(()=\u003eStorage.save(model.counter));\n  if (msg is Increment) {    \n    return Upd(Model(model.counter + 1), effects: persistCounterCmd);\n  }\n  if (msg is Decrement) {\n    return Upd(Model(model.counter - 1), effects: persistCounterCmd);\n  }\n  return Upd(model);\n}\n```\n\n### Create program and run Flutter app\n```dart\nvoid main() {\n  final program = Program(\n      () =\u003e Model(0), //create initial state\n      update,\n      view);\n  runApp(MyApp(program));\n}\n\nclass MyApp extends StatelessWidget {\n  final Program darteaProgram;\n\n  MyApp(this.darteaProgram);\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Dartea counter example',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: darteaProgram.build(key: Key('root_key')),\n    );\n  }\n}\n```\n\nAnd that's it.\n\n### External world and subscriptions\n`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:\n```dart\nTSubHolder Function(TSubHolder currentSub, Dispatch\u003cTMsg\u003e dispatch, TModel model);\n```\nThis function is called from `Dartea` engine right after every `model's` update. Here is an example of `Timer` subscription from the counter example.\n```dart\nconst _timeout = const Duration(seconds: 1);\nTimer _periodicTimerSubscription(\n    Timer currentTimer, Dispatch\u003cMessage\u003e dispatch, Model model) {\n  if (model == null) {\n    currentTimer?.cancel();\n    return null;\n  }\n  if (model.autoIncrement) {\n    if (currentTimer == null) {\n      return Timer.periodic(_timeout, (_) =\u003e dispatch(Increment()));\n    }\n    return currentTimer;\n  }\n  currentTimer?.cancel();\n  return null;\n}\n```\nThere 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).\nWhen `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.\n\nFull counter example is [here](/examples/counter/)\n\n## Scaling and composition\nFirst 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.\n\n### Traditional Elm composition\n\n![Traditional MVU fractals](traditional_elm_fratctals.png)\n\nYou can see how our application tree could look like. The relations are explicit and very strict.\nWe can describe it in code something like this:\n```dart\n\nclass BlueModel {\n  final Object stateField;\n  final YellowModel yellow;\n  final GreenModel green;\n  //constructor, copyWith...\n}\n\nabstract class BlueMsg {}\nclass UpdateFieldBlueMsg implements BlueMsg {\n  final Object newField;\n}\nclass YellowModelBlueMsg implements BlueMsg {\n  final YellowMsg innerMsg;\n}\nclass GreenModelBlueMsg implements BlueMsg {\n  final GreenMsg innerMsg;\n}\n\nUpd\u003cBlueModel, BlueMsg\u003e updateBlue(BlueMsg msg, BlueModel model) {\n  if (msg is UpdateFieldBlueMsg) {\n    return Upd(model.copyWith(stateField: msg.newField));\n  }\n  if (msg is YellowModelBlueMsg) {\n    //update yellow sub-model\n    final yellowUpd = yellowUpdate(msg.innerMsg, model.yellow);\n    return Upd(model.copyWith(yellow: yellowUpd.model));\n  }\n  //the same for green model\n}\n\nWidget viewBlue(BuildContext ctx, Disptach\u003cBlueMsg\u003e dispatch, BlueModel model) {\n  return Column(\n    children: [\n      viewField(model.stateField),\n      //yellow sub-view\n      viewYellow((m)=\u003edispatch(YellowModelBlueMsg(innerMsg: m)), model.yellow),\n      //green sub-view\n      viewGreen((m)=\u003edispatch(GreenModelBlueMsg(innerMsg: m)), model.green),\n    ],\n  );\n}\n\n//The same for all other components\n```\nAs 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. \nI 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).\n\n### Alternative composition\nIn 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.\n```dart\nvoid main() =\u003e runApp(MyApp());\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return DarteaMessagesBus(\n      child: MaterialApp(        \n        home: HomeWidget(),\n      ),\n    );\n  }\n}\nclass HomeWidget extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return ProgramWidget(\n      key: DarteaStorageKey('home'),\n      init: _init,\n      update: _update,\n      view: _view,\n      withDebugTrace: true,\n      withMessagesBus: true,\n    );\n  }\n}\n```\nFirst, 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.\n```dart\n//from update function\nUpd\u003cModel, Msg\u003e update(Msg msg, Model model) {\n  return Upd(model, msgsToBus: [AnotherModelMsg1(), AnotherModelMsg2()]);\n}\n//from view function\nWidget view(BuildContext ctx, Dispatch\u003cMsg\u003e dispatch, Model model) {\n  return RaisedButton(\n    //...\n    onPressed:(){\n      final busDispatch = DarteaMessagesBus.dispatchOf(ctx);\n      busDispatch?(AnotherModelMsg3());\n    },\n    //..\n  );\n}\n```\nAnd if there are any components which set `withMessagesBus` to `true` and can handle `AnotherModelMsg1`, `AnotherModelMsg2` or `AnotherModelMsg3`, then they receive all those messages.\n\nSecond, 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`.\n```dart\nWidget _view(BuildContext ctx, Dispatch\u003cHomeMsg\u003e dispatch, HomeModel model) {\n  return Scaffold(\n    //...\n    body: model.selectedTab == Tab.trending\n        ? ProgramWidget(\n            key: DarteaStorageKey('trending_tab_program'),\n            //init, update, view\n          )\n    )\n        : ProgramWidget(\n            key: DarteaStorageKey('search_tab_program'),\n            //init, update, view\n          )\n    ),\n    bottomNavigationBar: _bottomNavigation(ctx, dispatch, model.selectedTab),\n    //...\n  );\n}\n```\nHere 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.\nSee full example of this approach in [GitHub client example](/examples/github_client/)\nUsing 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. \n\n## Sample apps\n* [Calculator](https://github.com/p69/dartea_calculator)\n* [Todo](https://github.com/brianegan/flutter_architecture_samples/tree/master/example/mvu)\n* [GitHub client](examples/github_client/)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fp69%2Fdartea","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fp69%2Fdartea","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fp69%2Fdartea/lists"}