跳转到正文
YGJoon 技术博客

Flutter深色模式适配实践

发布于:
·1.4k 字 · 约 3 分钟·作者:
两套 ThemeData 定义明暗配色,ThemeMode.system 跟随系统,ChangeNotifier 管手动切换与持久化
Flutter深色模式适配实践文章封面
目录

简介

深色模式(Dark Mode)已成为现代应用的标配功能,本文将详细介绍如何在Flutter应用中实现深色模式支持,包括系统主题跟随、手动切换、自定义组件适配等内容。

基础配置

1. 定义主题数据

import 'package:flutter/material.dart';

class AppTheme {
  static final ThemeData lightTheme = ThemeData(
    brightness: Brightness.light,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.white,
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.blue,
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.black87),
      bodyMedium: TextStyle(color: Colors.black87),
    ),
  );

  static final ThemeData darkTheme = ThemeData(
    brightness: Brightness.dark,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.grey[900],
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.grey[850],
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.white70),
      bodyMedium: TextStyle(color: Colors.white70),
    ),
  );
}

2. 主题状态管理

import 'package:flutter/material.dart';

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;

  ThemeMode get themeMode => _themeMode;

  bool get isDarkMode {
    if (_themeMode == ThemeMode.system) {
      return WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
    }
    return _themeMode == ThemeMode.dark;
  }

  void setThemeMode(ThemeMode mode) {
    _themeMode = mode;
    notifyListeners();
  }
}

3. 应用主题配置

import 'package:provider/provider.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ThemeProvider(),
      child: Consumer<ThemeProvider>(
        builder: (context, themeProvider, child) {
          return MaterialApp(
            theme: AppTheme.lightTheme,
            darkTheme: AppTheme.darkTheme,
            themeMode: themeProvider.themeMode,
            home: HomePage(),
          );
        },
      ),
    );
  }
}

自定义组件适配

1. 颜色适配

class AdaptiveColors {
  static Color getBackgroundColor(BuildContext context) {
    return Theme.of(context).brightness == Brightness.dark
        ? Colors.grey[900]!
        : Colors.white;
  }

  static Color getTextColor(BuildContext context) {
    return Theme.of(context).brightness == Brightness.dark
        ? Colors.white70
        : Colors.black87;
  }

  static Color getCardColor(BuildContext context) {
    return Theme.of(context).brightness == Brightness.dark
        ? Colors.grey[850]!
        : Colors.white;
  }
}

2. 自定义组件示例

class AdaptiveCard extends StatelessWidget {
  final Widget child;
  final EdgeInsets padding;

  const AdaptiveCard({
    Key? key,
    required this.child,
    this.padding = const EdgeInsets.all(16.0),
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: padding,
      decoration: BoxDecoration(
        color: AdaptiveColors.getCardColor(context),
        borderRadius: BorderRadius.circular(8.0),
        boxShadow: [
          BoxShadow(
            color: Theme.of(context).brightness == Brightness.dark
                ? Colors.black26
                : Colors.grey.withOpacity(0.1),
            blurRadius: 4.0,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: child,
    );
  }
}

主题切换实现

1. 主题切换按钮

class ThemeToggleButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(
        Provider.of<ThemeProvider>(context).isDarkMode
            ? Icons.light_mode
            : Icons.dark_mode,
      ),
      onPressed: () {
        var provider = Provider.of<ThemeProvider>(context, listen: false);
        provider.setThemeMode(
          provider.isDarkMode ? ThemeMode.light : ThemeMode.dark,
        );
      },
    );
  }
}

2. 主题设置页面

class ThemeSettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('主题设置')),
      body: Consumer<ThemeProvider>(
        builder: (context, provider, child) {
          return ListView(
            children: [
              RadioListTile<ThemeMode>(
                title: Text('跟随系统'),
                value: ThemeMode.system,
                groupValue: provider.themeMode,
                onChanged: (value) => provider.setThemeMode(value!),
              ),
              RadioListTile<ThemeMode>(
                title: Text('浅色模式'),
                value: ThemeMode.light,
                groupValue: provider.themeMode,
                onChanged: (value) => provider.setThemeMode(value!),
              ),
              RadioListTile<ThemeMode>(
                title: Text('深色模式'),
                value: ThemeMode.dark,
                groupValue: provider.themeMode,
                onChanged: (value) => provider.setThemeMode(value!),
              ),
            ],
          );
        },
      ),
    );
  }
}

图片资源适配

1. 图片资源管理

class AdaptiveImage {
  static ImageProvider getImage(BuildContext context, String name) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return AssetImage('assets/images/${isDark ? "dark" : "light"}/$name');
  }
}

2. 使用示例

Image(
  image: AdaptiveImage.getImage(context, 'background.png'),
  fit: BoxFit.cover,
)

动态切换优化

1. 平滑过渡动画

class ThemeAnimationWrapper extends StatelessWidget {
  final Widget child;

  const ThemeAnimationWrapper({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedTheme(
      data: Theme.of(context),
      duration: Duration(milliseconds: 200),
      child: child,
    );
  }
}

2. 主题切换回调

class ThemeAwareWidget extends StatefulWidget {
  final Widget Function(BuildContext context, bool isDark) builder;

  const ThemeAwareWidget({Key? key, required this.builder}) : super(key: key);

  @override
  _ThemeAwareWidgetState createState() => _ThemeAwareWidgetState();
}

class _ThemeAwareWidgetState extends State<ThemeAwareWidget> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 主题变化时的处理逻辑
  }

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return widget.builder(context, isDark);
  }
}

最佳实践

1. 全文零硬编码颜色,这是深色适配的第一道门槛

Color(0xFF333333) 在代码里出现一次,深色模式就多一个漏网之鱼。所有颜色走 Theme.of(context).colorScheme,文字走 textTheme——Material 3 的 ColorScheme.dark() 自带一套映射,配好 theme + darkTheme 就完成 80%。

2. 默认跟随系统,用户手动选择后才持久化

ThemeMode.system 做默认选项;用户在设置页手动切过一次,才把选择写进 SharedPreferences。反例:每次启动都重置为浅色——用户在系统级开了深色,你的 App 每次都”失忆”,体验是灾难。

3. 第三方组件要单独处理

WebView、图表库、富文本渲染器不跟随 App 的 ThemeData,深色模式下它们还是白色底。接入前先确认组件支不支持换肤,不支持的要准备深色样式注入(如 WebView 注入 CSS),否则就是一块白色补丁糊在深色界面上。

4. 深色下对比度同样要达标

深灰底 + 浅灰字看着高级,WCAG AA 的 4.5:1 一量就翻车。深色不是”把白色换成黑色”,文字和次要信息的对比度要在深色下重新量一遍。

实战案例

深色模式我还没在项目里做过——五个 App 全是浅色,连 ThemeMode.system 都没配。诚实说,这是排期问题不是技术问题:Flutter 的深色模式支持很成熟(ThemeData.dark() + themeMode),真正的坑在设计稿:我的项目 UI 全是浅色定的,按钮、卡片、图片的深色版要设计师重出一套,没有设计稿,程序员自己”反色”只会做出车祸现场。所以深色模式是”设计先行”的功能,技术只占 30%。我的计划是等哪个 App 改版时,让设计师出深色规范,一次性切。别自己拍脑袋反色,用户会骂的。参考:Flutter 官方文档深色模式章节。

总结

完善的深色模式支持能够:

  1. 提升用户体验
  2. 减少用户视觉疲劳
  3. 适应不同场景需求
  4. 展现应用专业度

通过合理的架构设计和优化措施,可以实现:


分享这篇文章:


相关文章

上一篇
Flutter 设备通信详解
下一篇
Flutter 生命周期管理详解