欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Flutter dio鸿蒙化实战指南

摘要:本文基于 LabManagementSystem 仓库的真实业务与接口结构,选择 pub.dev 上的 dio 作为网络层三方库,在 Flutter for OpenHarmony 场景下完成实验室列表与详情页的接入设计。文章重点不是环境安装,而是如何把一个已有的 Vue 3 + FastAPI + MySQL 业务系统平滑扩展到鸿蒙端,包括接口分析、dio 封装、模型映射、页面落地、常见问题排查,以及与 DevEco Studio 联调时的注意事项。结论是:对于当前仓库这种 REST 风格清晰、字段稳定的业务系统,dio 比直接手写 HttpClient 或简单 fetch 风格封装更适合作为 Flutter-OH 的网络基础设施。

作者信息

  • 作者:coco.D
  • 背景:本文依据仓库现有前后端代码撰写,核心接口、字段与业务流程均来自本仓库,不虚构后端能力。
  • 适用对象:希望将现有 Web 业务迁移到 Flutter for OpenHarmony / HarmonyOS 的开发者

为什么选 dio

当前仓库的前端位于 frontend/src/api/index.js,网络请求实现非常轻量,本质上是对 fetch 的简单封装,只覆盖了 3 个接口:

接口 作用 仓库现状
GET /api/health 健康检查 首页加载时调用
GET /api/labs 获取实验室列表 预约页加载时调用
GET /api/labs/{id} 获取单个实验室详情 已有后端支持

这套设计非常适合迁移到 Flutter for OpenHarmony,因为接口清晰、字段稳定、依赖少。问题在于,Web 端的 fetch 封装过于简单,到了鸿蒙端我们通常还需要:

  • 统一 baseUrl
  • 超时控制
  • 拦截器打印请求与响应
  • 业务异常统一处理
  • 未来追加 Token、重试、文件上传时保持扩展性

因此本文选择 pub.dev 上的 dio 作为三方库切入点。它本身不是鸿蒙专属库,但在 Flutter for OpenHarmony 项目中承担网络层这一类纯 Dart 能力非常合适,迁移成本也低。

先看仓库里已有的真实接口

为了避免“为了写鸿蒙而写鸿蒙”,我先从仓库后端抽取了实际可用的数据结构。

后端返回字段

backend/app/api/routes/labs.py 中的 _lab_to_dict() 明确给出了实验室接口字段:

def _lab_to_dict(lab: Lab):
    return {
        "id": lab.id,
        "name": lab.name,
        "location": lab.location,
        "capacity": lab.capacity,
        "status": lab.status,
    }

backend/app/models/lab.py 中对应的数据模型是:

字段 类型 含义
id Integer 主键
name String(128) 实验室名称
location String(128) 位置
capacity SmallInteger 容纳人数
status String(32) available/maintenance/closed

当前 Web 端调用方式

仓库已有 Web 前端这样调用接口:

export const api = {
  getHealth: () => request('GET', '/api/health'),
  getLabs: () => request('GET', '/api/labs'),
  getLab: (id) => request('GET', '/api/labs/' + id),
}

这给 Flutter-OH 迁移提供了很好的对照物。换句话说,我们不是重新定义后端协议,而是把同一套接口从 Vue 页迁移到鸿蒙端。

Flutter for OpenHarmony 接入步骤

下面的代码示例围绕一个最小可用目标展开:在鸿蒙设备或模拟器上拉取实验室列表,并支持查看详情。

1. 添加 dio 依赖

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.9.0

如果你的 Flutter-OH 项目已经接入了社区维护的 Flutter 三方库仓,可以优先从 AtomGit 对应仓库确认兼容状态;如果是纯 Dart 网络库,通常直接通过 pub.dev 集成即可。

2. 建立实验室模型

class Lab {
  const Lab({
    required this.id,
    required this.name,
    required this.location,
    required this.capacity,
    required this.status,
  });

  final int id;
  final String name;
  final String location;
  final int capacity;
  final String status;

