资讯动态

Flutter for OpenHarmony做微动漫首页:技术选型与适配实践

发布时间:2026/9/28 12:18:55 来源:尧图企业网站定制
1. 为什么用Flutter for OpenHarmony做微动漫首页技术选型与项目定位1.1 项目从0到1微动漫的形态与首页需求清单前阵子团队要做一款轻量级的动漫阅读App产品定位非常明确不追求大而全只做精选内容推荐和快速追番体量控制在几MB级别打开App三秒内能刷到内容。这类产品最尴尬的点在于它不像大型平台有充足的人力去投入双端开发但用户又实打实分布在不同的设备生态里。项目组评估了几条技术路线之后最终拍板用Flutter for OpenHarmony来做一套Dart代码同时覆盖移动双端还能吃到鸿蒙生态的设备红利。微动漫的首页承担了整个App最核心的流量入口需求拆下来其实非常清晰顶部Tab导航推荐、分类、榜单、我的四个一级入口搜索框放在首页顶部支持输入关键词跳转搜索页Banner轮播首页顶部展示运营位一般5到8张图自动轮播分类金刚区漫画、动画、轻小说、周边等快捷入口推荐信息流以卡片瀑布流的形式展示内容图文混排下拉刷新与触底加载刷新拉新数据滑动到底部翻页错误状态与重试网络异常时展示占位和重试按钮这些需求在移动端太常见了常见到几乎每个App都有但真正在OpenHarmony真机上把它们一个个跑通踩的坑远比想象中多。选择Flutter for OpenHarmony并不是因为它完美而是因为它是目前投入产出比最高的方案——Flutter的布局、渲染、状态管理生态可以直接复用只需要处理平台差异层。1.2 Flutter在OpenHarmony上的支持现状能跑通但要看清楚边界很多人一想到OpenHarmony上跑Flutter第一反应是跨平台框架怎么能跑到非Android内核的系统上。OpenHarmony确实不是Android但社区维护了一个独立的Flutter分支——flutter_flutter由OpenHarmony SIG特别兴趣组在维护从OpenHarmony 3.x开始就一直有版本在跟进。这个分支保留了Flutter的核心渲染逻辑和Dart运行时底层图形栈对接的是OpenHarmony的Render Service。也就是说Flutter层写的大部分业务代码可以零改动迁移但涉及原生能力的插件就不一定全兼容了需要逐个验证。实际测试下来这个分支在OpenHarmony 3.2/4.0对应的API版本上已经能稳定跑通基础UI、网络请求、图片加载这些能力。但有几个边界要注意插件生态不完整很多pub.dev上的插件没有匹配鸿蒙的原生实现渲染引擎和标准Flutter不完全一致部分效果需要降级处理热重载支持有限开发调试节奏要调整所以如果你是做纯Flutter业务页面、又没有重度依赖Android/iOS特有原生插件的场景Flutter for OpenHarmony完全可用。反之如果你的核心功能强依赖某个没有鸿蒙适配的插件建议先在插件协议层做一层抽象方便切换原生实现。1.3 状态管理选型为什么这次用Cubit而不是Bloc首页的状态管理我最终选了flutter_bloc库里的Cubit组件没有直接用Bloc全家桶。先说一下背景微动漫首页的状态其实不复杂无外乎初次加载、刷新、翻页、失败重试这几种事件种类少、状态转移清晰用Bloc的Event-Stream模式会显得很笨重——每个动作都要定义一个Event类还要写mapEventToState的样板代码。Cubit把Event层去掉了直接方法是动作内部调用emit切换状态。比如刷新行为就是调用refresh()方法翻页就是调用loadMore()方法代码一眼能看明白。我对比过几个方案的取舍方案优点缺点适合场景Cubit代码量少、状态流转直观、上手快复杂联动时需要手动编排中小型页面、事件类型少的业务Bloc事件可追溯、便于debug、适合复杂流程样板代码多、学习成本略高大型应用、复杂交互流程Provider轻量、灵活状态管理比较自由、规范约束弱小型Demo、逻辑简单的页面Riverpod编译期安全、可测试性好概念多、熟悉曲线陡追求可维护性的中型项目微动漫这种轻量产品首页如果引入一整套Event体系以后每个新功能都要写两遍模板时间成本不划算。而且Cubit在以后需求变复杂时是可以平滑升级到Bloc的状态类不用大改。这是比较稳妥的思路。2. OpenHarmony真机上的Flutter环境搭建版本对应与工程初始化2.1 开发环境清单不是装个Flutter就能跑在OpenHarmony上跑Flutter环境比普通Flutter开发要多一套东西而且版本对应关系非常敏感某一步版本不对后面的报错会让人一头雾水。需要的核心环境包括DevEco StudioOpenHarmony应用开发的标准IDE负责管理SDK和签名OpenHarmony SDK根据目标设备系统版本安装对应API的SDKFlutter for OpenHarmony分支从openharmony-sig/flutter_flutter拉取注意要用release分支而不是masterFlutter命令行工具与分支配套的flutter命令用于创建工程和构建真机或模拟器推荐用真机OpenHarmony模拟器的图形验证效果有限版本对应关系是最大的坑。我整理了一个对照表供参考具体以你拉取分支时的文档为准Flutter分支标签对应OpenHarmony版本API Level实测稳定度OpenHarmony-3.2-Release3.2API 9基础功能可稳定运行OpenHarmony-4.0-Release4.0API 10推荐流等复杂UI可用其他开发分支不固定不固定建议只在CI验证切忌直接去flutter官网装最新版的标准Flutter SDK然后跑在OpenHarmony工程上分支不匹配会出现各种莫名其妙的链接错误和运行崩溃。2.2 从分支拉取到初始化工程两条可走的路线我实践下来有两条路线可以创建工程路线一直接用Flutter分支创建。先准备好OpenHarmony分支的Flutter SDK然后执行flutter create --project-name micro_comic --org com.example micro_comic_home生成后打开工程目录会发现和标准Flutter工程的差别主要在多了一个ohos平台目录里面是OpenHarmony的原生工程结构。之后所有Dart业务代码都在lib/下写平台差异代码通过ohos目录接入。路线二从DevEco Studio侧新建。先创建一个空OpenHarmony工程再把Flutter模块作为一个依赖接入。这条路线适合你需要对一个已有OpenHarmony原生应用进行混合开发的场景也就是常说的原生项目嵌入flutter页面。我当时选的是路线一因为微动漫首页是全新开发的没有历史包袱路线一更符合Flutter项目的组织习惯。如果以后原生部分变多再迁移到路线二也不难。2.3 真机默认配置验证首次运行该检查什么工程创建完成后第一次跑真机前务必要检查几个默认配置项。首先确认设备连接正常flutter devices在支持OpenHarmony的分支下设备列表里会显示类似OHOS device的条目。如果看不到设备检查开发者模式是否开启、USB调试授权是否弹窗确认以及hdc工具链是否可用——OpenHarmony用的是hdc而不是adb这是很多人第一次上手时容易忽视的地方。跑之前看下工程的ohos/目录下有没有配置好签名信息。OpenHarmony工程需要签名才能安装到真机DevEco Studio里有自动签名功能命令行构建则需要通过profile文件指定。签名不对的典型表现是构建成功但安装失败错误信息指向证书或profile不匹配。首次flutter run的构建时间会比较长因为要把OpenHarmony原生层和Flutter引擎一起编译我在一台主流配置的机器上第一次跑了大约十几分钟。这个等待时间恰好可以用来核对3.4节里的环境问题清单。2.4 环境问题排查三个高频报错的完整处理链路搭建过程中我遇到了几个典型报错每一个在搜索引擎里都能搜到很多人问这里把排查思路完整写出来。第一个是构建时的Gradle警告You are applying Flutters main Gradle plugin imperatively using the apply script method。这个警告在Flutter 3.16之后大量出现原因是Flutter的Gradle插件改成了通过插件DSL声明方式接入而旧模板里用的是apply script方式。OpenHarmony分支的模板更新速度比主分支慢所以这个警告出现的概率很高。处理方式是打开ohos/app/build.gradle把旧的apply from: $flutterRoot/packages/flutter_tools/gradle/flutter.gradle改成新版插件声明方式具体写法参考当前分支仓库里样例工程的做法。第二个是版本不匹配警告The current configured Flutter SDK is not known to be fully supported。这说明当前Flutter分支和你配置的OpenHarmony SDK版本不在官方验证过的组合里。不要忽略这个警告直接跑后续大概率会踩到编译器找不到API或者引擎运行崩溃的坑。处理方法就是严格按照分支仓库的版本对照表调整OpenHarmony SDK版本。第三个是打包阶段的java.lang.AssertionError: could not close ...错误。这个错误我遇到的情况是构建产出目录被上一轮进程占用导致的。链路的最后一步是执行flutter clean删除build和ohos的build目录再杀掉残留的Java进程重新构建能解决大部分情况。如果还不行检查磁盘空间是否充足以及是否同时开启了多个DevEco/命令行构建进程。3. 首页数据层与状态层设计模型、仓库、Cubit三位一体3.1 数据模型怎么建先别急着上freezed首页涉及的数据结构并不算复杂大概四类Banner、分类入口、推荐卡片、Tab元信息。我在建模时没有选择freezed因为freezed本身会引入build_runner和大量注解依赖对一个小体量项目来说生成的代码比手写代码还多反而加重了编译负担。这里推荐直接手写不可变类和fromJson工厂方法简单直接。以Banner为例class BannerItem { final String id; final String title; final String imageUrl; final String linkUrl; const BannerItem({ required this.id, required this.title, required this.imageUrl, required this.linkUrl, }); factory BannerItem.fromJson(MapString, dynamic json) { return BannerItem( id: json[id] as String? ?? , title: json[title] as String? ?? , imageUrl: json[imageUrl] as String? ?? , linkUrl: json[linkUrl] as String? ?? , ); } }字段全部给一个默认值防止服务端返null时直接抛异常。这是在真实项目中很容易踩的坑服务端理论上不会返回null但一旦返回整个解析链就崩了。微动漫项目用了mock数据源我故意在mock里塞了几个畸形字段来验证解析兜底逻辑实测下来这个习惯帮了不少忙。推荐卡片ComicCard的结构会多一点包括封面图、标题、作者、评分、标签列表、更新状态等。分类入口CategoryEntry就简单得多一个图标地址加一个跳转路由名。3.2 仓库层把接口和页面彻底解耦数据层我用了仓库模式核心思路是定义abstract class HomeRepository然后分别提供Mock实现和远程实现。首页页面不关心数据从哪来只依赖抽象接口。abstract class HomeRepository { FutureListBannerItem fetchBanners(); FutureListCategoryEntry fetchCategories(); FutureListComicCard fetchRecommendList({ required int page, int pageSize 20, }); }Mock实现返回本地固定数据用来跑通UI和交互流程远程实现走真实HTTP接口。切换方式可以用编译环境变量也可以在入口处根据运行模式注入不同的依赖class HomeRepositoryImpl implements HomeRepository { final Dio _dio; HomeRepositoryImpl(this._dio); override FutureListBannerItem fetchBanners() async { final resp await _dio.get(/api/home/banners); final list resp.data[data] as Listdynamic? ?? []; return list.map((e) BannerItem.fromJson(e)).toList(); } ... }这样设计的好处是页面开发、接口联调、单元测试可以并行推进。当时我们的后端接口还没完全就绪但首页UI用Mock数据已经完整跑通了接口好了之后切换一个Repository实现就行页面一行代码没动。这个解耦在跨端项目里尤其重要因为OpenHarmony分支的某些网络库行为可能和标准端有差异你需要有一个干净的隔离层来单独排查。3.3 HomeCubit核心状态流转四种状态吃透首页所有场景状态层是首页的心脏。我把首页状态设计成不可变的数据类用一个枚举标记整体状态enum LoadStatus { initial, loading, success, failure } class HomeState { final LoadStatus status; final String errorMsg; final ListBannerItem banners; final ListCategoryEntry categories; final ListComicCard recommendList; final int currentPage; final bool hasMore; const HomeState({ this.status LoadStatus.initial, this.errorMsg , this.banners const [], this.categories const [], this.recommendList const [], this.currentPage 0, this.hasMore true, }); HomeState copyWith({ LoadStatus? status, String? errorMsg, ListBannerItem? banners, ListCategoryEntry? categories, ListComicCard? recommendList, int? currentPage, bool? hasMore, }) { return HomeState(...); } }Cubit里的核心方法有三个loadHome()处理首屏加载和下拉刷新loadMore()处理触底翻页retry()处理错误重试。关键逻辑我贴出来class HomeCubit extends CubitHomeState { HomeCubit({required HomeRepository repository}) : _repository repository, super(const HomeState()); final HomeRepository _repository; Futurevoid loadHome() async { if (state.status LoadStatus.loading) return; emit(state.copyWith(status: LoadStatus.loading)); try { final results await Future.wait([ _repository.fetchBanners(), _repository.fetchCategories(), _repository.fetchRecommendList(page: 1), ]); emit(state.copyWith( status: LoadStatus.success, errorMsg: , banners: results[0] as ListBannerItem, categories: results[1] as ListCategoryEntry, recommendList: results[2] as ListComicCard, currentPage: 1, hasMore: (results[2] as ListComicCard).length 20, )); } catch (e) { emit(state.copyWith( status: LoadStatus.failure, errorMsg: e.toString(), )); } } Futurevoid loadMore() async { if (state.status ! LoadStatus.success || !state.hasMore) return; final nextPage state.currentPage 1; try { final more await _repository.fetchRecommendList(page: nextPage); emit(state.copyWith( recommendList: [...state.recommendList, ...more], currentPage: nextPage, hasMore: more.length 20, )); } catch (_) { // 底部加载失败时保留原列表避免数据丢失 } } }注意loadHome里用Future.wait把Banner、分类、推荐列表三个请求并发发出首屏耗时取最慢的那个。这里有一个tradeoff——并发会让首屏时间变短但如果其中一个接口挂了整个首页都会失败。对微动漫的体量来说这个风险可以接受因为首页本质上是一个强聚合同一屏展示的内容分步加载反而会让用户看到一半空壳UI。4. 首页UI逐块落地从顶部导航到推荐流的完整实现4.1 整体结构用CustomScrollView撑起一整个首页首页UI如果用ListViewBanner区、金刚区和推荐卡片流就只能拼在ListView的header和item里逻辑会变得很散。我最终用的是CustomScrollView把整个首页拆成几个Sliver块结构一目了然SliverAppBar顶部导航和搜索框SliverToBoxAdapter轮播区和分类金刚区SliverList推荐卡片列表这样的结构还有一个好处滚动性能好。Sliver机制会懒加载不可见区域的内容对图片比较多的推荐流来说内存占用比普通ListView拼header的方式更可控。主页面大体是这样的骨架class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: CustomScrollView( controller: _scrollController, slivers: const [ HomeTopBar(), HomeBannerSection(), HomeCategorySection(), HomeRecommendSection(), ], ), ), ); } }4.2 顶部导航与搜索框Tab切换不能丢状态顶部导航是一个自定义的TabBar放在SliverAppBar的title位置。注意我没有直接用Material的TabBar组件套TabBarView因为TabBarView默认切换Tab时会销毁远离当前Tab的子树这就埋下了状态丢失的隐患——热搜词里那么多人问flutter navigator切换页面后会丢失状态吗多数就是TabBarView这个默认行为导致的。我的方案是在首页这一层维护当前选中Tab的索引每个Tab的内容区域用IndexedStack包住让所有Tab的Widget存活在内存里。这是最保底的方案缺点是会多占一点内存但对微动漫app这种轻量内容那点内存开销完全不值一提。class HomeTopBar extends StatefulWidget { ... } class _HomeTopBarState extends StateHomeTopBar { int _currentIndex 0; override Widget build(BuildContext context) { return SizedBox( height: 48, child: Row( children: [ Expanded( child: TabBar( tabs: const [ Tab(text: 推荐), Tab(text: 分类), Tab(text: 榜单), Tab(text: 我的), ], onTap: (index) setState(() _currentIndex index), ), ), IconButton( icon: const Icon(Icons.search), onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (_) const SearchPage(), )); }, ), ], ), ); } }4.3 轮播区与金刚区自动播放和防误触的细节轮播区用PageView.builder实现外层限制高度大约160。自动播放的核心是Timer交替翻页这里有两个要点。第一翻页动画时间要大于定时器间隔否则会出现轮播抖动。我实测下来的经验是动画300ms、定时器4s比较合适。第二用户手指按住图片时一定要暂停自动播放否则会出现在用户阅读时强制滑走的糟糕体验。具体做法是在NotificationListener里监听PointerDown和PointerUp事件动态暂停和恢复Timer。金刚区是两行共8个入口用GridView.count不滚动来展示shrinkWrap设为truephysics设为NeverScrollableScrollPhysics避免和外围滚动冲突。图标直接用静态资源加上文字标签点击行为统一走一个路由表把入口名映射到目标页面。这是一个经常被低估的扩展点——这个路由表在后续加设置页、搜索历史页、用户中心页时都能复用。4.4 推荐信息流与卡片设计让列表刷新不拖垮整个页面推荐列表是整个首页的流量大头卡片设计直接决定用户停留时长。我用的卡片结构是左侧封面图、右侧标题作者标签底部一行评分和更新状态提示。这个布局在宽屏和窄屏下的适配都很好。关键优化点在于状态读取方式。如果整个首页在State层用context.watchHomeCubit()那么推荐列表里任何一次paging操作的state更新都会导致整棵Widget树重建Banner、金刚区全部跟着重绘。更好的做法是用BlocSelector只监听我们关心的那部分数据BlocSelectorHomeCubit, HomeState, ListComicCard( selector: (state) state.recommendList, builder: (context, cards) { return SliverList( delegate: SliverChildBuilderDelegate( (context, index) ComicCardView(card: cards[index]), childCount: cards.length, ), ); }, )这样推荐列表翻页时只有滑块列表对应的builder会重建Banner和金刚区的Widget完全复用实测在低端OpenHarmony设备上的滚动掉帧明显减少。4.5 下拉刷新与触底加载别把刷新和加载做成冤家下拉刷新用RefreshIndicator包住CustomScrollViewonRefresh回调直接调cubit.loadHome()。这里有一个容易踩的坑loadHome()里我用了Future.wait并发请求如果其中一个接口超时整个刷新都会失败用户会被打回错误状态。处理方式是给超时较长的接口单独设置较短的timeout或者接受这种强一致刷新我选择了后者——因为刷新场景下用户预期就是重新加载全部内容弱化失败反而会造成数据不一致。触底加载是用ScrollController监听滚动位置当距离底部还剩300像素时触发loadMore()void _onScroll() { if (_scrollController.position.extentAfter 300) { _cubit.loadMore(); } }loadMore()内部有状态判断和hasMore判断所以这里不需要额外加防抖锁。但要注意ScrollController监听在页面销毁时要remove否则会引发内存泄漏。我在dispose里统一做清理。加载中的底部指示器我用了一个小条件当state.status是success且state.currentPage 1时在SliverList尾部追加一个loading占位item。这个方案能让用户明确感知到底部在加载避免以为列表到底了。5. 鸿蒙适配中的三个深坑渲染引擎、事件通道与页面状态5.1 Impeller渲染引擎OpenHarmony上需要主动切回Skia这是在真机上遇到的第一个诡异问题。Flutter从3.7开始逐步用Impeller替换Skia作为默认渲染引擎在标准Android/iOS上Impeller的性能表现确实更好。但在OpenHarmony分支上Impeller对OpenHarmony图形栈的适配并不完整表现是部分真机启动后页面白屏或者渲染出大量黑色的闪烁块按搜索热词的说法是一搜flutter impeller就能看到一堆同类问题。解决方向很简单回到Skia渲染。在OpenHarmony分支上跑起来时手动禁用Impellerflutter run --no-enable-impeller -d device-id如果是在IDE里通过DevEco的run配置运行需要在构建参数里同样追加这个flag。我把这个参数写进了工程的构建配置里确保以后不管谁跑这个工程都不会默认踩进Impeller的坑。关于底层原因我从社区讨论和实际表现推测是Impeller在OpenHarmony上使用的图形后端Vulkan/Metal适配还不完善部分驱动下的着色器编译会失败Skia的软件/硬件混合回退机制则成熟得多。等到OpenHarmony分支官方宣布Impeller稳定后再切回来也不迟这个适配工作不是业务层能解决的。5.2 EventChannel和鸿蒙原生层的持续通信首页需要一个能力获取状态栏高度和系统深色模式变化。这类数据不是一次调用能拿完的改动发生的时间点由系统决定所以要用EventChannel而不是MethodChannel。这个选择强烈影响体验——如果用了MethodChannel就得轮询既浪费电量又拿不到实时变化。在Flutter侧封装一个系统通道类class SystemChannel { static const EventChannel _statusBarEvent EventChannel(com.example.microcomic/status_bar); static const EventChannel _darkModeEvent EventChannel(com.example.microcomic/dark_mode); Streamdouble get statusBarHeightStream { return _statusBarEvent.receiveBroadcastStream().map((raw) { return (raw as num).toDouble(); }); } Streambool get darkModeStream { return _darkModeEvent.receiveBroadcastStream().map((raw) { return raw as bool; }); } }鸿蒙侧的原生代码需要找到ohos工程里的主Ability入口在OnStart里注册EventChannel的stream handler通过鸿蒙的系统能力监听状态栏变化并不断向Flutter侧发送事件。这里踩过一个小坑EventChannel在页面切到后台时会断流Flutter侧需要做好stream的重连逻辑否则从后台恢复时状态栏高度数据一直是旧的。5.3 页面状态丢失IndexedStack和AutomaticKeepAlive的取舍搜索热词里flutter navigator切换页面后会丢失状态吗这个问题我在这项目里真实踩到了。现象是首页推荐流滚动到第30条点进详情页查看返回首页滚动位置丢了停在顶部。排查链路比较典型。先确认为什么丢——详情页用Navigator.push压栈首页本身没有销毁但Tab内容区用了TabBarView且推荐流是个CustomScrollView没有配置KeepAlive。TabBarView切换时远离当前Tab的内容会被销毁重建重建后ScrollController的offset自然归零。修复方案分两层Tab层把TabBarView换成IndexedStack避免Tab切换销毁页面列表层给推荐流Widget混入AutomaticKeepAliveClientMixin同时在ScrollController初始化时传入PageStorageKey绑定页面存储class RecommendList extends StatefulWidget { ... } class _RecommendListState extends StateRecommendList with AutomaticKeepAliveClientMixin { override bool get wantKeepAlive true; ... }PageStorageKey的作用是让Scrollable的滚动偏移量通过PageStorage机制保存和恢复。这两层加完从详情页返回、切换Tab再回来滚动位置都能保持在原来位置。实测在API 10的设备上状态留存稳定。5.4 热重载受限之后我调整了开发调试节奏OpenHarmony分支的热重载支持不稳定是公认的事实。我在开发时经常遇到改完代码按R页面确实刷新了但滚动位置和状态全部重置还会偶发卡死只能重新flutter run。这个体验相比标准Flutter确实打折。调整后的调试策略是把状态层和UI层分开验证。状态层Cubit、仓库、模型写单元测试在JVM环境跑不依赖真机逻辑改动完全靠测试兜底。UI层则尽量把改动集中在某个独立的Sliver内改完不足够信任的部分才重启真机验证。另外建议在工程里加一份结构化日志把首页的事件和状态变化都打出来加载开始、加载成功、触底加载触发、失败原因。这样即使热重载时不时失灵也能从日志里快速定位问题。日志要打到终端而不是日志面板因为真机日志面板在OpenHarmony分支上的筛选能力一般。5.5 中文字体与图片缓存两个容易被低估的适配项最后补一个没有在需求清单里但上线前必须处理的点中文字体。部分OpenHarmony真机的系统字体对某些生僻字的支持不完整尤其是漫画标题和作者名里经常出现特殊字符直接渲染会变成方框或空白。排查方法是在测试用例里故意放一堆作者最爱用的生僻字逐个真机截图比对。如果发现问题可以在字体配置里指定系统里的中文字体族或者把常用中文子集字体打包进Assets。图片缓存方面也建议提前验证cached_network_image插件在OpenHarmony分支上的表现。这个插件依赖原生层缓存能力一些版本在鸿蒙上会静默失败表现为图片闪一下就空白。如果遇到就用Flutter侧自建一层内存缓存来兜底或者限制并发加载数量来降低磁盘缓存压力。这些都是文档里不会写、但真机会教你做人的细节。回到最初的问题为什么值得用Flutter for OpenHarmony来做这样一个轻量动漫App的首页我现在的答案是如果你已经熟悉Flutter生态希望在鸿蒙设备上用最低成本覆盖一套精致的首页体验这条路是通的——只要把渲染引擎、状态保活、事件通道这几个平台差异点提前想清楚整个开发过程其实是可控的。而且随着OpenHarmony分支的迭代这些坑只会越来越少但项目里的架构设计一旦站稳后续扩展详情页、阅读器、个人中心都只是在同一套地基上填砖而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