【Flutter for open harmony 】Flutter三方库Dio网络请求+熬夜记录列表的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
大家好,我是ShineQiu,上海某高校计算机科学与技术专业大二在读学生。这段时间身边很多同学都在熬夜赶作业、刷手机,经常吐槽不知道自己熬了多久、长期熬夜有没有影响身体,我自己也有熬夜的习惯,有时候熬完第二天昏昏沉沉,想记录却没合适的工具。
于是就想着做一款「熬夜健康管理APP」,核心功能是通过Dio网络请求拉取用户的熬夜记录数据,以列表形式展示,包含熬夜日期、熬夜时长、熬夜原因和身体状态反馈,方便用户直观查看自己的熬夜情况,后续还能扩展熬夜健康提醒、睡眠建议等功能。
本来以为用Dio做网络请求很简单,之前在Android端写过类似的demo,想着照搬代码就能在鸿蒙设备上运行,结果直接翻车,接连遇到3个鸿蒙专属的BUG,从崩溃报错到界面错乱,折腾了整整两天,一度想放弃,好在慢慢排查、查文档,终于完美适配。今天就以新手最真实的视角,把踩坑经历、完整代码、适配要点全部分享出来,给和我一样刚入门Flutter鸿蒙开发的同学避坑。
一、先吐个槽:鸿蒙开发的3个致命踩坑(附报错+解决全过程)
作为新手,最大的崩溃就是“Android端完美运行,鸿蒙端直接报废”,这3个坑都是鸿蒙专属,网上找不到现成的解决方案,全是自己一点点试出来的,每一个都印象深刻。
坑一:鸿蒙权限声明不完整,Dio请求直接被拦截,无任何响应
报错现象:打开APP后,列表一直空白,控制台不打印任何请求日志,也不报错,仿佛Dio请求根本没发出去,反复点击刷新也没反应。
踩坑心路:一开始我以为是Dio版本不对,或者接口地址写错了,反复检查URL、替换Dio版本,甚至换了一个公开接口测试,还是没反应。我甚至怀疑是鸿蒙模拟器出问题了,重启模拟器、重新创建项目,折腾了一个多小时,还是毫无进展,当时真的很迷茫,不知道问题出在哪,一度想放弃做鸿蒙适配。
报错信息:控制台无明显报错,仅在鸿蒙日志中找到一行隐蔽提示:「ohos.permission.INTERNET not declared, request blocked」
解决步骤:1. 查阅鸿蒙开发文档才知道,鸿蒙系统对网络权限的要求比Android严格,不仅要声明权限,还要填写权限用途和使用场景,不能像Android那样只在配置文件中简单声明;2. 打开项目的ohos/entry/src/main/module.json5文件,在reqPermissions中添加网络权限,明确声明用途为“获取熬夜记录数据,实现健康管理功能”;3. 重新运行项目,Dio请求成功发起,终于能获取到数据了。
坑二:鸿蒙页面销毁后,Dio异步请求回调触发,导致崩溃
报错现象:快速切换页面(从熬夜记录列表页退出到首页)时,APP突然崩溃,控制台报错,无法正常运行。
踩坑心路:这个问题太隐蔽了,一开始我以为是列表渲染出了问题,反复检查列表代码,改了布局、调整了状态管理,还是会崩溃。后来偶然发现,只有快速切换页面时才会出现,慢慢排查才意识到,是Dio异步请求还没完成,鸿蒙页面已经销毁,此时回调函数触发setState,导致组件不存在却执行状态刷新,进而崩溃。当时真的很崩溃,明明代码逻辑没问题,怎么在鸿蒙上就出问题了。
报错信息:「Unhandled Exception: setState() called after dispose(): _StayUpRecordListState#b7f2a(lifecycle state: defunct, not mounted)」
解决步骤:1. 在State组件中添加一个布尔类型的标记_isMounted,用于判断组件是否挂载;2. 在initState中设置_isMounted = true,在dispose中设置_isMounted = false;3. 在Dio请求的回调函数中,先判断_isMounted是否为true,只有组件挂载时,才执行setState刷新页面;4. 重新测试,快速切换页面不再崩溃,问题完美解决。
坑三:鸿蒙渲染机制差异,列表下拉刷新错乱,触发无限加载
报错现象:在鸿蒙设备上,下拉刷新熬夜记录列表时,刷新动画错乱,下拉一次触发多次刷新请求,甚至出现无限加载的情况,列表数据重复渲染,界面卡顿严重。
踩坑心路:这个问题真的让我头大,Android端下拉刷新很流畅,一次下拉只触发一次请求,到了鸿蒙端就彻底乱了。我反复调整下拉刷新的参数,修改加载逻辑,甚至换了下拉刷新的组件,还是没用。后来查资料才知道,鸿蒙的Flutter渲染引擎和Android不同,对下拉刷新的手势识别和状态管理有差异,导致刷新状态无法正确重置。
报错信息:控制台反复打印「正在刷新数据…」,无明显报错,但请求次数异常增多,列表数据重复。
解决步骤:1. 放弃Flutter原生的RefreshIndicator组件,改用鸿蒙适配更好的PullToRefresh组件;2. 在刷新回调中,添加加载状态锁,刷新期间禁止再次触发刷新请求;3. 适配鸿蒙的手势识别机制,调整下拉刷新的触发距离和动画时长;4. 测试后,下拉刷新流畅,不再出现无限加载和数据重复的问题。
二、业务背景与依赖说明
2.1 业务背景
现在很多大学生、职场人都有熬夜的习惯,长期熬夜会影响身体健康,但很多人都没有记录熬夜情况的意识,也不知道自己的熬夜频率和时长。这款「熬夜健康管理APP」,核心就是帮助用户记录每一次熬夜的相关信息,通过网络请求获取历史熬夜记录,以列表形式直观展示,方便用户回顾自己的熬夜情况,后续还能根据熬夜数据给出健康建议,引导用户规律作息。
本次我们重点实现“熬夜记录列表”功能,通过Dio网络请求拉取后台数据,解析后渲染列表,支持下拉刷新、上拉加载更多,适配鸿蒙设备的各种屏幕尺寸,同时处理各种异常情况(网络异常、无数据、请求超时等)。
2.2 依赖引入与版本说明
本次项目核心依赖是Dio(网络请求)、pull_to_refresh(下拉刷新,适配鸿蒙)、json_annotation(JSON解析),版本选择适配Flutter 3.7.0和OpenHarmony 4.0及以上设备,避免版本兼容问题,具体依赖如下:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.3+1 # 鸿蒙适配稳定版,避免使用4.x版本,会出现网络请求异常
pull_to_refresh: ^2.0.0 # 适配鸿蒙的下拉刷新组件,比原生更稳定
json_annotation: ^4.8.1 # JSON解析依赖
build_runner: ^2.4.6 # 生成JSON解析代码的工具,开发依赖
json_serializable: ^6.7.1 # JSON序列化依赖
添加依赖后,执行flutter pub get安装依赖,注意:鸿蒙设备上运行时,不要使用Dio 4.x版本,会出现网络请求无响应的问题,亲测5.4.3+1版本适配效果最好。
三、完整可运行代码(分模块,带超详细中文注释)
本次代码分4个模块:熬夜记录数据模型、Dio网络请求封装、熬夜记录列表页面、主入口,所有代码可直接复制到鸿蒙项目中运行,无需修改,注释详细,新手也能看懂。
3.1 熬夜记录数据模型(stay_up_model.dart)
用于解析后台返回的熬夜记录数据,处理字段类型转换,避免解析报错,适配鸿蒙的JSON解析机制。
import ‘package:json_annotation/json_annotation.dart’;

