본문 바로가기

개발/Study

[GoToLearn] Flutter CodeLab - MDC-103 Flutter:Material Theming with Color, Shape, Elevation, and Type

GoToLearn 4주차

GoToLearn 4주차 CodeLab과제인  MDC-103 Flutter:Material Theming with Color, Shape, Elevation, and Type을 진행해보겠습니다. 코드랩 제목을 보니 머터리얼 테마 관련 설정을 배워보는 시간인 것 같습니다. 그럼 시작해보도록 하겠습니다.

MDC-103 Flutter:Material Theming with Color, Shape, Elevation, and Type

https://codelabs.developers.google.com/codelabs/mdc-103-flutter#0

 

MDC-103 Flutter: 색상, 형태, 고도, 활자를 사용한 Material Theming  |  Google Codelabs

Flutter의 머티리얼 구성요소를 사용해 디자인으로 얼마나 쉽게 제품을 차별화하고 브랜드를 표현할 수 있는지 알아보세요.

codelabs.developers.google.com

소개

이번 코드랩에서 빌드할 항목

이번 코드랩은 MDC-102에 이어 Shirne 전자상거래 앱을 Color, Typography, Elevation, Shape, Layout을 사용해서 커스텀해봅니다. 아래 사진은 완성 시 나타나는 화면입니다.

사용하는 머터리얼 컴포넌트 및 하위 위젯

  • Theme - 테마
  • Typography - 서체
  • Elevation - 고도
  • Image list - 이미지 리스트

Flutter 환경 설정 및 Starter App

이번주도 환경설정은 패스하고, 지난 주에 진행했던 MDC-102에 이어 진행하도록 하겠습니다.


색상 변경

Colors.dart 파일 추가

Shirne 앱에서 사용할 Color를 정의할 colors.dart 파일을 생성하고 각 색상을 입력합니다.

import 'package:flutter/material.dart';

const kShrinePink50 = Color(0xFFFEEAE6);
const kShrinePink100 = Color(0xFFFEDBD0);
const kShrinePink300 = Color(0xFFFBB8AC);
const kShrinePink400 = Color(0xFFEAA4A4);

const kShrineBrown900 = Color(0xFF442B2D);

const kShrineErrorRed = Color(0xFFC5032B);

const kShrineSurfaceWhite = Color(0xFFFFFBFA);
const kShrineBackgroundWhite = Colors.white;

 

맞춤 색상 팔레트

아래 색상 테마는 디자이너가 만든 샘플이며 풍부한 팔레트를 위해 확장되었다고합니다. Shirne 앱의 색상은 2014 머터리얼 색상의 팔레트 색깔이 아니라고 하는데 어떤 의미인지 찾아보니 사이트(https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors) 가장 아래에 2014 머터리얼 디자인 컬러 팔레트가 나와있는것을 확인할 수 있었습니다. 해당 색에 일치하지 않는다는 의미인것 같습니다.

 

그리고 색상 기준으로 50, 100, 200, 300, 400... 900까지 Shade를 줄 수 있고 값이 올라 갈 수록 진해지는것을 확인할 수 있습니다. Shrine앱은 Pink 색상 기준 50, 100, 900, Brown 색상 기준 900을 사용한다고 하네요.

 

해당 색상들은 themeData 위젯으로 MaterialApp의 필드로 넣어주면 사용할 수 있습니다.

ThemeData.light() 설정

밝은 테마는 Flutter에 내장되어 있는 테마들 중 하나입니다. copyWith를 통해 밝은 테마의 값을 변경하여 사용하도록 해보겠습니다.

class ShrineApp extends StatelessWidget {
  const ShrineApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shrine',
      initialRoute: '/login',
      routes: {
        '/login': (BuildContext context) => const LoginPage(),
        '/': (BuildContext context) => const HomePage(),
      },
      // 변경된 테마 적용
      theme: _kShrineTheme,
    );
  }
}

final ThemeData _kShrineTheme = _buildShrineTheme();

ThemeData _buildShrineTheme() {
  final ThemeData base = ThemeData.light();

  // copyWith를 사용해서 아래 정의된 값 이외에는 변경하지 않고 그대로 사용
  return base.copyWith(
    colorScheme: base.colorScheme.copyWith(
      primary: kShrinePink100,
      onPrimary: kShrineBrown900,
      secondary: kShrineBrown900,
      error: kShrineErrorRed,
    ),
  );
}

 

위 코드랩까지 진행했을때 코드랩에 표시되는 결과와 색깔이 동일하지 않은데 뒤에서 맞추도록 해보고 일단은 진행하겠습니다.


서체 및 라벨 스타일 설정

사용자 정의 테마 설정

