Flutter + dio 适配开源鸿蒙实战:设备列表网络请求 + 动画 UI 优化全流程
摘要:本文基于Flutter for OpenHarmony生态,采用dio实现网络请求封装,构建完整的设备管理应用。项目包含设备列表展示、下拉刷新、上拉加载、多条件搜索和详情页跳转功能,并针对UI进行了动画优化(按压缩放、页面过渡等)和性能优化(缓存、防抖处理)。通过组件化设计和状态管理,实现了流畅的用户体验,最终成功构建兼容鸿蒙设备的HAP安装包,为Flutter跨平台开发提供实践参考。
Flutter + dio 适配开源鸿蒙实战:设备列表网络请求 + 动画 UI 优化全流程
关键词:Flutter、鸿蒙、OpenHarmony、dio、网络请求、设备列表、下拉刷新、上拉加载、搜索防抖、UI 动画、Flutter 鸿蒙适配
摘要
本文基于 Flutter for OpenHarmony 开源鸿蒙生态,使用 dio 实现网络请求封装,完成设备管理应用的完整功能:设备列表展示、下拉刷新、上拉加载、多条件搜索、设备详情页跳转。同时对 UI 进行深度优化:加入按压动画、页面过渡动画、组件化拆分、状态统一管理、缓存与防抖处理。最终代码通过 flutter analyze 检查,并成功构建 HAP 安装包,兼容鸿蒙设备。适合鸿蒙 Flutter 跨平台开发、网络封装、UI 优化学习。
一、功能概述
本项目已实现完整设备管理能力:
- 网络请求能力
使用 dio 实现网络请求
封装 ApiService,统一错误处理、缓存、重试机制
支持:设备列表、设备详情接口 - 设备列表功能
下拉刷新最新数据
上拉加载更多分页
加载中、加载失败、空数据状态统一处理 - 搜索功能
支持按 设备名称 / IP / 设备 ID 搜索
实时过滤列表
搜索防抖(300ms)避免频繁请求
一键清空搜索 - UI 与动画优化(本次重点)
DeviceListItem 组件化
卡片按压缩放动画(AnimatedContainer + Scale)
详情页淡入 + 滑入动画(FadeTransition + SlideTransition)
使用 GestureDetector 替代 ListTile,交互更灵活
状态文字 + 状态图标:在线 / 离线
统一圆角、间距、视觉层次 - 性能优化
内存缓存减少重复请求
搜索防抖优化性能
页面销毁及时释放控制器,避免内存泄漏
二、技术栈与依赖
dio: ^5.4.0
pull_to_refresh: ^2.0.0
状态管理:setState
平台:开源鸿蒙 OpenHarmony
构建产物:HAP 安装包
三、核心代码实现
3.1 ApiService 网络封装(含缓存 + 错误处理)
import 'package:dio/dio.dart';
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
late Dio dio;
final Map<String, dynamic> _cache = {};
final Duration _cacheDuration = Duration(minutes: 5);
ApiService._internal() {
dio = Dio(BaseOptions(
baseUrl: "https://xxx.com/api",
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
dio.interceptors.add(LogInterceptor(responseBody: true));
}
Future<List<dynamic>> getDeviceList({int page = 1}) async {
final key = "device_list_$page";
if (_cache.containsKey(key)) {
var cache = _cache[key];
if (DateTime.now().difference(cache['time']) < _cacheDuration) {
return cache['data'];
}
}
try {
final res = await dio.get("/devices", queryParameters: {"page": page});
var data = res.data['list'] ?? [];
_cache[key] = {"data": data, "time": DateTime.now()};
return data;
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<Map<String, dynamic>> getDeviceDetail(String id) async {
try {
final res = await dio.get("/devices/$id");
return res.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
Exception _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return Exception("网络连接超时");
case DioExceptionType.receiveTimeout:
return Exception("服务器响应超时");
case DioExceptionType.badResponse:
return Exception("服务器异常 ${e.response?.statusCode}");
default:
return Exception("网络错误:${e.message}");
}
}
}
3.2 设备列表页(刷新 + 加载 + 搜索)
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class DeviceListPage extends StatefulWidget {
@override
_DeviceListPageState createState() => _DeviceListPageState();
}
class _DeviceListPageState extends State<DeviceListPage> {
final RefreshController _refreshController = RefreshController();
final ApiService _api = ApiService();
final TextEditingController _searchCtrl = TextEditingController();
List<dynamic> _list = [];
List<dynamic> _filterList = [];
int _page = 1;
bool _hasMore = true;
bool _loading = false;
@override
void initState() {
super.initState();
_loadData();
_searchCtrl.addListener(_onSearchChange);
}
void _onSearchChange() {
Future.delayed(Duration(milliseconds: 300), () {
if (!mounted) return;
String key = _searchCtrl.text.trim().toLowerCase();
setState(() {
_filterList = _list.where((d) {
return d['name'].toLowerCase().contains(key) ||
d['ip'].toLowerCase().contains(key) ||
d['id'].toString().contains(key);
}).toList();
});
});
}
Future<void> _loadData({bool refresh = false}) async {
if (_loading) return;
setState(() => _loading = true);
try {
if (refresh) {
_page = 1;
_list.clear();
}
var data = await _api.getDeviceList(page: _page);
setState(() {
_list.addAll(data);
_filterList = List.from(_list);
_hasMore = data.length >= 10;
_page++;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
} finally {
setState(() => _loading = false);
refresh ? _refreshController.refreshCompleted() : _refreshController.loadComplete();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("设备列表")),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(8),
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
hintText: "搜索名称/IP/ID",
prefixIcon: Icon(Icons.search),
suffixIcon: _searchCtrl.text.isNotEmpty
? IconButton(icon: Icon(Icons.clear), onPressed: _searchCtrl.clear)
: null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
Expanded(
child: SmartRefresher(
controller: _refreshController,
enablePullUp: _hasMore,
onRefresh: () => _loadData(refresh: true),
onLoading: _loadData,
child: _buildList(),
),
),
],
),
);
}
Widget _buildList() {
if (_loading && _list.isEmpty) return Center(child: CircularProgressIndicator());
if (_filterList.isEmpty) return Center(child: Text("暂无数据"));
return ListView.builder(
itemCount: _filterList.length,
itemBuilder: (ctx, i) {
return DeviceListItem(
device: _filterList[i],
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DeviceDetail(id: _filterList[i]['id'])),
);
},
);
},
);
}
@override
void dispose() {
_searchCtrl.dispose();
_refreshController.dispose();
super.dispose();
}
}
3.3 设备列表项(带按压动画)
class DeviceListItem extends StatefulWidget {
final Map<String, dynamic> device;
final VoidCallback onTap;
const DeviceListItem({required this.device, required this.onTap});
@override
_DeviceListItemState createState() => _DeviceListItemState();
}
class _DeviceListItemState extends State<DeviceListItem> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _scale;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: Duration(milliseconds: 200));
_scale = Tween(begin: 1.0, end: 0.95).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _ctrl.forward(),
onTapUp: (_) => _ctrl.reverse(),
onTapCancel: () => _ctrl.reverse(),
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _scale,
builder: (_, child) => Transform.scale(scale: _scale.value, child: child),
child: Card(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.devices, color: Colors.blue, size: 32),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.device['name'] ?? "", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text("IP: ${widget.device['ip']} ID: ${widget.device['id']}"),
],
),
),
Column(
children: [
Icon(
widget.device['status'] == "online" ? Icons.check_circle : Icons.error,
color: widget.device['status'] == "online" ? Colors.green : Colors.red,
),
Text(
widget.device['status'] == "online" ? "在线" : "离线",
style: TextStyle(fontSize: 12, color: widget.device['status'] == "online" ? Colors.green : Colors.red),
),
],
),
],
),
),
),
),
);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
}
3.4 设备详情页(页面动画)
class DeviceDetail extends StatefulWidget {
final String id;
const DeviceDetail({required this.id});
@override
_DeviceDetailState createState() => _DeviceDetailState();
}
class _DeviceDetailState extends State<DeviceDetail> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _fade;
late Animation<Offset> _slide;
final ApiService _api = ApiService();
Map<String, dynamic>? _data;
bool _loading = true;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: Duration(milliseconds: 500));
_fade = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
_slide = Tween(begin: Offset(0, 0.2), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
_load();
}
Future<void> _load() async {
try {
_data = await _api.getDeviceDetail(widget.id);
setState(() => _loading = false);
_ctrl.forward();
} catch (e) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("设备详情")),
body: _loading
? Center(child: CircularProgressIndicator())
: FadeTransition(
opacity: _fade,
child: SlideTransition(
position: _slide,
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
children: [
_item("名称", _data!['name']),
_item("ID", _data!['id']),
_item("IP", _data!['ip']),
_item("状态", _data!['status']),
],
),
),
),
),
);
}
Widget _item(String label, String value) {
return Card(
margin: EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(label), Text(value, style: TextStyle(fontWeight: FontWeight.bold))],
),
),
);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
}
四、鸿蒙平台适配要点
网络权限(module.json5)
"requestPermissions": [
{"name": "ohos.permission.INTERNET"}
]
列表性能
防抖搜索避免频繁 rebuild
内存缓存减少网络请求
控制器及时 dispose
动画兼容
Flutter 原生动画在鸿蒙引擎表现稳定
避免过度叠加动画导致掉帧
五、验证结果
✅ flutter analyze 无警告无错误
✅ 成功构建 HAP 安装包
✅ 下拉刷新 / 上拉加载 / 搜索 / 详情正常
✅ 动画流畅,无内存泄漏
✅ 兼容开源鸿蒙设备
六、总结
本文完整实现:
dio 网络封装(错误处理、缓存、重试)
设备列表 + 刷新 + 加载 + 搜索防抖
组件化 + 动画 UI 优化(按压、页面过渡)
鸿蒙平台兼容 + HAP 构建成功
适合作为 Flutter 鸿蒙入门、网络请求实战、UI 动画优化、设备管理类 App 参考模板。
运用实例
更多推荐




所有评论(0)