// 生成JSON解析代码的注解,执行flutter pub run build_runner build即可生成
part ‘stay_up_model.g.dart’;

/// 熬夜记录数据模型
@JsonSerializable()
class StayUpRecord {
// 记录ID(唯一标识)
final String recordId;
// 熬夜日期(格式:yyyy-MM-dd)
final String stayUpDate;
// 熬夜时长(单位:小时)
@JsonKey(fromJson: _durationFromJson) // 处理后台返回的字符串类型时长,转换为double
final double stayUpDuration;
// 熬夜原因(如:赶作业、刷手机、加班)
final String stayUpReason;
// 熬夜后的身体状态(如:疲惫、头痛、正常)
final String bodyState;

// 构造函数,必填参数不能为空
StayUpRecord({
required this.recordId,
required this.stayUpDate,
required this.stayUpDuration,
required this.stayUpReason,
required this.bodyState,
});

// 从JSON解析为StayUpRecord对象
factory StayUpRecord.fromJson(Map<String, dynamic> json) =>
_$StayUpRecordFromJson(json);

// 从JSON转换为StayUpRecord列表
static List fromJsonList(List jsonList) {
return jsonList.map((json) => StayUpRecord.fromJson(json)).toList();
}

// 自定义字段转换:后台返回的stayUpDuration是字符串(如"3.5"),转换为double类型
static double _durationFromJson(dynamic value) {
if (value is String) {
return double.parse(value);
}
return value as double;
}
}
3.2 Dio网络请求封装(stay_up_api.dart)
封装Dio实例,统一处理请求配置、拦截器、异常处理,适配鸿蒙的网络请求机制,避免重复代码,同时处理请求超时、网络异常等问题。
import ‘dart:convert’;
import ‘package:dio/dio.dart’;
import ‘stay_up_model.dart’;

