设计理念

深色模式已经成为现代应用的标配功能,它不仅能够保护眼睛,还能节省电量。一个好的主题设置应该提供清晰的选项和实时预览效果。本文将详细介绍如何实现主题切换功能。
请添加图片描述

页面的基础结构

主题设置页面提供浅色和深色两种模式的选择,并展示预览效果。

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../controllers/note_controller.dart';

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

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('主题设置'),
      ),
      body: Obx(() => ListView(
        padding: EdgeInsets.all(16.w),
        children: [
          _buildThemeCard(controller),
          SizedBox(height: 16.h),
          _buildPreviewCard(controller),
        ],
      )),
    );
  }
}

页面使用StatelessWidget,状态管理交给GetX控制器。body使用Obx包裹ListView,实现响应式更新。页面内容分为两个卡片:主题选择和预览效果。每个卡片之间有16像素的间距。

主题选择卡片

主题选择卡片使用RadioListTile提供单选功能。

Widget _buildThemeCard(NoteController controller) {
  return Card(
    child: Column(
      children: [
        RadioListTile<bool>(
          title: const Text('浅色模式'),
          subtitle: const Text('明亮的界面风格'),
          secondary: const Icon(Icons.light_mode),
          value: false,
          groupValue: controller.isDarkMode.value,
          onChanged: (value) {
            controller.isDarkMode.value = value!;
            controller.saveData();
          },
        ),
        const Divider(height: 1),
        RadioListTile<bool>(
          title: const Text('深色模式'),
          subtitle: const Text('护眼的暗色风格'),
          secondary: const Icon(Icons.dark_mode),
          value: true,
          groupValue: controller.isDarkMode.value,
          onChanged: (value) {
            controller.isDarkMode.value = value!;
            controller.saveData();
          },
        ),
      ],
    ),
  );
}

第一个选项是浅色模式,value设置为false。RadioListTile的title显示模式名称,subtitle显示简短描述,secondary显示图标。groupValue绑定到controller.isDarkMode,onChanged回调在选择时更新值并保存。

预览效果卡片

预览卡片展示当前主题下的界面效果,让用户直观感受。

Widget _buildPreviewCard(NoteController controller) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '预览',
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 12.h),
          _buildPreviewSample(controller),
              border: Border.all(color: Colors.grey.withOpacity(0.3)),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '示例笔记标题',
                  style: TextStyle(
                    fontSize: 16.sp,
                    fontWeight: FontWeight.bold,
                    color: controller.isDarkMode.value
                        ? Colors.white
                        : Colors.black,
                  ),
                ),
                SizedBox(height: 4.h),
                Text(
                  '这是笔记内容的预览效果...',
                  style: TextStyle(
                    fontSize: 14.sp,
                    color: Colors.grey,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

预览卡片的标题使用粗体大字号,与主题选择卡片保持一致的样式。标题和预览内容之间有12像素的间距。crossAxisAlignment设置为start让内容左对齐。这种统一的设计风格让整个页面看起来更加协调。

预览容器的背景色根据当前主题动态变化。深色模式使用深灰色(0xFF1E1E1E),浅色模式使用白色。borderRadius设置圆角,border添加淡灰色边框。标题文字的颜色也根据主题变化,深色模式用白色,浅色模式用黑色。

主题的响应式管理

控制器中使用RxBool管理主题模式,实现响应式更新。

final RxBool isDarkMode = false.obs;

void loadData() async {
  final prefs = await SharedPreferences.getInstance();
  isDarkMode.value = prefs.getBool('isDarkMode') ?? false;
}

void saveData() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isDarkMode', isDarkMode.value);
}

isDarkMode使用RxBool实现响应式,任何修改都会自动通知UI更新。loadData方法从SharedPreferences加载保存的主题模式,默认值为false(浅色模式)。saveData方法将当前主题模式保存到本地存储。这种持久化设计确保用户的设置在应用重启后仍然有效。

主题的全局应用

主题设置需要应用到整个应用,通过MaterialApp的theme和darkTheme实现。

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final controller = Get.put(NoteController());
    
    return Obx(() => MaterialApp(
      title: '轻记',
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      themeMode: controller.isDarkMode.value 
          ? ThemeMode.dark 
          : ThemeMode.light,
      home: const MainPage(),
    ));
  }
}