  factory Lab.fromJson(Map<String, dynamic> json) {
    return Lab(
      id: json['id'] as int? ?? 0,
      name: json['name'] as String? ?? '',
      location: json['location'] as String? ?? '',
      capacity: json['capacity'] as int? ?? 0,
      status: json['status'] as String? ?? 'unknown',
    );
  }
}

这里没有加入仓库前端里那些演示用的 dept、图片等字段,原因很简单:后端真实返回里没有这些字段。鸿蒙化实践最容易踩的坑之一,就是 UI 先行导致字段假设失真。

3. 封装 dio 客户端

import 'package:dio/dio.dart';

class LabApiClient {
  LabApiClient(String baseUrl)
      : _dio = Dio(
          BaseOptions(
            baseUrl: baseUrl,
            connectTimeout: const Duration(seconds: 8),
            receiveTimeout: const Duration(seconds: 8),
            responseType: ResponseType.json,
            headers: const {
              'Content-Type': 'application/json',
            },
          ),
        ) {
    _dio.interceptors.add(
      LogInterceptor(
        requestBody: true,
        responseBody: true,
      ),
    );
  }

  final Dio _dio;

  Future<Map<String, dynamic>> getHealth() async {
    final response = await _dio.get('/api/health');
    return Map<String, dynamic>.from(response.data as Map);
  }

  Future<List<Lab>> getLabs() async {
    final response = await _dio.get('/api/labs');
    final data = Map<String, dynamic>.from(response.data as Map);
    final list = (data['data'] as List<dynamic>? ?? const []);
    return list
        .map((item) => Lab.fromJson(Map<String, dynamic>.from(item as Map)))
        .toList();
  }

  Future<Lab> getLabDetail(int id) async {
    final response = await _dio.get('/api/labs/$id');
    return Lab.fromJson(
      Map<String, dynamic>.from(response.data as Map),
    );
  }
}

这一层有两个实际价值:

  • 与仓库现有 fetch 封装形成一一对应,迁移成本低
  • 后续如果实验室预约提交接口上线,不需要重写网络基础设施

4. 在页面中消费接口

import 'package:flutter/material.dart';

class LabListPage extends StatefulWidget {
  const LabListPage({super.key, required this.apiClient});

  final LabApiClient apiClient;

  
  State<LabListPage> createState() => _LabListPageState();
}

class _LabListPageState extends State<LabListPage> {
  late Future<List<Lab>> _future;

  
  void initState() {
    super.initState();
    _future = widget.apiClient.getLabs();
  }

  Future<void> _refresh() async {
    final next = widget.apiClient.getLabs();
    setState(() {
      _future = next;
    });
    await next;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('实验室列表')),
      body: RefreshIndicator(
        onRefresh: _refresh,
        child: FutureBuilder<List<Lab>>(
          future: _future,
          builder: (context, snapshot) {
            if (snapshot.connectionState != ConnectionState.done) {
              return const Center(child: CircularProgressIndicator());
            }
            if (snapshot.hasError) {
              return ListView(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(24),
                    child: Text('加载失败:${snapshot.error}'),
                  ),
                ],
              );
            }

            final labs = snapshot.data ?? const <Lab>[];
            if (labs.isEmpty) {
              return ListView(
                children: const [
                  Padding(
                    padding: EdgeInsets.all(24),
                    child: Text('当前没有可预约实验室'),
                  ),
                ],
              );
            }

            return ListView.separated(
              itemCount: labs.length,
              separatorBuilder: (_, __) => const Divider(height: 1),
              itemBuilder: (context, index) {
                final lab = labs[index];
                return ListTile(
                  title: Text(lab.name),
                  subtitle: Text('${lab.location} · 容量 ${lab.capacity} 人'),
                  trailing: Text(lab.status),
                  onTap: () async {
                    final detail = await widget.apiClient.getLabDetail(lab.id);
                    if (!context.mounted) return;
                    showDialog<void>(
                      context: context,
                      builder: (_) => AlertDialog(
                        title: Text(detail.name),
                        content: Text(
                          '位置:${detail.location}\n'
                          '容量:${detail.capacity}\n'
                          '状态:${detail.status}',
                        ),
                      ),
                    );
                  },
                );
              },
            );
          },
        ),
      ),
    );
  }
}

