guricode

Flutter ThemeExtension 본문

앱/Flutter&Dart

Flutter ThemeExtension

agentrakugaki 2025. 8. 18. 19:22

 

Flutter에서 테마를 꾸밀 때, 기본적으로 제공하는 ThemeData만으로는 내가 원하는 모든 색상이나 스타일을 정의하기 어려운 경우가 많다. 그래서 Flutter는 ThemeExtension이라는 기능을 제공해서 직접 테마 속성을 확장할 수 있도록 해준다.


1. ThemeExtension 이란?

왜 필요한가?

ThemeData의 기본 속성만으로는 앱 전체에 걸쳐 쓰는 색상(main, sub 등)을 정의하기에 부족하다. 그래서 ThemeData.extensions 라는 확장 필드를 통해 직접 색상이나 스타일을 테마에 추가할 수 있다.

ThemeData(
  extensions: <ThemeExtension<dynamic>>[
    AppThemeExtension(
      main: Colors.red,
      mainLight: ..., // 사용자 정의 색상
      ...
    )
  ]
);

어떻게 구현하는가?

ThemeExtension<T>상속한 뒤, 제네릭 인자로 자기 자신을 넘긴다:

class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
  ...
}

이렇게 하면 아래 2가지 메서드를 반드시 재정의해야 한다:

  • copyWith() – 일부 값을 변경한 복사본 생성
  • lerp() – 애니메이션 중간값 계산 (색상 부드럽게 전환)

2. 실습 코드 정리

1) custom_theme.dart

class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
  final Color main;
  final Color mainLight;
  final Color sub;
  final Color background;

  AppThemeExtension({
    required this.main,
    required this.mainLight,
    required this.sub,
    required this.background,
  });

  @override
  AppThemeExtension copyWith({
    Color? main,
    Color? mainLight,
    Color? sub,
    Color? background,
  }) {
    return AppThemeExtension(
      main: main ?? this.main,
      mainLight: mainLight ?? this.mainLight,
      sub: sub ?? this.sub,
      background: background ?? this.background,
    );
  }

  @override
  AppThemeExtension lerp(AppThemeExtension? other, double t) {
    if (other == null) return this;

    return AppThemeExtension(
      main: Color.lerp(main, other.main, t)!,
      mainLight: Color.lerp(mainLight, other.mainLight, t)!,
      sub: Color.lerp(sub, other.sub, t)!,
      background: Color.lerp(background, other.background, t)!,
    );
  }
}

class LightTheme extends AppThemeExtension {
  LightTheme()
      : super(
          main: Colors.red,
          mainLight: Color(0xAAFF0000),
          sub: Color(0xFFFF0000),
          background: Colors.white,
        );
}

class DarkTheme extends AppThemeExtension {
  DarkTheme()
      : super(
          main: Color(0xFF0000FF),
          mainLight: Color(0xAAAA0000),
          sub: Color(0xFFFF00FF),
          background: Colors.black,
        );
}

2) theme.dart

final lightTheme = _theme(Brightness.light, LightTheme());
final darkTheme = _theme(Brightness.dark, DarkTheme());

ThemeData _theme(Brightness brightness, AppThemeExtension ext) {
  return ThemeData(
    brightness: brightness,
    useMaterial3: true,
    scaffoldBackgroundColor: ext.background,
    colorScheme: ColorScheme.fromSeed(
      brightness: brightness,
      seedColor: ext.main,
    ),
    extensions: [ext],
  );
}

// BuildContext 확장
extension BuildContextExtention on BuildContext {
  ThemeData get theme => Theme.of(this);
  AppThemeExtension get appColor => theme.extension<AppThemeExtension>()!;
}

여기서 BuildContextExtention 에 대해 간략하게 알아보자

BuildContextExtension 이란?

BuildContextExtension은 Dart의 extension 기능을 활용해 BuildContext 클래스에 새로운 기능을 붙인 확장 클래스다.
기존 Flutter 프레임워크의 BuildContext를 그대로 두고, 별도의 기능을 덧붙일 수 있도록 해준다.

 

  1. context.theme → Theme.of(context)와 같다.
  2. context.appColor → Theme.of(context).extension<AppThemeExtension>()!와 같다.

 

사용하면 좋은 이유

  • 매번 Theme.of(context) 혹은 Theme.of(context).extension<AppThemeExtension>()!를 호출하는 것이 코드가 길고 불편하기 때문이다.
  • View 단에서 테마에 접근하는 코드를 간결하게 만들고, 유지보수성을 높이기 위해 사용하는 것이다.
  • BuildContext는 Flutter 전역에서 거의 항상 접근 가능한 객체이기 때문에, 그에 기능을 덧붙이면 매우 강력한 효과를 준다.

각각의 기능 설명

1. ThemeData get theme => Theme.of(this);

  • 현재 context에 연결된 테마 데이터를 반환하는 getter다.
  • this는 BuildContext 자체를 가리키며, 결국 Theme.of(context)와 동일하다.
  • 이 확장 메서드로 인해 context.theme.primaryColor 같은 표현이 가능해진다.

2. AppThemeExtension get appColor => theme.extension<AppThemeExtension>()!;

  • ThemeData 내부에 정의된 ThemeExtension 객체를 꺼내서 반환한다.
  • <AppThemeExtension>은 제네릭으로 내가 만든 테마 확장 클래스다.
  • !는 null safety에서 null이 아님을 보장해주는 연산자다. 반드시 extensions에 이 타입이 포함돼 있어야 한다.

 실사용 흐름

  1. ThemeData에 AppThemeExtension을 등록해 둔다.
  2. View에서는 context.appColor.main, context.appColor.sub 등으로 간편하게 접근한다.
  3. 테마가 바뀔 경우에도 lerp 메서드를 통해 자연스럽게 애니메이션으로 전환된다.
  4. 이 모든 과정이 BuildContextExtension을 통해 훨씬 가독성 좋게 표현된다.

 

3) main.dart

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: ThemeMode.dark, // 다크모드 우선 적용
      theme: lightTheme,
      darkTheme: darkTheme,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ListView(
        children: [
          Container(height: 100, color: context.appColor.main),
          Container(height: 100, color: context.appColor.mainLight),
          Container(height: 100, color: context.appColor.sub),
        ],
      ),
    );
  }
}

 

 

 

 

 


3. 핵심 개념 요약

요소 설명
ThemeExtension<T> 테마 확장을 위한 추상 클래스 (T는 자기 자신을 넣음)
copyWith() 테마 인스턴스를 복사할 때 사용
lerp() 테마 전환 시 애니메이션 보간을 위해 사용
extensions: [...] ThemeData에 내가 만든 테마 확장 추가
context.extension<T>() 내가 만든 확장에 접근하는 방법
extension BuildContextExtention context에서 theme, appColor 쉽게 접근

3줄요약

  • ThemeExtension을 사용하면 내가 원하는 색상이나 속성을 테마에 자유롭게 추가할 수 있음
  • 테마를 커스터마이징할 수 있어 디자인의 일관성과 유지보수성 모두 향상됨
  • lerp, copyWith를 활용하면 다크/라이트 테마 전환도 부드럽게 처리 가능