MDC-102 Flutter: マテリアルの構造とレイアウト

1. はじめに

logo_components_color_2x_web_96dp.png

マテリアル コンポーネント(MDC)は、デベロッパーがマテリアル デザインを実装する際に役立ちます。Google のエンジニアと UX デザイナーのチームが作成した MDC には、美しく機能的な UI コンポーネントが多数含まれており、Android、iOS、ウェブ、Flutter.material.io/develop で利用できます。

Codelab MDC-101 では、2 つのマテリアル コンポーネント(テキスト フィールドと、インクの波紋付きのボタン)を使用してログインページを作成しました。今度は、この基礎にナビゲーション、構造、データを追加していきましょう。

作成するアプリの概要

この Codelab では、衣類と生活雑貨を販売する e コマースアプリ Shrine のホーム画面を作成します。これには次のものが含まれます。

  • トップ アプリバー
  • 商品が並んだグリッドリスト

Android

iOS

トップ アプリバーとさまざまな商品で構成されるグリッドを備えた e コマースアプリ

トップ アプリバーとさまざまな商品で構成されるグリッドを備えた e コマースアプリ

この Codelab で Flutter のコンポーネントとサブシステムをマテリアルにする

  • トップ アプリバー
  • グリッド
  • カード

Flutter 開発のご経験についてお答えください。

初心者 中級者 上級者

2. Flutter の開発環境をセットアップする

このラボを完了するには、Flutter SDKエディタの 2 つのソフトウェアが必要です。

この Codelab は、次のいずれかのデバイスを使って実行できます。

  • パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android または iOS
  • iOS シミュレータ(Xcode ツールのインストールが必要)
  • Android Emulator(Android Studio でセットアップが必要)
  • ブラウザ(デバッグには Chrome が必要)
  • WindowsLinuxmacOS のデスクトップ アプリケーション。開発はデプロイする予定のプラットフォームで行う必要があります。たとえば、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 からクローンを作成する

GitHub からこの Codelab のクローンを作成するには、次のコマンドを実行します。

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. エディタで Get Started: Test drive を開き、「Run the app」セクションの指示に従います。

完了しました。デバイスに MDC-101 Codelab の神社ログインページが表示されます。

Android

iOS

ログインページ(ユーザー名とパスワードのフィールド)、キャンセル ボタン、[次へ] ボタン

ログインページ(ユーザー名とパスワードのフィールド)、キャンセル ボタン、[次へ] ボタン

ログイン画面が正常に表示されたので、アプリに商品を追加してみましょう。

4. トップ アプリバーを追加する

現時点で、[Next] ボタンをクリックすると「You did it!」というホーム画面が表示されます。ここまでは問題ないのですこれで、ユーザーは何もする必要がありません。また、アプリ内のどの位置にいるかもわかりません。そこで、ナビゲーションを追加してみましょう。

マテリアル デザインには、高度なユーザビリティを実現するナビゲーション パターンがあります。特に目立つコンポーネントの 1 つが、トップ アプリバーです。

ナビゲーションを提供し、ユーザーが他のアクションにすばやくアクセスできるように、トップ アプリバーを追加しましょう。

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: フィールドに追加すると、アプリバーをページ上部に、本文を下にして、完璧なレイアウトを無料で提供できます。

プロジェクトを保存します。Shrine アプリが更新されたら、[Next] をクリックしてホーム画面を表示します。

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

神社をタイトルにしたアプリバー

神社をタイトルにしたアプリバー

アプリバーは多くの場合、タイトルの横にボタンがあります。アプリにメニュー アイコンを追加しましょう。

先頭の 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

タイトルに神社とハンバーガー メニュー アイコンがあるアプリバー

タイトルに神社とハンバーガー メニュー アイコンがあるアプリバー

メニュー アイコン(別名「ハンバーガー」)が、想定したとおりの場所に表示されます。

ボタンをタイトルの末尾に追加することもできます。Flutter では、これを「アクション」といいます。

アクションを追加する

IconButtons をさらに 2 つ追加するスペースがあります。

タイトルの後で、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

タイトルに神社、ハンバーガー メニュー アイコン、末尾の検索とカスタマイズのアイコンが表示されたアプリバー

タイトルに神社、ハンバーガー メニュー アイコン、末尾の検索とカスタマイズのアイコンが表示されたアプリバー

アプリに、先頭のボタン、タイトル、右側の 2 つのアクションが表示されます。アプリバーには、コンテンツとは別のレイヤにあることを示す微妙な影を使用して高度も表示されます。

