Flutter项目框架搭建小记

Posted by lx8421bcd on October 24, 2023

前言

今年Flutter更新3.0版本之后,性能、稳定性、API丰富程度相比于之前又更加强了一些,公司在新项目上也开始推行Flutter-native并行计划,安排我来主导这个事。我以前也就是写过一点flutter的demo,此次从零开始整flutter-native混编,记录一下框架搭建和SDK选型的思路,仅供参考。

工程组织

新项目

如果你是从零开始的新项目,或者说flutter开发作为并行项目,native的代码只是按需取用,那么直接新建一个flutter project开干,然后把需要用到的native代码导入进flutter项目对应的native目录(Android/iOS)。这样做是最简单方便的,遇到的问题也最少。

老项目添加flutter模块

上面的新项目是理想条件,实际上半路出家尝试Flutter的项目绝大部分是Android一个repo,iOS一个repo,各开发各的互不干涉,各有各的分支管理。现在你Flutter八字还没一撇呢就想把大家整到一个repo里干活肯定不现实。眼下最适合的方式是 flutter-module + git-submodule

flutter-module是flutter官方提供的一种将flutter项目以library module形式集成进现有native项目的方法。官方提供了两种创建flutter module的方式:

从flutter-module的工程结构上来看,官方是推荐在git中将flutter-module作为一个单独的repo来管理的。由Android和iOS的工程各自集成。这样Android、iOS、Flutter各自的影响都只在各自的repo内,需要用到另外一方的时候同步一下对方的代码即可。而git submodule目前看来是flutter-module repo有变更时通知native repo同步的最佳组织形式。

git submodule add <path/to/repos>.git
git add .
git commit -m "feature: add flutter submodule"
git push

之后初始化submodule

git submodule init
git submodule update

然后再初始化flutter-module

flutter pub get

flutter module生成.android和.ios目录后,就可以提供给native项目接入了:

Sample: multiple_flutters

Flutter-Native交互

除非你的项目非常新,非常纯粹,非常简单。不然和native交互是免不了的。而所谓”交互”,无非就是native和flutter相互发消息,接收者执行对应的操作罢了,没什么高深莫测的东西,这一点Flutter SDK已经封装的很完善了(MethodChannel)。主要的问题,是native如何管理flutter页面,或者说的更具体一点,如何管理 FlutterEngine

市面上已有的Flutter-Native交互框架,或称之为工程化框架,这篇文章已经有比较详细的叙述,文章比较客观,但说实话,这也给初尝flutter没什么经验的朋友造成了一定的选择困难。那么我在这里就根据个人经验说点主观的,在当前这个时间节点(2023 & flutter 3.16),native与flutter混合开发的flutter页面管理方案,我只推荐 FlutterEngineGroup

FlutterEngineGroup官方示例:multiple_flutters

FlutterEngineGroup相比于flutter_boost以及其它的几个基于FlutterEngineGroup封装的方案有几大优点:

  • 官方方案,维护和更新相对迅速、稳定
  • 简单易懂,扩展方便,可定制化度高
  • 性能稳定,支持View/Fragment级别页面显示

官方方案意味着其维护和更新必然是相对迅速和稳定的。不必担心发生Flutter版本更新了,一堆Flutter SDK都更新了,一堆新特性等着用,却被一个”交互框架”这种根上的东西卡住没法推进的尴尬情况,这是FlutterEngineGroup最大的优点。

FlutterEngineGroup本身只是一个FlutterEngine的管理组件,并没有诸如flutter-native间页面路由、数据收发等交互的现成方案,需要开发者自行实现。从某种角度来说,简单是一种优点,自行实现flutter-native交互协议有几个好处

  • 灵活,可高度定制化
  • 最大程度利用现有native代码

比如,以实际情况来说,如果你的需求是现有项目集成flutter模块,向flutter渐进式过渡,那么你大概率会遇到如下问题:

  • 现有项目已经有一套完善的路由、页面跳转机制
  • 并不是所有页面都是依靠路由deeplink跳转的,非对外页面在Android/iOS内的路由地址不一样
  • Android/iOS各端业务实现不一致,传参不一致

