[Flutter] Material Design 3 적용하기

Parrottkim·2023년 12월 6일
10
post-thumbnail

시작하기에 앞서

Flutter 3.16 릴리즈부터 Material Design 3가 기본적으로 활성화 되도록 변경되었습니다. Material Theme Builder를 이용하면 Flutter에서 쉽게 사용할 수 있도록 코드 형식으로 다운로드 받을 수 있습니다.

문제는 해당 코드에는 Light Theme, Dark Theme에서 사용되는 Color Scheme만 제공하고, 앱 전체적으로 다양한 상황에서 사용되는 Tonal Palettes에 관련된 색상에 대한 값은 제공되지 않는다는 점입니다.

그래서 이번 글에서는 Material Design 3의 Color Scheme, Tonal Palettes까지의 전체 색상을 Flutter에서 쉽게 구현하는 방법에 대해 알아보겠습니다.

Material Design 3 Colors

Source Color와 Key Colors

Material Design 3에서는 Source Color를 지정해주면 Source Color 색조와 채도를 조절해 5가지의 Key Color를 만들어냅니다.

이 5가지의 Key Color를 이용해 Tonal Palettes를 만들어내고, Tonal Palettes의 톤을 각각의 Color Role에 적용합니다.

파일 추가

새 Flutter 프로젝트를 생성 후, lib/theme.dart 파일을 생성하고, 5가지의 Key Colors 그리고 Error 색상을 ColorStyle 클래스로 추가합니다.

lib/theme.dart

class KeyColor {
  static Color primary = const Color(0xFF006971);
  static Color secondary = const Color(0xFF4A6365);
  static Color tertiary = const Color(0xFF505E7D);
  static Color neutral = const Color(0xFF5C5F5F);
  static Color neutralVariant = const Color(0xFF566061);
  static Color error = const Color(0xFFBA1A1A);
}

다음은, Tonal Palettes를 사용하기 위해 material_color_utilities 패키지를 추가합니다.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

...

	material_color_utilities: ^0.5.0

이제 Color 타입에서 사용할 확장 메소드를 추가합니다.

lib/theme.dart

extension Material3Palette on Color {
  Color tone(int tone) {
    assert(tone >= 0 && tone <= 100);
    final color = Hct.fromInt(value);
    final tonalPalette = TonalPalette.of(color.hue, color.chroma);
    return Color(tonalPalette.get(tone));
  }
}

이제 ColorStyle.primary.tone(40)과 같은 코드를 사용해 Primary 색상의 40에 할당된 톤 색상을 가져올 수 있습니다.

Light Theme에 해당하는 색상들을 추가합니다.

lib/theme.dart

ThemeData lightTheme = ThemeData(
  useMaterial3: true,
  scaffoldBackgroundColor: Colors.white,
  colorScheme: ColorScheme(
    brightness: Brightness.light,
    primary: KeyColor.primary,
    onPrimary: KeyColor.primary.tone(100),
    primaryContainer: KeyColor.primary.tone(90),
    onPrimaryContainer: KeyColor.primary.tone(10),
    secondary: KeyColor.secondary,
    onSecondary: KeyColor.secondary.tone(100),
    secondaryContainer: KeyColor.secondary.tone(90),
    onSecondaryContainer: KeyColor.secondary.tone(10),
    tertiary: KeyColor.tertiary,
    onTertiary: KeyColor.tertiary.tone(100),
    tertiaryContainer: KeyColor.tertiary.tone(90),
    onTertiaryContainer: KeyColor.tertiary.tone(10),
    error: KeyColor.error.tone(40),
    onError: KeyColor.error.tone(100),
    errorContainer: KeyColor.error.tone(90),
    onErrorContainer: KeyColor.error.tone(10),
    background: KeyColor.neutral.tone(99),
    onBackground: KeyColor.neutral.tone(10),
    surface: KeyColor.neutral.tone(99),
    onSurface: KeyColor.neutral.tone(10),
    surfaceVariant: KeyColor.neutralVariant.tone(90),
    onSurfaceVariant: KeyColor.neutralVariant.tone(30),
    outline: KeyColor.neutralVariant.tone(50),
    outlineVariant: KeyColor.neutralVariant.tone(80),
    inverseSurface: KeyColor.neutral.tone(90),
    inversePrimary: KeyColor.primary.tone(90),
    scrim: KeyColor.neutral.tone(0),
    shadow: KeyColor.neutral.tone(0),
  ),
);

