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


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的旅行行李打包进度应用。项目中使用 Flutter 第三方库 percent_indicator 展示行李打包完成度,通过圆形进度条和线性进度条直观显示不同分类的准备情况。

最终运行效果如下:

在这里插入图片描述

页面主要包含以下内容:

  • 顶部标题栏;
  • 总体打包完成度圆形进度条;
  • 证件、衣物、电子设备、洗护用品分类进度条;
  • 行李物品勾选列表;
  • 一键重置按钮;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

本文重点是演示如何在 Flutter for OpenHarmony 项目中使用 Flutter 第三方库 percent_indicator。项目代码写在 lib/main.dart 中,依赖配置写在 pubspec.yaml 中,符合 Flutter for OpenHarmony 第三方库实践方向。


前言

在移动应用开发中,进度展示是非常常见的功能。比如任务完成度、下载进度、学习进度、运动目标、预算使用情况、资料填写进度等,都可以通过进度条来展示。

如果只使用文字展示:

已完成 7 / 12

虽然也能表达信息,但不够直观。进度条可以让用户一眼看出当前完成情况,这就是 UI 存在的意义:减少人类脑子继续加班。脑子已经很累了,别让它连百分比都要自己换算。

本文选择使用 Flutter 第三方库 percent_indicator 来实现进度展示。它提供了圆形进度条和线性进度条组件,可以快速构建完成度类页面。

本项目以“旅行行李打包进度应用”为例,使用 percent_indicator 展示总体进度和分类进度,并结合 Flutter 状态更新实现物品勾选功能。


一、项目目标

本次实践主要实现以下目标:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 percent_indicator
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 percent_indicator
  • 使用 CircularPercentIndicator 展示总体完成度;
  • 使用 LinearPercentIndicator 展示分类完成度;
  • 使用 Flutter Material 组件构建页面;
  • 实现行李物品勾选和进度自动更新;
  • 实现一键重置功能;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 percent_indicator
功能场景 进度展示 / 行李清单
核心组件 CircularPercentIndicator / LinearPercentIndicator
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 percent_indicator

在实际应用中,进度展示需求非常多。例如:

  • 旅行行李打包进度;
  • 资料填写完成度;
  • 健身目标完成度;
  • 课程学习进度;
  • 项目任务进度;
  • 下载进度;
  • 问卷填写进度;
  • 每日习惯完成度。

如果完全自己使用 Flutter 原生组件绘制进度条,需要处理百分比、动画、圆角、中心文字、颜色、尺寸等细节。

percent_indicator 已经封装好了常用进度条组件,可以让开发者更快完成进度类页面。

在本项目中,percent_indicator 主要完成以下工作:

  • 绘制总体圆形进度条;
  • 绘制分类线性进度条;
  • 显示进度百分比;
  • 展示进度动画效果;
  • 提升页面数据的可读性。

四、创建 Flutter for OpenHarmony 项目

在已经配置好 Flutter for OpenHarmony 开发环境的前提下,可以创建一个 Flutter 项目。

示例项目名称:

flutter create packing_progress_demo

进入项目目录:

cd packing_progress_demo

项目创建完成后,主要关注两个文件:

packing_progress_demo
 ├── pubspec.yaml
 └── lib
     └── main.dart

其中:

文件 作用
pubspec.yaml 配置 Flutter 项目依赖
lib/main.dart 编写 Flutter 页面和业务逻辑

五、添加 percent_indicator 第三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中添加 percent_indicator

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  percent_indicator: ^4.2.5

完整结构大致如下:

name: packing_progress_demo
description: A Flutter for OpenHarmony percent indicator demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  percent_indicator: ^4.2.5

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

添加完成后,在终端执行:

flutter pub get

执行成功后,就可以在 Dart 代码中使用 percent_indicator 了。


六、项目结构

本项目主要修改 lib/main.dart 文件:

lib
 └── main.dart

本项目不需要编写 OpenHarmony 原生 ArkTS 页面,也不需要修改 Index.ets

因为这是 Flutter for OpenHarmony 项目,页面主体应该是 Flutter 代码。审核重点会看:

  • 是否使用 pubspec.yaml 添加 Flutter 第三方库;
  • 是否在 Dart 文件中 import package
  • 是否在 lib/main.dart 中实际调用第三方库;
  • 是否属于 Flutter for OpenHarmony 项目。

