Flutter+OpenHarmony实战:XMB Tracke

前言

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

在 Flutter 适配 OpenHarmony 的过程中,很多开发者最关心的问题并不是"页面怎么写",而是一个真实项目如何处理插件适配本地数据库状态管理路由组织隐私协议鸿蒙工程配置 。本文将基于当前项目 XMB Tracker,按照一个完整跨平台应用的开发链路,拆解它从 Flutter 业务层到 OpenHarmony 工程层的实现过程。

XMB Tracker 是一款离线目标管理与番茄钟应用,项目使用 Flutter + GetX + SQLite + OpenHarmony 适配插件 构建,支持目标创建、完成、归档、排序、番茄钟计时和本地设置保存。本文面向正在学习 Flutter 鸿蒙适配、Flutter 本地数据库开发、GetX 工程化实践的开发者。

本文所有分析均来自当前项目代码,重点关注可复用的工程方法:依赖选型、目录分层、SQLite 迁移、Repository 封装、GetX 状态刷新和 OpenHarmony 插件适配。


一、背景与目标

1.1 项目背景

目标管理和番茄钟是典型的个人效率场景,看似功能简单,但要做成一个可维护的移动端应用,需要处理不少工程问题:

  • 数据需要本地保存,不能每次打开应用都丢失。
  • 目标列表要支持完成、恢复、归档和排序。
  • 番茄钟要记录专注时间,并统计当天数据。
  • 用户设置要持久化,例如专注时长、休息时长、每日目标。
  • 如果运行到 OpenHarmony,还要关注插件是否有鸿蒙侧实现。

因此,XMB Tracker 的价值不只是完成一个小工具,而是提供了一个Flutter 本地应用 + OpenHarmony 适配的完整样例。

1.2 项目目标

本项目的目标可以归纳为四点:

  1. 使用 Flutter 实现跨平台 UI。
  2. 使用 GetX 完成路由、依赖注入和响应式状态管理。
  3. 使用 SQLite 保存目标、番茄钟记录和用户设置。
  4. 使用 OpenHarmony 适配分支解决平台插件兼容问题。

1.3 功能总览

功能模块 核心能力 关键代码
应用入口 初始化数据库、启动路由 lib/main.dart
目标管理 新增、编辑、完成、删除、排序 HomeControllerTabOneView
归档管理 已完成目标归档、按日期分组 GoalRepositoryTabArchiveView
番茄钟 专注计时、休息计时、今日统计 TabTwoViewPomodoroRepository
设置中心 专注时长、休息时长、每日目标 SettingsRepository
鸿蒙适配 OHOS 工程、权限、适配插件 ohos/pubspec.yaml

1.4 技术栈

技术 项目用途 说明
Flutter 跨平台 UI 统一编写业务界面
Dart 业务语言 Flutter 项目主语言
GetX 状态与路由 响应式状态、依赖注入
SQLite 本地数据库 离线保存目标和番茄钟记录
sqflite Flutter SQLite 插件 项目使用 OpenHarmony 适配分支
OpenHarmony 鸿蒙平台 目标运行平台之一

二、环境准备

2.1 开发环境

开发和运行该项目建议准备如下环境:

环境 用途
Flutter SDK 编译和运行 Flutter 项目
Dart SDK 执行 Dart 代码,随 Flutter 提供
DevEco Studio 查看和调试 OpenHarmony 工程
OpenHarmony Flutter SDK 构建鸿蒙 HAP
Git 拉取 Git 方式声明的插件依赖

2.2 基础依赖

项目在 pubspec.yaml 中声明了基础依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.6
  get: ^4.6.6
  flutter_svg: ^2.0.9
  path: ^1.8.3
  simple_animations: ^5.0.2

其中 get 是项目的核心依赖之一,承担状态管理、依赖注入和路由能力。

2.3 OpenHarmony 适配依赖

项目中与平台能力相关的插件使用了鸿蒙适配分支:

yaml 复制代码
path_provider:
  git:
    url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
    path: "packages/path_provider/path_provider"
    ref: "master"

