Mustang: A framework to build Flutter applications

Overview

Mustang

A framework to build Flutter applications. Following features are available out of the box.

  • State Management
  • Persistence
  • Cache
  • File layout and naming standards
  • Reduces boilerplate with open_mustang_cli

Contents

Framework Components

  • Screen - Screen is a reusable widget. It usually represents a screen in the app or a page in Browser.

  • Model - A Dart class representing application data.

  • State - Provides access to subset of Models needed for a Screen. It is a Dart class with 1 or more Model fields.

  • Service - A Dart class for async communication and business logic.

Component Communication

  • Every Screen has a corresponding Service and a State. All three components work together to continuously rebuild the UI whenever there is a change in the application state.

    Architecture

    1. Screen reads State while building the UI
    2. Screen invokes methods in the Service as a response to user events (scroll, tap etc.,)
    3. Service
      • reads/updates Models in the WrenchStore
      • makes API calls, if needed
      • informs State if WrenchStore is mutated
    4. State informs Screen to rebuild the UI
    5. Back to Step 1

Persistence

Persistence

By default, app state is maintained in memory by WrenchStore. When the app is terminated, the app state is lost permanently. However, there are cases where it is desirable to persist and restore the app state. For example,

  • Save and restore user's session token to prevent user having to log in everytime
  • Save and restore partial changes in a screen so that the work can be resumed from where the user has left off.

Enabling persistence is simple and works transparently.

import 'package:xxx/src/models/serializers.dart' as app_serializer;

WidgetsFlutterBinding.ensureInitialized();

// In main.dart before calling runApp method,
// 1. Enable persistence like below
WrenchStore.config(
  isPersistent: true,
  storeName: 'myapp',
);

// 2. Initialize persistence
Directory dir = await getApplicationDocumentsDirectory();
await WrenchStore.initPersistence(dir.path);

// 3. Restore persisted state before the app starts
await WrenchStore.restoreState(app_serializer.json2Type, app_serializer.serializerNames);

With the above change, app state (WrenchStore) is persisted to the disk and will be restored into WrenchStore when the app is started.

Cache

Cache

Cache feature allows switching between instances of the same type on need basis.

Persistence is a snapshot of the app state in memory (WrenchStore). However, there are times when data need to be persisted but restored only when needed. An example would be a technician working on multiple jobs at the same time i.e, technician switches between jobs. Since the WrenchStore allows only one instance of a type, there cannot be two instances of the Job object in the WrenchStore.

Cache APIs, available in Services, make it easy to restore any instance into memory (WrenchStore).

  • Future
         
           addObjectToCache
          
           (String key, T t)
    
          
         

    Save an instance of type T in the cache. key is an identifier for one or more cached objects.

  • Future
         
           deleteObjectsFromCache(String key)
    
         

    Delete all cached objects having the identifier key

  • static Future
         
           restoreObjects(
        String key,
        void Function(
            void Function
          
           (T t) update,
            String modelName,
            String jsonStr,
        ) callback,
    )
    
          
         

    Restores all objects in the cache identified by the key into memory WrenchStore and also into the persisted store so that the in-memory and persisted app state remain consistent.

  • bool itemExistsInCache(String key)
    

    Returns true if an identifier key exists in the Cache, false otherwise.

Model

  • A Class annotated with appModel

  • Model name should start with $

  • Initialize fields with InitField annotation

  • Methods/Getters/Setters are NOT supported inside Model classes

  • If a field should be excluded when a Model is persisted, annotate that field with SerializeField(false)

    @appModel
    class $User {
      late String name;
    
      late int age;
    
      @InitField(false)
      late bool admin; 
    
      @InitField(['user', 'default'])
      late BuiltList<String> roles;
      
      late $Address address;  // $Address is another model annotated with @appModel
      
      late BuiltList<$Vehicle> vehicles;  // Use immutable versions of List/Map inside Model classes
      
      @SerializeField(false)
      late String errorMsg; // errorMsg field will not be included when $User model is persisted 
    }

State

  • A class annotated with screenState

  • State name should start with $

  • Fields of the class must be Model classes

    @screenState
    class $ExampleScreenState {
      late $User user;
      
      late $Vehicle vehicle;
    }

