Skip to content

脱离 Material:定制自己的风格

Flutter 默认使用 Material Design 风格,但你有完全的自由去创造任何视觉风格。本文将带你从「离开 Material」到「搭建自己的设计体系」,一步步实现。

核心思路

Flutter 的 UI 由 Widget 树构成,Widget 只是描述"长什么样"的配置。Material 组件(ElevatedButtonCardAppBar...)本质上也只是 Widget 的封装。脱离 Material 风格,只需要:

  1. 不使用 Material 组件,改用基础组件自己组合
  2. 或者换一套设计体系(Cupertino / 自定义组件库)
  3. 定义自己的设计令牌(Design Token),统一管理颜色、间距、圆角等

三种路线对比

路线适合场景难度工作量
深度定制 ThemeData想保留 Material 组件,但改外观★☆☆
使用 CupertinoAppiOS 风格即可★★☆
完全自建设计体系独特品牌风格、无任何框架痕迹★★★

路线一:深度定制 ThemeData(保留 Material 组件)

如果你只是不喜欢 Material 的默认配色和圆角,但还想用 ElevatedButtonCard 等组件,可以通过 ThemeData 改到"面目全非":

dart
MaterialApp(
  theme: ThemeData(
    useMaterial3: true,

    // 1. 用自定义颜色替换整个色彩方案
    colorScheme: const ColorScheme(
      brightness: Brightness.light,
      primary: Color(0xFFFF6B35),        // 橙色品牌主色
      onPrimary: Colors.white,
      primaryContainer: Color(0xFFFFDCC8),
      onPrimaryContainer: Color(0xFF8B3E15),
      secondary: Color(0xFF2EC4B6),      // 青色辅助色
      onSecondary: Colors.white,
      surface: Color(0xFFFAFAFA),
      onSurface: Color(0xFF1A1A2E),
      error: Color(0xFFE63946),
      onError: Colors.white,
      outline: Color(0xFFE0E0E0),
    ),

    // 2. 全局圆角改大 → 更柔和的风格
    cardTheme: CardThemeData(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      elevation: 0,
      color: Color(0xFFF5F5F5),
    ),

    // 3. 按钮改成圆角胶囊形
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        shape: const StadiumBorder(),
        padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
      ),
    ),

    // 4. 输入框去掉边框 → 更现代的底线式
    inputDecorationTheme: const InputDecorationTheme(
      border: UnderlineInputBorder(),
      focusedBorder: UnderlineInputBorder(
        borderSide: BorderSide(color: Color(0xFFFF6B35), width: 2),
      ),
    ),

    // 5. 全局字体
    textTheme: const TextTheme(
      headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w800),
      bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
    ),
  ),
)

效果

同一套 Material 组件,通过 ThemeData 深度定制后可以呈现出与默认风格完全不同的视觉效果。这是成本最低的路线。

路线二:使用 CupertinoApp(iOS 风格)

Flutter 内置了 Cupertino 组件库,实现了 iOS 风格的 UI。如果你喜欢 iOS 风格,这是最简单的选择。

基本用法

dart
// 使用 CupertinoApp 替代 MaterialApp
CupertinoApp(
  theme: const CupertinoThemeData(
    primaryColor: Color(0xFFFF6B35),     // 主色(影响按钮、开关等)
    brightness: Brightness.light,
    scaffoldBackgroundColor: Color(0xFFF2F2F7), // iOS 系统灰背景
  ),
  home: const HomePage(),
)

常用 Cupertino 组件对照

Material 组件Cupertino 替代说明
ScaffoldCupertinoPageScaffold页面脚手架
AppBarCupertinoNavigationBar导航栏
ElevatedButtonCupertinoButton按钮
SwitchCupertinoSwitch开关
SliderCupertinoSlider滑块
TextFieldCupertinoTextField输入框
AlertDialogCupertinoAlertDialog对话框
BottomNavigationBarCupertinoTabBar底部标签栏
ActivityIndicatorCupertinoActivityIndicator加载指示器