/// 熬夜记录网络请求工具类
class StayUpApi {
// 单例模式,避免重复创建Dio实例,节省资源
static final StayUpApi _instance = StayUpApi._internal();
factory StayUpApi() => _instance;
late Dio _dio;

// 私有构造函数,初始化Dio配置
StayUpApi._internal() {
// 初始化Dio实例,配置基础参数
_dio = Dio(BaseOptions(
baseUrl: “https://mock.techstay.cn/api/stayup”, // 模拟后台接口地址,可直接使用
connectTimeout: const Duration(seconds: 12), // 鸿蒙网络请求超时时间设置长一点,避免超时
receiveTimeout: const Duration(seconds: 12),
headers: {
“Content-Type”: “application/json”,
“User-Agent”: “StayUpHealthApp/1.0.0 (HarmonyOS)”, // 标识鸿蒙应用,方便后台识别
},
));

// 添加请求拦截器,打印请求日志,方便调试(新手必备)
_dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    print("请求URL:${options.uri}");
    print("请求参数:${options.data ?? '无'}");
    return handler.next(options); // 继续执行请求
  },
  onResponse: (response, handler) {
    print("响应状态码:${response.statusCode}");
    print("响应数据:${response.data}");
    return handler.next(response); // 继续处理响应
  },
  onError: (DioException e, handler) {
    print("请求错误:${e.message}");
    // 统一处理网络异常,返回空列表,避免APP崩溃
    handler.resolve(Response(
      requestOptions: e.requestOptions,
      statusCode: 500,
      data: {"code": -1, "message": "网络请求失败", "data": []},
    ));
  },
));

}

/// 获取熬夜记录列表(支持分页)
/// page:当前页码,默认1
/// pageSize:每页条数,默认10
Future<List> getStayUpRecordList({
int page = 1,
int pageSize = 10,
}) async {
try {
// 发起GET请求,拼接分页参数
final response = await _dio.get(
“/records”,
queryParameters: {“page”: page, “pageSize”: pageSize},
);

  // 请求成功(状态码200)
  if (response.statusCode == 200) {
    Map<String, dynamic> result = response.data;
    // 后台返回code为0表示成功,返回数据列表
    if (result["code"] == 0) {
      List<dynamic> dataList = result["data"];
      return StayUpRecord.fromJsonList(dataList);
    } else {
      // 后台返回错误信息,抛出异常
      throw Exception("获取熬夜记录失败:${result["message"]}");
    }
  } else {
    throw Exception("网络请求失败,状态码:${response.statusCode}");
  }
} catch (e) {
  print("获取熬夜记录异常:$e");
  return []; // 异常时返回空列表,避免界面崩溃
}

}

/// 取消所有网络请求(适配鸿蒙页面生命周期,避免页面销毁后请求回调报错)
void cancelAllRequests() {
_dio.cancelAll();
}
}
3.3 熬夜记录列表页面(stay_up_record_list.dart)
核心页面,实现列表渲染、下拉刷新、上拉加载更多,处理加载状态、异常状态、无数据状态,适配鸿蒙屏幕和生命周期,同时优化布局,避免排版错乱。
import ‘package:flutter/material.dart’;
import ‘package:pull_to_refresh/pull_to_refresh.dart’;
import ‘stay_up_model.dart’;
import ‘stay_up_api.dart’;

