
如果 Flutter UI 也能像 Tailwind CSS 一样,通过工具类来设置样式,会是什么体验?本文将通过实际示例,带你探索
fluttersdk_wind的使用方法,了解如何利用它实现响应式设计、深色模式、不同交互状态的样式,以及跨平台应用开发。
如果你搞过前端,你一定听说过Tailwind CSS,特别是在这个AI时代,不过我第一次知道Tailwind CSS是因为AI的冲击差点黄铺子了,但也正是因为AI的盛行,大家更不愿意Tailwind CSS死掉,因为Tailwind CSS对AI极其友好。说了这么多,我想说的是,如果有一个类似Tailwind的Flutter库,也就是说Tailwind CSS for Flutter,你会愿意尝试吗?
一谈到flutter,很多人会想到的第一个问题,不是状态管理,不是路由配置,是嵌套。
你想要一个圆角卡片,带阴影,有 hover 效果,响应式适配平板。打开编辑器,开始码。
Container 套 padding,padding 套 margin,margin 套 decoration,decoration 套 BoxDecoration,BoxDecoration 里塞 borderRadius 和 boxShadow,最后 child 里再放一个 Text 配 TextStyle。
六层嵌套,就为了一个卡片。
Flutter 团队自己都承认这个问题,在 GitHub 上专门开了个 issue 讨论 verbosity。issue 编号 #161345,感兴趣的可以去翻翻。
Wind 做的事情很简单,把 Tailwind 的 utility-first 语法搬到了 Flutter。你用 className 字符串写样式,它在底层帮你转成对应的 Widget 树。
如果你写过 Web,用过 Tailwind,你已经会 Wind 了。
先看效果。传统写法,一个圆角卡片带阴影和文字:
dart
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Text(
'Total Expenses',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
)
Wind 写法,同样的效果:
dart
WDiv(
className: 'p-6 bg-white rounded-xl shadow-lg',
child: WText(
'Total Expenses',
className: 'text-2xl font-bold',
),
)
同一个效果,一个字符串搞定。
没有 MouseRegion,没有 setState,没有 _isHovered 布尔值,没有 MediaQuery 分支判断。暗模式、响应式、hover 状态,全部写在那一个 className 字符串里。
Wind 不只是简单地把 Tailwind 搬过来。它是一个完整的样式引擎,不是 Widget 目录。你可以把它和你现有的组件库配合使用,它只管样式层。
能力清单
我扒了一下它的能力清单,几个关键数据:
27 个 W 前缀 Widget。WDiv、WText、WButton、WInput、WSelect、WPopover、WDatePicker、WCard、WTabs、WBadge、WSwitch、WRadio、WDynamic,还有五个 FormField 包装器,可以直接塞进 Flutter 的 Form 里。
20 个 token 解析器。flex、p-4、bg-blue-500、rounded-lg、shadow-md,这些类名在 Web 项目和 Flutter 项目之间可以直接复制粘贴,不用改。
全平台支持。Android、iOS、Web、Windows、macOS、Linux,一套代码全覆盖。
最有意思的是前缀系统可以叠加。这不是简单的 dark: 和 hover: 二选一,而是所有前缀自由组合:
dart
// 暗模式背景
dark:bg-gray-800
// 平板及以上横向排列
md:flex-row
// hover 时放大阴影
hover:shadow-xl
// iOS 平台圆角更大
ios:rounded-2xl
// 组合:平板 + hover + 变蓝
md:hover:bg-blue-600
前缀的顺序是固定的:state: > breakpoint: > dark: > platform: > utility。冲突时最后一个赢,比如 p-2 p-6 最终是 padding 6。
有一个规则我觉得特别好:每个颜色 token 必须带 dark: 配对。写 bg-white 不带 dark:bg-gray-800 会被当成 bug,debug 模式下 parser 会提示你。不认识的 token 在 release 里静默丢弃,在 debug 里会提示,不会 throw,不会红屏。
间距系统也很直觉。一步等于 4 个逻辑像素,所以 p-4 就是 16px,和 Tailwind 完全一致。字号和字间距也是像素直接对应。
别光说,来点真的
光看 Hello World 没意思。我们来搭一个 Expense Dashboard,展示 Wind 在真实场景里怎么用。
这个 Dashboard 包含:月收入、月支出、剩余余额、消费分类列表,还要响应式适配和暗模式。
第一步:做一个可复用的卡片组件
与其到处复制粘贴 WDiv,不如先做一个可复用的 Widget:
dart
class SummaryCard extends StatelessWidget {
const SummaryCard({
super.key,
required this.title,
required this.amount,
required this.icon,
required this.valueClass,
});
final String title;
final String amount;
final IconData icon;
final String valueClass;
@override
Widget build(BuildContext context) {
return WDiv(
className: '''
flex-1
p-5
bg-white
dark:bg-gray-800
rounded-xl
shadow-md
''',
children: [
WDiv(
className: 'flex flex-row items-center gap-3',
children: [
WDiv(
className: '''
p-3
bg-gray-100
dark:bg-gray-700
rounded-lg
''',
child: Icon(icon),
),
WText(
title,
className: '''
text-sm
text-gray-500
dark:text-gray-400
''',
),
],
),
const SizedBox(height: 16),
WText(
amount,
className: '''
text-2xl
font-bold
$valueClass
''',
),
],
);
}
}
这里有一个很重要的工程原则:不要把 utility class 当成组件架构的替代品。utility 管样式,Widget 管可复用的 UI 行为。
第二步:搭 Dashboard
dart
class ExpenseDashboard extends StatelessWidget {
const ExpenseDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: WDiv(
className: '''
flex flex-col
gap-6
p-4
bg-gray-100
dark:bg-gray-900
md:p-6
''',
children: [
WText(
'Expense Dashboard',
className: '''
text-2xl
font-bold
text-gray-900
dark:text-white
md:text-3xl
''',
),
WDiv(
className: '''
flex flex-col
gap-4
md:flex-row
''',
children: [
const SummaryCard(
title: 'Income',
amount: '₹50,000',
icon: Icons.account_balance_wallet,
valueClass: 'text-green-600',
),
const SummaryCard(
title: 'Expenses',
amount: '₹24,850',
icon: Icons.receipt_long,
valueClass: 'text-red-500',
),
const SummaryCard(
title: 'Balance',
amount: '₹25,150',
icon: Icons.savings,
valueClass: 'text-blue-600',
),
],
),
],
),
),
);
}
}
注意看这一行:
css
flex flex-col md:flex-row
默认竖向排列,中等屏幕以上变成横向。不用 MediaQuery 判断屏幕宽度,不用 if-else 分支,一个前缀搞定。
暗模式也不用额外写条件
bg-white dark:bg-gray-800
暗模式和亮模式的样式写在一起,不需要单独写条件逻辑。改个 JSON,App 里的 UI 就变了。
加一个消费分类列表
再做一个可复用的消费条目组件:
dart
class ExpenseItem extends StatelessWidget {
const ExpenseItem({
super.key,
required this.title,
required this.amount,
required this.icon,
});
final String title;
final String amount;
final IconData icon;
@override
Widget build(BuildContext context) {
return WDiv(
className: '''
flex flex-row
items-center
gap-4
p-4
bg-white
dark:bg-gray-800
rounded-xl
''',
children: [
WDiv(
className: '''
p-3
bg-gray-100
dark:bg-gray-700
rounded-lg
''',
child: Icon(icon),
),
WDiv(
className: 'flex-1',
children: [
WText(
title,
className: '''
font-medium
text-gray-900
dark:text-white
''',
),
WText(
'September 2026',
className: '''
text-sm
text-gray-500
dark:text-gray-400
''',
),
],
),
WText(
amount,
className: 'font-bold text-red-500',
),
],
);
}
}
用的时候一行搞定:
dart
const ExpenseItem(
title: 'Food',
amount: '₹4,250',
icon: Icons.restaurant,
)
比到处复制一大坨样式树好维护太多了。
状态和平台前缀
Wind 也支持状态前缀:hover:、focus:、disabled:、loading:、selected:。
dart
WButton(
className: '''
px-5
py-3
rounded-lg
bg-blue-600
hover:bg-blue-700
''',
onTap: () {
debugPrint('Add expense');
},
child: const WText(
'Add Expense',
className: 'text-white font-medium',
),
)
对同时跑桌面和 Web 的 Flutter 应用来说,hover 和 focus 状态很重要,Wind 这块做得很顺手。
平台前缀也很有用。同一个组件在不同平台有细微差别时,不用到处写平台判断:
dart
WDiv(
className: '''
p-4
rounded-xl
ios:rounded-2xl
android:rounded-lg
''',
child: ...,
)
平台特定的样式紧贴着组件,不会散落到 Widget 树各处。
Wind vs 传统 Flutter
两种方式都成立,不是谁替代谁。
| 场景 | 传统 Flutter | Wind |
|---|---|---|
| Padding | padding: EdgeInsets.all(16) |
p-4 |
| 背景色 | Container / decoration |
WDiv + utilities |
| 文字样式 | TextStyle(...) |
text-*, font-* |
| 圆角 | BorderRadius.circular(12) |
rounded-* |
| 阴影 | BoxShadow(...) |
shadow-* |
| 响应式 | MediaQuery 条件判断 |
响应式前缀 md: |
| 暗模式 | 条件判断 + 切换 | dark: 前缀 |
| 平台差异 | Platform.isIOS 检查 |
平台前缀 ios: |
| hover 状态 | MouseRegion + setState |
状态前缀 hover: |
Wind 是在 Flutter Widget 系统之上加了一层样式抽象,不是替代它。
性能:别想当然
这点要提前说清楚。utility-first 语法不会自动让 App 变快。
Wind 文档说了解析过的 className 会被缓存,key 是 className + breakpoint + brightness + platform + states 的组合。这很有用,因为同一个 utility 字符串会反复出现。
但 App 性能取决于太多因素了:Widget rebuild、状态管理、布局复杂度、图片、网络、动画、列表渲染、业务逻辑......
不要因为觉得 Wind 能提升 FPS 而选它。要因为它的样式模型能改善你的开发和维护效率而选它。
大段 className 怎么管
utility-first 有一个真实痛点:class 字符串可能变得很长。
短一点的还好:
css
flex flex-col gap-4 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-lg md:flex-row
但如果一直往上加:
css
flex flex-col gap-4 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-lg md:flex-row lg:p-8 xl:max-w-6xl ...
到一定程度,可读性就差了。
解决方案不是停止用 utilities,而是提取可复用组件:
dart
class AppCard extends StatelessWidget {
const AppCard({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return WDiv(
className: '''
p-5
bg-white
dark:bg-gray-800
rounded-xl
shadow-md
''',
child: child,
);
}
}
然后:
dart
AppCard(
child: WText(
'Total Expenses',
className: 'text-xl font-bold',
),
)
干净的分层:
Widget
↓
Component
↓
Wind utilities
另一个要点:别把业务逻辑混进 className。
dart
// 不推荐
WDiv(
className: isPremiumUser
? 'bg-blue-600 p-6 rounded-xl'
: 'bg-gray-600 p-6 rounded-xl',
)
// 推荐:业务逻辑在样式层之外
final cardClass = isPremiumUser
? 'bg-blue-600'
: 'bg-gray-600';
或者更好,直接做成一个组件:
dart
PremiumCard(
isPremium: isPremiumUser,
)
组件内部决定用哪些 Wind utilities。这样项目大了也不会乱。
什么时候用 Wind
适合的场景:
- 团队已经在用 Tailwind CSS
- 在做响应式 Flutter 应用
- 目标平台包含 Web 或桌面
- 有大量重复样式
- 想要 utility-first 的开发体验
- 想让样式变体紧贴 Widget
需要想清楚的场景:
- 团队习惯传统 Flutter Widget 写法
- 应用已经有成熟的设计系统
- 开发者不太适应 utility-class 语法
- 大量三方包有自己的样式抽象
- 项目有严格的 UI 架构规范
关键是要根据自己的项目需求来评估,别因为语法看着眼熟就直接上。
Wind 对 AI 编程的友好程度
Wind 不只是给人写的,还是给 AI 写的。
想象一下 AI 生成代码的场景。传统方式下,AI 要生成一大坨:
dart
Container(
padding: ...,
decoration: ...,
child: ...
)
换成 Wind,AI 只需要生成一个紧凑的 utility 字符串:
css
p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md
自带了一个 Wind UI Skill,教 AI 编程助手懂 className 语法、暗模式配对规则和反模式。一行命令装上:
bash
npx skills add fluttersdk/ai --skill wind-ui
支持 Claude Code、Cursor、OpenCode、Gemini CLI、VS Code Copilot、Codex CLI、Cline、Roo Code。
更狠的是还有一个 MCP 服务器挂在 mcp.fluttersdk.com,暴露了一个 search-docs 工具。AI 写代码的时候可以实时查文档,不会瞎编 className。仓库里还维护了一个 llms.txt,给 AI 提供机器可读的包清单。
三件套配齐了:
AI
↓
Utility classes
↓
Wind
↓
Flutter widgets
↓
Application UI
Skill 教语法,MCP 查文档,llms.txt 给全局认知。AI 写 Wind 代码比写原始 Flutter 快太多。
上手三步
dart
// 1. 安装
flutter pub add fluttersdk_wind
// 2. 包一层 WindTheme
import 'package:flutter/material.dart';
import 'package:fluttersdk_wind/fluttersdk_wind.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return WindTheme(
data: WindThemeData(),
builder: (context, controller) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: controller.toThemeData(),
home: const ExpenseDashboard(),
);
},
);
}
}
// 3. 用 className 写样式
WDiv(
className: '''
flex flex-col
gap-4
p-6
bg-white
dark:bg-gray-800
rounded-xl
shadow-lg
md:flex-row
hover:shadow-xl
''',
children: [
WText('Wind', className: 'text-xl font-bold'),
WButton(
className: 'px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600',
onTap: () {},
child: const WText('Get started'),
),
],
)
WindTheme 是关键,它给 Wind 提供了解析 utility class 所需的主题上下文。
要求 Flutter 3.27.0 和 Dart 3.4.0 以上。运行时只依赖 flutter_svg 和 diagnostics contracts 包,很轻量。MIT 协议,1.0 开始就稳定了,现在版本 1.6.1。
官网上还有个在线 Playground,可以直接在浏览器里写 Wind 代码看效果,不用先建项目。
生产环境的几条建议
如果你决定在真实项目里用 Wind,我推荐这几条实践。
1. 做可复用组件。 别到处重复长 className 字符串。AppCard、SummaryCard、ExpenseItem、AppButton,常用的抽出来。
2. 建立设计词汇表。 定好项目常用的 utilities:rounded-xl、p-4、p-6、gap-4、text-sm、text-xl、font-bold......一致性比用满所有可用 utilities 重要。
3. 业务逻辑和样式分离。 你的 Provider、Repository、Service 不应该知道 Wind 的存在。
4. 测试响应式布局。 尤其是跨 Android、iOS、Web、桌面全平台的时候。
5. 不要写巨型 className 字符串。 如果一段样式难以理解,就提取成组件。
最终的架构应该是这样的:
markdown
Presentation
│
├── Screens
│
├── Components
│ ├── AppCard
│ ├── SummaryCard
│ ├── ExpenseItem
│ └── AppButton
│
└── Wind styling
↓
Application
↓
Domain
↓
Data
Wind 管样式。Widget 管 UI 组合。状态管理层管状态。Domain 层管业务规则。Repository 管数据。
这个分层比你的 UI 用 utility class 还是传统 Flutter 样式重要得多。
最后说两句
Wind 带来的真正价值不是少写几行代码。
是一个一致的样式语言。
你可以这样写:
dart
WDiv(
className: '''
flex flex-col
gap-4
p-6
bg-white
dark:bg-gray-800
rounded-xl
shadow-lg
md:flex-row
''',
children: [
...
],
)
在一处同时表达布局、间距、颜色、响应式行为和暗模式。
如果你已经熟悉 Tailwind,Wind 值得试一试。如果你是传统 Flutter 开发者,先在一个小功能上试试,别急着全项目铺开。
搭一个 Dashboard。搭一个响应式页面。试试暗模式。试试桌面/Web 的 hover 状态。
然后决定 utility-first 这条路适不适合你的团队。
一个字符串干掉六层嵌套,这个买卖不亏。但真正值钱的是,从此你有了统一的样式语言。
Flutter 在进化,我们构建 Flutter UI 的方式也在跟着进化。
最好的理解方式不是看文章。是动手搭一个。