Flutterアプリケーション開発概論

【共通UI】AppColors・AppCard・AppTextField・ErrorBoxを作る

10LINE風チームタスク管理アプリを作りながら、ログイン・データベース・権限管理を学ぶ
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

このページでやること

このページでは、アプリの中で何度も使うUI部品を作ります。

UIとは、ボタン、入力欄、カード、エラー表示など、ユーザーが見る画面部品のことです。

今回作る共通UIは、次の4つです。

部品一言説明
AppColorsアプリで使う色をまとめる
AppCard白いカード型の箱を作る
AppTextField入力欄の見た目を統一する
ErrorBoxエラーメッセージを赤く表示する

このページでは、npmや環境変数の設定は使いません。

やることは、main.dart に共通部品のコードを追加して、保存して、画面で確認するだけです。


今日のゴール

完成すると、次のように部品を短く使えるようになります。

AppCard(
  child: Text('プロフィール作成'),
)
AppTextField(
  controller: emailController,
  label: 'メールアドレス',
)
ErrorBox(
  message: 'ログインに失敗しました。',
)

同じデザインを何度も書かなくてよくなります。


作業の流れ

1. lib/main.dart を開く
↓
2. AppColors を確認する
↓
3. AppCard を追加する
↓
4. AppTextField を追加する
↓
5. ErrorBox を追加する
↓
6. 保存する
↓
7. flutter run で確認する

完成コードを使っている場合は、すでに入っています。

このページでは、「何のためにあるのか」を確認しながら進めます。


Step 1:main.dartを開く

Flutterプロジェクトの中で、次のファイルを開きます。

lib/main.dart

ターミナルから開く場合は、次を実行します。

code lib/main.dart

code が使えない場合は、VS Codeの左側から lib/main.dart を開いてください。


Step 2:AppColorsを確認する

まず、色をまとめる AppColors を確認します。

AppColors は、アプリ全体で使う色の一覧です。

class AppColors {
  static const bg = Color(0xFFF3F4F6);
  static const chatBg = Color(0xFFECEFF1);
  static const card = Colors.white;
  static const lineGreen = Color(0xFF06C755);
  static const lineGreenDark = Color(0xFF00B900);
  static const text = Color(0xFF111111);
  static const subText = Color(0xFF8A8A8A);
  static const border = Color(0xFFE5E5E5);
  static const danger = Color(0xFFE53935);
  static const success = Color(0xFF06C755);
  static const warning = Color(0xFFFFB300);

  static const primary = lineGreen;
}

Color は、色を表すFlutterの型です。

型とは、データの種類のことです。

0xFF06C755 のような文字は、色を数字で表したものです。

ここでは、LINE風に見えるように、緑、白、薄いグレーを中心にしています。


AppColorsを作る理由

色を直接あちこちに書くと、あとで変更が大変です。

たとえば、緑を変えたいときに、コードの中を全部探す必要があります。

Color(0xFF06C755)

でも、AppColors.lineGreen にしておけば、色の変更は1か所だけで済みます。

AppColors.lineGreen

共通化とは、同じものを1か所にまとめて何度も使えるようにすることです。


Step 3:AppCardを追加する

次に、白いカードUIを作ります。

カードUIとは、情報を入れる白い箱のような見た目です。

main.dart の下の方に、次のコードを追加します。

class AppCard extends StatelessWidget {
  const AppCard({
    super.key,
    required this.child,
    this.onTap,
  });

  final Widget child;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final card = Container(
      decoration: BoxDecoration(
        color: AppColors.card,
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: AppColors.border),
      ),
      padding: const EdgeInsets.all(14),
      child: child,
    );

    if (onTap == null) {
      return card;
    }

    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(14),
        child: card,
      ),
    );
  }
}

StatelessWidget は、状態を持たない画面部品のことです。

状態とは、画面の中で変化するデータのことです。

Widget は、Flutterの画面部品のことです。

child は、カードの中に入れる部品です。


AppCardの使い方

たとえば、ログイン画面ではこのように使えます。

AppCard(
  child: Column(
    children: [
      Text('WorkBoard'),
      Text('チームの会話のように、タスクを共有する'),
    ],
  ),
)

Column は、部品を縦に並べるWidgetです。

AppCard を使うと、白背景、角丸、薄い線、余白を毎回書かなくて済みます。


Step 4:AppTextFieldを追加する

次に、入力欄を共通化します。

入力欄とは、メールアドレスやパスワードを入力する場所のことです。

