用药提醒应用 - 技术解析文档

一、项目背景概述

本项目是一款基于 Flutter 框架开发的用药提醒应用,旨在帮助用户管理每日的服药计划。应用以简洁直观的界面展示当日需要服用的药物清单,用户可以通过点击药物卡片来标记药物是否已服用,系统会实时统计已服用和未服用的药物数量,帮助用户养成按时服药的良好习惯。

该应用采用 Material Design 3 设计规范,整体视觉风格现代简洁,配合流畅的动画效果,为用户提供了愉悦的交互体验。项目虽然体量不大,但涵盖了 Flutter 开发中的多个核心知识点,包括状态管理、列表构建、动画容器、手势检测以及主题系统的应用等,是一个非常典型的 Flutter 入门级实战项目。

二、架构分析

整体架构图

应用入口 main函数

根组件 MyApp

首页组件 MyHomePage

用药提醒主组件

药物数据模型 Medicine

列表构建 ListView.builder

药物条目组件

手势检测 GestureDetector

切换服用状态 toggleMedicine

状态更新 setState

架构说明

本项目采用了 Flutter 典型的组件化分层架构,从入口到具体业务组件形成了清晰的层级关系。应用的根组件负责全局主题配置和应用初始化,首页组件提供基础的页面脚手架,而核心的业务逻辑则封装在独立的状态组件中。

数据层采用了简单的内存数据模型,药物数据以列表形式存储在状态组件中,通过 setState 方法驱动界面更新。每个药物条目被抽离为独立的无状态组件,通过回调函数与父组件通信,实现了良好的组件解耦。

三、入口组件流程

应用的入口流程非常标准,遵循 Flutter 官方推荐的应用启动模式。

应用启动入口

应用从 main 函数启动,调用 runApp 方法将根组件挂载到屏幕上。根组件是一个无状态组件,它的构建方法返回一个 MaterialApp 实例,这是 Flutter 应用的标准顶层容器。

在主题配置方面,应用使用了 ColorScheme.fromSeed 方法,以深紫色作为种子颜色生成完整的配色方案,同时启用了 Material 3 设计规范。这种配色方式的优势在于可以通过一个种子色自动生成一整套协调的颜色系统,包括主色、辅色、中性色等各个色阶,确保应用整体视觉的一致性。

应用标题设置为 Flutter for OpenHarmony,调试横幅被设置为隐藏,使界面更加清爽。

首页组件

首页组件是一个有状态组件,虽然它本身不维护复杂的状态,但它继承了 StatefulWidget 的特性,为后续的功能扩展预留了空间。首页的构建方法返回一个 Scaffold 脚手架组件,包含顶部的应用栏和主体内容区域。

应用栏的标题通过 widget.title 获取,这种通过构造函数传递参数的方式是 Flutter 组件通信的基础模式。主体区域直接嵌入了用药提醒的核心组件,这种将业务逻辑封装在独立组件中的做法,使得首页代码保持简洁,同时业务组件可以在其他地方复用。

四、核心组件逐段解析

用药提醒主组件

这是应用的核心业务组件,是一个有状态组件,负责管理药物列表数据和用户交互逻辑。

数据模型设计

在组件内部定义了药物数据模型类,该模型包含五个属性:药物名称、剂量、服用时间、图标和服用状态。其中前四个是不可变的 final 属性,而服用状态是可变的,因为用户可以通过交互来切换这个状态。

这种数据模型的设计体现了面向对象的封装思想,将药物相关的所有信息聚合在一个类中,便于管理和传递。模型类使用了命名参数构造函数,并且所有必填参数都标记了 required,这是 Dart 空安全特性下的最佳实践。

药物列表初始化

组件初始化时,创建了包含三个药物的示例列表。每个药物对象都包含完整的信息:阿莫西林 500mg 早上8点服用,布洛芬 200mg 中午12点服用,维生素C 100mg 晚上6点服用。这些药物使用了不同的图标来区分,阿莫西林和布洛芬使用药片图标,维生素C使用药液图标,增加了界面的辨识度。

状态切换方法

toggleMedicine 方法是组件的核心交互逻辑,它接收一个索引参数,通过 setState 方法切换对应位置药物的服用状态。setState 是 Flutter 状态管理的基础,它会通知框架状态发生了变化,触发组件的重新构建,从而更新界面显示。

这种父组件管理状态、子组件通过回调触发状态变化的模式,是 Flutter 中最基础也是最常用的状态管理方式,称为状态提升。将状态放在共同的父组件中管理,可以让多个子组件共享同一份数据。

