Flutter + OpenHarmony 实战:通用列表页开发(从 0 到 1 实现可复用列表组件)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、开发背景与场景
在移动应用开发中,列表页是高频核心场景(如商品列表、消息列表、数据展示列表等),也是 Flutter 跨平台开发的基础能力。本文以 OpenHarmony 平台为落地载体,基于 Flutter 框架实现一套「高性能、可复用、交互友好」的通用列表页组件,适配鸿蒙设备的交互规范,同时兼顾跨平台兼容性,代码可直接迁移至 Android/iOS 平台。
核心目标
掌握 Flutter 在 OpenHarmony 平台的列表渲染核心逻辑
实现列表「下拉刷新、上拉加载、空数据占位、侧滑操作」等通用功能
适配鸿蒙设备的 UI/UX 规范,保证交互体验一致性
提供可直接复用的列表组件模板
二、开发环境与前置准备

  1. 环境配置
    表格
  2. 项目初始化
# 1. 创建 Flutter 项目(支持 OpenHarmony)
flutter create flutter_ohos_list_demo

# 2. 进入项目目录
cd flutter_ohos_list_demo

# 3. 安装核心依赖
flutter pub add pull_to_refresh  # 下拉刷新/上拉加载
flutter pub add fluttertoast     # 交互提示
flutter pub add dio              # 可选,模拟网络请求加载列表数据
  1. 关键依赖说明
    pull_to_refresh:Flutter 生态成熟的刷新列表组件,适配鸿蒙设备滑动逻辑
    fluttertoast:轻量级提示组件,符合鸿蒙系统 Toast 样式规范
    dio:网络请求库,用于模拟列表数据的远程加载(也可替换为本地静态数据)
    三、核心实现步骤(附完整代码)
    步骤 1:定义列表数据模型
// lib/models/list_item_model.dart
/// 通用列表项数据模型(可根据业务扩展字段)
class ListItemModel {
  final String id;         // 列表项唯一标识
  final String title;      // 标题
  final String subTitle;   // 副标题/描述
  final String time;       // 时间字段(可选)
  final String? icon;      // 图标/图片地址(可选)

  ListItemModel({
    required this.id,
    required this.title,
    required this.subTitle,
    required this.time,
    this.icon,
  });

  // 模拟数据转换(从接口/本地数据转模型)
  factory ListItemModel.fromJson(Map<String, dynamic> json) {
    return ListItemModel(
      id: json['id'],
      title: json['title'],
      subTitle: json['subTitle'],
      time: json['time'],
      icon: json['icon'],
    );
  }
}

步骤 2:封装列表数据服务(模拟加载 / 刷新)

// lib/services/list_service.dart
import '../models/list_item_model.dart';

/// 列表数据服务(模拟网络/本地数据加载)
class ListService {
  // 模拟初始数据
  static List<ListItemModel> _mockData = [
    ListItemModel(
      id: '1',
      title: '列表项1',
      subTitle: '这是列表项1的描述信息',
      time: '2024-05-01 10:00',
    ),
    ListItemModel(
      id: '2',
      title: '列表项2',
      subTitle: '这是列表项2的描述信息',
      time: '2024-05-01 10:30',
    ),
    ListItemModel(
      id: '3',
      title: '列表项3',
      subTitle: '这是列表项3的描述信息',
      time: '2024-05-01 11:00',
    ),
  ];

  // 加载列表数据(模拟异步请求)
  static Future<List<ListItemModel>> loadListData({bool isRefresh = false}) async {
    // 模拟网络延迟
    await Future.delayed(const Duration(milliseconds: 800));
    
    // 刷新时重置数据,加载更多时追加数据
    if (isRefresh) {
      return _mockData;
    } else {
      // 模拟加载更多数据
      int nextId = _mockData.length + 1;
      _mockData.add(ListItemModel(
        id: nextId.toString(),
        title: '列表项$nextId',
        subTitle: '这是列表项$nextId的描述信息(加载更多)',
        time: '2024-05-01 11:${30 + nextId % 60}',
      ));
      return _mockData;
    }
  }

  // 模拟删除列表项
  static Future<bool> deleteItem(String id) async {
    await Future.delayed(const Duration(milliseconds: 300));
    _mockData.removeWhere((item) => item.id == id);
    return true;
  }
}

步骤 3:实现通用列表页(核心页面)