5. グリッドにカードを追加する

アプリの構造がある程度できたので、コンテンツをカードに入れて整理しましょう。

GridView を追加する

まずは、トップ アプリバーの下にカードを 1 つ追加しましょう。Card ウィジェットだけでは情報が足りず、表示する場所をレイアウトできません。そのため、GridView ウィジェットにカプセル化します。

Scaffold の body の Center を 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: には横方向のアイテム数を指定します。2 列にします。

padding: フィールドは、GridView の 4 辺すべてにスペースを持たせます。もちろん、隣に GridView の子はまだないため、後ろ側または下側にパディングは表示されません。

childAspectRatio: フィールドは、アスペクト比(高さに対する幅)に基づいてアイテムのサイズを指定します。

デフォルトでは、GridView によってタイルがすべて同じサイズで作成されます。

カードが 1 つありますが、空です。カードに子ウィジェットを追加しましょう。

コンテンツをレイアウトする

カードには、画像、タイトル、セカンダリ テキストの領域が必要です。

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'),
            ],
          ),
        ),
      ],
    ),
  )
],

このコードは、子ウィジェットを縦方向にレイアウトするために使用する Column ウィジェットを追加します。

crossAxisAlignment: fieldCrossAxisAlignment.start を指定します。これは「テキストを前縁に揃える」という意味です。

AspectRatio ウィジェットは、指定された画像の種類に関係なく、画像の形状を決定します。

Padding は、テキストを側面から少し中に入れます。

2 つの Text ウィジェットは、間に 8 ポイントの空白(SizedBox)を挟んで縦方向に積み重ねられます。Padding の中に収めるために、Column をもう 1 つ作成します。

プロジェクトを保存します。

Android

iOS

画像、タイトル、サブテキストを含む 1 つのアイテム

画像、タイトル、サブテキストを含む 1 つのアイテム

このプレビューでは、角の丸い、影付き(カードの高度を表します)のカードが、端からインセットされて表示されます。マテリアルでは、この形状全体を「コンテナ」といいます(Container という実際のウィジェット クラスと混同しないでください)。

カードは通常、他のカードと一緒にコレクションで表示されます。コレクションとしてグリッド形式でレイアウトしましょう。

6. カード コレクションを作成する

複数のカードが画面に表示されている場合は、1 つまたは複数のコレクションにグループ化されます。コレクション内のカードは同一平面上にあります。つまり、カードは互いに 1 つの静止高度を共有します(カードが選択されるかドラッグされる場合を除きますが、ここではそのようなことは行いません)。

カードを増やしてコレクションにする

現在、Card は GridView の children: フィールドのインラインで作成されています。多くのコードがネストされ、読みづらくなっています。これを抜き出して、必要な数だけ空のカードを生成でき、Card のリストを返す関数にしてみましょう。

新しいプライベート関数を 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();
}

注: コンパイルと実行はまだ行いません。変更をもう 1 つ行います。

また、コンパイルしてみる前に、BuildContext_buildGridCards() に渡すように build() 関数を変更します。

// 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 に変更し、少し拡大して余分な空白を削除します。

fit: フィールドを画像に追加し、その値を BoxFit.fitWidth にします。

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

Android

iOS

切り抜かれた画像、タイトル、価格を含むアイテムのグリッド

切り抜かれた画像、タイトル、価格を含むアイテムのグリッド

これで、商品がアプリに完璧に表示されるようになりました。

7. 完了

アプリに、ログイン画面からホーム画面に移動して商品を表示する基本的なフローができました。ほんの数行のコードで、トップ アプリバー(タイトルと 3 つのボタンがある)と、カード(アプリのコンテンツを表示する)を追加しました。ホーム画面は、基本的な構造と実用的なコンテンツを備えた、シンプルかつ機能的なものになりました。

次のステップ

トップ アプリバー、カード、テキスト フィールド、ボタンで、マテリアル Flutter ライブラリの 4 つのコア コンポーネントを使用しました。詳しくは、マテリアル コンポーネントのウィジェット カタログをご覧ください。

アプリは完全に機能しますが、まだ特定のブランドや視点を表現していません。MDC-103: 色、形状、高度、タイプによるマテリアル デザインのテーマ設定では、これらのコンポーネントのスタイルをカスタマイズして、鮮やかでモダンなブランドを表現します。

この Codelab を完了するためにそれなりの時間と労力を必要とした

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない

今後もマテリアル コンポーネントを使用したい

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない