
【LikeとMy List】setStateでハートや保存ボタンのON/OFFを切り替える
この節で学ぶこと
前の節では、作品詳細画面にタイトル、説明文、メタ情報、Playボタン、Downloadボタン、アクションボタンを表示する方法を学びました。
今回の節では、その中でも特に Likeボタン と My Listボタン に注目します。
動画アプリでは、作品詳細画面に次のようなボタンがよくあります。
+ My List
♡ Like
↗ Share
このうち、My List と Like は、押すたびに見た目が変わります。
My Listを押す
↓
+ が ✓ に変わる
Likeを押す
↓
♡ が ♥ に変わる
このように、ユーザーの操作によって画面の見た目を変えるには、Flutterでは setState を使います。
この節では、setState を使って、保存ボタンやハートボタンのON/OFFを切り替える仕組みを学びます。
今回見るコード
詳細画面では、次の2つの状態を持っています。
class _MovieDetailPageState extends State<MovieDetailPage> {
bool isInMyList = false;
bool isLiked = false;
isInMyList は、作品がMy Listに追加されているかどうかを表します。
isLiked は、作品にLikeしているかどうかを表します。
それぞれ、最初は false です。
isInMyList = false
↓
まだMy Listに入っていない
isLiked = false
↓
まだLikeしていない
そして、画面の中では次のように使っています。
DetailActions(
isInMyList: isInMyList,
isLiked: isLiked,
onToggleMyList: () {
setState(() {
isInMyList = !isInMyList;
});
},
onToggleLike: () {
setState(() {
isLiked = !isLiked;
});
},
onShare: () => shareNetaflixMovie(
context: context,
movie: movie,
),
),
このコードが、My ListとLikeのON/OFF切り替えの中心です。
状態とは何か
まず、「状態」という言葉を整理しましょう。
アプリの画面には、変わらないものと変わるものがあります。
たとえば、作品タイトルは基本的に変わりません。
Squid Game
Wednesday
Stranger Things
一方で、LikeボタンやMy Listボタンは、ユーザーが押すことで変わります。
Likeしていない
↓
Likeしている
保存していない
↓
保存している
このように、画面の中で変わる情報のことを「状態」と考えると分かりやすいです。
Flutterでは、この状態を変数で持ちます。
今回の場合は、次の2つです。
bool isInMyList = false;
bool isLiked = false;
boolとは?
bool は、true か false のどちらかを入れる型です。
bool isLiked = false;
これは、isLiked という変数に false を入れているという意味です。
bool は、ON/OFFを表すときによく使います。
| 状態 | boolで表すと |
|---|---|
| Likeしている | true |
| Likeしていない | false |
| My Listに入っている | true |
| My Listに入っていない | false |
| 表示する | true |
| 表示しない | false |
今回のように、ボタンのON/OFFを切り替えるときには、bool がとても便利です。
最初はfalseにしておく
詳細画面を開いた直後は、次のようになっています。
bool isInMyList = false;
bool isLiked = false;
これは、初期状態ではMy Listにも入っておらず、Likeもされていないという意味です。
初期状態
↓
My List:OFF
Like:OFF
もちろん、実際のアプリでは、ユーザーが過去に保存した作品やLikeした作品をデータベースから読み込むことがあります。
ただし、この教材ではまず画面の中だけでON/OFFを切り替える基本を学びます。
そのため、最初は false にしています。
StatefulWidgetが必要な理由
isInMyList や isLiked のように、画面の中で変わる値を持つ場合は、StatefulWidget を使います。
class MovieDetailPage extends StatefulWidget {
もし StatelessWidget だと、基本的には画面の中で状態を持って更新することができません。
今回の詳細画面では、ユーザーがボタンを押すたびに見た目が変わります。
Likeボタンを押す
↓
ハートが変わる
My Listボタンを押す
↓
アイコンが変わる
このような「画面が変化するUI」には、StatefulWidget が向いています。
Stateクラスで状態を管理する
StatefulWidget では、実際の状態は State クラスの中に書きます。
class _MovieDetailPageState extends State<MovieDetailPage> {
bool isInMyList = false;
bool isLiked = false;
この _MovieDetailPageState が、詳細画面の状態を持っています。
isInMyList と isLiked は、この画面の中で使われる変数です。
_MovieDetailPageState
├── isInMyList
└── isLiked
この2つの値を変えることで、My ListボタンとLikeボタンの見た目を切り替えます。
setStateとは?
setState は、状態を変えて、画面を更新するための関数です。
今回のコードでは、次のように使っています。
setState(() {
isLiked = !isLiked;
});
このコードは、次の2つのことをしています。
1. isLikedの値を反対にする
2. 画面をもう一度描き直す
ここがとても大切です。
ただ変数の値を変えるだけでは、画面が変わらないことがあります。
Flutterに「状態が変わったので、画面を更新してください」と伝えるために setState を使います。
setStateを使わないとどうなる?
たとえば、次のように書いたとします。
isLiked = !isLiked;
この場合、変数の値は変わります。
しかし、Flutterが画面を更新してくれない場合があります。
つまり、内部では isLiked が変わっているのに、画面上のハートアイコンは変わらないかもしれません。
そこで、次のように setState の中で変更します。
setState(() {
isLiked = !isLiked;
});
こうすると、Flutterは「状態が変わった」と分かり、画面を再描画してくれます。
! は反対にする記号
Likeの切り替えでは、次のようなコードがあります。
isLiked = !isLiked;
この ! は、真偽値を反対にする記号です。
| 元の値 | !** をつけた値** |
|---|---|
true | false |
false | true |
つまり、isLiked = !isLiked; は、次のような意味です。
isLikedがfalseならtrueにする
isLikedがtrueならfalseにする
ボタンを押すたびにON/OFFを切り替えたい場合、とてもよく使う書き方です。
Likeボタンの切り替え
Likeボタンでは、次のコードを使っています。
onToggleLike: () {
setState(() {
isLiked = !isLiked;
});
},
これを日本語で読むと、次のようになります。
Likeボタンが押されたら
↓
setStateを実行する
↓
isLikedを反対にする
↓
画面を更新する
最初は isLiked = false です。
isLiked = false
↓
空のハートを表示
ボタンを押すと、true になります。
isLiked = true
↓
塗りつぶしのハートを表示
もう一度押すと、また false に戻ります。
isLiked = false
↓
空のハートに戻る
My Listボタンの切り替え
My Listボタンも、考え方は同じです。
onToggleMyList: () {
setState(() {
isInMyList = !isInMyList;
});
},
これは、次の意味です。
My Listボタンが押されたら
↓
setStateを実行する
↓
isInMyListを反対にする
↓
画面を更新する
最初は isInMyList = false です。
isInMyList = false
↓
+アイコンを表示
ボタンを押すと、true になります。
isInMyList = true
↓
チェックアイコンを表示
もう一度押すと、また false に戻ります。
isInMyList = false
↓
+アイコンに戻る
DetailActionsに状態を渡す
詳細画面では、DetailActions に状態を渡しています。
DetailActions(
isInMyList: isInMyList,
isLiked: isLiked,
onToggleMyList: () {
setState(() {
isInMyList = !isInMyList;
});
},
onToggleLike: () {
setState(() {
isLiked = !isLiked;
});
},
onShare: () => shareNetaflixMovie(
context: context,
movie: movie,
),
),
ここで大切なのは、DetailActions 自体が状態を持っているのではなく、親である _MovieDetailPageState が状態を持っているということです。
_MovieDetailPageState
├── isInMyList
├── isLiked
└── DetailActionsに渡す
DetailActions は、渡された値をもとに表示を変えています。
なぜ親で状態を持つのか
今回、isInMyList と isLiked は、MovieDetailPage の中で管理しています。
理由は、詳細画面全体に関係する状態だからです。
DetailActions の中だけで状態を持つこともできますが、親で管理しておくと、他の場所でも同じ状態を使いやすくなります。
たとえば、今後次のような表示を追加したいとします。
この作品はMy Listに追加済みです
この表示を詳細画面の別の場所に出したい場合、親が状態を持っていた方が扱いやすいです。
親が状態を持つ
↓
必要な子Widgetに状態を渡す
↓
画面全体で状態を使いやすい
Flutterでは、この考え方をよく使います。
DetailActionsの役割
DetailActions は、My List、Like、Shareの3つのボタンを横に並べるためのWidgetです。
class DetailActions extends StatelessWidget {
const DetailActions({
super.key,
required this.isInMyList,
required this.isLiked,
required this.onToggleMyList,
required this.onToggleLike,
required this.onShare,
});
final bool isInMyList;
final bool isLiked;
final VoidCallback onToggleMyList;
final VoidCallback onToggleLike;
final VoidCallback onShare;
受け取っている値は、次の5つです。
| 値 | 型 | 役割 |
|---|---|---|
isInMyList | bool | My Listに入っているか |
isLiked | bool | Likeされているか |
onToggleMyList | VoidCallback | My Listボタンを押したときの処理 |
onToggleLike | VoidCallback | Likeボタンを押したときの処理 |
onShare | VoidCallback | Shareボタンを押したときの処理 |
DetailActions は状態を受け取り、それに合わせてアイコンや色を変えます。
VoidCallbackとは?
VoidCallback は、「引数を受け取らず、戻り値も返さない関数」の型です。
少し難しく聞こえますが、ボタンを押したときの処理によく使います。
たとえば、次のような処理です。
onToggleLike: () {
setState(() {
isLiked = !isLiked;
});
},
この関数は、何か値を受け取っているわけではありません。
そして、何か値を返しているわけでもありません。
ただ、押されたときに処理を実行しています。
このような関数を受け取るときに、VoidCallback を使います。
DetailActionsの中でボタンを並べる
DetailActions の中では、Row を使ってボタンを横に並べます。
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
DetailAction(
icon: isInMyList
? Icons.check_rounded
: Icons.add_rounded,
label: 'My List',
active: isInMyList,
onTap: onToggleMyList,
),
DetailAction(
icon: isLiked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
label: 'Like',
active: isLiked,
onTap: onToggleLike,
),
DetailAction(
icon: Icons.share_outlined,
label: 'Share',
active: false,
onTap: onShare,
),
],
);
}
画面上では、次のように並びます。
My List Like Share
同じ形のボタンを3つ並べるために、DetailAction という部品を使っています。
mainAxisAlignment: spaceAroundとは?
Row には、次の指定があります。
mainAxisAlignment: MainAxisAlignment.spaceAround,
これは、横方向に並ぶ要素の間隔を自動で調整する指定です。
spaceAround を使うと、各ボタンの周りに均等な余白ができます。
My List Like Share
スマホ画面では、ボタン同士が近すぎると押しにくくなります。
そのため、適度に間隔を空けて並べています。
My Listのアイコンを切り替える
My Listボタンでは、isInMyList の値によってアイコンを変えています。
icon: isInMyList
? Icons.check_rounded
: Icons.add_rounded,
これは、三項演算子です。
意味は次の通りです。
isInMyListがtrueなら、チェックアイコン
isInMyListがfalseなら、追加アイコン
表にすると、こうなります。
isInMyList | 表示するアイコン |
|---|---|
false | Icons.add_rounded |
true | Icons.check_rounded |
これによって、ボタンを押すたびに「追加前」と「追加後」の見た目が切り替わります。
Likeのアイコンを切り替える
Likeボタンも同じです。
icon: isLiked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
意味は次の通りです。
isLikedがtrueなら、塗りつぶしハート
isLikedがfalseなら、空のハート
表にすると、こうなります。
isLiked | 表示するアイコン |
|---|---|
false | Icons.favorite_border_rounded |
true | Icons.favorite_rounded |
bool と三項演算子を組み合わせることで、状態に応じてUIを変えられます。
activeで色も切り替える
DetailAction には、active という値も渡しています。
DetailAction(
icon: isLiked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
label: 'Like',
active: isLiked,
onTap: onToggleLike,
),
ここでは、active: isLiked としています。
つまり、Likeされているときは active が true になります。
My Listも同じです。
active: isInMyList,
この active を使って、DetailAction の中で文字色やアイコン色を変えます。
ここまでのまとめ
ここまでで、LikeとMy ListのON/OFF切り替えの基本を確認しました。
大切なポイントは次の通りです。
- ON/OFFの状態は
boolで管理できる。 isInMyListはMy Listに入っているかを表す。isLikedはLikeされているかを表す。- 状態が変わる画面には
StatefulWidgetを使う。 setStateの中で状態を変えると、画面が更新される。!を使うと、trueとfalseを反転できる。DetailActionsに状態と処理を渡して、子Widget側で表示を変える。- 三項演算子を使うと、状態に応じてアイコンを切り替えられる。
activeを渡すことで、選択中かどうかを見た目に反映できる。
DetailActionの中でactiveを使う
前半では、DetailActions から DetailAction に active を渡すところまで見ました。
ここからは、DetailAction の中で active を使って、アイコンや文字の色を切り替える仕組みを見ていきます。
DetailAction は、My List、Like、Shareの1つ1つのボタンを作る部品です。
class DetailAction extends StatelessWidget {
const DetailAction({
super.key,
required this.icon,
required this.label,
required this.active,
required this.onTap,
});
final IconData icon;
final String label;
final bool active;
final VoidCallback onTap;
この中で、active が true か false かによって、ボタンの見た目を変えます。
activeで色を切り替える
DetailAction の build の中では、次のように色を決めています。
@override
Widget build(BuildContext context) {
final color = active ? NetflixColors.white : NetflixColors.muted;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
width: 88,
child: Column(
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
特に大事なのは、この部分です。
final color = active ? NetflixColors.white : NetflixColors.muted;
これは、次の意味です。
activeがtrueなら白
activeがfalseなら薄いグレー
表にすると、こうです。
active | 色 |
|---|---|
true | NetflixColors.white |
false | NetflixColors.muted |
つまり、ONになっているボタンは白く表示され、OFFのボタンは少し薄い色で表示されます。
アイコンと文字に同じ色を使う
決めた color は、アイコンと文字の両方に使っています。
Icon(icon, color: color, size: 28),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
),
これにより、ボタン全体の状態が分かりやすくなります。
OFF:薄いグレーのアイコンと文字
ON:白いアイコンと文字
アイコンだけ変えるよりも、文字色も一緒に変えた方が、ユーザーに状態が伝わりやすくなります。
GestureDetectorでタップを受け取る
DetailAction も、GestureDetector で包まれています。
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
width: 88,
child: Column(
children: [
...
],
),
),
);
GestureDetector は、タップなどの操作を受け取るためのWidgetです。
ここでは、ボタンを押したときに onTap を実行します。
onTap: onTap,
この onTap には、親から渡された処理が入っています。
My Listの場合は、onToggleMyList が渡されています。
Likeの場合は、onToggleLike が渡されています。
Shareの場合は、onShare が渡されています。
behavior: HitTestBehavior.opaqueとは?
GestureDetector には、次の指定があります。
behavior: HitTestBehavior.opaque,
これは、タップ判定の範囲を分かりやすくするための指定です。
DetailAction は、アイコンと文字だけでできています。
もしタップ判定がアイコンや文字の部分だけだと、少し押しにくくなることがあります。
そこで、SizedBox(width: 88) 全体をタップしやすくするために、HitTestBehavior.opaque を指定しています。
アイコンだけでなく
ボタンの幅全体をタップしやすくする
スマホアプリでは、見た目以上に「押しやすい範囲」を作ることが大切です。
SizedBoxでボタンの幅をそろえる
DetailAction の中では、SizedBox で幅を決めています。
SizedBox(
width: 88,
child: Column(
children: [
...
],
),
),
My List、Like、Shareの3つのボタンの幅を同じにするためです。
[ My List ] [ Like ] [ Share ]
幅がバラバラだと、横並びにしたときに少し乱れて見えます。
同じ幅にしておくと、整ったUIになります。
Columnでアイコンと文字を縦に並べる
DetailAction の中では、Column を使っています。
Column(
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
),
],
)
並びはこうです。
アイコン
↓
文字
動画アプリの詳細画面では、このようにアイコンを上、ラベルを下に置く操作ボタンがよく使われます。
Shareだけactiveがfalseの理由
DetailActions のShareボタンでは、次のように書いています。
DetailAction(
icon: Icons.share_outlined,
label: 'Share',
active: false,
onTap: onShare,
),
Shareは、My ListやLikeと違って、ON/OFFを持つボタンではありません。
押したら共有画面を開くだけです。
Shareを押す
↓
共有画面を開く
↓
状態は変わらない
そのため、active: false にしています。
一方で、My ListとLikeはON/OFFがあります。
active: isInMyList
active: isLiked
この違いを理解しておくと、ボタンごとの役割が分かりやすくなります。
状態が変わる流れをもう一度整理しよう
Likeボタンを押したときの流れを、もう一度整理します。
1. Likeボタンを押す
2. DetailActionのonTapが実行される
3. 親から渡されたonToggleLikeが呼ばれる
4. setStateが動く
5. isLiked = !isLiked で値が反転する
6. 画面が再描画される
7. isLikedに応じてアイコンと色が変わる
My Listも同じです。
1. My Listボタンを押す
2. DetailActionのonTapが実行される
3. 親から渡されたonToggleMyListが呼ばれる
4. setStateが動く
5. isInMyList = !isInMyList で値が反転する
6. 画面が再描画される
7. isInMyListに応じてアイコンと色が変わる
このように、setState は「値を変える」だけでなく、「画面を更新する合図」でもあります。
setStateの中で複数の状態を変えることもできる
今回のコードでは、LikeとMy Listを別々に切り替えています。
setState(() {
isLiked = !isLiked;
});
setState(() {
isInMyList = !isInMyList;
});
ただし、必要であれば、setState の中で複数の状態をまとめて変えることもできます。
たとえば、Likeしたら自動でMy Listにも追加したい場合は、次のようにできます。
setState(() {
isLiked = true;
isInMyList = true;
});
ただし、これはアプリの仕様によります。
教材の今の段階では、LikeとMy Listは別々に切り替える方が分かりやすいです。
状態は今だけ保存される
ここで大切な注意点があります。
今回の isLiked や isInMyList は、画面の中だけで管理されています。
bool isInMyList = false;
bool isLiked = false;
そのため、アプリを閉じたり、画面を作り直したりすると、状態は元に戻ります。
Likeする
↓
ハートがONになる
↓
アプリを閉じる
↓
もう一度開く
↓
またOFFに戻る
これは、データベースや端末内保存に記録していないからです。
実際のアプリで保存状態を残したい場合は、次のような仕組みが必要になります。
| 保存方法 | 例 |
|---|---|
| 端末内に保存 | SharedPreferencesなど |
| サーバーに保存 | Supabase、Firebaseなど |
| ログインユーザーごとに保存 | Auth + Database |
この教材では、まず setState で画面の中の状態を変える基本を学んでいます。
UIだけでなくデータ保存へ発展できる
今のコードは、見た目のON/OFFだけです。
しかし、この考え方は、あとからデータ保存にも発展できます。
たとえば、Likeボタンを押したときに、次のような処理を追加できます。
setStateで画面を変える
↓
データベースに保存する
↓
次回開いたときに保存状態を読み込む
今後、本格的なアプリにする場合は、次のような流れになります。
// イメージ
onToggleLike: () async {
setState(() {
isLiked = !isLiked;
});
// このあと、データベースに保存する処理を追加する
}
まずは画面上でON/OFFが切り替わることを理解し、そのあと保存機能へ進むと分かりやすいです。
よくあるつまずきポイント
Q. ボタンを押してもアイコンが変わりません。
setState の中で状態を変えているか確認してください。
setState(() {
isLiked = !isLiked;
});
次のように setState なしで書くと、画面が更新されないことがあります。
isLiked = !isLiked;
状態を変えて画面に反映したいときは、setState を使いましょう。
Q. setState が使えません。
setState は、StatefulWidget の State クラスの中で使います。
次のような StatelessWidget の中では使えません。
class DetailActions extends StatelessWidget {
今回の場合、setState は _MovieDetailPageState の中で使っています。
class _MovieDetailPageState extends State<MovieDetailPage> {
子Widgetである DetailActions には、親から処理を渡しています。
Q. LikeとMy Listが同時に変わってしまいます。
isLiked と isInMyList を別々の変数にしているか確認してください。
bool isInMyList = false;
bool isLiked = false;
Likeボタンでは isLiked だけを変えます。
setState(() {
isLiked = !isLiked;
});
My Listボタンでは isInMyList だけを変えます。
setState(() {
isInMyList = !isInMyList;
});
同じ変数を使ってしまうと、2つのボタンが同時に変わる原因になります。
Q. ShareボタンもON/OFFしてしまいます。
Shareは状態を持たないボタンです。
そのため、active は固定で false にしています。
DetailAction(
icon: Icons.share_outlined,
label: 'Share',
active: false,
onTap: onShare,
),
Shareは押したら共有画面を開くボタンなので、LikeやMy Listとは役割が違います。
チャレンジ
チャレンジ1:Likeの初期状態をONにしてみよう
次のコードを探してください。
bool isLiked = false;
これを次のように変更します。
bool isLiked = true;
詳細画面を開いた直後から、LikeボタンがONになっているか確認してください。
チャレンジ2:My Listの初期状態をONにしてみよう
次のコードを探してください。
bool isInMyList = false;
これを次のように変更します。
bool isInMyList = true;
詳細画面を開いた直後から、My Listボタンがチェック表示になるか確認してください。
チャレンジ3:LikeボタンのラベルをFavoriteに変えよう
次のコードを探してください。
label: 'Like',
これを次のように変更します。
label: 'Favorite',
ボタンの文字が変わるか確認してください。
チャレンジ4:My ListがONのときのアイコンを変更しよう
次のコードを探してください。
icon: isInMyList
? Icons.check_rounded
: Icons.add_rounded,
これを次のように変更します。
icon: isInMyList
? Icons.bookmark_rounded
: Icons.add_rounded,
My Listを押したとき、チェックではなくブックマークのアイコンに変わるか確認してください。
チャレンジの答え
チャレンジ1の答え
変更前:
bool isLiked = false;
変更後:
bool isLiked = true;
詳細画面を開いた直後から、LikeボタンがONになります。
チャレンジ2の答え
変更前:
bool isInMyList = false;
変更後:
bool isInMyList = true;
詳細画面を開いた直後から、My ListボタンがONになります。
チャレンジ3の答え
変更前:
label: 'Like',
変更後:
label: 'Favorite',
Likeボタンの表示文字が Favorite に変わります。
チャレンジ4の答え
変更前:
icon: isInMyList
? Icons.check_rounded
: Icons.add_rounded,
変更後:
icon: isInMyList
? Icons.bookmark_rounded
: Icons.add_rounded,
My ListがONのとき、チェックアイコンではなくブックマークアイコンになります。
この節のまとめ
この節では、setState を使って、LikeボタンとMy ListボタンのON/OFFを切り替える方法を学びました。
大切なポイントは次の通りです。
boolは、ON/OFFの状態を管理するときに便利。isLikedはLikeされているかどうかを表す。isInMyListはMy Listに追加されているかどうかを表す。setStateの中で状態を変えると、画面が再描画される。!を使うと、trueとfalseを反転できる。- 親Widgetで状態を持ち、子Widgetに状態と処理を渡すと管理しやすい。
- 三項演算子を使うと、状態に応じてアイコンを切り替えられる。
activeを使うと、選択中かどうかで色を変えられる。HitTestBehavior.opaqueを使うと、ボタンのタップ範囲を広げやすい。- 今回の状態は画面内だけで保存されるため、アプリを閉じると元に戻る。
- 本格的に保存したい場合は、端末内保存やデータベース保存へ発展させる。
次のステップ
次の節では、YouTube再生画面を作ります。
youtube_player_iframe を使って、作品データに登録した youtubeVideoId をもとに、アプリ内で予告編を再生する方法を学びます。
詳細画面の Play Trailer ボタンから再生画面へ移動する流れも、あわせて確認していきましょう。