☰
Flutter在OpenHarmony上的每日计划功能实战:跨端开发与状态管理
2026/10/8 2:38:01 网站建设 项目流程

做智慧学习助手App时,我毫不犹豫选用了Flutter来跑OpenHarmony设备。这个"flutter_for_openharmony"工程里,每日计划功能是整个产品的核心骨架,背后涉及跨端渲染、状态管理、本地持久化三件事的协同。这篇实战记录会完整还原我在OpenHarmony上用Flutter实现每日计划的全过程,包括工程搭建、数据层设计、Provider状态管理、组件通信、性能调优和踩坑实录,适合正在折腾Flutter跨端开发、又对OpenHarmony生态感兴趣的开发者参考。

1. 项目背景与整体设计思路

1.1 为什么在OpenHarmony上用Flutter

很多人会问,OpenHarmony原生开发用ArkTS不香吗?香,尤其是你要做系统级服务的时候。但我的实际情况是团队里已经有成熟的Flutter业务代码库,核心功能模块、组件封装都沉淀了两年多。如果全部用ArkTS重写,成本至少要翻三倍,而且后续维护需要同时维护两套UI代码。

Flutter的跨端优势在于渲染引擎自绘,不依赖系统原生控件。在OpenHarmony上,Flutter的适配层把Dart代码映射到OpenHarmony的Ability框架上,UI依然由Skia/Impeller绘制。这意味着我可以在Android、iOS、OpenHarmony三端共享业务逻辑和UI代码。每日计划这种高频交互页面,用Flutter开发一次,三端同步更新,收益非常可观。

另一个现实因素是生态进度。OpenHarmony的Flutter适配现在已经走通了主流组件和插件通道,虽然还比不上Android那么成熟,但跑一个以列表、表单、数据展示为主的应用已经足够稳。我实测下来,列表滑动、页面切换、输入交互的流畅度都能压到60帧,日常使用完全没有问题。

1.2 智慧学习助手的模块拆解与每日计划定位

整套智慧学习助手我拆成了五个模块:每日计划、学习计时、知识卡片、数据统计、错题本。每日计划排在最前面,因为它承担了整个App的启动引导和用户习惯养成角色。用户打开App的第一件事,就是看一眼今天的计划安排,勾掉已完成的任务。

每日计划功能的需求可以拆成这几个点:

  • 以“天”为单位展示计划列表,支持切换日期查看历史计划。
  • 每条计划包含时间段、任务内容、学科标签、完成状态。
  • 支持新增、编辑、删除计划,点击复选框切换完成状态。
  • 顶部展示当天完成进度条,完成率达到100%时给出激励提示。
  • 计划数据要本地持久化,离线可看,重启不丢。

这个功能看起来不复杂,但真正落地的时候,数据模型设计、日期切换的性能、状态同步逻辑、跨组件刷新,每个环节都有细节。我会在后面的章节逐一展开。

1.3 工程结构与技术选型

工程名就叫flutter_for_openharmony,整体结构采用模块化分层:

lib/ ├── main.dart # 入口,初始化Provider ├── models/ # 数据模型 │ └── plan_task.dart ├── pages/ │ ├── home_page.dart # 主页,包含日期选择器和计划列表 │ └── add_task_page.dart # 新建/编辑计划页 ├── providers/ │ └── task_provider.dart # 每日计划状态管理 ├── repository/ │ └── task_repository.dart # 本地存储封装 ├── widgets/ │ ├── progress_card.dart # 进度卡片 │ └── task_tile.dart # 计划列表项 └── utils/ └── date_utils.dart # 日期工具类

技术选型上面,存储方案用了SharedPreferences,而不是SQLite。每日计划的数据量天然很小,一个人一天最多几十条任务,用轻量KV存储完全够用,而且读写速度快、无需额外初始化数据库。状态管理选了Provider,理由是它轻量、学习曲线平缓,而且Flutter官方文档推荐,社区里资料最多,团队新人上手快。

2. 环境准备与工程创建

2.1 环境搭建:Flutter SDK与OpenHarmony侧的配合

网上关于"flutter安装与配置windows"的教程一抓一大把,但在OpenHarmony上跑Flutter,你需要额外装OpenHarmony的SDK和DevEco Studio。这里有一个关键认知:OpenHarmony的Flutter适配是通过flutter_flutter仓库的ohos分支实现的,不是你从flutter官网下载那个普通SDK就能直接跑。

