# boxes-flutter **Repository Path**: sunlunchang/boxes-flutter ## Basic Information - **Project Name**: boxes-flutter - **Description**: flutter基础开发框架 - **Primary Language**: Dart - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 4 - **Created**: 2021-06-06 - **Last Updated**: 2026-07-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Boxes Flutter `boxes_flutter` 是一个小而明确的 Flutter 基础包:Provider 驱动的 MVVM、页面状态与一次性 UI effect、类型化轻量路由、分页/选择 Controller,以及一组可测试的通用能力。 0.5 是破坏性重构。它保留 `BaseVm`、`FastVm`、`VmSub`、`FastVmSub` 这些核心概念,但不保留 0.4 的深层导入、动态路由、伪 Stateless 生命周期和冗余 `*Util`。旧项目请先阅读 [MIGRATION.md](MIGRATION.md)。 ## 设计边界 - Provider 是唯一的 DI 与监听方案,不再包装第二套容器。 - VM 不保存 `BuildContext`、Widget 或 View 回调。 - 导航与一次性 UI 行为使用类型化 effect;普通状态使用字段、`ValueNotifier` 或 `Command`。 - 路由保持 Navigator 1.0 的轻量实现,不包含 Router 2.0、浏览器地址同步或完整深链。 - 主入口可用于 Web;`dart:io` 能力只从独立入口导出。 - 工具方法传播错误,只有明确命名为 `try*` 的 API 才将异常转为 `Result`。 - 包不发布到 pub.dev,通过 Git 依赖使用。 ## 环境与接入 仓库使用 FVM 固定: - Flutter `3.44.8` - Dart `3.12.2` - 最低 Flutter `3.44.0` - 最低 Dart `3.12.0` 应用通过 Gitee 引用: ```yaml dependencies: boxes_flutter: git: url: https://gitee.com/sunlunchang/boxes-flutter.git ref: dev # 或应用确认过的 commit ``` 然后执行: ```bash fvm flutter pub get ``` Flutter/Web 通用代码只导入: ```dart import 'package:boxes_flutter/boxes_flutter.dart'; ``` 仅 VM 平台需要文件系统时导入: ```dart import 'package:boxes_flutter/boxes_flutter_io.dart'; ``` 不要再导入 `package:boxes_flutter/flutter/slc/...` 或 `package:boxes_flutter/src/...`。 ## 应用初始化 框架提供英文和简体中文资源,但不替应用决定 locale: ```dart MaterialApp( localizationsDelegates: BoxesLocalizations.localizationsDelegates, supportedLocales: BoxesLocalizations.supportedLocales, onGenerateRoute: appRoutes.onGenerateRoute, theme: ThemeData( useMaterial3: true, extensions: [ BoxesTheme.fromThemeData(ThemeData()), ], ), ); ``` 应用可以只合并 `BoxesLocalizations.delegate` 与自己的 delegate、locale 列表。`context.boxesL10n` 返回非空的 `BoxesLocalizations`。 ## Base MVVM `BaseVm` 直接继承 `ChangeNotifier`。它显式管理实现了 `Disposable` 的 Command、分页/选择 Controller 和 `VmSub`: ```dart final class ProfileVm extends BaseVm { ProfileVm(ProfileRepository repository) { load = own>( Command0( () => captureResult(repository.loadCurrentUser), ), ); } late final Command0 load; String title = 'Profile'; void rename(String value) { title = value; notifyListeners(); } } final class ProfileView extends MvvmView { const ProfileView({super.key}); @override Widget buildView(BuildContext context, ProfileVm vm) { return Text(vm.title); } } ``` 用标准 Provider 创建和释放 VM: ```dart ChangeNotifierProvider( create: (_) => ProfileVm(repository), child: const ProfileView(), ); ``` 需要本地 `TextEditingController`、`AnimationController` 等 Flutter 对象时,页面使用普通 `StatefulWidget`,其 State 继承 `MvvmState`,并按 Flutter 规则在 State 中释放这些本地对象。 ### VmSub `VmSub` 的 owner 在构造时确定,且由 owner 注册: ```dart final class SearchVm extends BaseVm { SearchVm() { filters = registerVmSub(FilterVmSub(this)); } late final FilterVmSub filters; } final class FilterVmSub extends VmSub { FilterVmSub(super.owner); void changed() => notifyOwner(); } ``` 重复注册、owner 不匹配、dispose 后通知及重复 dispose 都会显式抛出状态错误。 ## Fast MVVM `FastVm` 在 `BaseVm` 上组合五态页面、页面级加载遮罩和类型化导航 effect。常规页面继承 `FastView`;需要本地 Controller 的页面使用 `FastState`。 ```dart final class OrdersVm extends FastVm { OrdersVm(this.repository); final OrdersRepository repository; List orders = const []; Future load() async { showPageLoading(); final Result> result = await repository.loadOrders(); switch (result) { case Success>(:final value): orders = value; value.isEmpty ? showEmpty() : showContent(); case Failure>(:final error, :final stackTrace): showFailure(error, stackTrace: stackTrace); } } } final class OrdersPage extends FastView { const OrdersPage({super.key}); @override Widget buildContent(BuildContext context, OrdersVm vm) { return ListView.builder( itemCount: vm.orders.length, itemBuilder: (_, int index) => Text(vm.orders[index].name), ); } } ``` `showLoading(message: ...)` 只覆盖当前页面,不调用 `showDialog`,也不会通过 `Navigator.pop` 误关路由。并发请求应分别使用自己的 `Command.isRunning`,不共享隐式加载计数。 每个可导航的 `FastVm` 同时只能绑定一个活动 `FastView`/`FastState`。未绑定就发 effect,或同时绑定两个页面,都会明确报错。 ## 类型化轻量路由 路由在应用层声明为常量持有的对象: ```dart typedef EditUserArgs = ({String? userId, String title}); final BoxesRoute homeRoute = BoxesRoute( name: '/', builder: (context, state) => const HomePage(), ); final BoxesRouteWithArgs editUserRoute = BoxesRouteWithArgs( name: '/users/edit', builder: (context, args, state) { return EditUserPage(userId: args.userId, title: args.title); }, ); final BoxesRouteRegistry appRoutes = BoxesRouteRegistry( routes: [homeRoute, editUserRoute], unknownRouteBuilder: (context, state) { return UnknownPage(location: state.location); }, ); ``` UI 可以直接拿到类型化结果: ```dart final User? saved = await context.boxesRouter.push( editUserRoute.request( (userId: '42', title: 'Edit user'), queryParameters: {'source': 'list'}, ), ); ``` `FastVm` 通过当前页面执行相同请求: ```dart final User? saved = await push( editUserRoute.request((userId: null, title: 'Create user')), ); ``` `replace` 保持 Navigator 的 replacement 语义,不会在失败后偷偷降级为 push。临时的 Flutter `Route` 只能由 UI 通过 `context.boxesRouter.pushRoute()` 推送。 ## 分页与选择 分页与网络库无关,loader 显式返回 `Result>`: ```dart final PaginationController users = PaginationController( pageSize: 20, loader: (PageRequest request) async { return captureResult>(() async { final UserResponse response = await api.list( page: request.pageNumber, size: request.pageSize, ); return Page( pageNumber: request.pageNumber, pageSize: request.pageSize, totalItems: response.total, items: response.rows, ); }); }, ); ``` - 页码从 1 开始。 - `refresh()` 开启新 generation 并替换数据。 - `loadNext()` 追加数据,重复调用复用同一个进行中 Future。 - 旧 generation 或旧 request 的响应不会覆盖新数据。 - `PaginationState.phase` 明确区分 initial、refreshing、loadingMore、data、empty、failure、noMore。 选择状态独立于业务实体: ```dart final SelectionController selection = SelectionController(); selection.select(user.id); selection.setAll(branchUserIds, selected: true); final SelectionState branchState = selection.stateOf(branchUserIds); ``` 单选使用 `SelectionMode.single`;批量写入多个 key 会显式报错。`selectedKeys` 对调用方不可变。 ## 工具能力 | 领域 | API | | --- | --- | | 文本 | `StringBoxes`、`NullableStringBoxes`、`Validators` | | 集合 | `chunked`、`distinctBy`、`associateBy`、`partition` | | 精确数值 | `DecimalMath`、`MoneyCodec` | | 字节 | `ByteSize` | | 时间 | `DateTimeBoxes`、可注入时钟的 `RelativeTimeFormatter` | | 编解码 | `JsonMapper`、`Base64TextCodec`、`HexCodec` | | 摘要 | `Digests.md5*`、`sha256*`、`sha512*` | | 随机 | 可注入 `Random` 的 `RandomSource` | | 异步 | `CountdownController`、`PendingTaskRegistry` | | 偏好 | `PreferencesStore`、`SharedPreferencesStore` | | 路径/资源 | `PathOps`、`AssetTextReader` | | 屏幕 | `context.mediaSize`、`context.flutterView` 等 | | IO 文件 | `FileSystemOps`,仅 `boxes_flutter_io.dart` | 摘要不是加密。需要保密性、认证或密钥管理时,请在应用层选择经过审计的密码学方案。 ## 开发与验证 ```bash fvm flutter pub get fvm flutter gen-l10n fvm dart format --output=none --set-exit-if-changed lib test fvm flutter analyze fvm flutter test fvm flutter test --platform chrome test/web_public_api_test.dart fvm flutter pub outdated ``` 包级 `pubspec.lock` 不提交。生成本地化后再次生成应保持零 diff。 `flutter analyze` 同时启用严格类型分析和 `public_member_api_docs`:所有手写公开 成员都必须提供中文 DartDoc,说明职责、所有权、生命周期、返回值和异常语义。 Flutter SDK 生成的本地化文件不手工编辑。 仓库技能位于 `.agents/skills`。`boxes-flutter-all` 只负责选择模块,MVVM、路由、 UI、util 技能分别维护真实公开 API、用法和平台边界;公开 API 变化时必须在同一 变更中同步更新对应 references。