【検索結果UI】該当作品の一覧と、見つからない時の表示を作る
この節で学ぶこと
前の節では、TextField に入力した文字を使って、作品をリアルタイム検索する方法を学びました。
今回の節では、その検索結果をどのように画面に表示するかを学びます。
検索機能では、文字を入力して作品を絞り込むだけでは不十分です。
検索結果があるときは、該当する作品を一覧で表示する必要があります。
検索結果がないときは、「見つかりませんでした」と分かりやすく表示する必要があります。
検索文字を入力する
↓
該当する作品を探す
↓
作品がある場合:一覧で表示する
↓
作品がない場合:見つからない表示を出す
この節では、ListView.separated、SearchResultTile、EmptySearchResult を使って、検索結果UIを作っていきます。
今回作るUIの全体像
検索結果画面は、大きく分けると2つの状態があります。
| 状態 | 表示するもの |
|---|---|
| 検索結果がある | 該当作品の一覧 |
| 検索結果がない | 見つからない時のメッセージ |
画面のイメージは、次のようになります。
検索結果がある場合
┌────────────────────┐
│ Search │
│ [検索欄] │
│ │
│ ┌────────────────┐ │
│ │画像 作品名 │ │
│ │ メタ情報 │ │
│ │ 説明文 │ │
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │画像 作品名 │ │
│ │ メタ情報 │ │
│ │ 説明文 │ │
│ └────────────────┘ │
└────────────────────┘
検索結果がない場合は、次のようにします。
検索結果がない場合
┌────────────────────┐
│ Search │
│ [検索欄] │
│ │
│ │
│ No titles found. │
│ │
└────────────────────┘
ユーザーにとって大切なのは、「検索できていない」のか「検索した結果0件だった」のかが分かることです。
そのため、0件のときも何かしらの表示を出します。
今回見るコード
検索結果UIで中心になるのは、次の3つです。
filteredMovies.isEmpty
ListView.separated
SearchResultTile
EmptySearchResult
Search画面の中では、次のように使っています。
Expanded(
child: filteredMovies.isEmpty
? const EmptySearchResult()
: ListView.separated(
itemCount: filteredMovies.length,
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
itemBuilder: (context, index) {
final movie = filteredMovies[index];
return SearchResultTile(movie: movie);
},
),
),
このコードでは、検索結果が空かどうかによって、表示するWidgetを切り替えています。
filteredMovies.isEmpty が true
↓
EmptySearchResult を表示
filteredMovies.isEmpty が false
↓
ListView.separated を表示
Expandedで検索結果エリアを広げる
検索結果部分は、Expanded で包まれています。
Expanded(
child: filteredMovies.isEmpty
? const EmptySearchResult()
: ListView.separated(
...
),
),
Expanded は、Column の中で残りの高さを使うためのWidgetです。
Search画面は、上から順に次のような構成です。
Searchタイトル
↓
検索欄
↓
検索結果エリア
タイトルと検索欄は、高さがだいたい決まっています。
その下の残りスペースを、検索結果一覧に使いたいので、Expanded を使います。
画面の残り高さ
↓
検索結果一覧に使う
もし Expanded を使わないと、リストの高さがうまく決まらず、レイアウトエラーが出ることがあります。
filteredMovies.isEmptyで0件かどうかを判定する
検索結果があるかどうかは、filteredMovies.isEmpty で確認します。
filteredMovies.isEmpty
isEmpty は、リストが空かどうかを調べるためのプロパティです。
| 検索結果 | filteredMovies.isEmpty |
|---|---|
| 0件 | true |
| 1件以上 | false |
たとえば、検索結果が0件なら、true になります。
filteredMovies = []
↓
filteredMovies.isEmpty は true
検索結果が2件なら、false になります。
filteredMovies = [作品A, 作品B]
↓
filteredMovies.isEmpty は false
この結果を使って、表示を切り替えます。
三項演算子で表示を切り替える
検索結果の表示切り替えには、三項演算子を使っています。
filteredMovies.isEmpty
? const EmptySearchResult()
: ListView.separated(
...
)
これは、次の意味です。
もし filteredMovies.isEmpty が true なら
EmptySearchResult を表示する
そうでなければ
ListView.separated を表示する
普通の言葉で書くと、こうです。
検索結果がないなら「見つかりませんでした」を表示。
検索結果があるなら、作品一覧を表示。
このように、条件によって表示するWidgetを変える場面では、三項演算子がよく使われます。
EmptySearchResultとは?
EmptySearchResult は、検索結果が0件だったときに表示するWidgetです。
class EmptySearchResult extends StatelessWidget {
const EmptySearchResult({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'No titles found.',
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.58),
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
);
}
}
検索結果がないとき、何も表示されないと、ユーザーは少し迷います。
何も表示されない
↓
通信エラー?
検索中?
該当なし?
そこで、No titles found. と表示します。
これにより、「検索したけれど該当作品がありません」と分かります。
Centerで中央に配置する
EmptySearchResult の中では、Center を使っています。
return Center(
child: Text(
'No titles found.',
...
),
);
Center は、子Widgetを中央に配置するWidgetです。
検索結果がない時のメッセージは、画面の中央に出すと分かりやすくなります。
画面中央
↓
No titles found.
画面の端に小さく出すよりも、状態が伝わりやすくなります。
見つからない時の文字を少し薄くする
EmptySearchResult の文字色は、真っ白ではなく、少し薄い白にしています。
color: NetflixColors.white.withValues(alpha: 0.58),
withValues(alpha: 0.58) は、透明度を調整する指定です。
真っ白だと強すぎるため、少し控えめにしています。
真っ白:強く目立つ
薄い白:補助メッセージらしく見える
検索結果がない時の表示は、重要ではありますが、警告のように強く見せる必要はありません。
そのため、少し薄い色にしています。
ListView.separatedで検索結果を一覧表示する
検索結果がある場合は、ListView.separated を使います。
ListView.separated(
itemCount: filteredMovies.length,
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
itemBuilder: (context, index) {
final movie = filteredMovies[index];
return SearchResultTile(movie: movie);
},
)
ListView.separated は、リストの項目と項目の間に区切りを入れやすいWidgetです。
今回のように、カードとカードの間に余白を入れたいときに便利です。
検索結果1
↓ 12px余白
検索結果2
↓ 12px余白
検索結果3
itemCountで表示件数を決める
itemCount には、検索結果の件数を指定しています。
itemCount: filteredMovies.length,
filteredMovies.length は、検索結果の数です。
たとえば、検索結果が3件なら、3件分のカードを作ります。
filteredMovies.length = 3
↓
SearchResultTileを3個作る
検索結果が10件なら、10個作ります。
filteredMovies.length = 10
↓
SearchResultTileを10個作る
このように、itemCount はリスト表示の件数を決める大事な値です。
separatorBuilderでカード同士の余白を作る
カードとカードの間には、separatorBuilder で余白を入れています。
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
ここでは、縦方向に12の余白を入れています。
カード
↓
12pxの余白
↓
カード
検索結果のカードがぴったりくっついていると、少し窮屈に見えます。
余白を入れることで、1つ1つの作品が見やすくなります。
itemBuilderで1件分の検索結果を作る
検索結果の1件分を作っているのが、itemBuilder です。
itemBuilder: (context, index) {
final movie = filteredMovies[index];
return SearchResultTile(movie: movie);
},
index には、何番目の検索結果かが入ります。
index = 0 → 1件目
index = 1 → 2件目
index = 2 → 3件目
その index を使って、filteredMovies から作品を取り出します。
final movie = filteredMovies[index];
そして、その作品を SearchResultTile に渡します。
return SearchResultTile(movie: movie);
SearchResultTileとは?
SearchResultTile は、検索結果1件分の見た目を作るWidgetです。
class SearchResultTile extends StatelessWidget {
const SearchResultTile({
super.key,
required this.movie,
});
final MovieItem movie;
movie を受け取り、その作品のポスター画像、タイトル、メタ情報、説明文を表示します。
SearchResultTile
↓
検索結果1件分のカード
検索結果の1件分を部品として切り出しておくと、Search画面本体のコードが読みやすくなります。
SearchResultTileの全体コード
SearchResultTile は、次のようなコードです。
class SearchResultTile extends StatelessWidget {
const SearchResultTile({
super.key,
required this.movie,
});
final MovieItem movie;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: movie),
),
);
},
child: Container(
decoration: BoxDecoration(
color: NetflixColors.darkGray,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(10),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
movie.posterUrl,
width: 78,
height: 112,
fit: BoxFit.cover,
),
),
const SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
movie.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 5),
Text(
'${movie.matchRate}% Match • ${movie.year} • ${movie.rating}',
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.72),
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
movie.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.62),
fontSize: 12.5,
height: 1.35,
),
),
],
),
),
],
),
),
);
}
}
ここからは、このコードを少しずつ分解して見ていきます。
GestureDetectorでカード全体をタップできるようにする
SearchResultTile は、GestureDetector で包まれています。
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: movie),
),
);
},
child: Container(
...
),
);
これにより、検索結果カード全体をタップできます。
検索結果カードをタップ
↓
MovieDetailPageへ移動
検索結果の画像だけ、またはタイトルだけをタップできるよりも、カード全体がタップできる方が操作しやすいです。
Navigatorで詳細画面へ移動する
カードを押したときは、Navigator.of(context).push を使って、詳細画面へ移動します。
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: movie),
),
);
ここで開いているのは、MovieDetailPage です。
MovieDetailPage(movie: movie)
検索結果で選んだ作品の movie を、そのまま詳細画面へ渡しています。
検索結果で作品をタップ
↓
MovieDetailPageにmovieを渡す
↓
その作品の詳細画面を表示する
これまでの教材で学んだ「画面遷移」と「データ渡し」が、ここでも使われています。
Containerで検索結果カードを作る
カードの背景は、Container で作っています。
Container(
decoration: BoxDecoration(
color: NetflixColors.darkGray,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(10),
child: Row(
...
),
)
Container は、背景色、余白、サイズなどを指定できる便利なWidgetです。
ここでは、検索結果1件分をカードのように見せるために使っています。
BoxDecorationで背景色と角丸を指定する
カードの見た目は、BoxDecoration で作っています。
decoration: BoxDecoration(
color: NetflixColors.darkGray,
borderRadius: BorderRadius.circular(10),
),
背景色は、NetflixColors.darkGray です。
color: NetflixColors.darkGray,
黒背景の画面の上に、少し明るいカードを置くことで、検索結果が見やすくなります。
角は少し丸めています。
borderRadius: BorderRadius.circular(10),
少し角を丸めることで、カードUIらしい見た目になります。
paddingでカード内に余白を作る
カードの内側には、余白を入れています。
padding: const EdgeInsets.all(10),
この余白がないと、画像や文字がカードの端にくっついてしまいます。
余白なし
↓
窮屈に見える
余白あり
↓
読みやすいカードになる
カードUIを作るときは、外側の余白だけでなく、内側の余白も大切です。
Rowで画像と文字を横に並べる
カードの中身は、Row で横並びにしています。
child: Row(
children: [
ClipRRect(
...
),
const SizedBox(width: 13),
Expanded(
child: Column(
...
),
),
],
),
構造は次の通りです。
左:ポスター画像
右:作品タイトル・メタ情報・説明文
検索結果では、一覧性が大切です。
ポスター画像と作品情報を横に並べることで、ユーザーが素早く作品を確認できます。
ClipRRectで画像を角丸にする
ポスター画像は、ClipRRect で包んでいます。
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
movie.posterUrl,
width: 78,
height: 112,
fit: BoxFit.cover,
),
),
ClipRRect は、子Widgetを角丸に切り抜くためのWidgetです。
ここでは、ポスター画像の角を少し丸めています。
borderRadius: BorderRadius.circular(6),
カード本体も角丸なので、画像も少し丸めると見た目がそろいます。
Image.networkでポスター画像を表示する
ポスター画像は、Image.network で表示しています。
Image.network(
movie.posterUrl,
width: 78,
height: 112,
fit: BoxFit.cover,
)
movie.posterUrl には、作品ごとのポスター画像URLが入っています。
movie.posterUrl
幅は78、高さは112です。
width: 78,
height: 112,
検索結果の一覧では、画像が大きすぎると1画面に表示できる件数が少なくなります。
逆に小さすぎると、どの作品か分かりにくくなります。
そのため、検索結果ではこのくらいの小さめのポスターサイズにしています。
BoxFit.coverで画像をきれいに収める
ポスター画像には、次の指定があります。
fit: BoxFit.cover,
BoxFit.cover は、指定した枠いっぱいに画像を表示する指定です。
画像の比率が少し違っても、余白が出ないように表示できます。
枠いっぱいに画像を表示
↓
はみ出した部分は自然に切り取る
↓
カード内で整って見える
ここまでのまとめ
ここまでで、検索結果がある場合とない場合の表示切り替え、そして検索結果カードの基本構造を確認しました。
大切なポイントは次の通りです。
- 検索結果エリアは
Expandedで残りの高さを使う。 filteredMovies.isEmptyで検索結果が0件かどうかを判定する。- 検索結果が0件なら
EmptySearchResultを表示する。 - 検索結果があるなら
ListView.separatedを表示する。 ListView.separatedは、項目間に余白を入れやすい。itemCountには検索結果の件数を指定する。itemBuilderで検索結果1件分を作る。SearchResultTileは、検索結果1件分のカードUI。GestureDetectorでカード全体をタップできるようにする。- カードをタップすると
MovieDetailPage(movie: movie)へ移動する。 ContainerとBoxDecorationでカードの背景色と角丸を作る。Rowでポスター画像と作品情報を横に並べる。ClipRRectでポスター画像を角丸にする。Image.networkとBoxFit.coverで画像をカード内にきれいに表示する。
Expandedで文字エリアを広げる
ここからは、SearchResultTile の右側にある文字エリアを見ていきます。
検索結果カードの中では、左にポスター画像、右に作品情報を置いていました。
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
movie.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 5),
Text(
'${movie.matchRate}% Match • ${movie.year} • ${movie.rating}',
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.72),
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
movie.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.62),
fontSize: 12.5,
height: 1.35,
),
),
],
),
),
Expanded を使うことで、画像の右側に残っている横幅を、文字エリアとして使えます。
左:画像の固定幅
右:残りの横幅を文字エリアにする
検索結果では、タイトルや説明文が長くなることがあります。
そのため、文字エリアにはできるだけ余裕を持たせます。
Columnで作品情報を縦に並べる
右側の文字エリアでは、Column を使っています。
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(movie.title),
const SizedBox(height: 5),
Text('${movie.matchRate}% Match • ${movie.year} • ${movie.rating}'),
const SizedBox(height: 8),
Text(movie.description),
],
)
並びは次の通りです。
作品タイトル
↓
マッチ率・公開年・年齢制限
↓
説明文
検索結果カードでは、ユーザーが作品をすばやく判断できることが大切です。
そのため、まずタイトルを見せ、その下に補足情報、最後に説明文を表示しています。
crossAxisAlignmentで左揃えにする
Column には、次の指定があります。
crossAxisAlignment: CrossAxisAlignment.start,
これは、Column の中身を左揃えにする指定です。
左揃え
タイトル
メタ情報
説明文
検索結果のような情報リストでは、左端がそろっている方が読みやすくなります。
中央揃えにすると、各行の開始位置がバラバラに見えて、少し読みにくくなります。
タイトルを1行で表示する
作品タイトルは、次のコードで表示しています。
Text(
movie.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
movie.title には、作品名が入っています。
movie.title
検索結果では、カードの高さをそろえたいので、タイトルは1行だけにしています。
maxLines: 1,
TextOverflow.ellipsisで省略する
タイトルが長い場合は、入りきらない部分を ... で省略します。
overflow: TextOverflow.ellipsis,
たとえば、タイトルが長くて1行に入りきらない場合は、次のようになります。
The Very Long Movie Title...
これを指定しないと、文字がはみ出したり、レイアウトが崩れたりすることがあります。
検索結果カードのように、横幅が限られているUIでは、TextOverflow.ellipsis がとても便利です。
タイトルを強く見せる
タイトルのスタイルは、白く、少し太くしています。
style: const TextStyle(
color: NetflixColors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
タイトルは検索結果の中で一番重要な情報です。
そのため、他のテキストよりも強く表示します。
| 情報 | 見せ方 |
|---|---|
| タイトル | 白・太字 |
| メタ情報 | 少し薄い白 |
| 説明文 | さらに控えめな白 |
このように強弱をつけることで、ユーザーは情報を読み取りやすくなります。
メタ情報を1行で表示する
タイトルの下には、メタ情報を表示しています。
Text(
'${movie.matchRate}% Match • ${movie.year} • ${movie.rating}',
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.72),
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
ここでは、次の3つを表示しています。
マッチ率
公開年
年齢制限
たとえば、次のような表示になります。
98% Match • 2021 • 18+
検索結果のカードでは、情報を詰め込みすぎないことが大切です。
カテゴリやシーズン情報まで入れると、1行が長くなりすぎる場合があります。
文字列の中に変数を入れる
メタ情報では、${} を使って文字列の中に変数を入れています。
'${movie.matchRate}% Match • ${movie.year} • ${movie.rating}'
これは、Dartの文字列補間です。
たとえば、次の値が入っているとします。
movie.matchRate = 98
movie.year = 2021
movie.rating = 18+
この場合、画面には次のように表示されます。
98% Match • 2021 • 18+
データの値を文字列の中に自然に組み込めるので、アプリ開発ではよく使います。
で情報を区切る
メタ情報では、• という記号で情報を区切っています。
'${movie.matchRate}% Match • ${movie.year} • ${movie.rating}'
これは、文字情報をコンパクトに見せるための区切り記号です。
98% Match • 2021 • 18+
カンマやスラッシュでもよいですが、動画アプリ風のUIでは • を使うと落ち着いた見た目になります。
メタ情報を少し薄くする
メタ情報の色は、真っ白ではありません。
color: NetflixColors.white.withValues(alpha: 0.72),
タイトルよりも少し薄くしています。
タイトル:強く表示
メタ情報:少し控えめ
説明文:さらに控えめ
このように階層をつけることで、ユーザーは自然にタイトルから読み始められます。
説明文を表示する
メタ情報の下には、作品説明文を表示しています。
Text(
movie.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: NetflixColors.white.withValues(alpha: 0.62),
fontSize: 12.5,
height: 1.35,
),
),
movie.description には、作品の説明文が入っています。
検索結果一覧では、説明文をすべて表示すると長くなりすぎます。
そのため、最大3行にしています。
maxLines: 3,
説明文も省略表示にする
説明文にも、TextOverflow.ellipsis を指定しています。
overflow: TextOverflow.ellipsis,
説明文が3行に収まらない場合は、最後を ... で省略します。
Hundreds of cash-strapped players accept a strange invitation...
検索結果では、すべてを読ませるのではなく、「気になったら詳細画面へ進む」ことを想定します。
そのため、検索結果カードでは説明文を短く見せる方が使いやすくなります。
heightで行間を調整する
説明文のスタイルには、height があります。
height: 1.35,
これは、行間を調整する指定です。
説明文は複数行になることがあるので、行間が詰まりすぎると読みにくくなります。
行間が狭い
↓
文字が詰まって見える
行間を少し広げる
↓
読みやすくなる
検索結果カードではスペースが限られているため、広げすぎず、1.35 くらいにしています。
検索結果UIの流れを整理する
ここまでの流れを、もう一度整理します。
filteredMoviesを作る
↓
filteredMovies.isEmptyを確認
↓
0件ならEmptySearchResult
↓
1件以上ならListView.separated
↓
itemBuilderでSearchResultTileを作る
↓
SearchResultTileにmovieを渡す
↓
左にポスター画像
↓
右にタイトル・メタ情報・説明文
↓
カードを押すと詳細画面へ移動
検索結果UIでは、ただリストを出すだけではなく、0件の時の表示、カードの余白、文字の省略、詳細画面への導線まで考える必要があります。
まずカスタマイズしてみよう
まずは、検索結果が見つからない時の文言を日本語にしてみましょう。
次のコードを探してください。
'No titles found.'
これを次のように変更します。
'該当する作品が見つかりませんでした。'
日本語のアプリとして見せたい場合は、この方が自然です。
検索結果カードの余白を広げてみよう
カード同士の間隔は、separatorBuilder で決めています。
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
もっとゆったり見せたい場合は、次のように変更します。
separatorBuilder: (context, index) {
return const SizedBox(height: 18);
},
カード間の余白が広がり、見た目にゆとりが出ます。
ポスター画像を少し大きくしてみよう
ポスター画像のサイズは、次のコードで決めています。
width: 78,
height: 112,
少し大きくしたい場合は、次のようにできます。
width: 88,
height: 126,
ただし、画像を大きくすると、1画面に表示できる検索結果の数は少なくなります。
一覧性を優先するか、ビジュアルの迫力を優先するかで調整しましょう。
説明文を2行にしてコンパクトにする
検索結果カードをもっとコンパクトにしたい場合は、説明文を2行にします。
maxLines: 3,
これを次のように変更します。
maxLines: 2,
カードの高さが少し低くなり、一覧として見やすくなります。
よくあるつまずきポイント
Q. 検索結果が0件の時、何も表示されません。
filteredMovies.isEmpty の時に、EmptySearchResult を表示しているか確認してください。
filteredMovies.isEmpty
? const EmptySearchResult()
: ListView.separated(
...
)
0件の時に何も表示しないと、ユーザーには状態が伝わりません。
Q. ListView が表示されず、レイアウトエラーになります。
Column の中で ListView を使う場合は、Expanded で包むことが多いです。
Expanded(
child: ListView.separated(
...
),
)
ListView は縦方向に広がろうとするため、親から高さを決めてもらう必要があります。
Expanded を使うことで、残りの高さをリストに与えられます。
Q. タイトルが長くて右にはみ出します。
Text に次の指定があるか確認してください。
maxLines: 1,
overflow: TextOverflow.ellipsis,
これにより、長いタイトルは1行で省略されます。
Q. 説明文が長すぎてカードが大きくなります。
説明文に、次の指定があるか確認してください。
maxLines: 3,
overflow: TextOverflow.ellipsis,
さらにコンパクトにしたい場合は、maxLines: 2 に変更してもよいです。
Q. 検索結果カードを押しても詳細画面に移動しません。
SearchResultTile が GestureDetector で包まれているか確認してください。
GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: movie),
),
);
},
child: Container(
...
),
)
MovieDetailPage(movie: movie) に、選んだ作品データを渡すことも忘れないようにしましょう。
チャレンジ
チャレンジ1:見つからない時の文言を日本語にしよう
次のコードを探してください。
'No titles found.'
これを次のように変更します。
'該当する作品が見つかりませんでした。'
検索結果が0件のとき、日本語のメッセージが表示されるか確認してください。
チャレンジ2:検索結果カードの間隔を広げよう
次のコードを探してください。
return const SizedBox(height: 12);
これを次のように変更します。
return const SizedBox(height: 18);
カード同士の間に余白が増えるか確認してください。
チャレンジ3:ポスター画像を少し大きくしよう
次のコードを探してください。
width: 78,
height: 112,
これを次のように変更します。
width: 88,
height: 126,
検索結果カードのポスター画像が少し大きくなるか確認してください。
チャレンジ4:説明文を2行までにしよう
説明文の Text にある次のコードを探してください。
maxLines: 3,
これを次のように変更します。
maxLines: 2,
検索結果カードが少しコンパクトになるか確認してください。
チャレンジの答え
チャレンジ1の答え
変更前:
'No titles found.'
変更後:
'該当する作品が見つかりませんでした。'
0件表示が日本語になります。
チャレンジ2の答え
変更前:
return const SizedBox(height: 12);
変更後:
return const SizedBox(height: 18);
検索結果カード同士の間隔が広がります。
チャレンジ3の答え
変更前:
width: 78,
height: 112,
変更後:
width: 88,
height: 126,
ポスター画像が少し大きくなります。
チャレンジ4の答え
変更前:
maxLines: 3,
変更後:
maxLines: 2,
説明文が最大2行になり、検索結果カードがコンパクトになります。
この節のまとめ
この節では、検索結果がある場合の一覧表示と、見つからない時の表示を作りました。
大切なポイントは次の通りです。
- 検索結果UIでは、結果がある場合とない場合の両方を考える。
filteredMovies.isEmptyを使うと、検索結果が0件かどうかを判定できる。- 0件の時は
EmptySearchResultを表示する。 EmptySearchResultを用意すると、ユーザーに「該当なし」が伝わりやすい。Centerを使うと、見つからない時のメッセージを画面中央に表示できる。ListView.separatedを使うと、検索結果カードの間に余白を入れられる。itemCountには検索結果の件数を指定する。itemBuilderで検索結果1件分のWidgetを作る。SearchResultTileによって、検索結果1件分を部品化できる。GestureDetectorでカード全体をタップできるようにする。Navigator.of(context).pushで詳細画面へ移動できる。MovieDetailPage(movie: movie)に作品データを渡すことで、選んだ作品の詳細を表示できる。ContainerとBoxDecorationでカードの背景色と角丸を作れる。Rowで画像と文字を横に並べられる。ClipRRectで画像を角丸にできる。TextOverflow.ellipsisを使うと、長いタイトルや説明文を省略できる。maxLinesを使うと、表示する行数を制限できる。
次のステップ
次の節では、My Netflix画面を作ります。
プロフィール画像、通知、ダウンロード、My Listなどをまとめて表示し、アカウント画面らしいUIを作ります。
検索画面まで作ると、作品を「探す」導線ができます。
次は、ユーザー自身の情報や保存した作品を見せる画面を作って、アプリ全体の完成度を高めていきましょう。