Service

  • A class annotated with ScreenService

  • Provide State class as an argument to ScreenService annotation, to create an association between State and Service as shown below.

    @ScreenService(screenState: $ExampleScreenState)
    class ExampleScreenService {
      void getUser() {
        User user = WrenchStore.get<User>() ?? User();
          updateState1(user);
        }
    }
  • Service also provides following APIs

    • updateState - Updates screen state and/or re-build the screen. To update the State without re-building the screen. Set reload argument to false to update the State without re-building the Screen.

      • updateState()
      • updateState1(T model1, { reload: true })
      • updateState2(T model1, S model2, { reload: true })
      • updateState3(T model1, S model2, U model3, { reload: true })
      • updateState4(T model1, S model2, U mode3, V model4, { reload: true })
    • memoizeScreen - Invokes any method passed as argument only once.

      • T memoizeScreen (T Function() methodName)
        // In the snippet below, getScreenData method caches the return value of getData method, a Future.
        // Even when getData method is called multiple times, method execution happens only the first time.
        Future<void> getData() async {
          Common common = WrenchStore.get<Common>() ?? Common();
          User user;
          Vehicle vehicle;
        
          ...   
        }
        
        Future<void> getScreenData() async {
          return memoize(getData);
        }
    • clearMemoizedScreen - Clears value cached by memoizeScreen method.

      • void clearMemoizedScreen()
        Future<void> getData() async {
          ...
        }
        
        Future<void> getScreenData() async {
          return memoizeScreen(getData);
        }
        
        void resetScreen() {
          // clears Future
                 
                   cached by memoizeScreen()
                 
          clearMemoizedScreen();
        }

Screen

  • Use StateProvider widget to re-build the Screen automatically when there is a change in State

    ...
    Widget build(BuildContext context) {
      return StateProvider<HomeScreenState>(
          state: HomeScreenState(),
          child: Builder(
            builder: (BuildContext context) {
              // state variable provides access to model fields declared in the HomeScreenState class
              HomeScreenState? state = StateConsumer<HomeScreenState>().of(context);
              
              # Even when this widget is built many times, only 1 API call 
              # will be made because the Future from the service is cached
              SchedulerBinding.instance?.addPostFrameCallback(
                (_) => HomeScreenService().getScreenData(),
              );
    
              if (state?.common?.busy ?? false) {
                return Spinner();
              }
    
              if (state?.counter?.errorMsg.isNotEmpty ?? false) {
                return ErrorBody(errorMsg: state.common.errorMsg);
              }
                
              return _body(state, context);
            },
          ),
        );
      }

Folder Structure

  • Folder structure of a Flutter application created with this framework looks as below
      lib/
        - main.dart
        - src
          - models/
            - model1.dart
            - model2.dart
          - screens/
            - first/
              - first_screen.dart
              - first_state.dart
              - first_service.dart
            - second/
              - second_screen.dart
              - second_state.dart
              - second_service.dart
    
  • Every Screen needs a State and a Service. So, Screen, State, Service files are grouped inside a directory
  • All Model classes must be inside models directory

Quick Start

  • Install Flutter

      mkdir -p ~/lib && cd ~/lib
      
      git clone https://github.com/flutter/flutter.git -b stable
    
      # Add PATH in ~/.zshrc 
      export PATH=$PATH:~/lib/flutter/bin
      export PATH=$PATH:~/.pub-cache/bin
  • Install Mustang CLI

      dart pub global activate open_mustang_cli
  • Create Flutter project

      cd /tmp
      
      flutter create quick_start
      
      cd quick_start
      
      # Open the project in editor of your choice
      # vscode - code .
      # IntelliJ - idea .
  • Update pubspec.yaml

      ...
      dependencies:
        ...
        built_collection: ^5.1.1
        built_value: ^8.1.3
        mustang_core: ^1.0.2
        path_provider: ^2.0.6
    
      dev_dependencies:
        ...
        build_runner: ^2.1.4
        mustang_codegen: ^1.0.3    
  • Install dependencies

      flutter pub get
  • Generate files for a screen called counter. Following command creates file representing a Model, and also files representing Screen, Service and State.

      omcli -s counter
  • Generate runtime files and watch for changes.

      omcli -w # omcli -b generates runtime files once
  • Update the generated counter.dart model

      class $Counter {
        @InitField(0)
        late int value;
      }
  • Update the generated counter_screen.dart screen

      import 'package:flutter/material.dart';
      import 'package:mustang_core/mustang_widgets.dart';
      
      import 'counter_service.dart';
      import 'counter_state.state.dart';
      
      class CounterScreen extends StatelessWidget {
        const CounterScreen({
          Key key,
        }) : super(key: key);
          
        @override
        Widget build(BuildContext context) {
          return StateProvider<CounterState>(
            state: CounterState(),
            child: Builder(
              builder: (BuildContext context) {
                CounterState? state = StateConsumer<CounterState>().of(context);
                return _body(state, context);
              },
            ),
          );
        }
      
        Widget _body(CounterState? state, BuildContext context) {
          int counter = state?.counter?.value ?? 0;
          return Scaffold(
            appBar: AppBar(
              title: Text('Counter'),
            ),
            body: Center(
              child: Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text('$counter'),
                  ),
                  ElevatedButton(
                    onPressed: CounterService().increment,
                    child: const Text('Increment'),
                  ),
                ],
              ),
            ),
          );
        }
      }
  • Update the generated counter_service.dart service

      import 'package:mustang_core/mustang_core.dart';
      import 'package:quick_start/src/models/counter.model.dart';
          
      import 'counter_service.service.dart';
      import 'counter_state.dart';
          
      @ScreenService(screenState: $CounterState)
      class CounterService {
        void increment() {
          Counter counter = WrenchStore.get<Counter>() ?? Counter();
          counter = counter.rebuild((b) => b.value = (b.value ?? 0) + 1);
          updateState1(counter);
        }
      }
  • Update main.dart

      ...
    
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            ...
            primarySwatch: Colors.blue,
          ),
          home: CounterScreen(), // Point to Counter screen
        );
      }
    
      ...  
You might also like...

Provides Dart Build System builder for creating Injection pattern using annotations.

Provides Dart Build System builder for creating Injection pattern using annotations. Gate generator The core package providing generators using annoat

Dec 20, 2022

A Dart build script that downloads the Protobuf compiler and Dart plugin to streamline .proto to .dart compilation.

A Dart build script that downloads the Protobuf compiler and Dart plugin to streamline .proto to .dart compilation.

Oct 26, 2022

Parser builder - Lightweight template-based parser build system. Simple prototyping. Comfortable debugging. Effective developing

parser_builder Lightweight template-based parser build system. Simple prototypin

Dec 1, 2022

Flutter Version Management: A simple CLI to manage Flutter SDK versions.

Flutter Version Management: A simple CLI to manage Flutter SDK versions.

fvm Flutter Version Management: A simple cli to manage Flutter SDK versions. FVM helps with the need for a consistent app builds by allowing to refere

Jan 8, 2023

A flutter utility to easily create flavors in your flutter application

Flutter Flavorizr A flutter utility to easily create flavors in your flutter application Getting Started Let's start by setting up our environment in

Jan 1, 2023

🔥FlutterFire is a set of Flutter plugins that enable Flutter apps to use Firebase services.

🔥FlutterFire is a set of Flutter plugins that enable Flutter apps to use Firebase services.

FlutterFire is a set of Flutter plugins that enable Flutter apps to use Firebase services. You can follow an example that shows how to use these plugins in the Firebase for Flutter codelab.

Jan 2, 2023

Ecosistema de paquetes para Dart y Flutter desarrollados y mantenidos por la comunidad de Flutter Cuba relacionados con la tienda cubana de aplicaciones para dispositivos Android llamada Apklis.

Ecosistema de paquetes para Dart y Flutter desarrollados y mantenidos por la comunidad de Flutter Cuba relacionados con la tienda cubana de aplicaciones para dispositivos Android llamada Apklis.

Ecosistema de paquetes para Dart y Flutter desarrollados y mantenidos por la comunidad de Flutter Cuba relacionados con la tienda cubana de aplicacion

Oct 24, 2022

UME is an in-app debug kits platform for Flutter. Produced by Flutter Infra team of ByteDance

UME is an in-app debug kits platform for Flutter. Produced by Flutter Infra team of ByteDance

flutter_ume English Flutter 应用内调试工具平台 当前版本内置 10 个插件, 开发者可以创建自己的插件,并集成进 UME 平台。 详见本文为 UME 开发插件部分。 flutter_ume 快速接入 特别说明 功能介绍 为 UME 开发插件 版本说明 兼容性 单测覆盖率