界面布局结构

主组件的界面采用纵向列布局,整体结构分为三个部分:标题区域、药物列表区域和统计信息区域。

标题区域使用了 headlineSmall 样式的文本,并设置了粗体字重,确保标题在视觉上的突出性。标题与列表之间通过 SizedBox 设置了 16 像素的间距,营造出舒适的呼吸感。

药物列表使用 ListView.builder 构建,这是 Flutter 中构建长列表的推荐方式,因为它具有懒加载特性,只会构建当前可见的列表项,对于大量数据的列表可以显著提升性能。虽然本项目只有三个药物,性能优势不明显,但使用 ListView.builder 是一种良好的编程习惯。

列表被设置为 shrinkWrap: true 和 NeverScrollableScrollPhysics,这意味着列表会根据内容自动收缩高度,并且自身不处理滚动事件,滚动由外部的父容器处理。这种配置适用于列表项数量较少且嵌套在其他可滚动组件中的场景。

每个列表项都是一个药物条目组件,接收药物数据和切换回调函数作为参数。这种通过构造函数传递数据和回调的方式,实现了父子组件之间的单向数据流和事件通信。

统计信息区域显示已服用药物的数量和总数量,使用了 where 方法过滤出已服用的药物并计算长度。这是 Dart 集合操作的典型用法,通过函数式编程的方式简洁地实现了数据统计。

药物条目组件

药物条目组件是一个无状态组件,负责展示单个药物的信息并处理用户的点击交互。虽然它本身不维护状态,但它通过 AnimatedContainer 实现了丰富的视觉动画效果。

手势检测与动画容器

整个药物卡片被包裹在 GestureDetector 中,这使得整个卡片区域都可以响应点击事件。当用户点击时,会触发 onTap 回调,进而调用父组件传入的切换方法。

卡片的主体是一个 AnimatedContainer,这是 Flutter 提供的隐式动画组件。当容器的属性(如颜色、边框、阴影等)发生变化时,它会自动在旧值和新值之间进行插值过渡,产生平滑的动画效果。动画时长设置为 300 毫秒,缓动曲线使用 easeInOut,这种曲线在动画开始和结束时较慢,中间较快,给人自然流畅的感觉。

状态驱动的视觉变化

药物条目组件的视觉效果完全由药物的服用状态驱动。当药物标记为已服用时,卡片的背景色变为主题色的半透明状态,边框变为主题色,同时添加主题色的阴影效果;未服用时则是灰色背景和灰色边框,没有阴影。

这种状态驱动的 UI 设计是 Flutter 声明式编程的核心理念。开发者只需要描述不同状态下界面应该是什么样子,框架会自动处理状态切换时的过渡效果。

卡片内部布局

卡片内部采用横向行布局,从左到右依次是图标区域、文本信息区域和勾选状态指示器。

图标区域使用了 Transform.scale 进行缩放变换,当药物已服用时图标放大到 1.1 倍,给用户一种选中放大的视觉反馈。图标容器也是一个 AnimatedContainer,背景色和图标颜色都会根据服用状态变化,已服用时是主题色背景配白色图标,未服用时是灰色背景配灰色图标。

文本信息区域使用 Expanded 包裹,占据剩余的所有空间。文本区域包含两行:第一行是药物名称,已服用时会加粗并显示为主题色;第二行是剂量和时间信息,已服用时显示为半透明的主题色。通过字体粗细和颜色的变化,清晰地传达了药物的状态信息。

最右侧是一个圆形的勾选指示器,同样使用 AnimatedContainer 实现动画效果。未服用时是一个空心圆,已服用时变为实心圆并显示白色的对勾图标。这个指示器为用户提供了清晰的状态反馈,符合用户对勾选操作的心理预期。

五、状态管理

本项目采用了 Flutter 内置的 setState 状态管理方案,这是最简单直接的状态管理方式,适用于小型应用或组件内部状态的管理。

状态提升模式

应用的状态(药物列表数据)被提升到了用药提醒主组件中进行管理,而药物条目组件作为纯展示组件,不持有任何状态,只通过回调函数通知父组件发生了用户交互。这种模式的优势在于:

第一,单一数据源。所有状态集中在一处管理,避免了数据不一致的问题。第二,组件复用性。子组件只负责展示,可以在不同场景下复用。第三,逻辑清晰。状态变更的逻辑集中在父组件,便于追踪和调试。

状态变更流程

