Flutter + Dart/Flutter + Dart 공부

테마 및 스타일 적용, 커스텀 위젯 제작 기초

GREEN나무 2025. 5. 16. 10:10
728x90

학습 목표

  • 앱 전체 테마 설정: ThemeData로 색상·글꼴·전반적 스타일 정의
  • 텍스트 스타일 관리: TextStyle로 주요 텍스트 속성 통일
  • 커스텀 위젯 제작: Stateless/Stateful 위젯 조합해 재사용 가능한 버튼·리스트 아이템 만들기

학습전 둘러보기

카테고리 위젯·클래스 주요 속성·메서드·이벤트  사용 패키지
권한 관리 SwitchListTile value, onChanged → Permission.request(), Permission.status permission_handler
Permission Permission.photos, Permission.location, Permission.calendar  
테마 & 언어 DropdownButton<ThemeMode> value, items, onChanged → ThemeMode.light/dark/system provider
DropdownButton<Locale> value, items, onChanged → Locale('ko'), Locale('en') flutter_localizations, intl
ThemeData primaryColor, accentColor, fontFamily, textTheme, cardTheme, inputDecorationTheme  
알림 설정 FlutterLocalNotificationsPlugin initialize(), zonedSchedule(), show() flutter_local_notifications
SwitchListTile value, onChanged  
showTimePicker() 시간 선택 다이얼로그  
달력 연동 TableCalendar firstDay, lastDay, focusedDay, onDaySelected, calendarStyle, headerStyle table_calendar
일기예보 FutureBuilder future, builder http
http.get(), jsonDecode() OpenWeatherMap API 호출  
CircularProgressIndicator 로딩 인디케이터  
지도 연동 GoogleMap initialCameraPosition, markers, onMapCreated google_maps_flutter
Marker markerId, position, infoWindow  
저장·동기화 ListTile + IconButton onTap, trailing shared_preferences, hive, OAuth/Firebase
캐시·데이터 관리 ListTile + AlertDialog showDialog(), AlertDialog(actions: [...]) 기본 Flutter
폰트 & 텍스트 ThemeData(fontFamily) 전역 폰트 설정  
Slider 또는 DropdownButton 텍스트 크기 조절 기본 Flutter
계정 관리 ListTile, CircleAvatar 프로필 사진, onTap → 화면 전환 Firebase Auth, REST API
일기 보안 SwitchListTile value, onChanged → flutter_secure_storage, local_auth flutter_secure_storage, local_auth
  • 이벤트 처리: 대부분 위젯의 onChanged, onTap, onPressed 콜백
  • 상태 관리: provider 또는 riverpod 와 ChangeNotifier/StateNotifier 조합
  • 저장소: shared_preferences·hive 등으로 사용자 설정값 로컬 저장

 

🔷 앱 전체 테마 설정: ThemeData로 색상·글꼴·전반적 스타일 정의

1. ThemeData 기본 이해

ThemeData

  • 앱 전역 색상(primary, accent 등), 글꼴(family), 버튼·앱바·스낵바 테마 등을 한 곳에서 관리
  • 참고: 패키지 폰트 등록 방법
  • 예시 ( Flutter에서 앱 전체의 디자인 스타일을 정의하는 ThemeData 객체를 만들기 )
