ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter鸿蒙适配实战:从校园热水卡记录App到跨平台落地

Flutter鸿蒙适配实战:从校园热水卡记录App到跨平台落地 上过大学的人对宿舍楼下的热水卡基本都不陌生一张小小的卡片充值、插卡、扣费余额显示在刷卡机的小屏幕上洗到一半金额不足就直接给你断热水。我去年帮学校后勤做内部工具选型的时候被问到一个很实际的需求——能不能做一个能记录热水卡消费的App同时还要覆盖现在学生手里数量越来越多的鸿蒙手机。当时团队刚好在推Flutter跨平台方案于是这个项目就成立了用Flutter框架开发一款同时跑在Android和鸿蒙上的校园热水卡记录应用再把整套过程整理成教程沉淀下来。这篇文章直接围绕这个项目展开不会写那种跑一遍就删掉的Demo而是我踩过的坑、验证过的方案、可以直接复现的代码全都交代出来。适合三类人看一是想用Flutter做真实业务练手的新手二是学校或企业内部做轻量工具应用、又对鸿蒙设备有要求的工程师三是正在对比Flutter和ArkUI差异的开发者。下面开始。1. 项目全貌先搞清楚做的是什么、为什么这么做1.1 校园热水卡场景的真实痛点热水卡这东西在高校宿舍的使用频率非常高。我在调研阶段跑过几个校区热水系统基本都长一个样办卡时充一笔钱洗澡时把卡插到读卡器上机器按水表和热表的读数扣费余额直接显示在小屏幕上。看起来方便但问题在于——账目只有卡上的余额和机器上最后一条记录中间花过的每一笔都没人帮你记。很多学生习惯一次性充50块结果一个月后完全想不起来钱花哪儿了想查明细也没有渠道。所以这个应用的第一个核心需求很朴素记录每一笔热水消费。洗完澡回来在App里随手记一条几点洗的、花了多少钱、水温多少、洗了多久。月底打开列表一拉这个月的洗澡开销一目了然。第二个需求是余额预测。卡上的余额只能去宿舍楼下刷卡机上查到高峰期还要排队。如果把每次消费都记下来再录入充值记录App就能自己算出当前余额还能按近期的日均消费预测卡里的钱还能撑几天。这个功能在用户调研里被提得最多说明它是一个真实存在的高频诉求。第三个需求是多卡管理。一个宿舍通常有四张卡同一个App要能切换不同卡号帮室友一起记。月底统一结算的时候不用再翻聊天记录。这些需求组合在一起就是一个典型的轻量记账工具场景功能边界清晰非常适合用来跑通Flutter在鸿蒙上的整套开发链路。1.2 为什么是Flutter而不是ArkUI、Tauri或Electron这个项目摆在面前有三条路。第一条是鸿蒙原生ArkUI用DevEco Studio建工程在鸿蒙上性能最好但问题也很直接——只覆盖鸿蒙一个平台后续如果要出iOS版代码几乎要重写。第二条是Tauri 2这类Rust跨平台方案它在桌面端的表现确实越来越好也有社区在推进对鸿蒙的适配不过移动端生态和外围插件并没有想象中全面做一个小工具可以要依赖成熟的相机、地图、推送插件就很吃力。第三条就是Flutter。我最终选它原因很明确Flutter是一套Dart代码能覆盖iOS、Android、鸿蒙、Web和桌面引擎自绘UI不依赖系统原生控件所以跨平台的渲染一致性非常强。团队内部此前做过一个跨平台音乐管理系统同样是Flutter写的积累了组件通信、状态管理、多端打包的一整套经验热水卡应用可以直接复用这些基建。要特别说明一点Flutter在鸿蒙上并不是官方天然支持的目前走的是OpenHarmony社区的Flutter SDK分支这套路线。整条引擎、Dart运行时、渲染管线都做了鸿蒙适配在ArkUI原生层留一个容器组件承载FlutterView业务层全部用Dart写。这也是当前把Flutter应用搬上鸿蒙最成熟的落地方案流程已经在多个商业项目里验证过。1.3 功能蓝图与整体技术栈为了让你有个全局视角我把最终落地的功能拆成了四块。首页负责展示卡账户信息和实时余额提供一键记一笔的快捷入口。记录页负责消费明细展示支持下拉刷新和按日期筛选。统计页按周、月汇总消费金额与次数用柱状图呈现趋势。设置页负责多张卡的管理、当前卡号切换以及数据的导入导出。技术栈方面核心组合是这样的模块选型理由UI框架FlutterOpenHarmony适配分支一套代码覆盖Android与鸿蒙状态管理Provider轻量、无侵入适合中小型工具应用数据库sqfliteSQLite封装成熟跨平台一致性好日期处理intl格式化与周月计算标准图表fl_chart柱状图、折线图开箱即用桌面排查工具DB4S开源跨平台SQLite管理开发期直接查库这套组合没有引入Redux或RxDart之类的重型方案对一个小型记账工具来说足够清爽新手也能快速理清数据流。数据层完全与平台解耦换平台不用改任何业务代码。2. 环境准备与工程搭建2.1 SDK与工具链Flutter、鸿蒙SDK、JDK怎么配先说环境版本这是后面所有问题的重要基础。我当时的组合是Flutter 3.16鸿蒙适配分支、DevEco Studio 4.0、Dart 3.2。这里有个关键细节不要直接去flutter.dev下载官方SDK因为官方SDK目前不带鸿蒙目标。要从OpenHarmony的flutter_flutter仓库拉取适配分支这个分支把Flutter tools里关于鸿蒙平台的部分包括目标类型、构建产物、编码签名、模拟器部署都做了补齐。配置环境变量时记得把flutter的bin目录加到PATH同时确认JDK版本。鸿蒙构建链路依赖Java工具链JDK 17是社区适配分支验证过的版本低版本的JDK会在打包HAR时报奇怪的ClassNotFoundException。有人问Android SDK是不是可以不要答案是不能。Flutter的标准构建体系仍然依赖Android SDK来解析平台配置所以我把ANDROID_HOME指向本地已有目录鸿蒙侧SDK由DevEco Studio自动管理两条工具链并行互不干扰。环境配好之后先跑一遍flutter doctor确认Dart SDK和设备连接状态再进DevEco Studio确认鸿蒙SDK是否装上。第一次配环境最容易卡在设备连不上其实是hdc的路径没加进PATH。hdc就是鸿蒙设备连接服务作用和ADB类似把它所在目录加入PATH之后flutter devices就能正常识别到鸿蒙设备。2.2 从flutter create到鸿蒙HAR包集成全流程环境没问题之后开始建项目。终端里执行flutter create hot_water_card --org com.example.student得到的是一个标准Flutter工程目录下能看到android、ios、web这些平台目录唯独没有鸿蒙目录。鸿蒙接入的方法是先在DevEco Studio里创建一个HarmonyOS Empty Ability工程包名要和Flutter工程的org保持一致然后把Flutter模块作为依赖集成进去。我在实操中用的是社区推荐的集成方式在鸿蒙工程根目录放一个flutter模块子目录编译Flutter工程生成HAR包再在鸿蒙侧的build-profile.json里做依赖声明。这里顺便回答两个大家问得比较多的问题一个是flutter aar一个是apply插件报错。在Android原生工程里集成Flutter时产物是AAR包鸿蒙这边对应的产物格式是HAR。很多从Android转过来的人习惯直接在模块级别apply Flutter的插件结果报出“you are applying flutters main gradle plugin imperatively using the apply”这种错误。原因不复杂鸿蒙工程用的是DevEco Studio主导的hvigor构建体系不再沿用Android Gradle Plugin那一套直接apply当然不识别。正确做法是用hvigor的依赖管理加载Flutter HAR包而不是照搬Gradle的写法。2.3 Android Studio与DevEco Studio的协同节奏实际开发时我的习惯是同时开两个IDE。Dart和Flutter相关代码在Android Studio里写装上Flutter和Dart插件跑analyze、断点调试都很顺手鸿蒙工程的配置文件、签名、真机部署交给DevEco Studio处理。两个IDE共享同一个顶层目录只是各自打开其中兼容的部分所以.gradle、.idea这类IDE专属目录不要提交进版本库否则两边会互相污染配置。有个细节值得记住代码生成和lint检查以Android Studio为准因为Flutter插件的智能提示在DevEco Studio里目前还是偏弱。反过来鸿蒙侧的权限声明、模块依赖、hvigor配置不要手工去翻XML改hvigor的配置模型和Gradle差异很大手改很容易导致构建缓存失效最后莫名其妙出一堆编译错误。固定好分工两边各管一摊工作流会顺很多。3. 数据层设计热水卡记录的模型与持久化3.1 四张表搞定整个业务表结构与实体类热水卡这个业务看起来琐碎数据模型其实很收敛。我设计了四张表account用来存卡档案consume_record存消费明细recharge_record存充值记录setting_kv存当前激活卡号等少量键值配置。这里面最核心的是consume_record它决定了后面所有统计功能的实现。实体类我直接用Dart写字段类型和表结构一一对应。核心的两个类如下。class CardAccount { int id; String cardNo; // 卡号 String owner; // 持卡人姓名 String building; // 宿舍楼栋 double balanceAfter; // 最近一次更新后的余额 DateTime updatedAt; CardAccount({ required this.cardNo, required this.owner, required this.building, required this.balanceAfter, }); MapString, Object? toMap() { return { id: id, card_no: cardNo, owner: owner, building: building, balance_after: balanceAfter, updated_at: updatedAt.toIso8601String(), }; } factory CardAccount.fromMap(MapString, Object? map) { return CardAccount( cardNo: map[card_no] as String, owner: map[owner] as String, building: map[building] as String, balanceAfter: (map[balance_after] as num).toDouble(), ); } }class ConsumptionRecord { int id; int accountId; // 关联的卡ID DateTime usedAt; // 消费时间 double amount; // 消费金额 double waterTemp; // 水温 int durationMinutes; // 洗澡时长 double balanceAfter; // 消费后余额 String note; // 备注 MapString, Object? toMap() { return { id: id, account_id: accountId, used_at: usedAt.toIso8601String(), amount: amount, water_temp: waterTemp, duration_minutes: durationMinutes, balance_after: balanceAfter, note: note, }; } factory ConsumptionRecord.fromMap(MapString, Object? map) { return ConsumptionRecord( accountId: map[account_id] as int, usedAt: DateTime.parse(map[used_at] as String), amount: (map[amount] as num).toDouble(), waterTemp: (map[water_temp] as num).toDouble(), durationMinutes: map[duration_minutes] as int, balanceAfter: (map[balance_after] as num).toDouble(), note: map[note] as String? ?? , ); } }注意金额字段我用double这在财务上其实有精度隐患但因为热水卡的日常消费精确到分单笔金额很小double的误差不会造成实际问题。如果你要做的场景涉及更大的金额或需要严格对账建议改成int存分或者用decimal库二选一。3.2 sqflite数据库访问层封装sqflite在Flutter里是最常见的SQLite方案API设计也直观。我写了一个单例的DatabaseHelper统一管理数据库初始化、表创建和常用查询。数据库版本号初期就定成1后续如果增加表字段只需要提升版本号并写对应的onUpgrade逻辑不要直接删库重建否则用户的历史记录就丢了。class DatabaseHelper { static final DatabaseHelper _instance DatabaseHelper._internal(); factory DatabaseHelper() _instance; DatabaseHelper._internal(); static const _dbName hot_water_card.db; static const _dbVersion 1; Database? _db; FutureDatabase get database async { _db ?? await _initDb(); return _db!; } FutureDatabase _initDb() async { final dbPath await getDatabasesPath(); return openDatabase( p.join(dbPath, _dbName), version: _dbVersion, onCreate: (db, version) async { await db.execute( CREATE TABLE account ( id INTEGER PRIMARY KEY AUTOINCREMENT, card_no TEXT NOT NULL UNIQUE, owner TEXT NOT NULL, building TEXT NOT NULL, balance_after REAL NOT NULL, updated_at TEXT NOT NULL ) ); await db.execute( CREATE TABLE consume_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, used_at TEXT NOT NULL, amount REAL NOT NULL, water_temp REAL NOT NULL, duration_minutes INTEGER NOT NULL, balance_after REAL NOT NULL, note TEXT, FOREIGN KEY(account_id) REFERENCES account(id) ) ); await db.execute( CREATE TABLE recharge_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, recharged_at TEXT NOT NULL, amount REAL NOT NULL, balance_after REAL NOT NULL, FOREIGN KEY(account_id) REFERENCES account(id) ) ); await db.execute( CREATE TABLE setting_kv ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) ); }, ); } }插入和查询的代码不建议散落在每个页面里我把它抽成了Repository。比如ConsumeRepository管消费记录AccountRepository管卡档案页面只面向接口编程。这样以后如果要把本地存储换成服务端接口只动Repository层就可以UI完全不用改。这个抽象层在Flutter里不复杂但很多人会忽略等到数据源切换时再抽就晚了。3.3 开发期用DB4S直接查库效率翻倍开发过程中我强烈建议桌面装一个DB4SDatabase Browser for SQLite它的全称很长但定位很清晰一个开源跨平台的SQLite数据库管理工具。热水卡App的数据库存在手机上开发调试时有个很头痛的问题——你没法直观看到数据到底写没写对。用DB4S打开手机调试目录下的hot_water_card.db文件所有表结构、字段类型、数据行直接在界面上列出来比一遍遍在日志里打印SQL结果直观太多。我自己的流程是先在App里造一条消费记录然后用adb pull鸿蒙真机用hdc把db文件拉到电脑DB4S打开确认record行数和字段值再顺手练习几条SQL确认统计口径。等统计逻辑写完再回App里核对UI数字是否一致。这个循环帮我提前发现了两个数据问题一个是时间字段没有统一时区另一个是FOREIGN KEY没加索引导致月统计在数据量大时偏慢。开发期把数据层调扎实后面UI工作会非常轻松。4. UI层实现从页面骨架到鸿蒙布局适配4.1 底部导航与四个页面骨架Flutter里做底部导航的标准做法是BottomNavigationBar配合IndexedStack。IndexedStack的好处是所有子页面一旦创建就保持状态切换Tab时不会重建这对于记录列表这种需要保留滚动位置的场景特别重要。如果你用PageView加setState切换每次切回来列表会重新加载用户体验会打折扣。主壳子的代码大概是这个样子class MainShell extends StatefulWidget { override StateMainShell createState() _MainShellState(); } class _MainShellState extends StateMainShell { int _currentIndex 0; final _pages [ HomePage(), RecordPage(), StatsPage(), SettingsPage(), ]; override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: _pages, ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: _currentIndex, onTap: (index) setState(() _currentIndex index), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 首页), BottomNavigationBarItem(icon: Icon(Icons.receipt_long), label: 记录), BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: 统计), BottomNavigationBarItem(icon: Icon(Icons.settings), label: 设置), ], ), ); } }这里有一个很容易犯的错误把业务数据请求直接放在页面的initState里然后每次切Tab回来又请求一遍。正确做法是用Provider统一管理数据加载状态页面只负责监听状态变化。IndexedStack保证页面不重建Provider保证数据更新能推送到页面这两个配合才是一个完整的骨架。如果某个页面里的数据依赖于当前选中的卡号在页面内部通过Provider读取不要在MainShell层通过构造参数层层传。4.2 消费记录列表与下拉刷新的完整实现记录页是用户使用频率最高的页面我把它做成按日期分组的列表结构。顶部是一个搜索框支持按日期范围筛选下面用RefreshIndicator包住ListView下拉触发重新查询。这里有个实用细节RefreshIndicator的onRefresh回调必须返回一个Future刷新逻辑要等Future完成后再关闭转圈。如果查询是同步的建议包一层async方法哪怕方法体里没有真正异步操作也要保持Future语义否则转圈动画会闪一下立刻消失看起来像是Bug。记录列表项我用了Card加ListTile的组合左侧显示日期时间右侧显示金额和水温。金额用不同颜色区分正负消费是负向的红色系充值记录是正向的绿色系。列表项数量大了之后ListView.builder是必须的千万别用Column加children的方式动态生成数据量稍微上去一点就会卡顿。下拉刷新的关键代码Futurevoid _refreshRecords() async { try { final records await _repository.getRecordsByAccount(selectedAccountId); if (mounted) { setState(() { _records records; }); } } catch (e) { // 这里可以做统一的错误提示比如SnackBar } } override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: _refreshRecords, child: ListView.builder( itemCount: _records.length, itemBuilder: (context, index) { final record _records[index]; return _RecordCard(record: record); }, ), ); }关于新平台的注意点在鸿蒙适配分支上RefreshIndicator同样工作正常因为它是Flutter引擎自绘的组件不依赖原生控件。这一点是Flutter跨平台优势的典型体现ArkUI里要做下拉刷新需要用ScrollContainer配合onRefresh监听而Flutter这边一套代码两个系统表现一致。4.3 鸿蒙ArkUI的RelativeContainer、Flex、Tabs在Flutter里的对应写法很多开发者同时学ArkUI和Flutter会反复被两个框架的布局命名搞晕。这里我直接把鸿蒙侧常用的几个布局容器和Flutter的对应关系列出来方便对照迁移。鸿蒙ArkUI组件功能定位Flutter对应方案RelativeContainer相对定位子组件相互之间或相对父容器相对布局Stack配合Positioned或AlignFlex主轴/交叉轴弹性布局支持direction与wrapFlex、Row、Column结合ExpandedTabs页签导航支持顶部、底部及滑动联动TabBar配合TabBarView或BottomNavigationBar我实际项目里最常用的对应关系是Flex在Flutter里就是Row和Column椭圆属性对应mainAxisSize和crossAxisAlignmentRelativeContainer里的alignTo、alignLeft这种语义Flutter里直接用Stack加Positioned改左右上下距离就行Tabs做顶部页签时Flutter用TabBar加TabBarView底部的整页导航就用BottomNavigationBar。要提醒一点鸿蒙侧如果做混合开发原生页面里的ArkUI布局和FlutterView承载的Dart UI是两套独立渲染体系。布局对齐只能靠容器尺寸和边距去匹配不要指望ArkUI的RelativeContainer约束能作用到FlutterView内部。所以我在项目里定了一条规则能用Flutter层实现的UI就不放到ArkUI层去写避免两套布局系统互相牵扯。只有真正需要调用鸿蒙系统能力、必须在原生层实现的场景比如系统设置页跳转才开一个ArkUI容器。5. 组件通信与异步数据流5.1 父子组件通信回调、Provider、InheritedWidget怎么选Flutter的组件通信是个高频话题网上关于组件通信的讨论非常多热水卡这个App正好可以把几种方式都用一遍。我的原则很简单范围最窄的用构造参数跨多个不确定层级的用Provider全局唯一的配置状态用InheritedWidget或Provider的基础类型。父子组件最直接的通信方式是构造参数加回调。比如记录页里的AmountField子组件接收一个初始值用户输入变化时通过onAmountChanged回调告诉父组件。父组件把数据保存在State里最后一起落库。这样做的好处是数据流方向清晰子组件无状态可测试性强。跨页面共享数据就必须用Provider了。热水卡App里被多个页面共享的状态是当前激活卡号、余额和记录总数。我在应用入口用ChangeNotifier包了一层AppState记录插入成功后调用recordService.insert()AppState里的余额和记录总数自动更新所有监听了这个状态的页面同时收到通知。class AppState extends ChangeNotifier { CardAccount? _currentAccount; ListConsumptionRecord _recentRecords []; CardAccount? get currentAccount _currentAccount; ListConsumptionRecord get recentRecords _recentRecords; void loadFromDatabase() { _currentAccount _accountRepository.getActiveAccount(); _recentRecords _consumeRepository.getRecent(20); notifyListeners(); } Futurevoid addConsumption(ConsumptionRecord record) async { await _consumeRepository.insert(record); _recentRecords _consumeRepository.getRecent(20); notifyListeners(); } }InheritedWidget属于Flutter底层机制Provider本身就是基于它封装的。如果你自己写Provider重要的是记住上下文查找规则context.readT()拿不到时多半是ProviderType写错或者Provider没有包在合适的位置。新手容易在这里绕圈子我的建议是直接把Provider放到runApp外层省得排查查找不到的报错。5.2 Future的then回调为什么排进微任务队列开发过程中你会经常和Future打交道比如数据库查询、文件读写、动画调度。网上有帖子在问Future的then回调是不是放进微任务队列我直接给结论是。Dart是单线程事件循环模型所有异步结果都通过事件队列调度具体分成微任务队列和事件队列两层。Future.then注册的回调以及async函数里await后面的代码都会在当前同步代码执行完后立即排进微任务队列执行不需要经过事件循环的下一轮。理解这个对你写热水卡App有什么实际意义最直接的一点是微任务队列里的密集计算仍然会阻塞UI。比如你在then回调里循环几千条记录做汇总这个操作看似异步实际执行时依然卡住主线程用户会看到界面掉帧。解决办法是把重计算放到isolate里或者至少分批处理。另一个意义在于错误处理Future链上如果某个then回调里抛异常没有catchError捕获异常会一直传播到Dart VM的未处理异常处理区这就对应了日志里常见的unhandled exception输出。我处理跨页面数据刷新时也依赖这个机制插入一条消费记录后State更新、列表刷新、统计更新是在同一个微任务队列里依次执行的所以用户看到的永远是“先写库、再刷新、再通知”的顺序不会出现列表已经变但统计还没变的中间态。理解事件循环机制之后很多看似诡异的状态问题都能迎刃而解。6. 高频问题与避坑实录6.1 新建Flutter项目跑不起来的通用排查清单“flutter新建项目后跑不起来”是我收到过最多的反馈之一。这类问题大多数不在代码而在环境。我整理了一份排查顺序按这个走基本能解决90%的情况。第一步先执行flutter doctor看有没有警告。常见的警告包括Dart SDK版本不匹配、Android toolchain缺失、connected device状态异常。第二步执行flutter clean再执行flutter pub get。这一步能清掉一半的疑难杂症尤其是当你刚切过版本分支、或者pubspec.lock文件被IDE改过的时候。第三步检查Gradle缓存。Flutter项目在Android侧会走Gradle构建如果代理或镜像配置有问题依赖下载会卡死表现就是停在某个Building step很久不动。清理掉~/.gradle/caches里的临时文件再重试往往就好了。如果以上都没用重点检查项目目录里的android/gradle-wrapper.properties里的Gradle版本是否和本机JDK兼容。鸿蒙适配分支对这个组合很敏感我记得有一次怎么都编译不过最后发现是NDK版本太新导致的链接器报错换回NDK 25才正常。环境问题一定要按组合来看单独查一个变量很容易查不出结果。6.2 Dart VM初始化报错的定位思路真机运行Flutter应用时日志里常会出现这样一条E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception很多新手看到dart_vm_initializer立刻以为是SDK坏了实际上这行只是Dart VM报错的总入口真正的原因在它下面跟着的异常堆栈里。几乎所有的未捕获异常都会先经过这里所以不要停在这一行要继续往下看具体是哪一行Dart代码抛的。最常见的两种情况一是数据库查询返回的Map里某个字段类型和实体类构造参数不匹配比如SQLite里存的是IntegerDart侧却用String接收运行时cast失败。二是FutureBuilder在异步返回前页面已经销毁setState被调用到已卸载的State对象上报出“setState() called after dispose()”。我的对策是两件事第一所有数据库查询结果统一用map[field] as num?这类安全转换再赋值第二网络请求或数据库回调里调用setState前先检测mounted状态页返回后不再更新界面。6.3 Impeller渲染引擎与鸿蒙真机的兼容性Flutter从3.7开始逐步把渲染引擎从Skia切到Impeller解决了Skia在部分设备上的锯齿和光照效果问题。鸿蒙适配分支在推进过程中Impeller对OpenHarmony图形栈的适配也在逐步完善。我在真机上遇到的情况是大部分页面渲染正常但涉及模糊效果和阴影嵌套较深的场景个别旧芯片的鸿蒙真机会有帧率波动。遇到这类问题可以先确认当前到底是Skia还是Impeller在跑。方法很简单在自定义组件里绘制一个带阴影的圆角矩形观察阴影边缘是否出现明显的颗粒感Impeller的阴影是高斯模糊近似Skia的阴影是软阴影绘制细看有区别。如果不确定也可以用命令行直接关闭Impeller跑一次看性能变化flutter run --no-enable-impeller如果关闭后帧率恢复稳定说明当前设备对Impeller的兼容性还欠点火候可以暂时用Skia渲染如果关闭后更差说明Impeller在那边反而更优。做鸿蒙跨平台开发渲染引擎不是一层不变的一定要具体设备具体测试。另外PlatformView的热点也可以提一句。Flutter在鸿蒙上嵌入原生控件时走的是PlatformView机制和Android的逻辑类似但纹理注册和触摸事件分发的时序不太一样。如果后续要在App里嵌入ArkUI的扫码组件或地图组件提前做好设计不要等到集成阶段才去试。写到最后再说几句实在话这个项目从立项到跑通鸿蒙真机前后花了三个星期其中第一周全耗在环境配置和工具链适配上了。后来我复盘了一下最值得沉淀下来的经验其实不是某个具体API怎么用而是“跨平台开发不要想着一蹴而就”这件事。鸿蒙生态还在快速发展Flutter的适配分支也在持续更新最好的状态是始终保持工具链的可升级性——比如数据层独立、业务逻辑和UI分离这样上游SDK更新时你只需改少量兼容代码。如果你也打算复刻这个项目我给你的建议是先照着教程把整个流程完整跑一遍不要跳过环境配置那章然后立刻动手改两三个功能比如增加一个“日均水温统计”或者把记录列表改成周视图。真正把一个真实业务跑起来之后你对Flutter跨平台、组件通信、独立数据层的理解会和只看教程完全不在一个层次。祝你在鸿蒙上玩得开心有问题欢迎来交流我会把这段时间积累的排查方案持续更新下去。
返回列表