我当时卡了很久才发现这个坑。普通Flutter工程用flutter run直接跑的是Android或桌面端,要跑OpenHarmony设备,必须用OpenHarmony的SDK侧编译链去构建hap包。具体来说:

# 克隆OpenHarmony适配版Flutter SDK git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git # 配置环境变量 export PATH="$PATH:/path/to/flutter_flutter/bin" export OHOS_SDK_HOME="/path/to/ohos-sdk"

配置完成后用flutter doctor检查,如果能看到OpenHarmony相关的诊断项,说明环境基本就绪。注意DevEco Studio里配置的SDK路径要和OHOS_SDK_HOME保持一致,否则编译时会报找不到SDK。

2.2 创建flutter_for_openharmony工程与目录说明

创建工程不要用flutter create的默认模板,因为默认模板没有OpenHarmony的工程壳。正确步骤是:

flutter create --org com.example -t app flutter_for_openharmony

然后在工程根目录执行:

flutter pub get flutter build hap --debug

这里的关键点是检查ohos目录是否生成。OpenHarmony适配版的Flutter工具链会自动生成ohos目录,里面是OpenHarmony的Ability工程壳,类似于Android工程里的android目录。如果没生成,检查Flutter SDK版本和flutter doctor里的OpenHarmony通道是否启用。

App显示的App名称在ohos/app.json5和module.json5里配置,这个和Android的AndroidManifest.xml是对应关系。我建议一开始就把包名、版本号、应用图标都配置好,免得后面发布的时候再改,容易出低级错误。XTS认证测试也会检查这些基础信息的完整性。

2.3 依赖配置与flutter pub get实战

每日计划功能用到的依赖一共四个:

dependencies: flutter: sdk: flutter provider: ^6.1.1 # 状态管理 shared_preferences: ^2.2.2 # 本地存储 intl: ^0.18.1 # 日期格式化 uuid: ^3.0.7 # 生成任务ID

第一个坑出现在shared_preferences。OpenHarmony上直接跑Android版的shared_preferences插件可能会因为通道实现不一致而报错。我当时用的做法是在pubspec.yaml里通过git依赖引入OpenHarmony适配版的插件:

shared_preferences: git: url: https://gitee.com/openharmony-sig/flutter_packages.git path: packages/shared_preferences/shared_preferences

这种适配版的插件在OpenHarmony上通过Platform Channel走的是Ohos的通道实现,而不是Android的Java实现。另一个经验是不要一次性引入太多插件,OpenHarmony的插件生态还在成长中,每引入一个第三方插件前,先去查一下有没有OpenHarmony适配版本,防止编译不过。

3. 每日计划功能的数据层设计

3.1 数据模型设计:任务对象与状态枚举

好代码从好模型开始。每日计划的任务对象我定义了这些字段:

