瑞幸 UI 上 pub.dev 了 —— 22 个 Flutter 组件,与微信小程序版双端对齐

瑞幸 UI 上 pub.dev 了 ------ 22 个 Flutter 组件,与微信小程序版双端对齐

DESIGN.md 当作跨端的"单一真相",一套设计语言同时喂给 WeChat 小程序和 Flutter。

效果截图

背景

之前我写了《我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库》,发布了 npm 包 lkcn-ui

验证 DESIGN.md 真的是"可复用的设计规范"吗,那它至少应该能驱动两个不同的运行时。于是有了这一版:

  • GitHubhttps://github.com/qwfy5287/lkcn-ui-flutter
  • pub.devhttps://pub.dev/packages/lkcn_ui
  • 姊妹项目https://github.com/qwfy5287/lkcn-ui(小程序版)

双端对照

两个仓库,一份 DESIGN.md,相同的 22 个组件:

平台 包名 分发 仓库
微信小程序 lkcn-ui npm qwfy5287/lkcn-ui
Flutter lkcn_ui pub.dev qwfy5287/lkcn-ui-flutter

命名这里踩了个小坑:pub.dev 要求 snake_case,不能用连字符,所以 npm 的 lkcn-ui 到 pub.dev 就成了 lkcn_ui。这是 Dart/Flutter 生态的惯例,不算破坏品牌一致性。

版本号策略是 MAJOR.MINOR 对齐 + PATCH 独立 ------看到 npm 1.2.3 + pub 1.2.1 就知道 API 对齐、只是 Flutter 单独修了两个 bug。

设计语言的「跨端翻译」

如果说小程序版是把 DESIGN.md 翻译成 WXSS + WXML,那 Flutter 版就是翻译成 Dart Widget。这过程有 5 件事需要做决定:

1. Design Token:CSS 变量 → Dart const class

小程序版把 token 写成 CSS 变量,注入到 page {}

css 复制代码
page {
  --lkcn-blue: #1A6EFF;
  --lkcn-radius-md: 24rpx;
}

Flutter 没有 CSS 变量这种运行时机制,但它的类型系统更强。我用 const class 做等价物:

dart 复制代码
class LkcnColors {
  static const Color primary = Color(0xFF002FA7);      // 克莱因蓝
  static const Color accentOrange = Color(0xFFFF6A3D);
  static const Color accentGold = Color(0xFFC9A66B);
}

class LkcnRadius {
  static const double md = 12;
  static const double pill = 999;
}

使用:

dart 复制代码
Container(
  decoration: BoxDecoration(
    color: LkcnColors.primary,
    borderRadius: BorderRadius.circular(LkcnRadius.md),
  ),
)

好处是编译期常量、IDE 自动补全、类型安全;坏处是换肤没办法像 CSS 变量那样"覆盖即生效"------要彻底换肤得上 ThemeExtension。首版先不折腾这个。

2. 单位:rpx → logical pixels

小程序的 rpx 基于 750 设计稿,Flutter 的 logical pixel 是独立密度单位。换算规则就一条:rpx = lpt × 2

字号 28rpx 对应 14 lpt,间距 24rpx 对应 12 lpt,圆角 16rpx 对应 8 lpt。习惯了之后是肌肉记忆,但第一次做映射表时你会翻 variables.wxss 翻到吐。

3. 组件 API:kebab-case → PascalCase / enum

  • 组件类:lkcn-buttonLkcnButton
  • 枚举属性:type="primary"LkcnButtonType.primary
  • 事件回调:bind:tap="onClick"onTap: () {}

Flutter 的 enum 比字符串属性严格得多------如果你传了个不存在的 type 字符串,小程序只会默默 fallback,Flutter 直接编译不过。对库作者是好事。

4. 插槽:<slot> → Widget 参数

小程序靠 <slot> 传子内容,支持具名插槽。Flutter 对应的是具名参数:

