1. はじめに
Flutter は、1 つのコードベースからモバイル、ウェブ、デスクトップのアプリケーションを作成できる Google の UI ツールキットです。この Codelab では、次のような Flutter アプリケーションを作成します。

このアプリケーションは、「newstay」「lightstream」「mainbrake」「graypine」などの響きの良い名前を生成します。ユーザーは、次の名前を要求したり、現在の名前をお気に入りにしたり、別のページでお気に入りにした名前の一覧を確認したりできます。このアプリは、さまざまな画面サイズに対してレスポンシブです。
学習内容
- Flutter の動作に関する基礎知識
- Flutter でレイアウトを作成する方法
- ユーザー操作(ボタンを押すなど)をアプリ動作に接続する方法
- Flutter コードを整理された状態に保つ方法
- アプリをレスポンシブにする方法(画面の変更に対して)
- アプリのルックアンドフィールを一貫したものにする方法
すぐに面白い部分を体験できるように、基本的なスキャフォールドから始めます。

ここで Filip が Codelab の全貌をご紹介します。
[次へ] をクリックして、ラボを始めましょう。
2. Flutter 環境をセットアップする
エディタ
この Codelab をできる限り単純にするために、開発環境として Visual Studio Code(VS Code)を使用していると仮定します。無料なうえ、すべての主要なプラットフォームに対応しています。
もちろん、Android Studio やその他の IntelliJ IDE、Emacs、Vim、Notepad++ などのエディタを使用しても問題ありません。どれも Flutter に使用できます。
手順では VS Code 専用のショートカットをデフォルトとするため、この Codelab では VS Code の使用をおすすめします。「お使いのエディタで X を行うための適切な操作を行う」などとするよりも、「こちらをクリック」や「このキーを押す」とするほうが簡単です。

開発ターゲットを選ぶ
Flutter はマルチプラットフォームのツールキットです。アプリは、次のオペレーティング システムのいずれでも実行できます。
- iOS
- Android
- Windows
- macOS
- Linux
- ウェブ
とはいえ、おすすめなのは、第一の開発対象となるオペレーティング システムを選ぶことです。これを「開発ターゲット」と呼びます。開発中にアプリを実行するオペレーティング システムのことです。

たとえば、Flutter アプリの開発に Windows ノートパソコンを使用するとしましょう。開発ターゲットに Android を選ぶと、通常は USB ケーブルで Android デバイスを Windows ノートパソコンに接続し、開発中のアプリは接続した Android デバイスで実行します。しかし、Windows を開発ターゲットに選ぶことも可能です。その場合、開発中のアプリはエディタといっしょに Windows アプリとして実行します。
開発ターゲットにウェブを選びたくなるかもしれませんが、その場合は、Flutter の最も便利な開発機能であるステートフル ホットロードが使えなくなります。Flutter では、ウェブ アプリケーションのホットリロードはできません。
ここで選択してください。注意: 後からいつでも別のオペレーティング システムでアプリを実行できます。単に、開発ターゲットを明確にしていれば、次のステップにスムーズに進めるということです。
Flutter をインストールする
Flutter SDK の最新のインストール手順は、常に docs.flutter.dev にあります。
Flutter のウェブサイトでは、SDK 自身のインストールだけでなく、開発ターゲット関連のツールやエディタ プラグインについても説明されています。この Codelab でインストールする必要があるのは、次のものだけです。
- Flutter SDK
- Visual Studio Code と Flutter プラグイン
- 選んだ開発ターゲットに必要となるソフトウェア(たとえば、Windows がターゲットなら Visual Studio、macOS がターゲットなら Xcode)
次のセクションでは、初めての Flutter プロジェクトを作成します。
ここまでで問題があった場合は、以下の質問と答え(StackOverflow から)がトラブルシューティングの参考になるかもしれません。
よくある質問
- Flutter SDK のパスの確認方法を教えてください。
- Flutter が見付からなかった場合はどうすればよいですか?
- 「Waiting for another flutter command to release the startup lock」の問題はどうやって解決すればよいですか?
- Android SDK のインストール場所を Flutter に認識させるにはどうすればよいですか?
flutter doctor --android-licensesを実行したときの Java エラーにはどう対処すればよいですか?sdkmanagerツールが見付からない場合はどう対処すればよいですか?- 「
cmdline-toolscomponent is missing」というエラーにはどう対処すればよいですか? - CocoaPods を Apple Silicon(M1)で実行するにはどうすればよいですか?
- VS Code で保存時の自動整形を無効にするにはどうすればよいですか?
3. プロジェクトを作成する
最初の Flutter プロジェクトを作成する
Visual Studio Code を起動して、コマンド パレットを開き(F1、Ctrl+Shift+P、Shift+Cmd+P)、「flutter new」と入力します。[Flutter: New Project] コマンドを選択します。

