1. 为什么把健身俱乐部的活动管理搬到 OpenHarmony,我选 Flutter
先交代一下背景。这个项目不是从零开始造轮子,而是我们一套已经上线运营的健身俱乐部管理系统,需要从存量 App 中拆出一个"活动管理模块",跑在基于 OpenHarmony 的国产设备上。需求本身并不复杂:活动列表、活动日历、在线报名、扫码签到、积分记录,外加一个给运营人员看的后台统计界面。真正让我花时间研究的,反而不是业务逻辑,而是 Flutter 和 OpenHarmony 这对组合的适配深度。
对 Flutter 开发者来说,OpenHarmony 并不是一个可以直接无脑跑 Flutter 的 Android 平替。它有自己的 UI 框架(ArkUI)、自己的应用模型(Stage/FA)、自己的权限机制,甚至控制键返回事件的通路都跟 Android 不一样。但 Flutter 的优势在于,它能用一套 Dart 代码渲染出跨端界面,业务层几乎不需要动,真正需要处理的是"壳子"——也就是承载 Flutter 引擎的原生宿主工程,以及 Dart 与 OpenHarmony 系统能力之间的桥接层。
当前比较成熟的方案是使用 OpenHarmony SIG 维护的 flutter_flutter 和 flutter_engine 的鸿蒙分支,配合 DevEco Studio 构建宿主应用。截至项目落地时,我使用的是 OpenHarmony 4.0 Release + Flutter 3.7.x 分支,这套组合在 rk3568 开发板上的稳定性已经可以接受,能满足实际业务演示和小规模真实运营。如果你现在才开始做选型,建议先查询 OpenHarmony SIG 最新发布的版本支持矩阵,避免用太老的组合把自己卡在编译期。
这个模块面向的使用者有两类:一类是俱乐部的前台人员和私教,关心的是"今天有哪些课""谁报名了""谁还没签到";另一类是会员,关注"怎么报名""怎么查我的积分"。所以文章后面所有的设计和实现,都是围绕这两类角色的关键路径来推进的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目框架设计:核心数据模型与页面架构
2.1 活动管理模块的领域模型设计
在写任何页面之前,先把领域模型定下来。健身俱乐部的活动,和电商、外卖的"活动"不太一样,它的核心是"线下集合 + 时间窗口 + 人数上限 + 教练资源"。基于这个特征,我抽象了下面几个核心实体。
Activity:活动基础信息。包括标题、封面图、开始/结束时间、报名截止时间、活动地点、容纳上限、当前报名人数、活动状态。ActivitySchedule:排期明细。一个活动可以有多期,比如"燃脂搏击"这个活动每周一、周四各有一场,每场都是独立排期,拥有独立的报名名单和签到状态。MemberEnrollment:报名记录。记录会员报名哪一期、报名时间、签到时间、签到码、积分奖励状态等。CoachAssignment:教练排班。标记某个课程由哪位教练带课,方便前台排班和会员查看。MemberPointsRecord:积分流水。报名、签到、带新人、评价课程等行为会产生积分变动,需要可追溯的流水记录。
这个模块用到的数据库表有 6 张,表结构设计遵循一个原则:凡是和"时间"强相关的字段全部存时间戳,展示层再来格式化。这样做的好处是,跨时区、跨设备时不容易出现时间混乱,后面做日历视图的时候也省了不少事。
2.2 端侧页面架构:列表、详情、日历、签到的串联方式
整个模块的页面结构我用的是一个底部导航 + 二级跳转的模型,底部四个 Tab:活动广场、我的日程、消息中心、个人中心。活动广场对应活动列表和筛选;我的日程对应日历视图,日历聚焦的是"我报名的活动和教练带我上的课";消息中心用于报名成功、活动提醒、签到成功后的通知触达;个人中心承载积分与设置。
其中比较考验架构的,是活动详情页。一个会员在活动列表点击某个活动,进入详情,详情页要同时处理活动基础信息、排期选择、报名入口、教练信息展示、相关活动推荐。如果在架构上不做信息拆分,把详情的所有数据塞进一个接口和页面,后患无穷。我实际的拆分方式是:详情页首屏只加载活动基础信息 + 教练信息,排期列表单独用一个接口懒加载,报名操作走独立的提交接口。这样首屏渲染速度会快很多,在 rk3568 这种内存不算宽裕的设备上表现更明显。
以下是 Flutter 侧核心路由的配置代码,统一收口在 routes.dart 中:
dart复制abstract class AppRoutes {
static const String home = '/';
static const String activityDetail = '/activity/detail';
static const String activityCheckIn = '/activity/checkin';
static const String myCalendar = '/calendar/my';
static const String myPoints = '/points/my';
static final Map<String, WidgetBuilder> routes = {
home: (context) => const HomePage(),
activityDetail: (context) => const ActivityDetailPage(),
activityCheckIn: (context) => const CheckInPage(),
myCalendar: (context) => const MyCalendarPage(),
myPoints: (context) => const MyPointsPage(),
};
}
页面之间跳转时,我统一使用 ModalRoute.withName() 并传递参数对象,而不是用 MaterialPageRoute 直接在代码层 new 页面实例。这样做的原因在鸿蒙平台上更突出——鸿蒙的返回键交互、侧滑返回手势会在某些版本上让栈管理变得"有自己的想法",集中式路由管理方便在出问题时统一拦截处理。
2.3 列表分页与本地缓存策略
活动列表是一个高频刷新的场景。会员每天早上打开 App 看当天有哪些课,前台人员整天在列表和详情之间切换。这个场景我用 RefreshIndicator + 自研分页逻辑,每页 10 条,采用"下拉刷新 + 触底加载"的交互模式。接口数据流是标准的 Repository → Bloc → UI 单向流转。
我提供一段缓存层的核心代码,这一段价值很高,因为 OpenHarmony 真机上跑远端接口时,弱网情况很常见,本地缓存能直接决定体验:
dart复制class ActivityRepository {
final ApiProvider _api;
final CacheProvider _cache;
static const _cacheKeyPrefix = 'activity_list:';
Future<List<ActivityEntity>> fetchActivityPage({
required int page,
required int pageSize,
required Map<String, String> filters,
}) async {
final cacheKey = _cacheKeyPrefix + filters.toString();
// 优先返回缓存
if (page == 1) {
final cached = await _cache.getList(cacheKey);
if (cached != null && cached.isNotEmpty) {
return cached;
}
}
try {
