通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97

设置页面是每个App的标配,用户在这里可以调整应用的行为和外观。对于微动漫App来说,最重要的设置就是主题切换——让用户可以选择浅色、深色或跟随系统。

这篇文章会完整实现设置功能,从主题配置文件到 Provider 状态管理,再到设置页面的 UI,最后讲讲主题是如何在整个 App 中生效的。代码都是项目里实际跑着的。


请添加图片描述

主题系统的整体架构

主题切换涉及多个文件的配合:

theme.dart:定义浅色和深色两套主题的具体样式,包括颜色、字体、组件样式等。

ThemeProvider:管理当前主题状态,提供切换方法,负责持久化存储。

MicroAnimeApp:应用的根组件,监听 ThemeProvider 并应用主题。

SettingsScreen:设置页面,提供主题切换的 UI。

这四个部分各司其职,通过 Provider 连接在一起。


主题配置文件

先看主题的具体定义:

import 'package:flutter/material.dart';

class AppTheme {
  static const Color primaryColor = Color(0xFFFF6B9D);
  static const Color secondaryColor = Color(0xFF9B59B6);
  static const Color accentColor = Color(0xFFFFB347);
  static const Color darkBg = Color(0xFF1A1A2E);
  static const Color darkCard = Color(0xFF16213E);

先定义几个常用颜色。primaryColor 是粉红色,作为 App 的主色调。secondaryColor 是紫色,用于渐变和强调。accentColor 是橙色,用于点缀。

深色模式有专门的背景色 darkBg 和卡片色 darkCard,都是深蓝色调,比纯黑色看起来更舒服。


浅色主题配置

  static ThemeData lightTheme = ThemeData(
    useMaterial3: true,
    brightness: Brightness.light,
    primaryColor: primaryColor,
    colorScheme: ColorScheme.light(
      primary: primaryColor,
      secondary: secondaryColor,
      tertiary: accentColor,
      surface: Colors.white,
      background: const Color(0xFFF8F9FA),
    ),

useMaterial3: true 启用 Material 3 设计语言,组件样式更现代。

ColorScheme 定义了一整套颜色,Flutter 的组件会自动使用这些颜色。surface 是卡片等表面的颜色,background 是页面背景色。

    scaffoldBackgroundColor: const Color(0xFFF8F9FA),
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.white,
      foregroundColor: Colors.black87,
      elevation: 0,
      centerTitle: true,
    ),

scaffoldBackgroundColor 设置 Scaffold 的背景色,用浅灰色而不是纯白,看起来更柔和。

AppBarTheme 配置所有 AppBar 的默认样式:白色背景、深色文字、无阴影、标题居中。

    cardTheme: CardTheme(
      elevation: 2,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    ),
    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
      backgroundColor: Colors.white,
      selectedItemColor: primaryColor,
      unselectedItemColor: Colors.grey,
    ),
  );

CardTheme 设置卡片的阴影和圆角,16 像素的圆角让卡片看起来更圆润。

BottomNavigationBarTheme 设置底部导航栏的颜色,选中项用主色,未选中用灰色。


深色主题配置

  static ThemeData darkTheme = ThemeData(
    useMaterial3: true,
    brightness: Brightness.dark,
    primaryColor: primaryColor,
    colorScheme: ColorScheme.dark(
      primary: primaryColor,
      secondary: secondaryColor,
      tertiary: accentColor,
      surface: darkCard,
      background: darkBg,
    ),

深色主题的结构和浅色一样,只是颜色不同。brightness: Brightness.dark 告诉 Flutter 这是深色主题,一些组件会自动调整颜色。

注意 primaryColor 保持不变,这样 App 的品牌色在两种主题下都是一致的。

    scaffoldBackgroundColor: darkBg,
    appBarTheme: const AppBarTheme(
      backgroundColor: darkBg,
      foregroundColor: Colors.white,
      elevation: 0,
      centerTitle: true,
    ),
    cardTheme: CardTheme(
      color: darkCard,
      elevation: 2,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    ),
    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
      backgroundColor: darkCard,
      selectedItemColor: primaryColor,
      unselectedItemColor: Colors.grey,
    ),
  );
}

深色主题的 AppBar 和卡片都用深色背景,文字用白色。底部导航栏的背景用卡片色,和页面背景有区分。


ThemeProvider 的实现

