写 Flutter 不用再套六层 Widget 了

如果 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 的方式也在跟着进化。

最好的理解方式不是看文章。是动手搭一个。

相关推荐
小羊没烦恼!2 小时前
关于大型asp.net应用系统的架构-架构的选择
java·服务器·开发语言·前端·c#
GISer_Jing2 小时前
height、line-height、font-size相同字体遮挡问题
前端·ai
字节渡客2 小时前
接口参数偶尔丢失、乱码、解析失败,不是前端传参问题
前端
夏幻灵2 小时前
前端八股:JavaScript 深拷贝详解:实现方式、递归原理与循环引用
开发语言·前端·javascript
@#¥&~是乱码鱼啦2 小时前
ArkWeb开发手记01|Web 组件 H5 加载与生命周期管理
前端
suaizai_2 小时前
ComputerUse:让AI真正操控桌面系统
java·前端·javascript
CodeSheep2 小时前
朋友面试谈薪报价2w,HR非得压到1.9,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
前端·后端·程序员
IMPYLH3 小时前
HTML 的 <track> 元素
前端·html
涛涛ing3 小时前
35个组件,把GPU特效直接跑在真实DOM上:Canvas UI正在重新定义Web视觉
前端