在这种情况下你引入一个自成一派的flutter-native交互框架(比如flutter_boost),反而要修改你已有的原生代码去适配这个交互框架的调用,且不谈这个工程量与你自己实现一套交互协议相比孰高孰低,单是协调Android/iOS同步适配就够你喝一壶的。而自行实现交互就没有这么多问题了,大家基于”协议”来合作,预先定义好交互协议,然后flutter,Android,iOS端各自实现,最后联调一下即可,各端在具体实现上都有极高的自由度,也能最大限度的利用现有代码。

另一方面,对于已有的工程来说,我们所需要的”交互”可能不只是”打开页面”而已,可能我们需要一些更加定制化的”接口”,比如flutter启动时从native拉取一些缓存数据,flutter直接把数据存到native的database等等…… 对于这种需求,FlutterEngineGroup管理FlutterEngine的页面基于MethodChannel非常好实现。

当然了,FlutterEngineGroup这种简单,对于很多人来说,也是一种缺点,因为简单意味着简陋,比如当我的项目以Flutter为主导从零开始的时候,我可能就只需要一套现成的方案然后我native去适配它就好了,你却要我自行实现一套?烦不烦?

饶是如此,我仍然建议使用FlutterEngineGroup,毕竟Flutter本身仍处于高速发展阶段,SDK更新频率可谓是以月计,在这种大环境下,这种如同地基一般的基础脚手架如果依赖一个更新不是那么勤快的第三方SDK,风险实在是太高了。自己实现一套虽然麻烦一点,但其实也是一劳永逸的工作,而且,不被这种基础SDK卡脖子的感觉,真的,非常好,非常好……

项目包结构

Flutter&Dart的风格是一切皆代码,不像Android/iOS有一堆配置文件,也不像前端那样有html/css/js之类的划分。无论你是UI布局,或是多语言配置,还是颜色调色板、字体字号、Icon资源,在flutter里统统都是 .dart源代码文件。因此flutter的源码目录 lib会比一些项目更大,更复杂。对于 lib目录下的代码组织,个人有以下几点建议:

  • 根目录按功能模块分包,作用域一般为应用全局(如:util - 工具类库;net - 网络调用库;widget - 自定义组件库)
  • 业务相关代码统一放在一个目录下(modules/features/business etc.),采用PBF(Package by feature)模式即按需求分包(如:login - 登录;home - 首页;setting - 设置)
  • feature包内的内容如果很少,可以不分包,如果内容较多,再按照模块分包(page/widget/api/bean等)。如果feature包内内容分包过后还是很庞大很杂,可以考虑将feature拆分为子feature再按PBF模式分包
  • App全局配置的代码文件(如main.dart,路由,全局常量等)直接存放在 lib目录下

也就是说一个正常,划分合理的flutter项目的lib目录结构,差不多是以下这样

-lib
|---common/util
|---l10n
|---model
|---net
|---modules
    |---home
    |---login
    |---setting
    |---future_a
    	|---page_a.dart
        |---a_api.dart
    |---feature_big
        |---sub_feature_a
            |---page_sub_a.dart
            |---sub_a_api.dart
        |---sub_feature_b
|---themes
|---widgets
    |---dialogs.dart
    |---loading_view.dart
|---configs.dart
|---main.dart
|---routes.dart

另外也可以看看flutter Demo项目的经典实例:Flutter Gallery

可以看到其分包理念与我上述的几条建议基本一致。

资源引用化

Flutter里一切皆Dart,怎么组织各类资源反而一时间让人无所适从,但有一些native开发时的经验仍然是可以遵循并行之有效的。

俗话说,能暴露在编译期间的错误就不要留到运行时。换而言之,能让编译器直接给你弹个错说”找不到资源”肯定比等到运行时UI显示异常了再去查资源路径对不对方便。

