{"id":23438632,"url":"https://github.com/yohom/decorated_flutter","last_synced_at":"2025-04-13T06:50:23.788Z","repository":{"id":41278681,"uuid":"147671296","full_name":"yohom/decorated_flutter","owner":"yohom","description":"Flutter常用工具/Widget封装","archived":false,"fork":false,"pushed_at":"2025-04-09T03:50:59.000Z","size":2196,"stargazers_count":37,"open_issues_count":0,"forks_count":9,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-09T04:28:05.929Z","etag":null,"topics":["android","flutter","ios"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yohom.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-09-06T12:37:11.000Z","updated_at":"2025-03-22T14:25:54.000Z","dependencies_parsed_at":"2023-11-29T02:38:04.368Z","dependency_job_id":"c74e84f4-7b33-4d0f-8d18-369ac80d211b","html_url":"https://github.com/yohom/decorated_flutter","commit_stats":null,"previous_names":[],"tags_count":469,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yohom%2Fdecorated_flutter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yohom%2Fdecorated_flutter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yohom%2Fdecorated_flutter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yohom%2Fdecorated_flutter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yohom","download_url":"https://codeload.github.com/yohom/decorated_flutter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248675457,"owners_count":21143766,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","flutter","ios"],"created_at":"2024-12-23T14:51:42.921Z","updated_at":"2025-04-13T06:50:23.776Z","avatar_url":"https://github.com/yohom.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 基于BLoC的通用框架\n\n## BLoC的各种元素\n### 什么是`IO`？\n`IO`即`Input/Output`，Input/Output是相对于BLoC来说的，Input即向BLoC输入数据，Output即从BLoC输出数据。\n一个BLoC中所有的属性都是IO，负责接收来自widget层的数据（供后续操作读取数据）或者向widget层输出数据。widget层**只对数据做读取操作**，即widget对数据是*READONLY*的。\n当widget需要对数据发起修改操作时，需要通过`BLoCProvider.of(BuildContext)`/`context.of\u003cSomeBLoC\u003e()`来获取当前widget所依附的BLoC实例，然后调用BLoC的对应方法来操作数据。\n\n### BaseIO\n`BaseIO`是所有IO类型的祖宗类，存放了所有IO类型需要的公共属性和方法。\n\n#### 构造器概览\n```dart\nBaseIO({\n  /// 初始值, 传递给内部的[_subject]\n  T seedValue,\n\n  /// IO代表的语义\n  String semantics,\n\n  /// 是否同步发射数据, 传递给内部的[_subject]\n  bool sync = true,\n\n  /// 是否使用BehaviorSubject, 如果使用, 那么IO内部的[_subject]会保存最近一次的值\n  bool isBehavior = true,\n})  : _semantics = semantics,\n      _seedValue = seedValue,\n      latest = seedValue,\n      _subject = isBehavior\n          ? seedValue != null\n              ? BehaviorSubject\u003cT\u003e.seeded(seedValue, sync: sync)\n              : BehaviorSubject\u003cT\u003e(sync: sync)\n          : PublishSubject\u003cT\u003e(sync: sync) {\n    _subject.listen((data) {\n      latest = data;\n      L.d('[DECORATED_FLUTTER] 当前${semantics ??= data.runtimeType.toString()} latest: $latest'\n          '\\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++');\n    });\n  }\n```\n`BehaviorSubject`是`rxdart`的一个类，作为一个中转站，当有数据add到`BehaviorSubject`时，会转发给`BehaviorSubject`的订阅者。作用有些类似iOS里的`NotificationCenter`，Android中同RxJava中的`BehaviorSubject`。\n\n### Input\n`Input`表示只从widget接收数据，不对widget输出数据的业务单元。常见的使用场景为接收来自`TextFormField`的值。\n\n### Output\n`Output`表示只对widget输出数据，不从widget接收数据的业务单元。常见的使用场景为页面初始数据的加载，比如进入一个列表页，BLoC中的一个Output负责加载列表数据，widget层中的一个ListView绑定这个Output，进入页面前调用Output进行数据的加载，加载完成后刷新ListView。\u003cbr/\u003e\n整个过程Output不会从widget接收数据，只会对widget输出数据。\n\n### IO\n语义上，IO是既可以输入数据又可以输出数据的业务单元；实现上，`IO`即`class IO\u003cT\u003e extends BaseIO\u003cT\u003e with InputMixin, OutputMixin\u003cT, dynamic\u003e`。\u003cbr/\u003e\n常见的使用场景为勾选框，由于Flutter的哲学是widget本身不保存状态，所以像CheckBox这种widget的勾选状态是需要开发者自行维护的，CheckBox本身只负责通知开发者状态即将变化，当CheckBox的onChanged回调出发后，并不会真正的看到CheckBox被勾选，需要开发者自行更新CheckBox的`checked`的值之后才能看到被勾选。所以在这种场景下，数据流动就变成了`CheckBox(onChanged)`-\u003e`BLoC(更新状态值)`-\u003e`CheckBox(监听新的状态值)`。\n\n### 衍生IO\n#### ListIO\n`ListIO`是只接收列表类型的IO，在普通IO的基础上增加了如下方法：\n```dart\n/// 按条件过滤, 并发射过滤后的数据\nList\u003cT\u003e filterItem(bool test(T element));\n\n/// 追加, 并发射\nT append(T element, {bool fromHead = false});\n\n/// 追加一个list, 并发射\nList\u003cT\u003e appendAll(List\u003cT\u003e elements, {bool fromHead = false});\n\n/// 对list的item做变换之后重新组成list\nStream\u003cList\u003cS\u003e\u003e flatMap\u003cS\u003e(S mapper(T value));\n\n/// 替换指定index的元素, 并发射\nT replace(int index, T element);\n\n/// 替换最后一个的元素, 并发射\nT replaceLast(T element);\n\n/// 替换第一个的元素, 并发射\nT replaceFirst(T element);\n\n/// 删除最后一个的元素, 并发射\nT removeLast();\n\n/// 删除一个的元素, 并发射\nT remove(T element);\n\n/// 删除第一个的元素, 并发射\nT removeFirst();\n\n/// 删除指定索引的元素, 并发射\nT removeAt(int index);\n```\n\n#### BoolIO\n`BoolIO`是只接收布尔值的IO，增加了如下方法：\n```dart\n/// 翻转状态值\nbool toggle();\n```\n\n#### PageIO\n`PageIO`在ListIO基础之上进一步封装了分页操作，在`PageIO`会自动维护当前页数，开发者只需要调用`nextPage`方法即可请求下一页数据，如果如要刷新数据，不能再调用IO提供的`update`方法，而是需要使用PageIO中提供的`refresh`方法。\n\n\n## 编码规范\n- 尽量减少嵌套；\n- 尽量让一个Widget长什么样只取决于构造器参数，减少来历不明的变化引起的Widget变化，例如：\n  ```dart\n  class SomeWidget extends StatelessWidget {\n    @override\n    Widget build(BuildContext context) {\n      return StreamBuilder(\n        stream: /* 某处获取来的stream */\n        builder: (context, snapshot) {\n          return AWidget(snapshot.data);\n        }\n      );\n    }\n  }\n  \n  class AWidget extends StatelessWidget {\n    AWidget(AVM vm, {Key key}): super(key: key);\n\n    @override\n    Widget build(BuildContext context) {\n      return /* 根据vm参数构造出widget */\n    }\n  }\n  ```\n  优于\n  ```dart\n  class SomeWidget extends StatelessWidget {\n    @override\n    Widget build(BuildContext context) {\n      return AWidget();\n    }\n  }\n\n  class AWidget extends StatelessWidget {\n    AWidget({Key key}): super(key: key);\n\n    @override\n    Widget build(BuildContext context) {\n      return StreamBuilder(\n        stream: /* 某处获取来的stream */\n        builder: (context, snapshot) {\n          return /* 根据snapshot构造widget */;\n        }\n      );\n    }\n  }\n  ```\n  第二种写法虽然看着代码更少，但是跟某个stream直接绑定，导致难以复用，而且AWidget长什么样不受AWidget类内部逻辑控制(被`某处获取来的stream`控制)，非常不直观。除非这是一个全局使用的widget，绑定的stream也是全局的事件，那么可以这样使用，否则必须要从构造器中传入参数。\n- 优先使用StatelessWidget+BLoC模式实现功能，只有功能比较简单时，可以使用StatefulWidget代替实现；\n- StatelessWidget类里应该只有\n    1. build方法；\n    2. override方法；\n    3. 事件处理(私有)方法；\n- StatefulWidget不做限制；\n\n## 名称\n- 在遵循规范的前提下，不要怕名字太长或者太啰嗦，清晰精确的名称好于莫名其妙的省略；\n- 除非是广泛通用的缩写（例如`http`等），否则不允许使用任何形式的缩写，无意义的缩写会让看代码的人困惑；\n\n### 文件名\n- 文件名使用下划线风格；\n- 页面类以`.screen.dart`结尾；\n- 普通widget类以`.widget.dart`结尾；\n- dialog类以`.dialog.dart`结尾；\n- bloc类以`.bloc.dart`结尾；\n- 类扩展(extension)以`.x.dart`结尾；\n- mixin以`.mixin.dart`结尾；\n- 其他文件目前直接以`.dart`结尾；\n\n### 类名\n- 页面类以`Screen`结尾；\n- 普通widget类根据业务属性命名即可；\n\n### 方法名\n- 方法名使用驼峰命名法；\n- BLoC中的action以`perform`开头，比如登录动作方法命名为`performLogin`；\n\n### 变量名\n- 变量名使用驼峰命名法；\n\n## Widget结构\n- widget树中**无状态变化**的部分，要抽离出单独的widget类，把以构造器参数形式传入需要的数据，并修饰构造器为`const`；\n\n## BLoC结构\n- 为了区分action和IO，IO的成员统一放在同文件内名为`_ComponentMixin`的`mixin`中，action方法放在原`BLoC`中，一个典型的例子：\n```dart\nclass LoginBLoC extends LocalBLoC with _ComponentMixin {\n  LoginBLoC() : super('登录 BLoC');\n\n  Future\u003cbool\u003e performLogin() async {\n    // 获取最新的account和password并执行登录动作\n  }\n}\n\nmixin _ComponentMixin on LocalBLoC {\n  @override\n  List\u003cBaseIO\u003e get disposeBag =\u003e [\n        account,\n        password,\n      ];\n\n  final account = Input\u003cString\u003e(semantics: '账户');\n\n  final password = Input\u003cString\u003e(semantics: '密码');\n}\n```\n\n## 文件夹结构\n### ui相关文件组织\n- `ui`文件夹下分为`screen`文件夹和`widget`文件夹，`screen`文件夹存放所有的单页面，`widget`存放全局共用的控件；\n- `screen`下的文件分为主screen文件和其组成部分，当组成部分比较复杂时，可以再单独为其新建文件夹；\n- 在一个文件夹下，只允许存在两层关系的ui，如果超过两层的关系，则为较复杂的那部分新建文件夹；\n\n## 一些约定的写法\n### `Dialog`的使用\nDialog本身不处理任何业务逻辑，它只负责采集数据，采集完成之后通过`Navigator.pop(data)`回传给宿主widget，并在宿主widget进行业务操作；\n\n### widget\n- 优先使用`StatelessWidget`，只有当`StatelessWidget`实在不方便的时候再启用`StatefulWidget`，比如说要mixin一些辅助；\n- 优先把IO放在局部BLoC，只有当局部BLoC无法满足需求时，再放到全局BLoC；\n\n## 框架模式\n项目的框架模式为`BLoC`(Business Logic Component)，是一种基于流(Stream)的模式。\n一\"块\"UI会搭配一个专属的`BLoC`，`BLoC`中存放着这一块UI所有的状态，以及所有操作数据的接口。\n\n## 全局结构图\n*矩形为widget，圆角矩形普通类*\n![输入图片说明](https://images.gitee.com/uploads/images/2020/0723/071740_e4186103_944757.png \"全局数据流向导图.png\")\n\n## 单页面结构图\n*矩形为widget，圆角矩形普通类*\n\n![输入图片说明](https://images.gitee.com/uploads/images/2020/0722/085532_07d19a93_944757.png \"单页数据流向导图.png\")\n\n## 网络请求时序图\n以登录为例：\n\n![输入图片说明](https://images.gitee.com/uploads/images/2020/0724/085140_fcc8c24f_944757.png \"Screen Shot 2020-07-24 at 08.51.12.png\")\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyohom%2Fdecorated_flutter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyohom%2Fdecorated_flutter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyohom%2Fdecorated_flutter/lists"}