final ThemeData appTheme = ThemeData(
  primaryColor: Colors.indigo,
  accentColor: Colors.amber,
  fontFamily: 'NanumGothic',  // pubspec.yaml에 추가 필요 (패키지 폰트)  
  textTheme: TextTheme(
    headline6: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
    bodyText2: TextStyle(fontSize: 16),
  ),
  buttonTheme: ButtonThemeData(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    buttonColor: Colors.indigo,
  ),
  snackBarTheme: SnackBarThemeData(
    backgroundColor: Colors.grey[800],
    contentTextStyle: TextStyle(color: Colors.white),
  ),
  tabBarTheme: TabBarTheme(
    labelColor: Colors.amber,
    unselectedLabelColor: Colors.white70,
  ),
);
  • primaryColor: 앱의 주 색상으로 indigo(남색)를 사용합니다.
  • accentColor: 강조 색상으로 amber(호박색)를 지정합니다.
  • fontFamily: 'NanumGothic' 폰트를 사용하며, pubspec.yaml에 추가해야 합니다.
  • textTheme: 텍스트의 기본 스타일을 설정합니다.
    • headline6: 크기 20, 볼드체로 제목 스타일.
    • bodyText2: 크기 16의 일반 텍스트.
  • buttonTheme: 버튼의 모양과 색상.
    • 폼은 둥근 모서리(반지름 8).
    • 버튼 색상은 indigo.
  • snackBarTheme: 스낵바(알림창)의 배경과 텍스트 스타일.
    • 배경은 짙은 회색(Colors.grey[800]).
    • 텍스트 색상은 흰색.
  • tabBarTheme: 탭바의 활성/비활성 레이블 색상.
    • 선택된 탭은 amber.
    • 비활성 탭은 연한 흰색(Colors.white70).

이렇게 하면 앱의 전체 디자인을 일관되게 꾸밀 수 있어요!


2. 글꼴(Font) 적용

  • pubspec.yaml에 TTF/OTF 파일 등록 
    • flutter > fonts: Flutter 앱에서 사용할 폰트 목록입니다.
    • family: NanumGothic: 폰트 이름을 "NanumGothic"으로 지정합니다.
    • fonts > asset: 폰트 파일이 저장된 위치를 지정합니다. 여기서는 assets/fonts/NanumGothic-Regular.ttf라는 폰트 파일을 사용합니다.
    즉, 이 설정은 "NanumGothic"라는 폰트를 앱에 적용하기 위해 폰트 파일 경로를 지정하는 역할을 합니다.
  • flutter: fonts: - family: NanumGothic fonts: - asset: assets/fonts/NanumGothic-Regular.ttf
  • ThemeData(fontFamily: 'NanumGothic') 로 전역 설정 

3. Theme 접근·사용

https://docs.flutter.dev/cookbook/design/themes

 

Theme.of(context) 로 현재 테마 정보 가져오기

Text(
  '안녕하세요',
  style: Theme.of(context).textTheme.headline6,
);

 

다크/라이트 모드

1. 프로젝트 기본 설정

MaterialApp(
  theme: ThemeData.light(),    // 라이트 모드 테마
  darkTheme: ThemeData.dark(), // 다크 모드 테마
  themeMode: ThemeMode.system, // 시스템 설정을 따름
);

- MaterialApp 위젯만 잘 활용하면 앱 전체 테마 관리를 한눈에 할 수 있습니다.

-  theme: 라이트 모드일 때 적용될 디자인

-  darkTheme: 다크 모드일 때 적용될 디자인

-  themeMode

  • system : 기기 설정과 동기화
  • light : 항상 라이트 모드
  • dark : 항상 다크 모드

2. 테마 상태 관리 클래스 만들기

  • 사용자 선택을 반영하려면 상태 관리가 필요합니다. 여기서는 ChangeNotifier 기반의 간단한 클래스를 사용합니다.
    import 'package:flutter/material.dart';
    
    class ThemeNotifier with ChangeNotifier {
      bool _isDarkMode = false;    // 초기 상태: 라이트
    
      bool get isDarkMode => _isDarkMode;
    
      void toggleTheme() {
        _isDarkMode = !_isDarkMode;
        notifyListeners();
      }
    }

  • _isDarkMode : 현재 테마 상태 저장
  • toggleTheme() : 값을 반전시키고 구독 위젯에 변경 알림