如果你的目标是先跑通“看得见的功能”,这一版已经足够。它对齐了仓库里最核心的实验室列表能力,而且结构上保留了继续扩展预约提交、筛选、状态标记的空间。

5. 入口初始化

import 'package:flutter/material.dart';

void main() {
  const baseUrl = 'http://192.168.1.20:8000';
  final apiClient = LabApiClient(baseUrl);

  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: LabListPage(apiClient: apiClient),
    ),
  );
}

这里故意没有写成 http://127.0.0.1:8000。在 OpenHarmony 或 HarmonyOS 设备联调时,最常见的问题不是 dio 本身,而是宿主机网络地址配置错误。真机和模拟器访问本机后端时,通常需要改为宿主机局域网 IP,而不是直接写 localhost

与仓库现状对齐后的实践价值

这次鸿蒙化并不是“再做一个演示 App”,而是基于现有仓库抽出一条真正可复用的客户端接入路径。

对业务层的收益

  • 后端无需为鸿蒙端重写接口,现有 FastAPI 路由可直接复用
  • labs 表字段简单清晰,适合作为 Flutter-OH 首个联调样板
  • 当前 Web 端和鸿蒙端都围绕同一组 /api 路由工作,后期维护成本更低

对工程层的收益

  • Web 端继续保留 fetch,鸿蒙端使用 dio,两端职责分离但协议统一
  • dio 拦截器让设备侧问题定位更快,特别适合排查 404、500、超时、JSON 结构不匹配
  • 当前后端已启用 CORSMiddleware,浏览器跨域已放开;而 Flutter-OH 原生网络请求本身又不受浏览器同源策略限制,联调体验更顺畅

常见问题与解决思路

1. 页面能启动,但列表一直为空

先确认不是接口地址错误。仓库后端的实验室列表接口是 GET /api/labs,并且只返回 status == "available" 的实验室。如果数据库里状态被改成了 maintenanceclosed,接口会返回空数组,这并不一定是 Flutter 端故障。

2. dio 报连接超时

优先排查这 3 件事:

  1. FastAPI 是否真的启动在 8000 端口
  2. 鸿蒙设备是否能访问宿主机 IP
  3. baseUrl 是否写成了 localhost

这一类问题在迁移早期非常常见。很多时候我们会误以为是三方库兼容问题,实际上是设备网络路径没打通。

3. 详情接口能调,列表接口解析报错

仓库中两个接口的返回结构并不完全相同:

  • GET /api/labs 返回 { "data": [...] }
  • GET /api/labs/{id} 直接返回对象

如果你把两个接口都按同一种 JSON 结构解析,就会触发类型异常。这也是我建议单独封装 getLabs()getLabDetail() 的原因。

4. 与 DevEco Studio 怎么配合更高效

这部分我建议采用“双窗口联调”:

  • Flutter 侧负责热重载和 Dart 逻辑调试
  • DevEco Studio 侧负责看设备日志、运行态告警和鸿蒙工程配置

尤其是网络失败、权限提示、设备连接异常这类问题,单看 Flutter 控制台往往不够,DevEco Studio 的日志视图能补足很多上下文。

适配结论

回到本文主题,dio 这类来自 pub.dev 的 Flutter 三方库,并不需要因为目标平台换成 OpenHarmony / HarmonyOS 就被重新发明一次。只要它承担的是纯 Dart 层职责,并且你的 Flutter for OpenHarmony 工程本身能够正常编译运行,那么真正决定落地效果的,往往不是库能不能“装上”,而是你有没有把现有业务接口、返回结构、错误路径和联调方式梳理清楚。

LabManagementSystem 这个仓库来说,最适合的鸿蒙化起点不是复杂的预约提交流程,而是先把实验室列表与详情这条链路走通。这样做既能验证 Flutter-OH 网络层、JSON 解析和页面展示是否稳定,也能为后续接入登录、预约提交、设备申请等能力建立统一模板。

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