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


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的待办事项滑动操作应用。项目中使用 Flutter 第三方库 flutter_slidable 实现列表项左右滑动操作,让用户可以通过滑动待办事项完成标记、收藏和删除等操作。

最终运行效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

页面主要包含以下内容:

  • 顶部标题栏;
  • 待办事项统计卡片;
  • 筛选按钮;
  • 可左右滑动的待办事项列表;
  • 滑动完成任务;
  • 滑动收藏任务;
  • 滑动删除任务;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

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


前言

在移动应用开发中,列表操作非常常见。例如待办事项、消息列表、邮件列表、收藏列表、购物车列表等,都可能需要对某一项进行删除、完成、收藏、归档等操作。

如果只在每一项后面放一排按钮,页面会显得很拥挤。尤其是在手机端,屏幕空间本来就不富裕,再硬塞按钮,用户看着累,开发者写着也累,大家一起为 UI 设计的失败买单。

滑动操作是一种比较自然的交互方式。用户向左或向右滑动列表项,就可以看到对应操作按钮。

本文选择使用 Flutter 第三方库 flutter_slidable 来实现滑动列表项。它可以快速创建可滑动的列表操作区域,支持开始方向和结束方向的操作面板,也可以配合不同动画效果使用。

本项目以“待办事项滑动操作应用”为例,使用 flutter_slidable 实现任务完成、收藏和删除功能,并结合 Flutter 状态更新展示任务统计结果。


一、项目目标

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

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 flutter_slidable
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 flutter_slidable
  • 使用 Slidable 构建可滑动列表项;
  • 使用 ActionPane 设置滑动操作区域;
  • 使用 SlidableAction 实现完成、收藏和删除按钮;
  • 使用 setState() 更新待办事项状态;
  • 使用 Flutter Material 组件构建完整页面;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 flutter_slidable
功能场景 滑动列表 / 待办事项 / 列表操作
核心组件 Slidable / ActionPane / SlidableAction
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 flutter_slidable

在实际应用中,滑动列表项可以用于很多场景,例如:

  • 待办事项完成;
  • 消息已读和删除;
  • 邮件归档;
  • 收藏内容;
  • 清理购物车;
  • 删除浏览记录;
  • 文件列表管理;
  • 课程任务管理。

如果自己使用 Flutter 原生手写滑动操作,需要处理手势监听、滑动距离、按钮显示、动画回弹、列表状态更新等逻辑。能写,但没有必要把简单列表操作写成一场人类耐力测试。

flutter_slidable 已经封装好了列表滑动操作组件,可以让开发者直接把重点放在业务功能上。

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

  • 为待办事项列表项添加滑动交互;
  • 向右滑动显示“完成”操作;
  • 向左滑动显示“收藏”和“删除”操作;
  • 配合 Flutter 状态更新刷新页面;
  • 提升列表操作的交互体验。

四、创建 Flutter for OpenHarmony 项目

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

示例项目名称:

flutter create slidable_todo_demo

进入项目目录:

cd slidable_todo_demo

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

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

其中:

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

五、添加 flutter_slidable 第三方库

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

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  flutter_slidable: ^4.0.3

完整结构大致如下:

name: slidable_todo_demo
description: A Flutter for OpenHarmony slidable todo demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  flutter_slidable: ^4.0.3

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

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

flutter pub get

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


六、项目结构

本项目主要修改 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 项目。

看到 pubspec.yamllib/main.dartimport 'package:flutter_slidable/flutter_slidable.dart';,这才是正确方向。别拿 ArkTS 硬凑 Flutter,代码不是换个标题就能转世投胎的。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 flutter_slidable
  2. main.dart 中引入第三方库;
  3. 定义待办事项数据模型;
  4. 使用 ListView 展示任务列表;
  5. 使用 Slidable 包裹每一个任务项;
  6. 使用 startActionPane 设置右滑操作;
  7. 使用 endActionPane 设置左滑操作;
  8. 使用 SlidableAction 实现完成、收藏和删除;
  9. 操作后使用 setState() 更新页面。