次に、[Application] を選択し、プロジェクトを作成するフォルダを選択します。ホーム ディレクトリや「C:\src\」などです。
最後に、プロジェクトに名前を付けます。「namer_app」や「my_awesome_namer」などです。

すると、Flutter がプロジェクト フォルダを作成し、VS Code がそのフォルダを開きます。
次は、3 つのファイルの内容を、このアプリの基本的なスキャフォールドで上書きします。
初期アプリをコピーして貼り付ける
VS Code の左側のペインでエクスプローラが選択されていることを確認し、pubspec.yaml ファイルを開きます。

このファイルの内容を次のように置き換えます。
pubspec.yaml
name: namer_app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: '>=2.19.4 <4.0.0'
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
pubspec.yaml ファイルでは、現在のバージョン、依存関係、同梱するアセットなど、アプリの基本情報を指定します。
このプロジェクトのもう一つの設定ファイルである analysis_options.yaml を開きます。

その内容を、次のように置き換えます。
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
prefer_const_constructors: false
prefer_final_fields: false
use_key_in_widget_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_const_constructors_in_immutables: false
avoid_print: false
このファイルでは、Flutter がコードを解析する際の厳格さを指定します。今回は Flutter を初めて使うので、緩やかな設定にしています。これはいつでも後で調整できます。実際の製品版アプリの公開が近付けば、これよりも厳しくするのは間違いないでしょう。
最後に、lib/ ディレクトリの main.dart ファイルを開きます。

このファイルの内容を次のように置き換えます。
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [
Text('A random idea:'),
Text(appState.current.asLowerCase),
],
),
);
}
}
以上の 50 行のコードが、ここまでのアプリの全貌です。
次のセクションでは、デバッグモードでアプリケーションを実行し、開発を開始します。
4. ボタンを追加する
このステップでは、新たな単語のペアリングを生成する [Next] ボタンを追加します。
アプリを起動する
最初に、lib/main.dart を開き、対象デバイスが選択されていることを確認します。VS Code の右下に、現在の対象デバイスを表示するボタンがあります。クリックして変更します。

lib/main.dart を開いている間に、VS Code のウィンドウの右上にあるプレイ
ボタンをクリックします。

少し待つと、アプリがデバッグモードで起動します。まだまだ先は長そうです。

最初のホットリロード
lib/main.dart の最後で、1 つ目の Text オブジェクトの文字列に何かを追加して、ファイルを保存します(Ctrl+S または Cmd+S)。次に例を示します。
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
アプリはすぐに変化しますが、ランダムな単語は同じままです。これが Flutter の有名なステートフル ホットリロードの動作です。ホットリロードは、変更をソースファイルに保存したときにトリガーされます。

よくある質問
- VSCode でホットリロードが動作しない場合はどうすればよいですか?
- VSCode でホットリロードするために「r」を押す必要はありますか?
- ホットリロードはウェブでも動作しますか?
- 「Debug」バナーを削除するにはどうすればよいですか?
ボタンを追加する
次に、Column の下部で、2 つ目の Text インスタンスのすぐ下に、ボタンを追加します。
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
変更を保存すると、アプリが再び更新されます。ボタンが表示され、それをクリックすると、VS Code のデバッグ コンソールに「button pressed!」というメッセージが表示されます。