sqflite:
  git:
    url: "https://gitcode.com/openharmony-sig/flutter_sqflite.git"
    path: "sqflite"
    ref: "br_v2.3.3+1_ohos"

对于 OpenHarmony 适配项目,插件是否有平台实现非常关键。UI 代码可以跨平台,但数据库、文件路径、系统分享、文件选择等能力通常都需要平台侧支持。

2.4 常用命令

bash 复制代码
flutter pub get
flutter analyze
flutter test
flutter build hap

如果你使用的是定制版 Flutter OHOS SDK,构建命令可能略有差异,应以本地 SDK 文档为准。可以同时参考 OpenHarmony 文档中心 的工程构建说明。

建议先保证普通 Flutter 构建链路通过,再处理 OpenHarmony 构建问题。这样可以快速区分是 Dart 业务错误,还是鸿蒙平台配置问题。


三、项目结构设计

3.1 根目录结构

项目根目录如下:

text 复制代码
xmbtracker/
├── assets/
├── lib/
├── ohos/
├── test/
├── pubspec.yaml
├── analysis_options.yaml
└── README.md

各目录职责如下:

  • assets:图片和音频资源目录。
  • lib:Flutter 主业务代码。
  • ohos:OpenHarmony 平台工程。
  • test:测试代码。
  • pubspec.yaml:依赖、资源和版本配置。

3.2 lib 目录结构

text 复制代码
lib/
├── main.dart
└── app/
    ├── core/
    ├── data/
    ├── modules/
    ├── pages/
    ├── policy/
    ├── routes/
    └── widgets/

这个目录结构体现了清晰的分层思想:

  1. core 放主题、翻译等基础能力。
  2. data 放模型、数据库服务和数据仓库。
  3. modules 按业务模块组织页面、控制器和绑定。
  4. routes 统一管理页面路由。
  5. policy 放用户协议和隐私政策。

3.3 架构流程图

图示说明:XMB Tracker 采用 View、Controller、Repository、DbService、SQLite 的分层链路。发布文章时建议替换为真实项目截图或架构图。

3.4 数据流说明

text 复制代码
用户点击页面
  ↓
View 触发 Controller 方法
  ↓
Controller 调用 Repository
  ↓
Repository 访问 DbService
  ↓
SQLite 返回数据
  ↓
Rx 状态更新并刷新 UI

这种结构可以避免页面层直接拼 SQL,也能让数据库逻辑更容易测试和替换。


四、应用入口与路由配置

4.1 main.dart 初始化

应用入口代码如下:

dart 复制代码
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Get.putAsync<DbService>(() async => DbService().init(), permanent: true);
  runApp(const MyApp());
}

这里先初始化 Flutter Binding,再初始化 DbService。数据库作为全局服务,在应用启动前完成准备,可以避免页面进入后再等待数据库打开。

4.2 GetMaterialApp 配置

dart 复制代码
return GetMaterialApp(
  title: 'XMB Tracker',
  theme: AppTheme.light,
  darkTheme: AppTheme.dark,
  translations: AppTranslations(),
  locale: const Locale('en', 'US'),
  fallbackLocale: const Locale('en', 'US'),
  initialRoute: Routes.HOME,
  getPages: AppPages.routes,
  unknownRoute: AppPages.unknownRoute,
  debugShowCheckedModeBanner: false,
);

GetMaterialApp 集中配置主题、国际化、路由和未知页面,是 GetX 项目比较典型的入口组织方式。

4.3 路由表

dart 复制代码
class AppPages {
  static final List<GetPage<dynamic>> routes = <GetPage<dynamic>>[
    GetPage(
      name: Routes.HOME,
      page: () => const HomeView(),
      binding: HomeBinding(),
    ),
  ];

  static final GetPage<dynamic> unknownRoute = GetPage(
    name: '/notfound',
    page: () => const NotFoundView(),
  );
}

虽然当前项目主页面不多,但提前建立路由表,有利于后续扩展设置页、统计页、详情页。

4.4 依赖绑定

dart 复制代码
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<GoalRepository>(() => GoalRepository(), fenix: true);
    Get.lazyPut<HomeController>(() => HomeController());
  }
}

