在 Flutter 中使用 Dark Mode / Light Mode 來讓 APP 更具設計感吧!

春麗 S.T.E.M.
12 min readSep 12, 2024

--

目錄

⦿ Dark Mode / Light Mode
⦿ Android
⦿ iOS
⦿ Flutter
⦿ iOS
⦿ Android

Dark Mode / Light Mode

在 iOS 跟 Android 中,若希望 APP 更能符合當前的亮度風格,我們會去調整如下:

如果習慣亮色就會使用 Light Mode,如果是習慣暗色就會使用 Dark Mode,像上圖左就是亮色模式,上圖右就是暗色模式。

那麼,在 Flutter 中要如何去因為模式的切換而改變主題顯色呢?可以想見的第一步是我們必須要取得當前手機的亮暗色模式,在 Flutter 中,我們會使用 MediaQuery.of(context).platformBrightness

Android

在 Android Studio 中,我們會這樣去 Debug 是在 Dark Mode 還是 Light Mode,例如在 Fragment 中加入下面這段:

override fun onResume() {
super.onResume()

val nightModeFlags: Int = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
when (nightModeFlags) {
Configuration.UI_MODE_NIGHT_YES -> print("Dark mode is active")
Configuration.UI_MODE_NIGHT_NO -> print("Light mode is active")
}
}

這樣就可在 Fragment Resume 後,確認修改的 Mode,透過 resources.configuration.uiModeConfiguration.UI_MODE_NIGHT_MASK

確認現在的 Mode,放在 onResume() 裡,就可以即時修改,在 Debug 模式下追蹤 Function 跑到哪裡了。

iOS

在 iOS 中,我們可以透過點擊按鈕來去檢查現在的顏色模式,如下:

@IBAction func buttonTapped(_ sender: UIButton) {
let style = traitCollection.userInterfaceStyle

switch style {
case .dark: print("Dark Mode")
case .light: print("Light Mode")
case .unspecified: return
@unknown default: return

}
}

使用的是 traitCollection.userInterfaceStyle,下面我們就來使用 Flutter 的 MediaQuery

繼續閱讀|回目錄

Flutter

在 Flutter 中,想要透過手機切換 Dark / Light Mode,來變更我們的 APP 主題配色,首先,新增一個 AppTheme class 如下:

class AppTheme {
ThemeData themeData;

// 工廠方法根據 brightness 返回不同的主題
factory AppTheme(Brightness brightness) {
if (brightness == Brightness.dark) {
return AppTheme._internal(_buildDarkTheme());
} else {
return AppTheme._internal(_buildLightTheme());
}
}

AppTheme._internal(this.themeData);

// 深色模式的主題
static ThemeData _buildDarkTheme() {
return ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.grey[900],
hintColor: Colors.blue[200],
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
), colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark
),
);
}

// 淺色模式的主題
static ThemeData _buildLightTheme() {
return ThemeData(
brightness: Brightness.light,
primaryColor: Colors.white,
hintColor: Colors.blue[700],
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black),
), colorScheme: ColorScheme.fromSeed(
seedColor: Colors.white,
brightness: Brightness.light
),
);
}
}

APP 透過存取 themeData 來更改主題色。

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

@override
Widget build(BuildContext context) {
final appTheme = AppTheme(MediaQuery.of(context).platformBrightness);

return MaterialApp(
title: 'Dark/Light Mode App',
theme: appTheme.themeData, // 使用 AppTheme 中的 ThemeData
home: const SignUpPage(),
);
}
}

這個 themeData 就是 AppTheme 實例中的屬性,如果是 Dark Mode,就用 Dark Mode 的 function 來建構 Theme,反之亦然,也就是建構參數 Brightness,看你傳入的是 .dark 或不是,來決定用哪一個 function,不同的 function 建構出不同的主題配色

而我們取得現在 Mode 使用了 MediaQuery.of(context).platformBrightness,MediaQuery 是 class MediaQuery extends InheritedModel<_MediaQueryAspect> { },而從 _MediaQueryAspect 可以看到如下:

enum _MediaQueryAspect {
/// Specifies the aspect corresponding to [MediaQueryData.size].
size,
/// Specifies the aspect corresponding to [MediaQueryData.orientation].
orientation,
/// Specifies the aspect corresponding to [MediaQueryData.devicePixelRatio].
devicePixelRatio,
/// Specifies the aspect corresponding to [MediaQueryData.textScaleFactor].
textScaleFactor,
/// Specifies the aspect corresponding to [MediaQueryData.textScaler].
textScaler,
/// Specifies the aspect corresponding to [MediaQueryData.platformBrightness].
platformBrightness,
/// Specifies the aspect corresponding to [MediaQueryData.padding].
padding,
/// Specifies the aspect corresponding to [MediaQueryData.viewInsets].
viewInsets,
/// Specifies the aspect corresponding to [MediaQueryData.systemGestureInsets].
systemGestureInsets,
/// Specifies the aspect corresponding to [MediaQueryData.viewPadding].
viewPadding,
/// Specifies the aspect corresponding to [MediaQueryData.alwaysUse24HourFormat].
alwaysUse24HourFormat,
/// Specifies the aspect corresponding to [MediaQueryData.accessibleNavigation].
accessibleNavigation,
/// Specifies the aspect corresponding to [MediaQueryData.invertColors].
invertColors,
/// Specifies the aspect corresponding to [MediaQueryData.highContrast].
highContrast,
/// Specifies the aspect corresponding to [MediaQueryData.onOffSwitchLabels].
onOffSwitchLabels,
/// Specifies the aspect corresponding to [MediaQueryData.disableAnimations].
disableAnimations,
/// Specifies the aspect corresponding to [MediaQueryData.boldText].
boldText,
/// Specifies the aspect corresponding to [MediaQueryData.navigationMode].
navigationMode,
/// Specifies the aspect corresponding to [MediaQueryData.gestureSettings].
gestureSettings,
/// Specifies the aspect corresponding to [MediaQueryData.displayFeatures].
displayFeatures,
/// Specifies the aspect corresponding to [MediaQueryData.supportsShowingSystemContextMenu].
supportsShowingSystemContextMenu,
}

也就是可以透過 MediaQuery 得到 sizeorientation,而這次要的就是 platformBrightness,以 size 來說,在 iOS 中,我們常用 UIScreen.main.bounds 去取得手機相關大小資訊,在 Android 則是 DisplayMetrics。

在 Android 我們會這樣做:

val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

val windowMetrics = windowManager.currentWindowMetrics
val bounds = windowMetrics.bounds
val widthPixels = bounds.width()
val heightPixels = bounds.height()
Log.e("widthPixels", "onCreate: $widthPixels")
Log.e("heightPixels", "onCreate: $heightPixels")

同樣地要取得 bounds 之前要先取得 windowManager,才能夠獲取 current window metrics。

iOS

那麼,接下來是 Flutter 的成果,我們先看 iOS 的部份:

Android

再來看看 Android 的部份:

是不是很棒呢?

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet