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

下部のユーザー名・キャプション・音源表示を作る

この節で学ぶこと

前回の 4-11 では、コメントボタンを押したときに、下からTikTok風のコメント入力画面を表示する方法を学びました。

コメントボタンを押す
↓
BottomSheetが開く
↓
コメント一覧が表示される
↓
TextFieldでコメントを入力する
↓
送信するとコメントが追加される

今回の 4-12 では、動画の下部に表示される情報を作ります。

TikTok風アプリでは、動画の下に次のような情報が表示されます。

@pet_cafe_diary
小さな命の動きは、見ているだけで少しやさしい気持ちになる。
PET ♪ Healing Cafe Sound - Pet Cafe Diary

この部分は、ただ文字を置いているだけに見えるかもしれません。

しかし、実際にはかなり重要です。

なぜなら、下部情報には「誰の投稿か」「どんな内容か」「どんな音源か」がまとまっているからです。

この節で大切なのは、次の一文です。

下部情報は、動画データをユーザーに伝えるための情報表示エリアである。

まず下部情報を分解する

TikTok風アプリの下部情報は、いくつかの部品に分けられます。

BottomVideoInfo
├─ ユーザー名
├─ キャプション
└─ カテゴリ + 音源情報

それぞれの役割は、次の通りです。

表示役割
ユーザー名誰の投稿かを示す
キャプション動画の説明文を表示する
カテゴリ動画のテーマを短く示す
音源情報使用されている音源名を表示する

今回の完成イメージは、次のようなものです。

@pet_cafe_diary
小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日はペットカフェ風の癒し動画。
PET ♪ Healing Cafe Sound - Pet Cafe Diary

動画背景の上に白い文字を重ねるため、文字色や影も工夫します。

下部情報はどのデータから作るのか

前の節までに、動画データを ShortVideo classとして設計しました。

今回使う主なpropertyは、次の4つです。

final String userName;
final String caption;
final String musicTitle;
final String categoryLabel;

それぞれ、画面では次のように使います。

property画面での表示例役割
userName@pet_cafe_diary投稿者名
caption小さな命の動きは...説明文
musicTitleHealing Cafe Sound...音源名
categoryLabelPETカテゴリラベル

ここでも、基本の流れは同じです。

ShortVideoデータ
↓
BottomVideoInfoに渡す
↓
TextやRowで表示する

新しい言葉:情報設計とは何か

ここで、少し大切な考え方として「情報設計」という言葉を紹介します。

情報設計とは、どの情報を、どの順番で、どのように見せるかを考えることです。

初心者向けには、次のように理解してください。

情報設計 = ユーザーが分かりやすい順番で情報を並べること

TikTok風の下部情報では、次の順番が自然です。

1. 誰の投稿か
2. どんな内容か
3. どんなカテゴリ・音源か

そのため、上から順に、

ユーザー名
キャプション
カテゴリ + 音源

と並べます。

Columnで縦に並べる

下部情報は、縦方向に並んでいます。

そのため、Column を使います。

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('@pet_cafe_diary'),
    Text('小さな命の動きは...'),
    Row(
      children: [
        Text('PET'),
        Icon(Icons.music_note_rounded),
        Text('Healing Cafe Sound'),
      ],
    ),
  ],
)

構造で見ると、次のようになります。

Column
├─ Text ユーザー名
├─ Text キャプション
└─ Row カテゴリ + 音源

Column は縦に並べるWidgetです。

Row は横に並べるWidgetです。

今回の下部情報では、この2つを組み合わせます。

crossAxisAlignmentとは何か

Column で文字を左揃えにしたい場合は、crossAxisAlignment を使います。

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    ...
  ],
)

CrossAxisAlignment.start は、左揃えという意味です。

TikTok風アプリでは、下部情報は左下に配置されるため、左揃えが自然です。

@pet_cafe_diary
小さな命の動きは...
PET ♪ Healing Cafe Sound