5 分で終わる Flutter 講座
デバッグ コンソールを見るのも良いのですが、ボタンにはもっと意味のあることを表示したいものです。その前に、lib/main.dart のコードをよく見て、その動作を理解しましょう。
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
このファイルの一番上に main() 関数があります。今の形では、MyApp で定義したアプリの実行を Flutter に指示するだけです。
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
MyApp クラスは StatelessWidget を拡張しています。ウィジェットは、すべての Flutter アプリを作成する際の元になる要素です。ご覧のように、このアプリ自体がウィジェットです。
MyApp 内のコードでアプリ全体をセットアップします。さらに、アプリ全体の状態(詳しくは後ほど)を作成し、アプリに名前を付け、視覚的テーマを設定し、アプリの出発点となる「ホーム」ウィジェットを設定します。
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
次に、MyAppState クラスでアプリの状態を定義します。今回は Flutter を初めて使うので、この Codelab は単純で的を絞ったものにします。Flutter には、アプリの状態を管理する強力な方法が多数あります。特に説明しやすいのが、このアプリで採用している ChangeNotifier を使用する方法です。
MyAppStateでは、アプリが機能するために必要となるデータを定義します。今のところ、現在のランダムな単語のペアを収めた変数が 1 つあるだけです。後でここに追加します。- 状態クラスは
ChangeNotifierを拡張します。つまり、自身の変更に関する通知を行うことができるということです。たとえば、現在の単語ペアが変化したら、アプリ内のウィジェットが知る必要があります。 - 状態は
ChangeNotifierProviderを使用して作成されてアプリ全体に提供されます(MyAppの上記コードを参照)。これにより、アプリ内のどのウィジェットも状態を取得できるようになります。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
最後は、すでに変更しているウィジェット、MyHomePage です。以下の番号付きの行は、それぞれ、上記コード内の行番号のコメントに対応しています。
- どのウィジェットでも、そのウィジェットを常に最新にするために、周囲の状況が変化するたびに自動的に呼び出される
build()メソッドを定義します。 MyHomePageでは、watchメソッドを使用してアプリの現在の状態に対する変更を追跡します。- どの
buildメソッドも必ず、ウィジェットか、ウィジェットのネストしたツリー(こちらのほうが一般的)を返します。この場合、トップレベルのウィジェットはScaffoldです。Scaffoldはこの Codelab の対象ではありませんが、便利なウィジェットであり、実世界の Flutter アプリの多くで使用されています。 Columnは、Flutter における非常に基本的なレイアウト ウィジェットです。任意の数の子を従え、それらを上から下へ一列に配置します。デフォルトでは、その子を上に寄せます。すぐにこれを中央に寄せるように変更します。- この
Textウィジェットは最初のステップで変更しました。 - この 2 つ目の
TextウィジェットはappStateを取り、そのクラスの唯一のメンバーであるcurrent(WordPair)にアクセスします。WordPairには、asPascalCaseやasSnakeCaseなどの便利なゲッターがあります。ここではasLowerCaseを利用しますが、これは別のものに変えても構いません。 - Flutter コードでは行末のカンマを多用します。このカンマに関しては、
Columnのパラメータ リストの最後(かつ唯一)のメンバーがchildrenなので、必要ありません。それでも行末のカンマを使用するのはよいことです。メンバーの追加が簡単になるだけでなく、Dart の自動整形で行を追加する際のヒントにもなります。詳しくは「Code formatting」をご覧ください。
次は、ボタンを状態に接続します。
初めての動作
MyAppState までスクロールして、getNext メソッドを追加します。
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
新しい getNext() メソッドは、current に新しいランダムな WordPair を再代入します。また、監視している MyAppState に通知するために notifyListeners()(ChangeNotifier) のメソッド)の呼び出しも行います。
残っているのは、ボタンのコールバックから getNext を呼び出すことだけです。
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
保存して、アプリを試してみましょう。[Next] ボタンを押すたびに、新規のランダムな単語ペアが生成されるはずです。
次のセクションでは、ユーザー インターフェースを美しくします。
5. アプリを美しくする
以下がこの時点でのアプリの外観です。

あまり良くありません。このアプリの主眼であるランダムに生成された単語ペアは、もっと目立たせる必要があります。それがこのアプリを使う最大の理由です。また、アプリのコンテンツが奇妙に中央から外れており、アプリ全体も白黒の退屈な配色になっています。
このセクションでは、アプリのデザインを修正して、これらの問題に対処します。このセクションの最終的な目標は、次のようにすることです。

ウィジェットを抽出する
現在の単語ペアを表示する行は「Text(appState.current.asLowerCase)」のようになっています。これを複雑なものに変えるには、この行を抽出して別のウィジェットにするのが良いでしょう。UI の独立した論理部品を独立したウィジェットにすることは、Flutter で複雑性を管理するうえで重要な方法です。
Flutter ではウィジェットを抽出するリファクタリング ヘルパーが提供されていますが、それを使用する前に、抽出される行が必要なものだけにアクセスしていることを確認してください。現時点では、この行で appState にアクセスしていますが、本当に必要なのは現在の単語ペアが何であるかだけです。
そのため、MyHomePage ウィジェットを次のように書き換えます。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
いいですね。Text ウィジェットが appState 全体を参照することはなくなりました。
次は [Refactor] メニューを呼び出します。VS Code では、次の 2 つの方法があります。
- リファクタリングするコード(この場合は
Text)を右クリックして、プルダウン メニューから [Refactor...] を選択する
または
- カーソルをリファクタリングするコード(この場合は
Text)に移動して、Ctrl+.(Win / Linux)かCmd+.(Mac)を押す。