MaterialApp使用Obx包裹,实现主题的响应式切换。theme定义浅色主题,darkTheme定义深色主题。themeMode根据controller.isDarkMode的值动态切换。这种设计让主题切换能够立即应用到整个应用,无需重启。

主题的颜色适配

不同主题下,某些颜色需要适配以保持良好的可读性。

final isDark = Theme.of(context).brightness == Brightness.dark;
final bgColor = isDark ? const Color(0xFF1E1E1E) : Colors.white;
final textColor = isDark ? Colors.white : Colors.black;

Card(
  color: bgColor,
  child: Text(
    '示例文字',
    style: TextStyle(color: textColor),
  ),
)

通过Theme.of(context).brightness获取当前主题的亮度。根据亮度选择合适的背景色和文字颜色。深色模式使用深灰色背景和白色文字,浅色模式使用白色背景和黑色文字。这种适配确保在两种模式下都有良好的视觉效果。

主题的自动切换

可以根据系统设置自动切换主题,提供更智能的体验。

enum ThemeMode {
  light,
  dark,
  system,
}

final Rx<ThemeMode> themeMode = ThemeMode.system.obs;

ThemeMode get effectiveThemeMode {
  if (themeMode.value == ThemeMode.system) {
    final brightness = WidgetsBinding.instance.window.platformBrightness;
    return brightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
  }
  return themeMode.value;
}

定义ThemeMode枚举,包含浅色、深色和跟随系统三种选项。effectiveThemeMode计算实际使用的主题模式,如果设置为跟随系统,就根据系统的platformBrightness决定。这种设计让用户可以选择手动控制或跟随系统。

主题切换的动画

主题切换时可以添加动画过渡,让变化更加平滑。

AnimatedTheme(
  duration: const Duration(milliseconds: 300),
  data: controller.isDarkMode.value ? darkTheme : lightTheme,
  child: MaterialApp(
    home: const MainPage(),
  ),
)

使用AnimatedTheme包裹MaterialApp,主题切换时会有300毫秒的过渡动画。这种平滑的过渡让用户体验更加舒适,避免突兀的颜色变化。duration可以根据需要调整,太短会显得仓促,太长会显得拖沓。

主题的预设方案

除了深浅两种基础模式,还可以提供多种预设主题方案。

enum ThemeScheme {
  blue,
  green,
  purple,
  orange,
}

ThemeData getThemeData(ThemeScheme scheme, bool isDark) {
  final colorScheme = switch (scheme) {
    ThemeScheme.blue => Colors.blue,
    ThemeScheme.green => Colors.green,
    ThemeScheme.purple => Colors.purple,
    ThemeScheme.orange => Colors.orange,
  };
  
  return ThemeData(
    brightness: isDark ? Brightness.dark : Brightness.light,
    primarySwatch: colorScheme,
    useMaterial3: true,
  );
}

定义ThemeScheme枚举,包含多种颜色方案。getThemeData方法根据方案和模式生成对应的ThemeData。这种设计让用户可以选择自己喜欢的主题颜色,提供更个性化的体验。

主题的保存和加载

主题设置需要持久化保存,确保应用重启后仍然有效。

void saveTheme() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isDarkMode', isDarkMode.value);
  await prefs.setString('themeScheme', themeScheme.value.name);
}

void loadTheme() async {
  final prefs = await SharedPreferences.getInstance();
  isDarkMode.value = prefs.getBool('isDarkMode') ?? false;
  final schemeName = prefs.getString('themeScheme') ?? 'blue';
  themeScheme.value = ThemeScheme.values.firstWhere(
    (e) => e.name == schemeName,
    orElse: () => ThemeScheme.blue,
  );
}

saveTheme方法保存主题模式和颜色方案到SharedPreferences。loadTheme方法加载保存的设置,如果没有保存的值就使用默认值。这种持久化设计确保用户的个性化设置不会丢失。

主题的实时预览

在设置页面提供实时预览,让用户在切换前就能看到效果。

Widget buildThemePreview(bool isDark) {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(
        color: Colors.grey.withOpacity(0.3),
      ),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Container(
          height: 40.h,
          color: const Color(0xFF2196F3),
          child: Center(
            child: Text(
              'AppBar',
              style: TextStyle(color: Colors.white, fontSize: 16.sp),
            ),
          ),
        ),
        SizedBox(height: 8.h),
        Text(
          '笔记标题',
          style: TextStyle(
            fontSize: 16.sp,
            fontWeight: FontWeight.bold,
            color: isDark ? Colors.white : Colors.black,
          ),
        ),
        Text(
          '笔记内容预览...',
          style: TextStyle(
            fontSize: 14.sp,
            color: Colors.grey,
          ),
        ),
      ],
    ),
  );
}

