
Flutterの画面はWidgetでできている
この節で学ぶこと
前回の 4-1 では、TikTok風アプリをいきなり作り始めるのではなく、まず画面を部品に分けて考えることを学びました。
今回の 4-2 では、Flutterで画面を作るときの基本である Widget について学びます。
Flutterでは、画面に表示されるものは、ほとんどすべて Widget です。
文字もWidgetです。
アイコンもWidgetです。
背景もWidgetです。
動画を表示する部分もWidgetです。
ボタンもWidgetです。
余白もWidgetです。
つまり、Flutterの画面づくりは、Widgetという部品を組み合わせていく作業です。
この節で大切なのは、次の一文です。
Flutterの画面は、Widgetという小さな部品を組み合わせて作られている。
まずWidgetとは何か
Widget とは、Flutterで画面を作るための部品です。
初心者向けに言うと、次のように理解すると分かりやすいです。
Widget = 画面に置く部品
たとえば、TikTok風アプリには、次のような部品があります。
動画
文字
アイコン
ボタン
ユーザー画像
コメント欄
背景
余白
Flutterでは、これらをWidgetとして作っていきます。
| 画面にあるもの | Flutterでの考え方 |
|---|---|
| ユーザー名 | Text Widget |
| いいねアイコン | Icon Widget |
| 右側のボタン一覧 | Column Widget |
| 下部の説明文 | Text Widget |
| 動画の上に重ねる配置 | Stack Widget |
| コメント入力欄 | TextField Widget |
つまり、画面を見たときに「これは何の部品だろう」と考えることが、Flutterの第一歩です。
TikTok風アプリをWidgetとして見る
TikTok風の画面を、Widgetのまとまりとして見ると、次のようになります。
ShortVideoPage
├─ VideoBackground
├─ VideoGradientOverlay
├─ TopNavigation
├─ PageIndicator
├─ RightActionBar
└─ BottomVideoInfo
これは、1画面分の動画ページを部品に分けたものです。
さらに、右側のアクションバーも分解できます。
RightActionBar
├─ ProfileButton
├─ ActionButton いいね
├─ ActionButton コメント
├─ ActionButton 保存
├─ ActionButton 共有
└─ SpinningDisc
そして、ActionButton はさらに小さく見ると、次のようになります。
ActionButton
├─ Icon
└─ Text
このように、Flutterでは大きな画面を小さなWidgetに分けて考えます。
画面全体
↓
大きな部品
↓
小さな部品
↓
TextやIconなどの基本Widget
Widgetは入れ子になる
FlutterのWidgetは、よく「入れ子」になります。
入れ子とは、部品の中に部品が入っている状態です。
たとえば、中央に文字を表示するだけのコードを見てみます。
Center(
child: Text('Hello Flutter'),
)
これは、次のような構造です。
Center
└─ Text
Center の中に Text が入っています。
Center は、子どものWidgetを中央に置く部品です。
Text は、文字を表示する部品です。
つまり、このコードは次の意味になります。
TextをCenterの中に入れる。
CenterがTextを中央に配置する。
childとは何か
ここで新しい言葉が出てきます。
child です。
child は、Widgetの中に入れる子どものWidgetです。
Center(
child: Text('Hello Flutter'),
)
この場合、Center の child が Text です。
| コード | 意味 |
|---|---|
Center | 中央に配置するWidget |
child | 中に入れるWidget |
Text | 表示したい文字 |
初心者向けには、次のように覚えてください。
child = そのWidgetの中に1つだけ入れる部品
childrenとは何か
child と似た言葉に、children があります。
children は、複数の子どもWidgetを入れるときに使います。
たとえば、縦に複数の文字を並べる場合です。
Column(
children: [
Text('フォロー中'),
Text('おすすめ'),
Text('検索'),
],
)
この構造は、次のようになります。
Column
├─ Text('フォロー中')
├─ Text('おすすめ')
└─ Text('検索')
Column は、複数のWidgetを縦に並べるWidgetです。
そのため、child ではなく、children を使います。
| 書き方 | 意味 |
|---|---|
child | 子どもWidgetが1つ |
children | 子どもWidgetが複数 |
ここは初心者がよく混乱するところです。
1つ入れるなら child
複数入れるなら children
と覚えると分かりやすいです。
TikTok風UIでよく使う基本Widget
今回のTikTok風アプリでは、いくつかの基本Widgetをよく使います。
まずは、名前と役割をざっくり覚えましょう。
| Widget | 役割 | TikTok風アプリでの使い方 |
|---|---|---|
Text | 文字を表示する | ユーザー名、キャプション、数字 |
Icon | アイコンを表示する | ハート、コメント、保存、共有 |
Container | 箱を作る | 背景、丸いアイコン、ラベル |
Row | 横に並べる | 音符アイコンと音源名 |
Column | 縦に並べる | 右側ボタン一覧 |
Stack | 重ねる | 動画の上に文字やボタンを重ねる |
Positioned | 位置を指定する | 右側ボタンや下部情報の配置 |
GestureDetector | タップを検知する | いいねボタンや動画タップ |
PageView | ページを切り替える | 縦スワイプ動画 |
TextField | 文字入力する | コメント入力欄 |
全部を一度に覚えなくても大丈夫です。
この章では、実際にアプリを作りながら少しずつ使います。
Text Widget
Text は、文字を表示するWidgetです。
TikTok風アプリでは、ユーザー名やキャプションを表示するために使います。
Text(
'@pet_cafe_diary',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
)
このコードでは、次のような文字を表示します。
@pet_cafe_diary
style を使うと、文字の色や大きさを変えられます。
| 指定 | 意味 |
|---|---|
color | 文字色 |
fontSize | 文字サイズ |
fontWeight | 文字の太さ |
Icon Widget
Icon は、アイコンを表示するWidgetです。
TikTok風アプリでは、右側のボタンで使います。
Icon(
Icons.favorite_rounded,
color: Colors.white,
size: 34,
)
これは、ハートアイコンを表示します。
いいね済みの状態なら、色を変えることもできます。
Icon(
Icons.favorite_rounded,
color: Colors.pink,
size: 34,
)
このように、状態によって見た目を変えることができます。
これは後の節で、setState と組み合わせて使います。
Container Widget
Container は、箱を作るWidgetです。
背景色をつけたり、サイズを指定したり、丸くしたりできます。
たとえば、丸いプロフィールアイコンの土台を作るときに使います。
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.pink,
shape: BoxShape.circle,
),
child: Icon(
Icons.person,
color: Colors.white,
),
)
これは、ピンク色の丸い箱の中に、人のアイコンを置くコードです。
Container
└─ Icon
Container は、Flutterでとてもよく使います。
Row Widget
Row は、Widgetを横に並べるためのWidgetです。
TikTok風アプリでは、音符アイコンと音源名を横に並べるときに使います。
Row(
children: [
Icon(Icons.music_note, color: Colors.white),
SizedBox(width: 6),
Text(
'Healing Cafe Sound',
style: TextStyle(color: Colors.white),
),
],
)
構造は、次のようになります。
Row
├─ Icon
├─ SizedBox
└─ Text
SizedBox(width: 6) は、横方向の余白です。
Column Widget
Column は、Widgetを縦に並べるためのWidgetです。
TikTok風アプリでは、右側のボタン一覧に使います。
Column(
children: [
Icon(Icons.favorite, color: Colors.white),
Text('1.3万', style: TextStyle(color: Colors.white)),
Icon(Icons.comment, color: Colors.white),
Text('324', style: TextStyle(color: Colors.white)),
],
)
構造は、次のようになります。
Column
├─ Icon
├─ Text
├─ Icon
└─ Text
縦に並べたいときは Column。
横に並べたいときは Row。
まずはこの2つをしっかり覚えましょう。
Stack Widget
Stack は、Widgetを重ねるためのWidgetです。
TikTok風UIでは、とても重要です。
なぜなら、TikTok風の画面は「動画の上に文字やボタンが重なっている」からです。
背景動画
その上にグラデーション
その上に右側ボタン
その上に下部テキスト
Flutterでは、これを Stack で作ります。
Stack(
children: [
VideoBackground(),
VideoGradientOverlay(),
RightActionBar(),
BottomVideoInfo(),
],
)
構造は、次のようになります。
Stack
├─ VideoBackground
├─ VideoGradientOverlay
├─ RightActionBar
└─ BottomVideoInfo
Stack の中では、後に書いたWidgetほど上に重なります。
つまり、背景動画を先に書き、その上にボタンや文字を書きます。
Positioned Widget
Positioned は、Stack の中で位置を指定するためのWidgetです。
たとえば、右側にボタンを置きたい場合です。
Positioned(
right: 12,
bottom: 92,
child: RightActionBar(),
)
これは、次の意味です。
右から12px
下から92px
の位置にRightActionBarを置く
TikTok風アプリでは、次のような配置に使います。
| 配置したいもの | Positionedの使い方 |
|---|---|
| 右側ボタン | right と bottom |
| 下部投稿情報 | left、right、bottom |
| 上部ナビ | top、left、right |
| ページ表示 | left、top |
GestureDetector Widget
GestureDetector は、タップなどの操作を検知するWidgetです。
たとえば、いいねボタンを押したときに反応させたい場合に使います。
GestureDetector(
onTap: () {
print('いいねが押されました');
},
child: Icon(
Icons.favorite,
color: Colors.white,
),
)
このコードでは、ハートアイコンをタップすると、onTap の中の処理が実行されます。
| 用語 | 意味 |
|---|---|
GestureDetector | タップなどを検知するWidget |
onTap | タップされたときに実行する処理 |
child | タップ対象になるWidget |
TikTok風アプリでは、次のような操作に使います。
いいねを押す
コメントを押す
保存を押す
共有を押す
動画をタップして再生・停止する
StatelessWidgetとは何か
Flutterで自分のWidgetを作るとき、よく StatelessWidget が出てきます。
class TopNavigation extends StatelessWidget {
const TopNavigation({super.key});
@override
Widget build(BuildContext context) {
return Text('おすすめ');
}
}
StatelessWidget は、状態を持たないWidgetです。
初心者向けには、次のように理解してください。
StatelessWidget = 表示が自分で変化しないWidget
たとえば、上部の「フォロー中」「おすすめ」という表示は、今の段階では自分で変化しません。
そのような部品は、StatelessWidget で作れます。
StatefulWidgetとは何か
一方、画面の中には、操作によって変化するものがあります。
たとえば、いいねボタンです。
押していない
↓
白いハート
押した
↓
ピンクのハート
このように、状態が変わる画面では StatefulWidget を使います。
class ShortVideoHomePage extends StatefulWidget {
const ShortVideoHomePage({super.key});
@override
State<ShortVideoHomePage> createState() => _ShortVideoHomePageState();
}
StatefulWidget は、状態を持つWidgetです。
初心者向けには、次のように理解してください。
StatefulWidget = ユーザー操作などで表示が変わるWidget
TikTok風アプリでは、次のようなものが状態です。
| 状態 | 例 |
|---|---|
| 今見ている動画 | currentPageIndex |
| いいねした動画 | likedVideoIndexes |
| 保存した動画 | savedVideoIndexes |
| 共有した動画 | sharedVideoIndexes |
| コメント一覧 | commentMap |
| 動画の再生状態 | 再生中 / 停止中 |
buildメソッドとは何か
Widgetの中には、build というメソッドがあります。
@override
Widget build(BuildContext context) {
return Text('Hello');
}
build は、そのWidgetがどんな見た目になるかを決める場所です。
初心者向けには、次のように覚えてください。
buildメソッド = 画面の見た目を作る場所
たとえば、次のWidgetは、画面に文字を表示します。
class SimpleText extends StatelessWidget {
const SimpleText({super.key});
@override
Widget build(BuildContext context) {
return const Text('こんにちは');
}
}
このWidgetを画面に置くと、こんにちは と表示されます。
この節で手を動かすコード
ここでは、TikTok風アプリの画面をまだ作り込みすぎず、Widgetの基本を確認します。
DartPadに次のコードを貼り付けて実行してください。
import 'package:flutter/material.dart';
void main() {
runApp(const TikTokWidgetPracticeApp());
}
class TikTokWidgetPracticeApp extends StatelessWidget {
const TikTokWidgetPracticeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: WidgetPracticePage(),
);
}
}
class WidgetPracticePage extends StatelessWidget {
const WidgetPracticePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: const [
CenterMessage(),
Positioned(
top: 54,
left: 0,
right: 0,
child: TopNavigationPractice(),
),
Positioned(
right: 16,
bottom: 120,
child: ActionBarPractice(),
),
Positioned(
left: 16,
right: 90,
bottom: 36,
child: BottomInfoPractice(),
),
],
),
);
}
}
class CenterMessage extends StatelessWidget {
const CenterMessage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 220,
height: 320,
decoration: BoxDecoration(
color: Colors.white12,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white24,
),
),
child: const Center(
child: Text(
'ここに動画が入る',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}
class TopNavigationPractice extends StatelessWidget {
const TopNavigationPractice({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
'フォロー中',
style: TextStyle(
color: Colors.white54,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 20),
Text(
'おすすめ',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
class ActionBarPractice extends StatelessWidget {
const ActionBarPractice({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: const [
Icon(Icons.person_rounded, color: Colors.white, size: 34),
SizedBox(height: 24),
Icon(Icons.favorite_rounded, color: Colors.white, size: 36),
Text(
'1.3万',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Icon(Icons.mode_comment_rounded, color: Colors.white, size: 34),
Text(
'324',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Icon(Icons.bookmark_rounded, color: Colors.white, size: 34),
Text(
'1.2K',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
],
);
}
}
class BottomInfoPractice extends StatelessWidget {
const BottomInfoPractice({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'@pet_cafe_diary',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'小さな命の動きは、見ているだけで少しやさしい気持ちになる。',
style: TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.4,
),
),
SizedBox(height: 8),
Row(
children: [
Icon(Icons.music_note_rounded, color: Colors.white, size: 17),
SizedBox(width: 6),
Expanded(
child: Text(
'Healing Cafe Sound - Pet Cafe Diary',
style: TextStyle(color: Colors.white, fontSize: 13),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
);
}
}
実行すると何が分かるか
このコードでは、まだ動画は再生していません。
しかし、TikTok風アプリの画面構造をWidgetで組み立てています。
Stack
├─ CenterMessage
├─ TopNavigationPractice
├─ ActionBarPractice
└─ BottomInfoPractice
つまり、今回確認しているのは、動画機能ではなく「Widgetの構造」です。
実行すると、次のことが分かります。
- Stackで重ねるUIが作れる
- Positionedで上・右・下に部品を置ける
- Columnで右側ボタンを縦に並べられる
- Rowで音符アイコンと音源名を横に並べられる
- TextやIconでTikTok風の情報を置ける
コードの構造を分解する
今回のコードは、次のような構造です。
TikTokWidgetPracticeApp
└─ MaterialApp
└─ WidgetPracticePage
└─ Scaffold
└─ Stack
├─ CenterMessage
├─ TopNavigationPractice
├─ ActionBarPractice
└─ BottomInfoPractice
このように、Flutterでは画面を小さなWidgetに分けて作ります。
1つずつのWidgetを見ると、難しくありません。
CenterMessage
中央に「ここに動画が入る」と表示する部品
後の節で、この部分を本物の動画表示に置き換えます。
TopNavigationPractice
上部に「フォロー中」「おすすめ」を表示する部品
TikTok風UIの上部タブにあたる部分です。
ActionBarPractice
右側にプロフィール、いいね、コメント、保存を並べる部品
後の節では、タップできるボタンにしていきます。
BottomInfoPractice
下部にユーザー名、説明文、音源名を表示する部品
動画の投稿情報を表示する部分です。
ここで大切な考え方
この節で一番大切なのは、完成形をいきなり作ろうとしないことです。
まずは、動画を再生しなくてもよいです。
まずは、ボタンが動かなくてもよいです。
最初にやるべきことは、画面をWidgetとして配置することです。
まず配置
↓
次にデータ
↓
次に動き
↓
最後に仕上げ
これは、どんなアプリでも同じです。
商品一覧アプリでも、最初は商品カードの形だけを作ります。
予約アプリでも、最初は予約カードの形だけを作ります。
SNSアプリでも、最初は投稿カードの形だけを作ります。
動きはその後で足していきます。
よくあるつまずき1:Widgetが多すぎて混乱する
Flutterでは、Widgetがたくさん出てきます。
初心者は、名前が多くて混乱しやすいです。
しかし、最初から全部を覚える必要はありません。
まずは、次の5つを意識してください。
| Widget | 使い方 |
|---|---|
Text | 文字 |
Icon | アイコン |
Row | 横並び |
Column | 縦並び |
Stack | 重ねる |
TikTok風UIでは、この5つだけでもかなりの部分が理解できます。
よくあるつまずき2:childとchildrenを間違える
child と children はとてもよく間違えます。
Center(
child: Text('1つだけ'),
)
これは1つだけ入れるので child です。
Column(
children: [
Text('1つ目'),
Text('2つ目'),
],
)
これは複数入れるので children です。
1つなら child
複数なら children
よくあるつまずき3:Stackの重なり順が分からない
Stack は、後に書いたWidgetほど上に重なります。
Stack(
children: [
背景,
文字,
ボタン,
],
)
この場合、背景が一番下です。
文字とボタンは、その上に重なります。
TikTok風UIでは、背景動画を最初に置きます。
その後に、グラデーション、ボタン、文字を置きます。
背景動画
↓
グラデーション
↓
ボタン
↓
文字
よくあるつまずき4:StatelessWidgetとStatefulWidgetの違い
最初は、次の理解で大丈夫です。
| 種類 | 使う場面 |
|---|---|
StatelessWidget | 表示が変わらない部品 |
StatefulWidget | 表示が変わる部品 |
この節で作った練習コードは、まだボタンを押しても表示が変わりません。
そのため、ほとんど StatelessWidget で作っています。
後の節で、いいねや保存の状態を変えるときに StatefulWidget を使います。
手を動かす練習1:Textを変更する
次の部分を探してください。
'ここに動画が入る'
これを次の文字に変更してみてください。
動画背景エリア
実行すると、中央の文字が変わります。
これにより、Text が文字表示のWidgetであることを確認できます。
手を動かす練習2:Iconの色を変える
次のハートアイコンを探してください。
Icon(Icons.favorite_rounded, color: Colors.white, size: 36),
Colors.white を Colors.pink に変えてみてください。
Icon(Icons.favorite_rounded, color: Colors.pink, size: 36),
ハートの色がピンクになります。
後の節では、この色変更を「タップされたら変わる」ようにします。
手を動かす練習3:右側ボタンの間隔を変える
次のような SizedBox を探してください。
SizedBox(height: 20),
この数字を大きくすると、縦の余白が広がります。
SizedBox(height: 32),
これで、SizedBox が余白を作るWidgetだと分かります。
手を動かす練習4:下部テキストを増やす
BottomInfoPractice の Column の中に、次のTextを追加してみてください。
Text(
'#pet #cafe #flutter',
style: TextStyle(
color: Colors.white70,
fontSize: 13,
),
),
下部の情報が増えます。
このように、Column の children にWidgetを追加すると、縦に部品を増やせます。
この節の確認問題
確認問題1
Flutterで画面を作る部品を何と呼びますか。
答え
Widget と呼びます。
確認問題2
Text は何をするWidgetですか。
答え
文字を表示するWidgetです。
確認問題3
Row と Column の違いは何ですか。
答え
Row は横に並べるWidgetです。
Column は縦に並べるWidgetです。
確認問題4
TikTok風UIで Stack が重要な理由は何ですか。
答え
動画の上に、文字やボタンを重ねて表示するためです。
確認問題5
child と children の違いは何ですか。
答え
child は1つのWidgetを入れるときに使います。
children は複数のWidgetを入れるときに使います。
確認問題6
StatelessWidget はどのようなWidgetですか。
答え
自分の状態を持たず、表示が変化しないWidgetです。
確認問題7
StatefulWidget はどのようなWidgetですか。
答え
ユーザー操作などによって、表示が変化するWidgetです。
この節のまとめ
この節では、Flutterの画面が Widget でできていることを学びました。
TikTok風アプリも、特別な魔法で作るわけではありません。
動画背景、上部ナビ、右側ボタン、下部情報、コメント欄などを、Widgetとして分けて作っていきます。
TikTok風アプリ
├─ 動画背景のWidget
├─ 上部ナビのWidget
├─ 右側ボタンのWidget
├─ 下部情報のWidget
└─ コメント欄のWidget
この節で特に大切なのは、次の考え方です。
Flutterでは、画面をWidgetという部品に分け、入れ子にして組み立てる。
そして、TikTok風UIのように重なりのある画面では、Stack が重要になります。
背景動画
↓
その上に文字
↓
その上にボタン
この流れを理解できると、複雑そうな画面も怖くなくなります。
次の節では、MaterialApp と Scaffold を使って、Flutterアプリの土台をさらに丁寧に作っていきます。