Provider 负责管理主题状态:

import 'package:flutter/material.dart';
import '../services/storage_service.dart';

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

  ThemeMode get themeMode => _themeMode;

继承 ChangeNotifier 让这个类具备通知监听者的能力。_themeMode 存储当前主题模式,默认跟随系统。

ThemeMode 是 Flutter 内置的枚举,有三个值:lightdarksystem


构造函数中加载主题

  ThemeProvider() {
    _loadTheme();
  }

  Future<void> _loadTheme() async {
    try {
      print('🔄 ThemeProvider: 加载主题...');
      await StorageService.instance.init();
      final theme = StorageService.instance.getString('theme') ?? 'system';
      _themeMode = _getThemeMode(theme);
      print('✅ ThemeProvider: 已加载主题: $theme -> $_themeMode');
      notifyListeners();
    } catch (e) {
      print('❌ ThemeProvider: 加载主题错误: $e');
    }
  }

构造函数里调用 _loadTheme,从本地存储加载之前保存的主题设置。这样 App 重启后主题不会丢失。

?? 'system' 处理第一次启动没有保存过主题的情况,默认跟随系统。


切换主题

  void setTheme(String theme) {
    print('🔄 ThemeProvider: 设置主题为: $theme');
    _themeMode = _getThemeMode(theme);
    StorageService.instance.setString('theme', theme);
    print('✅ ThemeProvider: 主题已设置: $_themeMode');
    notifyListeners();
  }

这个方法做三件事:更新内存状态、保存到本地存储、通知监听者。调用后整个 App 的主题会立即切换。

  ThemeMode _getThemeMode(String theme) {
    switch (theme) {
      case 'light':
        return ThemeMode.light;
      case 'dark':
        return ThemeMode.dark;
      default:
        return ThemeMode.system;
    }
  }

  bool get isDark => _themeMode == ThemeMode.dark;
}

_getThemeMode 把字符串转成 ThemeMode 枚举。isDark 是个便捷属性,方便其他地方判断当前是否深色模式。


在 App 根组件应用主题

主题需要在 MaterialApp 上配置才能生效:

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

  
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: '微动漫',
          debugShowCheckedModeBanner: false,
          theme: AppTheme.lightTheme,
          darkTheme: AppTheme.darkTheme,
          themeMode: themeProvider.themeMode,
          home: const SplashScreen(),
        );
      },
    );
  }
}

Consumer 监听 ThemeProvider,主题变化时 MaterialApp 会重建。

theme 是浅色主题,darkTheme 是深色主题,themeMode 决定用哪个。当 themeMode 是 system 时,Flutter 会根据系统设置自动选择。


Provider 的注册

Provider 需要在 App 启动时注册:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
        ChangeNotifierProvider(create: (_) => FavoritesProvider()),
        ChangeNotifierProvider(create: (_) => HistoryProvider()),
        ChangeNotifierProvider(create: (_) => SearchProvider()),
      ],
      child: const MicroAnimeApp(),
    ),
  );
}

MultiProvider 可以同时注册多个 Provider。ChangeNotifierProvider 会在需要时创建 Provider 实例,并在 App 销毁时自动释放。

WidgetsFlutterBinding.ensureInitialized() 确保 Flutter 绑定初始化完成,在调用原生功能前必须调用。


设置页面的实现

设置页面提供主题切换的 UI:

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({super.key});

  
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  String _selectedTheme = 'system';

StatefulWidget 是因为需要维护当前选中的主题选项。_selectedTheme 用来控制 RadioListTile 的选中状态。


初始化时同步状态


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    final themeProvider = context.read<ThemeProvider>();
    setState(() {
      switch (themeProvider.themeMode) {
        case ThemeMode.light:
          _selectedTheme = 'light';
          break;
        case ThemeMode.dark:
          _selectedTheme = 'dark';
          break;
        default:
          _selectedTheme = 'system';
      }
    });
  });
}

页面打开时从 Provider 获取当前主题,同步到本地状态。用 addPostFrameCallback 是因为 initState 里不能直接访问 context。

这样用户进入设置页面时,能看到当前选中的是哪个主题。


主题切换处理

void _changeTheme(String? theme) {
  if (theme == null) return;
  print('🎨 选择主题: $theme');
  setState(() {
    _selectedTheme = theme;
  });
  context.read<ThemeProvider>().setTheme(theme);
  print('✅ 主题已更改为: $theme');
}