示例页面

dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('我的应用'),
      ),
      child: SafeArea(
        child: Center(
          child: CupertinoButton.filled(
            onPressed: () {},
            child: const Text('iOS 风格按钮'),
          ),
        ),
      ),
    );
  }
}

限制

Cupertino 组件库没有 Material 那么丰富。对于缺少的组件(如 CardChip),你需要自己用基础组件组合,或者混用少量 Material 组件。

混用 Material 和 Cupertino

你可以在 CupertinoApp 中使用部分 Material 组件,只需在需要的地方包裹 Material

dart
// 在 CupertinoApp 中使用 Material 的 Card
Material(
  child: Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Text('混合使用'),
    ),
  ),
)

路线三:完全自建设计体系

这是最灵活的路线——不依赖任何设计框架,用 Flutter 的基础组件组合出完全属于自己的风格。

第一步:定义设计令牌(Design Token)

设计令牌是设计体系的"原子",所有视觉属性从这里取值:

dart
/// 应用设计令牌 —— 所有视觉属性的唯一定义源
class AppDesign {
  // ---- 颜色 ----
  static const Color primary = Color(0xFFFF6B35);
  static const Color secondary = Color(0xFF2EC4B6);
  static const Color background = Color(0xFFFAFAFA);
  static const Color surface = Color(0xFFFFFFFF);
  static const Color textPrimary = Color(0xFF1A1A2E);
  static const Color textSecondary = Color(0xFF6B7280);
  static const Color error = Color(0xFFE63946);
  static const Color divider = Color(0xFFE5E7EB);

  // ---- 间距 ----
  static const double spacing4 = 4;
  static const double spacing8 = 8;
  static const double spacing12 = 12;
  static const double spacing16 = 16;
  static const double spacing24 = 24;
  static const double spacing32 = 32;
  static const double spacing48 = 48;

  // ---- 圆角 ----
  static const double radiusSmall = 6;
  static const double radiusMedium = 12;
  static const double radiusLarge = 20;
  static const double radiusFull = 9999; // 胶囊形

  // ---- 字号 ----
  static const double fontSizeSmall = 12;
  static const double fontSizeBody = 14;
  static const double fontSizeTitle = 18;
  static const double fontSizeHeadline = 24;
  static const double fontSizeDisplay = 32;

  // ---- 阴影 ----
  static List<BoxShadow> shadowSmall = [
    const BoxShadow(
      color: Color(0x0A000000),
      blurRadius: 4,
      offset: Offset(0, 2),
    ),
  ];
  static List<BoxShadow> shadowMedium = [
    const BoxShadow(
      color: Color(0x1A000000),
      blurRadius: 8,
      offset: Offset(0, 4),
    ),
  ];

  // ---- 动画时长 ----
  static const Duration animationFast = Duration(milliseconds: 150);
  static const Duration animationNormal = Duration(milliseconds: 300);
}

第二步:基于令牌封装基础组件

ContainerGestureDetectorDecoratedBox 等基础组件,组合出自有风格的组件:

自定义按钮

dart
class AppButton extends StatelessWidget {
  const AppButton({
    super.key,
    required this.onPressed,
    required this.child,
    this.variant = AppButtonVariant.filled,
  });

  final VoidCallback onPressed;
  final Widget child;
  final AppButtonVariant variant;

  @override
  Widget build(BuildContext context) {
    final isFilled = variant == AppButtonVariant.filled;
    final isOutlined = variant == AppButtonVariant.outlined;

    return GestureDetector(
      onTap: onPressed,
      child: AnimatedContainer(
        duration: AppDesign.animationNormal,
        padding: const EdgeInsets.symmetric(
          horizontal: AppDesign.spacing24,
          vertical: AppDesign.spacing12,
        ),
        decoration: BoxDecoration(
          color: isFilled ? AppDesign.primary : Colors.transparent,
          border: isOutlined
              ? Border.all(color: AppDesign.primary, width: 1.5)
              : null,
          borderRadius: BorderRadius.circular(AppDesign.radiusMedium),
        ),
        child: DefaultTextStyle(
          style: TextStyle(
            color: isFilled ? Colors.white : AppDesign.primary,
            fontSize: AppDesign.fontSizeBody,
            fontWeight: FontWeight.w600,
          ),
          child: child,
        ),
      ),
    );
  }
}