3. Provider로 연결하기

  • pubspec.yaml에 provider 추가 후, 최상위에 ChangeNotifierProvider를 감싸 줍니다.
    dependencies:
      flutter:
        sdk: flutter
      provider: ^6.0.0
    
    void main() {
      runApp(
        ChangeNotifierProvider(
          create: (_) => ThemeNotifier(),
          child: MyApp(),
        ),
      );
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final themeNotifier = Provider.of<ThemeNotifier>(context);
    
        return MaterialApp(
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          themeMode: themeNotifier.isDarkMode
              ? ThemeMode.dark
              : ThemeMode.light,
          home: MyHomePage(),
        );
      }
    }
    
    • Provider.of<ThemeNotifier>(context)로 상태 읽기
    • themeMode에 반영해 전체 테마 즉시 변경

4. 사용자 인터페이스에 스위치 추가하기

    • 설정 화면이나 앱바에 토글 스위치를 두면 사용자가 쉽게 전환할 수 있습니다.
      class MyHomePage extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          final notifier = Provider.of<ThemeNotifier>(context);
      
          return Scaffold(
            appBar: AppBar(title: Text('테마 설정')),
            body: Center(
              child: SwitchListTile(
                title: Text('다크 모드'),
                value: notifier.isDarkMode,
                onChanged: (_) {
                  Provider.of<ThemeNotifier>(context, listen: false)
                      .toggleTheme();
                },
              ),
            ),
          );
        }
      }
      
      • SwitchListTile으로 직관적인 토글 UI
      • listen: false 옵션으로 불필요한 리빌드를 방지
       

+

      1. 초기값 저장 : shared_preferences에 사용자의 마지막 선택을 저장해 재시작해도 유지
      2. 부드러운 전환   AnimatedTheme 위젯을 감싸 테마 변경 시 페이드 효과 주기
      3. 커스텀 테마   색상·폰트를 직접 정의해 브랜드 아이덴티티 반영
      4. 테마별 리소스 교체   이미지, 아이콘을 다크/라이트 버전으로 나눠 적절히 로드

🔷 텍스트 스타일 관리: TextStyle로 주요 텍스트 속성 통일

Flutter 앱에서 일관된 텍스트 디자인을 유지하려면 TextStyle을 활용해 주요 텍스트 속성을 통일해야 합니다. 이를 통해 유지보수성과 가독성을 모두 높일 수 있습니다.

1.TextStyle 주요 속성

  • fontSize: 글자 크기 (e.g. 14.0)
  • fontWeight: 글자 굵기 (e.g. FontWeight.bold)
  • fontStyle: 기울임체 여부 (FontStyle.italic)
  • color: 글자 색상
  • letterSpacing: 자간 조절
  • wordSpacing: 단어 간격
  • height: 줄 높이(행간)
  • fontFamily: 폰트 패밀리

2. 개별 위젯에 적용하기

Text(
  '안녕하세요',
  style: TextStyle(
    fontSize: 16.0,
    fontWeight: FontWeight.w500,
    color: Colors.black87,
  ),
);

3. Theme에 통합해 전역 관리하기

  1. ThemeData의 textTheme 설정
final appTheme = ThemeData(
  textTheme: TextTheme(
    headline1: TextStyle(fontSize: 96, fontWeight: FontWeight.light),
    headline6: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
    bodyText2: TextStyle(fontSize: 16),
  ),
);
  1. MaterialApp에 적용
    MaterialApp(
      theme: appTheme,
      home: MyHomePage(),
    );
    
  2. Theme.of(context).textTheme 사용
    Text(
      '제목 텍스트',
      style: Theme.of(context).textTheme.headline6,
    );
    

4. 커스텀 TextStyle 묶음 정의

  • Styles 클래스 생성
class Styles {
  static const header = TextStyle(
    fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue);
  static const body = TextStyle(
    fontSize: 16, fontWeight: FontWeight.normal);
  static const caption = TextStyle(
    fontSize: 12, color: Colors.grey);
}
  • 위젯 적용
Text('본문', style: Styles.body);
Text('설명', style: Styles.caption);

다음은 요청하신 내용을 공부용 블로그 포맷에 맞춰 보기 좋고 실용적으로 정리한 예시입니다. 각 항목은 주제별로 묶고, 커스텀 위젯 제작 설명도 추가했습니다.