[Refactor] メニューで、[Extract Widget] を選択します。「BigCard」などの名前を割り当て、Enter をクリックします。
すると、新しいクラスである BigCard が現在のファイルの最後に自動的に作られます。このクラスは以下のようになります。
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
リファクタリング後もアプリは正しく動作しています。
カードを追加する
次は、この新しいウィジェットを、このセクションの最初で構想したような、はっきりした UI にします。
BigCard クラスとその中の build() メソッドを見つけます。前と同じように、Text ウィジェットで [Refactor] メニューを呼び出します。ただし今回は、ウィジェットの抽出を行います。
今回は [Wrap with Padding] を選択します。これにより、Padding という Text ウィジェットの周囲に新しい親ウィジェットが作られます。保存すると、ランダムな単語が空間的な余裕を持って表示されます。

パディングをデフォルト値の 8.0 から増やします。たとえば、パディングを増やすために 20 などを使用します。
次は、レベルがもう一段階上がります。Padding ウィジェットにカーソルを置き、[Refactor] メニューを呼び出して、[Wrap with widget...] を選択します。
これで親ウィジェットを選択できるようになります。「Card」と入力して、Enter を押します。

これにより、Padding ウィジェットが、そして必然的に Text も、Card ウィジェットに包まれます。

テーマとスタイルを設定する
カードがもっと目立つように、もっと豊かな色で塗りつぶします。一貫したカラーパターンを維持するのは常によいことですから、アプリの Theme を使用して色を選びます。
BigCard の build() メソッドに次の変更を加えます。
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
上記の 2 行では、いろいろな処理を行っています。
- 1 行目では、
Theme.of(context)でアプリの現在のテーマをリクエストしています。 - 2 行目では、カードの色をテーマの
colorSchemeプロパティと同じになるよう定義しています。カラーパターンには多数の色が含まれていますが、primaryがこのアプリの最も目立つ特徴的な色です。
これで、カードはアプリのプライマリ カラーで塗りつぶされます。

MyApp までスクロールし、そこで ColorScheme のシード色を変更すると、この色とアプリ全体のカラーパターンを変更できます。

色がスムーズなアニメーションで変化しています。これを暗黙的アニメーションと呼びます。多くの Flutter ウィジェットでは、UI を状態から状態へジャンプさせるのではなく、値と値の間を滑らかに補間します。
カードの下の浮き上がりボタンの色も変化しています。これが、値をハードコードするのではなく、アプリ全体が対象の Theme を使用するメリットです。
TextTheme
このカードにはまだ問題があります。テキストが小さすぎ、読みにくい色になっていることです。これを修正するために、BigCard の build() メソッドに次のような変更を加えます。
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
変更内容は次のとおりです。
theme.textTheme,を使用して、アプリのフォントテーマにアクセスします。このクラスには、bodyMedium(中サイズの標準テキスト用)、caption(画像のキャプション用)、headlineLarge(大きな見出し用)などのメンバーがあります。displayMediumプロパティは、ディスプレイ テキスト用の大きなスタイルです。ここで「ディスプレイ」という単語は、ディスプレイ書体(見出し書体)などのタイポグラフィでの意味で使用されています。displayMediumのドキュメントには「ディスプレイ スタイルは短く、重要なテキストにのみ使用します」と書かれており、まさに我々のユースケースです。- 理論上、テーマの
displayMediumプロパティはnullにすることもできますが、このアプリの記述に使用している Dart というプログラミング言語は null 安全なので、nullになる可能性のあるオブジェクトのメソッドは呼び出せません。この場合、!演算子(感嘆符演算子)を使い、承知のうえで行っているということを Dart に対して保証できます(この場合にdisplayMediumが null でないことは確実です。断定できる理由は、この Codelab の範囲外です)。 displayMediumのcopyWith()を呼び出すと、定義した変更が反映されたテキスト スタイルのコピーが返されます。この場合は、テキストの色のみを変更しています。- 新しい色を取得するために、再びアプリのテーマにアクセスしています。カラーパターンの
onPrimaryプロパティには、アプリのプライマリ カラーに使用するのに適した色が定義されています。
アプリの表示は次のように変わっているはずです。

気が向いたら、さらにカードを変更しましょう。たとえば、次のようにします。
copyWith()を使うと、テキスト スタイルの色以外にも多数のプロパティを変更できます。変更できるプロパティの完全なリストを確認するには、copyWith()の括弧内にカーソルを移動し、Ctrl+Shift+Space(Win / Linux)かCmd+Shift+Space(Mac)を押してください。- 同じように、
Cardウィジェットにも、他に変更できるパラメータがあります。たとえば、elevationパラメータの値を増やせば、カードの影を大きくできます。 - 色をいじってみましょう。
theme.colorScheme.primary以外にも、.secondaryや.surfaceなど無数にあります。これらの色のすべてにonPrimaryと同等のものがあります。
アクセシビリティを高める
Flutter を使うことで、アプリのアクセシビリティはデフォルトで確保されます。たとえば、すべての Flutter アプリでは、アプリのすべてのテキストと対話的要素が、TalkBack や VoiceOver などのスクリーン リーダーから認識できるようになっています。

