MDC-102 Flutter: 머티리얼 구조 및 레이아웃

1. 소개

logo_components_color_2x_web_96dp.png

머티리얼 구성요소(MDC)를 통해 개발자는 머티리얼 디자인을 구현할 수 있습니다. Google의 엔지니어와 UX 디자이너로 구성된 팀에서 만든 MDC는 수십 가지 아름다운 기능 UI 구성요소가 특징이며 Android, iOS, 웹, Flutter.material.io/develop에서 제공됩니다.

MDC-101 Codelab에서는 머티리얼 구성요소 두 가지(텍스트 입력란, 잉크 물결 효과가 있는 버튼)를 사용하여 로그인 페이지를 빌드했습니다. 이제 탐색, 구조, 데이터를 추가하여 이러한 기초를 확장해보겠습니다.

빌드할 항목

이 Codelab에서는 Shrine 앱(의류와 가정용품을 판매하는 전자상거래 앱)의 홈 화면을 빌드합니다. 다음 항목이 포함됩니다.

  • 상단 앱 바
  • 제품으로 가득 찬 그리드 목록

Android

iOS

상단 앱 바와 제품이 가득 찬 그리드가 있는 전자상거래 앱

상단 앱 바와 제품이 가득 찬 그리드가 있는 전자상거래 앱

이 Codelab의 Material Flutter 구성요소 및 하위 시스템

  • 상단 앱 바
  • 그리드
  • 카드

Flutter 개발 경험 수준을 평가해주세요.

초급 중급 고급

2. Flutter 개발 환경 설정

이 실습을 완료하려면 Flutter SDK편집기라는 두 가지 소프트웨어가 필요합니다.

다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.

  • 컴퓨터에 연결되어 있으며 개발자 모드로 설정된 실제 Android 또는 iOS 기기
  • iOS 시뮬레이터(Xcode 도구 설치 필요)
  • Android Emulator(Android 스튜디오 설정 필요)
  • 브라우저(디버깅 시 Chrome 필요)
  • Windows, Linux 또는 macOS 데스크톱 애플리케이션. 배포에 사용할 플랫폼에서 개발해야 합니다. 따라서 Windows 데스크톱 앱을 개발하려면 적절한 빌드 체인에 액세스할 수 있도록 Windows에서 개발해야 합니다. docs.flutter.dev/desktop에 운영체제별 요구사항이 자세히 설명되어 있습니다.

3. Codelab 시작 앱 다운로드

MDC-101에서 계속 진행

MDC-101을 완료했다면 이 Codelab을 위한 코드가 준비된 것입니다. 상단 앱 바 추가 단계로 건너뜁니다.

처음부터 새로 시작

시작 Codelab 앱 다운로드

시작 앱은 material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series 디렉터리에 있습니다.

...또는 GitHub에서 클론

이 Codelab을 GitHub에서 클론하려면 다음 명령어를 실행하세요.

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 102-starter_and_101-complete

프로젝트 열기 및 앱 실행

  1. 원하는 편집기에서 프로젝트를 엽니다.
  2. 선택한 편집기에 맞게 시작하기: 시험 운용의 '앱 실행'에 대한 안내를 따릅니다.

완료되었습니다. 기기에 MDC-101 Codelab의 Shrine 로그인 페이지가 표시됩니다.

Android

iOS

사용자 이름 및 비밀번호 입력란, 취소 및 다음 버튼이 있는 로그인 페이지

사용자 이름 및 비밀번호 입력란, 취소 및 다음 버튼이 있는 로그인 페이지

로그인 화면이 잘 표시되므로 이제 제품으로 앱을 채워보겠습니다.

4. 상단 앱 바 추가

여기서 '다음' 버튼을 클릭하면 '축하합니다'라고 쓰인 홈 화면을 확인할 수 있습니다. 훌륭합니다. 하지만 이제 사용자가 취해야 할 조치는 없으며 앱에서 어떤 위치에 있는지 알 수 없습니다. 이제 탐색을 추가해 보겠습니다.