dart 复制代码
LkcnCard(
  title: '我的资产',
  child: Column(children: [...]),   // 主内容
  footer: Row(...),                  // footer 槽
)

一个命名参数 = 一个插槽,清晰、类型安全、IDE 能提示。

5. Demo 的组织:pages/demo-*example/lib/demos/*

小程序版每个 demo 是独立 page(wxml/wxss/js/json 四件套),通过 pages.json 注册。Flutter 版按 pub.dev 惯例,example/ 是个独立的可运行 app,每个组件对应一个 .dart 文件,用 MaterialPageRoute 跳转:

bash 复制代码
example/
├── lib/
│   ├── main.dart              # 按 原子/交互/容器/业务 分组的索引页
│   └── demos/
│       ├── button_demo.dart
│       ├── product_card_demo.dart
│       └── ... (21 个)
└── pubspec.yaml               # path: ../ 引用主包

cd example && flutter run 就能跑,iOS / Android / macOS / Web 四端都能看。这比小程序的"打开微信开发者工具"门槛低多了。

几个还原得比较得意的组件

LkcnStepper:加购从 + 展开到 [-] n [+]

瑞幸菜单页最有辨识度的微交互,Flutter 版用 setState 切两个形态:

dart 复制代码
LkcnStepper(
  value: _quantity,
  onChanged: (v) => setState(() => _quantity = v),
)

弹性动画走 LkcnMotion.bounce(即 Cubic(0.34, 1.56, 0.64, 1)),跟 WXSS cubic-bezier 常量完全一致。

LkcnPrice:三段式价格渲染

"符号小 + 整数大 + 小数小"的层次是瑞幸价格的灵魂:

dart 复制代码
LkcnPrice(value: 9.9, original: 32, prefix: '预估到手')

内部把 9.9 拆成 9.9 两段不同字号,¥ 给第三种字号,原价走 TextDecoration.lineThrough

LkcnCouponScroll:票据左侧半圆缺口

小程序版靠 CSS clip-path 裁出缺口,Flutter 没有这个 API。我用 CustomPainter 手画 path:

dart 复制代码
final path = Path()
  ..moveTo(r, 0)
  ..lineTo(size.width - r, 0)
  // ...
  ..lineTo(0, size.height * 0.5 + 6)
  ..arcToPoint(                          // ← 半圆缺口
    Offset(0, size.height * 0.5 - 6),
    radius: const Radius.circular(6),
    clockwise: false,
  )
  ..close();

最终效果和小程序版几乎一致。CustomPainter 写起来比 CSS clip-path 啰嗦,但控制粒度更细。

LkcnMembershipPlan:会员订阅全流程

方案选择器 + 订阅 CTA + 协议勾选,三件事一个 Widget 解决:

dart 复制代码
LkcnMembershipPlan(
  plans: const [
    LkcnPlan(name: '连续包月', price: 9.9, badge: '爆款天天 9.9 起'),
    LkcnPlan(name: '月卡', price: 19.9),
  ],
  agreement: '开通会员代表接受',
  agreementLinks: const [
    LkcnAgreementLink(text: '《会员服务协议》'),
    LkcnAgreementLink(text: '《自动续费协议》'),
  ],
  onSubscribe: (plan, agreed) {
    // agreed = false 时可以弹 toast 提示勾选
  },
)

快速上手

pubspec.yaml

yaml 复制代码
dependencies:
  lkcn_ui: ^0.1.0

业务代码:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:lkcn_ui/lkcn_ui.dart';

class MenuPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: LkcnColors.pageBg,
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          LkcnProductCard(
            image: 'https://.../coconut-latte.png',
            title: '生椰拿铁',
            tags: const ['全球销量第一', 'IIAC 金奖'],
            price: 9.9,
            originalPrice: 32,
            pricePrefix: '预估到手',
            onAdd: () {},
          ),
          const SizedBox(height: 16),
          LkcnButton.cta(
            text: '立即开通连续包月 ¥9.9',
            size: LkcnButtonSize.large,
            block: true,
            round: true,
            onTap: () {},
          ),
        ],
      ),
    );
  }
}

22 个组件速览

  • 原子:Button · Tag · Price · Badge · Avatar
  • 交互:SearchBar · Segment · Stepper · Tabs · Tabbar
  • 容器:Card · Grid · Swiper · NoticeBar · LocationBar · FloatingButton · CategorySidebar
  • 业务:ProductCard · CouponScroll · PromoCard · LevelCard · MembershipPlan

每个的 API 尽量跟 npm 版同名、同语义。小程序那边的 bind:add 事件在 Flutter 是 onAdd,小程序的 custom-class 在 Flutter 通过 child/padding 参数调------这些映射关系看完一遍 README 就能对上号。

一些数据

  • 22 个组件,零第三方依赖(只依赖 Flutter SDK)
  • 约 3000 行 Dart 代码(不含 example)
  • flutter analyze / example && flutter analyze0 警告 0 错误
  • lib/ 目录 25 个 .dart 文件
  • Dart SDK:^3.11.3,Flutter:>=3.22.0
  • MIT License

跨端维护的几条经验

做完这版 Flutter 之后,最深的感受是:跨端组件库的真正难点不在代码,在保持纪律。

  1. DESIGN.md 做单一真相:色值 / 间距 / 圆角这些决策写在文档里,而不是写在某一端代码的注释里。PR 有分歧时,以文档为准。
  2. MAJOR.MINOR 对齐 + PATCH 独立:两端版本号不强求完全一致,但 API 变更要同步发版。
  3. Issue 加端标签[wx] / [flutter] / [design] 三类,避免跨端 issue 混战。
  4. demo 先行:改组件前先改 demo,再改源码 ------ 这样能强制你想清楚 API 长什么样。

后续计划

  • 每个组件写 widget test,提 pub.dev Like / Popularity 评分
  • ThemeExtension 版的 Design Token,支持运行时换肤
  • 深色模式
  • GitHub Actions CI:analyze + test + 自动 pub publish
  • VitePress 双端文档站(两端 API 并排展示)

觉得有用的话,欢迎 Star / 试用:

  • GitHub:https://github.com/qwfy5287/lkcn-ui-flutter
  • pub.dev:https://pub.dev/packages/lkcn_ui
  • 小程序版姊妹项目:https://github.com/qwfy5287/lkcn-ui

🧑‍💻 顺便求职

目前正在找工作,前端优先,全栈也可以胜任 ,坐标 厦门

案例集(前端 / 全栈):my.feishu.cn/wiki/XUmGw8...

有合适岗位欢迎评论或私信,感谢。

相关推荐
liulian09164 小时前
【Flutter for OpenHarmony】原生卡片 Widget 集成实战:从零构建待办清单桌面组件
flutter·华为·学习方法·harmonyos
2601_949593654 小时前
Flutter OpenHarmony 三方库 video_player 视频播放器适配详解
flutter·音视频
liulian09165 小时前
Flutter 三方库 connectivity_plus 的鸿蒙化适配与网络状态管理实战
网络·flutter·华为·学习方法·harmonyos
摘星编程6 小时前
当所有人都在堆参数的时候,DeepSeek V4悄悄做了三件别人不敢做的事
开源
十六年开源服务商6 小时前
2026 WordPress社区参与计划深度解析
开源
MonkeyKing6 小时前
InheritedWidget 原理与性能
flutter
冬奇Lab6 小时前
一天一个开源项目(第77篇):MoneyPrinterV2 —— 全自动短视频生产与流量变现的开源‘印钞机
人工智能·开源·资讯
liulian09166 小时前
【Flutter For OpenHarmony】Flutter 三方库 flutter_secure_storage 的鸿蒙化适配指南
flutter·华为·学习方法·harmonyos
darkb1rd7 小时前
RedSun:Defender 逻辑漏洞深度解析与指南
开源·github·好物分享