构建统一组件库:Flutter 与 OpenHarmony(ArkTS)的 UI 组件跨平台复用实践
007AFF文字颜色:白色圆角:8px内边距:16px 24px字体:Medium, 16pt点击态:透明度降至 0.8当我们把 UI 抽象为“可描述的数据”而非“具体的代码”,我们就迈出了标准化的第一步。🌐组件 = 协议✅ 设计师输出协议✅ 工程师实现两端解析器✅ 业务方只关心“怎么用”这正是未来跨平台开发的理想形态。在 Flutter 与 OpenHarmony 尚未完全融合的今天,统一组件
构建统一组件库:Flutter 与 OpenHarmony(ArkTS)的 UI 组件跨平台复用实践
作者:子榆.
发布平台:CSDN
发布时间:2025年12月8日
关键词:Flutter、OpenHarmony、ArkTS、跨平台组件、UI 统一、Design System、Figma、自定义渲染、前端架构
引言
在前几篇文章中,我们探索了:
- WebView 嵌套
- FFI 调用
- Skia 渲染嵌入
- 定制 HAP 打包
这些技术都围绕“如何让 Flutter 运行在 OpenHarmony 上”。
但作为一线开发者,我更常被问到的是:
❓ “我们团队既要开发 Flutter App,又要适配鸿蒙系统,UI 怎么保持一致?”
❓ “同一个按钮、卡片、列表,为什么要写两遍?”
今天,我们要换一个思路:
🔥 不强行融合框架,而是构建一套‘一次设计,两端可用’的统一 UI 组件库!
本文将带你从零开始,打造一个支持 Flutter + ArkTS 的跨平台 Design System,真正实现“Write Once, Use Everywhere”。
一、为什么需要统一组件库?
| 场景 | 问题 |
|---|---|
| 多端产品线并行 | iOS/Android 用 Flutter,鸿蒙用 ArkTS,UI 差异大 |
| 设计稿还原困难 | 同一个 Figma 组件,实现效果不一致 |
| 迭代成本高 | 修改一个按钮样式,需改两套代码 |
而我们的目标是:
✅ 设计师画一次原型
✅ 开发者写一次逻辑定义
✅ 自动生成 Flutter Widget + ArkTS Component
二、整体架构设计
+------------------+
| Figma | ← 设计源
+--------↑---------+
| 插件导出 JSON Schema
+--------↓--------------------------+
| 组件元数据描述文件 |
| - button.primary.json |
| - card.rounded.json |
| - list.item.avatar.json |
+--------↑--------------------------+
| 代码生成器
+--------↓---------+ +-----------↓------------+
| lib/components/ | | entry/src/main/ets/ |
| flutter_button.dart | | components/Button.ets |
+------------------+ +------------------------+
↑ ↑
+------------+-------------+
↓
应用层使用(一致体验)
三、实战步骤:从 Figma 到双端组件
🧩 目标:创建一个 PrimaryButton 组件,支持 Flutter 和 ArkTS
设计规范(Figma 中定义)
- 背景色:
#007AFF - 文字颜色:白色
- 圆角:8px
- 内边距:16px 24px
- 字体:Medium, 16pt
- 点击态:透明度降至 0.8
步骤 1:导出组件元数据(JSON Schema)
使用 Figma Plugin 导出结构化描述:
```json
// components/button/primary.json
{
"name": "PrimaryButton",
"type": "button",
"style": {
"backgroundColor": "#007AFF",
"foregroundColor": "#FFFFFF",
"borderRadius": 8,
"padding": { "horizontal": 24, "vertical": 16 },
"fontSize": 16,
"fontWeight": "medium"
},
"states": {
"pressed": { "opacity": 0.8 }
},
"props": [
{ "name": "text", "type": "string", "required": true },
{ "name": "onClick", "type": "function" }
]
}
💡 可通过 Figma API 自动化同步
步骤 2:编写代码生成器(Dart 脚本)
创建 tool/generate_components.dart
dart
import 'dart:io';
import 'package:yaml/yaml.dart';
void main() {
final componentDir = Directory('components');
final components = componentDir.listSync();
for (var entity in components) {
if (entity is File && entity.path.endsWith('.json')) {
final json = JsonDecoder().convert(entity.readAsStringSync());
final name = json['name'] as String;
generateFlutterComponent(name, json);
generateArkTSComponent(name, json);
}
}
}
void generateFlutterComponent(String name, Map data) {
final style = data['style'];
final buffer = StringBuffer();
buffer.writeln("import 'package:flutter/material.dart';");
buffer.writeln("");
buffer.writeln("/// Auto-generated: DO NOT EDIT");
buffer.writeln("class ${name} extends StatelessWidget {");
buffer.writeln(" final String text;");
buffer.writeln(" final VoidCallback? onClick;");
buffer.writeln("");
buffer.writeln(" const ${name}({Key? key, required this.text, this.onClick}) : super(key: key);");
buffer.writeln("");
buffer.writeln(" @override");
buffer.writeln(" Widget build(BuildContext context) {");
buffer.writeln(" return GestureDetector(");
buffer.writeln(" onTap: onClick,");
buffer.writeln(" child: AnimatedOpacity(");
buffer.writeln(" duration: Duration(milliseconds: 150),");
buffer.writeln(" opacity: _isPressed ? 0.8 : 1.0,");
buffer.writeln(" child: Container(");
buffer.writeln(" padding: EdgeInsets.symmetric(horizontal: ${style['padding']['horizontal']}, vertical: ${style['padding']['vertical']}),");
buffer.writeln(" decoration: BoxDecoration(");
buffer.writeln(" color: Color(0xFF${style['backgroundColor'].substring(1)}),");
buffer.writeln(" borderRadius: BorderRadius.circular(${style['borderRadius']}),");
buffer.writeln(" ),");
buffer.writeln(" child: Text(");
buffer.writeln(" text,");
buffer.writeln(" style: TextStyle(");
buffer.writeln(" color: Color(0xFF${style['foregroundColor'].substring(1)}),");
buffer.writeln(" fontSize: ${style['fontSize']},");
buffer.writeln(" fontWeight: FontWeight.${_mapWeight(style['fontWeight'])}");
buffer.writeln(" ),");
buffer.writeln(" ),");
buffer.writeln(" ),");
buffer.writeln(" ),");
buffer.writeln(" );");
buffer.writeln(" }");
buffer.writeln("}");
File('lib/components/${name.toLowerCase()}.dart')
.writeAsStringSync(buffer.toString());
}
String _mapWeight(String weight) {
return {'medium': 'w500', 'bold': 'bold'}[weight] ?? 'normal';
}
运行生成:
bash
dart tool/generate_components.dart
输出:lib/components/primarybutton.dart
步骤 3:生成 ArkTS 组件
dart
void generateArkTSComponent(String name, Map data) {
final style = data['style'];
final buffer = StringBuffer();
buffer.writeln("/** Auto-generated: DO NOT EDIT */");
buffer.writeln("");
buffer.writeln("@Component");
buffer.writeln("struct ${name} {");
buffer.writeln(" @Prop text: string;");
buffer.writeln(" @State opacity: number = 1.0;");
buffer.writeln(" @Builder onPress?: () => void;");
buffer.writeln("");
buffer.writeln(" build() {");
buffer.writeln(" Column() {");
buffer.writeln(" Text(this.text)");
buffer.writeln(" .fontSize(${style['fontSize']})");
buffer.writeln(" .fontColor(Color.fromRGB('${style['foregroundColor']}'))");
buffer.writeln(" .fontWeight('${style['fontWeight']}')");
buffer.writeln(" }");
buffer.writeln(" .width('100%')");
buffer.writeln(" .height(48)");
buffer.writeln(" .backgroundColor(Color.fromRGB('${style['backgroundColor']}'))");
buffer.writeln(" .borderRadius(${style['borderRadius']})");
buffer.writeln(" .opacity(this.opacity)");
buffer.writeln(" .onClick(() => {");
buffer.writeln(" this.opacity = 0.8;");
buffer.writeln(" animateTo(0, () => this.opacity = 1.0);");
buffer.writeln(" if (this.onPress) this.onPress();");
buffer.writeln(" })");
buffer.writeln(" }");
buffer.writeln("}");
File('ets/components/${name}.ets')
.writeAsStringSync(buffer.toString());
}
输出:ets/components/PrimaryButton.ets
步骤 4:在双端项目中使用
Flutter 端使用
dart
Column(
children: [
const PrimaryButton(
text: "提交订单",
onClick: () => print("Clicked!"),
),
const SizedBox(height: 16),
const PrimaryButton(
text: "继续浏览",
onClick: () => navigateNext(),
)
],
)
OpenHarmony 端使用(ArkTS)
ets
Column({ space: 10 }) {
PrimaryButton({
text: '提交订单',
onPress: () => console.info('Clicked!')
})
PrimaryButton({
text: '继续浏览',
onPress: () => router.pushUrl(...)
})
}
.width('100%').padding(20)
四、进阶优化建议
| 功能 | 实现方式 |
|---|---|
| 主题支持 | 在 JSON 中添加 theme.light / theme.dark |
| 响应式布局 | 添加 breakpoints 字段,生成不同尺寸规则 |
| 图标集成 | 支持 SVG → Dart Path / ArkTS VectorDrawable 转换 |
| CI/CD 自动化 | Git Hook 触发生成,PR 预览对比 |
| 文档站点 | 使用 Docusaurus 展示所有组件 Demo |
五、结语:组件即协议
当我们把 UI 抽象为“可描述的数据”而非“具体的代码”,我们就迈出了标准化的第一步。
🌐 组件 = 协议
✅ 设计师输出协议
✅ 工程师实现两端解析器
✅ 业务方只关心“怎么用”
这正是未来跨平台开发的理想形态。
在 Flutter 与 OpenHarmony 尚未完全融合的今天,统一组件库是我们能掌控的最佳实践路径。
它不要求你精通两个框架的所有底层细节,只需要:
- 建立标准
- 编写工具
- 持续迭代
💬 如果你也正在做类似的事情,欢迎加入我们的开源项目:
GitHub: https://github.com/ziyu-tech/unified-component-system
一起打造属于中国开发者的跨平台 Design System!
参考资料
- Figma Developer API:https://www.figma.com/developers/api
- ArkTS 官方文档:https://developer.harmonyos.com/cn/docs/documentation/doc-references-v5
- Flutter Widgets Catalog:https://docs.flutter.dev/ui/widgets
- 示例代码仓库:https://github.com/ziyu-tech/cross-platform-components
❤️ 欢迎交流
你在团队中是如何管理多端 UI 一致性的?有没有遇到什么坑?
欢迎在评论区留言分享你的经验!
📌 关注我 @子榆,我会持续输出 Flutter × OpenHarmony 工程化最佳实践,包括:
- 状态管理统一方案(Riverpod + ArkState)
- 日志与埋点抽象层
- 自动化测试框架
让我们共同推动国产生态的高质量发展!
版权声明:本文原创,转载请注明出处及作者。商业转载请联系授权。
作者主页:https://blog.csdn.net/ziyu
✅ 点赞 + 收藏 + 转发,让更多人告别“重复造轮子”的痛苦!
📌 标签:#Flutter #OpenHarmony #ArkTS #跨平台组件 #DesignSystem #Figma #前端架构 #UI一致性 #代码生成 #子榆 #CSDN #2025工程实践
https://openharmonycrossplatform.csdn.net/content
更多推荐



所有评论(0)