
video_playerで動画を再生する
この節で学ぶこと
前回の 4-6 では、PageView.builder を使って、TikTokのように上下スワイプで画面を切り替える仕組みを作りました。
前回の段階では、まだ本物の動画は再生していません。
代わりに、次のような仮の動画エリアを表示していました。
PET
FOOD
ROOM
今回の 4-7 では、いよいよ本物の動画を再生します。
DartPadでも動かしやすいように、Flutter公式が公開している .mp4 動画URLを使います。
この節で大切なのは、次の一文です。
動画を再生するには、video_playerパッケージとVideoPlayerControllerを使う。
まず今回作るもの
この節では、次のような画面を作ります。

前回までの VideoPlaceholder を、本物の動画表示に置き換えます。
変更前
VideoPlaceholder
変更後
VideoBackground
最終的には、TikTok風アプリの背景として動画を全画面表示します。
video_playerとは何か
video_player は、Flutterで動画を再生するためのパッケージです。
パッケージとは、Flutterに機能を追加するための部品です。
Flutter本体だけでも多くのUIは作れますが、動画再生のような機能は、専用のパッケージを使うと便利です。
package = Flutterに機能を追加する道具
video_player = 動画再生機能を追加するパッケージ
DartPadで使う場合は、Packagesに次を追加します。
video_player: ^2.9.2
新しい言葉:VideoPlayerControllerとは何か
動画を再生するには、VideoPlayerController を使います。
Controller とは、何かを操作・管理するためのものです。
今回の場合は、動画を操作します。
VideoPlayerController
├─ 動画を読み込む
├─ 動画を再生する
├─ 動画を停止する
├─ 音量を設定する
└─ ループ再生を設定する
コードでは、次のように作ります。
final controller = VideoPlayerController.networkUrl(
Uri.parse(video.videoUrl),
);
これは、次の意味です。
video.videoUrl の動画を再生するためのControllerを作る。
新しい言葉:initializeとは何か
動画は、URLを指定しただけではすぐに使えません。
まず、動画を読み込んで、再生できる準備をする必要があります。
その準備をするのが initialize です。
await controller.initialize();
初心者向けには、次のように理解してください。
initialize = 動画を使う準備をする処理
動画のサイズや長さなどを取得するためにも、initialize が必要です。
新しい言葉:Futureとasync / await
動画の読み込みには時間がかかります。
そのため、Dartでは Future、async、await という書き方を使います。
Future<void> initializeVideo() async {
await controller.initialize();
}
ここでは、深く考えすぎなくて大丈夫です。
まずは次の理解で進めます。
| 用語 | 初心者向けの意味 |
|---|---|
Future | すぐには終わらない処理 |
async | 時間がかかる処理を書く印 |
await | 処理が終わるまで待つ |
動画読み込みは時間がかかるので、await controller.initialize() と書きます。
新しい言葉:StatefulWidgetが必要になる理由
前回までの画面は、表示するだけでした。
しかし、動画再生では次のような「変化」があります。
動画を読み込み中
↓
読み込み完了
↓
再生する
↓
一時停止する
このように、画面の状態が変わるため、StatefulWidget を使います。
StatelessWidget
表示が変わらない部品
StatefulWidget
状態が変わる部品
動画再生では、VideoPlayerController を作り、読み込み完了後に画面を更新する必要があります。
そのため、今回のメイン画面は StatefulWidget にします。
まずは1本の動画を再生する
最初から縦スワイプに戻すと難しいので、まずは1本の動画だけを再生してみます。