사전에 정의할 TextTheme이 있는 경우 같이 추가해줍니다.

다음엔 main.dart에 Light Theme을 적용해줍니다.

lib/main.dart

void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: lightTheme,
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

결과

Color Role


Tone Palettes

Theme.of(context).colorScheme.primary // Primary Color
Theme.of(context).colorScheme.primary.tone(10) // Primary Color Tone 10

이제 Color Scheme, Tonal Palettes 까지의 전체 색상을 쉽게 사용할 수 있게 되었습니다.

사전 설정이 조금 복잡할 수도 있지만, var primary40 = Colors(0xFF006971)와 같이 Tonal Palettes 색상 별 변수를 등록하는 방식은 모든 Tonal Palettes의 색상의 Hex code 값을 직접 확인해서 작성해야 하기 때문에 상당히 귀찮은 작업이 될 수 있으며, 항상 외부 Class Import가 필요하기 때문에 분명한 이점이 있다고 생각합니다.

+ TextTheme 추가하기

Flutter의 TextTheme은 기본적으로 Material Design의 Typography 정의를 따르지만, 다음과 같이 실제 개발에서 사용할 때는 자간이나 줄 간격을 수정해서 사용해야 하는 경우가 생깁니다. 이럴 때, 사전에 수정된 Typography를 미리 적용하고 앱 전체에서 사용하려면 어떻게 해야할까요?

먼저 TextTheme을 추가해야 하는데, 위의 Typography는 자간을 퍼센티지로 설정해두었습니다. TextStyle에서 자간 속성인 letterSpacing은 실수로만 입력이 가능하기 때문에, 퍼센티지를 실수로 변환해주는 확장 메소드를 추가합니다.

lib/theme.dart

extension PercentageLetterSpacing on TextStyle {
  TextStyle withLetterSpacing(double percentage) {
    double letterSpacing = double.parse(((percentage / 100) * fontSize!)
        .toStringAsFixed(2)); // toStringAsFixed(2): to two decimal places
    return copyWith(letterSpacing: letterSpacing);
  }
}

다음은, TextTheme을 추가하고, ThemeData에 TextTheme을 적용합니다.

lib/theme.dart

TextTheme textTheme = TextTheme(
  displayLarge: TextStyle(
    fontSize: 57.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  displayMedium: TextStyle(
    fontSize: 45.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  displaySmall: TextStyle(
    fontSize: 36.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  headlineLarge: TextStyle(
    fontSize: 32.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  headlineMedium: TextStyle(
    fontSize: 28.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  headlineSmall: TextStyle(
    fontSize: 24.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  titleLarge: TextStyle(
    fontSize: 22.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  titleMedium: TextStyle(
    fontSize: 16.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  titleSmall: TextStyle(
    fontSize: 14.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  labelLarge: TextStyle(
    fontSize: 14.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  labelMedium: TextStyle(
    fontSize: 12.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  labelSmall: TextStyle(
    fontSize: 11.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  bodyLarge: TextStyle(
    fontSize: 16.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  bodyMedium: TextStyle(
    fontSize: 14.0,
    height: 1.2,
  ).withLetterSpacing(-4),
  bodySmall: TextStyle(
    fontSize: 12.0,
    height: 1.2,
  ).withLetterSpacing(-4),
);

ThemeData lightTheme = ThemeData(
  
  ...

  textTheme: textTheme,
);

// 일반적인 사용
Text(
  '${텍스트}',
  style: Theme.of(context).textTheme.displayLarge,
),

// 스타일 변경
Text(
  '${텍스트}',
  style: Theme.of(context).textTheme.displayLarge!.copyWith(
    fontWeight: FontWeight.w700,
  ),
),

다음과 같이 사용할 수 있습니다.

profile
Flutter developer

2개의 댓글

comment-user-thumbnail
2023년 12월 6일

아주 유용한 정보 감사합니다!

1개의 답글