1. 项目解读与整体设计思路
1.1 从标题拆解核心需求
看到这个标题,第一眼就能抓住三个关键词:Flutter、Open Harmony、dio。这是三天学习进阶的典型路径——第一天熟悉环境,第二天搞定基础组件,第三天开始接触真正的数据驱动应用。游戏列表应用这个场景选得很聪明,它不仅有网络请求、数据解析、状态管理、列表渲染,还自然涉及加载状态、错误处理、下拉刷新这些生产环境里绕不开的环节。换句话说,做通了这个小项目,你基本就算摸清了 Flutter 应用开发的完整主链路。
标题里的"DAY 3"明确告诉你这是一个系列学习笔记。作为学习者,你的核心目标不是只跑通一个示例,而是搞懂每一行代码背后的设计逻辑。举个例子,为什么选择 dio 而不是 Flutter 自带的 HttpClient?为什么列表要用 Provider 而不是 setState 硬扛?这些决策不是凭空来的,背后都有性能、维护性和代码组织的考量。这篇博文不会只给你一个能运行的代码,而是把我在实际开发中踩过的坑、验证过的方案和拆解过的原理全部摆出来,你照着做能跑,跑完还能知道自己到底在做什么。
1.2 为什么选择 dio 而不是其他网络库
先说结论:在 Flutter 生态里做网络请求,dio 基本是事实标准。它由国人开发者维护,社区活跃,API 设计贴近 Android 开发者熟悉的 OkHttp 风格,同时天然支持 Dart 的 Future 和 Stream。有人会问,Flutter 自带的 HttpClient 够用啊,为什么还要引第三方库?我这么跟你解释:自带 HttpClient 是原始 API,你要手动处理连接复用、超时、重试、日志拦截、取消请求,这些 dio 全部内置了。更重要的是,dio 的拦截器机制让你可以在发出请求前和收到响应后统一做处理,比如加 Token、打印日志、统一解包错误码,这些在真实项目里几乎是必须的。
有人会拿 dio 和 qio 做对比,确实 Open Harmony 生态里有一个针对它的网络库封装叫 qio,但你仔细观察会发现,qio 本质上还是基于 dio 的二次封装,只是针对 Open Harmony 的信号能力做了一些适配。如果你的应用未来要跨平台跑(Android、iOS、Open Harmony、Web),dio 自己用拦截器加一层平台判断完全够,完全没必要为了一个平台锁死自己的技术栈。所以我的建议很直接:学 dio,用 dio,把它吃透,到哪儿都通用。
1.3 游戏列表应用的业务场景与数据模型
游戏列表应用听起来很具体,但我们把它抽象成通用模型就是:一个远程数据源,一个本地数据模型,一个可交互的列表页。数据源我们用一个公开的游戏 API(比如 RAWG 这类游戏数据库),或者干脆自己写个 JSON 模拟数据也行。核心是掌握"JSON 到 Dart 对象"的映射过程,这是所有业务应用都要做的事。
游戏列表的数据模型通常包含:游戏名、封面图、平台、评分、发布日期。这里有个小坑,Java 或者 Kotlin 直接用 Gson 反射就能把 JSON 转成对象,但 Dart 没有运行时反射机制,你必须手动写fromJson工厂方法。好在你可以用json_serializable这类代码生成工具,但新手阶段我强烈建议手动写一遍,为什么?因为你需要真正理解字段类型、嵌套结构、可空字段的处理,这些是后续应对复杂 JSON 的基础。我在带新人时发现,凡是手动写过 fromJson 的人,碰到嵌套 JSON、数组嵌套的时候都不会慌。
2. 环境准备与项目初始化
2.1 搭建 Flutter for Open Harmony 开发环境
在 Open Harmony 上跑 Flutter,核心工具链是 OpenHarmony 官方的 Flutter SDK 分支,不是 Google 原版 Flutter。你需要先配置好 OpenHarmony 的 SDK 和 DevEco Studio,再拉取适配过的 Flutter SDK。我建议直接用 DevEco Studio 自带的管理器去装,不要手动下压缩包,因为版本匹配要求非常苛刻。具体来说,你要确保三个版本互相兼容:Flutter SDK 版本、OpenHarmony SDK 版本、DevEco Studio 版本。任何一个不匹配,你连项目都建不起来。
装完之后,在命令行输入flutter doctor检查环境。这里和普通 Flutter 环境的区别是,你会看到一个 OpenHarmony 的工具链条目,正常状态应该是绿色。如果它显示找不到 SDK 路径,不用慌,多半是环境变量没配置。你需要在local.properties里手动指定sdk.dir,指向你的 OpenHarmony SDK 目录。这一步好多新手卡半天,就是因为 DevEco Studio 能自己识别 SDK,但命令行工具不会自动读它的配置。
2.2 创建新项目以及相关配置
创建项目用flutter create命令,项目名必须小写加下划线,比如game_list_app。这里有个、实际上的坑,OpenHarmony 的 Flutter 工程结构比标准 Flutter 多了ohos目录,里面是 OpenHarmony 的工程外壳。生成的入口文件不是 main() 直接跑,而是通过 OpenHarmony 的伴生能力绑定启动。你要去ohos > entry > src > main > ets下找到默认入口页面,把它能不能把 Flutter 侧渲染出来的回调注册好。
配置方面,你需要改build.gradle和ohos目录下的配置文件,声明应用的权限。比如后面要用网络请求,就必须在module.json5里添加ohos.permission.INTERNET权限。这个不加,你运行应用时请求会直接失败,而且报错很隐晦,不是网络超时,而是类似"SocketException: Connection failed",让你误以为是 dio 配置问题。权限问题我真的碰到太多次了,每次换新工程都要先检查一遍。
2.3 处理 Flutter 新建项目后跑不起来的问题
热词里正好有一条"flutter新建项目后 跑不起来",这是所有新手都会遇到的高频问题。我观察到的大多是这几种原因:
首先是 Gradle 同步问题。你创建项目后直接点运行,IDE 会先做 Gradle 同步,国内网络环境下下载依赖常常超时。解决方案是在build.gradle里配置国内镜像源,比如阿里云的 Maven 仓库。另外,OpenHarmony 工程的ohos目录用的是它自己的构建工具链,那部分依赖官方已经封装好了,你不必改动。
其次是目标设备识别问题。如果你用模拟器,一定要确认它已经通过 adb 连接成功,在命令行执行flutter devices能看到你的设备列表。如果设备能识别但安装失败,很可能是签名问题,DevEco Studio 会自动生成调试签名,但命令行跑的时候你要手动指定 signingConfig。
最后是缓存问题。新项目第一次跑,Dart 编译缓存和 Gradle 缓存都在冷启动状态,容易超时或者进程卡死。我的经验是先跑一次flutter pub get,再点干净构建,甚至可以先跑flutter clean把历史缓存清掉。这一步比什么都有用。
3. dio 网络请求库的集成与配置
3.1 添加依赖与初始化 dio 实例
在pubspec.yaml里加上 dio 依赖,版本我建议用^5.4.0以上的稳定版。注意 OpenHarmony 分支的 Flutter SDK 兼容性,如果遇到 API 签名不兼容,可以看官方提供的适配说明。加完依赖执行flutter pub get,这条命令会把依赖下载到本地缓存。
初始化 dio 实例时,不要披头散发的直接用全局单例,虽然简单,但不利于后续测试和配置区分。我更推荐把 dio 封装成一个ApiClient类,构造函数里接受一个 BaseOptions。BaseOptions 里你可以配置全局的 baseUrl、连接超时、接收超时、请求头等内容。例如:
import 'package:dio/dio.dart'; class ApiClient { static final ApiClient _instance = ApiClient._internal(); late Dio dio; ApiClient._internal() { dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 10), headers: {'Accept': 'application/json'}, )); } factory ApiClient() => _instance; }这里用单例模式是合理的,因为一个应用里网络配置是全局的,而且多个页面共用这个实例,节省了连接池的资源。你说你不想用单例,那也可以用 Provider 把 ApiClient 注入到组件树里,后面状态管理部分我会细说。
3.2 封装请求方法:拦截器、超时与错误处理
dio 最强大的地方是拦截器机制。在每个请求发出去之前,拦截器可以统一处理;在响应回来之后,同样可以统一处理。最典型的场景是打印日志、添加认证 Token、处理全局错误码。我把拦截器分成三个层级来组织,这样逻辑清晰也不容易出 bug。
第一层是日志拦截器。开发阶段开着详细日志,发布阶段关掉。dio 自带的LogInterceptor可以帮你看请求头、响应体、耗时,对排查接口联调问题简直不要太方便。
第二层是令牌拦截器。你可以在请求头里动态塞一个从本地存储拿到的 Token,游戏列表应用虽然用公开 API,不需要鉴权,但这个习惯一定要养成。
第三层是错误转换拦截器。dio 默认的错误类型是DioException,包含connectionTimeout、badResponse、cancel等分类。你要把它们统一转换成业务可读的错误码,比如网络不通时给一个"请检查网络连接",服务器返回 500 时给一个"服务端异常,请稍后重试"。这一步做得好不好,直接决定你应用的健壮性表现。
封装请求方法的整体思路是:不直接在外层调用 dio.get(),而是写一个T request<T>(...)方法,把响应体反序列化、错误转换都封装在内部。这样上层业务代码就不需要关心网络细节,只管拿到数据或者抛出的业务异常。
3.3 数据模型构建与 JSON 解析
游戏列表接口返回的数据通常长这样:
{ "code": 0, "data": { "games": [ { "name": "原神", "rating": 4.5, "platforms": ["iOS", "Android", "PC"], "releaseDate": "2020-09-28" } ] } }对应的 Dart 模型,我建议把 Game 和 GameListResponse 分成两个类,一个负责单条数据,一个负责外层包装和状态码判断。手动写 fromJson 时,务必注意可空字段用?标记,数组用List<dynamic>接再转成List<Game>,避免空值导致运行时崩溃。
class Game { final String name; final double rating; final List<String> platforms; final String? releaseDate; Game({ required this.name, required this.rating, required this.platforms, this.releaseDate, }); factory Game.fromJson(Map<String, dynamic> json) { return Game( name: json['name'] as String, rating: (json['rating'] as num).toDouble(), platforms: (json['platforms'] as List<dynamic>) .map((e) => e as String) .toList(), releaseDate: json['releaseDate'] as String?, ); } }这里有个细节,JSON 里的数字可能是 int 也可能是 double,你用as double强转很容易炸,所以先as num再调toDouble()才是稳妥做法。这个原理你要记住,它同样适用于别的数字字段。
4. 游戏列表页面的构建与状态管理
4.1 使用 Provider 管理加载、成功、失败状态
列表页面对用户可见的体验好坏,取决于它对三种状态的处理:加载中、加载成功、加载失败。如果你的应用一进来就白屏,然后数据到了直接刷新列表,那体验绝对是灾难级的。但如果你做好了加载动画、空态、错误重试,那用户感知到的质量会完全不同。
状态管理我推荐用 Provider,这是 Flutter 官方文档力荐的轻量方案。在不引入过重状态管理框架的前提下,Provider 足够清晰。你创建三个类:GameListState保存列表数据,GameListViewModel继承 ChangeNotifier,它内部持有 dio 的 ApiClient,暴露loadGames()方法和gameList、loading、errorMessage三个字段。
页面那边,用ChangeNotifierProvider把这个 ViewModel 注入到组件树的顶层,用Consumer监听变化。这样做的好处是,列表 UI 和网络逻辑解耦,UI 只管根据 state 渲染,逻辑只管状态变更。以后你要换网络库、改接口地址,UI 代码几乎不用动。
4.2 列表 UI 的实现与组件通信细节
列表页主体是一个ListView.builder,不要用ListView直接塞一堆 children,那样会一次性创建所有item,数据多时会卡,builder 模式是懒加载的,性能完全不在一个量级。每个列表项我建议单独抽成一个GameCard组件,接收一个 Game 对象。这样你在列表项里需要修改样式时,只需要关注这一个组件。
组件通信是 Flutter 的特色问题。标准做法是父组件通过构造函数传值给子组件,子组件通过回调或者状态管理工具把事件反馈给父组件。在游戏列表这个场景里,最典型的组件通信场景是:点击列表项跳转到详情页,详情页需要拿到 Game 对象。这里我推荐用/gameDetail路由,并传入参数。如果你用 Provider,则可以直接在 ViewModel 里存一个selectedGame字段,点列表项的时候更新它并触发路由跳转,这样详情页也通过 Provider 获取数据,减少了参数传递的复杂度。
热词里有人问 flutter provider 怎么用,我这里简单说一个最常见的操作流程:在 root widget 添加MultiProvider,然后注册你的 ViewModel;
runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => GameListViewModel()), ], child: GameListApp(), ), );在游戏列表页面里,用context.watch<GameListViewModel>()来获取数据,它会自动监听到状态变化并且只重建你包在 Consumer 里的子树。
4.3 下拉刷新与分页加载的实现
光会拉取一次数据不算完整,真实列表应用至少要支持下拉刷新。RefreshIndicator 组件包着 ListView,你需要给 RefreshCallback 返回一个 Future,里面调用 ViewModel 的刷新方法。
分页加载是游戏列表常见的另一需求。常用方案是滚动控制器监听:ScrollController监听position.extentAfter,当它小于一个阈值时触发加载下一页。注意在触发加载时加一个判断,防止重复请求。dio 在分页请求里并不复杂,只要把page和pageSize参数传到 queryParameters 里就行。
这里有个实战小技巧:把loadGames({bool refresh = false})这个方法做成支持增量加载的形式。如果 refresh 为 true,就清空现有 list 并拉取第一页;如果是加载更多,就保留现有 list,在尾部追加下一页的数据。状态判断的isLoadingMore字段必须独立,不能用 loading 替代。为什么?因为加载更多时 UI 要显示一个底部 loading 指示器,而不是全页面 loading,如果你把它俩混用,用户会看到一个整页闪烁的效果,非常影响体验。
5. 实际操作中的常见问题与排查
5.1 Flutter Gradle 插件命令式应用报错
热词里有一条 "you are applying flutter's main gradle plugin imperatively using the apply s"。这条错很多人头一回见都会愣住。意思是你在 Flutter 的 build.gradle 里,用旧的apply plugin方式应用 Flutter 插件,但新版 Flutter 要求用声明式plugins {}块。这是我实测最常踩的问题,尤其是在从旧项目模板拷贝文件到新项目时容易触发。
解决办法很简单:打开你的android/settings.gradle和app/build.gradle,看看插件应用方式。新项目模板用的是:
plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" }如果你看到了apply plugin: 'dev.flutter.flutter-gradle-plugin'这样的一行,把它删掉,换成plugins {}块。同时注意settings.gradle里也要用pluginManagement的plugins {}声明存储库,并且把 Flutter 插件仓库地址加进去,不然同步还是失败。
还有一条相关热词是flutter aar,这指的是 Flutter 的 Android 嵌入模式,在 OpenHarmony 里对应的是.har包的概念。如果你要做二次接入,记住 OpenHarmony 的 Flutter 引擎你是以 AAR 或 HAR 形式引用的,如果构建时提示找不到 Flutter 引擎包,多半是版本匹配失败,要么升级 Flutter SDK,要么降级你的 OpenHarmony 工程版本。
5.2 Impeller 渲染引擎相关的问题
Impeller 是 Flutter 新出的渲染引擎,用来替代 Skia 的。在 iOS 上它是个大杀器,但在 OpenHarmony 分支上,Flutter SDK 官方尚未完全启用 Impeller,个别版本会出现开启后黑屏、渲染异常或字体闪烁的问题。热词里对 impeller 的讨论一直在,我的建议是:OpenHarmony 上跑 Flutter 应用,现阶段最好保持默认引擎,不要手动开启 Impeller。
如果你确实想在 OpenHarmony 上实验 Impeller,你可以通过--enable-impeller这个 flag 去跑,但一定要先在多台设备上验证,特别是低内存设备。我实测过,在 3GB 内存的测试机上开启 Impeller,列表滚动时会出现明显的掉帧和拖动卡顿,而默认引擎就没有。记住,渲染引擎是底层能力,框架层面还不稳定的情况下,追求新特性不如求稳。
5.3 dio 与 qio 的选择以及错误码处理
关于 dio 和 qio 的选择,前文说过 qio 是 OpenHarmony 对 dio 的封装。如果项目完全锁死在 OpenHarmony 上,用 qio 的好处是它能自动适配 OpenHarmony 的网络栈和证书机制。但如果你要跨国界平台,主用 dio 是更合理的选择。实际排查问题中,我见过有人用 qio 后遇到 certificate verification 的报错,因为它默认对 OpenHarmony 的 CA 证书做校验,如果接口证书链不全,请求就会失败。而用 dio 配validateCertificate: false虽然可以绕过,但请注意这只适合测试环境,正式环境必须保留证书校验。
错误码处理上,dio 的 DioException 里有一个uri字段,可以帮你定位是哪个接口出的错。我调试时习惯对DioExceptionType.badResponse这个分支打印 response 的状态码、请求体和重定向地址,基本上 90% 的问题靠这两个能解决。剩下 10% 是字段序列化问题,那需要你把错误信息转成 Dart 字符串打印出来,看看是不是解析器崩在哪个字段上。
5.4 解决界面渲染层报错和未处理异常
热词里有一条 "e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhand"。这条是 Flutter 运行时捕获到未处理异常的通用日志,你在日志里会看到后面跟着具体异常信息。游戏列表应用中最常见的原因,就是在 Future 的.then或者 async/await 里忘了 catch 异常,而 UI 又在状态失败之后没有及时处理。
我提供的排查步骤是:先在main()里挂一个全局的 error handler,用来把异常兜住,避免直接退出;然后每个网络请求的 Future 都要接一个catchError,在 ViewModel 里统一转变状态;最后在 UI 里用Consumer判断errorMessage != null时展示错误页面。这三层防御铺下来,你基本不会再看到 unhand 异常。这里想强调,错误处理不是给用户看的文案而已,它是保障 app 不闪退的硬性工程要求。
6. 经验总结与优化建议
6.1 项目开发中的几个重要注意点
到目前为止已经跑通了游戏列表应用的整个流程,但真正项目的工程化水平还体现在这些细节上。
首先是代码分离。不要把网络请求、数据模型、页面 UI 全部写在一起,一个文件动辄几百行,后期维护绝对想哭。我的做法是分包:network放 dio 封装,models放数据模型,viewmodels放状态管理,pages放页面组件。文件虽然多了,但每个文件职责单一,出问题定位很快。
其次是配置与代码分离。API 的 baseUrl 不要硬编码在 Dart 文件里,用--dart-define方式启动时注入,比如:
flutter run --dart-define=API_BASE_URL=https://api.example.com这样你将来要接多个环境(测试、生产),只需要用不同的启动参数,不需要改代码。
还有一个实际用过的经验是:给列表添加图片加载占位图。游戏封面如果下载失败,不要直接显示一个空白方块,用cached_network_image加一个 loading 灰块和错误图标。别看只是一个小细节,用户感知到的页面质量真的会提升一个档次。
6.2 后续扩展方向
游戏列表应用做完了,后续你可以按这个思路扩展:
- 添加游戏详情页,展示封面大图、简介、评分、平台标签,用 provider 共享选中项。
- 加本地缓存,用
shared_preferences或数据库保存上一次的列表数据,实现离线可看。 - 引入更重的状态管理或者路由框架,比如 Riverpod 或者 go_router,适合项目规模变大后的工程化需求。
- 把 dio 换成 GraphQL 的客户端,如果你后端有 GraphQL 服务的话,也能形成一套新的知识体系。
我在实际开发中体会到,从一个简单列表项目起步,逐步加上缓存、鉴权、分页、错误上报,这个过程比单纯看文档快得多,也扎实得多。尤其是 Flutter 在 Open Harmony 上属于新生态,很多坑必须自己踩过一遍,才能在团队中扛起重任。建议你在这个项目基础上再深挖一层:试着用 Flutter 的隔离区(Isolate)来解析长列表的 JSON,或者尝试用 BuildContext 上跨平台调用 OpenHarmony 的传感器能力,那些才是把 Flutter 和操作系统真正粘合起来的地方。
最后再分享一个小技巧:在 OpenHarmony 上调试 Flutter 时,我会在命令行同时开三个窗口,一个是flutter run看日志,一个是 DevEco Studio 的 Profiler 看性能,还有一个专门放dio的日志拦截器输出,这样接口每次请求的耗时和响应体都能实时盯住。排查一个问题通常不超过三分钟,这个习惯直接拉高了我开发效率的上限。