Flutter for OpenHarmony 实战:基于HarmonyOS ArkTS API 24 实现一个饼图
在移动开发领域我们总是面临着选择与适配。今天你的Flutter应用在Android和iOS上跑得正欢明天可能就需要考虑一个新的平台HarmonyOS鸿蒙。这不是一道选答题而是很多团队正在面对的现实。Flutter的优势很明确——写一套代码就能在两个主要平台上运行开发体验流畅。而鸿蒙代表的是下一个时代的互联生态它不仅仅是手机系统更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙听起来像是一个“跨界”任务但它本质上是一次有价值的技术拓展让产品触达更多用户也让技术栈覆盖更广。不过这条路走起来并不像听起来那么简单。Flutter和鸿蒙从底层的架构到上层的工具链都有着各自的设计逻辑。会遇到一些具体的问题代码如何组织原有的功能在鸿蒙上如何实现那些平台特有的能力该怎么调用更实际的是从编译打包到上架部署整个流程都需要重新摸索。这篇文章想做的就是把这些我们趟过的路、踩过的坑清晰地摊开给你看。我们不会只停留在“怎么做”还会聊到“为什么得这么做”以及“如果出了问题该往哪想”。这更像是一份实战笔记源自真实的项目经验聚焦于那些真正卡住过我们的环节。无论你是在为一个成熟产品寻找新的落地平台还是从一开始就希望构建能面向多端的应用这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同掌握了关键的衔接技术不仅能完成这次迁移更能积累起应对未来技术变化的能力。混合工程结构深度解析项目目录架构当Flutter项目集成鸿蒙支持后典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构my_flutter_harmony_app/ ├── lib/ # Flutter业务代码基本不变 │ ├── main.dart # 应用入口 │ ├── home_page.dart # 首页 │ └── utils/ │ └── platform_utils.dart # 平台工具类 ├── pubspec.yaml # Flutter依赖配置 ├── ohos/ # 鸿蒙原生层核心适配区 │ ├── entry/ # 主模块 │ │ └── src/main/ │ │ ├── ets/ # ArkTS代码 │ │ │ ├── MainAbility/ │ │ │ │ ├── MainAbility.ts # 主Ability │ │ │ │ └── MainAbilityContext.ts │ │ │ └── pages/ │ │ │ ├── Index.ets # 主页面 │ │ │ └── Splash.ets # 启动页 │ │ ├── resources/ # 鸿蒙资源文件 │ │ │ ├── base/ │ │ │ │ ├── element/ # 字符串等 │ │ │ │ ├── media/ # 图片资源 │ │ │ │ └── profile/ # 配置文件 │ │ │ └── en_US/ # 英文资源 │ │ └── config.json # 应用核心配置 │ ├── ohos_test/ # 测试模块 │ ├── build-profile.json5 # 构建配置 │ └── oh-package.json5 # 鸿蒙依赖管理 └── README.md目录展示效果图片功能代码实现FlPieChart 组件实现主页面集成开发中容易遇到的问题总结开发中用到的技术点展示效果图片flutter 实时预览 效果展示运行到鸿蒙虚拟设备中效果展示功能代码实现FlPieChart 组件实现FlPieChart 是一个基于 fl_chart 库实现的饼图组件具有以下特性支持自定义标题和数据实现点击交互效果包含图例说明响应式布局设计核心代码实现importpackage:flutter/material.dart;importpackage:fl_chart/fl_chart.dart;classFlPieChartextendsStatefulWidget{finalStringtitle;finalListMapString,dynamicdata;finaldouble?width;finaldouble?height;constFlPieChart({Key?key,requiredthis.title,requiredthis.data,this.width,this.height,}):super(key:key);overrideStateFlPieChartcreateState()_FlPieChartState();}class_FlPieChartStateextendsStateFlPieChart{int?_touchedIndex;String_clickInfo;// 颜色列表finalListColorcolors[Colors.blue,Colors.green,Colors.orange,Colors.purple,Colors.red,Colors.yellow,Colors.teal,Colors.pink,];void_onChartTap(PieTouchResponse?response){if(response!nullresponse.touchedSection!null){setState((){_touchedIndexresponse.touchedSection!.touchedSectionIndex;finaldataItemwidget.data[_touchedIndex!];_clickInfo点击了: ${dataItem[name]} (${dataItem[value]});});// 显示点击信息提示ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(${widget.data[_touchedIndex!][name]}: ${widget.data[_touchedIndex!][value]}),duration:constDuration(seconds:1),),);}}overrideWidgetbuild(BuildContextcontext){returnContainer(width:widget.width??double.infinity,height:widget.height??400,margin:constEdgeInsets.all(16),padding:constEdgeInsets.all(16),decoration:BoxDecoration(borderRadius:BorderRadius.circular(8),color:Colors.white,boxShadow:[BoxShadow(color:Colors.grey.withOpacity(0.2),spreadRadius:1,blurRadius:3,offset:constOffset(0,1),),],),child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[Text(widget.title,style:constTextStyle(fontSize:18,fontWeight:FontWeight.bold,color:Colors.black87,),),constSizedBox(height:16),Expanded(child:Row(children:[// 饼图Expanded(child:PieChart(PieChartData(pieTouchData:PieTouchData(touchCallback:(event,response)_onChartTap(response),),borderData:FlBorderData(show:false,),sectionsSpace:0,centerSpaceRadius:60,sections:widget.data.asMap().entries.map((entry){finalindexentry.key;finaldataentry.value;finalisTouchedindex_touchedIndex;finalradiusisTouched?100.0:80.0;returnPieChartSectionData(value:data[value]asdouble???(data[value]asint).toDouble(),title:data[name]asString,color:colors[index%colors.length],radius:radius,titleStyle:TextStyle(fontSize:isTouched?14:12,fontWeight:FontWeight.bold,color:Colors.white,),);}).toList(),),),),// 图例Container(width:100,child:ListView.builder(itemCount:widget.data.length,itemBuilder:(context,index){finaldatawidget.data[index];returnPadding(padding:constEdgeInsets.symmetric(vertical:4),child:Row(children:[Container(width:12,height:12,decoration:BoxDecoration(color:colors[index%colors.length],shape:BoxShape.circle,),),constSizedBox(width:8),Expanded(child:Text(data[name]asString,style:constTextStyle(fontSize:12,color:Colors.black87,),overflow:TextOverflow.ellipsis,),),],),);},),),],),),if(_clickInfo.isNotEmpty)Padding(padding:constEdgeInsets.only(top:12),child:Text(_clickInfo,style:constTextStyle(fontSize:14,color:Colors.grey,),),),],),);}}组件参数说明参数类型必填描述titleString是饼图标题dataListMapString, dynamic是饼图数据每个元素包含 name 和 value 字段widthdouble?否组件宽度默认充满父容器heightdouble?否组件高度默认 400使用方法FlPieChart(title:访问来源分析,data:[{value:335,name:直接访问},{value:310,name:邮件营销},{value:234,name:联盟广告},{value:135,name:视频广告},{value:1548,name:搜索引擎}],)主页面集成主页面使用 MyHomePage 组件实现集成了两个 FlPieChart 实例分别展示访问来源分析和产品销售分布数据。核心代码实现importpackage:flutter/material.dart;importpackage:aa/widgets/fl_pie_chart.dart;voidmain(){runApp(constMyApp());}classMyAppextendsStatelessWidget{constMyApp({super.key});overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:Flutter for openHarmony,theme:ThemeData(colorScheme:ColorScheme.fromSeed(seedColor:Colors.deepPurple),useMaterial3:true,),debugShowCheckedModeBanner:false,home:constMyHomePage(title:Flutter for openHarmony),);}}classMyHomePageextendsStatefulWidget{constMyHomePage({super.key,requiredthis.title});finalStringtitle;overrideStateMyHomePagecreateState()_MyHomePageState();}class_MyHomePageStateextendsStateMyHomePage{// 饼图数据finalListMapString,dynamicpieData1[{value:335,name:直接访问},{value:310,name:邮件营销},{value:234,name:联盟广告},{value:135,name:视频广告},{value:1548,name:搜索引擎}];finalListMapString,dynamicpieData2[{value:120,name:A产品},{value:200,name:B产品},{value:150,name:C产品},{value:80,name:D产品},{value:70,name:E产品}];overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:Text(widget.title),),body:SingleChildScrollView(padding:constEdgeInsets.all(16),child:Column(crossAxisAlignment:CrossAxisAlignment.center,children:Widget[constSizedBox(height:20),// 第一个饼图FlPieChart(title:访问来源分析,data:pieData1,),constSizedBox(height:40),// 第二个饼图FlPieChart(title:产品销售分布,data:pieData2,),constSizedBox(height:40),constText(交互说明点击饼图扇区查看详细信息,style:TextStyle(fontSize:16,color:Colors.grey,),),constSizedBox(height:40),],),),);}}开发中容易遇到的问题WebView 依赖问题问题初始使用 flutter_echarts 库时在 OpenHarmony 平台上遇到 WebViewPlatform 初始化失败的错误原因flutter_echarts 库依赖 WebView 实现图表渲染而 OpenHarmony 平台对 WebView 的支持不完善解决方案改用纯 Flutter 实现的 fl_chart 库避免 WebView 依赖fl_chart API 调用问题问题使用 fl_chart 库时PieTouchData 的 touchCallback 方法签名与预期不符原因不同版本的 fl_chart 库 API 可能存在差异解决方案使用适配的回调方式通过匿名函数转换参数格式数据格式问题问题饼图数据中的 value 字段类型需要为 double原因fl_chart 库的 PieChartSectionData.value 参数要求为 double 类型解决方案在构建 PieChartSectionData 时对 int 类型的 value 进行类型转换布局适配问题问题在不同屏幕尺寸下饼图和图例的布局可能出现异常原因固定宽度的图例可能在小屏幕上显示不全解决方案使用 Expanded 组件和响应式布局确保在不同屏幕尺寸下的良好显示效果总结开发中用到的技术点Flutter 状态管理使用 StatefulWidget 和 setState 管理组件状态实现饼图点击交互效果通过状态变量 _touchedIndex 跟踪当前点击的饼图扇区Flutter 布局系统使用 Container、Column、Row、Expanded 等布局组件构建响应式界面运用 BoxDecoration 实现卡片阴影效果提升视觉体验fl_chart 图表库基于 fl_chart 库实现饼图渲染配置 PieChartData、PieChartSectionData 等参数实现自定义饼图效果使用 PieTouchData 实现饼图点击交互数据处理使用 ListMapString, dynamic 存储和管理饼图数据通过 asMap().entries.map() 方法转换数据格式适配图表库要求用户交互实现饼图点击事件处理显示详细信息使用 ScaffoldMessenger 显示 SnackBar 提示信息代码组织采用组件化开发方式将饼图封装为独立的 FlPieChart 组件分离业务逻辑和 UI 展示提高代码可维护性跨平台适配选择跨平台兼容性好的 fl_chart 库避免平台特定依赖确保代码在 Flutter 支持的所有平台包括 OpenHarmony上正常运行