当用户点击药物卡片时,事件的传递路径是:手势检测器检测到点击、调用回调函数、父组件执行状态切换方法、调用 setState 触发重建、组件树重新构建、界面更新。

这个流程体现了 Flutter 单向数据流的设计思想:数据从上往下传递,事件从下往上传递,状态由上层组件统一管理。

六、关键代码详解

动画容器的使用

AnimatedContainer 是本项目中使用最频繁的动画组件,它几乎贯穿了所有需要状态切换动画的地方。其核心原理是监听自身属性的变化,当检测到新旧值不同时,自动创建一个补间动画,在指定的时长内逐渐从旧值过渡到新值。

在药物条目中,AnimatedContainer 被嵌套使用了三层:最外层的卡片容器、中间的图标容器和最右侧的勾选指示器。每一层都有独立的动画时长和曲线,但由于它们的状态变化是同步触发的,整体上形成了协调一致的动画效果。

列表构建器的使用

ListView.builder 是 Flutter 中构建可滚动列表的高效方式。它的构造函数接收 itemCount 和 itemBuilder 两个关键参数:itemCount 指定列表项的总数,itemBuilder 是一个回调函数,根据索引返回对应的列表项组件。

与 ListView 的普通构造函数相比,builder 构造函数的优势在于它是惰性的,只有当列表项即将滚动到可视区域时才会被创建。这对于包含成百上千条数据的长列表来说,性能差异是非常显著的。

主题系统的应用

项目中大量使用了 Theme.of(context) 来获取主题数据,而不是硬编码颜色值。这种做法的好处是主题一致性、易于维护和支持暗色模式。所有组件使用同一套配色方案确保视觉风格统一,如果需要更改应用主题只需修改一处配置即可全局生效,同时可以轻松实现亮色和暗色主题的切换。

例如,药物名称的颜色在已服用状态下使用主题的主色调,这会从主题的配色方案中获取主色调。当主题的种子颜色改变时,所有使用这个主色调的地方都会自动更新。

七、技术总结

本项目虽然功能简单,但代码结构清晰,设计规范,充分展示了 Flutter 开发的最佳实践。

技术亮点

第一,组件化设计。将药物条目抽离为独立组件,提高了代码的复用性和可维护性。第二,隐式动画。大量使用 AnimatedContainer 实现流畅的状态切换动画,提升了用户体验。第三,Material 3 设计。采用最新的 Material Design 3 设计规范,界面现代美观。第四,声明式 UI。通过状态驱动界面变化,代码逻辑清晰易懂。第五,空安全。代码遵循 Dart 空安全规范,使用 required 标记和可空类型处理。

可优化点

第一,数据持久化。当前药物数据是硬编码在代码中的,实际应用中应该从本地存储或服务器获取。第二,状态管理。对于更复杂的应用,可以考虑使用 Provider、Riverpod 或 Bloc 等状态管理方案。第三,通知功能。真正的用药提醒应该集成本地通知,在指定时间提醒用户服药。第四,药物管理。应该提供添加、编辑、删除药物的功能,而不只是展示固定的药物列表。第五,日历视图。可以增加日历视图,让用户查看历史服药记录和未来服药计划。

学习价值

对于 Flutter 初学者来说,这个项目具有很高的学习价值。它涵盖了以下核心知识点:Flutter 应用的基本结构和启动流程、有状态组件和无状态组件的区别与使用场景、setState 状态管理的基本原理和用法、ListView.builder 的使用和懒加载原理、AnimatedContainer 隐式动画的实现方式、GestureDetector 手势检测的应用、主题系统的使用和 Material Design 规范、Dart 数据模型类的设计和构造函数的使用。

总体而言,这是一个结构规范、代码质量较高的 Flutter 入门项目,既展示了 Flutter 框架的核心特性,也为后续的功能扩展预留了良好的架构基础。

请添加图片描述

Flutter for OpenHarmony 实战:药物提醒​ - 定时提醒服药,记录用药历史

目录

前言:跨生态开发的新机遇

在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。

Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。

不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。

无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。

混合工程结构深度解析

项目目录架构

当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:

