mvc_pattern

codecov CI Medium Pub.dev GitHub stars Last Commit flutter and mvc 该框架包 mvc_application 以 mvc_pattern 为核心。 StateX 该包 mvc_application 允许更轻松、我敢说,更快速的开发和更好的可维护性。无需“重新发明轮子”,已有内置功能。方便且集成功能

  • 错误处理
  • 系统偏好设置
  • 通知
  • 日期选择器
  • 应用颜色选择器
  • 对话框
  • 可自定义底部栏
  • 加载屏幕
  • 时区
  • 本地化

该包 mvc_web 使用 mvc_application,但用于 Web。

Flutter 框架的“吻”

遵循“KISS 原则”,此举旨在以内在的方式将 MVC 设计模式提供给 Flutter,并整合了许多 Flutter 框架本身。所有内容都包含在一个独立的 Flutter 包中。

事实上,这一切的出现仅仅是因为我想要一个地方来存放我的“可变”代码(应用程序的业务逻辑),而不会引起编译器抱怨!将此类代码放入 StatefulWidget 或 StatelessWidget 中当然是不推荐的——这些对象中只应包含不可变代码。当然,所有这些代码都可以放在 State 对象中。这很好,因为您无论如何都希望访问 State 对象。毕竟,它是 Flutter 中“状态管理”的主要参与者。但是,这会导致 State 对象变得相当大且混乱!

将代码放在单独的 Dart 文件中将是解决方案,但那样的话,就需要一种方法来访问那个至关重要的 State 对象。我希望单独的 Dart 文件拥有 State 对象的所有功能和能力。换句话说,那个单独的 Dart 文件需要访问 State 对象!

现在,我无意重新发明轮子。我想保持一切都是 Flutter,所以我停下来仔细研究 Flutter,看看如何将其应用于一些已知的ear design pattern。那时我看到了 State 对象(特别是它的 build() 函数)作为“视图”,而具有对该 State 对象访问权限的单独 Dart 文件作为“控制器”。

这个包本质上就是结果,它只涉及两个“新”类:StateX 和 StateXController。StateX 对象是一个具有显式生命周期的 State 对象(Android 开发者会欣赏这一点),而 StateXController 对象可以成为那个具有 State 对象(在此情况下为 StateX)访问权限的单独 Dart 文件。所有这些都使用 Flutter 对象和库完成——这里没有重新发明。它看起来和尝起来都像 Flutter。

事实上,它恰好是以设计模式的“祖父”MVC命名的,但它实际上更像是PAC设计模式。实际上,您可以使用任何其他架构。通过设计,您可以直接使用类 StateX 和 StateXController。喂!您也可以将扩展 StateXController 的对象称为 BLoC。再说一遍,我想要的只是某种方式将 State 对象绑定到包含应用程序“核心”的单独 Dart 文件。我认为您会发现它很有用。

安装

我并不总是喜欢“安装”页面中建议的版本号。相反,在安装我的库包时,请始终升级到“主要”语义版本号。这意味着始终输入一个以两个零结尾的版本号“.0.0”。这允许您采用引入新功能的任何“次要”版本以及涉及错误修复的任何“补丁”版本。语义版本号始终采用以下格式:major.minor.patch

  1. 补丁——我已修复错误
  2. 次要——我已引入新功能
  3. 主要——我基本上创建了一个新应用程序。它破坏了向后兼容性,并具有全新的用户体验。您要到修改 pubspec.yaml 文件中的主要编号后才能获得此版本。

因此,在这种情况下,请将此添加到您的包的 pubspec.yaml 文件中

dependencies:
  mvc_pattern:^8.9.0

文档

请参阅这篇免费的 Medium 文章,了解该包的完整概述及示例:FlutterFramework

示例代码

复制并粘贴下面的代码即可开始。检查每个代码序列开头指定的路径,以确定这些文件应位于何处。

/// example/lib/main.dart

import 'package:example/src/view.dart';

void main() => runApp(MyApp(key: const Key('MyApp')));

/// example/src/app/view/my_app.dart

import 'package:example/src/view.dart';

import 'package:example/src/controller.dart';

class MyApp extends AppStatefulWidgetMVC {
  const MyApp({Key? key}) : super(key: key);

  /// This is the App's State object
  @override
  AppStateMVC createState() => _MyAppState();
}

class _MyAppState extends AppStateMVC<MyApp> {
  factory _MyAppState() => _this ??= _MyAppState._();
  static _MyAppState? _this;

  @override
  Widget buildApp(BuildContext context) => MaterialApp(
        home: FutureBuilder<bool>(
            future: initAsync(),
            builder: (context, snapshot) {
              //
              if (snapshot.hasData) {
                //
                if (snapshot.data!) {
                  /// Key identifies the widget. New key? New widget!
                  /// Demonstrates how to explicitly 're-create' a State object
                  return MyHomePage(key: UniqueKey());
                } else {
                  //
                  return const Text('Failed to startup');
                }
              } else if (snapshot.hasError) {
                //
                return Text('${snapshot.error}');
              }
              // By default, show a loading spinner.
              return const Center(child: CircularProgressIndicator());
            }),
      );
}

/// example/src/app/controller/app_controller.dart

import 'package:example/src/view.dart';