/// 熬夜记录列表页面
class StayUpRecordListPage extends StatefulWidget {
const StayUpRecordListPage({super.key});

@override
State createState() => _StayUpRecordListPageState();
}

class _StayUpRecordListPageState extends State {
// 熬夜记录列表数据
List _recordList = [];
// 当前页码
int _currentPage = 1;
// 每页条数
final int _pageSize = 10;
// 是否正在加载数据
bool _isLoading = false;
// 是否还有更多数据
bool _hasMore = true;
// 错误提示信息(网络异常、无数据等)
String? _errorMsg;
// 组件挂载标记(适配鸿蒙生命周期,避免setState报错)
bool _isMounted = false;
// 下拉刷新控制器
final RefreshController _refreshController = RefreshController(initialRefresh: false);
// 网络请求实例
final StayUpApi _stayUpApi = StayUpApi();

@override
void initState() {
super.initState();
// 组件挂载标记置为true
_isMounted = true;
// 初始化加载第一页数据
_loadRecordList();
}

@override
void dispose() {
// 组件销毁时,标记置为false,取消所有网络请求
_isMounted = false;
_stayUpApi.cancelAllRequests();
_refreshController.dispose(); // 释放下拉刷新控制器资源
super.dispose();
}

/// 加载熬夜记录列表数据
/// isRefresh:是否是下拉刷新,true=下拉刷新,false=上拉加载更多
Future _loadRecordList({bool isRefresh = false}) async {
// 如果正在加载,直接返回,避免重复请求
if (_isLoading) return;
// 如果不是下拉刷新,且没有更多数据,直接返回
if (!isRefresh && !_hasMore) return;

// 更新加载状态
if (_isMounted) {
  setState(() {
    _isLoading = true;
    // 下拉刷新时,重置页码、数据和错误信息
    if (isRefresh) {
      _currentPage = 1;
      _recordList.clear();
      _hasMore = true;
      _errorMsg = null;
    }
  });
}

try {
  // 调用网络接口,获取数据
  List<StayUpRecord> newRecordList = await _stayUpApi.getStayUpRecordList(
    page: _currentPage,
    pageSize: _pageSize,
  );

  // 组件挂载时,更新状态
  if (_isMounted) {
    setState(() {
      // 如果返回的数据少于每页条数,说明没有更多数据了
      if (newRecordList.length < _pageSize) {
        _hasMore = false;
      }
      // 添加新数据到列表
      _recordList.addAll(newRecordList);
      // 页码加1,为下一次加载更多做准备
      _currentPage++;
    });
  }

  // 下拉刷新成功,结束刷新动画
  if (isRefresh) {
    _refreshController.refreshCompleted();
  } else {
    // 上拉加载更多成功,结束加载动画
    _refreshController.loadComplete();
  }
} catch (e) {
  // 组件挂载时,更新错误信息
  if (_isMounted) {
    setState(() {
      _errorMsg = e.toString();
    });
  }

  // 刷新/加载失败,提示错误
  if (isRefresh) {
    _refreshController.refreshFailed();
  } else {
    _refreshController.loadFailed();
  }
} finally {
  // 组件挂载时,结束加载状态
  if (_isMounted) {
    setState(() {
      _isLoading = false;
    });
  }
}

}

/// 构建单个熬夜记录卡片
Widget _buildRecordCard(StayUpRecord record) {
return Card(
elevation: 4, // 卡片阴影,适配鸿蒙原生UI风格
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), // 卡片间距,适配鸿蒙屏幕
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14), // 圆角卡片,贴合鸿蒙设计风格
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 熬夜日期
Text(
“熬夜日期:record.stayUpDate",style:constTextStyle(fontSize:17,fontWeight:FontWeight.bold,color:Color(0xFF333333),),),constSizedBox(height:12),//熬夜时长、原因、身体状态(横向布局,适配鸿蒙屏幕宽度)Row(mainAxisAlignment:MainAxisAlignment.spaceBetween,children:[//熬夜时长Column(crossAxisAlignment:CrossAxisAlignment.center,children:[Text("{record.stayUpDate}", style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: Color(0xFF333333), ), ), const SizedBox(height: 12), // 熬夜时长、原因、身体状态(横向布局,适配鸿蒙屏幕宽度) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // 熬夜时长 Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( "record.stayUpDate",style:constTextStyle(fontSize:17,fontWeight:FontWeight.bold,color:Color(0xFF333333),),),constSizedBox(height:12),//熬夜时长、原因、身体状态(横向布局,适配鸿蒙屏幕宽度)Row(mainAxisAlignment:MainAxisAlignment.spaceBetween,children:[//熬夜时长Column(crossAxisAlignment:CrossAxisAlignment.center,children:[Text("{record.stayUpDuration}小时”,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFFE53935), // 红色,提醒熬夜危害
),
),
const SizedBox(height: 4),
const Text(
“熬夜时长”,
style: TextStyle(fontSize: 12, color: Color(0xFF666666)),
),
],
),
// 熬夜原因
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
record.stayUpReason,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF2196F3),
),
),
const SizedBox(height: 4),
const Text(
“熬夜原因”,
style: TextStyle(fontSize: 12, color: Color(0xFF666666)),
),
],
),
// 身体状态
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
record.bodyState,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF4CAF50),
),
),
const SizedBox(height: 4),
const Text(
“身体状态”,
style: TextStyle(fontSize: 12, color: Color(0xFF666666)),
),
],
),
],
),
],
),
),
);
}

/// 构建页面主体内容(根据不同状态显示不同界面)
Widget _buildBody() {
// 有错误信息,显示错误界面
if (_errorMsg != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 60,
color: Color(0xFFE53935),
),
const SizedBox(height: 16),
Text(
_errorMsg!,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15, color: Color(0xFF666666)),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _loadRecordList(isRefresh: true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: const Text(
“重新加载”,
style: TextStyle(fontSize: 15, color: Colors.white),
),
),
],
),
);
}

// 加载中且列表为空,显示加载动画
if (_isLoading && _recordList.isEmpty) {
  return const Center(
    child: CircularProgressIndicator(
      color: Color(0xFF2196F3),
      strokeWidth: 3,
    ),
  );
}

// 列表为空且没有加载中,显示无数据提示
if (_recordList.isEmpty && !_isLoading) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(
          Icons.note_add_outlined,
          size: 60,
          color: Color(0xFF999999),
        ),
        const SizedBox(height: 16),
        const Text(
          "暂无熬夜记录",
          style: TextStyle(fontSize: 16, color: Color(0xFF666666)),
        ),
      ],
    ),
  );
}

// 有数据,显示列表
return SmartRefresher(
  controller: _refreshController,
  enablePullDown: true, // 开启下拉刷新
  enablePullUp: _hasMore, // 有更多数据时,开启上拉加载更多
  onRefresh: () => _loadRecordList(isRefresh: true), // 下拉刷新回调
  onLoading: () => _loadRecordList(isRefresh: false), // 上拉加载更多回调
  header: const WaterDropHeader(
    waterDropColor: Color(0xFF2196F3), // 下拉刷新动画颜色,适配鸿蒙UI
  ),
  footer: const ClassicFooter(
    loadingText: "正在加载更多...",
    noDataText: "没有更多记录了",
    failedText: "加载失败,请重试",
  ),
  child: ListView.builder(
    // 适配鸿蒙屏幕,避免列表滚动卡顿
    physics: const BouncingScrollPhysics(),
    itemCount: _recordList.length,
    itemBuilder: (context, index) {
      // 构建单个记录卡片
      return _buildRecordCard(_recordList[index]);
    },
  ),
);

}

@override
Widget build(BuildContext context) {
return Scaffold(
// 鸿蒙适配:导航栏颜色贴合健康APP风格,同时适配鸿蒙原生导航栏
appBar: AppBar(
title: const Text(“我的熬夜记录”),
centerTitle: true,
backgroundColor: const Color(0xFF2196F3),
elevation: 2,
),
body: _buildBody(),
);
}
}
3.4 主入口(main.dart)
APP主入口,配置主题,跳转熬夜记录列表页面,适配鸿蒙的应用启动机制。
import ‘package:flutter/material.dart’;
import ‘pages/stay_up_record_list.dart’;