DartPadのPackagesに video_player: ^2.9.2 を追加したうえで、次のコードを貼り付けてください。(最初から入っている場合あり)
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(const VideoPlayerPracticeApp());
}
class VideoPlayerPracticeApp extends StatelessWidget {
const VideoPlayerPracticeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: VideoPlayerPracticePage(),
);
}
}
class VideoPlayerPracticePage extends StatefulWidget {
const VideoPlayerPracticePage({super.key});
@override
State<VideoPlayerPracticePage> createState() =>
_VideoPlayerPracticePageState();
}
class _VideoPlayerPracticePageState extends State<VideoPlayerPracticePage> {
late final VideoPlayerController controller;
bool isReady = false;
@override
void initState() {
super.initState();
initializeVideo();
}
Future<void> initializeVideo() async {
controller = VideoPlayerController.networkUrl(
Uri.parse(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
),
);
await controller.initialize();
controller
..setLooping(true)
..setVolume(0)
..play();
setState(() {
isReady = true;
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void togglePlay() {
setState(() {
if (controller.value.isPlaying) {
controller.pause();
} else {
controller.play();
}
});
}
@override
Widget build(BuildContext context) {
if (!isReady) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: togglePlay,
child: Center(
child: AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: VideoPlayer(controller),
),
),
),
);
}
}
実行して確認すること
実行すると、黒背景の中央に動画が表示されます。
動画部分をタップすると、一時停止と再生を切り替えられます。
タップ
↓
再生中なら停止
もう一度タップ
↓
停止中なら再生
ここでは、次のことを確認してください。
- video_playerで動画が表示できる
- VideoPlayerControllerで動画を管理している
- initializeが終わるまで読み込み表示を出している
- GestureDetectorでタップ操作を受け取っている
コードを順番に理解する
import
import 'package:video_player/video_player.dart';
これは、video_player パッケージを使うための読み込みです。
このimportがないと、VideoPlayerController や VideoPlayer が使えません。
controllerを用意する
late final VideoPlayerController controller;
ここでは、動画を管理するcontrollerを用意しています。
late は、「あとで必ず値を入れる」という意味です。
このコードでは、initState の中で initializeVideo() を呼び、その中でcontrollerを作ります。
initStateとは何か
@override
void initState() {
super.initState();
initializeVideo();
}
initState は、StatefulWidgetが最初に作られたときに一度だけ呼ばれる場所です。
初心者向けには、次のように覚えてください。
initState = 最初に一度だけ準備をする場所
動画再生では、最初にcontrollerを作り、動画を読み込む必要があります。
そのため、initState で initializeVideo() を呼びます。
動画を読み込む
controller = VideoPlayerController.networkUrl(
Uri.parse(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
),
);
これは、ネット上の動画URLを指定してcontrollerを作っています。
networkUrl は、インターネット上の動画を再生するときに使います。
initializeする
await controller.initialize();
これは、動画を使える状態にする処理です。
動画の読み込みが終わるまで待ちます。
ループ、音量、再生
controller
..setLooping(true)
..setVolume(0)
..play();
この書き方は少し見慣れないかもしれません。
.. は、同じcontrollerに対して続けて処理を書くための記法です。
普通に書くと、次と同じです。
controller.setLooping(true);
controller.setVolume(0);
controller.play();
それぞれの意味は次の通りです。
| コード | 意味 |
|---|---|
setLooping(true) | 繰り返し再生する |
setVolume(0) | 音量を0にする |
play() | 再生する |
DartPadやブラウザでは、自動再生の制限があるため、音量を0にしておくと再生されやすくなります。
setStateで画面を更新する
setState(() {
isReady = true;
});
setState は、状態が変わったことをFlutterに知らせるための関数です。
ここでは、動画の準備が終わったので、isReady を true にしています。
isReady = false
↓
読み込み中画面を表示
isReady = true
↓
動画画面を表示
setState を呼ぶことで、Flutterが画面を再描画します。
disposeでcontrollerを片付ける
@override
void dispose() {
controller.dispose();
super.dispose();
}
dispose は、Widgetが不要になったときに呼ばれる場所です。
動画controllerは、使い終わったら片付ける必要があります。
初心者向けには、次のように覚えてください。
dispose = 使い終わったものを片付ける場所
動画controllerを片付けないと、メモリを無駄に使ってしまうことがあります。
VideoPlayerで動画を表示する
VideoPlayer(controller)
VideoPlayer は、実際に動画を画面に表示するWidgetです。
ただし、VideoPlayer はcontrollerが必要です。
VideoPlayerController
↓
動画を管理する
VideoPlayer
↓
動画を画面に表示する
この2つはセットで考えると分かりやすいです。
AspectRatioとは何か
AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: VideoPlayer(controller),
)
AspectRatio は、縦横比を保つためのWidgetです。
動画には、それぞれ縦横比があります。
横長の動画を無理に正方形にすると、映像がつぶれて見えます。
AspectRatio を使うと、動画本来の比率で表示できます。
初心者向けには、次のように理解してください。
AspectRatio = 縦横比を守って表示するWidget
GestureDetectorでタップを受け取る
GestureDetector(
onTap: togglePlay,
child: Center(
child: AspectRatio(...),
),
)
GestureDetector は、タップなどの操作を検知するWidgetです。
ここでは、動画をタップすると togglePlay が呼ばれます。
void togglePlay() {
setState(() {
if (controller.value.isPlaying) {
controller.pause();
} else {
controller.play();
}
});
}
この処理は、次の意味です。
もし再生中なら停止する
そうでなければ再生する
次に全画面背景として動画を表示する
TikTok風アプリでは、動画は中央に小さく表示するのではなく、画面全体に広げます。
そのため、次は FittedBox を使います。
新しい言葉:FittedBoxとは何か
FittedBox は、子Widgetを指定した範囲に合わせて拡大・縮小するWidgetです。
FittedBox(
fit: BoxFit.cover,
child: ...
)
BoxFit.cover は、画面全体を埋めるように表示する指定です。
動画の一部が切れることはありますが、TikTok風の全画面背景に近い見た目になります。
BoxFit.cover
↓
画面全体を埋める
↓
必要なら一部を切り取る
全画面背景として動画を表示するコード
次は、先ほどのコードをTikTok風の全画面背景に近づけます。
DartPadに次のコードを貼り付けてください。

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(const FullScreenVideoPracticeApp());
}
class FullScreenVideoPracticeApp extends StatelessWidget {
const FullScreenVideoPracticeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FullScreenVideoPracticePage(),
);
}
}
class FullScreenVideoPracticePage extends StatefulWidget {
const FullScreenVideoPracticePage({super.key});
@override
State<FullScreenVideoPracticePage> createState() =>
_FullScreenVideoPracticePageState();
}
class _FullScreenVideoPracticePageState
extends State<FullScreenVideoPracticePage> {
late final VideoPlayerController controller;
bool isReady = false;
@override
void initState() {
super.initState();
initializeVideo();
}
Future<void> initializeVideo() async {
controller = VideoPlayerController.networkUrl(
Uri.parse(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
),
);
await controller.initialize();
controller
..setLooping(true)
..setVolume(0)
..play();
setState(() {
isReady = true;
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void togglePlay() {
setState(() {
if (controller.value.isPlaying) {
controller.pause();
} else {
controller.play();
}
});
}
@override
Widget build(BuildContext context) {
if (!isReady) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: togglePlay,
child: VideoBackground(controller: controller),
),
),
const Positioned.fill(
child: VideoGradientOverlay(),
),
const Positioned(
top: 52,
left: 0,
right: 0,
child: TopNavigationPractice(),
),
const Positioned(
left: 16,
right: 92,
bottom: 36,
child: BottomVideoInfoPractice(),
),
],
),
);
}
}
class VideoBackground extends StatelessWidget {
const VideoBackground({
super.key,
required this.controller,
});
final VideoPlayerController controller;
@override
Widget build(BuildContext context) {
final size = controller.value.size;
return Container(
color: Colors.black,
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: size.width,
height: size.height,
child: VideoPlayer(controller),
),
),
);
}
}
class VideoGradientOverlay extends StatelessWidget {
const VideoGradientOverlay({super.key});
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.45),
Colors.transparent,
Colors.transparent,
Colors.black.withOpacity(0.86),
],
stops: const [0.0, 0.24, 0.58, 1.0],
),
),
),
);
}
}
class TopNavigationPractice extends StatelessWidget {
const TopNavigationPractice({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
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),
),
),
),
),
],
),
],
),
);
}
}
class BottomVideoInfoPractice extends StatelessWidget {
const BottomVideoInfoPractice({super.key});
@override
Widget build(BuildContext context) {
return const SafeArea(
top: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'@pet_cafe_diary',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
color: Colors.black87,
blurRadius: 8,
),
],
),
),
SizedBox(height: 8),
Text(
'小さな命の動きは、見ているだけで少しやさしい気持ちになる。今日はペットカフェ風の癒し動画。',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.35,
shadows: [
Shadow(
color: Colors.black87,
blurRadius: 8,
),
],
),
),
SizedBox(height: 10),
Row(
children: [
Icon(
Icons.music_note_rounded,
color: Colors.white,
size: 17,
),
SizedBox(width: 6),
Expanded(
child: Text(
'Healing Cafe Sound - Pet Cafe Diary',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
shadows: [
Shadow(
color: Colors.black87,
blurRadius: 8,
),
],
),
),
),
],
),
],
),
);
}
}
実行して確認すること
このコードを実行すると、動画が画面全体の背景として表示されます。
さらに、その上に次のUIが重なっています。
上部:フォロー中 / おすすめ
下部:ユーザー名 / キャプション / 音源情報
ここで確認するポイントは、次の3つです。
1. 動画をVideoPlayerで再生している
2. FittedBox + BoxFit.coverで全画面背景にしている
3. Stackで動画の上に文字を重ねている
これで、かなりTikTok風アプリの見た目に近づきました。
VideoBackgroundの仕組み
VideoBackground は、動画を背景として表示するWidgetです。
class VideoBackground extends StatelessWidget {
const VideoBackground({
super.key,
required this.controller,
});
final VideoPlayerController controller;
ここでは、外から VideoPlayerController を受け取っています。
required this.controller,
そして、VideoPlayer(controller) で動画を表示します。
VideoPlayer(controller)
size.widthとsize.height
final size = controller.value.size;
動画には、元のサイズがあります。
そのサイズを取得して、SizedBox に指定しています。
SizedBox(
width: size.width,
height: size.height,
child: VideoPlayer(controller),
)
このようにすることで、動画本来の縦横比を保ちながら、FittedBox で画面全体に広げられます。
グラデーションを重ねる理由
動画の上に白文字を置くと、背景によっては文字が読みにくくなることがあります。
そこで、VideoGradientOverlay を重ねています。
const Positioned.fill(
child: VideoGradientOverlay(),
),
VideoGradientOverlay は、上と下を少し暗くする半透明のグラデーションです。
動画そのまま
↓
白文字が読みにくいことがある
動画 + 黒グラデーション
↓
白文字が読みやすくなる
TikTok風UIでは、下部に文字が多いため、下を少し暗くすると読みやすくなります。
IgnorePointerとは何か
VideoGradientOverlay では、IgnorePointer を使っています。
return IgnorePointer(
child: DecoratedBox(...),
);
IgnorePointer は、タップなどの操作を無視するためのWidgetです。
グラデーションは見た目のためだけに重ねています。
もしグラデーションがタップを受け取ってしまうと、動画のタップ操作が邪魔されることがあります。
そこで、IgnorePointer を使います。
初心者向けには、次のように理解してください。
IgnorePointer = 見た目だけ表示して、タップ操作は受け取らないようにするWidget
この節で覚える動画再生の流れ
動画再生の基本の流れは、次の通りです。
1. VideoPlayerControllerを作る
2. initializeで動画を準備する
3. setLoopingでループ設定する
4. setVolumeで音量を設定する
5. playで再生する
6. VideoPlayerで画面に表示する
7. disposeで片付ける
コードで見ると、次の流れです。
controller = VideoPlayerController.networkUrl(
Uri.parse(videoUrl),
);
await controller.initialize();
controller
..setLooping(true)
..setVolume(0)
..play();
表示するときは、次のようにします。
VideoPlayer(controller)
使い終わったら、次のように片付けます。
controller.dispose();
手を動かす練習1:別の動画に変える
動画URLを次の部分で指定しています。
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'
これを次のように変えてみてください。
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'
動画が変わります。
この練習で、動画URLを変えると表示される動画も変わることを確認できます。
手を動かす練習2:ループを止める
次の部分を探してください。
..setLooping(true)
これを次のように変えてみます。
..setLooping(false)
動画が最後まで再生されると停止します。
TikTok風アプリでは、基本的に繰り返し再生したいので、最終的には true に戻してください。
手を動かす練習3:音量を変える
次の部分を探してください。
..setVolume(0)
これを次のように変えてみます。
..setVolume(0.5)
環境によっては音が出ます。
ただし、DartPadやブラウザでは自動再生の制限があるため、音あり自動再生は止められる場合があります。
学習中は、まず setVolume(0) のままで進めるのがおすすめです。
手を動かす練習4:グラデーションを濃くする
次の部分を探してください。
Colors.black.withOpacity(0.86),
これを次のように変えてみます。
Colors.black.withOpacity(0.95),
画面下部がさらに暗くなります。
下部の文字が読みやすくなる反面、動画は暗く見えます。
このように、UIでは「見やすさ」と「映像の見え方」のバランスを調整します。
よくあるつまずき1:動画が表示されない
動画が表示されない場合、まず次を確認してください。
- DartPadのPackagesにvideo_playerを追加しているか
- import 'package:video_player/video_player.dart'; があるか
- 動画URLが正しいか
- initializeが終わる前にVideoPlayerを表示していないか
今回のコードでは、isReady を使って、初期化が終わるまでローディングを表示しています。
if (!isReady) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
);
}
よくあるつまずき2:initializeを忘れる
VideoPlayerController は、作っただけでは動画を表示できません。
必ず次のように初期化します。
await controller.initialize();
initialize を忘れると、動画のサイズが分からず、正しく表示できないことがあります。
よくあるつまずき3:disposeを忘れる
controllerを作ったら、使い終わったときに片付けます。
@override
void dispose() {
controller.dispose();
super.dispose();
}
動画やアニメーションのcontrollerは、dispose で片付ける習慣をつけるとよいです。
よくあるつまずき4:constをつけすぎる
動画controllerのように、状態が変わるものを扱うWidgetでは、すべてに const をつけられるわけではありません。
たとえば、次のようなコードは controller が変数なので、const にはできません。
VideoBackground(controller: controller)
const でエラーになる場合は、外して大丈夫です。
よくあるつまずき5:全画面にならない
動画を全画面背景にするには、次の組み合わせが大切です。
Positioned.fill(
child: VideoBackground(controller: controller),
)
さらに、VideoBackground の中で FittedBox を使います。
FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: size.width,
height: size.height,
child: VideoPlayer(controller),
),
)
この組み合わせで、動画が画面いっぱいに広がります。
この節の確認問題
確認問題1
video_player は何のために使いますか。
答え
Flutterで動画を再生するために使います。
確認問題2
VideoPlayerController は何をするものですか。
答え
動画を読み込んだり、再生・停止したりするための管理役です。
確認問題3
initialize は何のために必要ですか。
答え
動画を使える状態に準備するためです。
動画のサイズなどを取得するためにも必要です。
確認問題4
VideoPlayer は何をするWidgetですか。
答え
VideoPlayerController が管理している動画を、画面に表示するWidgetです。
確認問題5
FittedBox と BoxFit.cover は何のために使いましたか。
答え
動画を画面全体に広げ、全画面背景のように表示するためです。
確認問題6
dispose は何のために使いますか。
答え
使い終わったcontrollerを片付けるためです。
この節のまとめ
この節では、video_player を使って、DartPad上で動画を再生しました。
最初は1本の動画を中央に表示し、その後、TikTok風に全画面背景として表示しました。
動画再生の基本の流れは、次の通りです。
VideoPlayerControllerを作る
↓
initializeで準備する
↓
setLoopingやsetVolumeを設定する
↓
playで再生する
↓
VideoPlayerで表示する
↓
disposeで片付ける
また、TikTok風UIでは、動画を全画面背景として表示し、その上に文字やボタンを重ねます。
Stack
├─ VideoBackground
├─ VideoGradientOverlay
├─ TopNavigation
└─ BottomVideoInfo
この節で一番大切なのは、次の一文です。
VideoPlayerControllerで動画を管理し、VideoPlayerで画面に表示する。
次の節では、今回作った動画再生と、前回学んだ PageView.builder を組み合わせます。
複数の動画を上下スワイプで切り替え、TikTok風アプリの中心部分を作っていきます。