머티리얼 디자인은 높은 수준의 사용성을 보장하는 탐색 패턴을 제공합니다. 가장 눈에 띄는 구성요소 중 하나는 상단 앱 바입니다.

탐색 기능을 제공하고 사용자가 다른 작업에 빠르게 액세스할 수 있도록 상단 앱 바를 추가해보겠습니다.

AppBar 위젯 추가

home.dart에서 AppBar를 Scaffold에 추가하고 강조표시된 const를 삭제합니다.

return const Scaffold(
  // TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
  ),

AppBar를 Scaffold의 appBar: 필드에 추가하면 완벽한 레이아웃이 무료로 제공되어 AppBar는 페이지 상단에, 본문은 그 아래에 유지됩니다.

프로젝트를 저장합니다. Shrine 앱을 업데이트할 때 다음을 클릭하여 홈 화면을 표시합니다.

Android

iOS

화면에 '해냈어!'가 표시됩니다.

화면에 '해냈어!'가 표시됩니다.

AppBar의 모양은 훌륭하지만 제목이 있어야 합니다.

텍스트 위젯 추가

home.dart에서 제목을 AppBar에 추가합니다.

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: const Text('SHRINE'),
    // TODO: Add trailing buttons (102)

프로젝트를 저장합니다.

Android

iOS

Shrine이 제목인 앱 바

Shrine이 제목인 앱 바

대다수 앱 바에는 제목 옆에 버튼이 있습니다. 앱에 메뉴 아이콘을 추가해보겠습니다.

선행 IconButton 추가

계속 home.dart에서 AppBar의 leading: 입력란 IconButton을 설정합니다. title: 입력란 앞에 배치하여 선행에서 후행 순서를 모방합니다.

    // TODO: Add buttons and title (102)
    leading: IconButton(
      icon: const Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

프로젝트를 저장합니다.

Android

iOS

제목으로 Shrine이 있고 햄버거 메뉴 아이콘이 있는 앱 바

제목으로 Shrine이 있고 햄버거 메뉴 아이콘이 있는 앱 바

메뉴 아이콘('햄버거'라고도 함)이 예상한 위치에 바로 표시됩니다.

제목 뒤쪽에 버튼을 추가할 수도 있습니다. Flutter에서는 이를 '작업'이라고 합니다.

작업 추가

IconButton을 두 개 더 추가할 수 있습니다.

제목 뒤 AppBar 인스턴스에 추가합니다.

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: const Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: const Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

프로젝트를 저장합니다. 홈 화면이 다음과 같이 표시됩니다.

Android

iOS

제목으로 Shrine, 햄버거 메뉴 아이콘, 후행 검색 및 맞춤설정 아이콘이 있는 앱 바

제목으로 Shrine, 햄버거 메뉴 아이콘, 후행 검색 및 맞춤설정 아이콘이 있는 앱 바

이제 앱에 선행 버튼과 제목, 오른쪽의 두 작업이 있습니다. 또한, 앱 바는 콘텐츠와 다른 레이어에 있음을 나타내는 엷은 그림자를 사용하여 고도를 표시합니다.

5. 그리드에 카드 추가

이제 앱에 구조가 생겼으므로 콘텐츠를 카드에 배치하여 구성해보겠습니다.

GridView 추가

이제 상단 앱 바 아래에 카드 하나를 추가해보겠습니다. 카드 위젯 자체에는 볼 수 있는 위치에 배치할 정보가 충분치 않아서 GridView 위젯에 캡슐화하려고 합니다.

Scaffold 본문의 중심을 GridView로 바꿉니다.

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: Build a grid of cards (102)
  children: <Widget>[Card()],
),

이제 이 코드를 압축해제합니다. GridView는 count() 생성자를 호출합니다. 표시되는 항목 수를 셀 수 있고 항목 수가 무한대가 아니기 때문입니다. 그러나 레이아웃을 정의하려면 더 많은 정보가 필요합니다.