Dec 30, 2022
Comments
  • Dynamic aspect generation

    Dynamic aspect generation

    Changelog:

    • support for following annotations aspect, Before, After, Around, OnException

    Example:

     @Before([firebaseAspect])
      @Around(aroundAspect)
      @After([afterAspect])
      Future<void> getData({
        bool showBusy = true,
      }) async {
       ...
    }
    

    Generated File

     @override
      Future<void> getData({bool showBusy = true}) async {
        await $$FirebaseAspect().invoke();
    
        await $$AroundAspect().invoke(() async {
          await super.getData(
            showBusy: showBusy,
          );
        });
        await $$AfterAspect().invoke();
      }
    
    opened by amit-jha-cmd 0
  • Offline Prefetch And Config

    Offline Prefetch And Config

    Add screens to prefetch in mustang.yaml at the root of your project

    serializer: package:wrench_flutter_common/flutter_common.dart
    screen:
      imports:
        - package:wrench_widgets/widgets.dart
      progress_widget: WrenchProgressIndicatorScreen()
      error_widget: WrenchErrorWithDescriptionScreen()
    prefetch:
      screens:
        - job_details_screen
        - verify_update_vehicle_screen
        - checklist_screen
        - add_photos_screen
    

    Generated Offline Service

    import 'package:app2/src/screens/route_screens/job_details/job_details_service.service.dart';
    import 'package:app2/src/screens/route_screens/verify_update_vehicle/verify_update_vehicle_service.service.dart';
    import 'package:app2/src/screens/route_screens/checklist/checklist_service.service.dart';
    import 'package:app2/src/screens/route_screens/add_photos/add_photos_service.service.dart';
    
    class OfflineService {
      static Future<void> preFetch() async {
        await JobDetailsService().memoizedGetData();
        await VerifyUpdateVehicleService().memoizedGetData();
        await ChecklistService().memoizedGetData();
        await AddPhotosService().memoizedGetData();
      }
    }
        
    
    opened by amit-jha-cmd 0
Owner
null
An extension to the Flutter SDK for building Flutter applications for Tizen devices.

Flutter for Tizen An extension to the Flutter SDK for building Flutter applications for Tizen devices. Flutter and the related logo are trademarks of

null 356 Dec 16, 2022
Environment specific config generator for Dart and Flutter applications during CI/CD builds

Environment Config Generator Environment specific config generator. Allows to specify env configuration during CI/CD build. Primarily created to simpl

Denis Beketsky 86 Dec 2, 2022
A cli tool to run Flutter applications and auto hot reload it when files are changed

Dashmon A minimalistic CLI tool to run Flutter applications and auto hot reload it when files are changed. It will watch changes your application code

Erick 31 Oct 6, 2022
A flutter package with classes to help testing applications using the canvas

Canvas test helpers MockCanvas is a utility class for writing tests for canvas operations. It supports the same API as the regular Canvas class from d

Blue Fire 12 Jan 31, 2022
A simple command-line application to generate simple folder and file structure for Flutter Applications

Kanza_cli is a simple command line tool to generate folder and file structure for your Flutter apps. To use it, you should do the followings: 1. First

Kanan Yusubov 9 Dec 16, 2022
Auto is a Flutter automated testing framework developed for testers.

Auto Auto-A simpler Flutter UI automation test solution. No need to write any code Recording test scripts is very simple Mult

null 19 Oct 12, 2022
An extensible flutter-framework

dart_board An extensible flutter-framework Dart Board allows you to break your app into features and then integration cleanly and consistently. It's e

Adam Hammer 56 Dec 19, 2022
Rocket is a parsing framework for parsing binary data structures using efficient parsing algorithms

rocket Version 0.1.10 (BETA) Rocket is a parsing framework for parsing binary data structures using efficient parsing algorithms. Breaking change: The

null 5 Dec 5, 2021
FaaS (Function as a service) framework for writing portable Dart functions

Functions Framework for Dart This is a community-supported project, meaning there is no official level of support. The code is not covered by any SLA

Google Cloud Platform 485 Dec 26, 2022
Official CLI for the GetX framework

Official CLI for the GetX framework

Shahanul Haque Shawon 0 Nov 23, 2021