Mustang

一个用于构建 Flutter 应用程序的框架。开箱即用,提供以下功能。

  • 状态管理
  • 持久化
  • 缓存
  • 文件布局和命名标准
  • 通过 open_mustang_cli 减少样板代码

框架组件

  • Screen – Screen 是一个可复用的 widget。它通常代表应用中的一个屏幕或浏览器中的一个页面。

  • Model – 一个代表应用程序数据的 Dart 类。

  • State – 提供对 Screen 所需的 Model 子集的访问。它是一个具有一个或多个 Model 字段的 Dart 类。

  • Service – 一个用于异步通信和业务逻辑的 Dart 类。

组件通信

  • 每个 Screen 都有一个对应的 Service 和一个 State。这三个组件协同工作,在应用程序状态发生变化时持续
    重建 UI。

    Architecture

    1. Screen 在构建 UI 时读取 State
    2. Screen 响应用户事件(如 滚动点击 等)调用 Service 中的方法
    3. Service
      • 读取/更新 WrenchStore 中的 Model
      • 根据需要进行 API 调用
      • 如果 WrenchStore 被修改,则通知 State
    4. State 通知 Screen 重建 UI
    5. 返回步骤 1

持久化

Persistence

默认情况下,app stateWrenchStore 在内存中维护。当应用程序终止时,app state 会永久
丢失。但是,在某些情况下,需要持久化和恢复 app state。例如:

  • 保存并恢复用户的会话令牌,以防止用户每次都必须登录
  • 保存并恢复屏幕中的部分更改,以便可以从用户离开的地方恢复工作。

启用持久化很简单,并且可以透明地工作。

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

通过以上更改,app state (WrenchStore) 将持久化到磁盘,并在应用程序启动时恢复到 WrenchStore 中。

缓存

Cache

Cache 功能允许在需要时在相同类型的实例之间切换。

Persistence 是内存中 app state (WrenchStore) 的快照。但是,有时需要持久化数据
但仅在需要时才恢复。一个例子是技术人员同时处理多个工作,即技术人员在工作之间切换。
由于 WrenchStore 只允许一种类型的实例,因此 WrenchStore 中不能有两个 Job 对象实例。

Service 中可用的 Cache API 可以轻松地将任何实例恢复到内存 (WrenchStore)。

  • Future<void> addObjectToCache<T>(String key, T t)
    

    将类型 T 的实例保存在缓存中。key 是一个或多个缓存对象的标识符。

  • Future<void> deleteObjectsFromCache(String key)
    

    删除缓存中所有带有标识符 key 的缓存对象

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

    将缓存中由 key 标识的所有对象恢复到内存 WrenchStore 和持久化存储中
    以便内存和持久化的应用程序状态保持一致。

  • bool itemExistsInCache(String key)
    

    如果缓存中存在标识符 key,则返回 true,否则返回 false

Model

  • 一个用 appModel 注释的类

  • Model 名称应以 $ 开头

  • 使用 InitField 注释初始化字段

  • 方法/Getter/Setter 不支持在 Model 类中

  • 如果一个字段在 Model 被持久化时应该被排除,则用 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

  • 一个用 screenState 注释的类

  • State 名称应以 $ 开头

  • 类的字段必须是 Model

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

Service

  • 一个用 ScreenService 注释的类

  • State 类作为参数提供给 ScreenService 注释,以在 StateService 之间创建关联,如下所示。

    @ScreenService(screenState: $ExampleScreenState)
    class ExampleScreenService {
      void getUser() {
        User user = WrenchStore.get<User>() ?? User();
          updateState1(user);
        }
    }
  • Service 还提供以下 API

    • updateState – 更新屏幕状态和/或重新构建屏幕。要更新 State 而不重新构建屏幕,请将 reload 参数设置为 false

      • 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 – 仅调用一次作为参数传递的任何方法。

      • T memoizeScreen<T>(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 – 清除 memoizeScreen 方法缓存的值。

      • void clearMemoizedScreen()

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

Screen

  • 使用 StateProvider widget 在 State 发生变化时自动重新构建 Screen

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

文件夹结构

  • 使用此框架创建的 Flutter 应用程序的文件夹结构如下所示:

      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
    
  • 每个 Screen 都需要一个 State 和一个 Service。因此,Screen、State、Service 文件被分组在一个目录中。
  • 所有 Model 类都必须放在 models 目录中。

快速入门

  • 安装 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
  • 安装 Mustang CLI

      dart pub global activate open_mustang_cli
  • 创建 Flutter 项目

      cd /tmp
      
      flutter create quick_start
      
      cd quick_start
      
      # Open the project in editor of your choice
      # vscode - code .
      # IntelliJ - idea .
  • 更新 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    
  • 安装依赖项

      flutter pub get
  • 为名为 counter 的屏幕生成文件。以下命令将创建代表 Model 的文件,以及代表 ScreenServiceState 的文件。

      omcli -s counter
  • 生成运行时文件并监视更改。

      omcli -w # omcli -b generates runtime files once
  • 更新生成的 counter.dart 模型

      class $Counter {
        @InitField(0)
        late int value;
      }
  • 更新生成的 counter_screen.dart 屏幕

      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'),
                  ),
                ],
              ),
            ),
          );
        }
      }
  • 更新生成的 counter_service.dart 服务

      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);
        }
      }
  • 更新 main.dart

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

GitHub

查看 Github