尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flutter接入OpenHarmony实战:垃圾分类App首页开发与适配

Flutter接入OpenHarmony实战:垃圾分类App首页开发与适配 把 Flutter 接到 OpenHarmony 上做应用这几年一直是个既热门又有点尴尬的话题。说热门是因为鸿蒙生态的设备越来越多App 开发者的确需要一个跨端方案来降低重复成本说尴尬是因为 Flutter 官方并不直接支持 OpenHarmony真正能跑通的是社区维护的那条 Flutter 引擎分支。今天这篇是“垃圾分类指南 App 实战”的第三篇重点聊聊首页该怎么落地从前期的信息架构、主题适配到搜索框、分类卡片、轮播知识卡和最近搜索这几个核心模块的实现再到 OpenHarmony 真机上跑起来会遇到哪些坑。如果你已经用 Flutter 写过几个页面但从来没碰过 OpenHarmony 分支或者正准备把一个现有 Flutter 项目迁移到鸿蒙生态这篇应该能让你少走不少弯路。1. 首页设计思路用户打开App的第一眼看到什么1.1 垃圾分类场景的首页刚需分析做首页之前我先把产品诉求捋了一遍。垃圾分类这个 App 的典型用户绝大多数时候是“分不清某样东西扔哪个桶”打开 App 的行为非常短暂且目的明确查一下马上关掉。这种情况下首页最重要的不是内容多丰富而是让用户在一屏之内触达查询入口。同时还要考虑另一类用户想系统学习分类知识、或者刚刚开始做分类的新手。这类用户会往分类入口、知识模块点。因此首页核心就是两个动作查搜索和学浏览分类/知识卡片。基于这个判断我把首页拆成了四块顶部搜索区、四大分类卡片区、知识轮播区、底部 Tab 导航。四大分类对应国内通用的可回收物、有害垃圾、厨余垃圾、其他垃圾。知识卡片可以放一些冷知识比如“碎玻璃是什么垃圾”“椰子壳是不是厨余垃圾”这种容易混淆的条目比单纯放新闻公告有用得多。提示首页功能不宜贪多。MVP 阶段先保住搜索和分类入口识别、语音、积分商城这些功能放到后续版本否则首页会变成一个功能堆砌场反而让核心诉求被淹没。1.2 信息层级与布局决策我的布局顺序是搜索框置顶四大分类卡片紧跟其后再往下是知识轮播和最近搜索。“搜索置顶”这个决策很多人会纠结觉得太占地方但我试过把搜索框放到第三四屏的版本用户停留时间明显变短因为真正想查东西的人找不到入口直接放弃了。四大分类卡片用两列网格展示每张卡片显示一个类别的名称、典型物品例子和桶的颜色标记。这比四张一行的小图标更有辨识度尤其对中年用户来说大卡片点起来更不容易误触。知识轮播放在第三屏用横向滑动卡片实现。这个模块的定位是“有则更好”不承担核心链路所以不能写死整屏的高度我用大约 140 到 160 的逻辑像素高度塞到一屏内不至于把下面的最近搜索挤出首屏。底部 Tab 我规划了四个首页、识别、指南、我的。识别这个 Tab 在当前版本只放一个占位页预留相机和语音输入的扩展位指南页放分类知识长文。首页作为默认 Tab整个 App 打开后先渲染的就是首页。1.3 配色、圆角与卡片规范垃圾分类 App 的视觉基调不用花哨主色直接选绿色系传达环保感。我用的是Color(0xFF2E7D32)这种带一点深沉的绿色做 seed color再辅助浅绿色背景Color(0xFFF6F8F6)让页面整体有呼吸感。四大分类需要区分度可以在卡片上给每个类别一个略微不同的辅助色。圆角我统一用 16 到 20 的弧度卡片间距 12页面左右边距 16。这些数值不是拍脑袋定的我参照了主流生活服务类 App 的间距标准既保证卡片之间有明确边界又不会让小屏设备显得拥挤。组件规范这块我把卡片、搜索框、按钮的圆角和边框统一收敛到主题里而不是每个页面各自写死。这样后续改视觉风格只需要动一个文件避免了重构时在十几个文件里翻圆角参数的痛苦。2. 工程骨架与主题适配2.1 目录结构怎么组织Flutter 项目一旦页面变多目录乱不乱直接影响到排障效率。我的习惯是按下述方式组织lib/ main.dart app.dart theme/ app_theme.dart models/ category.dart knowledge_item.dart search_record.dart pages/ home/ home_page.dart widgets/ search_input.dart category_grid.dart knowledge_carousel.dart recent_search_list.dart identify/ identify_page.dart guide/ guide_page.dart profile/ profile_page.dart data/ categories.dart knowledge_items.dart search_record_store.dartpages/home下面单独开widgets目录是因为首页的组件足够多全塞进一个home_page.dart会导致代码上千行维护体验很差。不过我不建议用 Dart 的part关键字去拆文件虽然在 OpenHarmony 的 Dart 编译链路里part能用但它会引入隐式的共享作用域调试时跳转逻辑反而绕单独建文件再 import 是更清晰的做法。data目录放静态数据源和本地存储封装。垃圾分类的类别和知识条目在 MVP 阶段是写死的不需要后端接口所以我把它们做成 Dart 常量文件后续要接服务端时再替换成网络请求即可。注意OpenHarmony 分支的 Flutter 工程结构和标准 Flutter 工程基本一致都是用flutter create生成后再改造。但入口文件不能照搬 Android/iOS 那套模板社区维护的分支一般会附带示例工程建议直接参考示例里的main.dart和app.dart初始化写法。2.2 底部Tab框架与页面状态保持底部 Tab 的实现我用了IndexedStack而不是每次切换都Navigator.push新页面。很多从 Android 转过来的同学会把 Tab 每项都当成一个独立页面来做但这在 Flutter 里有个坑用Navigator.push的话每次切 Tab 都会建新 State页面里列表的滚动位置、搜索框内容全部重置体验很差。IndexedStack的做法是四个页面常驻内存切换时只是改变索引State 完全保留。首页之前滚动到了第五屏切去指南页再切回来位置还在原地这就是用户想要的“状态不丢”。class MainShell extends StatefulWidget { const MainShell({super.key}); override StateMainShell createState() _MainShellState(); } class _MainShellState extends StateMainShell { int _currentIndex 0; final ListWidget _pages const [ HomePage(), IdentifyPage(), GuidePage(), ProfilePage(), ]; override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: _pages, ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { setState(() _currentIndex index); }, type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: 首页), BottomNavigationBarItem(icon: Icon(Icons.camera_alt_outlined), label: 识别), BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), label: 指南), BottomNavigationBarItem(icon: Icon(Icons.person_outline), label: 我的), ], ), ); } }IndexedStack的代价是四个页面从 App 启动时就会一起构建如果某个 Tab 里有重量级地图或视频流首帧耗时就会被拖上来。我的处理方案是占位页和轻量页直接放进来重页后续再改成懒加载。首页在这个版本信息量不大常驻完全无压力。BottomNavigationBar 的切换动画在部分 OpenHarmony 真机上会有轻微掉帧我试过通过ThemeData的pageTransitionsTheme把过渡动画时长调短效果有改善。如果你对这个动画就是看不顺眼也可以不用BottomNavigationBar自己用Row Expanded搭一个底部栏点击时只改颜色和索引没有任何动画但这属于体验取舍我当前版本保留了默认动画因为绝大多数真机上表现是正常的。2.3 全局主题与组件复用主题配置我单独放在theme/app_theme.dart用ThemeData统一管理颜色、输入框样式、卡片样式。这里有个细节Flutter 版本更新后CardTheme的类型在 3.27 左右从CardTheme变成了CardThemeData你要是照抄旧代码编译报错先看自己的 Flutter 版本再决定用哪个类型不要盲目升级依赖。ThemeData buildAppTheme() { final ColorScheme colorScheme ColorScheme.fromSeed( seedColor: const Color(0xFF2E7D32), brightness: Brightness.light, ); return ThemeData( useMaterial3: true, colorScheme: colorScheme, scaffoldBackgroundColor: const Color(0xFFF6F8F6), appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, backgroundColor: Colors.transparent, foregroundColor: Color(0xFF1B5E20), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: Colors.white, hintStyle: const TextStyle(color: Color(0xFF9E9E9E)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), cardTheme: CardThemeData( elevation: 0, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), color: Colors.white, ), ); }这套主题的好处是搜索框和卡片在首页以外的地方也能直接继承样式。比如指南页的条目卡片、我的页的个人信息卡都不用重新定义圆角和背景色只要在业务页面里用Card组件就能保持视觉统一。动态字体方面“我的”页会有字号设置的入口但首页目前不需要特殊处理保持系统默认字号即可。如果你要支持大字体建议所有组件都用textScaleFactor做一次边界测试尤其是分类卡片文字是否溢出、搜索框 hint 是否被截断这类问题在鸿蒙设备上出现过因为我测试机默认字号调大后卡片标题差点被挤出外层容器。3. 首页核心模块的完整实现3.1 搜索框防抖、清空与键盘处理搜索框是首页的灵魂我单独封装成SearchInput组件。除了基础的输入框样式还需要处理三件事防抖、一键清空、键盘搜索键触发。防抖不是先例大家都懂用户连续输入“电”“电池”“电池”的过程里如果每敲一个字都去触发搜索既浪费资源又会让结果闪来闪去。我设置 500 毫秒的延迟用户停止输入半秒后才真正发起查询。class SearchInput extends StatefulWidget { const SearchInput({ super.key, required this.onSearch, required this.onClear, }); final ValueChangedString onSearch; final VoidCallback onClear; override StateSearchInput createState() _SearchInputState(); } class _SearchInputState extends StateSearchInput { final TextEditingController _controller TextEditingController(); Timer? _debounce; override void dispose() { _debounce?.cancel(); _controller.dispose(); super.dispose(); } void _handleChanged(String text) { _debounce?.cancel(); _debounce Timer(const Duration(milliseconds: 500), () { if (text.trim().isEmpty) return; widget.onSearch(text.trim()); }); } void _handleSubmitted(String text) { _debounce?.cancel(); widget.onSearch(text.trim()); } override Widget build(BuildContext context) { return TextField( controller: _controller, onChanged: _handleChanged, textInputAction: TextInputAction.search, onSubmitted: _handleSubmitted, decoration: InputDecoration( hintText: 搜索垃圾名称比如“电池”, prefixIcon: const Icon(Icons.search), suffixIcon: ValueListenableBuilderTextEditingValue( valueListenable: _controller, builder: (context, value, _) { if (value.text.isEmpty) { return const SizedBox.shrink(); } return IconButton( icon: const Icon(Icons.cancel), onPressed: () { _controller.clear(); widget.onClear(); }, ); }, ), ), ); } }清空按钮这里有两个实现方案简单做法是suffixIcon直接判断_controller.text.isEmpty但这样点击清空后按钮不会立刻消失因为TextField没有触发 rebuild。我用ValueListenableBuilder监听TextEditingController的值变化输入一清空按钮就自动隐藏交互反馈更即时。键盘的处理要注意textInputAction必须设置为TextInputAction.search这样软键盘右下角会变成搜索按钮用户输完可以直接点键盘搜索键触发查询。在 OpenHarmony 真机上输入法的弹起速度在不同机型上有差异我建议在Scaffold上用resizeToAvoidBottomInset: false做一次测试如果搜索框在键盘弹起后被顶到屏幕中上方就用默认的 true否则交互会奇怪。3.2 分类入口两列卡片网格四大分类卡片我用GridView.builder实现放在页面主体靠上的位置。因为首页整体是纵向滚动GridView嵌套在SingleChildScrollView或者CustomScrollView里时必须设置shrinkWrap: true和physics: NeverScrollableScrollPhysics()让它变成一个不自带滚动的网格跟着父级列表一起滚。class CategoryGrid extends StatelessWidget { const CategoryGrid({ super.key, required this.categories, required this.onTap, }); final ListCategory categories; final ValueChangedCategory onTap; override Widget build(BuildContext context) { return GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.7, ), itemCount: categories.length, itemBuilder: (context, index) { final category categories[index]; return _CategoryCard( category: category, onTap: () onTap(category), ); }, ); } }childAspectRatio: 1.7是经过调试的卡片太扁会显得拥挤太高又会把知识轮播挤到第二屏之外。1.7 在大多数手机宽度下卡片高度大约是宽度的 0.59 倍刚好容纳两行文字加一个小图标。每张卡片的内容我放了类别名、一句典型物品提示和一个类别图标。比如“可回收垃圾”对应“塑料瓶、纸箱、金属罐”“有害垃圾”对应“电池、过期药品、废灯管”。这些描述字很小但比只放一个分类名词有引导性用户在不确定“杀虫剂”属于哪类时光看“有害垃圾”卡片上的“过期药品”就能建立联想。点击卡片的跳转逻辑里我传了一个完整的Category对象给详情页而不是只传 id 再让详情页自己拉数据。这个细节是因为分类数据量小建模简单传对象能省去详情页的加载等待也避免索引越界问题。3.3 知识轮播PageView与定时器配合知识轮播展示的是垃圾分类中容易搞错的冷知识比如“大棒骨不是厨余垃圾而是其他垃圾”“榴莲壳也是其他垃圾”。这类内容有传播性能让用户产生“原来如此”的感觉也顺带推动分享。轮播组件我用PageView实现每个知识条目是一张卡片支持用户手动滑动滑到边缘后不会停住而是用定时器自动轮播。核心代码是定时器控制页面切换到下一张class KnowledgeCarousel extends StatefulWidget { const KnowledgeCarousel({super.key, required this.items}); final ListKnowledgeItem items; override StateKnowledgeCarousel createState() _KnowledgeCarouselState(); } class _KnowledgeCarouselState extends StateKnowledgeCarousel { late final PageController _pageController; Timer? _timer; int _currentPage 0; override void initState() { super.initState(); _pageController PageController(viewportFraction: 0.9); _startAutoPlay(); } override void dispose() { _timer?.cancel(); _pageController.dispose(); super.dispose(); } void _startAutoPlay() { _timer?.cancel(); _timer Timer.periodic(const Duration(seconds: 4), (timer) { if (!_pageController.hasClients) return; final next (_currentPage 1) % widget.items.length; _pageController.animateToPage( next, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); }); } override Widget build(BuildContext context) { return SizedBox( height: 140, child: PageView.builder( controller: _pageController, itemCount: widget.items.length, onPageChanged: (index) { setState(() _currentPage index); }, itemBuilder: (context, index) { final item widget.items[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 6), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.title, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Expanded( child: Text( item.desc, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFF616161)), ), ), ], ), ), ); }, ), ); } }这里有几个容易踩的坑。第一个是Timer.periodic必须在dispose里取消否则页面销毁后定时器还在跑触发animateToPage时PageController已经释放控制台会红屏报错。第二个是viewportFraction: 0.9让左右两张卡片露出一部分边缘用户一眼能看出这组卡片是可以左右滑的这种视觉提示比任何引导文案都直观。第三个是判断hasClients因为PageController在 widget 还未附加到树上的时候调用会产生异常加个防御更稳妥。另一个细节是轮播卡片内容超过两行时做了ellipsis截断标题加粗、描述用普通灰色。这种设计保证卡片高度统一轮播切换时不会因为文字长度不同而出现跳动。3.4 最近搜索落盘、读取与空态最近搜索这个模块看起来简单但直接影响用户的重复查询效率。我实现了一个SearchRecordStore类内部用shared_preferences存一个最多保留 10 条的最近搜索列表去重后新的排最前面。因为 OpenHarmony 分支的shared_preferences不是官方直接支持的需要引入适配版插件社区维护的 flutter 包仓库里有对应的shared_preferences_ohos在pubspec.yaml里做依赖替换即可。class SearchRecordStore { static const String _key recent_search_records; static const int _maxCount 10; FutureListString load() async { final prefs await SharedPreferences.getInstance(); final list prefs.getStringList(_key) ?? []; return list; } Futurevoid add(String keyword) async { final prefs await SharedPreferences.getInstance(); final list prefs.getStringList(_key) ?? []; list.remove(keyword); list.insert(0, keyword); if (list.length _maxCount) { list.removeRange(_maxCount, list.length); } await prefs.setStringList(_key, list); } Futurevoid clear() async { final prefs await SharedPreferences.getInstance(); await prefs.remove(_key); } }最近搜索列表的 UI 用ListView.builder渲染每条前面放一个历史图标右侧放删除单个条目的按钮。当列表为空时显示“暂无查询记录去搜索框试试吧”的空态文案并隐藏删除全部按钮。这个模块有一个体验细节搜索成功后不要立即setState刷新列表而是等搜索结果的页面切换完成后再异步从SharedPreferences里拉一遍。原因是当前页面是首页如果用户在结果页里反复搜索首页不必每次都同步刷新这样也能避免过度重建。4. 数据源选择与状态管理落地4.1 静态JSON还是轻量数据库MVP 阶段的首页数据来源有两种选择Dart 常量文件和 JSON 资源文件。我最终选择 Dart 常量文件比如data/categories.dart里直接放一个ListCategory而不是从assets/json/categories.json里加载。原因是分类和知识条目都是静态的、体量极小用常量文件可以免去rootBundle.loadString的异步解析过程代码里也方便直接使用Category对象类型提示完整。当然这也有个前提数据更新频率极低不需要走热更新。如果后续要做运营后台分类数据每天变那就要切到 JSON 资源 远端更新方案。我的建议是数据层做一层抽象CategoryRepository现在返回静态数据后续换成网络数据页面代码完全不用动这是个常规但容易忽视的设计。4.2 状态管理选型MVP阶段别过度设计首页在这个版本里的共享状态其实很少最近搜索列表变化、搜索框清空状态、轮播页索引。我用ChangeNotifierProvider加provider包来管理没有上bloc或Riverpod不是因为哪个框架不好而是 MVP 阶段的状态流还没复杂到需要额外引入模板代码。以最近搜索列表为例我用HomeController extends ChangeNotifier持有recentSearches列表和loading标志位页面通过context.watchHomeController()监听变化。这种做法在 Flutter 里是最接近“够用就好”的状态管理没有冗余概念OpenHarmony 分支也完全支持。如果你是从bloc转过来的兄弟建议克制一下。首页这种单一页面用bloc会造成事件和状态的大量空转尤其是搜索防抖和轮播定时器这类纯 UI 逻辑硬放进bloc里反而难调试。4.3 Loading、Error、Empty三种状态的兜底首页虽然大多是静态数据但SharedPreferences读取是异步的搜索也是异步的所以页面仍然需要处理加载中、失败、空数据三种状态。最近搜索列表加载时我用一个半透明的占位骨架而不是转圈。骨架屏在 Flutter 里实现很简单if (_controller.loading) { return const SizedBox( height: 140, child: Center(child: CircularProgressIndicator()), ); } if (_controller.recentSearches.isEmpty) { return const SizedBox( height: 140, child: Center( child: Text( 暂无查询记录去搜索框试试吧, style: TextStyle(color: Colors.grey), ), ), ); }空态文案其实也是产品细节尽量不用“暂无数据”这种开发语气改成“去搜索框试试吧”能引导用户行动。数据加载失败的情况在这个版本极少但我在SearchRecordStore.load外层加了 try-catch失败时返回空列表而不是抛异常页面不至于白屏。在实际开发时这三种状态往往被忽略到最后一刻结果真机测试时偶尔看到空白一块排查半天才发现是异步没到位。建议从第一天就把三态写进组件里哪怕数据是同步的也预留好接口。5. OpenHarmony适配踩坑与真机调试5.1 跑起来之前的环境检查清单Flutter 在 OpenHarmony 上的开发流程和标准 Flutter 有一个明显的分叉点SDK 路径和工具链。我用的是社区维护的 Flutter OpenHarmony 分支开发工具是 OpenHarmony 官方的 IDE 加命令行 Flutter。第一次跑真机时最容易踩的坑是flutter doctor检测不到 OpenHarmony SDK。我整理了开发环境检查清单DEVECO_SDK_HOME 环境变量是否指向已安装的 OpenHarmony SDK 目录。命令行里flutter doctor -v是否能看到 OpenHarmony 相关项通过。真机是否开启开发者模式以及是否通过 hdc 连接成功。工程里build.gradle和entry/src/main/ohos/module.json5里的包名是否与签名证书一致。这四项缺一个都会让flutter run在最后一步失败。我项目刚迁移时跳过第二项一直以为配置没问题结果编译时报“找不到 OpenHarmony SDK 平台”回头看是环境变量没生效终端里 export 完直接退了 shell重新打开又变回去。解决办法就是把它写进 shell 配置文件里一劳永逸。5.2 真机运行常见问题速查我在多台 OpenHarmony 真机上跑过首页整理了一份问题速查表给同路人参考现象可能原因修复思路flutter run找不到设备hdc 服务未启动或 USB 调试授权未通过先执行hdc list targets确认设备在线后再跑 Flutter首页白屏入口页面未在main_pages.json注册打开entry/src/main/resources/base/profile/main_pages.json把首页路由加进去图片资源不显示资源路径挂载错误检查pubspec.yaml的 assets 路径和文件名大小写搜索键盘无法弹出输入法框架与 App 冲突升级 Flutter OpenHarmony 分支版本或临时切换默认输入法热重载失效修改了原生侧的 OpenHarmony 代码重启应用部分原生改动必须重新编译启动黑屏后恢复引擎首次初始化较慢用真机性能模式测试确保设备未进入省电模式这里面main_pages.json的白屏问题我印象最深。第一次打包安装后应用图标一点开就是白屏日志里没有任何异常查了半天发现自己把首页路由写成了根节点/但main_pages.json里没有注册MainAbility对应的pages/Index。这个文件是 OpenHarmony 工程的入口配置标准 Flutter 里压根没有所以特别容易漏。另一个和标准 Flutter 差异明显的地方是日志工具。OpenHarmony 真机调试用hdc hilog而不是adb logcat很多搜索代码用print打的日志在 hilog 里按 tag 过滤时看不到我最后统一用debugPrint加前缀的方式才在日志里找到页面刷新失败的原因。5.3 首页性能调优的两个方向首页在 OpenHarmony 设备上的性能问题我实际遇到的主要集中在两个方面首帧渲染和列表滑动流畅度。首帧方面影响最大的是主页组件树复杂度。IndexedStack四个页面同时构建首页虽然轻但指南页和我的页带了一些静态列表如果这些页面里混入了高分辨率图片首帧会被拖慢。我的做法是把首页之外的 Tab 页内容换成占位骨架只保留页面标题和少量说明文字真正的内容等用户切到对应 Tab 再初始化。骨架屏和真实页面之间切换用淡入动画用户感知不到好坏差异但首屏时间能明显缩短。滑动流畅度方面我遇到过一个奇怪现象首页CustomScrollView往下滚动时到了知识轮播区域会偶尔卡顿一下。排查后发现是轮播的PageView在滚动过程中会和外层滚动冲突造成重复曝光和布局计算。我用NeverScrollableScrollPhysics限制内层滑动把PageView的滚动能力关掉一部分只在用户手动横滑时才激活纵滑时轮播不参与手势竞争卡顿就消失了。渲染引擎上Flutter 分支里 Impeller 默认启用但部分 OpenHarmony 设备的 GPU 驱动对 Impeller 支持不完整会出现轻微闪烁。实测下来如果遇到闪烁可以在启动时使用--no-enable-impeller关闭 Impeller 回退到 Skia。代价是渲染性能略降但稳定优先后续等社区分支把 Impeller 的 OpenHarmony 适配打磨好再切回去。另外如果你后续要把首页的搜索事件通过EventChannel发给 OpenHarmony 原生侧比如接入系统级语音识别需要留意 OpenHarmony 的 channel 注册方式和 Android 不一样不能直接在MainActivity里注册要在Ability的OnStart里通过FlutterEngine拿到 channel 实例。这个和标准 Flutter 的差异比较隐蔽我下一篇写识别模块时再展开。最后补一个我觉得特别值得记住的实践不要把 OpenHarmony 当成 Android 的平替来写。虽然 Flutter 帮你屏蔽了 90% 的差异但那 10% 的差异——插件适配、生命周期、开发者工具——才是决定项目能不能落地的部分。首页只是一切开始等把平台通道、系统相机这些能力真正接进来时坑只会更多但也只有蹚过去才能把这套跨端方案的价值吃透。当前版本的首页已经能跑稳了下一步我打算把识别模块接到系统相机上做一个拍照识别垃圾类别的功能那时候平台通道的事情我再来补一篇实战记录。
返回列表