第三方库引入代码如下:

import 'package:flutter_slidable/flutter_slidable.dart';

滑动列表项核心代码如下:

Slidable(
  startActionPane: ActionPane(
    motion: const ScrollMotion(),
    children: [
      SlidableAction(
        onPressed: (context) {},
        icon: Icons.done,
        label: '完成',
      ),
    ],
  ),
  endActionPane: ActionPane(
    motion: const DrawerMotion(),
    children: [
      SlidableAction(
        onPressed: (context) {},
        icon: Icons.star,
        label: '收藏',
      ),
      SlidableAction(
        onPressed: (context) {},
        icon: Icons.delete,
        label: '删除',
      ),
    ],
  ),
  child: ListTile(
    title: Text('待办事项'),
  ),
)

这段代码是本文的重点,说明项目确实使用了 Flutter 第三方库实现滑动操作。


八、main.dart 完整代码

打开文件:

lib/main.dart

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

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

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

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

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

class TodoItem {
  TodoItem({
    required this.title,
    required this.category,
    required this.description,
    required this.icon,
    required this.color,
    this.isDone = false,
    this.isStarred = false,
  });

  final String title;
  final String category;
  final String description;
  final IconData icon;
  final Color color;
  bool isDone;
  bool isStarred;
}

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

  
  State<SlidableTodoHomePage> createState() => _SlidableTodoHomePageState();
}

class _SlidableTodoHomePageState extends State<SlidableTodoHomePage> {
  final List<String> _filters = const [
    '全部',
    '未完成',
    '已完成',
    '收藏',
  ];

  final List<TodoItem> _todos = [
    TodoItem(
      title: '完成 Flutter 第三方库文章',
      category: '项目',
      description: '整理 pubspec.yaml、main.dart、运行截图和文章说明。',
      icon: Icons.article,
      color: Colors.teal,
    ),
    TodoItem(
      title: '复习 C 语言指针',
      category: '学习',
      description: '重点看指针变量、地址、数组和函数参数传递。',
      icon: Icons.code,
      color: Colors.blue,
    ),
    TodoItem(
      title: '整理桌面文件',
      category: '生活',
      description: '清理截图、压缩包和临时文件,不然电脑迟早变成电子垃圾场。',
      icon: Icons.folder,
      color: Colors.orange,
      isDone: true,
    ),
    TodoItem(
      title: '准备项目运行截图',
      category: '项目',
      description: '运行 Flutter for OpenHarmony 项目,截图首页效果图。',
      icon: Icons.screenshot_monitor,
      color: Colors.deepPurple,
    ),
    TodoItem(
      title: '英语口语练习',
      category: '学习',
      description: '朗读一段英文材料,注意语音语调和停顿。',
      icon: Icons.language,
      color: Colors.green,
      isStarred: true,
    ),
    TodoItem(
      title: '检查依赖是否写对',
      category: '项目',
      description: '确认 pubspec.yaml 中第三方库名称和缩进都正确。',
      icon: Icons.fact_check,
      color: Colors.redAccent,
    ),
  ];

  String _selectedFilter = '全部';

  List<TodoItem> get _filteredTodos {
    if (_selectedFilter == '未完成') {
      return _todos.where((todo) => !todo.isDone).toList();
    }

    if (_selectedFilter == '已完成') {
      return _todos.where((todo) => todo.isDone).toList();
    }

    if (_selectedFilter == '收藏') {
      return _todos.where((todo) => todo.isStarred).toList();
    }

    return _todos;
  }

  int get _doneCount {
    return _todos.where((todo) => todo.isDone).length;
  }

  int get _starredCount {
    return _todos.where((todo) => todo.isStarred).length;
  }

  int get _unfinishedCount {
    return _todos.where((todo) => !todo.isDone).length;
  }

  void _selectFilter(String filter) {
    setState(() {
      _selectedFilter = filter;
    });
  }