buildThemePreview方法创建一个主题预览组件,模拟真实的应用界面。包含AppBar、标题和内容等元素,让用户可以直观地看到主题效果。这种预览功能能够帮助用户做出更好的选择。

主题的导出和导入

支持导出和导入主题设置,方便在不同设备间同步。

Map<String, dynamic> exportTheme() {
  return {
    'isDarkMode': isDarkMode.value,
    'themeScheme': themeScheme.value.name,
    'version': '1.0',
  };
}

void importTheme(Map<String, dynamic> data) {
  isDarkMode.value = data['isDarkMode'] ?? false;
  final schemeName = data['themeScheme'] ?? 'blue';
  themeScheme.value = ThemeScheme.values.firstWhere(
    (e) => e.name == schemeName,
    orElse: () => ThemeScheme.blue,
  );
  saveTheme();
}

exportTheme方法将主题设置导出为Map,可以转换为JSON字符串。importTheme方法从Map导入主题设置。这种导出导入功能让用户可以在多个设备间保持一致的主题设置。

主题的重置功能

提供重置功能,让用户可以快速恢复到默认设置。

void resetTheme() {
  isDarkMode.value = false;
  themeScheme.value = ThemeScheme.blue;
  saveTheme();
  Get.snackbar('提示', '已重置为默认主题', 
    snackPosition: SnackPosition.BOTTOM);
}

resetTheme方法将主题重置为浅色模式和蓝色方案,然后保存并显示提示消息。这个功能可以在设置页面添加一个重置按钮,让用户在调节不满意时快速恢复。

主题的可访问性

主题设置应该考虑可访问性,确保所有用户都能使用。

Semantics(
  label: '主题模式选择',
  hint: '选择浅色或深色模式',
  child: RadioListTile<bool>(
    title: const Text('浅色模式'),
    value: false,
    groupValue: controller.isDarkMode.value,
    onChanged: (value) {
      controller.isDarkMode.value = value!;
      controller.saveData();
    },
  ),
)

为主题选择添加Semantics标签,帮助屏幕阅读器理解组件的用途。label描述组件是什么,hint提供使用提示。这些语义信息让视障用户也能顺利使用主题设置功能。

主题的性能优化

主题切换应该是高效的,避免不必要的重建。

class ThemeProvider extends InheritedWidget {
  final bool isDarkMode;
  final Function(bool) onThemeChanged;

  const ThemeProvider({
    Key? key,
    required this.isDarkMode,
    required this.onThemeChanged,
    required Widget child,
  }) : super(key: key, child: child);

  static ThemeProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
  }

  
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

使用InheritedWidget提供主题数据,只有当主题真正改变时才通知子组件重建。updateShouldNotify方法比较新旧值,只有不同时才返回true。这种优化能够避免不必要的重建,提升性能。

主题的调试工具

在开发阶段,可以添加调试工具快速切换主题。

Widget buildThemeDebugger() {
  return FloatingActionButton(
    mini: true,
    onPressed: () {
      isDarkMode.value = !isDarkMode.value;
    },
    child: Icon(
      isDarkMode.value ? Icons.light_mode : Icons.dark_mode,
    ),
  );
}

buildThemeDebugger创建一个小型悬浮按钮,点击可以快速切换主题。图标根据当前模式动态变化。这个工具只在开发环境中显示,方便开发者测试不同主题下的界面效果。

总结

主题设置是现代应用的重要功能,它不仅影响外观,还关系到用户体验。通过合理的架构设计,我们可以实现灵活、高效的主题切换系统。

本文介绍了从基础的主题选择到高级的个性化配置的完整实现方案。关键点包括:使用GetX实现响应式状态管理、通过SharedPreferences持久化设置、提供实时预览效果、考虑可访问性和性能优化。

一个好的主题系统应该既简单易用,又功能丰富。通过本文的介绍,开发者可以为Flutter应用构建出优秀的主题切换功能,为用户提供个性化的使用体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