class AppController extends StateXController with AppControllerMVC {
  factory AppController() => _this ??= AppController._();
  AppController._();
  static AppController? _this;

  /// Initialize any 'time-consuming' operations at the beginning.
  /// Initialize asynchronous items essential to the Mobile Applications.
  /// Typically called within a FutureBuilder() widget.
  @override
  Future<bool> initAsync() async {
    // Simply wait for 10 seconds at startup.
    /// In production, this is where databases are opened, logins attempted, etc.
    return Future.delayed(const Duration(seconds: 10), () {
      return true;
    });
  }

  /// Supply an 'error handler' routine if something goes wrong
  /// in the corresponding initAsync() routine.
  /// Returns true if the error was properly handled.
  @override
  bool onAsyncError(FlutterErrorDetails details) {
    return false;
  }
}

/// example/src/home/view/my_home_page.dart

import 'package:example/src/view.dart';

import 'package:example/src/controller.dart';

/// The Home page
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, this.title = 'Flutter Demo'}) : super(key: key);

  // Fields in a StatefulWidget should always be "final".
  final String title;

  @override
  State createState() => _MyHomePageState();
}

/// This 'MVC version' is a subclass of the State class.
/// This version is linked to the App's lifecycle using [WidgetsBindingObserver]
class _MyHomePageState extends StateX<MyHomePage> {
  /// Let the 'business logic' run in a Controller
  _MyHomePageState() : super(Controller()) {
    /// Acquire a reference to the passed Controller.
    con = controller as Controller;
  }
  late Controller con;

  @override
  void initState() {
    /// Look inside the parent function and see it calls
    /// all it's Controllers if any.
    super.initState();

    /// Retrieve the 'app level' State object
    appState = rootState!;

    /// You're able to retrieve the Controller(s) from other State objects.
    var con = appState.controller;

    con = appState.controllerByType<AppController>();

    con = appState.controllerById(con?.keyId);
  }

  late AppStateMVC appState;

  /// This is 'the View'; the interface of the home page.
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              /// Display the App's data object if it has something to display
              if (con.dataObject != null && con.dataObject is String)
                Padding(
                  padding: const EdgeInsets.all(30),
                  child: Text(
                    con.dataObject as String,
                    key: const Key('greetings'),
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: Theme.of(context).textTheme.headline4!.fontSize,
                    ),
                  ),
                ),
              Text(
                'You have pushed the button this many times:',
                style: Theme.of(context).textTheme.bodyText2,
              ),
              // Text(
              //   '${con.count}',
              //   style: Theme.of(context).textTheme.headline4,
              // ),
              SetState(
                builder: (context, dataObject) => Text(
                  '${con.count}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          key: const Key('+'),

          /// Refresh only the Text widget containing the counter.
          onPressed: () => con.incrementCounter(),

          /// The traditional approach calling the State object's setState() function.
          // onPressed: () {
          //   setState(con.incrementCounter);
          // },
          /// You can have the Controller called the interface (the View).
//          onPressed: con.onPressed,
          child: const Icon(Icons.add),
        ),
      );

  /// Supply an error handler for Unit Testing.
  @override
  void onError(FlutterErrorDetails details) {
    /// Error is now handled.
    super.onError(details);
  }
}

/// example/src/home/controller/controller.dart

import 'package:example/src/view.dart';

import 'package:example/src/model.dart';

class Controller extends StateXController {
  factory Controller([StateX? state]) => _this ??= Controller._(state);
  Controller._(StateX? state)
      : _model = Model(),
        super(state);
  static Controller? _this;

  final Model _model;

  /// Note, the count comes from a separate class, _Model.
  int get count => _model.counter;

  // The Controller knows how to 'talk to' the Model and to the View (interface).
  void incrementCounter() {
    //
    _model.incrementCounter();

    /// Only calls only 'SetState' widgets
    /// or widgets that called the inheritWidget(context) function
    inheritBuild();

    /// Retrieve a particular State object.
    final homeState = stateOf<MyHomePage>();

    /// If working with a particular State object and if divisible by 5
    if (homeState != null && _model.counter % 5 == 0) {
      //
      dataObject = _model.sayHello();
      setState(() {});
    }
  }

  /// Call the State object's setState() function to reflect the change.
  void onPressed() => setState(() => _model.incrementCounter());
}

/// example/src/view.dart

export 'package:flutter/material.dart' hide StateSetter;

export 'package:mvc_pattern/mvc_pattern.dart';

export 'package:example/src/app/view/my_app.dart';

export 'package:example/src/home/view/my_home_page.dart';

export 'package:example/src/home/view/page_01.dart';

export 'package:example/src/home/view/page_02.dart';

export 'package:example/src/home/view/page_03.dart';

export 'package:example/src/home/view/common/build_page.dart';

/// example/src/controller.dart

export 'package:example/src/app/controller/app_controller.dart';

export 'package:example/src/home/controller/controller.dart';

export 'package:example/src/home/controller/another_controller.dart';

export 'package:example/src/home/controller/yet_another_controller.dart';

/// example/src/model.dart

export 'package:example/src/home/model/data_source.dart';

有关 StateX 包的更多信息,请参阅文章 ‘StateX in Flutter’ online article

GitHub

查看 Github