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

【TaskCard】タスクをLINEの吹き出し風UIで表示する

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

このページでやること

このページでは、タスク1件分を表示する TaskCard を、LINEの吹き出し風UIに整えます。

前のページでは、タスクの status を使って、未対応・進行中・完了に分けました。

今回は、タスク一覧画面で見やすくなるように、タスクをカードではなく「吹き出し」のように表示します。

Firestoreからタスクを取得
↓
TaskCardに渡す
↓
LINEの吹き出し風に表示する

今日のゴール

タスクが次のように表示されることがゴールです。

未対応 優先度:中

ログイン画面を作る
メールアドレスとパスワードの入力欄を作る

担当:test@example.com

見た目は、白い吹き出しのような箱にします。

薄いグレー背景
↓
白い吹き出し
↓
ステータス・優先度・タスク名・説明・担当者を表示

このページで出てくる単語

単語一言説明
TaskCardタスク1件分を表示する部品
吹き出しUIチャットのメッセージのような見た目
Container箱を作るWidget
BoxDecoration背景色・角丸・枠線などを設定するもの
Column縦に並べるWidget
Row横に並べるWidget
statusタスクの進行状況
priorityタスクの優先度

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


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

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

環境変数も設定しません。

このページでやることは、TaskCard の見た目を整えるだけです。

main.dartを開く
↓
TaskCardを探す
↓
吹き出し風UIに変更する
↓
保存する
↓
flutter runで確認する

Step 1:main.dartを開く

次のファイルを開きます。

lib/main.dart

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

code lib/main.dart

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


Step 2:TaskCardを探す

main.dart の中で、次を検索します。

class TaskCard

VS Codeなら、次で検索できます。

command + F

この TaskCard が、タスク1件分の見た目を作っている場所です。


Step 3:TaskCardの役割を確認する

TaskCard は、次の値を受け取ります。

final String title;
final String description;
final String status;
final String priority;
final String assigneeEmail;

それぞれの意味は次の通りです。

画面に出す内容
titleタスク名
descriptionタスク説明
status未対応・進行中・完了
priority優先度
assigneeEmail担当者メール

Step 4:TaskCardを吹き出し風に差し替える

今ある TaskCard を、次のコードに差し替えてください。

class TaskCard extends StatelessWidget {
  const TaskCard({
    super.key,
    required this.title,
    required this.description,
    required this.status,
    required this.priority,
    required this.assigneeEmail,
  });