中央揃えではなく、左揃えにすることで、投稿情報として読みやすくなります。

まず下部情報だけ作る

最初に、動画や右側ボタンを考えず、下部情報だけを作ってみます。

DartPadに次のコードを貼り付けてください。

import 'package:flutter/material.dart';

void main() {
  runApp(const BottomInfoPracticeApp());
}

class BottomInfoPracticeApp extends StatelessWidget {
  const BottomInfoPracticeApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: BottomInfoPracticePage(),
    );
  }
}

class ShortVideo {
  const ShortVideo({
    required this.userName,
    required this.caption,
    required this.musicTitle,
    required this.categoryLabel,
  });

  final String userName;
  final String caption;
  final String musicTitle;
  final String categoryLabel;
}

const video = ShortVideo(
  userName: 'pet_cafe_diary',
  caption: '小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日はペットカフェ風の癒し動画。',
  musicTitle: 'Healing Cafe Sound - Pet Cafe Diary',
  categoryLabel: 'PET',
);

class BottomInfoPracticePage extends StatelessWidget {
  const BottomInfoPracticePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: BottomVideoInfo(video: video),
        ),
      ),
    );
  }
}

class BottomVideoInfo extends StatelessWidget {
  const BottomVideoInfo({
    super.key,
    required this.video,
  });

  final ShortVideo video;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '@${video.userName}',
          style: const TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          video.caption,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 14,
            height: 1.35,
          ),
        ),
        const SizedBox(height: 10),
        Row(
          children: [
            Text(
              video.categoryLabel,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 12,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(width: 8),
            const Icon(
              Icons.music_note_rounded,
              color: Colors.white,
              size: 17,
            ),
            const SizedBox(width: 5),
            Text(
              video.musicTitle,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 13,
              ),
            ),
          ],
        ),
      ],
    );
  }
}

実行して確認すること

実行すると、黒背景の上に下部情報が表示されます。

@pet_cafe_diary
小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日はペットカフェ風の癒し動画。
PET ♪ Healing Cafe Sound - Pet Cafe Diary

この段階では、まだ画面下部に固定していません。

まずは、BottomVideoInfo という部品そのものを理解することが目的です。

@${video.userName} の意味

次のコードを見てください。

Text(
  '@${video.userName}',
)

これは、ユーザー名の前に @ をつけて表示しています。

video.userName が、

pet_cafe_diary

なら、画面には次のように表示されます。

@pet_cafe_diary

'@${video.userName}' のような書き方は、文字列の中に変数の値を埋め込む方法です。

新しい言葉:文字列補間とは何か

'@${video.userName}' のように、文字列の中に変数を入れる書き方を、文字列補間と呼びます。

初心者向けには、次のように理解してください。

文字列補間 = 文字の中に変数の値を入れる書き方

例です。

final name = 'pet_cafe_diary';
print('@$name');

結果は次のようになります。

@pet_cafe_diary

@${video.userName} のように {} を使うと、propertyにも対応しやすくなります。

SizedBoxで余白を作る

次のようなコードが出てきます。

const SizedBox(height: 8),

これは、縦方向の余白を作っています。

ユーザー名
↓ 8pxの余白
キャプション

SizedBox は、余白を作るときによく使います。

書き方意味
SizedBox(height: 8)縦方向に8pxの余白
SizedBox(width: 8)横方向に8pxの余白

UIでは、情報同士が詰まりすぎると読みにくくなります。

そのため、適度な余白を入れることが大切です。

height: 1.35とは何か

キャプションの TextStyle には、height があります。

style: const TextStyle(
  color: Colors.white,
  fontSize: 14,
  height: 1.35,
),

この height は、行の高さです。

キャプションが2行以上になったとき、行間を調整できます。

heightが小さい
↓
行間が詰まる

heightが大きい
↓
行間が広がる

文章が読みやすくなるように、1.3 から 1.5 くらいを使うことがあります。