なんらかの作業が必要となる場合もあります。このアプリの場合、生成された単語ペアを発音するにあたってスクリーン リーダーで問題が発生する可能性があります。人間であれば「cheaphead」を構成する 2 つの単語を問題なく識別できますが、スクリーン リーダーでは単語の途中の「ph」が「f」と発音される可能性があります。
この簡単な解決方法は、pair.asLowerCase を "${pair.first} ${pair.second}" に置き換えることです。後者では、文字列補間を使用して、pair に含まれている 2 つの単語から "cheap head" などの文字列を作成しています。複合語ではなく 2 つの別々の単語を使うことで、2 つの単語がスクリーン リーダーに適切に識別され、視覚障がいのあるユーザーの体験が改善されます。
とはいえ、pair.asLowerCase の視覚的な単純さは維持したいところです。Text の semanticsLabel プロパティを使用して、このテキスト ウィジェットの視覚的内容を、スクリーン リーダーにとってより適切な意味的内容でオーバーライドします。
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
これで、UI は同じままで、スクリーン リーダーが生成された各単語ペアを正しく発音するようになりました。自分のデバイスでスクリーン リーダーを使用して、実際に試してみましょう。
UI を中央に寄せる
ランダムな単語ペアの表示は十分改善されたので、次はアプリのウィンドウや画面の中央に配置しましょう。
まず、BigCard が Column の一部であることを思い出してください。Column はデフォルトで子を上部にまとめますが、これは簡単にオーバーライドできます。MyHomePage の build() メソッドを次のように変更します。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
これによって、Column 内の子が主軸(縦軸)に沿って中央寄せされます。

すでに子は交差軸に沿って中央寄せされています(言い換えれば、すでに水平方向に中央寄せされています)。しかし Column 自体は Scaffold 内で中央寄せされていません。これは Widget Inspector で確認できます。

Widget Inspector はこの Codelab の範囲外ですが、Column がハイライト表示されているとき、それがアプリの幅全体を埋めていないのがわかります。子が必要とする水平方向のスペースしか埋めていません。
この Column 自体を中央寄せすることができます。Column にカーソルを置き、[Refactor] メニューを呼び出し(Ctrl+. または Cmd+.)、[Wrap with Center] を選択します。

アプリの表示は次のように変わっているはずです。

必要に応じて、もう少し調整できます。
BigCardの上のTextウィジェットは削除できます。説明的なテキスト(「A random AWESOME idea:」)は、それがなくても理解できる UI になっているので、もはや不要だと言えるでしょう。そのほうがすっきりします。BigCardとElevatedButtonの間にSizedBox(height: 10)ウィジェットを追加することもできます。そうすれば、2 つのウィジェットの間隔が少し広がります。SizedBoxウィジェットはスペースを埋めるだけで、それ自身は何もレンダリングしません。視覚的な「ギャップ」を作るためによく利用されます。
このようなオプションの変更を加えると、MyHomePage のコードは次のようになります。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
アプリの表示は次のようになります。

次のセクションでは、生成された単語をお気に入りに登録する(つまり「いいね」する)機能を追加します。
6. 機能を追加する
アプリは機能し、ときには興味深い単語ペアを生成します。しかし、[Next] をクリックすると、単語ペアは永遠に失われてしまいます。「Like」ボタンのような、最適な提案を「記憶」する方法があるとよいでしょう。

ビジネス ロジックを追加する
MyAppState までスクロールして、次のコードを追加します。
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
変更点を確認しましょう。
favoritesにMyAppStateという新規のプロパティを追加しました。このプロパティは空のリスト[]で初期化されています。- また、ジェネリクスを使って、このリストが
<WordPair>[]のみを含むように指定しました。これにより、WordPair以外を追加しようとすると、Dart によりアプリの実行すら拒否されるようになります。そうすると、favoritesリストに望ましくないオブジェクト(nullなど)が隠れていないことがわかるので、それを安心して使うことができます。
- 新しいメソッド
toggleFavorite()の追加も行いました。このメソッドは、お気に入りのリストから現在の単語ペアを取り除くか(すでにそこにある場合)、追加します(まだそこにない場合)。どちらの場合も、その後でこのコードからnotifyListeners();が呼び出されます。
ボタンを追加する
「ビジネス ロジック」からは離れて、今度はまたユーザー インターフェースに関する作業を行いましょう。[Like] ボタンを [Next] ボタンの左に配置するには、Row が必要です。Row ウィジェットは、先程の Column と同等ですが、こちらは水平方向です。
まず、既存のボタンを Row で包みます。MyHomePage の build() メソッドに移動し、ElevatedButton にカーソルを置いて、[Refactor] メニューを Ctrl+. または Cmd+. で呼び出し、[Wrap with Row] を選択します。

保存すると、Row が Column と同じような働きをして、子を左にまとめているのがわかります(Column は子を上にまとめていました)。これを修正するために、前と同じ方法を使えますが、今回は mainAxisAlignment で行います。しかし、教育の目的で mainAxisSize を使用します。これは、Row に対して水平方向のスペースをすべて埋めないように指示するものです。
以下のように変更します。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
UI は以前の形に戻りました。

次に [Like] ボタンを追加し、それを toggleFavorite() に接続します。まず試しに、次のコードブロックを見ずに、自分でやってみてください。

下のやり方とまったく同じにならなくても問題ありません。大きな課題にチャレンジしたいのでない限り、ハートのアイコンを気にする必要はありません。
間違ってもまったく問題ありません。Flutter を始めて 1 時間ほどなのですから。

以下は、MyHomePage に 2 つ目のボタンを追加する方法の 1 つです。今回は ElevatedButton.icon() コンストラクタを使用してアイコン付きのボタンを作成します。また build メソッドの最初では、現在の単語ペアがすでにお気に入りにあるかどうかに応じて適切なアイコンを選びます。さらに、SizedBox を再度使用し、2 つのボタンの間隔を少し広げています。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
アプリは次のようになります。

残念ながら、お気に入りを見ることはできません。ここで、完全に別の画面をアプリに追加することが必要になりました。では次のセクションでお会いしましょう。
7. ナビゲーション レールを追加する
ほとんどのアプリでは、すべてを一画面に収めることができません。このアプリでは可能かもしれませんが、教育の目的で、ユーザーのお気に入りのための別の画面を作っていきます。2 画面を切り替えるために、初めての StatefulWidget を実装します。

このステップの本題にできるだけ早く入るために、MyHomePage を 2 つのウィジェットに分割します。
MyHomePage のすべてを選択し、以下のコードに置き換えます。
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
保存すると、UI の見た目は問題ありませんが、機能には問題があることがわかります。ナビゲーション レールの ♥︎(ハート)をクリックしても何も起きません。

変更点を確認しましょう。
- まず、
MyHomePageのすべての内容が新しいウィジェットGeneratorPageに抽出されています。MyHomePageウィジェットの中で抽出されていないのはScaffoldだけです。 - 新しい
MyHomePageには 2 つの子を持つRowが含まれています。1 つ目のウィジェットはSafeAreaで、2 つ目はExpandedウィジェットです。 SafeAreaは、その子がハードウェア ノッチやステータスバーで隠れないようにするものです。このアプリでは、このウィジェットがNavigationRailを包んで、ナビゲーション ボタンがモバイル ステータスバーなどで隠されるのを防いでいます。- NavigationRail の
extended: falseの行はtrueに変更できます。そうすることで、アイコンの隣のラベルが表示されます。これ以降のステップで、これを水平方向に十分なスペースがあるときに自動的に行う方法を学びます。 - このナビゲーション レールには、2 つのデスティネーション(Home と Favorites)があり、それぞれにアイコンとラベルがあります。現在の
selectedIndexの定義もしています。インデックスに 0 が選択されると最初のデスティネーションが選択され、1 が選択されると 2 つ目のデスティネーションがされます。以降も同様です。今のところ、0 にハードコードされています。 - また、このナビゲーション レールでは、
onDestinationSelectedでデスティネーションのうちの 1 つが選択されたときに何が起きるかも定義しています。現時点では、要求されたインデックス値をprint()で出力するだけです。 Rowの 2 番目の子はExpandedウィジェットです。Expanded ウィジェットは Row や Column で使用すると非常に便利です。これを使用すると、ある子は必要なだけのスペースを埋め(この場合はNavigationRail)、別のウィジェットは残りのスペースをできる限り埋める(この場合はExpanded)というレイアウトを表現できます。Expandedについての考え方を 1 つ言えば、「欲張り」だということです。このウィジェットの役割をもっと詳しく知るには、NavigationRailウィジェットをもう一つのExpandedで包んでみてください。その結果、次のようなレイアウトになります。