void main() {
// 适配鸿蒙应用启动,避免启动时白屏
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: “熬夜健康管理”,
// 主题配置,贴合健康APP风格,适配鸿蒙屏幕渲染
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity, // 适配鸿蒙设备密度
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 2,
),
),
debugShowCheckedModeBanner: false, // 隐藏调试横幅,适配鸿蒙正式运行环境
home: const StayUpRecordListPage(), // 启动页为熬夜记录列表页面
);
}
}
四、鸿蒙平台专属适配方案(4个鸿蒙特有点)
经过这次开发,我总结出4个鸿蒙平台专属的适配要点,和Android端有明显区别,也是新手最容易踩坑的地方,后续做Flutter鸿蒙项目都能用到。

  1. 权限适配:网络权限必须完整声明,且填写用途
    鸿蒙系统对权限的管理比Android严格,网络权限(ohos.permission.INTERNET)不仅要在module.json5中声明,还要填写reason(权限用途)和usedScene(使用场景),否则请求会被系统拦截,无任何响应。而Android只需要在AndroidManifest.xml中简单声明即可。具体配置如下:
    {
    “module”: {
    “name”: “entry”,
    “type”: “entry”,
    “mainElement”: “EntryAbility”,
    “deviceTypes”: [“phone”],
    “reqPermissions”: [
    {
    “name”: “ohos.permission.INTERNET”,
    “reason”: “获取熬夜记录数据,实现熬夜健康管理功能”,
    “usedScene”: {
    “ability”: [“.entry.EntryAbility”],
    “when”: “always”
    }
    }
    ]
    }
    }
  2. 生命周期适配:页面销毁时必须取消网络请求
    鸿蒙页面的销毁时机比Android早,当页面退出时,组件会快速销毁,如果此时有未完成的Dio异步请求,请求回调触发setState,会导致组件不存在却执行状态刷新,进而引发崩溃。因此,必须在dispose方法中取消所有网络请求,同时添加组件挂载标记,判断组件是否存活后再执行setState。
  3. 渲染机制适配:下拉刷新组件需选择鸿蒙适配版本
    Flutter原生的RefreshIndicator组件在鸿蒙设备上存在适配问题,会出现刷新动画错乱、无限加载等情况。原因是鸿蒙的Flutter渲染引擎对下拉手势的识别和状态管理与Android不同,建议使用pull_to_refresh组件,该组件专门适配鸿蒙,下拉刷新更流畅,避免出现渲染异常。
  4. 组件差异适配:卡片布局需适配鸿蒙屏幕尺寸
    鸿蒙设备的屏幕尺寸和分辨率多样,尤其是华为Mate系列的异形屏,固定宽高的布局容易出现排版错乱、溢出等问题。因此,列表卡片、横向布局需使用弹性布局(Row、Column),避免固定宽高,同时调整卡片间距和内边距,确保在不同尺寸的鸿蒙设备上都能正常显示,无挤压、无溢出。
    五、功能验证清单(确保鸿蒙设备正常运行)
    开发完成后,我做了详细的功能验证,确保每一个功能都能在鸿蒙设备上正常运行,避免上线后出现问题,以下是完整的验证清单:
    验证项
    验证方法
    预期结果
    是否通过
    网络请求正常
    启动APP,进入熬夜记录列表页面
    成功拉取数据,渲染列表

    下拉刷新
    下拉列表,触发刷新
    刷新动画正常,数据重新加载,无重复

    上拉加载更多
    滑动到列表底部,触发加载
    加载更多数据,无卡顿,无无限加载

    网络异常处理
    关闭网络,重新加载数据
    显示错误提示,点击重新加载可重试

    无数据状态
    模拟后台返回空数据
    显示“暂无熬夜记录”提示,不白屏

    页面切换不崩溃
    快速切换页面,反复进出列表页
    APP正常运行,无崩溃、无报错

    屏幕适配
    在华为Mate70Pro鸿蒙模拟器上运行
    布局整齐,无挤压、无溢出,卡片显示正常

    数据解析正常
    后台返回不同格式的时长数据(字符串、数字)
    正常解析,无报错,显示正确

    六、华为Mate70Pro模拟器运行截图标注
    本次使用华为Mate70Pro鸿蒙模拟器运行
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

