Flutterアプリケーション開発概論

【ポスターカードUI】作品画像をタップして詳細画面へ移動する

この節で学ぶこと

前の節では、Home画面に PopularTrendingTop10 のような横スクロール作品一覧を作る方法を学びました。

今回の節では、その横スクロールの中に表示されている ポスターカード を詳しく見ていきます。

ポスターカードとは、作品の縦長画像が表示されているカードのことです。

このカードをタップすると、作品の詳細画面に移動します。

今回学ぶ流れは、次の通りです。

作品データをカードに渡す
↓
ポスター画像を表示する
↓
カードを角丸にする
↓
Top10用のランキング番号を重ねる
↓
GestureDetectorでタップできるようにする
↓
Navigator.pushで詳細画面へ移動する

この節では、ただ画像を表示するだけではなく、タップした作品の情報を次の画面へ渡す ところまで学びます。

ここが分かると、一覧画面から詳細画面へ移動するアプリらしい動きが作れるようになります。


今回見るコード

ポスターカードは、MoviePosterCard というクラスで作っています。

class MoviePosterCard extends StatelessWidget {
  const MoviePosterCard({
    super.key,
    required this.movie,
    required this.width,
    required this.height,
    this.rank,
  });

  final MovieItem movie;
  final double width;
  final double height;
  final int? rank;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.of(context).push(
          MaterialPageRoute<void>(
            builder: (context) => MovieDetailPage(movie: movie),
          ),
        );
      },
      child: SizedBox(
        width: rank == null ? width : width + 18,
        height: height,
        child: Stack(
          children: [
            Positioned(
              right: 0,
              top: 0,
              bottom: 0,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(6),
                child: Image.network(
                  movie.posterUrl,
                  width: width,
                  height: height,
                  fit: BoxFit.cover,
                ),
              ),
            ),
            if (rank != null)
              Positioned(
                left: 0,
                bottom: -9,
                child: Text(
                  '$rank',
                  style: const TextStyle(
                    color: Colors.black,
                    fontSize: 84,
                    fontWeight: FontWeight.w900,
                    height: 1,
                    shadows: [
                      Shadow(
                        color: Colors.white,
                        blurRadius: 1.6,
                        offset: Offset(1.1, 1.1),
                      ),
                      Shadow(
                        color: Colors.white,
                        blurRadius: 1.6,
                        offset: Offset(-1.1, -1.1),
                      ),
                    ],
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

少し長く見えますが、分けて見ると分かりやすいです。

1. 作品データを受け取る
2. カードの大きさを受け取る
3. 必要ならランキング番号を受け取る
4. 画像を表示する
5. ランキング番号を重ねる
6. タップしたら詳細画面へ移動する

順番に見ていきましょう。


MoviePosterCardの役割

MoviePosterCard は、作品1つ分のポスター画像を表示するためのWidgetです。

Home画面の横スクロール一覧では、この MoviePosterCard が何枚も並んでいます。

[ポスターカード] [ポスターカード] [ポスターカード] [ポスターカード]

1枚のカードには、次のような役割があります。

役割内容
画像を表示するmovie.posterUrl を使ってポスター画像を表示する
サイズを決めるwidthheight でカードの大きさを決める
ランキング番号を出すrank があるときだけ番号を重ねる
タップできるようにするGestureDetector でタップを受け取る
詳細画面へ移動するNavigator.pushMovieDetailPage を開く

つまり、MoviePosterCard は、見た目と動きをセットで持った部品です。


受け取っている値を確認しよう

MoviePosterCard は、次の4つの値を受け取っています。

const MoviePosterCard({
  super.key,
  required this.movie,
  required this.width,
  required this.height,
  this.rank,
});

それぞれの役割は次の通りです。

役割
movieMovieItem表示する作品データ
widthdoubleカードの横幅
heightdoubleカードの高さ
rankint?Top10用のランキング番号

moviewidthheight には required がついています。

required this.movie,
required this.width,
required this.height,

つまり、この3つは必ず渡す必要があります。

一方で、rank には required がついていません。

this.rank,

これは、ランキング番号が必要な場合だけ渡せばよい、という意味です。


movieには何が入っている?

movie には、MovieItem が入ります。

final MovieItem movie;

MovieItem は、作品情報をひとまとまりにしたクラスです。

たとえば、次のような情報を持っています。

MovieItem(
  title: 'Squid Game',
  posterUrl:
      'https://image.tmdb.org/t/p/w500/dDlEmu3EZ0Pgg93K2SVNLCjCSvE.jpg',
  backdropUrl:
      'https://image.tmdb.org/t/p/w1280/oaGvjB0DvdhXhOAuADfHb261ZHa.jpg',
  youtubeVideoId: 'oqxAJKy0ii4',
  year: '2021',
  rating: '18+',
  seasonLabel: 'Season 1',
  category: 'TV Drama',
  matchRate: 98,
  homeCategory: HomeCategory.shows,
),

ポスターカードでは、この中の posterUrl を使って画像を表示します。

movie.posterUrl

そして、カードをタップしたときには、この movie をそのまま詳細画面へ渡します。


widthとheightでカードサイズを決める

カードの大きさは、widthheight で決まります。

final double width;
final double height;

double は、小数も扱える数字の型です。

たとえば、横スクロール一覧では、次のように渡しています。

MoviePosterCard(
  movie: movie,
  width: itemWidth,
  height: itemHeight,
  rank: showRank ? index + 1 : null,
);

itemWidthitemHeight は、ContentRow 側で決めています。

final itemWidth = large ? 132.0 : 116.0;
final itemHeight = large ? 190.0 : 164.0;

つまり、通常の一覧では小さめ、Top10風の一覧では少し大きめのカードになります。


rankはなぜint?なのか

rank は次のように定義されています。

final int? rank;

int は整数です。int? は、「整数が入ることもあるし、null になることもある」という意味です。

今回のポスターカードでは、ランキング番号が必要な場合と不要な場合があります。

通常の作品一覧では、ランキング番号はいりません。

Popular on NETAFLIX
[作品画像] [作品画像] [作品画像]

Top10風の一覧では、ランキング番号を表示します。

Top 10 TV Shows
1 [作品画像]  2 [作品画像]  3 [作品画像]

そのため、rank はあってもなくてもよい値として、int? にしています。


rankがnullかどうかで見た目を変える

カードの横幅は、rank があるかどうかで変えています。

width: rank == null ? width : width + 18,

これは、次の意味です。

rankがnullなら、通常のwidthを使う
rankがnullでないなら、width + 18にする

ランキング番号を表示するときは、画像の左側に大きな数字を重ねます。

その分、少し横幅に余裕が必要です。

だから、rank があるときだけ width + 18 にしています。


GestureDetectorでカードをタップできるようにする

ポスターカード全体は、GestureDetector で包まれています。

return GestureDetector(
  onTap: () {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (context) => MovieDetailPage(movie: movie),
      ),
    );
  },
  child: SizedBox(
    ...
  ),
);

GestureDetector は、タップやスワイプなどの操作を受け取るためのWidgetです。

今回は、カードをタップしたときに詳細画面へ移動したいので、onTap を使っています。

onTap: () {
  ...
},

これで、ポスター画像をタップできるようになります。


onTapの中で何をしている?

onTap の中では、次の処理をしています。

Navigator.of(context).push(
  MaterialPageRoute<void>(
    builder: (context) => MovieDetailPage(movie: movie),
  ),
);

これは、新しい画面へ移動するためのコードです。

ここでは、MovieDetailPage を開いています。

しかも、ただ詳細画面を開くだけではありません。

MovieDetailPage(movie: movie)

と書いているので、タップした作品データを詳細画面へ渡しています。

つまり、カードごとに違う作品情報を詳細画面へ送っています。


Navigator.of(context).push は、新しい画面へ移動するときに使います。

Navigator.of(context).push(
  MaterialPageRoute<void>(
    builder: (context) => MovieDetailPage(movie: movie),
  ),
);

イメージとしては、今の画面の上に新しい画面を重ねるような動きです。

Home画面
↓
ポスターカードをタップ
↓
MovieDetailPageが上に表示される

戻るボタンを押すと、前のHome画面に戻れます。

Flutterでは、画面移動の基本としてよく使う書き方です。


MaterialPageRouteとは?

MaterialPageRoute は、Flutterで新しい画面を開くためのルートです。

MaterialPageRoute<void>(
  builder: (context) => MovieDetailPage(movie: movie),
),

builder の中で、次に表示したい画面を返しています。

今回の場合は、MovieDetailPage です。

builder: (context) => MovieDetailPage(movie: movie),

このように書くことで、タップしたときに作品詳細画面へ移動できます。


詳細画面にmovieを渡す理由

詳細画面では、作品ごとに違う情報を表示します。

たとえば、Squid Game のカードを押した場合は、Squid Game のタイトルや説明文を表示したいです。

Wednesday のカードを押した場合は、Wednesday の情報を表示したいです。

そのため、カードをタップしたときに、どの作品が押されたのかを詳細画面に渡す必要があります。

MovieDetailPage(movie: movie)

このように渡しているので、詳細画面側では次のように作品情報を使えます。

widget.movie.title
widget.movie.description
widget.movie.backdropUrl

一覧画面から詳細画面へデータを渡す流れは、アプリ開発でとても大切です。

ポスター画像を表示する

後半では、ポスターカードの見た目を詳しく見ていきます。

まず、作品画像を表示している部分です。

Image.network(
  movie.posterUrl,
  width: width,
  height: height,
  fit: BoxFit.cover,
),

Image.network は、インターネット上の画像URLを読み込んで表示するWidgetです。

今回のコードでは、movie.posterUrl を使っています。

movie.posterUrl

これは、MovieItem の中に入っているポスター画像のURLです。

たとえば、作品データ側では次のように設定されています。

posterUrl:
    'https://image.tmdb.org/t/p/w500/dDlEmu3EZ0Pgg93K2SVNLCjCSvE.jpg',

このURLの画像が、ポスターカードとして表示されます。


posterUrlとbackdropUrlの違い

作品データには、画像URLが2種類あります。

posterUrl
backdropUrl

それぞれの役割は違います。

項目使う場所画像の特徴
posterUrl横スクロールの作品カード縦長のポスター画像
backdropUrlヒーロー画面・詳細画面の背景横長の背景画像

今回のポスターカードでは、縦長の画像を使いたいので、posterUrl を使っています。

もしここで backdropUrl を使うと、横長画像を縦長カードに無理やり入れることになり、見た目が崩れやすくなります。

Image.network(
  movie.posterUrl,
  ...
)

ポスターカードには、基本的に posterUrl を使うと覚えておきましょう。


widthとheightを画像にも指定する

画像には、カードと同じ幅と高さを指定しています。

width: width,
height: height,

これにより、画像がカードサイズに合わせて表示されます。

たとえば、通常カードなら次のようなサイズになります。

幅:116
高さ:164

Top10風カードなら、次のようなサイズになります。

幅:132
高さ:190

外側の SizedBox だけでなく、画像にもサイズを指定することで、カードの大きさが安定します。


BoxFit.coverで画像をきれいに収める

画像には、次の指定があります。

fit: BoxFit.cover,

BoxFit.cover は、画像を枠いっぱいに表示する指定です。

枠からはみ出る部分は少し切り取られますが、空白が出にくくなります。

画像を枠いっぱいに広げる
↓
はみ出る部分は自然に切り取る
↓
カードいっぱいに画像が表示される

ポスターカードのように、決まったサイズの中に画像をきれいに入れたいときによく使います。


BoxFit.coverを使わないとどうなる?

もし fit: BoxFit.cover を指定しない場合、画像の縦横比によっては、カードの中に余白ができることがあります。

たとえば、画像が少し横長だった場合、縦長カードの中に入れると上下または左右に余白が出ることがあります。

余白が出る
↓
カードとしての一体感が弱くなる
↓
動画アプリ風の見た目から少し離れる

そのため、作品カードでは BoxFit.cover を使って、画像をしっかりカード全体に表示しています。


ClipRRectで画像を角丸にする

ポスター画像は、ClipRRect で包まれています。

ClipRRect(
  borderRadius: BorderRadius.circular(6),
  child: Image.network(
    movie.posterUrl,
    width: width,
    height: height,
    fit: BoxFit.cover,
  ),
),

ClipRRect は、中に入っているWidgetを角丸に切り抜くためのWidgetです。

ここでは、ポスター画像の角を少し丸くしています。

borderRadius: BorderRadius.circular(6),

これにより、画像が四角すぎず、カードらしい見た目になります。


角丸の大きさを調整する

角丸の大きさは、次の数字で変えられます。

BorderRadius.circular(6)

数字を大きくすると、角がより丸くなります。

BorderRadius.circular(14)

逆に、数字を小さくすると、角ばった見た目になります。

BorderRadius.circular(2)

動画アプリ風のUIでは、角丸を大きくしすぎると少し可愛らしい印象になります。

今回のように、落ち着いた黒背景の動画アプリにしたい場合は、6 くらいの控えめな角丸が合いやすいです。


Stackで画像とランキング番号を重ねる

ポスターカードの中では、Stack を使っています。

child: Stack(
  children: [
    Positioned(
      right: 0,
      top: 0,
      bottom: 0,
      child: ClipRRect(
        ...
      ),
    ),
    if (rank != null)
      Positioned(
        left: 0,
        bottom: -9,
        child: Text(
          '$rank',
          ...
        ),
      ),
  ],
),

Stack は、Widget同士を重ねるためのWidgetです。

今回のカードでは、次の2つを重ねています。

ポスター画像
ランキング番号

Top10風の行では、大きな数字を画像の左側に重ねることで、ランキングらしい見た目にしています。


Positionedで画像の位置を決める

ポスター画像は、Positioned で位置を指定しています。

Positioned(
  right: 0,
  top: 0,
  bottom: 0,
  child: ClipRRect(
    ...
  ),
),

ここでは、画像を右側に寄せています。

right: 0

さらに、上と下を 0 にしています。

top: 0,
bottom: 0,

これにより、画像がカードの右側で上下いっぱいに配置されます。

ランキング番号がない通常カードでも、この構造を使っています。


なぜ画像をright: 0にしているのか

通常のカードだけなら、画像を中央に置けば十分です。

しかし、この MoviePosterCard はTop10風のカードにも使います。

Top10風では、左側に大きなランキング番号を置き、右側にポスター画像を置きます。

1 [ポスター画像]

そのため、画像を右側に寄せる設計にしています。

right: 0

そして、ランキング番号があるときだけ、カード全体の幅を少し広げています。

width: rank == null ? width : width + 18,

これにより、通常カードにもTop10カードにも同じ MoviePosterCard を使えるようになっています。


if文でランキング番号を出し分ける

ランキング番号は、rank があるときだけ表示しています。

if (rank != null)
  Positioned(
    left: 0,
    bottom: -9,
    child: Text(
      '$rank',
      ...
    ),
  ),

Dartでは、Widgetリストの中で if を使うことができます。

これにより、条件に合うときだけWidgetを追加できます。

今回の場合は、こういう意味です。

rankがある
↓
ランキング番号を表示する

rankがnull
↓
ランキング番号を表示しない

通常のPopularやTrendingでは、ranknull です。

Top10風の行では、rank123 のような数字が入ります。


ランキング番号の位置を決める

ランキング番号も、Positioned で位置を決めています。

Positioned(
  left: 0,
  bottom: -9,
  child: Text(
    '$rank',
    ...
  ),
),

左位置は 0 です。

left: 0

下位置は -9 です。

bottom: -9

bottom にマイナスの値を入れると、少し下にはみ出すように配置されます。

これにより、大きなランキング番号がカードの下に少し食い込むような、迫力のある見た目になります。


Textでランキング番号を表示する

ランキング番号は、Text で表示しています。

Text(
  '$rank',
  style: const TextStyle(
    color: Colors.black,
    fontSize: 84,
    fontWeight: FontWeight.w900,
    height: 1,
    shadows: [
      Shadow(
        color: Colors.white,
        blurRadius: 1.6,
        offset: Offset(1.1, 1.1),
      ),
      Shadow(
        color: Colors.white,
        blurRadius: 1.6,
        offset: Offset(-1.1, -1.1),
      ),
    ],
  ),
),

'$rank' は、数字を文字として表示するための書き方です。

たとえば、rank1 の場合、画面には 1 と表示されます。

'$rank'

ランキング番号のデザイン

ランキング番号の見た目は、TextStyle で決めています。

color: Colors.black,
fontSize: 84,
fontWeight: FontWeight.w900,
height: 1,

それぞれの意味は次の通りです。

指定意味
color: Colors.black数字を黒にする
fontSize: 84数字を大きくする
fontWeight: FontWeight.w900かなり太くする
height: 1行の高さを詰める

Top10風の数字は、かなり大きく、太く表示しています。

作品カードよりも目立つくらいの数字にすることで、「ランキング」の雰囲気が出ます。


shadowsで数字に白い縁取りをつける

ランキング番号には、白い影をつけています。

shadows: [
  Shadow(
    color: Colors.white,
    blurRadius: 1.6,
    offset: Offset(1.1, 1.1),
  ),
  Shadow(
    color: Colors.white,
    blurRadius: 1.6,
    offset: Offset(-1.1, -1.1),
  ),
],

黒い数字だけだと、黒背景の上では見えにくくなります。

そこで、白い影を少しずらして重ねています。

黒い数字
+
白い影
↓
輪郭が見えやすくなる

このように、黒背景の上で黒文字を使いたい場合は、影や縁取りを入れると読みやすくなります。


なぜ影を2つ使っているのか

Shadow は2つ入っています。

Shadow(
  color: Colors.white,
  blurRadius: 1.6,
  offset: Offset(1.1, 1.1),
),
Shadow(
  color: Colors.white,
  blurRadius: 1.6,
  offset: Offset(-1.1, -1.1),
),

1つ目は右下方向に白い影を出しています。

offset: Offset(1.1, 1.1)

2つ目は左上方向に白い影を出しています。

offset: Offset(-1.1, -1.1)

これにより、数字の片側だけでなく、両側にうっすら白い輪郭が出ます。

本格的な縁取りではありませんが、簡単に数字を読みやすくする工夫です。


ポスターカードの全体構造を整理しよう

MoviePosterCard の構造を整理すると、こうなります。

MoviePosterCard
└── GestureDetector
    └── SizedBox
        └── Stack
            ├── Positioned
            │   └── ClipRRect
            │       └── Image.network
            └── if rank != null
                └── Positioned
                    └── Text

このように、画像を表示するだけでなく、タップ処理、サイズ指定、重ね合わせ、条件分岐が組み合わさっています。

ポスターカードは小さな部品ですが、Flutterアプリの基本がたくさん詰まっています。


まずカスタマイズしてみよう

今回は、ポスターカードの角丸を少し大きくしてみましょう。

次のコードを探してください。

borderRadius: BorderRadius.circular(6),

これを次のように変更します。

borderRadius: BorderRadius.circular(14),

保存して、Home画面を確認してください。

ポスターカードの角が少し丸くなります。

見た目が柔らかくなりますが、動画アプリ風のシャープさは少し弱くなるかもしれません。


ランキング番号を小さくしてみよう

Top10風の番号が大きすぎると感じる場合は、次のコードを変更します。

fontSize: 84,

少し小さくするなら、次のようにします。

fontSize: 72,

保存して、Top10風の作品一覧を確認してください。

数字が少し控えめになります。


タップ時に動いた感じを出すには?

今の MoviePosterCard は、タップするとすぐに詳細画面へ移動します。

もっと「押した感じ」を出したい場合は、GestureDetector の代わりに InkWell を使う方法もあります。

ただし、InkWell の波紋効果をきれいに出すには、Material と組み合わせる必要があります。

教材のこの段階では、まず GestureDetector でシンプルにタップ処理を理解するのが分かりやすいです。

まずはGestureDetectorでタップ処理を理解する
↓
慣れてきたらInkWellやアニメーションに挑戦する

よくあるつまずきポイント

Q. ポスター画像が表示されません。

まず、posterUrl が正しいか確認してください。

movie.posterUrl

URLが間違っていたり、画像が削除されていたりすると表示されません。

また、ネットワーク画像なので、通信環境によって表示に時間がかかる場合もあります。

Q. 画像が変な比率で表示されます。

次の指定が入っているか確認してください。

fit: BoxFit.cover,

BoxFit.cover を使うと、画像がカードいっぱいに表示されます。

Q. カードを押しても詳細画面に移動しません。

GestureDetectoronTap が入っているか確認してください。

onTap: () {
  Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (context) => MovieDetailPage(movie: movie),
    ),
  );
},

また、MovieDetailPage が正しく定義されているかも確認しましょう。

Q. 詳細画面に違う作品が表示されます。

MoviePosterCard に渡している movie が正しいか確認してください。

final movie = items[index];

return MoviePosterCard(
  movie: movie,
  width: itemWidth,
  height: itemHeight,
  rank: showRank ? index + 1 : null,
);

items[index] で取り出した作品を、そのまま MoviePosterCard に渡すことが大切です。

Q. Top10の番号が表示されません。

ranknull になっていないか確認してください。

rank: showRank ? index + 1 : null,

Top10風に表示したい ContentRow では、次の指定が必要です。

showRank: true,

チャレンジ

チャレンジ1:ポスターカードの角丸を大きくしよう

次のコードを探してください。

borderRadius: BorderRadius.circular(6),

これを次のように変更します。

borderRadius: BorderRadius.circular(14),

カードの印象がどう変わるか確認してください。


チャレンジ2:ランキング番号を少し小さくしよう

次のコードを探してください。

fontSize: 84,

これを次のように変更します。

fontSize: 72,

Top10風カードの数字が少し小さくなります。


チャレンジ3:ランキング番号の位置を少し上にしよう

次のコードを探してください。

bottom: -9,

これを次のように変更します。

bottom: 0,

数字が少し上に移動します。


チャレンジ4:詳細画面へ渡すデータの流れを説明しよう

次の流れを、自分の言葉で説明してみましょう。

items[index]
↓
MoviePosterCard(movie: movie)
↓
MovieDetailPage(movie: movie)
↓
詳細画面で作品情報を表示

コードを書くだけでなく、「どこからどこへデータが渡っているのか」を説明できるようになると、アプリ全体を理解しやすくなります。


チャレンジの答え

チャレンジ1の答え

変更前:

borderRadius: BorderRadius.circular(6),

変更後:

borderRadius: BorderRadius.circular(14),

ポスターカードの角がより丸くなります。


チャレンジ2の答え

変更前:

fontSize: 84,

変更後:

fontSize: 72,

Top10風のランキング番号が少し小さくなります。


チャレンジ3の答え

変更前:

bottom: -9,

変更後:

bottom: 0,

ランキング番号が少し上に移動します。


チャレンジ4の答え

データの流れは、次のようになります。

ContentRowのitemBuilderで、items[index]から1作品分のデータを取り出す
↓
取り出したmovieをMoviePosterCardに渡す
↓
MoviePosterCardのカードがタップされる
↓
Navigator.pushでMovieDetailPageを開く
↓
MovieDetailPage(movie: movie)として、同じ作品データを詳細画面へ渡す
↓
詳細画面では、そのmovieを使ってタイトルや説明文、画像を表示する

この流れを理解すると、一覧画面から詳細画面へデータを渡す仕組みが分かります。


この節のまとめ

この節では、ポスターカードUIを使って、作品画像を表示し、タップしたら詳細画面へ移動する方法を学びました。

大切なポイントは次の通りです。

  • MoviePosterCard は、作品1つ分のポスターカードを作るWidget。
  • Image.network を使うと、URLから画像を読み込んで表示できる。
  • ポスターカードには、縦長画像の posterUrl を使う。
  • BoxFit.cover を使うと、画像をカードいっぱいに表示できる。
  • ClipRRect を使うと、画像を角丸にできる。
  • Stack を使うと、画像とランキング番号を重ねられる。
  • Positioned を使うと、重ねたWidgetの位置を細かく指定できる。
  • if (rank != null) を使うと、ランキング番号を出すカードと出さないカードを分けられる。
  • GestureDetector を使うと、カード全体をタップできるようになる。
  • Navigator.pushMaterialPageRoute を使うと、詳細画面へ移動できる。
  • MovieDetailPage(movie: movie) と書くことで、タップした作品データを詳細画面へ渡せる。

次のステップ

次の節では、作品詳細画面を詳しく見ていきます。

詳細画面では、背景画像、タイトル、作品説明、メタ情報、Playボタン、Downloadボタン、My List、Like、Shareなど、たくさんのUIが組み合わさっています。

一覧画面で選んだ作品情報を、どのように詳細画面で表示しているのかを確認していきましょう。

教材トップへ戻る