第 11 章 主题与深色模式(完整可运行示例)

Material 3 的主题只需一个种子色,其余色调由框架推导;深色模式通过 themeMode 切换并持久化。

创建步骤
  1. flutter create theme_demo && cd theme_demo;
  2. flutter pub add shared_preferences(用于记住主题选择);
  3. 替换 lib/main.dart;点右上角图标切换,杀掉 App 重进验证记忆生效。
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 启动前读出上次选择的主题,避免首帧闪烁
  final prefs = await SharedPreferences.getInstance();
  runApp(ThemeDemoApp(prefs: prefs));
}

class ThemeDemoApp extends StatefulWidget {
  const ThemeDemoApp({super.key, required this.prefs});

  final SharedPreferences prefs;

  @override
  State<ThemeDemoApp> createState() => _ThemeDemoAppState();
}

class _ThemeDemoAppState extends State<ThemeDemoApp> {
  static const _key = 'theme_mode';
  ThemeMode _mode = ThemeMode.system;

  @override
  void initState() {
    super.initState();
    final saved = widget.prefs.getString(_key);
    _mode = ThemeMode.values.firstWhere(
      (m) => m.name == saved,
      orElse: () => ThemeMode.system,
    );
  }

  Future<void> _toggle() async {
    final next = switch (_mode) {
      ThemeMode.system => ThemeMode.light,
      ThemeMode.light => ThemeMode.dark,
      ThemeMode.dark => ThemeMode.system,
    };
    setState(() => _mode = next);
    await widget.prefs.setString(_key, next.name);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      // 浅色与深色各给一套;颜色统一来自 ColorScheme
      theme: _buildTheme(Brightness.light),
      darkTheme: _buildTheme(Brightness.dark),
      themeMode: _mode,
      home: HomePage(mode: _mode, onToggle: _toggle),
    );
  }

  ThemeData _buildTheme(Brightness brightness) {
    final scheme =
        ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D6B), brightness: brightness);
    return ThemeData(
      useMaterial3: true,
      colorScheme: scheme,
      scaffoldBackgroundColor:
          brightness == Brightness.light ? const Color(0xFFF4F6F5) : null,
      appBarTheme: AppBarTheme(
        backgroundColor: Colors.transparent,
        elevation: 0,
        titleTextStyle: TextStyle(
            fontSize: 20, fontWeight: FontWeight.w700, color: scheme.onSurface),
      ),
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: scheme.surface,
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
          borderSide: BorderSide(color: scheme.outlineVariant),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key, required this.mode, required this.onToggle});

  final ThemeMode mode;
  final VoidCallback onToggle;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final label = switch (mode) {
      ThemeMode.system => '跟随系统',
      ThemeMode.light => '浅色',
      ThemeMode.dark => '深色',
    };

    return Scaffold(
      appBar: AppBar(
        title: const Text('主题示例'),
        actions: [
          IconButton(
            tooltip: label,
            icon: Icon(switch (mode) {
              ThemeMode.system => Icons.brightness_auto,
              ThemeMode.light => Icons.light_mode_outlined,
              ThemeMode.dark => Icons.dark_mode_outlined,
            }),
            onPressed: onToggle,
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('当前:$label',
                      style: const TextStyle(fontWeight: FontWeight.w700)),
                  const SizedBox(height: 6),
                  Text('所有颜色来自 ColorScheme,切换主题无需改任何页面代码。',
                      style: TextStyle(
                          fontSize: 12.5, color: scheme.onSurfaceVariant)),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          const TextField(decoration: InputDecoration(labelText: '输入框会跟随主题')),
          const SizedBox(height: 16),
          FilledButton(onPressed: () {}, child: const Text('主按钮')),
          const SizedBox(height: 10),
          OutlinedButton(onPressed: () {}, child: const Text('次按钮')),
        ],
      ),
    );
  }
}
关键点
动态效果 · 深浅色切换
主题示例
主按钮
← 上一章第 10 章 动画(完整可运行示例)下一章 →第 12 章 实用插件速查与入门
✉ 联系作者:linghurongjun@sina.com