在 Flutter 与原生混合开发中保证暗夜模式一致性,需要从 Flutter 端和原生端(Android/iOS)同时进行配置和同步。以下是完整的解决方案:
- Flutter 端配置
1.1 使用 Provider 状态管理(推荐)
dart
// theme_provider.dart
import 'package:flutter/material.dart';
class ThemeProvider with ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode => _themeMode == ThemeMode.dark;
void setThemeMode(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
}
void toggleTheme() {
_themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
}
1.2 MaterialApp 配置
dart
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => ThemeProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
theme: ThemeData.light().copyWith(
// 自定义亮色主题
primaryColor: Colors.blue,
scaffoldBackgroundColor: Colors.white,
),
darkTheme: ThemeData.dark().copyWith(
// 自定义暗色主题
primaryColor: Colors.blue[700],
scaffoldBackgroundColor: Colors.grey[900],
),
themeMode: themeProvider.themeMode,
home: const HomePage(),
);
},
);
}
}
- Android 端配置
2.1 在 MainActivity 中同步主题
kotlin
// MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "theme_channel"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
when (call.method) {
"getSystemTheme" -> {
val nightMode = resources.configuration.uiMode and
android.content.res.Configuration.UI_MODE_NIGHT_MASK
val isDark = nightMode == android.content.res.Configuration.UI_MODE_NIGHT_YES
result.success(isDark)
}
"setAppTheme" -> {
val isDark = call.arguments as Boolean
setAppTheme(isDark)
result.success(null)
}
else -> result.notImplemented()
}
}
}
private fun setAppTheme(isDark: Boolean) {
// 设置原生端主题
if (isDark) {
// 应用暗色主题
setTheme(android.R.style.Theme_DeviceDefault_Dark)
} else {
// 应用亮色主题
setTheme(android.R.style.Theme_DeviceDefault_Light)
}
recreate() // 重新创建Activity应用主题
}
}
2.2 AndroidManifest.xml 配置
xml
<application
android:name=".MainApplication"
android:theme="@style/LaunchTheme">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:theme="@style/NormalTheme">
<!-- 其他配置 -->
</activity>
</application>
- iOS 端配置
3.1 在 AppDelegate 中同步主题
swift
// AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
let CHANNEL = "theme_channel"
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "getSystemTheme":
self?.getSystemTheme(result: result)
case "setAppTheme":
if let arguments = call.arguments as? [String: Any],
let isDark = arguments["isDark"] as? Bool {
self?.setAppTheme(isDark: isDark)
result(nil)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getSystemTheme(result: FlutterResult) {
if #available(iOS 13.0, *) {
let isDark = UITraitCollection.current.userInterfaceStyle == .dark
result(isDark)
} else {
result(false) // iOS 13以下默认亮色
}
}
private func setAppTheme(isDark: Bool) {
// 设置原生端主题
if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = isDark ? .dark : .light
}
}
}
- Flutter 与原生通信
4.1 创建通信工具类
dart
// native_theme_handler.dart
import 'package:flutter/services.dart';
class NativeThemeHandler {
static const MethodChannel _channel = MethodChannel('theme_channel');
// 获取系统主题
static Future<bool> getSystemTheme() async {
try {
final bool isDark = await _channel.invokeMethod('getSystemTheme');
return isDark;
} on PlatformException {
return false;
}
}
// 设置原生端主题
static Future<void> setNativeTheme(bool isDark) async {
try {
await _channel.invokeMethod('setAppTheme', {'isDark': isDark});
} on PlatformException catch (e) {
print('Failed to set native theme: ${e.message}');
}
}
}
4.2 在主题提供者中同步
dart
// 修改 theme_provider.dart
class ThemeProvider with ChangeNotifier {
// ... 其他代码
Future<void> syncWithSystem() async {
final bool isSystemDark = await NativeThemeHandler.getSystemTheme();
_themeMode = isSystemDark ? ThemeMode.dark : ThemeMode.light;
await NativeThemeHandler.setNativeTheme(isSystemDark);
notifyListeners();
}
Future<void> setThemeAndSync(ThemeMode mode) async {
_themeMode = mode;
await NativeThemeHandler.setNativeTheme(mode == ThemeMode.dark);
notifyListeners();
}
}
- 监听系统主题变化
5.1 Flutter 端监听
dart
// 在 main.dart 或首页中添加
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initTheme();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangePlatformBrightness() {
// 系统亮度变化时同步
context.read<ThemeProvider>().syncWithSystem();
super.didChangePlatformBrightness();
}
void _initTheme() async {
// 初始化时同步主题
await context.read<ThemeProvider>().syncWithSystem();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('主题演示'),
actions: [
IconButton(
icon: const Icon(Icons.brightness_6),
onPressed: () {
context.read<ThemeProvider>().toggleTheme();
NativeThemeHandler.setNativeTheme(
context.read<ThemeProvider>().isDarkMode
);
},
),
],
),
body: const Center(
child: Text('主题同步示例'),
),
);
}
}
- 持久化存储
dart
// 使用 shared_preferences 保存主题偏好
import 'package:shared_preferences/shared_preferences.dart';
class ThemeProvider with ChangeNotifier {
// ... 其他代码
Future<void> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
final themeIndex = prefs.getInt('themeMode') ?? ThemeMode.system.index;
_themeMode = ThemeMode.values[themeIndex];
// 如果是系统模式,同步系统主题
if (_themeMode == ThemeMode.system) {
await syncWithSystem();
} else {
await NativeThemeHandler.setNativeTheme(_themeMode == ThemeMode.dark);
}
notifyListeners();
}
Future<void> saveTheme() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('themeMode', _themeMode.index);
}
Future<void> setThemeAndSync(ThemeMode mode) async {
_themeMode = mode;
await NativeThemeHandler.setNativeTheme(mode == ThemeMode.dark);
await saveTheme();
notifyListeners();
}
}
-
使用注意事项
-
初始化顺序:在应用启动时先加载保存的主题设置
-
错误处理:妥善处理平台通信可能出现的异常
-
性能考虑:避免频繁的主题切换操作
-
测试:分别在亮色和暗色模式下测试所有界面
这样配置后,你的 Flutter 与原生混合应用就能在各个平台上保持暗夜模式的一致性了。