构建统一组件库: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!


参考资料


❤️ 欢迎交流

你在团队中是如何管理多端 UI 一致性的?有没有遇到什么坑?
欢迎在评论区留言分享你的经验!

📌 关注我 @子榆,我会持续输出 Flutter × OpenHarmony 工程化最佳实践,包括:

  • 状态管理统一方案(Riverpod + ArkState)
  • 日志与埋点抽象层
  • 自动化测试框架

让我们共同推动国产生态的高质量发展!


版权声明:本文原创,转载请注明出处及作者。商业转载请联系授权。
作者主页https://blog.csdn.net/ziyu

点赞 + 收藏 + 转发,让更多人告别“重复造轮子”的痛苦!


📌 标签:#Flutter #OpenHarmony #ArkTS #跨平台组件 #DesignSystem #Figma #前端架构 #UI一致性 #代码生成 #子榆 #CSDN #2025工程实践

https://openharmonycrossplatform.csdn.net/content

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