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

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의 MDC 구성요소

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

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

초급 중급 고급

시작하기 전에

Flutter를 사용하여 모바일 앱을 개발하려면 다음을 실행해야 합니다.

  1. Flutter SDK를 다운로드하고 설치합니다.
  2. Flutter SDK를 사용하여 PATH를 업데이트합니다.
  3. Flutter와 Dart 플러그인이 포함된 Android 스튜디오나 선호하는 편집기를 설치합니다.
  4. Android Emulator, iOS 시뮬레이터(Xcode가 포함된 Mac 필요)를 설치하거나 실제 기기를 사용합니다.

Flutter 설치에 관한 자세한 내용은 시작하기: 설치를 참고하세요. 편집기를 설정하려면 시작하기: 편집기 설정을 참고하세요. Android Emulator를 설치할 때 최신 시스템 이미지가 있는 Pixel 3 휴대전화와 같은 기본 옵션을 자유롭게 사용하세요. VM 가속을 사용 설정하는 것이 좋지만 필수는 아닙니다. 위 네 단계를 완료한 후 Codelab으로 돌아가면 됩니다. 이 Codelab을 완료하려면 플랫폼(Android나 iOS) 하나에만 Flutter를 설치하면 됩니다.

Flutter SDK가 올바른 상태인지 확인

이 Codelab을 진행하기 전에 SDK가 올바른 상태인지 확인하세요. 이전에 Flutter SDK를 설치한 경우 flutter upgrade를 사용하여 SDK가 최신 상태인지 확인합니다.

 flutter upgrade

flutter upgrade를 실행하면 flutter doctor.가 자동으로 실행됩니다. 최신 Flutter가 설치되어 있어 업그레이드가 필요하지 않으면 flutter doctor를 수동으로 실행합니다. 그러면 설정 완료를 위해 설치해야 하는 종속 항목이 있는지 보고됩니다. 관련 없는 체크표시(예: iOS용으로 개발하지 않으려는 경우 Xcode)는 무시해도 됩니다.

 flutter doctor

자주 묻는 질문(FAQ)

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

프로젝트 설정

다음 안내에서는 개발자가 Android 스튜디오(IntelliJ)를 사용한다고 가정합니다.

프로젝트 열기

1. Android 스튜디오를 엽니다.

2. 시작 화면이 표시되면 Open an existing Android Studio project를 클릭합니다.

3. material-components-flutter-codelabs/mdc_100_series 디렉터리로 이동하여 Open을 클릭합니다. 프로젝트가 열립니다. 프로젝트를 한 번 빌드할 때까지는 Dart Analysis에 표시되는 오류를 무시해도 됩니다.

4. 메시지가 표시되면 다음 단계를 따릅니다.

  • 플랫폼 및 플러그인 업데이트나 FlutterRunConfigurationType을 설치합니다.
  • Dart 또는 Flutter SDK가 구성되지 않은 경우 Flutter 플러그인용 Flutter SDK 경로를 설정합니다.
  • Android 프레임워크를 구성합니다.
  • 'Get dependencies'나 'Run ‘flutter packages get''을 클릭합니다.

그런 다음 Android 스튜디오를 다시 시작합니다.

시작 앱 실행

다음 안내에서는 Android Emulator 또는 기기에서 테스트한다고 가정하지만 Xcode를 설치한 경우 iOS 시뮬레이터 또는 기기에서 테스트해도 됩니다.

1. 기기나 에뮬레이터를 선택합니다. Android Emulator가 아직 실행되지 않는다면 Tools -> Android -> AVD Manager를 선택하여 가상 기기를 만들고 에뮬레이터를 시작합니다. AVD가 이미 있는 경우 다음 단계와 같이 Android 스튜디오의 기기 선택기에서 바로 에뮬레이터를 시작할 수 있습니다. iOS 시뮬레이터의 경우 아직 실행되고 있지 않다면 Flutter Device Selection -> Open iOS Simulator를 선택하여 개발 머신에서 시뮬레이터를 실행합니다.

2. Flutter 앱을 시작합니다.

  • 편집기 화면 상단에서 Flutter Device Selection 드롭다운 메뉴를 찾아 기기(예: <version>용으로 빌드된 iPhone SE 또는 Android SDK)를 선택합니다.
  • 재생 아이콘()을 누릅니다.

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

Android

iOS

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

여기서 '다음' 버튼을 클릭하면 '축하합니다'라고 쓰인 홈 화면을 확인할 수 있습니다. 훌륭합니다. 그러나 이제 사용자는 실행할 작업이 없거나 앱 내에서 사용자의 위치를 파악할 수 없습니다. 이러한 문제를 해결하기 위해 탐색을 추가해보겠습니다.

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

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

AppBar 위젯 추가

home.dart에서 AppBar를 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: Text('SHRINE'),
    // TODO: Add trailing buttons (102)

프로젝트를 저장합니다.

Android

iOS

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

선행 IconButton 추가

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

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

프로젝트를 저장합니다.

Android

iOS

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

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

작업 추가

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

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

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

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

Android

iOS

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

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

GridView 추가

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

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

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: 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는 각 하위 요소의 너비를 다음과 같이 계산합니다. ([width of the entire grid] - [left padding] - [right padding]) / number of columns. 값을 적용하면 다음과 같습니다. ([width of the entire grid] - 16 - 16) / 2

높이는 다음과 같이 가로세로 비율을 적용하여 너비에서 계산됩니다. ([width of the entire grid] - 16 - 16) / 2 * 9 / 8. 8과 9를 뒤집었습니다. 너비부터 시작하여 높이를 계산하기 때문입니다(그 반대가 아님).

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

콘텐츠 배치

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

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: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

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

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

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

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

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

프로젝트를 저장합니다.

Android

iOS

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

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

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

카드를 컬렉션으로 곱하기

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

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

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) => 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: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <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: 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/products_repository.dart';
import 'model/product.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 == null || 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: 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.headline6,
                    maxLines: 1,
                  ),
                  SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.subtitle2,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

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

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

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: 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

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

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

다음 단계

상단 앱 바와 카드, 텍스트 입력란, 버튼으로 이제 MDC-Flutter 라이브러리의 핵심 구성요소 네 가지를 사용했습니다. Flutter 위젯 카탈로그를 방문하여 더 많은 구성요소를 살펴볼 수 있습니다.

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

다음 Codelab

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

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

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

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