- ナビゲーション レールには左側に少しの残りスペースがあればよいですが、2 つの
Expandedウィジェットが使用可能な水平方向のスペースをすべて分け合っています。 Expandedウィジェットの中には色の付いたContainerがあり、コンテナの中にはGeneratorPageがあります。
ステートレス ウィジェットとステートフル ウィジェット
ここまでは、状態に関するすべてのニーズを MyAppState が満たしてきました。これまでに記述してきたウィジェットがすべてステートレスなのは、それが理由です。それらのウィジェットには、自身の変更可能な状態が含まれていません。どのウィジェットも自身を変更できません。必ず MyAppState を経由させる必要があります。
これを変更しようとしています。
ナビゲーション レールの selectedIndex の値を保持する手段が必要です。また、onDestinationSelected コールバック内からこの値を変更できる必要もあります。
MyAppState の別のプロパティとして selectedIndex を追加することもできます。これは機能します。しかし、すべてのウィジェットが自分の値を自分の中に保存すると、すぐにアプリ状態が無意味に肥大化することは想像に難くないでしょう。

一部の状態は 1 つのウィジェットにだけ関連しているので、そのウィジェット内に留めるべきです。
State を持つタイプのウィジェットである StatefulWidget の説明に入りましょう。まず、MyHomePage をステートフル ウィジェットに変換します。
カーソルを MyHomePage の最初の行(class MyHomePage... で始まる行)に置き、Ctrl+. または Cmd+. を使用して [Refactor] メニューを呼び出します。次に、[Convert to StatefulWidget] を選択します。

IDE によって新しいクラス _MyHomePageState が作成されます。このクラスは State を拡張しているため、自身の値を管理できます(自身を変更できます)。また、古いステートレス ウィジェットの build メソッドが(ウィジェットに残らずに)_MyHomePageState に移動しています。これはそのまま移動し、build メソッドの内容は変わっていません。今は単に別の場所にあるだけです。
setState
新しいステートフル ウィジェットで追跡する必要がある変数は selectedIndex の 1 つだけです。_MyHomePageState に次の 3 の変更を加えます。
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
変更点を確認しましょう。
- 新しい変数
selectedIndexを導入し、0に初期化しました。 NavigationRailの定義で、先程まであったハードコードの0の代わりに、この新しい変数を使用します。onDestinationSelectedコールバックが呼び出されたときに、単に新しい値をコンソールに出力するのではなく、setState()の呼び出しの中でselectedIndexに代入します。この呼び出しは、前に使用したnotifyListeners()メソッドに似ていますが、こちらは UI を更新するためのものです。

これで、ナビゲーション レールがユーザー操作に応答するようになりました。しかし、右側の拡張された領域は同じままです。これは、表示するスクリーンの決定に selectedIndex を使用していないためです。
selectedIndex を使用する
_MyHomePageState の build メソッドの先頭、return Scaffold の直前に、以下のコードを配置します。
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
このコードを確認しましょう。
Widget型のpageという新しい変数を宣言しています。- 次に switch 文で、
selectedIndexの現在の値に基づいて、画面をpageに代入しています。 FavoritesPageはまだないので、Placeholderという、配置した場所に十字が入った四角形を描画して、その部分の UI が未完成であることを示す便利なウィジェットを使用します。

- また、switch 文にフェイル ファストの原則を適用し、
selectedIndexが 0 でも 1 でもない場合にエラーをスローするようにしています。これによって以降のバグを防ぐことができます。ナビゲーション レールに新たなデスティネーションを追加して、コードの更新を忘れると、開発中にプログラムがクラッシュします(機能しない理由を推測したり、バグのあるコードを製品版に入れて公開したりすることを避けられます)。
こうして、右に表示したいウィジェットが page に含まれるようになったので、その他にどんな変更が必要かは推測できるかもしれません。
以下は、その残り一つの変更の後の _MyHomePageState です。
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
これで、GeneratorPage と、この後で [Favorites] ページになるプレースホルダとの間で切り替わるようになりました。

レスポンシブ
次は、ナビゲーション レールをレスポンシブにします。つまり、十分なスペースがあるときに(extended: true を使用して)ラベルを自動的に表示するようにします。

Flutter では、アプリを自動的にレスポンシブにするウィジェットがいくつか用意されています。たとえば、Row と Column に似た Wrap ウィジェットは、垂直方向または水平方向に十分なスペースがないときに、自動的に子を次の「行」に送ります(「追い出し」といいます)。FittedBox という、指定に従って利用可能なスペースに子を自動的に合わせるウィジェットもあります。
しかし、NavigationRail は、どの状況でも十分なスペースがあることを認識できないため、十分なスペースがあるときに自動的にラベルを表示することができません。その判断はデベロッパーに任されています。
MyHomePage が 600 ピクセル幅以上ある場合にのみ、ラベルを表示することにしましょう。
この場合に使用するウィジェットは LayoutBuilder です。これを使用すると、利用可能なスペースに応じてウィジェット ツリーを変更できます。
繰り返しになりますが、VS Code にある Flutter の [Refactor] メニューを使用して、必要な変更を行います。ただし、今回は少々複雑になります。
_MyHomePageStateのbuildメソッドの中で、Scaffoldにカーソルを置きます。- [Refactor] メニューを
Ctrl+.(Windows / Linux)またはCmd+.(Mac)で呼び出します。 - [Wrap with Builder] を選択し、Enter を押します。
- 新たに追加された
Builderの名前をLayoutBuilderに変更します。 - コールバックのパラメータ リストを
(context)から(context, constraints)に変更します。