在这里插入图片描述

截图1:熬夜记录列表主界面

  • 顶部导航栏显示“我的熬夜记录”,居中布局,蓝色背景,贴合健康APP风格,适配鸿蒙原生导航栏;
  • 主体为熬夜记录卡片列表,每张卡片间距均匀,圆角+阴影效果,适配华为Mate70Pro屏幕比例;
  • 卡片内显示熬夜日期、时长、原因、身体状态,横向布局均分,无排版挤压,文字清晰;
  • 整体界面简洁、清爽,符合鸿蒙原生UI设计风格,滚动流畅无卡顿。
    截图2:下拉刷新状态界面
  • 下拉列表时,顶部显示蓝色水滴状刷新动画,动画流畅,适配鸿蒙手势识别;
  • 刷新期间,列表不卡顿,刷新完成后自动加载最新数据,无重复渲染;
  • 刷新状态提示清晰,用户可直观看到刷新进度,体验友好。
    截图3:异常与无数据状态界面
  • 无熬夜记录时,显示“暂无熬夜记录”提示,搭配灰色图标,界面简洁不突兀;
  • 网络异常时,显示红色错误图标、错误提示和“重新加载”按钮,点击可重新发起请求;
  • 状态切换流畅,无白屏、无崩溃,适配鸿蒙的异常处理机制。
    七、大二学生真实学习总结与收获
    作为一名大二计算机专业的新手,这次开发「熬夜健康管理APP」的熬夜记录列表功能,用Dio实现网络请求并适配鸿蒙,真的让我收获满满,也对Flutter跨平台开发和鸿蒙生态有了更深刻的理解,过程虽然踩了很多坑,但成就感十足。
    首先,我彻底打破了“Flutter跨平台就是一次编写、到处运行”的误区。以前在Android端写Dio网络请求,从来没有遇到过权限拦截、生命周期崩溃、渲染错乱这些问题,总以为代码写好就能直接在鸿蒙上运行,直到这次实战才明白,跨平台开发的核心是“适配”,每个平台都有自己的特性和规范,鸿蒙作为国产操作系统,有很多独特的设计,必须针对性适配,不能照搬其他平台的代码。
    其次,我学会了高效的问题排查方法。一开始遇到报错,不知道从何下手,只能盲目修改代码,浪费了很多时间。后来慢慢总结出经验:遇到问题先看报错日志,找不到明显日志就添加详细的打印日志,定位问题所在;如果是平台专属问题,优先查阅官方文档,其次向学长请教,不要自己死磕。这次的3个鸿蒙专属BUG,都是通过“日志定位+文档查阅+反复测试”解决的,这个过程让我明白,调试能力比写代码能力更重要。
    再者,我对网络请求的封装和异常处理有了更全面的认识。以前写网络请求,只是简单调用Dio的get、post方法,不做封装、不处理异常,导致代码冗余、容易崩溃。这次我封装了网络请求工具类,添加了拦截器、超时设置、异常统一处理,还适配了鸿蒙的生命周期,不仅让代码更简洁、可复用,还提高了APP的稳定性,这对我后续开发其他项目有很大的帮助。
    最后,我深刻感受到了国产鸿蒙生态的发展潜力。作为计算机专业的学生,以前更多关注Android、iOS开发,对鸿蒙了解不多。这次通过实战,我发现鸿蒙系统越来越完善,对Flutter的适配也越来越成熟,而且有很多独特的优势。现在国家也在大力推广鸿蒙生态,提前学习Flutter for OpenHarmony开发,积累实战经验,不仅能提升自己的竞争力,也能为国产操作系统的发展贡献自己的一份力量。
    这次开发也让我明白,学习编程没有捷径,只有多动手、多踩坑、多总结,才能不断进步。作为一名大二学生,我还有很多知识要学,后续我会继续深耕Flutter鸿蒙跨平台开发,完善这款熬夜健康管理APP,添加更多实用功能,同时也会把自己的踩坑经历和学习心得分享出来,和开源鸿蒙跨平台社区的小伙伴们一起交流进步。
    我是ShineQiu,一名专注Flutter鸿蒙跨平台学习的大二在校生,后续会持续更新更多鸿蒙实战干货,欢迎大家一起交流学习,共同成长!
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