main.dart の下の方に、次のコードを追加します。

class AppTextField extends StatelessWidget {
  const AppTextField({
    super.key,
    required this.controller,
    required this.label,
    this.obscureText = false,
    this.keyboardType,
    this.maxLines = 1,
  });

  final TextEditingController controller;
  final String label;
  final bool obscureText;
  final TextInputType? keyboardType;
  final int maxLines;

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      obscureText: obscureText,
      keyboardType: keyboardType,
      maxLines: obscureText ? 1 : maxLines,
      decoration: InputDecoration(
        labelText: label,
        border: const OutlineInputBorder(),
      ),
    );
  }
}

TextEditingController は、入力された文字を管理するための道具です。

label は、入力欄に表示する名前です。

obscureText は、パスワードのように文字を隠す設定です。

keyboardType は、メール入力用など、キーボードの種類を指定する設定です。


AppTextFieldの使い方

メールアドレス入力欄は、こう書けます。

AppTextField(
  controller: emailController,
  label: 'メールアドレス',
  keyboardType: TextInputType.emailAddress,
)

パスワード入力欄は、こう書けます。

AppTextField(
  controller: passwordController,
  label: 'パスワード',
  obscureText: true,
)

obscureText: true にすると、入力した文字が見えにくくなります。


Step 5:ErrorBoxを追加する

次に、エラー表示用の部品を作ります。

エラーとは、ログイン失敗や保存失敗など、処理がうまくいかなかった状態のことです。

main.dart の下の方に、次のコードを追加します。

class ErrorBox extends StatelessWidget {
  const ErrorBox({
    super.key,
    required this.message,
  });

  final String message;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: AppColors.danger.withValues(alpha: 0.08),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: AppColors.danger.withValues(alpha: 0.22),
        ),
      ),
      padding: const EdgeInsets.all(12),
      child: Text(
        message,
        style: const TextStyle(
          color: AppColors.danger,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

message は、表示するエラー文です。

AppColors.danger は、削除やエラーに使う赤色です。

alpha は、透明度のことです。


ErrorBoxの使い方

ログイン失敗時などに、次のように使います。

ErrorBox(
  message: 'ログインに失敗しました。',
)

完成コードでは、エラーがあるときだけ表示します。

if (errorText != null) ...[
  const SizedBox(height: 12),
  ErrorBox(message: errorText!),
]

if は、条件に合うときだけ処理する書き方です。

errorText != null は、エラー文がある場合という意味です。

null は、値がない状態のことです。


ここまでの完成コード

このページで必要な共通UIは、次の4つです。

class AppColors {
  static const bg = Color(0xFFF3F4F6);
  static const chatBg = Color(0xFFECEFF1);
  static const card = Colors.white;
  static const lineGreen = Color(0xFF06C755);
  static const lineGreenDark = Color(0xFF00B900);
  static const text = Color(0xFF111111);
  static const subText = Color(0xFF8A8A8A);
  static const border = Color(0xFFE5E5E5);
  static const danger = Color(0xFFE53935);
  static const success = Color(0xFF06C755);
  static const warning = Color(0xFFFFB300);

  static const primary = lineGreen;
}

class AppCard extends StatelessWidget {
  const AppCard({
    super.key,
    required this.child,
    this.onTap,
  });

  final Widget child;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final card = Container(
      decoration: BoxDecoration(
        color: AppColors.card,
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: AppColors.border),
      ),
      padding: const EdgeInsets.all(14),
      child: child,
    );

    if (onTap == null) {
      return card;
    }

    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(14),
        child: card,
      ),
    );
  }
}

class AppTextField extends StatelessWidget {
  const AppTextField({
    super.key,
    required this.controller,
    required this.label,
    this.obscureText = false,
    this.keyboardType,
    this.maxLines = 1,
  });

  final TextEditingController controller;
  final String label;
  final bool obscureText;
  final TextInputType? keyboardType;
  final int maxLines;

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      obscureText: obscureText,
      keyboardType: keyboardType,
      maxLines: obscureText ? 1 : maxLines,
      decoration: InputDecoration(
        labelText: label,
        border: const OutlineInputBorder(),
      ),
    );
  }
}

class ErrorBox extends StatelessWidget {
  const ErrorBox({
    super.key,
    required this.message,
  });