my_flutter_harmony_app/
├── lib/                          # Flutter业务代码(基本不变)
│   ├── main.dart                 # 应用入口
│   ├── home_page.dart           # 首页
│   └── utils/
│       └── platform_utils.dart  # 平台工具类
├── pubspec.yaml                  # Flutter依赖配置
├── ohos/                         # 鸿蒙原生层(核心适配区)
│   ├── entry/                    # 主模块
│   │   └── src/main/
│   │       ├── ets/              # ArkTS代码
│   │       │   ├── MainAbility/
│   │       │   │   ├── MainAbility.ts       # 主Ability
│   │       │   │   └── MainAbilityContext.ts
│   │       │   └── pages/
│   │       │       ├── Index.ets           # 主页面
│   │       │       └── Splash.ets          # 启动页
│   │       ├── resources/        # 鸿蒙资源文件
│   │       │   ├── base/
│   │       │   │   ├── element/  # 字符串等
│   │       │   │   ├── media/    # 图片资源
│   │       │   │   └── profile/  # 配置文件
│   │       │   └── en_US/        # 英文资源
│   │       └── config.json       # 应用核心配置
│   ├── ohos_test/               # 测试模块
│   ├── build-profile.json5      # 构建配置
│   └── oh-package.json5         # 鸿蒙依赖管理
└── README.md

展示效果图片

flutter 实时预览 效果展示

运行到鸿蒙虚拟设备中效果展示

功能代码实现

应用入口实现

应用的入口文件 main.dart 负责初始化应用并加载主页面。在本项目中,我们直接在首页集成了药物提醒功能,无需额外的页面跳转。

import 'package:flutter/material.dart';
import 'components/medicine_reminder.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter for OpenHarmony',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(title: 'Flutter for OpenHarmony'),
    );
  }
}

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

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: const MedicineReminder(),
    );
  }
}

实现要点

  • 使用 MaterialApp 配置应用主题和标题
  • 通过 Scaffold 构建基本页面结构
  • 直接在 body 中引入 MedicineReminder 组件,实现首页直接展示

药物提醒主组件

medicine_reminder.dart 是整个功能的核心组件,负责管理药物列表、处理状态更新和展示用药统计信息。

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

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

  
  State<MedicineReminder> createState() => _MedicineReminderState();
}

class _MedicineReminderState extends State<MedicineReminder> {
  List<Medicine> medicines = [
    Medicine(
      name: '阿莫西林',
      dosage: '500mg',
      time: '08:00',
      taken: false,
      icon: Icons.medication,
    ),
    Medicine(
      name: '布洛芬',
      dosage: '200mg',
      time: '12:00',
      taken: false,
      icon: Icons.medication,
    ),
    Medicine(
      name: '维生素C',
      dosage: '100mg',
      time: '18:00',
      taken: false,
      icon: Icons.medication_liquid,
    ),
  ];

  void toggleMedicine(int index) {
    setState(() {
      medicines[index].taken = !medicines[index].taken;
    });
  }

  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '今日用药提醒',
            style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                  fontWeight: FontWeight.bold,
                ),
          ),
          const SizedBox(height: 16),
          ListView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: medicines.length,
            itemBuilder: (context, index) {
              return MedicineItem(
                medicine: medicines[index],
                onToggle: () => toggleMedicine(index),
              );
            },
          ),
          const SizedBox(height: 16),
          Text(
            '已服用: ${medicines.where((m) => m.taken).length}/${medicines.length}',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ],
      ),
    );
  }
}

class Medicine {
  final String name;
  final String dosage;
  final String time;
  final IconData icon;
  bool taken;

  Medicine({
    required this.name,
    required this.dosage,
    required this.time,
    required this.icon,
    required this.taken,
  });
}

实现要点

  • 使用 StatefulWidget 管理药物列表状态
  • 定义 Medicine 数据模型,包含药物名称、剂量、服用时间、图标和服用状态
  • 实现 toggleMedicine 方法处理药物服用状态的切换
  • 使用 ListView.builder 动态构建药物列表
  • 实时计算并展示服用情况统计

单个药物项组件

medicine_item.dart 负责展示单个药物的详细信息,并处理点击交互效果。

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

class MedicineItem extends StatelessWidget {
  final Medicine medicine;
  final VoidCallback onToggle;