先更新本地状态让 UI 立即响应,然后调用 Provider 的 setTheme 方法。Provider 会保存设置并通知 MaterialApp 切换主题。


设置页面的 UI


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('设置')),
    body: ListView(
      children: [
        const Padding(
          padding: EdgeInsets.all(16),
          child: Text(
            '外观',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
        ),

ListView 展示设置项,分组标题用 Padding 包裹的 Text。

        RadioListTile<String>(
          title: const Text('浅色主题'),
          value: 'light',
          groupValue: _selectedTheme,
          onChanged: _changeTheme,
        ),
        RadioListTile<String>(
          title: const Text('深色主题'),
          value: 'dark',
          groupValue: _selectedTheme,
          onChanged: _changeTheme,
        ),
        RadioListTile<String>(
          title: const Text('跟随系统'),
          value: 'system',
          groupValue: _selectedTheme,
          onChanged: _changeTheme,
        ),

RadioListTile 是带单选按钮的列表项。value 是这个选项代表的值,groupValue 是当前选中的值,两者相等时显示选中状态。

三个选项覆盖了用户的主要需求:强制浅色、强制深色、跟随系统。


应用信息展示

设置页面还展示一些应用信息:

        const Divider(),
        const Padding(
          padding: EdgeInsets.all(16),
          child: Text(
            '关于',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
        ),
        const ListTile(
          title: Text('应用版本'),
          subtitle: Text('1.0.0'),
        ),
        const ListTile(
          title: Text('应用名称'),
          subtitle: Text('微动漫'),
        ),
        const ListTile(
          title: Text('数据来源'),
          subtitle: Text('Jikan API'),
        ),

Divider 是分隔线,用来区分不同的设置分组。ListTile 的 subtitle 显示具体的值。


API 信息展示

        const Divider(),
        Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                '使用的 API',
                style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 12),
              _buildApiInfo('Jikan API', '动漫数据、角色、推荐等'),
              _buildApiInfo('AnimeChan', '动漫名言'),
              _buildApiInfo('Catboy API', '可爱图片'),
            ],
          ),
        ),
      ],
    ),
  );
}

展示 App 使用的 API 信息,让用户了解数据来源。

Widget _buildApiInfo(String name, String description) {
  return Padding(
    padding: const EdgeInsets.only(bottom: 12),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          name,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 2),
        Text(
          description,
          style: const TextStyle(fontSize: 12, color: Colors.grey),
        ),
      ],
    ),
  );
}

_buildApiInfo 是个辅助方法,用来构建 API 信息项。名称用粗体,描述用小号灰色字体。


主题切换的即时效果

主题切换后,整个 App 的颜色会立即变化,包括:

AppBar:背景色和文字颜色。

Scaffold:页面背景色。

Card:卡片背景色和阴影。

BottomNavigationBar:底部导航栏的颜色。

各种组件:按钮、输入框、对话框等都会自动适配。

这是因为我们在 ThemeData 里配置了这些组件的默认样式,Flutter 会自动应用。


深色模式的适配技巧

有些地方需要手动判断当前主题来调整样式:

final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? Colors.white : Colors.black87;
final bgColor = isDark ? Colors.grey[800] : Colors.grey[200];

通过 Theme.of(context).brightness 判断当前是深色还是浅色模式,然后设置对应的颜色。

骨架屏组件就用了这个技巧:

final isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isDark ? Colors.grey[800]! : Colors.grey[300]!;
final highlightColor = isDark ? Colors.grey[700]! : Colors.grey[100]!;

深色模式下用深灰色,浅色模式下用浅灰色,这样骨架屏在任何主题下都能和谐融入界面。


小结

设置功能涉及的知识点:ThemeData 配置ColorScheme 颜色方案Provider 状态管理本地存储持久化RadioListTile 单选列表Consumer 监听状态变化addPostFrameCallback 延迟执行

主题系统的核心是 ThemeData,它定义了整个 App 的视觉风格。通过 Provider 管理主题状态,用户的选择可以持久化保存,App 重启后依然生效。

一个好的设置页面不仅要功能完善,还要让用户一眼就能找到想要的��项。主题切换放在最显眼的位置,应用信息放在下面,层次分明。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