crossAxisCount:는 전체 항목 수를 지정합니다. 열이 두 개 필요합니다.

padding: 입력란은 GridView의 4면에 모두 공간을 제공합니다. 물론 후행 또는 하단 면에서 패딩을 볼 수는 없습니다. 아직 그 옆에 GridView 하위 요소가 없기 때문입니다.

childAspectRatio: 입력란은 가로세로 비율(너비 나누기 높이)에 따라 항목 크기를 식별합니다.

기본적으로 GridView는 크기가 모두 같은 타일을 만듭니다.

카드가 하나 있지만 비어 있습니다. 카드에 하위 위젯을 추가해보겠습니다.

콘텐츠 배치

카드에는 이미지, 제목, 보조 텍스트에 관한 영역이 있어야 합니다.

GridView의 하위 요소를 업데이트합니다.

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    clipBehavior: Clip.antiAlias,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              const SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

이 코드는 하위 위젯을 세로로 배치하는 데 사용되는 열 위젯을 추가합니다.

crossAxisAlignment: field는 '텍스트를 앞 가장자리에 정렬'함을 의미하는 CrossAxisAlignment.start를 지정합니다.

AspectRatio 위젯은 제공되는 이미지의 종류와 상관없이 이미지의 모양을 결정합니다.

패딩은 측면에서 텍스트를 안쪽으로 조금 가져옵니다.

텍스트 위젯은 세로로 스택되며 그 사이에는 8포인트 빈 공간(SizedBox)이 있습니다. 패딩 내부에 보관하는 또 다른 을 만듭니다.

프로젝트를 저장합니다.

Android

iOS

이미지, 제목, 보조 텍스트가 있는 단일 항목

이미지, 제목, 보조 텍스트가 있는 단일 항목

이 미리보기에서는 모서리가 둥글고 그림자(카드의 고도를 표현함)가 있는 카드가 가장자리에서 삽입된 것을 확인할 수 있습니다. 전체 모양은 머티리얼에서 '컨테이너'라고 합니다. 컨테이너라는 실제 위젯 클래스와 혼동해서는 안 됩니다.

카드는 보통 다른 카드와 함께 컬렉션으로 표시됩니다. 그리드에 컬렉션으로 카드를 배치해봅니다.

6. 카드 컬렉션 만들기

화면에 여러 카드가 표시될 때마다 하나 이상의 컬렉션으로 그룹화됩니다. 컬렉션의 카드는 동일 평면상에 있습니다. 즉, 카드는 서로 동일한 휴면 고도를 공유합니다. 단, 카드를 선택하거나 드래그하는 경우는 예외이지만 여기서는 이러한 작업을 실행하지 않습니다.

카드를 컬렉션으로 곱하기

이제 카드가 GridView children: 입력란의 인라인으로 구성됩니다. 쉽게 읽을 수 없는 중첩 코드가 많습니다. 원하는 만큼 빈 카드를 많이 생성할 수 있는 함수로 추출하여 카드 목록을 반환해보겠습니다.

build() 함수 위에 새 비공개 함수를 만듭니다(밑줄로 시작하는 함수가 비공개 API임).

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) {
      return Card(
        clipBehavior: Clip.antiAlias,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            AspectRatio(
              aspectRatio: 18.0 / 11.0,
              child: Image.asset('assets/diamond.png'),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const <Widget>[
                  Text('Title'),
                  SizedBox(height: 8.0),
                  Text('Secondary Text'),
                ],
              ),
            ),
          ],
        ),
      );
    },
  );
  return cards;
}

생성된 카드를 GridView의 children 입력란에 할당합니다. GridView에 포함된 모든 항목을 새로운 이 코드로 바꿔야 합니다.

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // Replace
),

프로젝트를 저장합니다.

Android

iOS

이미지, 제목, 보조 텍스트가 있는 항목의 그리드

이미지, 제목, 보조 텍스트가 있는 항목의 그리드