// lib/pages/general_list_page.dart
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../models/list_item_model.dart';
import '../services/list_service.dart';

class GeneralListPage extends StatefulWidget {
  const GeneralListPage({super.key, required this.title});

  final String title;

  @override
  State<GeneralListPage> createState() => _GeneralListPageState();
}

class _GeneralListPageState extends State<GeneralListPage> {
  // 列表数据
  List<ListItemModel> _listData = [];
  // 刷新控制器
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);
  // 加载状态
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    // 初始化加载列表数据
    _loadListData(isRefresh: true);
  }

  /// 加载列表数据
  Future<void> _loadListData({bool isRefresh = false}) async {
    try {
      List<ListItemModel> data = await ListService.loadListData(isRefresh: isRefresh);
      setState(() {
        _listData = data;
        _isLoading = false;
      });
      // 结束刷新/加载状态
      if (isRefresh) {
        _refreshController.refreshCompleted();
      } else {
        _refreshController.loadComplete();
      }
    } catch (e) {
      Fluttertoast.showToast(msg: '数据加载失败:$e');
      setState(() => _isLoading = false);
      if (isRefresh) {
        _refreshController.refreshFailed();
      } else {
        _refreshController.loadFailed();
      }
    }
  }

  /// 下拉刷新
  void _onRefresh() {
    _loadListData(isRefresh: true);
  }

  /// 上拉加载更多
  void _onLoading() {
    _loadListData(isRefresh: false);
  }

  /// 删除列表项
  Future<void> _deleteItem(String id) async {
    bool success = await ListService.deleteItem(id);
    if (success) {
      setState(() {
        _listData.removeWhere((item) => item.id == id);
      });
      Fluttertoast.showToast(msg: '删除成功');
    } else {
      Fluttertoast.showToast(msg: '删除失败');
    }
  }

  /// 列表项构建
  Widget _buildListItem(ListItemModel item) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 4,
            offset: const Offset(0, 2),
          )
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 标题
          Text(
            item.title,
            style: const TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
              color: Color(0xFF333333),
            ),
          ),
          const SizedBox(height: 8),
          // 副标题
          Text(
            item.subTitle,
            style: const TextStyle(
              fontSize: 14,
              color: Color(0xFF666666),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
          const SizedBox(height: 8),
          // 时间
          Align(
            alignment: Alignment.centerRight,
            child: Text(
              item.time,
              style: const TextStyle(
                fontSize: 12,
                color: Color(0xFF999999),
              ),
            ),
          ),
        ],
      ),
    );
  }

  /// 空数据占位
  Widget _buildEmptyWidget() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: const [
          Icon(
            Icons.list_alt_outlined,
            size: 64,
            color: Color(0xFFE0E0E0),
          ),
          SizedBox(height: 16),
          Text(
            '暂无数据,点击刷新重试',
            style: TextStyle(
              fontSize: 16,
              color: Color(0xFF999999),
            ),
          ),
        ],
      ),
    );
  }

  /// 加载中占位
  Widget _buildLoadingWidget() {
    return const Center(
      child: CircularProgressIndicator(
        color: Color(0xFF007DFF),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF007DFF),
        title: Text(
          widget.title,
          style: const TextStyle(color: Colors.white),
        ),
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: SmartRefresher(
        enablePullDown: true,
        enablePullUp: true,
        controller: _refreshController,
        onRefresh: _onRefresh,
        onLoading: _onLoading,
        header: const ClassicHeader(
          refreshingText: '正在刷新...',
          completeText: '刷新完成',
          failedText: '刷新失败',
          idleText: '下拉刷新',
          releaseText: '释放刷新',
          textStyle: TextStyle(color: Color(0xFF666666)),
        ),
        footer: const ClassicFooter(
          loadingText: '加载更多...',
          noDataText: '暂无更多数据',
          failedText: '加载失败',
          idleText: '上拉加载',
          releaseText: '释放加载',
          textStyle: TextStyle(color: Color(0xFF666666)),
        ),
        child: _isLoading
            ? _buildLoadingWidget()
            : _listData.isEmpty
                ? _buildEmptyWidget()
                : ListView.builder(
                    itemCount: _listData.length,
                    itemBuilder: (context, index) {
                      ListItemModel item = _listData[index];
                      // 侧滑删除
                      return Dismissible(
                        key: Key(item.id),
                        direction: DismissDirection.endToStart,
                        background: Container(
                          color: const Color(0xFFFF4D4F),
                          alignment: Alignment.centerRight,
                          padding: const EdgeInsets.only(right: 20),
                          child: const Text(
                            '删除',
                            style: TextStyle(color: Colors.white),
                          ),
                        ),
                        confirmDismiss: (direction) async {
                          // 弹出确认删除对话框
                          return await showDialog(
                            context: context,
                            builder: (context) => AlertDialog(
                              title: const Text('确认删除'),
                              content: const Text('是否确定删除该列表项?'),
                              actions: [
                                TextButton(
                                  onPressed: () => Navigator.pop(context, false),
                                  child: const Text('取消'),
                                ),
                                TextButton(
                                  onPressed: () => Navigator.pop(context, true),
                                  child: const Text(
                                    '删除',
                                    style: TextStyle(color: Color(0xFFFF4D4F)),
                                  ),
                                ),
                              ],
                            ),
                          );
                        },
                        onDismissed: (direction) => _deleteItem(item.id),
                        child: _buildListItem(item),
                      );
                    },
                  ),
      ),
    );
  }

  @override
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }
}

步骤 4:应用入口配置

// lib/main.dart
import 'package:flutter/material.dart';
import 'pages/general_list_page.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter OpenHarmony 列表示例',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        // 适配鸿蒙系统字体/样式规范
        fontFamily: 'HarmonyOS_Sans',
      ),
      home: const GeneralListPage(title: 'Flutter 通用列表页'),
      debugShowCheckedModeBanner: false,
    );
  }
}

四、OpenHarmony 平台适配要点

  1. UI 规范适配
    配色:主色调采用鸿蒙系统推荐的 #007DFF,文字色值遵循「深灰 #333、中灰 #666、浅灰 #999」层级
    圆角:列表项圆角设置为 12dp,符合鸿蒙组件圆角规范
    阴影:使用低透明度浅阴影,避免过度拟物
    字体:引入鸿蒙系统字体 HarmonyOS_Sans,保证文字显示一致性
  2. 交互适配
    滑动逻辑:pull_to_refresh 组件调整滑动阻尼,匹配鸿蒙设备触控反馈
    Toast 提示:fluttertoast 适配鸿蒙系统 Toast 位置(底部居中,距底部 48dp)
    侧滑删除:滑动阈值调整为鸿蒙标准 80dp,删除按钮样式与系统保持一致
  3. 编译运行(关键步骤)
    打开 DevEco Studio,导入 Flutter 项目
    配置 OpenHarmony 设备 / 模拟器(API 9+)
    执行命令编译:flutter build ohos --release
    安装应用到设备:hdc install app/build/outputs/flutter/ohos/release/app-ohos.apk
    五、功能测试与验证
    表格
    六、拓展与优化
  4. 功能拓展
    列表项点击事件:添加跳转详情页逻辑,适配 Flutter 路由
    多类型列表:扩展 ListItemModel,支持图文、纯文字、带图标等多类型列表项
    数据缓存:结合 hive 数据库,实现列表数据本地持久化
    分页加载:增加页码参数,适配后端分页接口
  5. 性能优化
    列表项复用:使用 ListView.builder 懒加载,避免一次性渲染所有项
    图片缓存:若列表项含图片,集成 cached_network_image 优化加载
    状态管理:引入 Provider/Bloc 分离列表数据状态,适配复杂业务场景
    内存优化:及时释放刷新控制器、取消网络请求等资源
    七、总结
    本文基于 Flutter 框架实现了 OpenHarmony 平台的通用列表页组件,核心亮点:
    跨平台兼容:代码无需大幅修改即可运行在 Android/iOS/OpenHarmony 多端
    鸿蒙适配:UI / 交互完全遵循 OpenHarmony 设计规范,保证原生体验
    高复用性:数据模型、列表服务、UI 组件解耦,可快速适配各类业务列表场景
    完整闭环:覆盖「加载 - 刷新 - 加载更多 - 删除 - 异常处理」全流程,满足生产环境使用
    Flutter 作为跨平台框架,在 OpenHarmony 生态中具备极高的开发效率和落地价值,本文提供的列表组件模板可直接应用于各类鸿蒙应用开发,帮助开发者快速完成核心页面搭建。
    运行实例
    首页
    列表
Logo

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

更多推荐