
PageView.builderで縦スワイプUIを作る
この節で学ぶこと
前回の 4-5 では、TikTok風アプリで使う動画データを ShortVideo classとして設計しました。
動画1本分の情報を、次のようにまとめました。
class ShortVideo {
const ShortVideo({
required this.videoUrl,
required this.userName,
required this.caption,
required this.musicTitle,
required this.likes,
required this.comments,
required this.saves,
required this.shares,
required this.avatarColor,
required this.categoryLabel,
});
final String videoUrl;
final String userName;
final String caption;
final String musicTitle;
final int likes;
final int comments;
final int saves;
final int shares;
final Color avatarColor;
final String categoryLabel;
}
今回の 4-6 では、この動画データを使って、TikTokのように上下スワイプで画面を切り替える仕組みを作ります。
TikTok風アプリの中心になる動きは、次のようなものです。
1本目の動画を見る
↓
上にスワイプする
↓
2本目の動画に切り替わる
↓
さらに上にスワイプする
↓
3本目の動画に切り替わる
Flutterでは、このようなページ単位のスワイプUIを PageView.builder で作れます。
この節で大切なのは、次の一文です。
PageView.builderを使うと、Listのデータを1件ずつページとして表示できる。
まずPageViewとは何か
PageView は、ページを1枚ずつ切り替えるためのWidgetです。
初心者向けには、次のように理解すると分かりやすいです。
PageView = 画面をページ単位でスワイプして切り替えるWidget
たとえば、横にスワイプするチュートリアル画面や、画像スライダーなどにも使えます。
今回作るTikTok風アプリでは、縦方向にスワイプします。
PageView
├─ 1ページ目:動画1
├─ 2ページ目:動画2
└─ 3ページ目:動画3
TikTok風アプリでは、1ページが1本の動画画面になります。
PageView.builderとは何か
PageView.builder は、ページを必要な分だけ作るためのWidgetです。
PageView.builder(
itemCount: videos.length,
itemBuilder: (context, index) {
return Text('ページ $index');
},
)
ここで大切なのは、itemBuilder です。
itemBuilder は、ページを1枚ずつ作る場所です。
itemBuilder
↓
indexを受け取る
↓
index番目のページを作る
たとえば、index が0なら1ページ目、index が1なら2ページ目を作ります。
index = 0 → 1ページ目
index = 1 → 2ページ目
index = 2 → 3ページ目
新しい言葉:builderとは何か
builder は、「必要になったときに作る仕組み」です。
普通の PageView では、あらかじめすべてのページを並べることもできます。
PageView(
children: [
Text('1ページ目'),
Text('2ページ目'),
Text('3ページ目'),
],
)
これでも動きます。
しかし、動画がたくさんある場合、最初から全部作るのは効率がよくありません。
そこで PageView.builder を使います。
PageView.builder(
itemBuilder: (context, index) {
return Text('$indexページ目');
},
)
初心者向けには、次のように覚えてください。
builder = 必要になったページを、その場で作る仕組み
TikTok風アプリでは、動画ページを1枚ずつ作るために PageView.builder を使います。
新しい言葉:itemCountとは何か
itemCount は、全部で何ページ作るかを表します。
itemCount: videos.length,
これは、次の意味です。
videosの数だけページを作る
たとえば、videos に3本の動画が入っている場合、
videos.length
は 3 です。
そのため、3ページ作られます。
videos.length = 3
↓
PageViewは3ページ作る
新しい言葉:itemBuilderとは何か
itemBuilder は、1ページ分のWidgetを作る場所です。
itemBuilder: (context, index) {
return Text('ページ $index');
},
index は、今作っているページ番号です。
| index | 意味 |
|---|---|
0 | 1ページ目 |
1 | 2ページ目 |
2 | 3ページ目 |
FlutterのListやPageViewでは、番号は0から始まります。
初心者がよく間違えるところなので注意してください。
1ページ目は index 0
2ページ目は index 1
3ページ目は index 2
TikTok風アプリではindexで動画を取り出す
前回、動画データを List<ShortVideo> として持つ考え方を学びました。
const videos = [
ShortVideo(...),
ShortVideo(...),
ShortVideo(...),
];
このListから動画を1本取り出すには、次のように書きます。
videos[index]
index が0なら、1本目の動画です。
videos[0]
index が1なら、2本目の動画です。
videos[1]
つまり、PageView.builder と List を組み合わせると、次のような流れになります。
PageView.builderがindexを渡す
↓
videos[index]で動画データを取り出す
↓
その動画データを画面に表示する
これが、TikTok風アプリの縦スワイプの基本です。
まずは動画なしで縦スワイプを作る
この節では、まだ本物の動画再生はしません。