Rubik 글꼴을 추가하기 위해 pubspec.yaml 파일에 아래 코드를 추가 시킵니다. pub get을 시켜줍니다.

  fonts:
    - family: Rubik
      fonts:
        - asset: fonts/Rubik-Regular.ttf
        - asset: fonts/Rubik-Medium.ttf
          weight: 500

이후 login.dart 에서 Shrine 텍스트에 Text 테마를 추가 시켜 줍니다.

healine5는 더이상 사용하지 않아 headlineSmall로 대체하겠습니다.

Column(
  children: <Widget>[
    Image.asset('assets/diamond.png'),
    const SizedBox(height: 16.0),
    // style 추가
    Text('SHRINE', style: Theme.of(context).textTheme.headlineSmall,),
  ],
),

TextTheme을 반환하는 함수를 만들어 줍니다. deprecated 된 값들이 많아 변경해주도록 하겠습니다.

TextTheme.apply 함수를 통해 텍스트 테마의 특정 속성을 변경할 수 있는 방법을 사용합니다.

TextTheme _buildShrineTextTheme(TextTheme base) {
  return base.copyWith(
    // headline5
    headlineSmall: base.headlineSmall!.copyWith(
      fontWeight: FontWeight.w500,
    ),
    // headline6
    titleLarge: base.titleLarge!.copyWith(
      fontSize: 18.0,
    ),
    // caption
    bodySmall: base.bodySmall!.copyWith(
      fontWeight: FontWeight.w400,
      fontSize: 14.0,
    ),
    // bodyText1
    bodyLarge: base.bodyLarge!.copyWith(
      fontWeight: FontWeight.w500,
      fontSize: 16.0,
    ),
  ).apply(
    fontFamily: 'Rubik',
    displayColor: kShrineBrown900,
    bodyColor: kShrineBrown900,
  );
}

 

새로운 텍스트 테마 사용

새로운 텍스트 테마를 사용하기 위해 _buildShrineTheme() 함수에 테마에 대한 값을 넣어주도록 합니다.

ThemeData _buildShrineTheme() {
  final ThemeData base = ThemeData.light();
  return base.copyWith(
    colorScheme: base.colorScheme.copyWith(
      primary: kShrinePink100,
      onPrimary: kShrineBrown900,
      secondary: kShrineBrown900,
      error: kShrineErrorRed,
    ),
    // 텍스트 테마 추가
    textTheme: _buildShrineTextTheme(base.textTheme),
    textSelectionTheme: const TextSelectionThemeData(
      selectionColor: kShrinePink100,
    ),
  );
}

텍스트 축소 및 라벨 아래로 위치

라벨이 너무 커서 텍스트 크기를 줄여주고, 라벨이 아래에 위치하도록 수정해줍니다.

  Column(
    mainAxisAlignment: MainAxisAlignment.end,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Text(
        product.name,
        style: theme.textTheme.titleLarge,
        softWrap: true,
        overflow: TextOverflow.ellipsis,
        maxLines: 1,
      ),  
      const SizedBox(height: 4.0),
      Text(
      formatter.format(product.price),
        style: theme.textTheme.bodySmall,
      ),
  ],
),

텍스트 필드 테마 지정

InputDecorationTheme을 활용하여 텍스트 필드의 테마를 지정해주고, 기존에 있던 텍스트 필드의 fill 효과를 제거해줍니다.

inputDecorationTheme: const InputDecorationTheme(
  border: OutlineInputBorder(),
),
TextField(
  controller: _usernameController,
  decoration: const InputDecoration(
    labelText: 'Username',
  ),
),
const SizedBox(height: 12.0),
TextField(
  controller: _passwordController,
  decoration: const InputDecoration(
    labelText: 'Password',
  ),
  obscureText: true,
),

 

텍스트 필드에 텍스트를 입력 시 테투디와 플로팅 라벨이 기본색으로 렌더링되어 쉽게 눈에 띄지 않습니다. 이를 변경해보겠습니다.

ThemeData _buildShrineTheme() {
  final ThemeData base = ThemeData.light();
  return base.copyWith(
    colorScheme: base.colorScheme.copyWith(
      primary: kShrinePink100,
      onPrimary: kShrineBrown900,
      secondary: kShrineBrown900,
      error: kShrineErrorRed,
    ),
    textTheme: _buildShrineTextTheme(base.textTheme),
    textSelectionTheme: const TextSelectionThemeData(
      selectionColor: kShrinePink100,
    ),
    inputDecorationTheme: const InputDecorationTheme(
      border: OutlineInputBorder(),
      // 추가
      focusedBorder: OutlineInputBorder(
        borderSide: BorderSide(
          width: 2.0,
          color: kShrineBrown900,
        ),
      ),
      // 추가
      floatingLabelStyle: TextStyle(
        color: kShrineBrown900,
      ),
    ),
  );
}

 

또한 취소 버튼도 대비가 잘 되도록 변경을 진행합니다.

TextButton(
  onPressed: () {
    _usernameController.clear();
    _passwordController.clear();
  },
  child: const Text('CANCEL'),
  style: TextButton.styleFrom(
  foregroundColor: Theme.of(context).colorScheme.secondary,
  ),
),

고도 조정

Next 버튼 고도 변경

ElevatedButton의 기본 고도는 2.0입니다. 8.0으로 변경을 진행합니다.

ElevatedButton(
  onPressed: () {
    Navigator.pop(context);
  },
  child: const Text('NEXT'),
  style: TextButton.styleFrom(elevation: 8.0),
);

카드의 고도 변경

카드의 고도는 0으로 변경을 진행합니다.

elevation: 0.0,

Shape 추가

Shirne 앱은 8각 또는 직사각형 형태의 기하학 스타일로 되어있다고합니다. 그 모양을 추가해보도록 하겠습니다.

텍스트 필드의 모양 변경

const InputDecorationTheme(
  // CutCornerBorder 추가
  border: CutCornersBorder(),
  focusedBorder: CutCornersBorder(
    borderSide: BorderSide(
      width: 2.0,
      color: kShrineBrown900,
    ),
  ),
  floatingLabelStyle: TextStyle(
    color: kShrineBrown900,
  ),
),

취소, 다음 버튼에 모양 추가

TextButton(
  onPressed: () {
    _usernameController.clear();
    _passwordController.clear();
  },
  child: const Text('CANCEL'),
  style: TextButton.styleFrom(
      foregroundColor: Theme.of(context).colorScheme.secondary,
      shape: const BeveledRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(7.0)),
      )),
),
ElevatedButton(
  onPressed: () {
    Navigator.pop(context);
  },
  child: const Text('NEXT'),
  style: ElevatedButton.styleFrom(
      foregroundColor: kShrineBrown900,
      backgroundColor: kShrinePink100,
      elevation: 8.0,
      shape: const BeveledRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(7.0)),
    ),
  ),
),

레이아웃 변경

GridView를 AsymmetricView로 대체

코드랩에서 제공되는 AsymmetriceView를 사용하여 기존 GridView를 대체합니다.

body: AsymmetricView(
  products: ProductsRepository.loadProducts(Category.all),
),

다른 테마 사용해보기

색상 수정

색상을 보라색으로 변경한 뒤 테마 데이터만 변경하여 Shrine 앱이 어떻게 변경되는지 확인합니다.

const kShrinePurple = Color(0xFF5D1049);

ThemeData _buildShrineTheme() {
  final ThemeData base = ThemeData.light();
  return base.copyWith(
    colorScheme: base.colorScheme.copyWith(
      primary: kShrinePurple,
      secondary: kShrinePurple,
      error: kShrineErrorRed,
    ),
    scaffoldBackgroundColor: kShrineSurfaceWhite,
    textSelectionTheme: const TextSelectionThemeData(
      selectionColor: kShrinePurple,
    ),
    inputDecorationTheme: const InputDecorationTheme(
      border: CutCornersBorder(),
      focusedBorder: CutCornersBorder(
        borderSide: BorderSide(
          width: 2.0,
          color: kShrinePurple,
        ),
      ),
      floatingLabelStyle: TextStyle(
        color: kShrinePurple,
      ),
    ),
  );
}

수정 진행

코드랩의 완성 결과물이 코드랩에 있는 이미지와 다름을 확인하여 수정을 진행합니다.

AppBarTheme 지정

코드랩을 진행하다보니 Appbar의 색상이 들어가지 않는것을 확인했습니다.

appbarTheme를 추가하여 appbar 색상을 추가합니다.

appBarTheme: const AppBarTheme(
  color: kShrinePink100,
),

완성

코드랩을 완성화면 아래와 같은 결과물을 얻을 수 있습니다. 확실히 이뻐진 UI를 확인할 수 있네요.


후기

이번 코드랩에서는 머터리얼 디자인에서 Theme를 활용해서 화면을 꾸미는 방법을 배웠던 것 같습니다. 평소에 많이 보긴했지만 어떤식으로 사용하는지 어떻게 사용하는지 확인할 수 있는 계기였던 것 같습니다. 재밌네요 !

실습코드

https://github.com/wonyong-park/flutter_codelabs/tree/main/material-components-flutter-codelabs-101-starter

 

flutter_codelabs/material-components-flutter-codelabs-101-starter at main · wonyong-park/flutter_codelabs

flutter_codelabs_repo. Contribute to wonyong-park/flutter_codelabs development by creating an account on GitHub.

github.com