
【カテゴリタブ】Shows・Movies・Games・New & Hotをタップして表示内容を切り替える
この節で学ぶこと
前の節では、Home画面上部にあるメニューアイコン、ロゴ、プロフィール画像を配置するヘッダーUIを学びました。
今回の節では、その下にあるカテゴリタブを見ていきます。
Shows
Movies
Games
New & Hot
このタブを押すと、Home画面に表示される作品が切り替わります。
たとえば、Shows を押すと番組・シリーズ系の作品が表示され、Movies を押すと映画系の作品が表示される、という仕組みです。
この節では、次の流れで学びます。
カテゴリの種類をenumで定義する
↓
作品データにカテゴリ情報を持たせる
↓
選択中のカテゴリを状態として管理する
↓
タブをタップしたらカテゴリを変更する
↓
カテゴリに合う作品だけを表示する
カテゴリ切り替えの全体像
今回のカテゴリ切り替えは、主に次の4つで作られています。
| 役割 | コード |
|---|---|
| カテゴリの種類を決める | HomeCategory |
| 表示用のカテゴリ名を作る | HomeCategoryLabel |
| カテゴリタブの見た目を作る | TopCategoryTabs |
| 選択中カテゴリに合わせて作品を絞り込む | filteredMovies |
流れとしては、こうです。
ユーザーがMoviesタブを押す
↓
selectedCategoryがHomeCategory.moviesになる
↓
moviesリストからmoviesカテゴリの作品だけを取り出す
↓
Home画面の作品一覧が切り替わる
ここで大切なのは、タブを押しただけで画面が変わるのではなく、選択中のカテゴリ状態が変わることで、表示する作品が変わる という考え方です。
HomeCategoryとは?
まず、カテゴリの種類を決めているコードを見てみましょう。
enum HomeCategory {
all,
shows,
movies,
games,
newHot,
}
enum は、選択肢をあらかじめ決めておくための仕組みです。
今回の場合、Home画面で使えるカテゴリをここで決めています。
| enumの値 | 意味 |
|---|---|
all | すべて |
shows | 番組・シリーズ |
movies | 映画 |
games | ゲーム |
newHot | 新着・話題 |
文字列で 'shows' や 'movies' と書く方法もありますが、文字列だとスペルミスが起きやすくなります。
たとえば、'movies' を間違えて 'moveis' と書いても、Dartは普通の文字として扱ってしまいます。
でも、enum を使えば、決められた選択肢の中から選ぶ形になるので、安全にコードを書きやすくなります。
labelで表示名を作る
HomeCategory はコード上の名前なので、そのまま画面に表示すると少し使いにくいです。
そこで、画面に表示するための文字を label として用意しています。
extension HomeCategoryLabel on HomeCategory {
String get label {
switch (this) {
case HomeCategory.all:
return 'All';
case HomeCategory.shows:
return 'Shows';
case HomeCategory.movies:
return 'Movies';
case HomeCategory.games:
return 'Games';
case HomeCategory.newHot:
return 'New & Hot';
}
}
}
これは、カテゴリごとに表示する文字を返すコードです。
たとえば、
HomeCategory.shows.label
とすると、次の文字が返ってきます。
Shows
HomeCategory.newHot.label なら、次の文字になります。
New & Hot
このように、コード上のカテゴリ名と、画面に表示する文字を分けています。
なぜextensionを使うの?
ここでは extension を使っています。
extension HomeCategoryLabel on HomeCategory {
extension は、既存の型にあとから便利な機能を追加するようなものです。
今回の場合は、HomeCategory に label という便利な読み方を追加しています。
これによって、タブを表示するときに次のように書けます。
tab.label
もし extension を使わない場合、毎回 switch 文を書いたり、別の関数を呼び出したりする必要があります。
extension にしておくと、カテゴリ自身が表示名を持っているように扱えるので、コードが読みやすくなります。
作品データにもカテゴリを持たせる
カテゴリタブで作品を切り替えるためには、作品データ側にもカテゴリ情報が必要です。
そのため、MovieItem には homeCategory があります。
final HomeCategory homeCategory;
作品データでは、次のように指定しています。
MovieItem(
title: 'Squid Game',
// 省略
homeCategory: HomeCategory.shows,
),
これは、
Squid GameはShowsカテゴリの作品です
という意味です。
映画として表示したい作品なら、次のようにします。
homeCategory: HomeCategory.movies,
新着・話題として表示したい作品なら、次のようにします。
homeCategory: HomeCategory.newHot,
タブを押したときに表示内容を切り替えられるのは、作品データにこのカテゴリ情報が入っているからです。
selectedCategoryで現在のカテゴリを管理する
Home画面では、現在どのカテゴリが選ばれているかを selectedCategory で管理しています。
HomeCategory selectedCategory = HomeCategory.all;
これは、最初はすべての作品を表示するという意味です。
| selectedCategory | 表示内容 |
|---|---|
HomeCategory.all | すべての作品 |
HomeCategory.shows | Showsの作品 |
HomeCategory.movies | Moviesの作品 |
HomeCategory.games | Gamesの作品 |
HomeCategory.newHot | New & Hotの作品 |
タブを押すと、この selectedCategory が変わります。
そして、selectedCategory が変わることで、表示する作品も変わります。
作品を絞り込む処理
カテゴリに合わせて作品を絞り込んでいるのが、次の処理です。
final filteredMovies = selectedCategory == HomeCategory.all
? movies
: movies
.where((movie) => movie.homeCategory == selectedCategory)
.toList();
少し難しく見えますが、やっていることはシンプルです。
もしAllが選ばれているなら、すべての作品を表示する
そうでなければ、選ばれているカテゴリと同じ作品だけを表示する
という意味です。
たとえば、selectedCategory が HomeCategory.movies の場合は、
movie.homeCategory == HomeCategory.movies
の作品だけが残ります。
whereとは?
ここで出てくる where は、リストの中から条件に合うものだけを取り出すための処理です。
movies.where((movie) => movie.homeCategory == selectedCategory)
このコードは、
moviesリストの中から、
movie.homeCategoryがselectedCategoryと同じ作品だけを取り出す
という意味です。
たとえば、作品データが次のようになっていたとします。
Squid Game → shows
Interstellar → movies
Wednesday → newHot
このとき、selectedCategory が movies なら、Interstellar だけが表示対象になります。
toListとは?
where の後には、toList() がついています。
.toList();
where は条件に合うものを取り出しますが、そのままだと少し特殊な形です。
画面で扱いやすいリストに戻すために、toList() を使っています。
whereで絞り込む
↓
toListでリストにする
↓
画面表示に使う
という流れです。
TopCategoryTabsとは?
次に、カテゴリタブの見た目を作っている TopCategoryTabs を見ていきます。
class TopCategoryTabs extends StatelessWidget {
const TopCategoryTabs({
super.key,
required this.selectedCategory,
required this.onSelectCategory,
});
final HomeCategory selectedCategory;
final ValueChanged<HomeCategory> onSelectCategory;
TopCategoryTabs は、Shows、Movies、Games、New & Hot のタブを横並びで表示するWidgetです。
受け取っている値は2つです。
| 値 | 役割 |
|---|---|
selectedCategory | 今選ばれているカテゴリ |
onSelectCategory | タブを押したときに呼ばれる処理 |
TopCategoryTabs 自体は StatelessWidget です。
理由は、選択状態を自分で持っているわけではなく、親から受け取って表示しているだけだからです。
タブとして表示するカテゴリを決める
TopCategoryTabs の中では、表示するカテゴリを次のように決めています。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
];
ここで注目したいのは、HomeCategory.all が入っていないことです。
HomeCategory.all は、内部的には存在します。
でも、ヘッダーのカテゴリタブには表示していません。
つまり、コード上ではすべて表示用の all を持っているけれど、画面上のタブには Shows、Movies、Games、New & Hot だけを出しています。
このように、データとして持つカテゴリ と 画面に表示するカテゴリ は、必ずしも完全に同じでなくても大丈夫です。
タブを横に並べる
タブは Row で横に並べています。
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: tabs.map((tab) {
...
}).toList(),
);
Row は、Widgetを横方向に並べるためのWidgetです。
今回の表示はこうなります。
Shows Movies Games New & Hot
mainAxisAlignment: MainAxisAlignment.center を指定しているので、タブ全体が中央寄せになります。
mainAxisAlignment: MainAxisAlignment.center,
ヘッダーの中央にカテゴリタブが並ぶため、動画アプリ風の雰囲気が出ます。
mapでタブを作る
タブの数だけWidgetを作るために、map を使っています。
children: tabs.map((tab) {
final selected = tab == selectedCategory;
return GestureDetector(
onTap: () => onSelectCategory(tab),
child: ...
);
}).toList(),
tabs の中には、4つのカテゴリが入っています。
[
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
]
この4つを1つずつ取り出して、タブのWidgetに変換しています。
HomeCategory.shows → Showsタブ
HomeCategory.movies → Moviesタブ
HomeCategory.games → Gamesタブ
HomeCategory.newHot → New & Hotタブ
このように、データのリストからUIを作る考え方は、Flutterでとてもよく使います。
選択中かどうかを判断する
タブごとに、選択中かどうかを判断しています。
final selected = tab == selectedCategory;
これは、
今作っているタブが、現在選ばれているカテゴリと同じか?
を見ています。
たとえば、selectedCategory が HomeCategory.movies のとき、Movies タブだけが selected == true になります。
それ以外のタブは false です。
この selected を使って、選択中タブの色や背景を変えます。
GestureDetectorでタップできるようにする
タブは GestureDetector で包まれています。
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onSelectCategory(tab),
child: ...
)
GestureDetector は、タップなどの操作を受け取るためのWidgetです。
ここでは、タブを押したときに次の処理が動きます。
onSelectCategory(tab)
つまり、
押されたタブのカテゴリを親に伝える
ということです。
たとえば、Movies タブを押した場合は、
onSelectCategory(HomeCategory.movies)
が実行されます。
onSelectCategoryはどこにつながっている?
onSelectCategory は、親である NetflixHomePage から渡されています。
Home画面側では、次のように受け取ります。
NetflixTopArea(
hero: hero,
selectedCategory: selectedCategory,
onTapMenu: openMenu,
onSelectCategory: selectCategory,
),
ここで渡されているのが、selectCategory です。
void selectCategory(HomeCategory category) {
setState(() {
selectedCategory = category;
});
}
つまり、カテゴリタブを押したときの流れはこうです。
タブを押す
↓
TopCategoryTabsでonSelectCategory(tab)が呼ばれる
↓
NetflixHomePageのselectCategoryが動く
↓
selectedCategoryが変更される
↓
setStateで画面が更新される
↓
filteredMoviesが再計算される
↓
表示される作品が変わる
この流れが、カテゴリ切り替えの中心です。
選択中タブの見た目を変える
カテゴリタブでは、選択中かどうかによって見た目を変えています。
たとえば、選択中のタブだけ背景を少し明るくしたり、文字を白くしたりできます。
イメージとしては、次のようなコードです。
AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: selected
? Colors.white.withValues(alpha: 0.16)
: Colors.transparent,
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: selected
? Colors.white.withValues(alpha: 0.22)
: Colors.white.withValues(alpha: 0.0),
),
),
child: Text(
tab.label,
style: TextStyle(
color: selected ? NetflixColors.white : NetflixColors.muted,
fontSize: 13,
fontWeight: selected ? FontWeight.w800 : FontWeight.w700,
),
),
)
ここでは、selected が true のときだけ、背景や文字色を変えています。
選択中:白っぽい背景 + 白文字
未選択:透明背景 + グレー文字
ユーザーは見た目の違いによって、今どのカテゴリを見ているのか分かります。
AnimatedContainerとは?
ここで使っている AnimatedContainer は、Container の変化をなめらかにアニメーションしてくれるWidgetです。
AnimatedContainer(
duration: const Duration(milliseconds: 180),
...
)
たとえば、選択中タブの背景色が変わるとき、パッと切り替わるのではなく、少しだけなめらかに変わります。
duration: const Duration(milliseconds: 180),
これは、アニメーション時間を180ミリ秒にするという意味です。
タブのような小さなUIでは、速めのアニメーションが合います。
遅すぎると、操作がもたついて見えることがあります。
Textでカテゴリ名を表示する
タブの文字は、Text で表示しています。
Text(
tab.label,
style: TextStyle(
color: selected ? NetflixColors.white : NetflixColors.muted,
fontSize: 13,
fontWeight: selected ? FontWeight.w800 : FontWeight.w700,
),
)
ここで出てくる tab.label は、先ほどの extension で用意した表示名です。
HomeCategory.movies.label
なら、
Movies
になります。
選択中かどうかによって、文字の色と太さも変えています。
color: selected ? NetflixColors.white : NetflixColors.muted,
fontWeight: selected ? FontWeight.w800 : FontWeight.w700,
選択中のタブを少し強く見せることで、現在地が分かりやすくなります。
作品一覧側も切り替わる
カテゴリが切り替わると、Home画面の作品一覧も変わります。
Home画面では、絞り込まれた作品リストを使って、コンテンツ行を表示しています。
final items = filteredMovies.isEmpty ? movies : filteredMovies;
final hero = items.first;
ここでは、filteredMovies が空でなければ、そのカテゴリに合った作品を使います。
もし該当作品がない場合は、空画面にしないように movies を使っています。
filteredMoviesに作品がある → その作品を表示
filteredMoviesが空 → すべての作品を表示
教材用のアプリでは、カテゴリに作品が少ない場合もあるため、このようにしておくと画面が真っ黒になりにくいです。
カテゴリごとの作品を増やすには?
カテゴリタブを活かすには、作品データに homeCategory を正しく入れる必要があります。
たとえば、映画作品を追加したい場合は、次のようにします。
MovieItem(
title: 'Interstellar',
subtitle: 'A journey beyond time and space.',
description: 'A team travels through a wormhole to find a new home for humanity.',
posterUrl: 'https://example.com/interstellar-poster.jpg',
backdropUrl: 'https://example.com/interstellar-backdrop.jpg',
youtubeVideoId: 'zSWdZVtXT7E',
year: 2014,
rating: '13+',
seasonLabel: 'Movie',
category: 'Sci-Fi',
matchRate: 96,
homeCategory: HomeCategory.movies,
),
ここで重要なのは、最後の部分です。
homeCategory: HomeCategory.movies,
この指定があることで、Movies タブを押したときに表示されるようになります。
まずカスタマイズしてみよう
今回は、Games タブの表示名を変えてみましょう。
次のコードを探してください。
case HomeCategory.games:
return 'Games';
これを次のように変えてみます。
case HomeCategory.games:
return 'Play';
保存して、画面を確認してください。
カテゴリタブの Games が Play に変わっていれば成功です。
このように、表示名だけを変えたい場合は、label の部分を変更します。
タブの順番をカスタマイズしてみよう
タブの順番は、次のリストで決まっています。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
];
たとえば、New & Hot を先頭にしたい場合は、次のようにします。
final tabs = [
HomeCategory.newHot,
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
];
これで表示順が変わります。
New & Hot
Shows
Movies
Games
ただし、ユーザーが見慣れている順番もあるので、見た目だけでなく使いやすさも考えて決めましょう。
選択中タブの色をカスタマイズしてみよう
選択中タブの背景色は、次のような部分で決まっています。
color: selected
? Colors.white.withValues(alpha: 0.16)
: Colors.transparent,
選択中をもっと目立たせたい場合は、少し濃くします。
color: selected
? Colors.white.withValues(alpha: 0.28)
: Colors.transparent,
逆に、もっと控えめにしたい場合は、薄くします。
color: selected
? Colors.white.withValues(alpha: 0.08)
: Colors.transparent,
UIでは、目立たせすぎると少し重く見えることがあります。
アプリ全体の雰囲気に合わせて調整しましょう。
よくあるつまずきポイント
Q. タブを押しても表示が変わりません。
onSelectCategory が正しく呼ばれているか確認してください。
onTap: () => onSelectCategory(tab),
さらに、親側で setState を使っているか確認します。
void selectCategory(HomeCategory category) {
setState(() {
selectedCategory = category;
});
}
setState がないと、値が変わっても画面が更新されません。
Q. Moviesタブを押しても作品が出ません。
作品データに homeCategory: HomeCategory.movies が入っているか確認してください。
homeCategory: HomeCategory.movies,
もしMoviesカテゴリの作品が1つもなければ、Moviesタブを押しても表示対象が空になります。
Q. タブの文字が変わりません。
表示名を変えたい場合は、HomeCategoryLabel の label を確認してください。
case HomeCategory.games:
return 'Games';
ここを変更すれば、画面に表示される文字が変わります。
Q. 新しいカテゴリを追加したらエラーが出ました。
enum に新しい値を追加した場合は、label の switch にも追加する必要があります。
たとえば、anime を追加した場合です。
enum HomeCategory {
all,
shows,
movies,
games,
newHot,
anime,
}
この場合、label にも追加します。
case HomeCategory.anime:
return 'Anime';
さらに、タブに表示したい場合は tabs にも追加します。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
HomeCategory.anime,
];
作品データにも、必要に応じて次のように入れます。
homeCategory: HomeCategory.anime,
カテゴリを追加するときは、複数の場所をセットで変更する必要があります。
チャレンジ
チャレンジ1:GamesタブをPlayに変えよう
次のコードを探してください。
case HomeCategory.games:
return 'Games';
これを次のように変更します。
case HomeCategory.games:
return 'Play';
画面上のタブ名が変わるか確認してください。
チャレンジ2:New & Hotを先頭に表示しよう
次のコードを探してください。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
];
これを次のように変更します。
final tabs = [
HomeCategory.newHot,
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
];
タブの表示順が変わるか確認してください。
チャレンジ3:選択中タブの背景を少し濃くしよう
次のコードを探してください。
Colors.white.withValues(alpha: 0.16)
これを次のように変更します。
Colors.white.withValues(alpha: 0.28)
選択中のタブが少し目立つようになります。
チャレンジ4:Animeカテゴリを追加してみよう
まず、HomeCategory に anime を追加します。
enum HomeCategory {
all,
shows,
movies,
games,
newHot,
anime,
}
次に、label にも追加します。
case HomeCategory.anime:
return 'Anime';
さらに、タブにも追加します。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
HomeCategory.anime,
];
最後に、作品データにも anime カテゴリを指定します。
homeCategory: HomeCategory.anime,
チャレンジの答え
チャレンジ1の答え
変更前:
case HomeCategory.games:
return 'Games';
変更後:
case HomeCategory.games:
return 'Play';
カテゴリタブの表示名が Games から Play に変わります。
チャレンジ2の答え
変更前:
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
];
変更後:
final tabs = [
HomeCategory.newHot,
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
];
New & Hot が一番左に表示されます。
チャレンジ3の答え
変更前:
Colors.white.withValues(alpha: 0.16)
変更後:
Colors.white.withValues(alpha: 0.28)
選択中タブの背景が少し濃くなります。
チャレンジ4の答え
enum に追加します。
enum HomeCategory {
all,
shows,
movies,
games,
newHot,
anime,
}
label に追加します。
case HomeCategory.anime:
return 'Anime';
tabs に追加します。
final tabs = [
HomeCategory.shows,
HomeCategory.movies,
HomeCategory.games,
HomeCategory.newHot,
HomeCategory.anime,
];
作品データに追加します。
homeCategory: HomeCategory.anime,
これで、Anime タブを押したときに、HomeCategory.anime の作品が表示されるようになります。
この節のまとめ
この節では、Shows、Movies、Games、New & Hot のカテゴリタブを押して、Home画面の表示内容を切り替える方法を学びました。
大切なポイントは次の通りです。
HomeCategoryは、Home画面で使うカテゴリの種類を決めるenum。extensionを使うと、HomeCategoryにlabelのような便利な表示名を追加できる。- 作品データには
homeCategoryを持たせる。 selectedCategoryは、現在選ばれているカテゴリを表す。whereを使うと、リストから条件に合う作品だけを取り出せる。toList()を使うと、絞り込んだ結果をリストとして扱える。TopCategoryTabsは、カテゴリタブの見た目を作るWidget。GestureDetectorを使うと、タブをタップできるようになる。onSelectCategory(tab)で、押されたカテゴリを親に伝える。- 親側で
setStateを使うことで、画面が更新される。 - 選択中タブの見た目を変えると、今どのカテゴリを見ているか分かりやすくなる。
次のステップ
次の節では、下から表示されるメニューシートを見ていきます。
メニューアイコンを押したときに、画面下からメニューが出てくる仕組みです。
showModalBottomSheet を使うと、設定メニューやアクションメニューのようなUIを作れるようになります。