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

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'),
)

この場合、CenterchildText です。

コード意味
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の使い方
右側ボタンrightbottom
下部投稿情報leftrightbottom
上部ナビtopleftright
ページ表示lefttop

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を間違える

childchildren はとてもよく間違えます。

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.whiteColors.pink に変えてみてください。

Icon(Icons.favorite_rounded, color: Colors.pink, size: 36),

ハートの色がピンクになります。

後の節では、この色変更を「タップされたら変わる」ようにします。

手を動かす練習3:右側ボタンの間隔を変える

次のような SizedBox を探してください。

SizedBox(height: 20),

この数字を大きくすると、縦の余白が広がります。

SizedBox(height: 32),

これで、SizedBox が余白を作るWidgetだと分かります。

手を動かす練習4:下部テキストを増やす

BottomInfoPracticeColumn の中に、次のTextを追加してみてください。

Text(
  '#pet #cafe #flutter',
  style: TextStyle(
    color: Colors.white70,
    fontSize: 13,
  ),
),

下部の情報が増えます。

このように、Columnchildren にWidgetを追加すると、縦に部品を増やせます。

この節の確認問題

確認問題1

Flutterで画面を作る部品を何と呼びますか。

答え

Widget と呼びます。

確認問題2

Text は何をするWidgetですか。

答え

文字を表示するWidgetです。

確認問題3

RowColumn の違いは何ですか。

答え

Row は横に並べるWidgetです。

Column は縦に並べるWidgetです。

確認問題4

TikTok風UIで Stack が重要な理由は何ですか。

答え

動画の上に、文字やボタンを重ねて表示するためです。

確認問題5

childchildren の違いは何ですか。

答え

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 が重要になります。

背景動画
↓
その上に文字
↓
その上にボタン

この流れを理解できると、複雑そうな画面も怖くなくなります。

次の節では、MaterialAppScaffold を使って、Flutterアプリの土台をさらに丁寧に作っていきます。

教材トップへ戻る