Binding 负责声明页面依赖,页面进入时注入 Controller 和 Repository,避免 View 层手动创建对象。


五、SQLite 数据库设计

5.1 DbService 职责

DbService 负责打开数据库、创建表和执行迁移:

dart 复制代码
class DbService extends GetxService {
  late Database db;

  Future<DbService> init() async {
    final Directory docsDir = await getApplicationDocumentsDirectory();
    final String dbPath = p.join(docsDir.path, 'xmbtracker.db');
    db = await openDatabase(
      dbPath,
      version: 5,
      onCreate: (Database db, int version) async {
        // 创建表
      },
      onUpgrade: (Database db, int oldVersion, int newVersion) async {
        // 执行迁移
      },
    );
    return this;
  }
}

这里使用 getApplicationDocumentsDirectory() 获取数据库目录,对移动端和 OpenHarmony 适配都更加稳妥。

5.2 数据表清单

表名 作用 关键字段
goals 保存目标 namedue_datestatuspriorityarchived
checkins 保存打卡日期 date
pomodoro_sessions 保存番茄钟记录 started_atduration_minutesis_break
settings 保存用户设置 keyvalue

5.3 goals 表

sql 复制代码
CREATE TABLE goals (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  due_date TEXT NOT NULL,
  status INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  priority INTEGER NOT NULL DEFAULT 0,
  archived INTEGER NOT NULL DEFAULT 0,
  sort_index INTEGER NOT NULL DEFAULT 0,
  archive_date TEXT
);

字段说明:

  • status:目标状态,0 表示进行中,1 表示已完成。
  • priority:优先级,用于排序。
  • archived:是否进入归档。
  • sort_index:手动排序时的顺序值。
  • archive_date:归档时间,用于历史记录分组。

5.4 settings 表

