☰
Flutter+Dio在开源鸿蒙上的跨平台数据清单应用实战
2026/10/22 7:57:46 网站建设 项目流程

1. 先从项目讲到方案:为什么要用 Flutter + Dio 这套组合

先交代一下背景。DAY3 的训练营目标很直接:在开源鸿蒙(OpenHarmony)平台上,用 Flutter 框架从零搭出一个跨平台的数据清单应用,并且通过网络请求从服务端拉取真实数据展示在列表里。这里的“跨平台”不是嘴上说说,同一套 Flutter 代码要能在 OpenHarmony 设备、Android、iOS 以及桌面端跑起来,底层的平台差异由 Flutter 引擎帮我们抹平。

选 Flutter 而不是其他跨平台方案,主要基于三点考量。第一,Flutter 的渲染引擎是自己实现的,UI 在各平台上的还原度非常高,不像某些方案依赖系统原生控件,容易在不同设备上出现样式偏差。第二,Flutter 对 OpenHarmony 的适配已经进入可稳定使用的阶段,官方和社区都有持续跟进,依赖库的鸿蒙化改造也基本覆盖了主流插件。第三,Dart 语言本身的上手门槛低,配合热重载机制,调试效率比传统的原生开发高不少。

网络层选择 Dio 的理由更直接。Dio 是目前 Dart 生态里功能最完整的 HTTP 客户端库,拦截器、全局错误处理、文件上传下载、取消请求这些能力都是开箱即用的,而且它是基于纯 Dart 实现的,不依赖平台原生代码,这意味着在 OpenHarmony 上跑不会遇到 JNI 绑定或者原生插件缺失的坑。实际做下来,用 Dio 封装一个统一的网络请求入口,后期维护成本能降很多。

再说说这个“数据清单应用”到底解决了什么问题。训练营的场景设定是:一个普通的待办事项/数据列表管理页面,需要从 API 拉取远端数据,支持下拉刷新、加载状态管理、错误重试,并且要有干净的架构分层,方便后续叠加搜索、筛选、详情页等功能。这类应用在真实业务里非常常见,相当于一个迷你版的内容管理系统前端。做完这个项目,你对跨平台开发中“UI 层 + 数据层 + 状态管理”的整体协作方式会有直观的理解。

这个指南适合谁?如果你是刚接触 OpenHarmony 开发、想在跨平台方向上找一条务实路线的开发者,或者已经在用 Flutter 但没在开源鸿蒙设备上跑过真实项目,这篇内容可以直接当作参考。我按实际操作顺序来写,包括环境搭建、代码实现、鸿蒙适配、问题排查,每一步都附上了实测过的细节,希望能让你少踩几个坑。

2. 环境准备与工程初始化

2.1 版本选型:这一步最容易翻车

OpenHarmony + Flutter 的组合,最大的坑往往不在代码,而在版本匹配。

OpenHarmony 系统本身有多个分支版本,Flutter 也有自己的版本节奏,两边不是天然对齐的。从我实际测试的情况来看,建议优先选择 OpenHarmony 4.0 及以上版本的系统镜像,配合 Flutter 3.10+ 以及对应的 OpenHarmony Flutter SDK 适配分支。这里有个判断标准:打开 Flutter 的“flutter doctor”检查时,如果能看到 OpenHarmony 相关的开发环境被识别出来,说明引擎层已经支持了。

具体操作步骤:

  1. 安装 OpenHarmony 官方 IDE 并配置 SDK。SDK 版本选择 API 10 或 API 11 都行,但不同 API 版本对 Flutter 工程的兼容性有细微差别,建议新项目直接用 API 11。
  2. 安装 Flutter SDK,确认 Dart SDK 版本跟随 Flutter 版本走,不要单独混用旧版 Dart。
  3. 在 Flutter SDK 中执行flutter config --enable-openharmony(不同时期的适配版本命令可能略有不同),启用 OpenHarmony 构建支持。
  4. 在 IDE 中配置好 OpenHarmony SDK 的路径,确保命令行工具能识别到设备或模拟器。

提示:如果flutter doctor里 OpenHarmony 相关的检查项是红色叉号,大概率是 SDK 路径配置问题,也可能是版本不匹配导致的状态冲突。检查环境变量 PATH 里是否同时存在多个 Flutter 版本,这会导致引擎误判。

2.2 创建工程并配置 pubspec.yaml