  const MedicineItem({
    super.key,
    required this.medicine,
    required this.onToggle,
  });

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onToggle,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: medicine.taken
              ? Theme.of(context).colorScheme.primary.withOpacity(0.1)
              : Colors.grey[100],
          borderRadius: BorderRadius.circular(12),
          border: Border.all(
            color: medicine.taken
                ? Theme.of(context).colorScheme.primary
                : Colors.grey[300]!,
            width: 2,
          ),
          boxShadow: medicine.taken
              ? [
                  BoxShadow(
                    color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
                    blurRadius: 8,
                    offset: const Offset(0, 2),
                  ),
                ]
              : [],
        ),
        child: Row(
          children: [
            Transform.scale(
              scale: medicine.taken ? 1.1 : 1.0,
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 300),
                width: 48,
                height: 48,
                decoration: BoxDecoration(
                  color: medicine.taken
                      ? Theme.of(context).colorScheme.primary
                      : Colors.grey[200],
                  borderRadius: BorderRadius.circular(24),
                ),
                child: Icon(
                  medicine.icon,
                  color: medicine.taken ? Colors.white : Colors.grey[600],
                ),
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    medicine.name,
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                          fontWeight: medicine.taken ? FontWeight.bold : null,
                          color: medicine.taken
                              ? Theme.of(context).colorScheme.primary
                              : null,
                        ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '${medicine.dosage} · ${medicine.time}',
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                          color: medicine.taken
                              ? Theme.of(context).colorScheme.primary.withOpacity(0.7)
                              : Colors.grey[600],
                        ),
                  ),
                ],
              ),
            ),
            AnimatedContainer(
              duration: const Duration(milliseconds: 300),
              width: 24,
              height: 24,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                border: Border.all(
                  color: medicine.taken
                      ? Theme.of(context).colorScheme.primary
                      : Colors.grey[400]!,
                  width: 2,
                ),
                color: medicine.taken
                    ? Theme.of(context).colorScheme.primary
                    : Colors.transparent,
              ),
              child: medicine.taken
                  ? const Icon(
                      Icons.check,
                      size: 16,
                      color: Colors.white,
                    )
                  : null,
            ),
          ],
        ),
      ),
    );
  }
}

实现要点

  • 使用 GestureDetector 处理点击事件
  • 使用 AnimatedContainer 实现平滑的状态切换动画
  • 使用 Transform.scale 实现药物图标在服用状态下的缩放效果
  • 通过 BoxDecoration 实现不同状态的视觉区分
  • 使用 Icon 组件展示药物图标和服用状态图标

组件使用方法

  1. 在首页集成药物提醒功能

    body: const MedicineReminder(),
    
  2. 自定义药物列表
    _MedicineReminderState 中修改 medicines 列表:

    List<Medicine> medicines = [
      Medicine(
        name: '阿莫西林',
        dosage: '500mg',
        time: '08:00',
        taken: false,
        icon: Icons.medication,
      ),
      // 添加更多药物...
    ];
    
  3. 调整动画效果
    修改 AnimatedContainerdurationcurve 参数:

    AnimatedContainer(
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
      // ...
    );
    

本次开发中容易遇到的问题

1. 动画实现问题

问题描述:在实现药物服用状态切换时,尝试使用 BoxDecorationtransform 参数实现缩放效果,但遇到语法错误。

解决方案BoxDecoration 不支持 transform 参数,应使用 Transform.scale 包装 AnimatedContainer 来实现缩放效果。

示例代码

// 错误示例
BoxDecoration(
  // ...
  transform: Matrix4.identity()..scale(1.1), // 不支持
  // ...
);

// 正确示例
Transform.scale(
  scale: medicine.taken ? 1.1 : 1.0,
  child: AnimatedContainer(
    // ...
  ),
);

2. 状态管理问题

问题描述:修改药物服用状态后,UI 没有及时更新。

解决方案:确保在状态变化时调用 setState() 方法,通知 Flutter 框架重建 UI。

示例代码

void toggleMedicine(int index) {
  setState(() {
    medicines[index].taken = !medicines[index].taken;
  });
}

3. 布局滚动问题

问题描述:在 Column 中使用 ListView.builder 时,出现布局溢出或滚动冲突。

解决方案:为 ListView.builder 添加 shrinkWrap: truephysics: const NeverScrollableScrollPhysics() 参数,使其适应父容器高度并禁用内部滚动。

示例代码

ListView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  itemCount: medicines.length,
  itemBuilder: (context, index) {
    // ...
  },
);

4. 鸿蒙平台适配问题

问题描述:在鸿蒙设备上运行时,Flutter 动画效果可能不如预期流畅。

解决方案

  • 减少动画复杂度,使用更简单的过渡效果
  • 确保使用 AnimatedContainer 等内置动画组件,避免自定义动画
  • 测试不同动画时长,找到适合鸿蒙平台的最佳值

5. 组件依赖问题

问题描述:在创建组件时,出现循环依赖错误。

解决方案

  • 确保组件之间的依赖关系清晰,避免循环导入
  • 可以考虑将数据模型单独提取到一个文件中,避免循环依赖

总结本次开发中用到的技术点

1. Flutter 核心技术