也就是说,业务代码引用的各类资源,我们最好尽量将其集中定义在某些配置文件中,然后通过引用定义变量/常量的形式引用其值,而非直接在业务代码里hard coded。最好的情况是,能够像Android/iOS一样,这些配置文件能够由IDE自动生成,这样在相关资源改变时我们运行一下生成命令,即可高效,准确的将资源的修改同步到我们的代码之中,并根据变更及时修改引用上的错误。

所幸,flutter目前的生态支持我们实现这种效果。

颜色&字体

这两个没什么好说的,直接在dart代码里定义常量就行了。色表和字号表一般不会轻易变动,就算改动直接改源代码也很方便。

有一个需求点是类似于native暗黑模式切换的形式切换自定义色表、字号表,这个需要搭配主题切换功能实现。不过这又是另一个话题了,此处不赘述。

图片&文件

在flutter中类似资源一般的存放方法是在项目的根目录(与 lib目录齐平)下建立一个assets目录,将资源文件放在这个目录下,然后再在assets目录下建立一个images目录专门存放图片。而使用assets内的文件则是通过在代码内传入assets资源路径获取,这种原始的做法显然不利于debug。

推荐使用 flutter_gen 来实现资源相对路径管理。如何配置直接参考官方文档即可。

当然,flutter_gen的功能很多,但这里仅推荐用其管理assets资源文件。color和dimens如上所述,定义常量本来就已经足够简单好管理了,没什么必要给自己复杂化。

文本

如果你非常确定你的应用在它的整个生命周期内,都只有一种语言,绝对不会涉及到多语言问题。那直接把文本写死到代码里就完事了。没啥规范不规范的,这样做最方便省事,可读性也相对较好。

但如果你的应用有语言切换的需求,那么所有文本通过相对引用来管理就是个必须要考虑的需求。这点可以直接通过引入flutter的国际化框架来解决。

多语言支持

直接使用官方的intl就行了,功能最全,最完善。文档:flutter应用里的国际化

在Android Studio和VSCode上,可以直接使用 flutter_intl 插件来完成国际化的初始化工作

PS:flutter用于存放国际化语言配置的.arb文件是没有办法写传统的注释的,但并不是没有注释,如果你要在这个文件里写注释,可以这么写

{
    ...
    "@explain1":"注释内容1,必须以@开头"
    ...
}

状态管理

新项目直接GetX,没什么好说的,GetX对于其他几个流行的状态管理框架来说堪称降维打击。

如果读者不信邪 ,可以尝试用BLOC、Provider、GetX,分别实现一个Flutter官方demo上的计数器,看看文件数,代码数,代码可读性,你就会明白什么叫降维打击。

用GetX最大的好处,简而言之三个字:有的选

所谓”有的选”,展开成一句话就是”使用GetX,你拥有根据项目复杂度和业务特点选择状态管理架构的权力”。

像BLOC,Provider之类的状态管理框架,看似小巧,但实际上是”搭售”了一整套代码风格(coding style),甚至于说是代码规范(code format)的,换而言之,使用上述这些状态管理框架,你的整个状态管理架构就会被塑造成这些框架所规定的样式。哪怕是最最最最最简单的刷新一下Nickname,你也得BLOC/Provider-ChangeNotifer来一套。哪怕类似于这种的业务根本不需要这么复杂的状态管理架构,但没办法,不按这个样式写,你根本无法实现数据刷新。

而GetX的响应式变量和GetxController-GetBuilder体系则给了你”自由”,按项目复杂度选择状态管理架构的自由。对于简单的表单型UI,你直接用响应式变量一把梭就能解决,而对于复杂UI,GetxController-GetBuilder的组合又能让你实现MVC、MVVM的架构。

这种自由度并不是单纯的出于什么”偷懒”的角度去考虑,而是出于对项目的掌控力,以及对代码复杂度管理的角度去考虑的。在本来很简单的业务上增加代码复杂度,降低可读性,仅仅为了追求所谓的风格一致,太弱智了。