  void _toggleDone(TodoItem todo) {
    setState(() {
      todo.isDone = !todo.isDone;
    });

    _showMessage(todo.isDone ? '已完成:${todo.title}' : '已取消完成:${todo.title}');
  }

  void _toggleStar(TodoItem todo) {
    setState(() {
      todo.isStarred = !todo.isStarred;
    });

    _showMessage(todo.isStarred ? '已收藏:${todo.title}' : '已取消收藏:${todo.title}');
  }

  void _deleteTodo(TodoItem todo) {
    setState(() {
      _todos.remove(todo);
    });

    _showMessage('已删除:${todo.title}');
  }

  void _resetTodos() {
    setState(() {
      for (final TodoItem todo in _todos) {
        todo.isDone = false;
        todo.isStarred = false;
      }
      _selectedFilter = '全部';
    });

    _showMessage('已重置任务状态');
  }

  void _showMessage(String message) {
    if (!mounted) {
      return;
    }

    ScaffoldMessenger.of(context).clearSnackBars();
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: const Duration(milliseconds: 1200),
      ),
    );
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final List<TodoItem> todos = _filteredTodos;

    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),
            _buildFilterCard(theme),
            const SizedBox(height: 16),
            _buildTodoListCard(theme, todos),
            const SizedBox(height: 16),
            _buildActionCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildOverviewCard(ThemeData theme) {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Container(
              width: 76,
              height: 76,
              decoration: BoxDecoration(
                color: theme.colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                Icons.swipe,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            Text(
              'Flutter for OpenHarmony',
              style: theme.textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              '使用 flutter_slidable 构建可左右滑动操作的待办事项列表',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),
            Row(
              children: [
                _buildStatItem(
                  theme,
                  title: '总任务',
                  value: '${_todos.length}',
                  icon: Icons.list_alt,
                ),
                _buildStatItem(
                  theme,
                  title: '未完成',
                  value: '$_unfinishedCount',
                  icon: Icons.pending_actions,
                ),
                _buildStatItem(
                  theme,
                  title: '已完成',
                  value: '$_doneCount',
                  icon: Icons.check_circle,
                ),
                _buildStatItem(
                  theme,
                  title: '收藏',
                  value: '$_starredCount',
                  icon: Icons.star,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatItem(
    ThemeData theme, {
    required String title,
    required String value,
    required IconData icon,
  }) {
    return Expanded(
      child: Column(
        children: [
          Icon(
            icon,
            color: theme.colorScheme.primary,
          ),
          const SizedBox(height: 6),
          Text(
            value,
            style: theme.textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            title,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFilterCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Wrap(
          spacing: 10,
          runSpacing: 10,
          children: _filters.map((filter) {
            final bool selected = filter == _selectedFilter;

            return ChoiceChip(
              label: Text(filter),
              selected: selected,
              onSelected: (_) {
                _selectFilter(filter);
              },
              selectedColor: theme.colorScheme.primaryContainer,
              labelStyle: TextStyle(
                color: selected
                    ? theme.colorScheme.onPrimaryContainer
                    : theme.colorScheme.onSurface,
                fontWeight: selected ? FontWeight.bold : FontWeight.normal,
              ),
            );
          }).toList(),
        ),
      ),
    );
  }

  Widget _buildTodoListCard(ThemeData theme, List<TodoItem> todos) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(16, 20, 16, 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.only(left: 4, right: 4, bottom: 14),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      '待办事项列表',
                      style: theme.textTheme.titleLarge?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  Text(
                    '${todos.length} 条',
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
            if (todos.isEmpty)
              Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    Icon(
                      Icons.inbox,
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: Text(
                        '当前筛选条件下没有任务。',
                        style: theme.textTheme.bodyMedium?.copyWith(
                          color: theme.colorScheme.onSurfaceVariant,
                        ),
                      ),
                    ),
                  ],
                ),
              )
            else
              ...todos.map((todo) {
                return Padding(
                  padding: const EdgeInsets.only(bottom: 12),
                  child: _buildSlidableTodoItem(theme, todo),
                );
              }),
          ],
        ),
      ),
    );
  }

  Widget _buildSlidableTodoItem(ThemeData theme, TodoItem todo) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(18),
      child: Slidable(
        key: ValueKey(todo.title),
        startActionPane: ActionPane(
          motion: const ScrollMotion(),
          extentRatio: 0.28,
          children: [
            SlidableAction(
              onPressed: (_) {
                _toggleDone(todo);
              },
              backgroundColor: todo.isDone ? Colors.orange : Colors.green,
              foregroundColor: Colors.white,
              icon: todo.isDone ? Icons.undo : Icons.done,
              label: todo.isDone ? '取消' : '完成',
            ),
          ],
        ),
        endActionPane: ActionPane(
          motion: const DrawerMotion(),
          extentRatio: 0.56,
          children: [
            SlidableAction(
              onPressed: (_) {
                _toggleStar(todo);
              },
              backgroundColor: Colors.amber,
              foregroundColor: Colors.white,
              icon: todo.isStarred ? Icons.star : Icons.star_border,
              label: todo.isStarred ? '取消' : '收藏',
            ),
            SlidableAction(
              onPressed: (_) {
                _deleteTodo(todo);
              },
              backgroundColor: Colors.redAccent,
              foregroundColor: Colors.white,
              icon: Icons.delete,
              label: '删除',
            ),
          ],
        ),
        child: Container(
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: todo.isDone
                ? theme.colorScheme.surfaceContainerHighest
                : todo.color.withOpacity(0.12),
            borderRadius: BorderRadius.circular(18),
            border: Border.all(
              color: todo.isStarred
                  ? Colors.amber
                  : todo.color.withOpacity(0.20),
              width: todo.isStarred ? 1.4 : 1,
            ),
          ),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                width: 48,
                height: 48,
                decoration: BoxDecoration(
                  color: todo.color.withOpacity(0.16),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Icon(
                  todo.icon,
                  color: todo.color,
                ),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      todo.title,
                      style: theme.textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                        decoration: todo.isDone
                            ? TextDecoration.lineThrough
                            : TextDecoration.none,
                        color: todo.isDone
                            ? theme.colorScheme.onSurfaceVariant
                            : theme.colorScheme.onSurface,
                      ),
                    ),
                    const SizedBox(height: 6),
                    Text(
                      todo.category,
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: todo.color,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      todo.description,
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                        height: 1.5,
                      ),
                    ),
                    const SizedBox(height: 10),
                    Row(
                      children: [
                        Icon(
                          todo.isDone
                              ? Icons.check_circle
                              : Icons.radio_button_unchecked,
                          size: 18,
                          color: todo.isDone
                              ? Colors.green
                              : theme.colorScheme.onSurfaceVariant,
                        ),
                        const SizedBox(width: 6),
                        Text(
                          todo.isDone ? '已完成' : '未完成',
                          style: theme.textTheme.bodySmall?.copyWith(
                            color: todo.isDone
                                ? Colors.green
                                : theme.colorScheme.onSurfaceVariant,
                          ),
                        ),
                        const SizedBox(width: 14),
                        if (todo.isStarred) ...[
                          const Icon(
                            Icons.star,
                            size: 18,
                            color: Colors.amber,
                          ),
                          const SizedBox(width: 6),
                          Text(
                            '已收藏',
                            style: theme.textTheme.bodySmall?.copyWith(
                              color: Colors.amber,
                            ),
                          ),
                        ],
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildActionCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: OutlinedButton.icon(
          onPressed: _resetTodos,
          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: 'flutter_slidable',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value: "import 'package:flutter_slidable/flutter_slidable.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value: 'Slidable / ActionPane / SlidableAction',
            ),
            _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. 引入 flutter_slidable 第三方库

代码开头引入第三方库:

import 'package:flutter_slidable/flutter_slidable.dart';

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

本项目中主要使用以下组件:

Slidable
ActionPane
SlidableAction
ScrollMotion
DrawerMotion

其中:

组件 作用
Slidable 包裹普通列表项,让它支持滑动操作
ActionPane 定义滑动后显示的操作区域
SlidableAction 定义具体操作按钮
ScrollMotion 滑动操作区域跟随滚动的动画效果
DrawerMotion 类似抽屉展开的动画效果

2. 定义待办事项数据模型

项目中定义了待办事项模型:

class TodoItem {
  TodoItem({
    required this.title,
    required this.category,
    required this.description,
    required this.icon,
    required this.color,
    this.isDone = false,
    this.isStarred = false,
  });

  final String title;
  final String category;
  final String description;
  final IconData icon;
  final Color color;
  bool isDone;
  bool isStarred;
}

字段说明如下:

字段 作用
title 任务标题
category 任务分类
description 任务说明
icon 任务图标
color 任务主题色
isDone 是否完成
isStarred 是否收藏

其中 isDoneisStarred 是可变化状态,用于响应用户滑动操作。


3. 使用 Slidable 包裹列表项

滑动操作的核心是:

Slidable(
  child: Container(...),
)

child 就是平时显示出来的列表项内容。

用户没有滑动时,只能看到普通任务卡片。

用户向左或向右滑动时,就会显示对应操作按钮。


4. 使用 startActionPane 设置右滑操作

本项目中,右滑显示“完成”操作:

startActionPane: ActionPane(
  motion: const ScrollMotion(),
  children: [
    SlidableAction(
      onPressed: (_) {
        _toggleDone(todo);
      },
      icon: Icons.done,
      label: '完成',
    ),
  ],
)

startActionPane 表示开始方向的操作区域。在常见从左到右的语言环境中,它通常对应右滑后显示的左侧操作。

这里用于完成或取消完成任务。


5. 使用 endActionPane 设置左滑操作

本项目中,左滑显示“收藏”和“删除”操作:

endActionPane: ActionPane(
  motion: const DrawerMotion(),
  children: [
    SlidableAction(
      onPressed: (_) {
        _toggleStar(todo);
      },
      icon: Icons.star,
      label: '收藏',
    ),
    SlidableAction(
      onPressed: (_) {
        _deleteTodo(todo);
      },
      icon: Icons.delete,
      label: '删除',
    ),
  ],
)

endActionPane 表示结束方向的操作区域。

这里用于收藏任务和删除任务。

这样一个任务项就可以同时支持多个操作,不需要在页面上摆一堆按钮。终于让列表看起来不像小广告栏了。


6. 使用 SlidableAction 实现操作按钮

每一个滑动按钮都是一个 SlidableAction

SlidableAction(
  onPressed: (_) {
    _deleteTodo(todo);
  },
  backgroundColor: Colors.redAccent,
  foregroundColor: Colors.white,
  icon: Icons.delete,
  label: '删除',
)

参数说明如下:

参数 作用
onPressed 点击按钮后的回调
backgroundColor 按钮背景色
foregroundColor 按钮文字和图标颜色
icon 按钮图标
label 按钮文字

7. 完成任务状态切换

完成任务方法如下:

void _toggleDone(TodoItem todo) {
  setState(() {
    todo.isDone = !todo.isDone;
  });
}

如果任务未完成,滑动后会改为已完成。

如果任务已完成,再次滑动可以取消完成。

页面会根据 isDone 改变文字样式,例如添加删除线:

decoration: todo.isDone
    ? TextDecoration.lineThrough
    : TextDecoration.none

这样用户可以直观看到任务是否完成。


8. 收藏任务状态切换

收藏任务方法如下:

void _toggleStar(TodoItem todo) {
  setState(() {
    todo.isStarred = !todo.isStarred;
  });
}

收藏后,任务卡片边框会变成黄色,并显示“已收藏”。

这类状态反馈很重要。不然用户点了半天不知道有没有成功,最后开始怀疑软件、手机、人生和开发者。


9. 删除任务

删除任务方法如下:

void _deleteTodo(TodoItem todo) {
  setState(() {
    _todos.remove(todo);
  });
}

删除后,该任务会从列表中移除,统计数据也会自动更新。


10. 任务筛选功能

页面提供了四种筛选条件:

final List<String> _filters = const [
  '全部',
  '未完成',
  '已完成',
  '收藏',
];

筛选逻辑如下:

List<TodoItem> get _filteredTodos {
  if (_selectedFilter == '未完成') {
    return _todos.where((todo) => !todo.isDone).toList();
  }

  if (_selectedFilter == '已完成') {
    return _todos.where((todo) => todo.isDone).toList();
  }

  if (_selectedFilter == '收藏') {
    return _todos.where((todo) => todo.isStarred).toList();
  }

  return _todos;
}

这样用户可以快速查看不同状态的任务。


十、运行项目

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

flutter pub get

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

查看设备:

flutter devices

运行项目:

flutter run

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

运行成功后,页面会显示“待办滑动操作”。用户可以右滑任务完成事项,也可以左滑任务进行收藏或删除。


十一、开发中遇到的问题

1. flutter_slidable 依赖没有生效

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

flutter_slidable: ^4.0.3

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。编辑器有时候像没睡醒,依赖装好了它还装作没看见,经典软件行为。


2. import 导入报错

如果下面代码报错:

import 'package:flutter_slidable/flutter_slidable.dart';

通常有几种原因:

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

其中 YAML 缩进最容易出问题。依赖必须写在 dependencies 下面,并且缩进要正确。一个空格就能让项目不认依赖,编程世界真是贴心到令人窒息。


3. 滑动后没有操作按钮

如果滑动列表项后没有按钮,可以检查:

  • 是否使用了 Slidable
  • 是否设置了 startActionPaneendActionPane
  • ActionPane 中是否添加了 SlidableAction
  • 列表项外层是否遮挡了滑动区域;
  • 设备或模拟器是否支持正常拖动操作。

最基础结构应该类似:

Slidable(
  startActionPane: ActionPane(...),
  endActionPane: ActionPane(...),
  child: ListTile(...),
)

4. 点击操作后页面没有刷新

如果点击“完成”“收藏”“删除”后页面没有变化,可以检查是否调用了:

setState(() {
  ...
});

Flutter 中状态变化后必须调用 setState(),否则页面不会重新构建。它不是读心框架,不会根据你的眼神更新 UI。


5. 删除任务后列表显示异常

如果删除任务后列表异常,可以检查 Slidable 是否设置了唯一 key

key: ValueKey(todo.title),

列表中每个可滑动项最好有稳定的 key,这样 Flutter 能正确识别每一项的状态。


6. 滑动区域太窄或太宽

可以通过 extentRatio 调整滑动操作区域宽度:

extentRatio: 0.56

数值越大,滑出的按钮区域越宽。

如果按钮太多但区域太窄,文字可能显示不完整。UI 不会因为你塞得多就自动变聪明,它只会挤成一团。


7. 运行不到 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
第三方库 flutter_slidable OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / Slidable @Entry / @Component

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


十三、总结

本篇完成了一个基于 flutter_slidable 的 Flutter for OpenHarmony 待办事项滑动操作应用。项目通过 Flutter 第三方库实现列表项左右滑动操作,并结合待办事项状态更新实现完成、收藏和删除功能。

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

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 flutter_slidable 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入 flutter_slidable
  • 使用 Slidable 构建可滑动列表项;
  • 使用 ActionPane 设置滑动操作区域;
  • 使用 SlidableAction 实现完成、收藏和删除按钮;
  • 使用 ChoiceChip 实现任务筛选;
  • 使用 setState() 更新任务状态;
  • 使用 Flutter Material 组件构建完整页面;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

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

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

  • 添加新建任务功能;
  • 添加编辑任务功能;
  • 添加任务优先级;
  • 添加任务截止时间;
  • 添加本地数据保存;
  • 添加任务提醒;
  • 添加分类管理;
  • 添加搜索功能;
  • 添加暗色主题;
  • 添加云端同步。

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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