状态管理

  • 使用 StatefulWidgetsetState() 管理应用状态
  • 实现了基于列表的动态状态更新

布局组件

  • 使用 ColumnRow 构建基础布局
  • 使用 ListView.builder 高效渲染药物列表
  • 使用 Scaffold 构建应用基本结构

动画效果

  • 使用 AnimatedContainer 实现平滑的状态过渡
  • 使用 Transform.scale 实现药物图标缩放动画
  • 使用 Curves.easeInOut 优化动画曲线

交互处理

  • 使用 GestureDetector 处理点击事件
  • 实现了响应式的用户交互反馈

2. 鸿蒙平台适配

混合工程结构

  • 保持 Flutter 代码结构不变
  • 利用 ohos_flutter 插件提供的鸿蒙集成能力
  • 遵循鸿蒙应用的资源管理规范

性能优化

  • 针对鸿蒙平台优化动画性能
  • 确保组件渲染效率
  • 合理使用 Flutter 的布局缓存机制

3. 开发最佳实践

组件化开发

  • 将功能拆分为 MedicineReminderMedicineItem 两个组件
  • 实现了组件间的清晰职责划分
  • 通过回调函数实现组件间通信

代码组织

  • 按功能模块划分文件结构
  • 遵循 Flutter 的代码风格规范
  • 使用有意义的变量和方法命名

用户体验

  • 提供即时的视觉反馈
  • 使用一致的设计语言
  • 确保交互操作的流畅性

可维护性

  • 代码结构清晰,易于理解
  • 组件化设计便于后续扩展
  • 预留了药物数据持久化的扩展空间

通过本次实战,我们成功实现了一个功能完整、交互友好的药物提醒应用,并适配了鸿蒙平台。该应用不仅满足了基本的药物管理需求,还通过精心设计的动画效果提升了用户体验。同时,我们也积累了 Flutter 应用适配鸿蒙平台的宝贵经验,为后续的跨平台开发打下了坚实基础。

请添加图片描述

flutter_openHarmony(简称 Flutter‑OH)

注意:不是Google官方产物,是OpenHarmony社区TPC组织维护的Flutter引擎移植版本。把Flutter的Dart/Skia引擎做底层改造,让Flutter应用可以直接编译输出 HAP包,跑在OpenHarmony/纯血鸿蒙设备上,不需要依赖Android兼容层。

简单讲:一套Dart/Flutter业务代码,可以同时编译 Android、iOS、OpenHarmony(HAP)

核心原理

对Flutter Engine做Embedder嵌入适配,对接OpenHarmony Rosen图形管线、UIAbility生命周期,通过MethodChannel实现 Dart ↔ ArkTS双向通信,Flutter自绘UI渲染到鸿蒙Surface,复用方舟编译器、系统权限、分布式能力。

  • Dart业务代码几乎不变
  • 底层引擎适配鸿蒙图形、线程、生命周期
  • 输出产物是标准HAP应用包,可上架鸿蒙应用市场

主要优势

  1. 存量Flutter项目低成本接入鸿蒙生态
    纯Dart业务、纯Widget界面几乎不用改代码即可编译出鸿蒙HAP;只有带Android/iOS原生桥接的插件,才需要做鸿蒙适配替换。已经有成熟Flutter App,想快速覆盖鸿蒙设备,不用全部重写ArkTS。

  2. 多端UI高度一致性
    Flutter自绘渲染,不受各平台控件差异影响,手机、平板、车机界面表现统一;滚动、动画、首页各类动效(轮播、吸顶、骨架屏、入场动画)跨平台表现一致,和你前面问的App首页各种效果可以一套代码全部实现。

  3. 继承Flutter完整开发体验
    保留热重载、DevTools调试、完整Widget组件库;pub.dev海量纯Dart三方库直接复用,是鸿蒙跨端方案里三方库最丰富的方案。提供定制CLI,一条命令完成编译、真机调试、打包HAP。

  4. 可调用OpenHarmony原生系统能力
    支持调用分布式软总线、分布式数据KV、原子化服务、鸿蒙权限体系、硬件能力;Flutter页面和ArkTS原生页面可以混合开发、互相跳转,复杂原生逻辑继续写ArkTS,UI业务交给Flutter实现。

  5. 全场景设备覆盖
    支持OpenHarmony手机、平板、智慧屏、车机等设备,适合需要多终端统一UI的业务。引擎做了懒加载,跟随UIAbility生命周期启停,控制内存占用,减少后台资源消耗。

Logo

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

更多推荐