sql 复制代码
CREATE TABLE settings (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

settings 使用 key-value 结构,适合保存少量配置,例如专注时长、休息时长、每日目标、排序方式、隐私协议确认状态。


六、数据模型与 Repository 封装

6.1 Goal 模型

dart 复制代码
class Goal {
  final int? id;
  final String name;
  final DateTime dueDate;
  final int status;
  final DateTime createdAt;
  final int priority;
  final bool archived;
  final int sortIndex;
  final DateTime? archiveDate;
}

模型层使用 Dart 类型表达业务含义,比如 archivedbool,日期用 DateTime,避免页面层直接处理数据库字段。

6.2 业务计算属性

dart 复制代码
bool get isCompleted => status == 1;

bool get isOverdue =>
    !isCompleted &&
    DateTime.now().isAfter(
      DateTime(dueDate.year, dueDate.month, dueDate.day, 23, 59, 59),
    );

这些 getter 可以让 View 层直接使用 goal.isCompletedgoal.isOverdue,提升代码可读性。

6.3 GoalRepository 新增数据

dart 复制代码
class GoalRepository {
  final DbService _db = Get.find<DbService>();

  Future<int> insert(Goal goal) async {
    return _db.db.insert(
      'goals',
      goal.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }
}

Repository 的核心作用是封装数据访问细节,让 Controller 面向业务方法编程。

6.4 目标查询与排序

dart 复制代码
Future<List<Goal>> getAllByPriorityDesc() async {
  final List<Map<String, dynamic>> maps = await _db.db.query(
    'goals',
    where: 'archived=0',
    orderBy: 'priority DESC, due_date ASC',
  );
  return maps.map(Goal.fromMap).toList();
}

目标支持按时间、按优先级、按手动顺序排序。其中按优先级排序会先展示高优先级目标,再按截止日期排列。

6.5 一键归档

dart 复制代码
Future<void> archiveCompleted() async {
  final String nowIso = DateTime.now().toIso8601String();
  await _db.db.update(
    'goals',
    <String, Object?>{'archived': 1, 'archive_date': nowIso},
    where: 'status=1 AND archived=0',
  );
}

归档不是删除数据,而是修改目标状态。这样用户可以在归档页继续查看历史记录。


七、GetX 状态管理实现

7.1 HomeController 状态定义

dart 复制代码
final RxInt currentIndex = 0.obs;
final RxList<Goal> goals = <Goal>[].obs;
final RxInt inProgressCount = 0.obs;
final RxInt completedCount = 0.obs;
final RxInt archivedCount = 0.obs;
final RxString sortMode = 'time'.obs;
final RxString filterMode = 'ongoing'.obs;

这些状态会被页面中的 Obx 监听。数据改变后,UI 自动刷新,无需手动调用 setState

7.2 刷新全部状态

dart 复制代码
Future<void> refreshAll() async {
  final String mode = await _settings
      .getInt('sort_mode', 0)
      .then((int v) => v == 0 ? 'time' : v == 1 ? 'priority' : 'manual');
  sortMode.value = mode;

  final List<Goal> list;
  if (filterMode.value == 'completed') {
    list = await _repo.getCompletedByTime();
  } else if (mode == 'priority') {
    list = await _repo.getAllByPriorityDesc();
  } else if (mode == 'manual') {
    list = await _repo.getAllByManual();
  } else {
    list = await _repo.getAllAsc();
  }

  goals.assignAll(list);
}

refreshAll 是目标管理状态的统一刷新入口。新增、编辑、完成、归档后都调用它,保证页面数据和数据库一致。

7.3 新增目标

dart 复制代码
Future<void> addGoal(String name, DateTime dueDate, {int priority = 0}) async {
  final Goal goal = Goal(
    name: name,
    dueDate: dueDate,
    status: 0,
    createdAt: DateTime.now(),
    priority: priority,
  );
  await _repo.insert(goal);
  await refreshAll();
}

Controller 负责补齐默认字段,例如新目标默认是进行中,创建时间取当前时间。

7.4 完成与恢复

dart 复制代码
Future<void> toggleComplete(Goal goal) async {
  final int newStatus = goal.isCompleted ? 0 : 1;
  await _repo.setStatus(goal.id!, newStatus);
  await refreshAll();
}

这个方法同时支持完成目标和恢复目标,页面只需要传入当前目标对象。


八、页面交互与番茄钟实现

8.1 HomeView 页面结构

dart 复制代码
return Scaffold(
  body: Obx(() => IndexedStack(
        index: controller.currentIndex.value,
        children: pages,
      )),
  bottomNavigationBar: Obx(() => NavigationBar(
        selectedIndex: controller.currentIndex.value,
        onDestinationSelected: controller.onTabSelected,
        height: 64,
        destinations: <Widget>[
          NavigationDestination(icon: Icon(Icons.flag_outlined), label: '目标'),
          NavigationDestination(icon: Icon(Icons.timer_outlined), label: '专注'),
          NavigationDestination(icon: Icon(Icons.archive_outlined), label: '归档'),
          NavigationDestination(icon: Icon(Icons.grid_view), label: '功能'),
        ],
      )),
);

首页使用 IndexedStack 保留 Tab 状态,切换页面时不会销毁已有页面。

8.2 目标页面交互

目标页包含以下交互:

  • 顶部渐变区域显示标题和日期。
  • 统计区域展示进行中和已完成数量。
  • ChoiceChip 切换进行中和已完成目标。
  • ReorderableListView 支持手动排序。
  • 底部操作栏支持归档和排序切换。

8.3 番茄钟状态

dart 复制代码
int _remainingSeconds = 25 * 60;
bool _isRunning = false;
bool _isBreak = false;
int _todayFocusMinutes = 0;
DateTime? _sessionStart;
int _focusMinutes = 25;
int _breakMinutes = 5;
int _dailyTarget = 4;

这些字段分别保存倒计时状态、是否休息、今日专注分钟和用户设置。

8.4 启动计时

dart 复制代码
void _start({bool isBreak = false}) {
  if (_isRunning) return;
  setState(() {
    _isBreak = isBreak;
    _remainingSeconds = (isBreak ? _breakMinutes : _focusMinutes) * 60;
    _isRunning = true;
    _sessionStart = DateTime.now();
  });
  _tick();
}

启动计时时会根据专注或休息模式设置剩余时间,并记录本轮开始时间。

8.5 今日专注统计

dart 复制代码
Future<int> countTodayFocusMinutes() async {
  final DateTime now = DateTime.now();
  final DateTime start = DateTime(now.year, now.month, now.day);
  final DateTime end = start.add(const Duration(days: 1));
  final List<Map<String, Object?>> rows = await _db.db.rawQuery(
    'SELECT SUM(duration_minutes) as total FROM pomodoro_sessions '
    'WHERE is_break=0 AND started_at>=? AND started_at<?',
    <Object?>[start.toIso8601String(), end.toIso8601String()],
  );
  final Object? v = rows.first['total'];
  if (v is num) return v.toInt();
  return 0;
}

统计时只计算 is_break=0 的专注记录,不把休息时间计入今日专注时长。


九、OpenHarmony 工程适配

9.1 ohos 目录

text 复制代码
ohos/
├── AppScope/
├── entry/
├── build-profile.json5
├── hvigorfile.ts
├── oh-package.json5
└── package.json

lib 中的 Flutter 业务代码负责页面和逻辑,ohos 目录负责 OpenHarmony 平台侧工程配置。

9.2 module.json5 配置

json 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone"
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

这里声明了入口 Ability、设备类型和权限。由于 XMB Tracker 核心能力是离线使用,如果后续没有网络同步或在线内容,可以评估是否移除 INTERNET 权限。

9.3 插件适配检查

插件 OpenHarmony 关注点
path_provider 是否能返回可写的应用文档目录
sqflite 是否能正常打开数据库并执行迁移
file_selector 平台文件选择器是否注册成功
share_plus 系统分享能力是否可用

9.4 适配建议

  1. 先确认 Dart 侧没有语法和分析错误。
  2. 再确认插件依赖是否来自 OpenHarmony 适配分支。
  3. 然后检查 ohos/entry/src/main/module.json5 权限和入口配置。
  4. 最后使用真机或模拟器验证数据库、路径和页面功能。

Flutter 适配 OpenHarmony 时,最常见的问题不是 Widget 不能用,而是插件没有平台实现、依赖分支不对或 OHOS 工程配置不完整。

总结

本文严格围绕 Flutter 第三方库适配 OpenHarmony【XMB Tracker】 项目展开,按照背景目标、环境准备、项目结构、入口路由、数据库设计、数据模型、Repository、GetX 状态、页面交互、鸿蒙适配和常见问题的顺序,完整拆解了一个离线目标管理与番茄钟应用的实现方式。

这个项目最值得借鉴的地方有三点:第一,使用 SQLite 保证离线数据可靠保存;第二,使用 Repository Pattern 隔离 SQL 和业务逻辑;第三,使用 OpenHarmony 适配插件 解决 Flutter 插件在鸿蒙平台上的平台实现问题。

后续如果继续优化,建议优先处理中文编码统一、Repository 单元测试、番茄钟后台计时校准和权限最小化。完成这些后,XMB Tracker 可以成为一个更完整的 Flutter + OpenHarmony 实战模板。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作 Flutter 与 OpenHarmony 实战内容的动力!


相关资源:

相关推荐
大雷神2 小时前
第21篇|侧边导航:平板和 2in1 为什么不照搬手机布局
harmonyos
脑极体10 小时前
点亮星河AI+鸿蒙,一座艺术场馆的日神觉醒
人工智能·华为·harmonyos
●VON10 小时前
鸿蒙Flutter实战:分类管理页BottomSheet CRUD
数据库·flutter·华为·harmonyos·鸿蒙
GitCode官方13 小时前
开源鸿蒙 PC 直播回顾|从环境搭建到真机验证:鸿蒙 PC 命令行迁移全链路。
华为·开源·harmonyos
想你依然心痛14 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR文化遗产数字修复工坊
华为·ar·harmonyos·智能体
woodWu14 小时前
Flutter 复杂拖拽排序实战:同源排序 + 跨容器拖拽完整落地
flutter
小小小小小鹿14 小时前
Vibe Coding 实战:Flutter 自定义路径布局
flutter·vibecoding
程序员老刘17 小时前
Dart 3.12 更新要点:乏善可陈
flutter·ai编程·dart