🔷 Flutter 실전 위젯 커스터마이징 가이드

Flutter 앱을 제작하면서 재사용 가능한 UI 컴포넌트를 만들고, 테마·애니메이션·권한 등 기능을 확장하는 방법을 정리했다.


📌 1. 커스텀 위젯 제작 – 구조화 & 재사용

✅ Stateless 위젯으로 기본 버튼 만들기

class PrimaryButton extends StatelessWidget {
  final String label;
  final VoidCallback onTap;
  const PrimaryButton({ required this.label, required this.onTap });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        primary: Theme.of(context).primaryColor,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
      ),
      onPressed: onTap,
      child: Text(label, style: TextStyle(color: Colors.white)),
    );
  }
}
  • label: 버튼에 표시할 텍스트
  • onTap: 버튼 클릭 시 실행할 콜백
  • UI 스타일은 테마 기반으로 통일감 유지

✅ Stateful 위젯을 활용한 할 일 리스트 항목

class TodoItem extends StatefulWidget {
  final String text;
  final VoidCallback onDelete;
  const TodoItem({ required this.text, required this.onDelete });

  @override
  _TodoItemState createState() => _TodoItemState();
}

class _TodoItemState extends State<TodoItem> {
  bool _checked = false;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Checkbox(
        value: _checked,
        onChanged: (v) => setState(() => _checked = v!),
      ),
      title: Text(widget.text),
      trailing: IconButton(
        icon: Icon(Icons.delete, color: Colors.redAccent),
        onPressed: widget.onDelete,
      ),
    );
  }
}
  • 체크박스 상태 _checked는 내부에서 관리
  • 삭제 버튼은 외부로 콜백 전달
  • ListTile 구성으로 레이아웃 일관성 확보

🎨 2. 테마 & 다국어 설정 – Provider 패턴

Provider 설정

dependencies:
  provider: ^6.0.0
  flutter_localizations:
flutter:
  uses-material-design: true
  generate: true

테마·언어 설정 모델

class SettingsNotifier with ChangeNotifier {
  ThemeMode themeMode = ThemeMode.system;
  Locale locale = Locale('ko');

  void setTheme(ThemeMode m) {
    themeMode = m;
    notifyListeners();
  }

  void setLocale(Locale l) {
    locale = l;
    notifyListeners();
  }
}

UI 예시

final settings = context.watch<SettingsNotifier>();

DropdownButton<ThemeMode>(
  value: settings.themeMode,
  items: ThemeMode.values.map((m) =>
    DropdownMenuItem(value: m, child: Text(m.name))).toList(),
  onChanged: settings.setTheme,
);
  • Provider로 상태 공유
  • UI 전역 설정을 컨트롤 가능

✨ 3. 애니메이션 적용

✅ Implicit 애니메이션

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  width: _expanded ? 200 : 100,
  height: 50,
  child: ElevatedButton(...),
)
  • 간단한 크기·색상 전환에 유용

✅ Explicit 애니메이션

AnimationController + Tween + AnimatedBuilder
  • 복잡한 트랜지션에 사용
  • Hero 위젯으로 화면 전환 강조 가능

📦 4. 카드 · 리스트 · 폼 스타일 커스터마이징

ThemeData 설정

ThemeData(
  cardTheme: CardTheme(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    elevation: 4,
  ),
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(),
    filled: true,
    fillColor: Colors.grey[100],
  ),
)
  • CardTheme, FormFieldTheme 활용으로 일관된 스타일링
  • ListView.separated 사용 시 dividerColor로 간격 조절

🔐 5. 권한 설정

permission_handler 설정

dependencies:
  permission_handler: ^10.2.0
Future<void> _checkPermission(Permission perm) async {
  final status = await perm.request();
}
SwitchListTile(
  title: Text('위치 권한'),
  value: false,
  onChanged: (_) => _checkPermission(Permission.location),
)
  • 권한 요청 후 상태에 따라 로직 처리 가능