这次别再碰 ArkTS。不是 ArkTS 不好,是这题不考它。考试让写作文,你交数学证明,再优雅也会被叉掉,残酷但合理。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 percent_indicator
  2. main.dart 中引入第三方库;
  3. 定义行李物品数据模型;
  4. 按分类统计已完成数量和总数量;
  5. 计算总体打包完成百分比;
  6. 使用 CircularPercentIndicator 展示总体进度;
  7. 使用 LinearPercentIndicator 展示分类进度;
  8. 使用 CheckboxListTile 实现物品勾选;
  9. 勾选状态变化后使用 setState() 刷新页面。

第三方库引入代码如下:

import 'package:percent_indicator/percent_indicator.dart';

圆形进度条核心代码如下:

CircularPercentIndicator(
  radius: 78,
  lineWidth: 14,
  percent: totalPercent,
  center: Text('${(totalPercent * 100).toStringAsFixed(0)}%'),
)

线性进度条核心代码如下:

LinearPercentIndicator(
  lineHeight: 12,
  percent: categoryPercent,
)

八、main.dart 完整代码

打开文件:

lib/main.dart

将其中内容替换为下面代码:

import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Packing Progress Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const PackingHomePage(),
    );
  }
}

class PackingItem {
  PackingItem({
    required this.name,
    required this.category,
    required this.icon,
    required this.isPacked,
  });

  final String name;
  final String category;
  final IconData icon;
  bool isPacked;
}

class PackingHomePage extends StatefulWidget {
  const PackingHomePage({super.key});

  
  State<PackingHomePage> createState() => _PackingHomePageState();
}

class _PackingHomePageState extends State<PackingHomePage> {
  final List<PackingItem> _items = [
    PackingItem(
      name: '身份证 / 护照',
      category: '证件',
      icon: Icons.badge,
      isPacked: true,
    ),
    PackingItem(
      name: '车票 / 机票信息',
      category: '证件',
      icon: Icons.confirmation_number,
      isPacked: false,
    ),
    PackingItem(
      name: '银行卡',
      category: '证件',
      icon: Icons.credit_card,
      isPacked: true,
    ),
    PackingItem(
      name: '换洗衣物',
      category: '衣物',
      icon: Icons.checkroom,
      isPacked: true,
    ),
    PackingItem(
      name: '外套',
      category: '衣物',
      icon: Icons.dry_cleaning,
      isPacked: false,
    ),
    PackingItem(
      name: '袜子',
      category: '衣物',
      icon: Icons.inventory_2,
      isPacked: false,
    ),
    PackingItem(
      name: '手机充电器',
      category: '电子设备',
      icon: Icons.charging_station,
      isPacked: true,
    ),
    PackingItem(
      name: '耳机',
      category: '电子设备',
      icon: Icons.headphones,
      isPacked: false,
    ),
    PackingItem(
      name: '充电宝',
      category: '电子设备',
      icon: Icons.battery_charging_full,
      isPacked: false,
    ),
    PackingItem(
      name: '牙刷牙膏',
      category: '洗护用品',
      icon: Icons.cleaning_services,
      isPacked: true,
    ),
    PackingItem(
      name: '毛巾',
      category: '洗护用品',
      icon: Icons.dry,
      isPacked: false,
    ),
    PackingItem(
      name: '洗面奶',
      category: '洗护用品',
      icon: Icons.spa,
      isPacked: false,
    ),
  ];

  final List<String> _categories = const [
    '证件',
    '衣物',
    '电子设备',
    '洗护用品',
  ];

  int get _packedCount {
    return _items.where((item) => item.isPacked).length;
  }

  double get _totalPercent {
    if (_items.isEmpty) {
      return 0;
    }
    return _packedCount / _items.length;
  }

  int _categoryTotal(String category) {
    return _items.where((item) => item.category == category).length;
  }

  int _categoryPacked(String category) {
    return _items
        .where((item) => item.category == category && item.isPacked)
        .length;
  }

  double _categoryPercent(String category) {
    final int total = _categoryTotal(category);

    if (total == 0) {
      return 0;
    }

    return _categoryPacked(category) / total;
  }

  IconData _categoryIcon(String category) {
    switch (category) {
      case '证件':
        return Icons.badge;
      case '衣物':
        return Icons.checkroom;
      case '电子设备':
        return Icons.devices;
      case '洗护用品':
        return Icons.spa;
      default:
        return Icons.category;
    }
  }

  Color _progressColor(double percent, ThemeData theme) {
    if (percent >= 0.8) {
      return Colors.green;
    }

    if (percent >= 0.5) {
      return Colors.orange;
    }

    return theme.colorScheme.primary;
  }