まずは、PageView.builder の仕組みを理解するために、動画カードのような画面を上下スワイプで切り替えます。
DartPadに次のコードを貼り付けてください。
import 'package:flutter/material.dart';
void main() {
runApp(const TikTokPageViewPracticeApp());
}
class TikTokPageViewPracticeApp extends StatelessWidget {
const TikTokPageViewPracticeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: ShortVideoHomePage(),
);
}
}
class ShortVideo {
const ShortVideo({
required this.userName,
required this.caption,
required this.musicTitle,
required this.likes,
required this.avatarColor,
required this.categoryLabel,
});
final String userName;
final String caption;
final String musicTitle;
final int likes;
final Color avatarColor;
final String categoryLabel;
}
const videos = [
ShortVideo(
userName: 'pet_cafe_diary',
caption: '小さな命の動きは、見ているだけで少しやさしい気持ちになる。',
musicTitle: 'Healing Cafe Sound - Pet Cafe Diary',
likes: 12800,
avatarColor: Color(0xFFE91E63),
categoryLabel: 'PET',
),
ShortVideo(
userName: 'food_and_nature',
caption: 'おいしいものを探す旅の途中で出会った、自然の小さなリズム。',
musicTitle: 'Kitchen Walk - Food & Nature',
likes: 8732,
avatarColor: Color(0xFF2196F3),
categoryLabel: 'FOOD',
),
ShortVideo(
userName: 'daily_pet_room',
caption: 'ペットと過ごす午後。何気ない一瞬が、あとから思い出になる。',
musicTitle: 'Room Light - Daily Pet Room',
likes: 24100,
avatarColor: Color(0xFFFF9800),
categoryLabel: 'ROOM',
),
];
class ShortVideoHomePage extends StatelessWidget {
const ShortVideoHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: PageView.builder(
scrollDirection: Axis.vertical,
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return ShortVideoPage(
video: video,
index: index,
);
},
),
);
}
}
class ShortVideoPage extends StatelessWidget {
const ShortVideoPage({
super.key,
required this.video,
required this.index,
});
final ShortVideo video;
final int index;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: VideoPlaceholder(
video: video,
index: index,
),
),
const Positioned(
top: 52,
left: 0,
right: 0,
child: TopNavigationPractice(),
),
Positioned(
right: 16,
bottom: 112,
child: RightActionBarPractice(video: video),
),
Positioned(
left: 16,
right: 92,
bottom: 36,
child: BottomVideoInfoPractice(video: video),
),
],
);
}
}
class VideoPlaceholder extends StatelessWidget {
const VideoPlaceholder({
super.key,
required this.video,
required this.index,
});
final ShortVideo video;
final int index;
@override
Widget build(BuildContext context) {
final colors = [
const Color(0xFF2A2A2A),
const Color(0xFF0D1B2A),
const Color(0xFF2A160B),
];
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
colors[index],
Colors.black,
],
),
),
child: Center(
child: Container(
width: 230,
height: 360,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.12),
borderRadius: BorderRadius.circular(28),
border: Border.all(
color: Colors.white.withOpacity(0.24),
),
),
child: Center(
child: Text(
video.categoryLabel,
style: const TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
),
),
),
);
}
}
class TopNavigationPractice extends StatelessWidget {
const TopNavigationPractice({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Row(
children: [
const Icon(
Icons.live_tv_rounded,
color: Colors.white,
size: 24,
),
const Spacer(),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'フォロー中',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 18),
const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'おすすめ',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
SizedBox(
width: 28,
height: 3,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(999),
),
),
),
),
],
),
],
),
const Spacer(),
const Icon(
Icons.search_rounded,
color: Colors.white,
size: 28,
),
],
),
),
);
}
}
class RightActionBarPractice extends StatelessWidget {
const RightActionBarPractice({
super.key,
required this.video,
});
final ShortVideo video;
@override
Widget build(BuildContext context) {
return Column(
children: [
ProfileButtonPractice(color: video.avatarColor),
const SizedBox(height: 22),
ActionButtonPractice(
icon: Icons.favorite_rounded,
label: formatCount(video.likes),
),
const SizedBox(height: 20),
const ActionButtonPractice(
icon: Icons.mode_comment_rounded,
label: 'コメント',
),
const SizedBox(height: 20),
const ActionButtonPractice(
icon: Icons.bookmark_rounded,
label: '保存',
),
const SizedBox(height: 20),
const ActionButtonPractice(
icon: Icons.reply_rounded,
label: '共有',
),
],
);
}
}
class ProfileButtonPractice extends StatelessWidget {
const ProfileButtonPractice({
super.key,
required this.color,
});
final Color color;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 54,
height: 64,
child: Stack(
alignment: Alignment.topCenter,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
),
child: const Icon(
Icons.person_rounded,
color: Colors.white,
size: 28,
),
),
Positioned(
bottom: 6,
child: Container(
width: 22,
height: 22,
decoration: const BoxDecoration(
color: Color(0xFFFF2D55),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_rounded,
color: Colors.white,
size: 18,
),
),
),
],
),
);
}
}
class ActionButtonPractice extends StatelessWidget {
const ActionButtonPractice({
super.key,
required this.icon,
required this.label,
});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(
icon,
color: Colors.white,
size: 34,
shadows: const [
Shadow(
color: Colors.black54,
blurRadius: 8,
),
],
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
color: Colors.black87,
blurRadius: 8,
),
],
),
),
],
);
}
}
class BottomVideoInfoPractice extends StatelessWidget {
const BottomVideoInfoPractice({
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: [
const Icon(
Icons.music_note_rounded,
color: Colors.white,
size: 17,
),
const SizedBox(width: 6),
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,
),
],
),
),
),
],
),
],
),
);
}
}
String formatCount(int value) {
if (value >= 10000) {
final result = value / 10000;
return '${result.toStringAsFixed(1)}万';
}
if (value >= 1000) {
final result = value / 1000;
return '${result.toStringAsFixed(1)}K';
}
return value.toString();
}
実行して確認すること
実行したら、画面を上下にスワイプしてください。
次のように画面が切り替わります。
PET
↓
FOOD
↓
ROOM
それぞれの画面で、ユーザー名、キャプション、いいね数、プロフィール色が変わります。
これは、PageView.builder が videos[index] を使って、動画データを1件ずつ表示しているからです。
final video = videos[index];
そして、その video を ShortVideoPage に渡しています。
return ShortVideoPage(
video: video,
index: index,
);
今回のコードの全体構造
今回のコードは、次のような構造になっています。
TikTokPageViewPracticeApp
└─ MaterialApp
└─ ShortVideoHomePage
└─ Scaffold
└─ PageView.builder
└─ ShortVideoPage
├─ VideoPlaceholder
├─ TopNavigationPractice
├─ RightActionBarPractice
└─ BottomVideoInfoPractice
前回までは、1画面だけを Stack で作っていました。
今回は、その Stack で作った1画面を、PageView.builder の中で複数表示しています。
1画面分のUI
↓
PageView.builderで複数ページにする
↓
縦スワイプできるようになる
ShortVideoHomePageの解説
中心になるのは、ShortVideoHomePage です。
class ShortVideoHomePage extends StatelessWidget {
const ShortVideoHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: PageView.builder(
scrollDirection: Axis.vertical,
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return ShortVideoPage(
video: video,
index: index,
);
},
),
);
}
}
ここでは、Scaffold の body に PageView.builder を置いています。
body: PageView.builder(...)
これにより、画面全体がスワイプ可能なページになります。
scrollDirectionとは何か
scrollDirection は、スクロールする方向を指定する設定です。
scrollDirection: Axis.vertical,
Axis.vertical は、縦方向という意味です。
| 書き方 | 意味 |
|---|---|
Axis.vertical | 縦方向 |
Axis.horizontal | 横方向 |
TikTok風アプリでは、上下にスワイプしたいので、Axis.vertical を使います。
もし横スワイプにしたい場合は、次のように書きます。
scrollDirection: Axis.horizontal,
ただし、今回はTikTok風なので縦方向です。
itemCount: videos.length の意味
次の部分を見てください。
itemCount: videos.length,
これは、ページ数を動画の数に合わせるという意味です。
今回の videos には3本の動画があります。
const videos = [
ShortVideo(...),
ShortVideo(...),
ShortVideo(...),
];
そのため、videos.length は 3 です。
つまり、3ページ作ります。
videos.length = 3
↓
PageView.builderは3ページ作る
itemBuilderの中を見る
次の部分を見てください。
itemBuilder: (context, index) {
final video = videos[index];
return ShortVideoPage(
video: video,
index: index,
);
},
ここが、ページを作る場所です。
流れは次の通りです。
indexを受け取る
↓
videos[index]で動画データを取り出す
↓
ShortVideoPageに渡す
↓
1ページ分の画面を作る
たとえば、index が0なら、
final video = videos[0];
です。
1本目の動画データを取り出します。
index が1なら、
final video = videos[1];
です。
2本目の動画データを取り出します。
ShortVideoPageにデータを渡す
ShortVideoPage は、動画1本分の画面です。
class ShortVideoPage extends StatelessWidget {
const ShortVideoPage({
super.key,
required this.video,
required this.index,
});
final ShortVideo video;
final int index;
ここでは、外から video と index を受け取っています。
ShortVideoPage(
video: video,
index: index,
)
この仕組みによって、同じ ShortVideoPage でも、渡す動画データによって表示が変わります。
ShortVideoPage(video: videos[0])
↓
PETの画面
ShortVideoPage(video: videos[1])
↓
FOODの画面
ShortVideoPage(video: videos[2])
↓
ROOMの画面
同じWidgetでもデータが違えば表示が変わる
ここはとても大切です。
Flutterでは、同じWidgetを使っていても、渡すデータが変われば表示が変わります。
たとえば、BottomVideoInfoPractice を見てみます。
BottomVideoInfoPractice(video: video)
このWidgetの中では、次のように表示しています。
Text('@${video.userName}')
Text(video.caption)
Text(video.musicTitle)
そのため、渡された video が変われば、表示される文字も変わります。
video.userName が pet_cafe_diary
↓
@pet_cafe_diary
video.userName が food_and_nature
↓
@food_and_nature
この考え方は、どんなアプリでも使います。
同じカード部品
↓
違うデータを渡す
↓
違う表示になる
PageView.builderとListの関係
ここで、PageView.builder と List の関係を整理します。
| 役割 | コード | 意味 |
|---|---|---|
| 動画一覧 | videos | 複数の動画データ |
| ページ数 | videos.length | 動画の数だけページを作る |
| ページ番号 | index | 今作っているページ番号 |
| 動画を取り出す | videos[index] | index番目の動画 |
| 画面を作る | ShortVideoPage(video: video) | 動画データを画面に渡す |
この表を理解できると、TikTok風アプリの縦スワイプの仕組みが分かります。
indexは0から始まる
index は0から始まります。
今回の動画は3本です。
videos[0] → PET
videos[1] → FOOD
videos[2] → ROOM
videos[3] は存在しません。
もし itemCount: videos.length がある場合、Flutterは 0 から videos.length - 1 までのindexを使います。
今回なら、次の3つです。
0
1
2
初心者がよく間違えるポイントです。
3本あるからindexは0, 1, 2
動画データを増やすとページも増える
PageView.builder の便利なところは、videos にデータを追加すると、ページも増えることです。
たとえば、videos に4本目を追加します。
ShortVideo(
userName: 'sweet_table',
caption: '甘いものを囲む時間は、少しだけ日常をやわらかくしてくれる。',
musicTitle: 'Sweet Table Sound',
likes: 5400,
avatarColor: Color(0xFF9C27B0),
categoryLabel: 'SWEETS',
),
すると、videos.length が4になります。
videos.length = 4
↓
PageView.builderは4ページ作る
UI側のコードを増やす必要はありません。
データを増やすだけで、表示ページも増えます。
これが、データとUIを分ける大きなメリットです。
なぜPageView.builderを使うのか
もし、PageView.builder を使わずに書くと、次のようになります。
PageView(
scrollDirection: Axis.vertical,
children: [
ShortVideoPage(video: videos[0], index: 0),
ShortVideoPage(video: videos[1], index: 1),
ShortVideoPage(video: videos[2], index: 2),
],
)
3本ならまだよいです。
しかし、動画が10本、100本になったら大変です。
PageView.builder なら、次のように書けます。
PageView.builder(
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return ShortVideoPage(
video: video,
index: index,
);
},
)
動画が何本になっても、同じ形で表示できます。
動画が増えても、UIコードの形は変わらない。
手を動かす練習1:横スワイプに変えてみる
次の部分を探してください。
scrollDirection: Axis.vertical,
これを次のように変えてみます。
scrollDirection: Axis.horizontal,
すると、上下ではなく左右にスワイプする画面になります。
確認したら、TikTok風に戻すために Axis.vertical に戻してください。
手を動かす練習2:4本目の動画を追加する
videos の最後に、次のデータを追加してみてください。
ShortVideo(
userName: 'sweet_table',
caption: '甘いものを囲む時間は、少しだけ日常をやわらかくしてくれる。',
musicTitle: 'Sweet Table Sound',
likes: 5400,
avatarColor: Color(0xFF9C27B0),
categoryLabel: 'SWEETS',
),
追加すると、4ページ目が表示されるようになります。
データを追加するだけでページが増えることを確認してください。
手を動かす練習3:表示する文字を変更する
1本目のデータを探してください。
userName: 'pet_cafe_diary',
これを次のように変えてみます。
userName: 'cat_room_japan',
1ページ目のユーザー名が変わります。
これは、画面がデータから作られていることを確認する練習です。
手を動かす練習4:いいね数を変える
1本目の likes を変えてみます。
likes: 12800,
これを次のように変更します。
likes: 35000,
表示が 3.5万 に変わります。
これは、formatCount 関数が数字を表示用に変換しているためです。
よくあるつまずき1:itemBuilderのindexが分からない
index は、今作っているページの番号です。
itemBuilder: (context, index) {
...
}
index はFlutterが自動で渡してくれます。
自分で数字を入れる必要はありません。
Flutterがindexを渡す
↓
そのindexを使ってvideos[index]を取り出す
よくあるつまずき2:videos[index]でエラーになる
videos[index] でエラーになる場合、存在しない番号を取り出している可能性があります。
たとえば、動画が3本しかないのに、
videos[3]
とするとエラーです。
3本の場合、使えるindexは次の3つです。
0
1
2
itemCount: videos.length を指定しておけば、Flutterは範囲内のindexだけを使ってくれます。
よくあるつまずき3:itemCountを書き忘れる
itemCount を書かないと、PageView.builder は必要に応じてページを作り続けます。
これは後の「無限スクロール風」の節で使います。
しかし、最初に学ぶ段階では、まず itemCount を書いたほうが分かりやすいです。
itemCount: videos.length,
初心者向けには、まず次のように覚えてください。
最初は itemCount: videos.length を書く
よくあるつまずき4:同じ画面しか表示されない
ページを切り替えても同じ画面しか表示されない場合、videos[index] を使っていない可能性があります。
たとえば、常に videos[0] を渡していると、どのページでも1本目が表示されます。
間違い例です。
final video = videos[0];
正しい例です。
final video = videos[index];
index を使うことで、ページごとに違う動画データを表示できます。
よくあるつまずき5:Widgetを増やしすぎて分からなくなる
今回のコードには、たくさんのWidgetが出てきます。
しかし、中心だけを見るとシンプルです。
ShortVideoHomePage
└─ PageView.builder
└─ ShortVideoPage
└─ Stack
まずはこの流れを理解してください。
細かいボタンやテキストは、後から少しずつ見れば大丈夫です。
この節の確認問題
確認問題1
PageView は何をするWidgetですか。
答え
ページを1枚ずつスワイプで切り替えるWidgetです。
TikTok風アプリでは、動画ページを上下に切り替えるために使います。
確認問題2
PageView.builder の builder は何をする仕組みですか。
答え
必要になったページを、その場で作る仕組みです。
動画データが増えても、同じ形でページを作れます。
確認問題3
scrollDirection: Axis.vertical は何を意味しますか。
答え
縦方向にスクロールするという意味です。
TikTok風アプリでは、上下スワイプで動画を切り替えるために使います。
確認問題4
itemCount: videos.length は何をしていますか。
答え
videos に入っている動画の数だけページを作る、という意味です。
確認問題5
videos[index] は何を表しますか。
答え
videos の中から、index 番目の動画データを取り出す書き方です。
index が0なら1本目、1なら2本目の動画です。
確認問題6
同じ ShortVideoPage なのに、ページごとに表示が変わるのはなぜですか。
答え
渡している ShortVideo データがページごとに違うからです。
同じWidgetでも、渡すデータが変われば表示も変わります。
この節のまとめ
この節では、PageView.builder を使って、TikTokのような縦スワイプUIを作りました。
PageView.builder は、Listのデータを1件ずつ取り出し、ページとして表示できます。
PageView.builder(
scrollDirection: Axis.vertical,
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return ShortVideoPage(
video: video,
index: index,
);
},
)
このコードの流れは、次の通りです。
videosに複数の動画データを用意する
↓
PageView.builderがindexを作る
↓
videos[index]で動画を1本取り出す
↓
ShortVideoPageに渡す
↓
1ページ分の画面として表示する
この節で一番大切なのは、次の一文です。
PageView.builderは、Listのデータを1件ずつページに変えるWidgetである。
次の節では、3本の動画でもずっと続いているように見せる「無限スクロール風」の仕組みを作ります。