  final String title;
  final String description;
  final String status;
  final String priority;
  final String assigneeEmail;

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.centerLeft,
      child: Container(
        width: double.infinity,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.only(
            topLeft: Radius.circular(4),
            topRight: Radius.circular(18),
            bottomLeft: Radius.circular(18),
            bottomRight: Radius.circular(18),
          ),
          border: Border.all(color: AppColors.border),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.04),
              blurRadius: 10,
              offset: const Offset(0, 4),
            ),
          ],
        ),
        padding: const EdgeInsets.all(14),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                _Label(text: statusLabel(status)),
                _Label(text: priorityLabel(priority)),
              ],
            ),
            const SizedBox(height: 10),
            Text(
              title,
              style: const TextStyle(
                color: AppColors.text,
                fontSize: 16,
                fontWeight: FontWeight.w900,
              ),
            ),
            if (description.isNotEmpty) ...[
              const SizedBox(height: 6),
              Text(
                description,
                style: const TextStyle(
                  color: AppColors.subText,
                  height: 1.5,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
            if (assigneeEmail.isNotEmpty) ...[
              const SizedBox(height: 12),
              Container(
                decoration: BoxDecoration(
                  color: AppColors.bg,
                  borderRadius: BorderRadius.circular(999),
                ),
                padding: const EdgeInsets.symmetric(
                  horizontal: 10,
                  vertical: 6,
                ),
                child: Text(
                  '担当:$assigneeEmail',
                  style: const TextStyle(
                    color: AppColors.subText,
                    fontSize: 12,
                    fontWeight: FontWeight.w800,
                  ),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  String statusLabel(String value) {
    switch (value) {
      case 'doing':
        return '進行中';
      case 'done':
        return '完了';
      case 'todo':
      default:
        return '未対応';
    }
  }

  String priorityLabel(String value) {
    switch (value) {
      case 'high':
        return '優先度:高';
      case 'low':
        return '優先度:低';
      case 'normal':
      default:
        return '優先度:中';
    }
  }
}

これで、タスクが白い吹き出し風に表示されます。


Step 5:吹き出し風に見せている部分

吹き出し風に見せている中心は、ここです。

borderRadius: const BorderRadius.only(
  topLeft: Radius.circular(4),
  topRight: Radius.circular(18),
  bottomLeft: Radius.circular(18),
  bottomRight: Radius.circular(18),
),

左上だけ少し角丸を小さくしています。

これによって、普通のカードではなく、チャットの吹き出しに近い雰囲気になります。

左上:少し角を小さくする
↓
吹き出しっぽく見える

Step 6:背景色と影を見る

この部分です。

color: Colors.white,
border: Border.all(color: AppColors.border),
boxShadow: [
  BoxShadow(
    color: Colors.black.withValues(alpha: 0.04),
    blurRadius: 10,
    offset: const Offset(0, 4),
  ),
],

意味はこうです。

コード役割
color: Colors.white吹き出しを白にする
border薄い線をつける
boxShadowほんの少し影をつける

背景が AppColors.chatBg の薄いグレーなので、白い吹き出しが見やすくなります。


Step 7:ラベルを横並びにする

ステータスと優先度は、次のように表示します。

Wrap(
  spacing: 8,
  runSpacing: 8,
  children: [
    _Label(text: statusLabel(status)),
    _Label(text: priorityLabel(priority)),
  ],
),

Wrap は、横に並べて、入りきらないときは自動で折り返すWidgetです。

スマホの画面が狭いときでも崩れにくくなります。

未対応 優先度:中

のように表示されます。


Step 8:タスク名を目立たせる

タスク名は、この部分です。

Text(
  title,
  style: const TextStyle(
    color: AppColors.text,
    fontSize: 16,
    fontWeight: FontWeight.w900,
  ),
),

fontWeight: FontWeight.w900 は、文字をかなり太くする設定です。

タスク名は重要なので、太字で見やすくします。


Step 9:説明文は空なら表示しない

説明文は、このように書いています。

if (description.isNotEmpty) ...[
  const SizedBox(height: 6),
  Text(
    description,
    style: const TextStyle(
      color: AppColors.subText,
      height: 1.5,
      fontWeight: FontWeight.w600,
    ),
  ),
],

description.isNotEmpty は、説明文が空ではない場合という意味です。

説明が空のタスクでは、余計な空白を出しません。

説明あり → 表示する
説明なし → 表示しない

Step 10:担当者メールを小さく表示する

担当者は、この部分で表示しています。

if (assigneeEmail.isNotEmpty) ...[
  const SizedBox(height: 12),
  Container(
    decoration: BoxDecoration(
      color: AppColors.bg,
      borderRadius: BorderRadius.circular(999),
    ),
    padding: const EdgeInsets.symmetric(
      horizontal: 10,
      vertical: 6,
    ),
    child: Text(
      '担当:$assigneeEmail',
      style: const TextStyle(
        color: AppColors.subText,
        fontSize: 12,
        fontWeight: FontWeight.w800,
      ),
    ),
  ),
],

担当者メールも、空なら表示しません。

入力されている場合だけ、小さなラベルのように出します。


Step 11:_Labelを確認する

TaskCard の中で _Label を使っています。

まだない場合は、次のコードを追加します。

class _Label extends StatelessWidget {
  const _Label({
    required this.text,
  });

  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: AppColors.bg,
        borderRadius: BorderRadius.circular(999),
        border: Border.all(color: AppColors.border),
      ),
      padding: const EdgeInsets.symmetric(
        horizontal: 10,
        vertical: 4,
      ),
      child: Text(
        text,
        style: const TextStyle(
          color: AppColors.subText,
          fontSize: 12,
          fontWeight: FontWeight.w800,
        ),
      ),
    );
  }
}

_Label は、ステータスや優先度を小さく表示するための部品です。


Step 12:TaskListPageの背景を確認する

TaskListPageScaffold が、次のようになっているか確認します。

return Scaffold(
  backgroundColor: AppColors.chatBg,
  appBar: AppBar(
    title: Text(teamName),
  ),
  body: ...
);

大事なのは、ここです。

backgroundColor: AppColors.chatBg,

薄いグレー背景にすることで、白い吹き出しが見やすくなります。


Step 13:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 14:実行する

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

flutter run

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

r

うまく反映されない場合は、R でホットリスタートします。

R

Step 15:画面を確認する

チームをタップして、タスク一覧画面を開きます。

タスクがある場合、次のような白い吹き出しで表示されればOKです。

未対応 優先度:中

ログイン画面を作る
メールとパスワードの入力欄を作る

担当:test@example.com

まだタスクがない場合は、Firebase Consoleでテスト用タスクを手動作成すると確認できます。


Step 16:Firebase Consoleでテストタスクを作る

Firebase Consoleを開きます。

https://console.firebase.google.com/

次の場所を開きます。

Firestore Database
↓
teams
↓
作成したteamId
↓
tasks

テスト用ドキュメントを1つ作ります。

title: ログイン画面を作る
description: メールとパスワードの入力欄を作る
status: todo
priority: normal
assigneeEmail: test@example.com
assigneeId:
createdBy: 自分のuid

アプリに戻って、吹き出し風に表示されれば成功です。


1か所だけ変更してみる

吹き出しの角丸を少し大きくしてみます。

この部分を探します。

topRight: Radius.circular(18),
bottomLeft: Radius.circular(18),
bottomRight: Radius.circular(18),

次のように変えます。

topRight: Radius.circular(24),
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),

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

r

吹き出しの角が丸くなれば成功です。

確認できたら、好みの数値に戻してOKです。


よくあるエラーと直し方

エラー原因直し方
TaskCard isn't definedTaskCardがないclass TaskCard を追加する
_Label isn't defined_Labelがない_Label を追加する
withValues でエラーFlutterのバージョン差withOpacity(0.04) に変える
タスクが表示されないtasksにデータがないテストタスクを作る
吹き出しに見えない背景が白のままTaskListPageAppColors.chatBg にする
画面が変わらない保存していないcommand + S
ホットリロードで変わらない状態が残っているR でホットリスタート

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

この部分でエラーが出ることがあります。

color: Colors.black.withValues(alpha: 0.04),

その場合は、次のように変更してください。

color: Colors.black.withOpacity(0.04),

どちらも、黒色を少しだけ透明にするための書き方です。


吹き出しに見えないとき

TaskListPage の背景を確認してください。

これは少し見えにくいです。

backgroundColor: AppColors.bg,

LINE風にするなら、こちらがおすすめです。

backgroundColor: AppColors.chatBg,

背景が薄いグレーになり、白い吹き出しが見やすくなります。


最短作業まとめ

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

1. TaskCardを吹き出し風にする

return Align(
  alignment: Alignment.centerLeft,
  child: Container(
    width: double.infinity,
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: const BorderRadius.only(
        topLeft: Radius.circular(4),
        topRight: Radius.circular(18),
        bottomLeft: Radius.circular(18),
        bottomRight: Radius.circular(18),
      ),
      border: Border.all(color: AppColors.border),
    ),
    padding: const EdgeInsets.all(14),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            _Label(text: statusLabel(status)),
            _Label(text: priorityLabel(priority)),
          ],
        ),
        const SizedBox(height: 10),
        Text(title),
      ],
    ),
  ),
);