🔔 6. 알림 설정

flutter_local_notifications 설정

dependencies:
  flutter_local_notifications: ^12.0.0
await _plugin.zonedSchedule(
  0,
  '일기 쓰기 알림',
  '오늘 하루를 기록해보세요!',
  tz.TZDateTime.local(...),
  const NotificationDetails(
    android: AndroidNotificationDetails('diary', 'Diary Reminders'),
  ),
  matchDateTimeComponents: DateTimeComponents.time,
);
  • 특정 시간대 반복 알림
  • 야간 모드 off 스위치 등 커스터마이징 가능

🗓 7. 달력 연동 (table_calendar)

dependencies:
  table_calendar: ^3.0.9
TableCalendar(
  firstDay: DateTime.utc(2020, 1, 1),
  lastDay: DateTime.utc(2030, 12, 31),
  focusedDay: DateTime.now(),
  calendarStyle: CalendarStyle(
    todayDecoration: BoxDecoration(color: Colors.blueAccent, shape: BoxShape.circle),
  ),
  onDaySelected: (d, f) {
    // 스케줄 상세 페이지로 이동
  },
)
  • 커스텀 데코레이션 가능
  • 선택일, 오늘 강조 스타일 분리

🌤 8. 날씨 위젯 – OpenWeatherMap API 연동

dependencies:
  http: ^0.13.4
  flutter_svg: ^1.0.3
final res = await http.get(Uri.parse('https://api.openweathermap.org/data/2.5/weather?q=Seoul&appid=YOUR_KEY'));
final data = jsonDecode(res.body);

Text('${data['main']['temp']}°C');
Text(data['weather'][0]['description']);
  • 날씨 아이콘은 flutter_svg로 표현 가능
  • 날씨 정보 시각화 시 LineChart 적용 가능

🗺 9. 지도 연동 – Google Maps

dependencies:
  google_maps_flutter: ^2.2.0
GoogleMap(
  initialCameraPosition: CameraPosition(target: LatLng(37.5665, 126.9780), zoom: 12),
  markers: {
    Marker(markerId: MarkerId('here'), position: LatLng(37.5665, 126.9780)),
  },
)
  • 커스텀 마커, 클러스터링, 경로 표시 가능
  • Theme.of(context) 활용으로 색상 통일 가능

🖼 10. 블로그 형식 레이아웃 팁

  • 비율 유지: AspectRatio, ClipRRect
  • 텍스트 흐름: Padding, RichText
  • 문서화: flutter_markdown 패키지 활용

✅ TIP

  • 커스텀 위젯으로 반복 UI 관리
  • 테마·언어·애니메이션은 앱 완성도에 직결
  • Flutter는 위젯 조합력과 상태 관리가 핵심

 


실습 과제

  1. PrimaryButton, SecondaryButton 커스텀 위젯 제작
  2. TodoItem 위젯 구현 및 삭제·완료 토글 기능 추가
  3. 전역 폰트, 스낵바, 탭바 스타일을 적용한 간단한 샘플 페이지 완성
  4. 앱 전역에 사용할 textTheme 커스터마이징
  5. Styles 클래스에 최소 5가지 스타일 정의 및 적용
  6. 다크 모드에서 텍스트 색상이 잘 보이도록 별도 스타일 설정

 


참고

https://docs.flutter.dev/cookbook/design/themes
https://docs.flutter.dev/cookbook/design/drawer
https://docs.flutter.dev/cookbook/design/package-fonts
https://docs.flutter.dev/cookbook/design/themes
https://docs.flutter.dev/cookbook/design/fonts
https://docs.flutter.dev/cookbook/design/snackbars
https://docs.flutter.dev/cookbook/design/tabs
https://docs.flutter.dev/cookbook/design/orientation
https://docs.flutter.dev/cookbook/design/fonts
https://docs.flutter.dev/cookbook/design/themes
https://docs.flutter.dev/cookbook/design/themes