Flutter + dio 适配开源鸿蒙实战:设备列表网络请求 + 动画 UI 优化全流程

关键词:Flutter、鸿蒙、OpenHarmony、dio、网络请求、设备列表、下拉刷新、上拉加载、搜索防抖、UI 动画、Flutter 鸿蒙适配
摘要
本文基于 Flutter for OpenHarmony 开源鸿蒙生态,使用 dio 实现网络请求封装,完成设备管理应用的完整功能:设备列表展示、下拉刷新、上拉加载、多条件搜索、设备详情页跳转。同时对 UI 进行深度优化:加入按压动画、页面过渡动画、组件化拆分、状态统一管理、缓存与防抖处理。最终代码通过 flutter analyze 检查,并成功构建 HAP 安装包,兼容鸿蒙设备。适合鸿蒙 Flutter 跨平台开发、网络封装、UI 优化学习。
一、功能概述
本项目已实现完整设备管理能力:

  1. 网络请求能力
    使用 dio 实现网络请求
    封装 ApiService,统一错误处理、缓存、重试机制
    支持:设备列表、设备详情接口
  2. 设备列表功能
    下拉刷新最新数据
    上拉加载更多分页
    加载中、加载失败、空数据状态统一处理
  3. 搜索功能
    支持按 设备名称 / IP / 设备 ID 搜索
    实时过滤列表
    搜索防抖(300ms)避免频繁请求
    一键清空搜索
  4. UI 与动画优化(本次重点)
    DeviceListItem 组件化
    卡片按压缩放动画(AnimatedContainer + Scale)
    详情页淡入 + 滑入动画(FadeTransition + SlideTransition)
    使用 GestureDetector 替代 ListTile,交互更灵活
    状态文字 + 状态图标:在线 / 离线
    统一圆角、间距、视觉层次
  5. 性能优化
    内存缓存减少重复请求
    搜索防抖优化性能
    页面销毁及时释放控制器,避免内存泄漏
    二、技术栈与依赖
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 参考模板。
运用实例
运用实例

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