之前负责的 Android 项目都是原生开发,公司有基于 Flutter 开发的项目,最近分了个问题单给我来改,准备基于实际问题一边修改问题,一边学习 Flutter 相关的知识。此文便记录下学习 Flutter 的过程。
环境准备
一般公司内部都会有相关的文档,如何准备相应的 Flutter 环境。Android 的话,基于 Android Studio 安卓 Flutter、Dart 插件就可以了。另外需要安装 Flutter 环境,主要就是涉及到几个指令:
1 | flutter doctor // 检查 Flutter 环境、Android SDK、证书是否齐全登 |
pub get是根据 pubspec.yaml 下载所有第三方库的指令。pub 是 Flutter/Dart 的 “包管理器”,相当于前端的 npm、Android 的 gradle、iOS 的 CocoaPods。
它大概长这样:
1 | name: demo |
当各类环境准备好之后,就可以直接运行项目了。项目能正常跑起来了,就可以看问题单了。
这里需要额外注意下下安装 Flutter 的版本,要和项目依赖的 Flutter 版本一致,最好不要自己随便下个版本就用了,否则可能会导致项目编译失败。
1 | Couldn't resolve the package 'dio' in 'package:dio/dio.dart'. |
问题单
问题单描述很简单:部分页面底部图标显示不全。
截图如下:
很明显,弹出的底部按钮被截断了。考虑到这个问题是在 Android 16 的手机上出现的,在我自己的测试机(Android 14)上没有这个问题,初步怀疑是 Android 16 的 Edge-to-Edge 导致的。现在需要找到这个弹窗的代码,进行排查。
当前的 Flutter 项目不同于原生 Android 项目,它没有 Activity,没有 Fragment,没有 XML。
首先全局搜索New Device discovered找到这个字符串的引用。
搜索的时候最好勾选All Places,这样可以找到所有引用,否则可能会漏掉一些引用(有些引用是通过相对路径引入,Project 则搜索不到)。
就比如拿到词条对应的 key 进行搜索:

第一个图搜索之看到定义的地方,却没有调用的地方。第二个图则能搜到在 local_net_can_active_alert_widget.dart 中调用了这个字符串,这个文件是在本地相对路径依赖的 FlutterPackages SDK(公司多个项目,都会依赖 SDK)中的。
找到文件便可以查看对应的代码了:
1 | class _LocalNetCanActiveAlertWidget extends StatelessWidget { |
dart语言虽说没怎么系统学习过,但也写过这么久的代码了,稍加看一下也能看出来这个页面的内容,是顶部一个 _headWidget,中间一个 _innerContentWidget,底部一个 _toolsWidget。中间的 _innerContentWidget 是 Flexible 的 child,所以它可以自动调整高度,以适应内容。
那么为何这样的一个界面,底部按钮会被截断?以及要如何修改呢?
现在有了 AI,即使很多东西不懂,但也可以很快找到答案。
Android 16 Edge-to-Edge 模式导致弹窗高度计算异常,可以使用 SafeArea 来解决。
修改成这样就好了:
1 | Widget _contentWidget(BuildContext context, List<LocalNetDevice> itemList) { |
修改是把 child 使用 SafeArea 包裹起来, top 为 false,可以看下 SafeArea 的内容:
1 | const SafeArea({ |
它可以支持四个方向的 padding,以及是否维护底部视图的 padding。默认是 true,所以会维护视图四个方向的 padding。问题中的这个弹窗是基于底部的,所以 top 可以设置成 false。
看,虽然咱不懂 Flutter,但经过一段时间的摸索,也可以把这个问题给解决了。
assets 引用
在排查问题时,也有个弹窗有类似的问题,就是底部图标显示不全。最终跟进到代码是这样的:
1 | /// 操作栏 |
截图是这样的:
是在点击更多按钮后弹出来的弹窗。
这里发现关于图标的使用,是通过 assets 引用的。assets/images/themes/default/localNetSearch/operation_more.png 就是更多按钮的图标。而关于图标的引入,是在 pubspec.yaml 中添加的。
1 | assets: |
对应的文件地址在:
只要是这样引入之后,就可以在代码中直接使用了。我问下了 AI,这类路径的重复问题,应该是可以去掉的,assets/images/ 应该就可以覆盖到下面的几个文件夹路径了,这个后面有时间再验证下。除了 image,其他的资源文件(比如本地 html,json 文件,raw 文件等),也可以这样引入。
状态传递
继续接上面的问题,关于状态传递,代码是通过 Cubit 来实现的。可以看到点击更多按钮后,执行的代码是onOpenMoreDeviceOperation:
1 | /// 显示或收起操作弹窗 |
可以看到,最终是 emit 了一个新的状态,包含了操作弹窗的显示状态,操作项列表,当前操作的设备。这个有点像 Kotlin 的 flow,发送一个状态出去了。
那么这个 emit 出去的状态,是怎么样被处理的呢?
查看代码有这样的一个类:
1 |
|
这个 state 类,可以理解为一个状态类,包含了当前页面的所有状态(及数据)。就有点像原生 Android 中的 ViewModel,它所持有的各类数据 LiveData,外部界面通过监听 LiveData 来处理数据。同样的,这里搜索 showOperationMenu 的使用,可以发现唯一一个使用的地方(freezed 文件是 flutter 自动生成,无需关注):
它的代码如下:
1 | class OperationMenuWidget extends StatelessWidget { |
即 OperationMenuWidget 是一个监听 showOperationMenu 状态变化的组件,当 showOperationMenu 变化时,会根据状态值来显示或收起操作弹窗。这样一个状态传递机制,就实现了操作弹窗的显示和收起。
Flutter Cubit
Cubit 是 Flutter 官方主流状态管理库
flutter_bloc中的轻量化状态管理方案,适用于绝大多数常规业务场景。其核心设计思想为UI 与业务逻辑分层解耦,通过统一管理页面数据、交互状态与业务逻辑,替代传统页面堆砌代码的开发方式,有效精简代码结构、提升项目可维护性与状态稳定性。Cubit 整体架构由三个核心概念构成,各司其职、单向联动:State 统一存储页面所有可变数据与交互状态,是唯一的数据源;Cubit 逻辑处理器集中封装所有业务逻辑、网络请求与状态更新行为,不依赖视图层;UI 视图层只负责监听状态变化并渲染界面,仅触发交互行为、不处理业务逻辑。所有状态更新均通过 emit 推送新 State 完成,以状态驱动界面自动刷新,实现逻辑、数据、视图完全分离,代码结构更规范、状态流转更可控。
这个咱们做安卓开发的应该也很熟悉了,MVVM 模式嘛。基于上面的问题,代码中对应的文件是这样的:
Cubit 就是处理逻辑(及数据)的地方,当数据处理完毕后,修改 State 再通过 emit 发送出去,而 Widget 作为 UI 层,监听数据的变化,来做界面的刷新。