2. 背景を薄いグレーにする

backgroundColor: AppColors.chatBg,

3. 保存して実行

flutter run

チェックリスト

□ main.dartを開いた
□ TaskCardを探した
□ TaskCardを吹き出し風に変更した
□ Containerを使った
□ 背景色を白にした
□ 角丸を設定した
□ statusLabelを表示した
□ priorityLabelを表示した
□ titleを表示した
□ descriptionがある時だけ表示した
□ assigneeEmailがある時だけ表示した
□ _Labelを確認した
□ TaskListPageの背景をAppColors.chatBgにした
□ 保存した
□ flutter runで確認した

ミニ確認問題

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

回答

タスク1件分を見やすく表示するためです。

今回は、LINEの吹き出し風UIで表示します。


Q2. 吹き出し風に見せるために使っている主な設定は何ですか?

回答

ContainerBoxDecorationborderRadius、白背景、薄い影です。


Q3. 説明文が空のときはどうしますか?

回答

表示しません。

if (description.isNotEmpty) で、説明文があるときだけ表示します。


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

回答

必要ありません。

このページでは、TaskCard の見た目を整えるだけです。


このページのまとめ

  • TaskCard はタスク1件分を表示する部品。
  • LINE風にするため、薄いグレー背景に白い吹き出しを置く。
  • borderRadius を調整して、吹き出しのような形にする。
  • statuspriority は小さなラベルで表示する。
  • title は太字で見やすく表示する。
  • description は空でなければ表示する。
  • assigneeEmail も空でなければ表示する。
  • このページではnpmや環境変数は不要。

次のページでやること

次のページでは、タスク作成UIを作ります。

右下の + ボタンからボトムシートを開き、タスク名・説明・優先度・担当者メールを入力できるようにします。

教材トップへ戻る