LayoutBuilder の builder コールバックは、制約が変化するたびに呼び出されます。これは、次のような場合に発生します。
- ユーザーがアプリのウィンドウのサイズを変更した。
- ユーザーがスマートフォンの向きをポートレート モードから横表示に変えた、またはその逆を行った。
MyHomePageの横のウィジェットのサイズが大きくなり、MyHomePageの制約が小さくなった。- その他
これで、現在の constraints を照会して、ラベルを表示するかどうかを決定できるようになりました。次の一行の変更を _MyHomePageState の build メソッドに加えます。
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
これで、アプリが画面サイズ、向き、プラットフォームなどの環境に応答するようになりました。つまり、レスポンシブになったということです。

残っているのは、先程の Placeholder を実際の [Favorites] 画面に置き換えることです。これは次のセクションで行います。
8. 新しいページを追加する
[Favorites] ページの代わりに使用した Placeholder ウィジェットを覚えていますか?

ここではこれを修正します。
冒険したいのであれば、このステップを自分でやってみてください。目標は、favorites のリストを新しいステートレス ウィジェット FavoritesPage で表示し、そのウィジェットを Placeholder の代わりに表示することです。
以下にポイントを示します。
- スクロールする
Columnが必要なときには、ListViewウィジェットをします。 context.watch<MyAppState>()を使用して任意のウィジェットからMyAppStateインスタンスにアクセスできることを忘れないでください。- 新しいウィジェットを試したいのなら、
ListTileにtitle(通常はテキスト用)、leading(アイコンまたはアバター用)、onTap(操作用)などのプロパティがあります。しかし、すでにご存じのウィジェットでも、同様の効果が得られます。 - Dart では、コレクション リテラルの中で
forループを使えます。たとえば、messagesに文字列のリストが含まれていると、次のようにコーディングできます。

関数型プログラミングのほうが詳しいようでしたら、Dart では messages.map((m) => Text(m)).toList() のようにコーディングすることもできます。もちろん、ウィジェットのリストを作成して、build メソッドの中で命令型で追加することもできます。
[Favorites] ページを自分で追加すれば、自身で判断することで、より多くのことを学べます。デメリットは、まだ自分で解決できない問題が発生する可能性があることです。注意: 間違っても構いません。それも学習の非常に重要な要素です。最初の一時間で Flutter の開発を完璧に行えるとは誰も期待していませんし、自分でも期待すべきでありません。

以下は、お気に入りページを実装する方法の一例に過ぎません。その実装方法が、コードをいじったり、UI を改良したり、独自の改造をしたりする動機になれば幸いです。
以下が新しい FavoritesPage クラスです。
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
このウィジェットは次のように動作します。
- アプリの現在の状態を取得します。
- お気に入りのリストが空の場合は、中央寄せされた「No favorites yet*.*」というメッセージを表示します。
- そうでない場合は(スクロール可能な)リストを表示します。
- リストの最初には概要を表示します(例: You have 5 favorites*.*)
- 次に、すべてのお気に入りについて反復処理を行い、それぞれに
ListTileを構築します。
あとに残っているのは、Placeholder ウィジェットを FavoritesPage で置き換えることだけです。完成です。

このアプリの完成版のコードは GitHub の Codelab のリポジトリで入手できます。
9. 次のステップ
おめでとうございます!
よくできました。Column と 2 つの Text ウィジェットを持つ機能しないスキャフォールドを、レスポンシブで魅力的な小さいアプリに作り変えました。

学習した内容
- Flutter の動作に関する基礎知識
- Flutter でレイアウトを作成する方法
- ユーザー操作(ボタンを押すなど)をアプリ動作に接続する方法
- Flutter コードを整理された状態に保つ方法
- アプリをレスポンシブにする方法
- アプリのルックアンドフィールを一貫したものにする方法
次のステップ
- このラボで記述したアプリでもっと実験する。
- 同じアプリの高度なバージョンを確認して、アニメーション付きのリスト、グラデーション、クロスフェードなどを追加する方法を確認する。

- flutter.dev/learn にアクセスして、学習を続ける。