环境就绪后,创建工程的流程和普通 Flutter 项目基本一致:

flutter create data_list_app

创建完成后,打开pubspec.yaml,在dependencies里添加需要的库:

dependencies: flutter: sdk: flutter dio: ^5.4.0 provider: ^6.1.1 pull_to_refresh: ^2.0.0

这里选库有个原则:优先选择纯 Dart 实现、不依赖 C++ 原生插件的库。以 Dio 为例,它底层是 socket 通信加 JSON 解析,完全走 Dart 虚拟机,不需要 OpenHarmony 侧额外适配原生代码。像pull_to_refresh这种 UI 组件库,也是纯 Flutter 渲染,即便在不同平台上,交互细节也能保持一致。

配置完依赖后,执行flutter pub get。如果网络环境不好,可以配置国内镜像源。但我要提醒一句:镜像源本身只影响包下载速度,不影响最终应用的运行逻辑。项目代码里不要写死任何镜像地址,那属于开发环境配置,不是应用逻辑。

工程目录结构我建议这样组织:

lib/ main.dart api/ http_client.dart data_api.dart models/ data_item.dart pages/ list_page.dart widgets/ loading_view.dart error_view.dart empty_view.dart state/ data_state.dart

这个结构本身不复杂,但它把“网络请求、数据模型、页面 UI、状态管理”四个关注点拆开了。后续加功能时,不会出现一个文件里堆着几百行代码的情况。

3. 网络层封装:用 DIO 把 HTTP 请求这件小事做扎实

3.1 为什么直接推荐 Dio,而不是用 http 包

很多新手会问:Flutter 自带的http包不也能发请求吗?为什么非得用 Dio?

说句实话,小项目用http完全没问题,但凡是涉及到“BaseURL 统一维护、请求头统一注入、异常统一处理、请求日志统一打印”这类工程化需求,http的写法就非常别扭——你必须在每个调用处重复写 try-catch、重复拼接 URL、重复塞 Headers。Dio 通过拦截器机制把这些横切逻辑集中到了配置层,业务代码只需要关心“发什么请求、拿到什么数据”。

另外还有一个实际因素:Dio 对取消请求、上传下载进度、表单构造等复杂场景的支持更成熟。数据清单应用虽然暂时只用 GET 请求,但后面如果要加“提交表单”“上传图片”这类功能,Dio 不用换库,直接在封装层加方法就行。

3.2 封装一个可复用的 Dio 实例

先看代码,再看解释。下面是一个最精简但足够工程用的 Dio 封装:

import 'package:dio/dio.dart'; class HttpClient { static late Dio _dio; static Dio get instance => _dio; static void init() { _dio = Dio( BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { // 这里可以做统一的日志打印、Token 注入 handler.next(options); }, onResponse: (response, handler) { // 统一解析响应,处理业务码 handler.next(response); }, onError: (DioException e, handler) { // 统一错误处理,比如网络超时、服务端异常 handler.next(e); }, ), ); } }

这段代码里有个容易忽视的点:connectTimeout和receiveTimeout一定要设置。OpenHarmony 设备在弱网环境下的表现会放大超时问题的严重性——如果不设置超时,一个 DNS 解析卡住可能就让整个页面转圈 30 秒以上,非常影响体验。10 秒是一个比较平衡的值,可以根据实际业务调整。

拦截器的写法要遵循handler.next()的调用规则。写日志、加 Token 这类操作放在onRequest里,在 next 之前完成;如果漏写next(),请求会永远停在拦截器里,这是个非常隐蔽的坑。

3.3 业务接口的定义方式

有了统一的 Dio 实例,业务接口的代码就非常薄了:

import 'package:dio/dio.dart'; import '../models/data_item.dart'; class DataApi { static Future<List<DataItem>> fetchDataList() async { final response = await HttpClient.instance.get('/api/data/list'); final list = response.data['data'] as List; return list.map((e) => DataItem.fromJson(e)).toList(); } }

这里的核心逻辑是把“HTTP 层的数据”转换成“业务层的数据模型”。不要直接在页面里写response.data['data'][0]['title']这样的代码,一旦接口结构调整,你需要在所有页面里找哪里用了['title'],那会是一场灾难。有了DataItem.fromJson这个转换层,改接口只需要动一个文件。

3.4 错误处理:别让异常打断用户操作

Dio 的异常类型有几种常见情况:连接超时、连接被拒绝、DNS 解析失败、HTTP 状态码错误。统一错误处理的核心思路是:把 DioException 转成用户能看懂的中文提示,而不是把原始异常堆栈丢给 UI。

String parseError(DioException e) { if (e.type == DioExceptionType.connectionTimeout) { return '连接超时,请检查网络后重试'; } else if (e.type == DioExceptionType.receiveTimeout) { return '响应超时,请稍后重试'; } else if (e.type == DioExceptionType.connectionError) { return '网络连接失败,请检查网络设置'; } else if (e.response != null) { return '服务异常(${e.response?.statusCode})'; } return '未知错误,请稍后重试'; }

这类提示文案甚至可以按平台差异化处理——OpenHarmony 上有些设备的中文系统字体渲染偏小,提示语太长会导致换行;理论上,错误提示最好是短句,控制在 12 个字以内,这样兼容性最好。当然,如果你做的是英文版应用,可以自动切换文案,这个看需求。

4. 数据清单页面的 UI 与状态管理实现

4.1 页面三态设计:加载中、加载失败、加载成功

一个合格的数据列表页面至少要处理三种状态:加载中(loading)、加载失败(error)、加载成功(success)。很多新手只写了 success 状态,结果网络一抖动,页面直接白屏。这里用 Provider 把状态管理的逻辑串起来。

先定义一个简单的状态类:

import 'package:flutter/foundation.dart'; import '../models/data_item.dart'; class DataState extends ChangeNotifier { List<DataItem> _items = []; bool _isLoading = false; String? _errorMessage; List<DataItem> get items => _items; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; Future<void> loadData() async { _isLoading = true; _errorMessage = null; notifyListeners(); try { _items = await DataApi.fetchDataList(); } catch (e) { _errorMessage = parseError(e as DioException); } finally { _isLoading = false; notifyListeners(); } } }

页面侧通过Consumer监听状态变化,根据状态渲染不同组件:

class ListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('数据清单')), body: Consumer<DataState>( builder: (context, state, child) { if (state.isLoading) { return LoadingView(); } if (state.errorMessage != null) { return ErrorView( message: state.errorMessage!, onRetry: () => state.loadData(), ); } if (state.items.isEmpty) { return EmptyView(); } return RefreshListView(items: state.items); }, ), ); } }

这一步其实已经解决了一个常见架构问题:UI 层永远只是状态的映射,不直接参与数据请求。这样做的好处是,列表页的 UI 组件可以单独测试,状态逻辑也可以单独测试,二者不耦合。

4.2 列表组件与下拉刷新

列表部分用的是ListView.builder,因为它按需构建 item,数据量大时内存表现更稳。配合pull_to_refresh库实现下拉刷新:

class RefreshListView extends StatelessWidget { final List<DataItem> items; const RefreshListView({required this.items}); @override Widget build(BuildContext context) { return SmartRefresher( controller: RefreshController(), onRefresh: () async { await context.read<DataState>().loadData(); }, child: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return Card( child: ListTile( title: Text(item.title), subtitle: Text(item.subtitle), trailing: Text(item.status), ), ); }, ), ); } }

这里一个重要的细节:onRefresh回调里必须 await 数据加载完成,否则刷新动画一结束列表数据还是旧的。我见过很多人在这个位置写了loadData()但不加 await,导致刷新完视觉上没变化,以为是网络问题。

4.3 数据模型定义与服务端字段对应

数据模型的字段一定要和服务端返回的 JSON 字段名保持严格一致。这里有个工程上的教训:如果服务端字段是蛇形命名(snake_case)比如item_name,而 Dart 端用的是驼峰,你需要在 fromJson 里做映射:

class DataItem { final String title; final String subtitle; final String status; DataItem({ required this.title, required this.subtitle, required this.status, }); factory DataItem.fromJson(Map<String, dynamic> json) { return DataItem( title: json['title'] ?? '', subtitle: json['subtitle'] ?? '', status: json['status'] ?? 'unknown', ); } }

注意?? ''这个兜底逻辑。服务端返回的数据里有些字段可能是 null,如果不做空值兜底,Item 构造时就会抛异常,整个列表渲染失败。实际开发中,宁可 UI 显示空白,也不要在解析层崩溃。这一条经验在 OpenHarmony 和 Android 上同样适用。

5. OpenHarmony 平台适配的五个关键细节