  String get _statusText {
    if (_totalPercent == 1) {
      return '行李已经全部准备完成';
    }

    if (_totalPercent >= 0.75) {
      return '大部分物品已经打包完成';
    }

    if (_totalPercent >= 0.4) {
      return '还有不少物品需要准备';
    }

    return '现在最好认真检查行李';
  }

  void _toggleItem(int index, bool value) {
    setState(() {
      _items[index].isPacked = value;
    });
  }

  void _resetAll() {
    setState(() {
      for (final PackingItem item in _items) {
        item.isPacked = false;
      }
    });
  }

  void _packAll() {
    setState(() {
      for (final PackingItem item in _items) {
        item.isPacked = true;
      }
    });
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('行李打包进度'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildOverviewCard(theme),
            const SizedBox(height: 16),
            _buildCategoryProgressCard(theme),
            const SizedBox(height: 16),
            _buildPackingListCard(theme),
            const SizedBox(height: 16),
            _buildActionCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildOverviewCard(ThemeData theme) {
    final double percent = _totalPercent;

    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Text(
              '旅行准备总进度',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            CircularPercentIndicator(
              radius: 82,
              lineWidth: 14,
              percent: percent,
              animation: true,
              animationDuration: 900,
              circularStrokeCap: CircularStrokeCap.round,
              progressColor: _progressColor(percent, theme),
              backgroundColor: theme.colorScheme.surfaceContainerHighest,
              center: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    '${(percent * 100).toStringAsFixed(0)}%',
                    style: theme.textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                      color: _progressColor(percent, theme),
                    ),
                  ),
                  Text(
                    '已完成',
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 18),
            Text(
              '$_packedCount / ${_items.length} 件物品已打包',
              style: theme.textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              _statusText,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCategoryProgressCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '分类打包进度',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            ..._categories.map((category) {
              final double percent = _categoryPercent(category);
              final int packed = _categoryPacked(category);
              final int total = _categoryTotal(category);

              return Padding(
                padding: const EdgeInsets.only(bottom: 18),
                child: Column(
                  children: [
                    Row(
                      children: [
                        Icon(
                          _categoryIcon(category),
                          color: theme.colorScheme.primary,
                          size: 22,
                        ),
                        const SizedBox(width: 10),
                        Expanded(
                          child: Text(
                            category,
                            style: theme.textTheme.titleMedium?.copyWith(
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                        Text(
                          '$packed / $total',
                          style: theme.textTheme.bodyMedium?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    LinearPercentIndicator(
                      padding: EdgeInsets.zero,
                      lineHeight: 12,
                      percent: percent,
                      animation: true,
                      animationDuration: 700,
                      barRadius: const Radius.circular(8),
                      progressColor: _progressColor(percent, theme),
                      backgroundColor: theme.colorScheme.surfaceContainerHighest,
                    ),
                  ],
                ),
              );
            }),
          ],
        ),
      ),
    );
  }

  Widget _buildPackingListCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '行李清单',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 10),
            ...List.generate(_items.length, (index) {
              final PackingItem item = _items[index];

              return CheckboxListTile(
                value: item.isPacked,
                onChanged: (value) {
                  _toggleItem(index, value ?? false);
                },
                secondary: Icon(
                  item.icon,
                  color: item.isPacked
                      ? Colors.green
                      : theme.colorScheme.onSurfaceVariant,
                ),
                title: Text(
                  item.name,
                  style: theme.textTheme.bodyLarge?.copyWith(
                    decoration: item.isPacked
                        ? TextDecoration.lineThrough
                        : TextDecoration.none,
                    color: item.isPacked
                        ? theme.colorScheme.onSurfaceVariant
                        : theme.colorScheme.onSurface,
                  ),
                ),
                subtitle: Text(item.category),
                controlAffinity: ListTileControlAffinity.trailing,
                contentPadding: EdgeInsets.zero,
              );
            }),
          ],
        ),
      ),
    );
  }

  Widget _buildActionCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Row(
          children: [
            Expanded(
              child: ElevatedButton.icon(
                onPressed: _packAll,
                icon: const Icon(Icons.done_all),
                label: const Text('全部完成'),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: OutlinedButton.icon(
                onPressed: _resetAll,
                icon: const Icon(Icons.refresh),
                label: const Text('重置清单'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildLibraryCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '第三方库说明',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            _buildInfoRow(
              theme,
              title: '库名称',
              value: 'percent_indicator',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value: "import 'package:percent_indicator/percent_indicator.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value: 'CircularPercentIndicator / LinearPercentIndicator',
            ),
            _buildInfoRow(
              theme,
              title: '应用场景',
              value: '进度展示、目标完成度、清单完成度、数据统计',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(
    ThemeData theme, {
    required String title,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 82,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 percent_indicator 第三方库

代码开头引入第三方库:

import 'package:percent_indicator/percent_indicator.dart';

这说明项目确实使用了 Flutter 第三方库,而不是 OpenHarmony 原生库。

本项目中主要使用两个组件:

CircularPercentIndicator
LinearPercentIndicator

其中:

组件 作用
CircularPercentIndicator 绘制圆形百分比进度条
LinearPercentIndicator 绘制线性百分比进度条

2. 定义行李物品数据模型

项目中定义了行李物品模型:

class PackingItem {
  PackingItem({
    required this.name,
    required this.category,
    required this.icon,
    required this.isPacked,
  });

  final String name;
  final String category;
  final IconData icon;
  bool isPacked;
}

字段说明如下:

字段 作用
name 物品名称
category 物品分类
icon 物品图标
isPacked 是否已经打包

其中 isPacked 是可变化的布尔值,用于记录用户是否勾选了该物品。


3. 计算总体打包进度

已打包数量通过下面代码统计:

int get _packedCount {
  return _items.where((item) => item.isPacked).length;
}

总体完成度通过下面代码计算:

double get _totalPercent {
  if (_items.isEmpty) {
    return 0;
  }
  return _packedCount / _items.length;
}

percent_indicator 中的 percent 参数范围是 0.01.0,所以需要用:

已完成数量 / 总数量

得到百分比小数。

例如:

6 / 12 = 0.5

表示完成 50%。


4. 使用 CircularPercentIndicator 展示总体进度

总体进度使用圆形进度条展示:

CircularPercentIndicator(
  radius: 82,
  lineWidth: 14,
  percent: percent,
  animation: true,
  animationDuration: 900,
  circularStrokeCap: CircularStrokeCap.round,
  center: Text('${(percent * 100).toStringAsFixed(0)}%'),
)

其中:

参数 作用
radius 圆形进度条半径
lineWidth 进度线宽度
percent 当前百分比
animation 是否开启动画
animationDuration 动画持续时间
circularStrokeCap 进度条端点样式
center 圆形中间显示的内容

通过这个组件,可以很直观地展示整体打包完成度。


5. 使用 LinearPercentIndicator 展示分类进度

分类进度使用线性进度条展示:

LinearPercentIndicator(
  lineHeight: 12,
  percent: percent,
  animation: true,
  animationDuration: 700,
  barRadius: const Radius.circular(8),
)

其中:

参数 作用
lineHeight 进度条高度
percent 当前分类完成度
animation 是否开启动画
animationDuration 动画持续时间
barRadius 进度条圆角

本项目中分别展示:

  • 证件;
  • 衣物;
  • 电子设备;
  • 洗护用品。

这样用户可以清楚看到哪一类行李还没准备好。


6. 使用 CheckboxListTile 实现物品勾选

物品清单使用 CheckboxListTile 实现:

CheckboxListTile(
  value: item.isPacked,
  onChanged: (value) {
    _toggleItem(index, value ?? false);
  },
  title: Text(item.name),
  subtitle: Text(item.category),
)

当用户点击勾选框时,会调用:

_toggleItem(index, value ?? false);

然后通过 setState() 更新页面。


7. 使用 setState 刷新进度

勾选状态变化后,调用:

void _toggleItem(int index, bool value) {
  setState(() {
    _items[index].isPacked = value;
  });
}

setState() 会触发页面重新构建,圆形进度条和线性进度条都会重新计算并更新显示。

如果不调用 setState(),数据变了页面也不会动。Flutter 不负责猜人类内心戏,它只认明确通知,冷酷但高效。


8. 一键完成和重置清单

页面提供了两个按钮:

ElevatedButton.icon(
  onPressed: _packAll,
  icon: const Icon(Icons.done_all),
  label: const Text('全部完成'),
)
OutlinedButton.icon(
  onPressed: _resetAll,
  icon: const Icon(Icons.refresh),
  label: const Text('重置清单'),
)

全部完成方法:

void _packAll() {
  setState(() {
    for (final PackingItem item in _items) {
      item.isPacked = true;
    }
  });
}

重置方法:

void _resetAll() {
  setState(() {
    for (final PackingItem item in _items) {
      item.isPacked = false;
    }
  });
}

这样可以快速测试不同完成度下的页面效果。


十、运行项目

完成代码后,在终端执行:

flutter pub get

然后连接 OpenHarmony 设备或启动 OpenHarmony 模拟器。

查看设备:

flutter devices

运行项目:

flutter run

如果环境配置正确,应用会运行到 OpenHarmony 设备或模拟器中。

运行成功后,页面会显示“行李打包进度”。用户可以勾选行李清单中的物品,页面顶部的圆形进度条和分类进度条会自动更新。


十一、开发中遇到的问题

1. percent_indicator 依赖没有生效

如果代码中出现找不到 percent_indicator 的问题,可以检查 pubspec.yaml 中是否添加了:

percent_indicator: ^4.2.5

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。编辑器有时候像没睡醒,依赖明明下载了,它还装作不知道,真是软件界的人类行为。


2. import 导入报错

如果下面代码报错:

import 'package:percent_indicator/percent_indicator.dart';

通常有几种原因:

  • pubspec.yaml 中没有添加依赖;
  • 没有执行 flutter pub get
  • YAML 缩进错误;
  • 包名写错;
  • 编辑器没有刷新依赖。

其中 YAML 缩进最容易出问题。配置文件对空格的敏感程度,简直像在审讯键盘。


3. 进度条不显示

如果进度条没有显示,可以检查:

  • 是否正确引入了 percent_indicator
  • percent 是否在 0.01.0 之间;
  • 外层布局是否给了足够空间;
  • 页面是否成功运行;
  • 是否被其他组件遮挡。

需要注意,percent 不是 50,而是 0.5。把 50 塞进去当然不对,进度条不是百分制高考。


4. 点击勾选后进度不更新

如果勾选后进度没有变化,可以检查是否调用了:

setState(() {
  _items[index].isPacked = value;
});

Flutter 中状态变化后,需要使用 setState() 通知页面刷新。


5. 进度百分比计算错误

正确计算方式是:

已完成数量 / 总数量

例如:

return _packedCount / _items.length;

不要写成:

_items.length / _packedCount

否则结果会超过 1,进度条可能出现异常。数学又一次冷漠地证明了人类不能乱除。


6. 运行不到 OpenHarmony 设备

如果项目无法运行到 OpenHarmony 设备或模拟器,可以检查:

  • Flutter for OpenHarmony 环境是否配置完成;
  • 设备是否连接成功;
  • flutter devices 是否能识别设备;
  • 是否执行了 flutter pub get
  • 是否选择了正确的运行设备;
  • 项目是否为 Flutter 项目,而不是原生鸿蒙项目。

如果 flutter devices 都识别不到设备,那就先处理环境问题,不要盯着进度条代码看半小时。进度条很无辜,至少这次大概率是。


十二、本文和原生鸿蒙项目的区别

本文是 Flutter for OpenHarmony 第三方库实践,不是 OpenHarmony 原生 ArkTS 项目。

主要区别如下:

对比项 本文写法 原生鸿蒙写法
UI 技术 Flutter ArkUI
主要语言 Dart ArkTS
页面入口 lib/main.dart Index.ets
依赖配置 pubspec.yaml oh-package.json5
依赖安装 flutter pub get ohpm install
第三方库 percent_indicator OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / CircularPercentIndicator @Entry / @Component

因此本文符合 Flutter for OpenHarmony 第三方库实践方向。


十三、总结

本篇完成了一个基于 percent_indicator 的 Flutter for OpenHarmony 旅行行李打包进度应用。项目通过 Flutter 第三方库展示总体圆形进度条和分类线性进度条,并结合清单勾选功能实现了进度自动更新。

通过本次实践,我主要完成了以下内容:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 percent_indicator 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入 percent_indicator
  • 使用 CircularPercentIndicator 展示总体进度;
  • 使用 LinearPercentIndicator 展示分类进度;
  • 使用 CheckboxListTile 实现行李物品勾选;
  • 使用 setState() 更新页面状态;
  • 实现一键完成和重置清单功能;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础行李清单应用,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

后续可以在这个基础上继续扩展,例如:

  • 添加自定义行李物品;
  • 添加删除物品功能;
  • 添加本地数据保存;
  • 添加多个旅行计划;
  • 添加出发倒计时;
  • 添加重要物品提醒;
  • 添加分类筛选;
  • 添加暗色主题;
  • 添加云端同步;
  • 添加分享清单功能。

整体来看,percent_indicator 可以帮助 Flutter 开发者快速实现进度展示。通过这个项目,可以理解 Flutter for OpenHarmony 中第三方库依赖配置、进度组件使用和页面状态更新之间的基本关系。

Logo

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

更多推荐