카드가 있지만 아직 아무것도 표시되지 않습니다. 이제 제품 데이터를 추가할 차례입니다.

제품 데이터 추가

앱에는 이미지와 이름, 가격이 포함된 제품이 있습니다. 이미 카드에 있는 위젯에 제품을 추가해보겠습니다.

그런 다음 home.dart에서 새 패키지와 데이터 모델에 제공한 일부 파일을 가져옵니다.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/product.dart';
import 'model/products_repository.dart';

마지막으로 _buildGridCards()를 변경하여 제품 정보를 가져오고 이 데이터를 카드에서 사용합니다.

// TODO: Make a collection of cards (102)

// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      clipBehavior: Clip.antiAlias,
      // TODO: Adjust card heights (103)
      child: Column(
        // TODO: Center items on the card (103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: Adjust the box size (102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: Align labels to the bottom and center (103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: Change innermost Column (103)
                children: <Widget>[
                 // TODO: Handle overflowing labels (103)
                 Text(
                    product.name,
                    style: theme.textTheme.titleLarge,
                    maxLines: 1,
                  ),
                  const SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.titleSmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

참고: 아직 컴파일되거나 실행되지 않습니다. 한 가지를 더 변경해야 합니다.

또한 컴파일을 시도하기 전에 build() 함수를 변경하여 BuildContext_buildGridCards()에 전달합니다.

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

앱을 핫 리스타트합니다.

Android

iOS

이미지, 제품 제목, 가격이 포함된 상품 그리드

이미지, 제품 제목, 가격이 포함된 상품 그리드

아시겠지만 카드 사이에 세로 공간을 추가하지 않습니다. 이는 기본적으로 상단과 하단에 4의 여백이 있기 때문입니다.

프로젝트를 저장합니다.

제품 데이터가 표시되지만 이미지 주위에 추가 공간이 있습니다. 이미지는 이 경우 기본적으로 .scaleDownBoxFit으로 그려집니다. 이를 .fitWidth로 변경하여 이미지를 조금 확대하고 추가 공백을 삭제합니다.

BoxFit.fitWidth 값을 사용하여 이미지에 fit: 입력란을 추가합니다.

  // TODO: Adjust the box size (102)
  fit: BoxFit.fitWidth,

Android

iOS

잘린 이미지, 제품 제목, 가격이 포함된 상품 그리드

잘린 이미지, 제품 제목, 가격이 포함된 상품 그리드

이제 제품이 앱에 완벽하게 표시됩니다.

7. 축하합니다.

앱에는 사용자를 로그인 화면에서 제품을 볼 수 있는 홈 화면으로 안내하는 기본적인 흐름이 있습니다. 코드 몇 줄만으로 상단 앱 바(제목과 버튼 세 개 포함)와 카드(앱 콘텐츠 표시)를 추가했습니다. 이제 홈 화면이 기본적인 구조와 실행 가능한 콘텐츠로 간단하고 기능적입니다.

다음 단계

상단 앱 바, 카드, 텍스트 필드, 버튼을 사용하여 이제 머티리얼 Flutter 라이브러리의 네 가지 핵심 구성요소를 사용했습니다. 머티리얼 구성요소 위젯 카탈로그에서 자세한 내용을 확인할 수 있습니다.

앱이 완전히 작동하지만 아직 특정 브랜드나 관점을 표현하지는 않습니다. MDC-103: 색상, 모양, 고도, 유형을 사용한 머티리얼 디자인 테마에서 이러한 구성요소의 스타일을 맞춤설정하여 생동감 있고 현대적인 브랜드를 표현합니다.

적절한 시간과 노력을 들여 이 Codelab을 완료할 수 있었습니다.

매우 동의함 동의함 보통 동의하지 않음 전혀 동의하지 않음

앞으로 머티리얼 구성요소를 계속 사용하고 싶습니다.

매우 동의함 동의함 보통 동의하지 않음 전혀 동의하지 않음