Flutter에서 다크 모드를 어떻게 구현하나요?
질문
Flutter 앱에서 다크 모드를 구현하는 방법과 사용자 설정에 따라 테마를 자동으로 변경하는 방법을 알려주세요.
답변
Flutter에서 다크 모드를 구현하는 방법은 크게 시스템 설정에 따라 자동으로 테마를 변경하는 방법과 앱 내에서 사용자가 직접 테마를 선택할 수 있게 하는 방법이 있습니다. 두 가지 접근 방식을 모두 설명하겠습니다.
1. 기본 테마 설정하기
먼저 라이트 모드와 다크 모드에 사용할 테마를 정의합니다.
import 'package:flutter/material.dart';
// 라이트 테마 정의
ThemeData _lightTheme = ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
colorScheme: ColorScheme.light(
primary: Colors.blue,
secondary: Colors.blueAccent,
surface: Colors.white,
background: Colors.grey[100]!,
onBackground: Colors.black87,
onSurface: Colors.black87,
),
scaffoldBackgroundColor: Colors.grey[100],
appBarTheme: AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0,
),
textTheme: TextTheme(
headline6: TextStyle(color: Colors.black87),
bodyText2: TextStyle(color: Colors.black87),
),
iconTheme: IconThemeData(color: Colors.black87),
);
// 다크 테마 정의
ThemeData _darkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blueGrey[700],
colorScheme: ColorScheme.dark(
primary: Colors.blueGrey[700]!,
secondary: Colors.blueGrey[500]!,
surface: Colors.grey[850]!,
background: Colors.grey[900]!,
onBackground: Colors.white70,
onSurface: Colors.white70,
),
scaffoldBackgroundColor: Colors.grey[900],
appBarTheme: AppBarTheme(
backgroundColor: Colors.grey[850],
foregroundColor: Colors.white70,
elevation: 0,
),
textTheme: TextTheme(
headline6: TextStyle(color: Colors.white70),
bodyText2: TextStyle(color: Colors.white70),
),
iconTheme: IconThemeData(color: Colors.white70),
);
2. 시스템 설정에 따른 자동 테마 변경
사용자의 시스템 설정에 따라 자동으로 테마를 변경하고 싶다면, MaterialApp
에 theme
와 darkTheme
속성을 설정하면 됩니다.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '다크 모드 예제',
theme: _lightTheme, // 라이트 테마
darkTheme: _darkTheme, // 다크 테마
themeMode: ThemeMode.system, // 시스템 설정에 따름
home: HomePage(),
);
}
}
여기서 themeMode
는 다음 세 가지 값 중 하나를 가질 수 있습니다:
ThemeMode.system
: 시스템 설정에 따라 테마 결정 (기본값)ThemeMode.light
: 항상 라이트 테마 사용ThemeMode.dark
: 항상 다크 테마 사용
3. 사용자 선택에 따른 테마 변경 (수동 제어)
사용자가 앱 내에서 테마를 직접 변경할 수 있도록 하려면, 상태 관리를 통해 themeMode
를 제어해야 합니다. 여기서는 Provider 패키지를 사용한 예시를 보여드리겠습니다.
먼저 pubspec.yaml에 provider 패키지를 추가합니다:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.5
그런 다음 테마 상태를 관리할 Provider를 만듭니다:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode => _themeMode == ThemeMode.dark;
// 초기화 및 저장된 테마 모드 로드
Future<void> initialize() async {
final prefs = await SharedPreferences.getInstance();
final savedThemeMode = prefs.getString('themeMode');
if (savedThemeMode == 'light') {
_themeMode = ThemeMode.light;
} else if (savedThemeMode == 'dark') {
_themeMode = ThemeMode.dark;
} else {
_themeMode = ThemeMode.system;
}
notifyListeners();
}
// 테마 모드 변경 메서드
Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
notifyListeners();
// 선택한 테마 모드 저장
final prefs = await SharedPreferences.getInstance();
if (mode == ThemeMode.light) {
await prefs.setString('themeMode', 'light');
} else if (mode == ThemeMode.dark) {
await prefs.setString('themeMode', 'dark');
} else {
await prefs.setString('themeMode', 'system');
}
}
// 시스템 모드로 설정
Future<void> useSystemTheme() async {
await setThemeMode(ThemeMode.system);
}
// 라이트 모드로 설정
Future<void> useLightTheme() async {
await setThemeMode(ThemeMode.light);
}
// 다크 모드로 설정
Future<void> useDarkTheme() async {
await setThemeMode(ThemeMode.dark);
}
// 테마 토글 (라이트 <-> 다크)
Future<void> toggleTheme() async {
if (_themeMode == ThemeMode.light) {
await useDarkTheme();
} else {
await useLightTheme();
}
}
}
이제 앱에 Provider를 적용합니다:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ThemeProvider 초기화
final themeProvider = ThemeProvider();
await themeProvider.initialize();
runApp(
ChangeNotifierProvider(
create: (_) => themeProvider,
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
title: '다크 모드 예제',
theme: _lightTheme,
darkTheme: _darkTheme,
themeMode: themeProvider.themeMode,
home: HomePage(),
);
},
);
}
}
4. 테마 선택 UI 구현하기
사용자가 테마를 변경할 수 있는 UI를 구현합니다:
class ThemeSelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
appBar: AppBar(title: Text('테마 설정')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'테마 모드 선택',
style: Theme.of(context).textTheme.headline6,
),
SizedBox(height: 16),
RadioListTile<ThemeMode>(
title: Text('시스템 설정 사용'),
value: ThemeMode.system,
groupValue: themeProvider.themeMode,
onChanged: (value) {
themeProvider.setThemeMode(value!);
},
),
RadioListTile<ThemeMode>(
title: Text('라이트 모드'),
value: ThemeMode.light,
groupValue: themeProvider.themeMode,
onChanged: (value) {
themeProvider.setThemeMode(value!);
},
),
RadioListTile<ThemeMode>(
title: Text('다크 모드'),
value: ThemeMode.dark,
groupValue: themeProvider.themeMode,
onChanged: (value) {
themeProvider.setThemeMode(value!);
},
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
themeProvider.toggleTheme();
},
icon: Icon(themeProvider.isDarkMode
? Icons.light_mode
: Icons.dark_mode),
label: Text(themeProvider.isDarkMode
? '라이트 모드로 전환'
: '다크 모드로 전환'),
),
],
),
),
);
}
}
5. 현재 테마에 따른 색상 사용하기
위젯에서 현재 테마의 색상을 사용하려면 다음과 같이 Theme.of(context)
를 활용합니다:
Widget build(BuildContext context) {
// 현재 테마의 색상 스킴 가져오기
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: Text('다크 모드 예제'),
actions: [
IconButton(
icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: () {
Provider.of<ThemeProvider>(context, listen: false).toggleTheme();
},
),
],
),
body: Container(
color: colorScheme.background,
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'현재 테마 모드',
style: textTheme.headline6,
),
SizedBox(height: 8),
Text(
isDark ? '다크 모드' : '라이트 모드',
style: textTheme.subtitle1,
),
SizedBox(height: 24),
Card(
color: colorScheme.surface,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'카드 내용',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
),
),
SizedBox(height: 8),
Text(
'현재 테마에 맞는 색상으로 표시됩니다.',
style: TextStyle(
color: colorScheme.onSurface.withOpacity(0.8),
),
),
],
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ThemeSelectionScreen()),
);
},
child: Icon(Icons.settings),
),
);
}
6. 특정 위젯에만 다른 테마 적용하기
특정 위젯에만 다른 테마를 적용하고 싶다면 Theme
위젯을 사용합니다:
Theme(
// 현재 테마를 기반으로 특정 속성만 변경
data: Theme.of(context).copyWith(
cardColor: Colors.amber,
textTheme: Theme.of(context).textTheme.copyWith(
bodyText1: TextStyle(color: Colors.purple),
),
),
child: Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('이 카드는 커스텀 테마를 사용합니다.'),
),
),
)
7. 테마 모드 감지하기
현재 테마 모드가 변경되었을 때 반응해야 하는 경우가 있습니다. 이때는 MediaQuery
를 사용하여 현재 밝기 모드를 감지할 수 있습니다:
Widget build(BuildContext context) {
final brightness = MediaQuery.of(context).platformBrightness;
final isDarkMode = brightness == Brightness.dark;
return Text(
'현재 시스템 테마는 ${isDarkMode ? '다크 모드' : '라이트 모드'}입니다.',
);
}
8. 다크 모드에 최적화된 이미지 사용하기
다크 모드와 라이트 모드에 따라 다른 이미지를 표시하고 싶다면:
Image.asset(
Theme.of(context).brightness == Brightness.dark
? 'assets/images/logo_dark.png'
: 'assets/images/logo_light.png',
width: 200,
height: 100,
)
결론
Flutter에서 다크 모드 구현은 ThemeData
와 ThemeMode
를 활용하여 비교적 간단하게 할 수 있습니다. 시스템 설정에 따른 자동 전환과 앱 내 테마 선택 기능을 모두 제공하면 사용자 경험을 향상시킬 수 있습니다.
다크 모드를 구현할 때 고려해야 할 사항:
- 색상 대비를 적절히 유지하여 접근성을 보장
- 텍스트와 배경 간의 충분한 대비 제공
- 같은 색상이라도 다크 모드에서는 채도와 명도 조정 필요
- 그림자와 입체감 표현을 위한 색상 조정
- 사용자 선택을 유지하기 위한 로컬 스토리지 활용
이러한 점들을 고려하여 구현하면 사용자에게 쾌적한 다크 모드 경험을 제공할 수 있습니다.