enum AppButtonVariant { filled, outlined, text }

自定义卡片

dart
class AppCard extends StatelessWidget {
  const AppCard({super.key, required this.child, this.padding});

  final Widget child;
  final EdgeInsetsGeometry? padding;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: AppDesign.surface,
        borderRadius: BorderRadius.circular(AppDesign.radiusLarge),
        boxShadow: AppDesign.shadowSmall,
      ),
      child: Padding(
        padding: padding ??
            const EdgeInsets.all(AppDesign.spacing16),
        child: child,
      ),
    );
  }
}

自定义输入框

dart
class AppTextField extends StatelessWidget {
  const AppTextField({
    super.key,
    this.hint,
    this.controller,
    this.obscureText = false,
    this.onChanged,
  });

  final String? hint;
  final TextEditingController? controller;
  final bool obscureText;
  final ValueChanged<String>? onChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: AppDesign.background,
        borderRadius: BorderRadius.circular(AppDesign.radiusMedium),
        border: Border.all(color: AppDesign.divider),
      ),
      padding: const EdgeInsets.symmetric(
        horizontal: AppDesign.spacing16,
        vertical: AppDesign.spacing12,
      ),
      child: TextField(
        controller: controller,
        obscureText: obscureText,
        onChanged: onChanged,
        style: const TextStyle(
          fontSize: AppDesign.fontSizeBody,
          color: AppDesign.textPrimary,
        ),
        decoration: InputDecoration.collapsed(
          hintText: hint,
          hintStyle: const TextStyle(color: AppDesign.textSecondary),
        ),
      ),
    );
  }
}

第三步:搭建页面框架

不使用 Scaffold,用基础组件组合自己的页面结构:

dart
class AppScaffold extends StatelessWidget {
  const AppScaffold({
    super.key,
    this.title,
    required this.body,
    this.bottomNav,
  });

  final String? title;
  final Widget body;
  final Widget? bottomNav;

  @override
  Widget build(BuildContext context) {
    return ColoredBox(
      color: AppDesign.background,
      child: SafeArea(
        child: Column(
          children: [
            // 自定义导航栏
            if (title != null)
              Container(
                height: 56,
                padding: const EdgeInsets.symmetric(
                  horizontal: AppDesign.spacing16,
                ),
                alignment: Alignment.centerLeft,
                child: Text(
                  title!,
                  style: const TextStyle(
                    fontSize: AppDesign.fontSizeHeadline,
                    fontWeight: FontWeight.w700,
                    color: AppDesign.textPrimary,
                  ),
                ),
              ),
            // 内容区
            Expanded(child: body),
            // 底部导航
            if (bottomNav != null) bottomNav!,
          ],
        ),
      ),
    );
  }
}

第四步:组合页面

使用你自己的组件搭建完整页面——没有任何 Material 痕迹:

dart
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return AppScaffold(
      title: '个人中心',
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(AppDesign.spacing16),
        child: Column(
          children: [
            // 头像卡片
            AppCard(
              child: Row(
                children: [
                  Container(
                    width: 64,
                    height: 64,
                    decoration: BoxDecoration(
                      color: AppDesign.primary.withOpacity(0.1),
                      borderRadius: BorderRadius.circular(AppDesign.radiusFull),
                    ),
                    child: const Icon(Icons.person, color: AppDesign.primary),
                  ),
                  const SizedBox(width: AppDesign.spacing16),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        'Flutter 开发者',
                        style: TextStyle(
                          fontSize: AppDesign.fontSizeTitle,
                          fontWeight: FontWeight.w600,
                          color: AppDesign.textPrimary,
                        ),
                      ),
                      const SizedBox(height: AppDesign.spacing4),
                      Text(
                        '查看个人资料',
                        style: TextStyle(
                          fontSize: AppDesign.fontSizeSmall,
                          color: AppDesign.textSecondary,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            const SizedBox(height: AppDesign.spacing16),

            // 设置列表
            AppCard(
              padding: EdgeInsets.zero,
              child: Column(
                children: [
                  _SettingItem(icon: Icons.settings, label: '设置'),
                  _divider(),
                  _SettingItem(icon: Icons.help, label: '帮助与反馈'),
                  _divider(),
                  _SettingItem(icon: Icons.info, label: '关于'),
                ],
              ),
            ),
            const SizedBox(height: AppDesign.spacing32),

            // 退出按钮
            AppButton(
              variant: AppButtonVariant.outlined,
              onPressed: () {},
              child: const Text('退出登录'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _divider() => const Padding(
    padding: EdgeInsets.symmetric(horizontal: AppDesign.spacing16),
    child: Divider(color: AppDesign.divider, height: 1),
  );
}

class _SettingItem extends StatelessWidget {
  const _SettingItem({required this.icon, required this.label});

  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(AppDesign.spacing16),
      child: Row(
        children: [
          Icon(icon, size: 20, color: AppDesign.textSecondary),
          const SizedBox(width: AppDesign.spacing12),
          Text(label, style: const TextStyle(
            fontSize: AppDesign.fontSizeBody,
            color: AppDesign.textPrimary,
          )),
          const Spacer(),
          const Icon(Icons.chevron_right, size: 18, color: AppDesign.textSecondary),
        ],
      ),
    );
  }
}

支持深色模式

自建设计体系也需要支持深色模式。最简单的方式是为 AppDesign 增加深色变体:

dart
class AppDesign {
  // 根据亮度返回不同的颜色
  static Color primaryOf(BuildContext context) {
    return isDark(context) ? const Color(0xFFFF8C5A) : const Color(0xFFFF6B35);
  }

  static Color backgroundOf(BuildContext context) {
    return isDark(context) ? const Color(0xFF121212) : const Color(0xFFFAFAFA);
  }

  static Color surfaceOf(BuildContext context) {
    return isDark(context) ? const Color(0xFF1E1E1E) : const Color(0xFFFFFFFF);
  }

  static Color textPrimaryOf(BuildContext context) {
    return isDark(context) ? const Color(0xFFE5E5E5) : const Color(0xFF1A1A2E);
  }

  static bool isDark(BuildContext context) {
    return MediaQuery.of(context).platformBrightness == Brightness.dark;
  }
}

在组件中使用:

dart
Container(
  color: AppDesign.backgroundOf(context),
  child: Text(
    'Hello',
    style: TextStyle(color: AppDesign.textPrimaryOf(context)),
  ),
)

更优雅的方案:ThemeExtension

使用 ThemeExtension(详见 ThemeData 主题)可以把自定义颜色注册到主题中,然后通过 Theme.of(context).extension<AppColors>() 获取。这种方式更规范,也支持 copyWithlerp

进阶:用 ThemeExtension 管理设计令牌

如果你想在自建设计体系的同时利用 Flutter 的主题机制,可以用 ThemeExtension 把设计令牌注册到 ThemeData 中:

dart
@immutable
class AppTokens extends ThemeExtension<AppTokens> {
  const AppTokens({
    required this.brandPrimary,
    required this.brandSecondary,
    required this.bgPage,
    required this.bgCard,
    required this.textDefault,
    required this.textMuted,
    required this.radiusCard,
    required this.radiusButton,
    required this.spaceUnit,
  });

  final Color brandPrimary;
  final Color brandSecondary;
  final Color bgPage;
  final Color bgCard;
  final Color textDefault;
  final Color textMuted;
  final double radiusCard;
  final double radiusButton;
  final double spaceUnit;

  @override
  AppTokens copyWith({...}) => AppTokens(...);  // 同上模式

  @override
  AppTokens lerp(covariant AppTokens? other, double t) {
    if (other == null) return this;
    return AppTokens(
      brandPrimary: Color.lerp(brandPrimary, other.brandPrimary, t)!,
      brandSecondary: Color.lerp(brandSecondary, other.brandSecondary, t)!,
      bgPage: Color.lerp(bgPage, other.bgPage, t)!,
      bgCard: Color.lerp(bgCard, other.bgCard, t)!,
      textDefault: Color.lerp(textDefault, other.textDefault, t)!,
      textMuted: Color.lerp(textMuted, other.textMuted, t)!,
      radiusCard: lerpDouble(radiusCard, other.radiusCard, t)!,
      radiusButton: lerpDouble(radiusButton, other.radiusButton, t)!,
      spaceUnit: lerpDouble(spaceUnit, other.spaceUnit, t)!,
    );
  }
}

注册到主题:

dart
MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    extensions: const [
      AppTokens(
        brandPrimary: Color(0xFFFF6B35),
        brandSecondary: Color(0xFF2EC4B6),
        bgPage: Color(0xFFFAFAFA),
        bgCard: Color(0xFFFFFFFF),
        textDefault: Color(0xFF1A1A2E),
        textMuted: Color(0xFF6B7280),
        radiusCard: 20,
        radiusButton: 12,
        spaceUnit: 4,
      ),
    ],
  ),
  darkTheme: ThemeData(
    useMaterial3: true,
    brightness: Brightness.dark,
    extensions: const [
      AppTokens(
        brandPrimary: Color(0xFFFF8C5A),
        brandSecondary: Color(0xFF4DD8C8),
        bgPage: Color(0xFF121212),
        bgCard: Color(0xFF1E1E1E),
        textDefault: Color(0xFFE5E5E5),
        textMuted: Color(0xFF9CA3AF),
        radiusCard: 20,
        radiusButton: 12,
        spaceUnit: 4,
      ),
    ],
  ),
)

在组件中获取:

dart
final tokens = Theme.of(context).extension<AppTokens>()!;

Container(
  decoration: BoxDecoration(
    color: tokens.bgCard,
    borderRadius: BorderRadius.circular(tokens.radiusCard),
  ),
  child: Text('Hello', style: TextStyle(color: tokens.textDefault)),
)

第三方设计体系

如果你不想从零搭建,也有一些现成的第三方设计体系可选:

名称风格地址
CupertinoiOS 原生风格Flutter 内置
MacosUImacOS 原生风格pub.dev/packages/macos_ui
FluentUIWindows Fluent 风格pub.dev/packages/fluent_ui
ShadcnUI现代极简风格pub.dev/packages/shadcn_ui
Mix样式与结构分离pub.dev/packages/mix

选择建议

  • iOS 风格 → 用内置 Cupertino
  • 跨桌面原生感 → macos_ui / fluent_ui
  • 现代 Web 感 → shadcn_ui
  • 高度灵活的样式系统 → Mix
  • 完全独特 → 自建(路线三)

决策流程图

不喜欢 Material 风格?

├─ 只是不喜欢默认配色/圆角
│   └─ ✅ 路线一:深度定制 ThemeData

├─ 想要 iOS 风格
│   └─ ✅ 路线二:CupertinoApp

├─ 想要桌面原生风格
│   └─ ✅ 第三方:macos_ui / fluent_ui

└─ 想要完全独特的品牌风格
    └─ ✅ 路线三:自建设计体系

速查表

需求方案
改 Material 配色ThemeData(colorScheme: ColorScheme(...))
改 Material 圆角ThemeData(cardTheme:, elevatedButtonTheme:, ...)
iOS 风格CupertinoApp + CupertinoPageScaffold
完全自建AppDesign 令牌类 + 自定义组件 + 基础 Widget
设计令牌管理ThemeExtension<AppTokens>
深色模式亮/暗两套 ThemeExtension + theme/darkTheme
桌面原生风格macos_ui / fluent_ui

基于 Flutter 官方文档整理