長い文字がはみ出す問題

音源名が長い場合、画面からはみ出すことがあります。

たとえば、次のような音源名です。

Healing Cafe Sound - Pet Cafe Diary Very Long Long Music Title

このまま Row に入れると、横幅が足りなくなり、エラーやはみ出しの原因になることがあります。

そこで使うのが、Expandedoverflow です。

新しい言葉:Expandedとは何か

Expanded は、RowColumn の中で、残りのスペースを使うためのWidgetです。

Expanded(
  child: Text(
    video.musicTitle,
  ),
)

Row の中で Expanded を使うと、残っている横幅の中でTextを表示してくれます。

Row
├─ PET
├─ ♪
└─ Expanded 音源名

音源名が長い場合でも、残り幅の中に収めやすくなります。

新しい言葉:overflowとは何か

overflow は、文字が入りきらないときに、どう表示するかを決める設定です。

overflow: TextOverflow.ellipsis,

TextOverflow.ellipsis は、入りきらない文字を ... で省略する設定です。

Healing Cafe Sound - Pet Cafe Diary Very Long...

TikTok風アプリでは、音源名やキャプションが長くなりすぎることがあります。

そのため、maxLinesoverflow を使って、見た目を崩さないようにします。

下部情報をより実用的にするコード

次に、ExpandedmaxLinesoverflow、カテゴリラベルの装飾を追加します。

DartPadに次のコードを貼り付けてください。

import 'package:flutter/material.dart';

void main() {
  runApp(const BottomInfoStyledApp());
}

class BottomInfoStyledApp extends StatelessWidget {
  const BottomInfoStyledApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: BottomInfoStyledPage(),
    );
  }
}

class ShortVideo {
  const ShortVideo({
    required this.userName,
    required this.caption,
    required this.musicTitle,
    required this.categoryLabel,
  });

  final String userName;
  final String caption;
  final String musicTitle;
  final String categoryLabel;
}

const video = ShortVideo(
  userName: 'pet_cafe_diary',
  caption: '小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日はペットカフェ風の癒し動画。何気ない日常の一瞬を、ショート動画として残します。',
  musicTitle: 'Healing Cafe Sound - Pet Cafe Diary Very Long Music Title',
  categoryLabel: 'PET',
);

class BottomInfoStyledPage extends StatelessWidget {
  const BottomInfoStyledPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: const [
          Positioned.fill(
            child: VideoMockBackground(),
          ),
          Positioned(
            left: 16,
            right: 96,
            bottom: 36,
            child: BottomVideoInfo(video: video),
          ),
        ],
      ),
    );
  }
}

