The Elm Architecture (TEA) for Flutter

Overview

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

Comments
  • TextFields are getting rebuild even when I use GlobalKeys.

    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.

    opened by cab404 9
  • How to force to update widgets upon certain circumstances?

    How to force to update widgets upon certain circumstances?

    We have DarteaStorageKey for storing state. But periodically one need to force to update widgets upon some circumstances. Are there recipes or how-to for this cases?

    opened by ValeriusGC 4
  • Wrong information in readme

    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.

    opened by rrousselGit 4
  • type 'NoSuchMethodError' is not a subtype of type 'Exception'

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

    ....

    opened by puruzio 3
  • AppLifeCycleState, no constant named suspending

    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.

    opened by nyck33 2
  • How can i effective forward message to another model?

    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
    
    opened by ValeriusGC 1
  • How to get Scaffold context inside of _view?

    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?

    opened by ValeriusGC 1
  • DarteaMessageBus not found

    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?

    opened by puruzio 1
  • What if one need pass some parameters to _init..?

    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?

    opened by ValeriusGC 0
  • May I use a single big model object instead of many models?

    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.

    opened by suexcxine 2
  • Could this package be used with AnguarDart?

    Could this package be used with AnguarDart?

    just like this repo: https://github.com/felangel/Bloc

    It separates to three packages: bloc, flutter_bloc and angular_bloc,

    Hopefully there would also be examples for AnguarDart usage with dartea !

    opened by suexcxine 3
  • How to use Animations?  and some questions

    How to use Animations? and some questions

    The mvu model is good, easy understand and easy use, but examples or demo is too little. Animation need a class extend State class and with XXXMixin class, so, How to use Animations with dartea? We needs many many many tutorial, Please tell me how to do? thank you. sorry, I'm a Chinese, so English is bad..

    opened by felixyin 1
  • copyWith

    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.

    opened by dodyg 2
  • Awesome

    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.

    opened by ghost 4
Owner
Pavel Shilyagov
Mobile developer: native (Kotlin, Swift), cross-platform (Dart, C#\F#)
Pavel Shilyagov
Bhargav Reddy 10 Nov 12, 2022
Flutter Architecture Blueprints is a project that introduces MVVM architecture and project structure approaches to developing Flutter apps.

Flutter Architecture Blueprints Flutter Architecture Blueprints is a project that introduces MVVM architecture and project structure approaches to dev

Katsuyuki Mori 2 Apr 9, 2022
Flutter Architecture Blueprints is a project that introduces MVVM architecture and project structure approaches to developing Flutter apps.

Flutter Architecture Blueprints Flutter Architecture Blueprints is a project that introduces MVVM architecture and project structure approaches to dev

Daichi Furiya 1.5k Dec 31, 2022
Ouday 25 Dec 15, 2022
Flutter-clean-architecture - A simple flutter project developed with TDD and using Clean Architecture principles.

Clean Architecture This is a study project to practice TDD and a good approach of Clean Architecture for flutter projects. It is based on Reso Coder s

Luiz Paulo Franz 8 Jul 21, 2022
Flutter Architecture inspired by Domain Driven Design, Onion and Clean Architecture

Inspiring Domain Driven Design Flutter Architecture Please take a look at my slides to learn more Strategic Domain Driven Design For Improving Flutter

Majid Hajian 324 Dec 25, 2022
Proyect with Clean Architecture / Hexagonal Architecture - Patron BLoC - The MovieDB API

flutter_movies_db A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you starte

null 2 Sep 22, 2022
Flutter App Templete is a project that introduces an approach to architecture and project structure for developing Flutter apps.

Flutter App Template "Flutter App Template" is a project that introduces an approach to architecture and project structure for developing Flutter apps

Altive 126 Jan 5, 2023
Flutter mvvm archi - Flutter Advanced Course - Clean Architecture With MVVM

flutter_mvvm_archi A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you start

Namesh Kushantha 0 Jan 8, 2022
Starter-Flutter - Starter flutter project for best architecture and seperation of code

Modular-Architecture Codebase CodeBase , Infrastructure and the common Layers (c

Ahmed Tawfiq 13 Feb 16, 2022
[Flutter SDK V.2] - Youtube Video is a Flutter application built to demonstrate the use of Modern development tools with best practices implementation like Clean Architecture, Modularization, Dependency Injection, BLoC, etc.

[Flutter SDK V.2] - Youtube Video is a Flutter application built to demonstrate the use of Modern development tools with best practices implementation like Clean Architecture, Modularization, Dependency Injection, BLoC, etc.

R. Rifa Fauzi Komara 17 Jan 2, 2023
🚀 Sample Flutter Clean Architecture on Rorty App focused on the scalability, testability and maintainability written in Dart, following best practices using Flutter.

Rorty Flutter Rorty ?? (work-in-progress for V2 ?? ??️ ??‍♀️ ⛏ ) Getting Started Flutter Clean Architecture in Rorty is a sample project that presents

Mr.Sanchez 138 Jan 1, 2023
COVID-19 application made with Flutter, following Test Driven Development (TDD) and Clean Architecture along with Internationalization with JSON.

Covid App COVID-19 application made with Flutter, following Test Driven Development (TDD) and Clean Architecture along with Internationalization with

Sandip Pramanik 4 Aug 4, 2022
Starting template for a new Flutter project. Using clean architecture + Riverpod.

flutter_project_template_riverpod Installation Add Flutter to your machine Open this project folder with Terminal/CMD Ensure there's no cache/build le

Bahri Rizaldi 39 Dec 27, 2022
Flutter boilerplate with TDD architecture

flutter_tdd_architecture A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you

Dao Hong Vinh 6 May 25, 2022
A beautiful design and useful project for Building a flutter knowledge architecture

Flutter Dojo Change log Flutter Dojo Change Log 我的网站 https://xuyisheng.top/ Wiki Flutter Dojo Wiki 体验APK Github Actions APK download 认识Flutter是在18年,移动

xuyisheng 1.4k Dec 21, 2022
A Flutter repo with a ready-to-go architecture containing flavors, bloc, device settings, json serialization and connectivity

Flutter Ready to Go A Flutter repo with a ready-to-go architecture containing flavors, bloc, device settings, json serialization and connectivity. Why

null 139 Nov 11, 2022
Flutter - Clean Architecture & TDD

Flutter - Clean Architecture & TDD Introducción Este proyecto consta de una aplicación móvil desarrollada en Flutter, la cual muestra información acer

Jorge Fernández 21 Oct 26, 2022
Demo project to implement MVU architecture in a Flutter application

flutter_mvu A playground project for implementing MVU in Flutter CICD Automated

Kevin Strathdee 0 Dec 16, 2021