Flutter for OpenHarmony 实战: 实现一个折线图
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
目录
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的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
展示效果图片
flutter 实时预览 效果展示

运行到鸿蒙虚拟设备中效果展示
功能代码实现
本次开发实现了一个基于Flutter for OpenHarmony的折线图组件,使用fl_chart库来实现类似ECharts的效果。下面详细介绍各个组件的开发实现和使用方法。
1. 数据模型设计
首先,我们需要定义一个数据模型来存储折线图的数据,包括日期、数值、标签和颜色等信息。
class LineChartDataModel {
final List<DateTime> dates;
final List<double> values;
final String label;
final Color color;
LineChartDataModel({
required this.dates,
required this.values,
required this.label,
required this.color,
});
}
这个数据模型包含四个关键字段:
dates:存储数据点的日期列表values:存储数据点的数值列表label:数据集的名称,用于图例显示color:数据集的颜色,用于折线和图例显示
2. 折线图主组件
接下来,我们创建一个名为CustomLineChart的有状态组件,作为折线图的主容器,负责管理状态和布局。
class CustomLineChart extends StatefulWidget {
final List<LineChartDataModel> data;
final String title;
final Function(int, double, DateTime)? onPointClick;
const CustomLineChart({
Key? key,
required this.data,
this.title = '折线图',
this.onPointClick,
}) : super(key: key);
_CustomLineChartState createState() => _CustomLineChartState();
}
class _CustomLineChartState extends State<CustomLineChart> {
int? _touchedIndex;
Widget build(BuildContext context) {
return Column(
children: [
Text(
widget.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Expanded(
child: LineChartWidget(
data: widget.data,
touchedIndex: _touchedIndex,
onPointTap: (index, value, date) {
setState(() {
_touchedIndex = index;
});
if (widget.onPointClick != null) {
widget.onPointClick!(index, value, date);
}
},
onTooltipClose: () {
setState(() {
_touchedIndex = null;
});
},
),
),
const SizedBox(height: 16),
_buildLegend(),
],
);
}
Widget _buildLegend() {
return Wrap(
spacing: 20,
runSpacing: 10,
children: widget.data.map((series) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: series.color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(series.label),
],
);
}).toList(),
);
}
}
组件说明
-
CustomLineChart接收三个参数:data:折线图数据列表title:折线图标题onPointClick:点击数据点时的回调函数
-
组件内部使用
_touchedIndex来跟踪当前点击的数据点索引,实现交互效果 -
_buildLegend方法用于生成图例,显示每个数据集的名称和颜色
3. 折线图绘制组件
然后,我们创建一个名为LineChartWidget的无状态组件,负责实际的折线图绘制逻辑。
class LineChartWidget extends StatelessWidget {
final List<LineChartDataModel> data;
final int? touchedIndex;
final Function(int, double, DateTime)? onPointTap;
final Function()? onTooltipClose;
const LineChartWidget({
Key? key,
required this.data,
this.touchedIndex,
this.onPointTap,
this.onTooltipClose,
}) : super(key: key);
Widget build(BuildContext context) {
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
drawHorizontalLine: true,
horizontalInterval: 1,
verticalInterval: 1,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey.withOpacity(0.3),
strokeWidth: 1,
);
},
getDrawingVerticalLine: (value) {
return FlLine(
color: Colors.grey.withOpacity(0.3),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < data[0].dates.length) {
final date = data[0].dates[index];
return Text('${date.month}/${date.day}');
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (value, meta) {
return Text('${value.toInt()}');
},
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.grey.withOpacity(0.3)),
),
minX: 0,
maxX: (data.isNotEmpty ? data[0].dates.length - 1 : 0).toDouble(),
minY: 0,
maxY: _getMaxY(),
lineBarsData: _getLineBarsData(),
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) {
final seriesIndex = spot.barIndex;
final dataIndex = spot.x.toInt();
final series = data[seriesIndex];
final value = series.values[dataIndex];
final date = series.dates[dataIndex];
return LineTooltipItem(
'${series.label}: ${value.toStringAsFixed(1)}\nDate: ${date.month}/${date.day}',
TextStyle(color: series.color),
);
}).toList();
},
),
handleBuiltInTouches: true,
touchCallback: (event, response) {
if (onTooltipClose != null && event is FlPointerExitEvent) {
onTooltipClose!();
}
},
),
),
);
}
double _getMaxY() {
double max = 0;
for (var series in data) {
for (var value in series.values) {
if (value > max) {
max = value;
}
}
}
return max * 1.1; // Add 10% padding
}
List<LineChartBarData> _getLineBarsData() {
return data.asMap().entries.map((entry) {
final index = entry.key;
final series = entry.value;
return LineChartBarData(
spots: series.values.asMap().entries.map((valueEntry) {
return FlSpot(valueEntry.key.toDouble(), valueEntry.value);
}).toList(),
isCurved: true,
color: series.color,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 6,
color: barData.color ?? Colors.blue,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
belowBarData: BarAreaData(
show: false,
),
);
}).toList() as List<LineChartBarData>;
}
}
组件说明
-
LineChartWidget接收四个参数:data:折线图数据列表touchedIndex:当前点击的数据点索引onPointTap:点击数据点时的回调函数onTooltipClose:关闭工具提示时的回调函数
-
组件内部实现了以下功能:
- 网格线绘制:使用
FlGridData配置网格线的样式和间隔 - 坐标轴标签:使用
FlTitlesData配置四个方向的标签,底部显示日期,左侧显示数值 - 边框设置:使用
FlBorderData配置图表边框 - 数据范围:根据数据自动计算X轴和Y轴的范围
- 折线绘制:使用
_getLineBarsData方法生成折线数据 - 交互效果:使用
LineTouchData配置点击交互和工具提示
- 网格线绘制:使用
-
_getMaxY方法用于计算Y轴的最大值,并添加10%的 padding 以确保所有数据点都能显示 -
_getLineBarsData方法用于生成折线数据,包括曲线设置、颜色、宽度、数据点等
4. 主页面集成
最后,我们需要在主页面中集成折线图组件,并添加示例数据和交互逻辑。
import 'package:flutter/material.dart';
import 'package:aa/components/line_chart.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter for openHarmony',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(title: 'Flutter for openHarmony'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late List<LineChartDataModel> _lineData;
void initState() {
super.initState();
_initLineData();
}
void _initLineData() {
// Generate sample dates for the last 7 days
final dates = List<DateTime>.generate(
7,
(i) => DateTime.now().subtract(Duration(days: 6 - i)),
);
// Create sample data sets
_lineData = [
LineChartDataModel(
dates: dates,
values: [12, 19, 13, 15, 20, 25, 22],
label: '数据集1',
color: Colors.blue,
),
LineChartDataModel(
dates: dates,
values: [10, 15, 18, 12, 16, 19, 21],
label: '数据集2',
color: Colors.green,
),
LineChartDataModel(
dates: dates,
values: [5, 8, 12, 10, 14, 17, 15],
label: '数据集3',
color: Colors.red,
),
];
}
void _onLinePointClick(int index, double value, DateTime date) {
// Handle line chart point click
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('点击了点 $index: 值 $value, 日期 ${date.month}/${date.day}'),
duration: const Duration(seconds: 2),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
const Text(
'折线图展示',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Expanded(
child: CustomLineChart(
data: _lineData,
title: '多数据集折线图',
onPointClick: _onLinePointClick,
),
),
],
),
),
);
}
}
集成说明
-
在
_MyHomePageState中,我们初始化了一个包含3个数据集的示例数据,每个数据集包含7天的日期和对应的数值 -
_onLinePointClick方法用于处理点击事件,当用户点击数据点时,显示一个包含详细信息的 SnackBar -
在
build方法中,我们使用CustomLineChart组件来显示折线图,并传递数据、标题和点击回调函数
5. 依赖配置
要使用fl_chart库,我们需要在pubspec.yaml文件中添加依赖:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
fl_chart: ^0.68.0 # Flutter chart library
使用方法
要使用这个折线图组件,只需按照以下步骤操作:
-
导入组件:
import 'package:your_package/components/line_chart.dart'; -
创建数据模型:
final dates = List<DateTime>.generate( 7, (i) => DateTime.now().subtract(Duration(days: 6 - i)), ); final lineData = [ LineChartDataModel( dates: dates, values: [12, 19, 13, 15, 20, 25, 22], label: '数据集1', color: Colors.blue, ), // 添加更多数据集... ]; -
添加组件到页面:
CustomLineChart( data: lineData, title: '多数据集折线图', onPointClick: (index, value, date) { // 处理点击事件 }, );
开发注意事项
-
数据长度一致性:确保每个数据集的
dates和values列表长度一致,否则可能会导致索引越界错误 -
日期处理:目前的实现假设所有数据集使用相同的日期列表,在底部标签中只使用第一个数据集的日期
-
性能优化:对于大量数据点的情况,可能需要考虑使用抽样或分页加载来优化性能
-
适配性:在不同屏幕尺寸上,可能需要调整图表的大小和字体大小,以确保良好的显示效果
-
版本兼容性:fl_chart库的API可能会随着版本更新而变化,需要注意版本兼容性问题
本次开发中容易遇到的问题
在本次开发过程中,我们遇到了一些常见的问题,下面列出并提供解决方案:
1. fl_chart版本兼容性问题
问题描述:在使用fl_chart库时,发现某些API在不同版本之间存在差异,导致编译错误。
解决方案:
- 明确指定fl_chart的版本,例如
^0.68.0 - 查阅对应版本的文档,了解API的正确使用方法
- 注意以下API变化:
SideTitles需要包装在AxisTitles中LineChartBarData的color属性替代了colors数组mouseCursorResolver参数的类型可能不同
2. 组件命名冲突
问题描述:自定义的LineChart组件与fl_chart库中的LineChart组件命名冲突,导致编译错误。
解决方案:
- 重命名自定义组件,例如改为
CustomLineChart - 在使用时确保导入路径正确,避免命名冲突
3. 数据类型转换问题
问题描述:在处理数据时,可能会遇到类型转换问题,例如将List<dynamic>转换为List<LineChartBarData>。
解决方案:
- 使用显式类型转换,例如
as List<LineChartBarData> - 确保数据处理过程中的类型一致性
4. 设备连接问题
问题描述:运行项目时,提示"No supported devices connected",无法在OpenHarmony设备上运行。
解决方案:
- 确保已正确安装OpenHarmony开发环境
- 连接OpenHarmony设备或启动OpenHarmony模拟器
- 检查设备是否已在开发模式下启用
5. 依赖下载问题
问题描述:执行flutter pub get时,依赖下载缓慢或失败。
解决方案:
- 配置Flutter镜像源,例如使用国内镜像
- 确保网络连接稳定
- 尝试使用
flutter pub get --verbose查看详细的错误信息
总结本次开发中用到的技术点
本次开发使用了以下技术点:
1. Flutter基础
- Widget体系:使用StatefulWidget和StatelessWidget构建UI组件
- 布局系统:使用Column、Expanded、Padding等布局组件
- 状态管理:使用setState管理组件状态
- 回调函数:使用Function类型传递回调函数,实现组件间通信
2. fl_chart库
- 折线图绘制:使用LineChart组件绘制折线图
- 数据模型:使用LineChartData、LineChartBarData等数据模型
- 配置选项:使用FlGridData、FlTitlesData、FlBorderData等配置图表样式
- 交互功能:使用LineTouchData实现点击交互和工具提示
3. 数据处理
- 日期处理:使用DateTime类生成和处理日期数据
- 数据转换:将原始数据转换为图表所需的格式
- 数据计算:计算数据范围和最大值
4. Flutter for OpenHarmony
- 项目结构:了解Flutter for OpenHarmony的项目结构
- 依赖管理:在pubspec.yaml中添加和管理依赖
- 跨平台适配:确保代码在OpenHarmony平台上正常运行
5. 开发工具和流程
- Flutter CLI:使用flutter命令行工具创建和运行项目
- 代码编辑器:使用支持Flutter的代码编辑器,例如VS Code或Android Studio
- 调试技巧:使用print语句和IDE的调试工具进行调试
6. 最佳实践
- 组件化开发:将图表功能封装为独立的组件,提高代码复用性
- 数据模型设计:使用清晰的数据模型来组织数据
- 代码注释:添加适当的注释,提高代码可读性
- 错误处理:考虑边界情况,避免运行时错误
通过本次开发,我们成功实现了一个功能完整、交互友好的折线图组件,展示了如何在Flutter for OpenHarmony项目中使用第三方库来实现复杂的图表功能。这为我们在未来的项目中开发类似的可视化组件提供了参考和基础。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)