5.1 网络权限:默认是不开的

OpenHarmony 的应用权限模型和 Android 类似,但配置文件不同。如果你的应用需要访问网络,必须在module.json5或对应的权限配置文件中声明ohos.permission.INTERNET。漏了这一步,Dio 发出的请求会直接失败,而且报错可能看起来像“网络不可用”。

坦白说,这个问题排查起来非常坑:因为你从 Android 项目拷过来的代码默认是有网络权限的,但 OpenHarmony 工程拿到手不配权限就黑盒失败。建议在工程创建之后,第一件事就把权限声明加上,然后先写一个最简单的 GET 请求验证通联,再展开业务代码。

5.2 服务器地址与安全域名

OpenHarmony 对明文 HTTP 请求有比较严格的限制。如果你的接口是http://开头(而不是https://),需要在配置里显式打开“允许明文流量”的开关,或者在网络安全配置中把该域名加入白名单。否则请求会被系统拦截。

真实项目里我建议直接上 HTTPS,省去这些配置层面的麻烦,同时安全性也有了保障。如果你只是本地联调,可以使用http://10.0.2.2之类的模拟器地址,但记得在发布前替换为正式环境地址。

5.3 真机调试与设备连接

OpenHarmony 真机调试和 Android 大同小异,核心是通过 IDE 连接设备后用flutter run部署。区别在于,OpenHarmony 设备有时候需要手动开启“开发者模式”,并且在 IDE 中信任设备。

我在实际调试中遇到一个频发问题:flutter run在 OpenHarmony 设备上首次构建耗时特别长。这不是卡死,而是首次需要生成引擎产物、编译原生壳工程,正常情况下会在 3-5 分钟内完成。如果你 10 分钟还没动静,再检查一下设备连接状态和 IDE 的构建日志。

5.4 界面适配与字体缩放

OpenHarmony 设备常见的屏幕比例有两类:一类是手机竖屏(接近 9:16),一类是平板或者折叠屏(接近 3:4 或更方)。Flutter 的 LayoutBuilder 在处理这类差异时相对友好,但要注意在长列表里不要使用固定高度,否则平板设备上会出现大片空白或者遮挡。

字体缩放也是一个隐藏的大坑。某些鸿蒙设备会默认开启系统级字体缩放,如果应用没有适配MediaQuery.of(context).textScaler,布局会出现文字溢出。表格和卡片布局在字体缩放 1.3 倍以上时尤其容易裂掉。建议测试一下系统里“字体大小”调到最大的情况。

5.5 性能观察与调优

在 OpenHarmony 设备上做性能调优,核心关注两点:帧率和内存占用。在 Flutter 侧,用flutter run --profile模式观察性能,注意列表滚动的丢帧情况。如果出现卡顿,优先检查是不是列表项里做了重复的图片解码或者复杂布局计算。

一个常见问题是:列表项里如果用了Text控件且文字内容较长,默认的 Text 会进行自动换行计算,在滚动过程中会有性能开销。这种情况下可以给Text加maxLines和overflow: TextOverflow.ellipsis限制计算范围,实测能明显改善滚动流畅度。

6. 常见问题与排查技巧实录

6.1 问题速查表

我把实际跑项目中遇到的典型问题整理成了表格,每一个都是踩过的坑,直接对照排查能省不少时间。

问题描述可能原因排查方法
Dio 请求报 Connection refused服务端未启动;IP 地址不通先用系统浏览器访问接口地址验证连通性
请求能通但一直转圈无法刷新RefreshController 忘记刷新完成状态在 onRefresh 完成后调用controller.refreshCompleted()
接口返回了数据但列表空白字段名不匹配,解析失败被 catch 后静默忽略打印解析异常,核对 JSON 字段名
真机上 UI 字体溢出系统字体缩放导致 Text 区域不够使用 maxLines 或调整自适应布局
首次构建时间极长引擎产物生成 + 原生编译耐心等待,观察 IDE 构建日志
下拉刷新时列表数据闪一下状态刷新时 loading 状态触发了全屏 loading单独控制 refresh 状态,不要让 loadingView 覆盖列表

6.2 控制台日志怎么看

OpenHarmony 上调试 FLutter 应用,控制台日志会有两种来源:一种是 Flutter 侧的print/debugPrint输出,另一种是 OpenHarmony 系统侧的原生日志。建议在网络请求的关键节点都埋上debugPrint日志,因为 Dio 拦截器里的日志如果不开 verbose 级别,错误信息很容易被吞掉。