class VideoMockBackground extends StatelessWidget {
  const VideoMockBackground({super.key});

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            Color(0xFFE91E63),
            Colors.black,
          ],
        ),
      ),
      child: Center(
        child: Container(
          width: 240,
          height: 360,
          decoration: BoxDecoration(
            color: Colors.white12,
            borderRadius: BorderRadius.circular(28),
            border: Border.all(
              color: Colors.white24,
            ),
          ),
          child: const Center(
            child: Text(
              'PET',
              style: TextStyle(
                color: Colors.white,
                fontSize: 40,
                fontWeight: FontWeight.bold,
                letterSpacing: 2,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class BottomVideoInfo extends StatelessWidget {
  const BottomVideoInfo({
    super.key,
    required this.video,
  });

  final ShortVideo video;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      top: false,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '@${video.userName}',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
              shadows: [
                Shadow(
                  color: Colors.black87,
                  blurRadius: 8,
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          Text(
            video.caption,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 14,
              height: 1.35,
              shadows: [
                Shadow(
                  color: Colors.black87,
                  blurRadius: 8,
                ),
              ],
            ),
          ),
          const SizedBox(height: 10),
          Row(
            children: [
              Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 7,
                  vertical: 3,
                ),
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.16),
                  borderRadius: BorderRadius.circular(999),
                  border: Border.all(
                    color: Colors.white.withOpacity(0.18),
                  ),
                ),
                child: Text(
                  video.categoryLabel,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              const SizedBox(width: 8),
              const Icon(
                Icons.music_note_rounded,
                color: Colors.white,
                size: 17,
              ),
              const SizedBox(width: 5),
              Expanded(
                child: Text(
                  video.musicTitle,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 13,
                    fontWeight: FontWeight.w600,
                    shadows: [
                      Shadow(
                        color: Colors.black87,
                        blurRadius: 8,
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

実行して確認すること

実行すると、画面左下に投稿情報が表示されます。

長いキャプションは2行まで表示され、それ以上は ... で省略されます。

長い音源名も1行で省略されます。

@pet_cafe_diary
小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日は...
PET ♪ Healing Cafe Sound - Pet Cafe Diary Very Long...

ここで確認するポイントは、次の3つです。

1. 長い文章が画面からはみ出していない
2. 右側ボタンが入るスペースを空けている
3. 動画背景の上でも文字が読みやすい

Positionedで左下に配置する

今回、下部情報は Stack の中で Positioned を使って配置しています。

Positioned(
  left: 16,
  right: 96,
  bottom: 36,
  child: BottomVideoInfo(video: video),
),

これは、次の意味です。

左から16px
右から96px
下から36px
の範囲にBottomVideoInfoを置く

右側を 96px 空けているのは、右側アクションバーと重ならないようにするためです。

下部情報
↓
右側ボタンに重ならないように右側に余白を取る

TikTok風UIでは、右側にいいね・コメント・保存・共有ボタンがあるため、下部テキストを画面いっぱいに広げると重なってしまいます。

そのため、right: 96 のように右側に余白を取ります。

SafeAreaを使う理由

BottomVideoInfo の中では、SafeArea を使っています。

return SafeArea(
  top: false,
  child: Column(...),
);

SafeArea は、スマホ画面の端やホームインジケーターにUIが重ならないようにするWidgetです。

下部情報は画面下に近いため、端末によっては見切れる可能性があります。

SafeArea を使うと、安全な範囲に収まりやすくなります。

SafeArea
↓
画面端やノッチ、ホームバーに重なりにくくする

top: false は、上方向のSafeAreaは気にしないという意味です。

今回のWidgetは下部に置くため、上方向の余白は不要です。

文字に影をつける理由

動画背景の上に白い文字を置くと、映像によっては見づらくなります。

そのため、文字に影をつけています。

shadows: [
  Shadow(
    color: Colors.black87,
    blurRadius: 8,
  ),
],

これは、文字の後ろにぼかした黒い影をつける設定です。

白文字だけ
↓
明るい背景では読みにくい

白文字 + 黒い影
↓
明るい背景でも読みやすい

TikTok風UIでは、動画の内容が常に変わるため、文字の可読性を上げる工夫が必要です。

カテゴリラベルをContainerで作る

カテゴリラベルは、Container で小さなバッジ風にしています。

Container(
  padding: const EdgeInsets.symmetric(
    horizontal: 7,
    vertical: 3,
  ),
  decoration: BoxDecoration(
    color: Colors.white.withOpacity(0.16),
    borderRadius: BorderRadius.circular(999),
    border: Border.all(
      color: Colors.white.withOpacity(0.18),
    ),
  ),
  child: Text(
    video.categoryLabel,
    style: const TextStyle(
      color: Colors.white,
      fontSize: 11,
      fontWeight: FontWeight.bold,
    ),
  ),
)

Container を使うことで、背景色、余白、角丸、枠線をつけられます。

PET
↓
小さな丸みのあるラベルとして表示

カテゴリは必須ではありませんが、動画のテーマが分かりやすくなります。

BorderRadius.circular(999)とは何か

カテゴリラベルでは、次のように書いています。

borderRadius: BorderRadius.circular(999),

これは、角を大きく丸める指定です。

数字を大きくすると、カプセル型のような丸いラベルになります。

角が少し丸い
↓
角が大きく丸い
↓
カプセル型

小さなラベルやボタンを丸くしたいときによく使います。

Rowでカテゴリと音源を横に並べる

カテゴリと音源情報は、横に並べています。

Row(
  children: [
    カテゴリラベル,
    SizedBox(width: 8),
    音符アイコン,
    SizedBox(width: 5),
    Expanded(
      child: Text(video.musicTitle),
    ),
  ],
)

構造は次の通りです。

Row
├─ PET
├─ 余白
├─ ♪
├─ 余白
└─ 音源名

このように、横並びの情報には Row を使います。

maxLinesとは何か

Text では、maxLines を使っています。

maxLines: 2,

これは、最大で何行まで表示するかを決める設定です。

キャプションでは2行まで表示しています。

Text(
  video.caption,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

音源名では1行まで表示しています。

Text(
  video.musicTitle,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
)

長い文章を無限に表示すると、画面が崩れます。

そのため、表示行数を制限します。

完成アプリとのつながり

最終的なTikTok風アプリでは、BottomVideoInfoShortVideoPage の中で使います。

Positioned(
  left: 16,
  right: 86,
  bottom: 28,
  child: BottomVideoInfo(video: video),
),

video は、PageView.builder で取り出した動画データです。

final video = videos[videoIndex];

つまり、ページが切り替わるたびに、下部情報も変わります。

videos[0]
↓
@pet_cafe_diary

videos[1]
↓
@food_and_nature

videos[2]
↓
@daily_pet_room

同じ BottomVideoInfo でも、渡すデータによって表示が変わります。

データとUIの関係をもう一度整理する

今回の下部情報は、固定文字ではなく、ShortVideo データから作っています。

Text('@${video.userName}')
Text(video.caption)
Text(video.musicTitle)
Text(video.categoryLabel)

この考え方がとても重要です。

データを変える
↓
UIの表示が変わる

たとえば、動画データを次のように変えます。

userName: 'food_and_nature',
caption: 'おいしいものを探す旅の途中で出会った、自然の小さなリズム。',
musicTitle: 'Kitchen Walk - Food & Nature',
categoryLabel: 'FOOD',

すると、同じ BottomVideoInfo を使っていても、表示が変わります。

@food_and_nature
おいしいものを探す旅の途中で出会った...
FOOD ♪ Kitchen Walk - Food & Nature

これが、Widgetを部品化し、データを渡すメリットです。

手を動かす練習1:ユーザー名を変える

次の部分を探してください。

userName: 'pet_cafe_diary',

これを次のように変えてみましょう。

userName: 'food_trip_japan',

画面の表示が、

@food_trip_japan

に変わります。

手を動かす練習2:キャプションを短くする

次の caption を短くしてみます。

caption: '今日はペットカフェ風の癒し動画。',

短くすると、省略記号が出なくなります。

この練習で、maxLinesoverflow がどのように働くかを確認できます。

手を動かす練習3:キャプションの最大行数を変える

次の部分を探してください。

maxLines: 2,

これを次のように変えてみます。

maxLines: 3,

キャプションが最大3行まで表示されるようになります。

ただし、表示する行数を増やすと、画面下部のスペースを多く使います。

TikTok風UIでは、動画を邪魔しすぎないバランスが大切です。

手を動かす練習4:カテゴリラベルを変える

次の部分を探してください。

categoryLabel: 'PET',

これを次のように変えてみます。

categoryLabel: 'FOOD',

カテゴリラベルの表示が変わります。

手を動かす練習5:右側余白を変える

次の部分を探してください。

right: 96,

これを次のように変えてみます。

right: 40,

下部情報の横幅が広がります。

ただし、右側アクションバーと重なりやすくなります。

TikTok風UIでは、右側にボタンがあることを想定して余白を取る必要があります。

よくあるつまずき1:Rowの中で文字がはみ出す

音源名が長い場合、Row の中で文字がはみ出すことがあります。

その場合は、Expanded で包みます。

Expanded(
  child: Text(
    video.musicTitle,
    maxLines: 1,
    overflow: TextOverflow.ellipsis,
  ),
)

Expanded がないと、Textが必要なだけ横幅を取ろうとして、画面からはみ出すことがあります。

よくあるつまずき2:文字が背景に埋もれる

動画背景が明るい場合、白文字が読みにくくなります。

対策として、文字に影をつけます。

shadows: [
  Shadow(
    color: Colors.black87,
    blurRadius: 8,
  ),
],

また、前の節で作ったような黒いグラデーションを動画の上に重ねるのも有効です。

動画
↓
黒グラデーション
↓
白文字

よくあるつまずき3:下部情報が右側ボタンに重なる

Positioned で下部情報を置くときに、right を指定していないと、右側ボタンと重なることがあります。

Positioned(
  left: 16,
  right: 96,
  bottom: 36,
  child: BottomVideoInfo(video: video),
)

right: 96 を入れることで、右側に余白を作れます。

よくあるつまずき4:constが使えない

BottomVideoInfo(video: video) のように、変数を渡す場合は、const をつけられないことがあります。

BottomVideoInfo(video: video)

video は実行中に渡される値なので、固定ではありません。

そのため、const BottomVideoInfo(video: video) でエラーになる場合があります。

エラーになったら、const を外してください。

よくあるつまずき5:SafeAreaの意味が分からない

下部情報は画面下に近いため、端末によってはホームバーや画面端と重なる可能性があります。

SafeArea を使うと、見切れを避けやすくなります。

SafeArea(
  top: false,
  child: Column(...),
)

top: false は、上側の余白は気にしないという意味です。

この節の確認問題

確認問題1

下部情報には、どのような情報を表示しますか。

答え

ユーザー名、キャプション、カテゴリ、音源情報を表示します。

確認問題2

BottomVideoInfoShortVideo データを渡す理由は何ですか。

答え

動画ごとに、ユーザー名やキャプション、音源情報を変えて表示するためです。

確認問題3

Column は何のために使いましたか。

答え

ユーザー名、キャプション、カテゴリ・音源情報を縦に並べるために使いました。

確認問題4

Row は何のために使いましたか。

答え

カテゴリラベル、音符アイコン、音源名を横に並べるために使いました。

確認問題5

Expanded はなぜ必要ですか。

答え

長い音源名が画面からはみ出さないように、残りの横幅の中で表示するためです。

確認問題6

overflow: TextOverflow.ellipsis は何をしますか。

答え

文字が入りきらないときに、末尾を ... で省略します。

確認問題7

下部情報を配置するとき、right: 96 のように右側余白を取る理由は何ですか。

答え

右側アクションバーと下部テキストが重ならないようにするためです。

この節のまとめ

この節では、TikTok風アプリの下部に表示するユーザー名・キャプション・音源情報を作りました。

下部情報は、次のような構造です。

BottomVideoInfo
├─ ユーザー名
├─ キャプション
└─ カテゴリ + 音源情報

Column で縦に並べ、音源情報の部分だけ Row で横に並べました。

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('@${video.userName}'),
    Text(video.caption),
    Row(
      children: [
        Text(video.categoryLabel),
        Icon(Icons.music_note_rounded),
        Expanded(
          child: Text(video.musicTitle),
        ),
      ],
    ),
  ],
)

また、長い文章がはみ出さないように、maxLinesoverflowExpanded を使いました。

maxLines: 1,
overflow: TextOverflow.ellipsis,

この節で一番大切なのは、次の一文です。

下部情報は、ShortVideoデータを受け取り、ユーザーが理解しやすい順番で表示するWidgetである。

次の節では、これまで作ってきた部品を整理し、Widgetを分割して読みやすいコードに整えていきます。

教材トップへ戻る