  final String message;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: AppColors.danger.withValues(alpha: 0.08),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: AppColors.danger.withValues(alpha: 0.22),
        ),
      ),
      padding: const EdgeInsets.all(12),
      child: Text(
        message,
        style: const TextStyle(
          color: AppColors.danger,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

Step 6:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 7:実行する

ターミナルで実行します。

flutter run

すでにアプリが起動している場合は、ターミナルで r を押します。

r

r は、ホットリロードです。

ホットリロードとは、アプリを起動したまま画面の変更を反映する機能です。


画面で確認すること

ログイン画面を見ます。

次のようになっていればOKです。

ログインフォームが白いカードの中に入っている
入力欄の見た目がそろっている
エラーが出たときに赤い箱で表示される

まだエラー表示が見えなくても大丈夫です。

ログインに失敗したときなどに表示されます。


1か所だけ変更してみる

試しに、カードの角丸を変えてみます。

AppCard のこの行を探します。

borderRadius: BorderRadius.circular(14),

数字を少し大きくします。

borderRadius: BorderRadius.circular(24),

保存して、ホットリロードします。

r

カードの角が丸くなれば成功です。

確認できたら、元に戻しておきます。

borderRadius: BorderRadius.circular(14),

このように、共通UIを作っておくと、1か所変えるだけで全体の見た目を調整できます。


npmや環境変数はこのページで必要?

このページでは、npmは使いません。

npmとは、Node.jsのパッケージ管理ツールです。

環境変数も使いません。

環境変数とは、パソコン全体で使う設定値のことです。

今回やることは、これだけです。

main.dartを開く
↓
共通UIのコードを追加する
↓
保存する
↓
flutter runで見る

よくあるエラーと直し方

エラー原因直し方
Color isn't definedFlutterのMaterialを読み込んでいないimport 'package:flutter/material.dart'; を確認
StatelessWidget isn't definedMaterialのimport漏れmaterial.dart を読み込む
AppColors isn't definedAppColorsより先に使っている、または未定義AppColorsを追加する
TextEditingController isn't definedMaterialのimport漏れmaterial.dart を読み込む
画面が変わらない保存していないcommand + S
それでも変わらないホットリロードしていないターミナルで r

withValues** でエラーが出る場合**

Flutterのバージョンによっては、次の部分でエラーが出ることがあります。

AppColors.danger.withValues(alpha: 0.08)

その場合は、次のように書き換えてください。

AppColors.danger.withOpacity(0.08)

こちらも同じく透明度を指定する書き方です。

ただし、新しいFlutterでは withValues が使えます。

完成コードでエラーが出ていなければ、そのままでOKです。


最短作業まとめ

読むのが大変な人は、ここだけ見てください。

1. lib/main.dart を開く
2. AppColors を確認する
3. AppCard を追加する
4. AppTextField を追加する
5. ErrorBox を追加する
6. 保存する
7. flutter run

ターミナルで実行:

flutter run

起動中なら:

r

チェックリスト

□ AppColorsを作った
□ AppCardを作った
□ AppTextFieldを作った
□ ErrorBoxを作った
□ 保存した
□ flutter runで起動した
□ ログイン画面のカードを確認した
□ 入力欄の見た目を確認した
□ エラー表示用の部品を確認した

ミニ確認問題

Q1. AppColorsは何のために作りますか?

回答

アプリで使う色を1か所にまとめるためです。

色を変更したいときに、1か所直すだけで済みます。


Q2. AppCardは何のために作りますか?

回答

白いカード型のUIを何度も使えるようにするためです。

ログイン画面、新規登録画面、メンバー一覧などで使えます。


Q3. AppTextFieldは何のために作りますか?

回答

入力欄の見た目と使い方を統一するためです。

メールアドレス、パスワード、名前、所属などの入力欄で使います。


Q4. ErrorBoxは何のために作りますか?

回答

ログイン失敗や保存失敗などのエラーを、見やすく表示するためです。


このページのまとめ

  • 共通UIを作ると、同じ見た目を何度も使える。
  • AppColors は色をまとめる部品。
  • AppCard は白いカードUIを作る部品。
  • AppTextField は入力欄を統一する部品。
  • ErrorBox はエラーを赤く表示する部品。
  • このページではnpmや環境変数は不要。
  • コードを追加したら、保存して、画面の変化を見る。
  • 共通UIを作ると、あとからデザイン変更が楽になる。

次のページでやること

次のページでは、認証分岐を作ります。

AuthGate を使って、ログインしている人にはチーム一覧画面、ログインしていない人にはログイン画面を表示します。

教材トップへ戻る