6.3 热重载的边界问题

Flutter 的热重载(hot reload)在 OpenHarmony 上是可用的,但改造幅度比在 Android 上受限。如果只是修改 UI 组件的布局代码,热重载没问题;但如果你改了pubspec.yaml、原生插件配置或者状态管理类的构造方式,热重载可能会崩溃或者状态错乱,这时候直接按R(hot restart)效果更干净。我个人的习惯是:改依赖、改模型、改拦截器配置这类结构性代码,全部用 hot restart 而不是 hot reload。

7. 从 Demo 到可交付:下一步扩展方向

数据清单应用跑通之后,这个项目其实有非常大的扩展空间。顺着这套架构,你可以往下加很多实际业务里绕不开的功能。

第一个方向是搜索与筛选。在现有DataState中增加query字段,调用接口时带上搜索参数,本质上就是给fetchDataList加一个 query 参数。这里需要注意:新接口返回的数据替换旧列表之前,最好清空原有列表并回到 loading 状态,否则会出现旧搜索词的结果闪烁一下再跳到新结果的观感问题。

第二个方向是分页加载。真实项目的列表不会一次性返回几百条数据,而是在用户滑动到底部时按页加载。这个功能在现有状态机上需要增加一个page属性和loadMore()方法,列表组件也要在 itemBuilder 里判断当前 index 是否接近列表底部。分页的难点不在请求本身,而在“加载更多、加载失败、已经全部加载完”这三个场景下,UI 分别要显示什么。我的经验是:底部放一个小组件,根据状态显示“加载中”“点击重试”“没有更多了”,比直接弹出 toast 提醒要稳。

第三个方向是数据缓存。用shared_preferences把上次成功的接口数据缓存到本地,启动时先渲染缓存数据,后台再发起请求更新,这样即使用户处在完全离线的状态,也能看到之前的清单列表。这在弱网环境下体验提升特别明显。不过要注意:缓存数据和实时数据不要混在一起显示,建议在页面顶部标注数据更新时间,给用户一个“当前数据可能不是最新”的心理预期。

第四个方向是详情页跳转。数据清单应用如果没有详情页,就像一个只有目录没有正文的书。Flutter 的导航传参逻辑在 OpenHarmony 上完全适用:用Navigator.push把DataItem对象传进详情页,详情页再根据 item 的 id 去请求更完整的接口数据。如果你要把这一环做扎实,接口设计上建议返回一个 item 的detailUrl字段,详情页用 WebView 渲染富文本内容,这样就不必为每一种数据格式单独开发排版组件了。

第五个方向是多主题适配。OpenHarmony 系统和 Android 一样,用户在系统设置中可以切换深色模式。Flutter 的ThemeData可以监听系统主题变化,但要注意:设置darkTheme的时候,列表项里手动指定的颜色值(比如某段文字写死Colors.black)可能会在深色模式下变得不可读。这里有个简单建议:所有颜色不写死,统一放进主题里,通过Theme.of(context).colorScheme取色。

8. 最后聊聊我在实操中的几点体会

整个 DAY3 项目做完,我最大的体会是:跨平台开发的真正难点,从来不是写业务代码,而是理解平台之间的差异如何影响最终的运行效果。Flutter + Dio 的组合让大部分业务代码保持了一致性,但 OpenHarmony 在权限声明、设备适配、系统字体缩放这些细节上,依然需要我们多花时间去验证和调整。

有个小技巧特别值得分享:在开发阶段,给 Dio 的拦截器开 verbose 级别的日志输出,并且把每一个请求的 URL、耗时、响应码都打出来。等整个项目的网络层稳定运行之后,再统一关掉这些日志,只保留异常信息输出。这样在定位问题的时候,就不用反复改代码加日志,省掉不少返工的时间。

另外一个建议是代码写完之后,抽时间用一部低配的 OpenHarmony 设备跑一遍全流程。开发机上高效的页面,低配设备不一定流畅;列表滚动是否掉帧、下拉刷新动画是否跟手、网络请求在弱网下超时是否及时,只有到真实设备上才能暴露出来。好的跨平台应用,不是在最强设备上跑得飞起,而是在最弱设备上依然可用。这条体验,无论是做 OpenHarmony 还是做其他平台,都同样成立。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询