enum TaskStatus { pending, // 未开始 completed, // 已完成 } enum Subject { chinese, math, english, science, other, } class PlanTask { final String id; // 唯一ID,用uuid生成 final String title; // 任务标题 final String? remark; // 备注 final Subject subject; // 学科标签 final DateTime planDate; // 计划日期,只取日期部分 final DateTime startTime; // 开始时间 final DateTime endTime; // 结束时间 final TaskStatus status; // 完成状态 final int studyMinutes; // 预计学习时长(分钟) final int createdAt; // 创建时间戳 }

几个设计时的考虑想分享一下:

planDate单独拎出来存日期,不要和startTime混在一起。因为日期筛选是每天的核心操作,如果存完整时间戳,筛选时还要做日期范围换算,容易出边界问题。我把日期规范化成当天的零点,比较的时候直接用==判断两个日期的year、month、day是否相等,简洁且不容易出错。

status用枚举而不是布尔值,是为了后续扩展。比如以后想加一个“进行中”或者“已逾期”的状态,枚举只需要加一个值,不影响其他逻辑。

studyMinutes这个字段看起来是冗余的,因为startTime和endTime相减就能算出来。但它的价值在于用户可能直接输入学习时长,而不是选择起止时间。存一份计算好的字段,统计和展示的时候少做一次运算。

3.2 本地存储:基于SharedPreferences的持久化方案

存储层的设计目标是:读写方便、序列化稳定、查询高效。我用SharedPreferences存JSON字符串,key的设计规则是plan_task_yyyy-MM-dd,value是一个JSON数组,每个元素是一条完整任务。

这样设计的好处是,按日期加载任务时只需要一次getString操作,不需要遍历全部数据再筛选。一天的JSON数组顶多几十KB,性能完全不是问题。

class TaskRepository { static const _keyPrefix = 'plan_task_'; final SharedPreferences _prefs; TaskRepository(this._prefs); /// 加载某天的计划 Future<List<PlanTask>> loadTasksByDate(DateTime date) async { final key = _buildKey(date); final jsonString = _prefs.getString(key); if (jsonString == null || jsonString.isEmpty) { return []; } final list = jsonDecode(jsonString) as List<dynamic>; return list .map((item) => PlanTask.fromJson(item as Map<String, dynamic>)) .toList(); } /// 保存某天的全部计划 Future<void> saveTasksByDate(DateTime date, List<PlanTask> tasks) async { final key = _buildKey(date); final jsonArray = jsonEncode( tasks.map((task) => task.toJson()).toList(), ); await _prefs.setString(key, jsonArray); } String _buildKey(DateTime date) { final dateStr = DateFormat('yyyy-MM-dd').format(date); return '$_keyPrefix$dateStr'; } }

这里有一个很重要的注意点:每次修改任务后必须调用saveTasksByDate,把整天的列表重新写回,而不是只改一条单独存一条。因为我的存储粒度是“天”,单独存一条会导致同一天的记录被拆成两个key,加载的时候要合并,逻辑就乱了。虽然每次都写全部数据看起来有点傻,但实际上一天的数据量非常小,序列化和写入耗时都在毫秒级,完全无感。

3.3 日期筛选与计划进度的核心算法

每日计划有一个关键需求——切日期看历史计划。这里我直接用一个横向滚动的日期选择器,展示最近14天和未来几天。切换日期时,Provider会重新加载那天的任务列表。

日期工具类里两个函数是核心:

class DateUtils { /// 判断两个日期是否为同一天 static bool isSameDay(DateTime a, DateTime b) { return a.year == b.year && a.month == b.month && a.day == b.day; } /// 获取某周的起始日(周一) static DateTime getWeekStart(DateTime date) { final weekday = date.weekday; // 1=周一,7=周日 return DateTime(date.year, date.month, date.day - (weekday - 1)); } /// 将时间戳格式化为 HH:mm static String formatTime(DateTime time) { return DateFormat('HH:mm').format(time); } }

进度计算逻辑放在Provider里,遍历当天的任务列表统计完成比例:

double get progress { if (_tasks.isEmpty) return 0; final completedCount = _tasks.where((t) => t.status == TaskStatus.completed).length; return completedCount / _tasks.length; }

有个细节我踩过坑:在计算进度时,要排除掉未到达开始时间的任务。比如现在是上午9点,有一条计划是晚上8点背单词,它还没到开始时间,不应该算进今天的总任务数里,否则进度条永远达不到100%,用户会很困惑。修正后的逻辑是:

double get progress { final now = DateTime.now(); final availableTasks = _tasks.where((t) => !t.startTime.isAfter(now) || t.status == TaskStatus.completed ).toList(); if (availableTasks.isEmpty) return 0; final completedCount = availableTasks.where((t) => t.status == TaskStatus.completed).length; return completedCount / availableTasks.length; }

4. 状态管理与页面实现

4.1 状态管理的选择:Provider为什么比setState和Riverpod更适合

每日计划涉及多个页面和组件的状态共享,比如旁边的时间线组件需要实时反映进度,底部统计卡片要展示完成任务数量。如果用setState,所有状态都堆在HomePage里,代码会迅速膨胀且难以维护。Riverpod功能更强但学习成本偏高,团队协作需要统一下心智。Provider恰好处于够用且简单的平衡点。

使用Provider时我强烈建议配合ChangeNotifier使用,这才是flutter provider的正确打开方式。核心代码:

class TaskProvider extends ChangeNotifier { final TaskRepository _repository; List<PlanTask> _tasks = []; DateTime _selectedDate = DateTime.now(); TaskProvider(this._repository); List<PlanTask> get tasks => _tasks; DateTime get selectedDate => _selectedDate; double progress => _calculateProgress(); /// 切换日期并加载对应任务 Future<void> loadTasksForDate(DateTime date) async { _selectedDate = date; _tasks = await _repository.loadTasksByDate(date); notifyListeners(); } /// 新增任务 Future<void> addTask(PlanTask task) async { _tasks.add(task); await _repository.saveTasksByDate(_selectedDate, _tasks); notifyListeners(); } /// 切换任务完成状态 Future<void> toggleTask(String taskId) async { final index = _tasks.indexWhere((t) => t.id == taskId); if (index == -1) return; final current = _tasks[index]; final updated = PlanTask( ...current, status: current.status == TaskStatus.completed ? TaskStatus.pending : TaskStatus.completed, ); _tasks[index] = updated; await _repository.saveTasksByDate(_selectedDate, _tasks); notifyListeners(); } }

这段代码的核心是notifyListeners()。所有依赖TaskProvider的组件,在监听时收到通知后会自动重建,从而保持界面状态同步。我在这个App里的每个页面都通过context.watch<TaskProvider>()来读取状态,确保页面响应式更新。

4.2 每日计划列表页:时间轴布局与交互设计

UI布局参考了日历和时间线应用的常见设计。页面顶部是横向日期选择器,中间是进度卡片,下方是计划列表。计划列表的每一项卡片包含时间段、任务标题、学科标签和完成复选框。

核心代码如下,TaskTile组件负责展示单条任务:

class TaskTile extends StatelessWidget { final PlanTask task; final VoidCallback onToggle; const TaskTile({ super.key, required this.task, required this.onToggle, }); @override Widget build(BuildContext context) { final isCompleted = task.status == TaskStatus.completed; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: InkWell( onTap: () { // 点击卡片跳转编辑页 Navigator.push( context, MaterialPageRoute( builder: (_) => AddTaskPage(task: task), ), ); }, child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // 复选框 Checkbox( value: isCompleted, onChanged: (_) => onToggle(), ), // 时间 + 内容 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${DateFormat('HH:mm').format(task.startTime)} - ' '${DateFormat('HH:mm').format(task.endTime)}', style: TextStyle( fontSize: 12, color: isCompleted ? Colors.grey : Colors.blueGrey, ), ), const SizedBox(height: 4), Text( task.title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, decoration: isCompleted ? TextDecoration.lineThrough : null, ), ), ], ), ), // 学科标签 Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _subjectColor(task.subject).withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( _subjectName(task.subject), style: TextStyle(fontSize: 12, color: _subjectColor(task.subject)), ), ), ], ), ), ), ); } }

列表本身我用了ListView.builder,而不是ListView直接包children。数据少的时候区别不大,但计划一多,ListView.builder的按需构建优势就出来了,滑动性能明显更好。这个原则在所有长列表场景都适用。

4.3 新建计划与完成任务的关键细节

新建计划页是一个表单页,需要输入任务标题、选择学科、设置开始时间和结束时间。表单用了GlobalKey<FormState>做校验,标题必填且不超过30个字,结束时间必须晚于开始时间。

提交逻辑:

void _submit() { if (!_formKey.currentState!.validate()) return; final title = _titleController.text.trim(); final start = DateTime( _selectedDate.year, _selectedDate.month, _selectedDate.day, _startTime.hour, _startTime.minute, ); final end = DateTime( _selectedDate.year, _selectedDate.month, _selectedDate.day, _endTime.hour, _endTime.minute, ); final task = PlanTask( id: const Uuid().v4(), title: title, remark: _remarkController.text.trim(), subject: _subject, planDate: _selectedDate, startTime: start, endTime: end, status: TaskStatus.pending, studyMinutes: end.difference(start).inMinutes, createdAt: DateTime.now().millisecondsSinceEpoch, ); context.read<TaskProvider>().addTask(task); Navigator.pop(context); }

完成任务的操作就是点击Checkbox,调用toggleTask更新状态。这里有个交互细节我特意处理过:完成任务的瞬间,给了一个小的动画反馈,让用户有“打勾”的满足感。实现方式是给TaskTile包一层AnimatedSwitcher,切换状态时透明度过渡。

如果你从来没做过类似功能,建议优先把状态正确性搞定再谈动画。先确保切换日期、新增、勾选这些操作数据不丢、界面正确刷新,再逐步打磨动效。我见过很多新手一上来就研究动画,结果数据没存好,重启后任务全没了,反而被bug困住半天。

5. 组件通信与跨页联动

5.1 Flutter组件通信的三种方式实战

做每日计划功能,不可避免要处理组件间通信。我最常用的是三种方式,按复杂度从低到高排列。

第一种是父传子,通过构造参数传值或回调函数。比如TaskTile接收task和onToggle回调,就是典型的父传子通信。优点是简单直接,数据流清晰。

第二种是子传父,通过回调函数把子组件的操作通知给父组件。比如TaskTile里的复选框被点击时,调用onToggle(),父组件TaskList去执行实际的状态变更逻辑。这套模式在处理“UI组件无状态化”时特别有效。几乎所有列表项组件都应该设计成无状态、受控组件,状态集中在Provider里,这样调试和复用都方便。

第三种是跨层共享,使用Provider或InheritedWidget。当页面A改了数据,页面B需要同步刷新时,Provider就会派上用场。我的每日计划功能里,进度卡片和计划列表同时依赖TaskProvider里的tasks数据,这种多组件共享同一状态源的场景,只能用跨层通信解决。

还有一种特殊情况——跨页面通信。比如用户在编辑页改了任务标题,返回列表页后需要刷新。我用Navigator.pop返回后,列表页的addListener会被触发,因为addTask已经调用了notifyListeners(),列表页的context.watch<TaskProvider>()会感知到变化并重建。这个机制是Provider自动完成的,不需要手动传参。

5.2 每日计划与全局学习数据的联动

每日计划不是孤立的,它要和学习计时、数据统计联动。具体地说:每完成一条计划,应该累计学习时长到全局统计中;每天的计划完成情况,要生成一张趋势图放在数据统计页。

我把这个联动逻辑做在了Provider层,而不是组件层。原因很简单:组件层做联动,UI和业务逻辑会缠在一起,越写越乱。Provider层做联动,所有状态变化都收敛到同一处,问题排查时只需要盯住Provider。

/// 切换完成状态时,联动更新学习统计 Future<void> toggleTask(String taskId) async { final index = _tasks.indexWhere((t) => t.id == taskId); if (index == -1) return; final current = _tasks[index]; final nextStatus = current.status == TaskStatus.completed ? TaskStatus.pending : TaskStatus.completed; _tasks[index] = PlanTask( ...current, status: nextStatus, ); await _repository.saveTasksByDate(_selectedDate, _tasks); // 联动更新累计学习时长 if (nextStatus == TaskStatus.completed) { _totalStudyMinutes += current.studyMinutes; } else { _totalStudyMinutes -= current.studyMinutes; } await _repository.saveTotalStudyMinutes(_totalStudyMinutes); notifyListeners(); }

这里涉及一个埋点思想:你是要“存结果”还是“算过程”。我在完成任务时直接把学习时长累加到全局统计,属于存结果。另一种做法是只存任务完成记录,统计时全区遍历所有完成的任务做求和。前一种代码简单、查询快,但统计可能不准;后一种数据更真实,但每次都要全表扫描。对于每日计划这种轻量级应用,存结果完全够用,但也有一个隐患:如果有人改了计划时间或删除任务,统计数字不会自动回滚。因此我还在Provider里加了removeTask时扣除对应时长的逻辑。这条经验你以后做类似功能大概率会用到。

6. 常见问题与性能调优

6.1 编译与运行阶段的典型报错实录

开发过程中我至少遇到十几个报错,挑四个最有代表性的分享。

第一个,"you are applying flutter's main gradle plugin imperatively using the apply s..."。这个报错信息看着很长,本质是Flutter的Gradle插件配置方式变了。以前用apply plugin:的方式已经被废弃,要改用plugins {}声明式配置。OpenHarmony工程创建时如果用了旧版模板,就会出现这个问题。解决方法是打开ohos/build.gradle,检查apply false行的位置,改成新版插件声明,然后重新同步Gradle。这个问题在搜索引擎里的出现频率极高,说明踩的人不在少数。

第二个,e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception。这个报错很坑,它不会告诉你具体异常内容,只抛出“未处理的异常”。我的排查思路是:先在main()入口包一个FlutterError.onError捕获全局异常,把堆栈打印出来。打印后发现是SharedPreferences插件初始化时序问题——在WidgetsFlutterBinding.ensureInitialized()之前就去调用SharedPreferences.getInstance()。解决办法是把存储初始化移到main()里、runApp之前完成。

第三个,flutter新建项目后跑不起来。大概率是Flutter SDK路径配置不对,或者OpenHarmony SDK版本不匹配。我用的是DevEco Studio 4.0配套的ohos-sdk,Flutter用的是ohos分支最新版。如果你用的版本组合和官方文档不一致,建议先统一版本再排查其他问题。

第四个,OpenHarmony XTS认证测试失败。这个问题在发布前才暴露。XTS是OpenHarmony生态的应用兼容性测试,会检查应用的权限声明、Ability配置、界面横竖屏适配等。我当时因为应用没有声明ohos.permission.KEEP_BACKGROUND_RUNNING权限,导致后台运行测试不过。修复方法是在module.json5里补上权限声明。

6.2 性能优化:Impeller渲染与列表滑动调优

OpenHarmony上Flutter默认使用Skia渲染,如果你的Flutter版本支持Impeller,可以尝试开启,有些场景的渲染帧率会有明显改善。我个人的实测结论是:列表类页面开启Impeller后滑动更跟手,内存占用也略有下降。

开启Impeller的方式是在main.dart里配置:

void main() { // 开启Impeller渲染 if (FlutterTesterBinding.instance is! FlutterTesterBinding) { // 实际OpenHarmony端通过引擎参数开启 const enableImpeller = bool.fromEnvironment('impeller.enable'); if (enableImpeller) { // 在构建时通过 --dart-define=impeller.enable=true 开启 } } runApp(const SmartStudyApp()); }

更推荐的做法是在构建时通过flutter build hap --dart-define=impeller.enable=true来开启,代码里有环境判断,方便随时切换对比。注意Impeller对GPU驱动有要求,部分老设备上可能会有渲染异常,建议在真机上做完整回归测试再决定是否默认开启。

列表滑动优化方面,我做了三个措施:

第一,给列表项TaskTile外层包裹RepaintBoundary,避免单个卡片的状态变化触发整个列表重绘。第二,列表项内部不要使用Opacity这类引发离屏渲染的组件,能省则省。第三,卡片阴影效果降低阴影模糊半径,或者改用细边框替代,减少绘制开销。这三招下来,连续滑动计划列表明显比初始版本流畅。

6.3 ArkTS和Flutter谁更流行:我的实际选择

被问最多的问题是:"arkts和flutter谁更流行?"这个问题要看场景。在OpenHarmony纯原生生态里,ArkTS当然是第一选择,它是HarmonyOS/OpenHarmony官方主推的声明式语言,系统能力调用直接、生态工具链完善。但如果从跨端复用的角度看,Flutter的流行度在全球范围内依然遥遥领先,而且OpenHarmony的Flutter适配已经具备生产可用性。

我的选择逻辑很简单:有一支成熟的Flutter团队、已经有跨端App的存量代码,就选Flutter;团队从零起步且只做OpenHarmony单一平台,果断上ArkTS。两者不是谁替代谁的关系。我的这个智慧学习助手项目,选择了Flutter,因为整个产品线覆盖Android、iOS、OpenHarmony三个平台,维护三套代码是噩梦。

如果你要入坑这个方向,我的建议是把Flutter作为主技术栈,把ArkTS作为补充学习项。懂得ArkTS能让你在遇到Flutter适配OpenHarmony的死角时,手动写一个原生Ability来兜底——比如调系统级API、操作传感器这类Flutter插件还没适配的场景。我在做照片选择器时就写过一次ArkTS的桥接代码,那种“两条腿走路”的踏实感,比只会一种技术要强太多。

最后再分享一个我个人的开发习惯:每完成一个功能点,就用真机跑一遍完整流程,而不是只在模拟器里点几下。模拟器上SharedPreferences读写、日期控件交互、列表滑动都没有问题,但真机上一开相机、一打开通知栏、一晃动屏幕,各种适配问题就全冒出来了。explorer计划功能虽然没有涉及复杂系统API,但真机测试帮我发现了日期选择器在横屏模式下被遮挡的问题,这些都是坐在电脑前凭脑子想不出来的。开发OpenHarmony应用尤其如此,设备形态差异大,只有一个办法能兜住——多跑真机,少谈理论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询