当时GetX也是因为这种”自由”,遭受了很大非议,这个以后再说说我个人的理解,本文不赘述。

Http调用

直接用Dio,这个应该没什么好说的,Dio之于Flutter有点像OkHttp之于Android,甚至二者的代码风格都差不多。

不过有一点要注意的是,Flutter的Retrofit可并不是Android的Retrofit。Flutter屏蔽了Dart反射,Flutter版Retrofit可没有办法像Android那么动态的生成接口调用,而是需要根据注解配合BuilderRunner来生成调用代码的。换而言之,Flutter版Retrofit的作用更像是”在Flutter上写Retrofit风格的代码”,而Flutter本身其实是没有这个需求的,Dio稍微封装一下请求和返回解析,两三百行代码就能解决,还能有更高的定制自由度。

JSON解析

Flutter中所谓的Json解析,主要有2种,String - Map<String,dynamic>之间的转换和 Map<String, dynamic> - Object之间的转换,其中,dart:covert库提供的 jsonEncode()jsonDecode()方法已经解决了前者,我们主要是要解决后者。

正如上文所说,Flutter屏蔽了Dart反射,所以在Flutter中并没有通过反射来解析,所以其实并没有什么”框架”来动态的将 Map<String,dynamic>“解析”成 Object。基本上都是通过BuilderRunner,为特定标识的Model类生成 fromJson()函数,以此实现转换。

目前一般是使用 json_annotation + json_serializable来解决生成的问题。

本地Key-Value存储

目前比较流行的两个选项,shared_preferences和hive

share_preferences就是极致的简单,只有一个数据源,不支持数据源切换,适合存储”应用级”的数据,适合比较简单的应用。当然了,封装一下,自己整个ID/Key什么的在单一数据源上再做数据区分也不是不行。

hive的功能要更加丰富一点,支持数据源切换,适合有多数据源需求(比如每个用户独有的设置项之类)的应用。需要注意一点的是,虽然hive支持存储Object(通过build Adapter等过程),但我不推荐使用这个功能,一是增加了很多不必要的预编译代码,其次是这个功能比较死板,一旦model出现字段变化等情况,容易造成崩溃和脏数据。

遇到复杂的本地数据化需求,该上Sqlite就上Sqlite,不要尝试用简陋的工具实现复杂的需求,自己找不痛快。

其它SDK

这里是一些不值得单独列一段出来说的SDK,按需集成

常用封装

在集成SDK之后需要进行的一些封装,在新建项目时有空闲可以先做,这里列一下

  • 路由管理封装:使用GetX基于GetX封装,否则参考flutter官方demo封装
  • 应用内广播: 类EventBus的封装,在flutter中已经有很多了,无论是自己实现还是找现成的都不复杂, 不赘述
  • 主题切换:如果应用有深色/浅色模式切换的需求,需要一套以此配置的主题相关的色表,则需要基于Flutter Theme来配置
  • 通用Alert弹窗: 虽然现成的很多,但这个建议还是封装一下,用得多,自定义需求高,而且一般应用样式都不一样。
  • 页面通用LoadingWidget: 这个需要根据自己项目的样式以及集成的下拉刷新/上拉加载SDK来封装
  • 自定义NavigationBar: 如果原生的AppBar不能满足需求的话,就需要自己按照项目UI需求封装了

小结

本文所记述的主要就是我在搭建项目框架时,遇到的一些技术问题和其解决方案,或者在当前(2023)flutter生态环境下,一些技术需求的最佳技术选型,以及选型时的部分考量。上文列出来的是我认为基本上所有应用都会涉及到的「业务无关」的基础组件,无论是直播、电商、社交、资讯。离「拎包入住」,也就是做完上面这一套直接就能开始写业务,还有一定距离,但基本上可以做到与native开发体验相差无几,可以说是”该有的都有了”。

感觉也没记多少东西,把搭架子时遇到的注意事项列一下,怎么就接近一万字了……