1.架构概览
为了降低Flutter工程里lib的复杂度,应尽量拆分一些代码成为独立的package。如图:
我们将通用的组件、领域模型、API、features、存储、repository等抽取成了单独的package。这时lib只剩下多国语言、基本的页面、路由等代码了:
这样做的好处是:
1.更细粒度的依赖控制。因为每个package有一个单独的pubspec.yaml文件,你无需在主package的pubspec.yaml中添加需要的全部依赖。
2.更清晰的边界。你的团队需要审慎对待应该公开哪些类和函数的问题。
3.更容易避免代码冲突。
4.当修改单个package时,只需要更短的集成测试时间。
2.package管理
接下去的问题是如何组织packages文件夹。常见的package划分策略是按层划分和按功能划分。在不使用package或者不支持package的情况下,可以将package视作文件夹。
2.1按层(layer)划分
按层划分意味着根据代码使用的技术因素来划分代码。例如,数据库相关代码是一个package,网络请求相关代码是一个package,widget在另一个package中。package的层次结构如图所示:
优势:
1.按层划分package符合我们的思维习惯,因此具有更低的学习曲线。
2.按层划分package鼓励代码重用。代码文件属于层,而不是某个功能时,你可以不经思考地使用某个组件,尽管它最初可能是为另一个功能开发的。
3.不同的项目最后可能会拥有类似,甚至相同的结构。
劣势:
1.按层划分结构不会立即传达有关应用的最有趣的信息。在浏览代码库时,您不太可能想知道它是否有页面文件,而想知道它具有哪些功能。
2.一切都是公开的。例如,每个页面文件都可以导入所有状态管理器文件,即使大多数页面使用单个状态管理器也是如此。这使得粗心的开发人员更容易导入他们不应该导入的文件。
3.开发人员必须不断地在文件树中跳来跳去。当经常一起更改的文件存储在不同的位置(例如页面和状态管理器)时,就会发生这种情况。这与《干净的代码》一书的著名作者罗伯特·塞西尔·马丁(Robert Cecil Martin)教导我们的单一责任原则背道而驰:“将出于相同原因而变化的事物聚集在一起。”
4.它不能很好地扩展。随着项目中文件数量的增加,包的数量保持不变。无论您的项目有 5 个页面还是 50 个页面,您仍然只有一个 ui 包。
5.这使得新团队成员的加入变得困难。您要么知道所有功能的工作原理,要么不知道任何功能的工作原理;没有中间地带。你觉得你必须了解一切才能参与帮助任何事情。
2.2按功能(feature)划分
按功能划分就是根据代码的领域关联性对代码进行分组。譬如,ui包下的quote_list_screen.dart和state_managers包下的quote_list_bloc.dart可以都放在quote_list包中。package的层次结构如图所示:
优势:
1.使用按功能划分的方法,查找文件变得轻而易举。代码库的结构反映了应用的设计。
2.扩展性很好。随着文件数量的增加,包的数量也会相应增加。
3.代码库变为自文档化。应用程序的大小及其功能一目了然。
4.可以完全控制可见性。例如,现在 quote_list_bloc.dart 只能在 quote_list 包内可见。
5.为新成员提供更顺畅的开始。你只需要了解你正在使用的功能。
6.可以获得更清晰的小组所有权。每个子团队都确切地知道它负责哪些包。
7.进行试验和迁移很容易。想要尝试一种新的状态管理方法?没关系。将其限制在单个功能包中,其他人不必为此担心。
劣势:
1.它助长了创建所谓的common package,也就是开发者用于存放被多个功能所使用的代码的包。这在理论上似乎看起来不错。但是在实践中common package变成了一个巨大的垃圾箱,里面的文件彼此完全无关。
2.代码重复的风险更高。如果你需要一些已经在另一个功能中实现的东西,那么你有可能要么不知道它,要么不想承担将其移动到common package的重任,所以你选择创建另一个版本。
3.在决定将文件放置在哪里时,它需要一定的心智负担。“它应该在那个包里吗?我应该为它创建另一个包吗?它应该在公共内部吗?“
2.3混合划分
如你所见,这两种方法都有优点和缺点。按层划分最适合与单个功能无关的文件,例如数据库和网络内容。相比之下,对于很少重用的文件(如页面和状态管理器),按功能划分则大放异彩。那么,为什么不将两者混合使用,并在你觉得需要时创建包呢?package的层次结构如图所示:
注意到有些包是基于功能的,例如quote_list,quote_details和sign_in。相比之下,上面展示的其他包都是基于分层的,例如key_value_storage和component_library。
以下是管理包分发的四条戒条:
1.feature有它们各自的包
何为feature?对于一些人,一个feature就是一个screen(页面)。对于其他人,一个feature是一组相关联的页面。此外,正式定义会告诉您一个页面可以汇集许多功能,例如主页面。同时,一项功能可以跨越不同的页面,例如电商结账流程。听起来很复杂,对吧?幸运的是,你不必那么教条主义。在这里,您可以认为功能是:
1.一个页面
2.一个执行网络或者数据库I/O调用的对话框。
除此之外,如果它只是一个虚拟的 UI 组件,你想在两个或多个页面之间共享,比如搜索栏,你应该把它放在component_library包。
2.feature彼此之间不了解
当页面 A 想要打开页面 B 时,它不会导入页面 B 并直接导航到它。相反,页面 A 的构造函数接收一个函数,当它想要打开页面 B 时,它可以调用该函数。最后,主应用程序包将连接这个过程。
3.repository有它们各自的包
存储库(repository)是负责通过协调不同的来源(如网络和数据库)来获取和发送数据的类。
4.没有common package
当您需要在两个或多个包之间共享某些内容时,您将创建一个更专用的包来处理该问题。您的五个包源自此规则:
component_library:保存正在或有可能跨不同页面重用的 UI 组件。
fav_qs_api:由于user_repository和quote_repository都跟远程quote API通信,为其单独创建一个包是有意义的。
key_value_storage:和fav_qs_api类似,但是它封装了本地存储功能。
domain_models:你可以预期存储库将需要在某个时候开始共享模型或自定义异常。因此,从一开始就为您的域模型提供单独的包是一件好事。
form_fields:包含不同功能共享的字段验证逻辑
参考:
《Real-World Flutter by Tutorials》