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

【共有機能】share_plusでiOSの共有画面を表示する

22NETFLIX風動画アプリを作りながらUI・検索・動画再生・共有機能を学ぶFlutter(iOS・Android)アプリ開発
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

この節で学ぶこと

前の節では、PageView.builder を使って、縦スクロールのClips画面を作りました。

今回の節では、作品を共有するための Shareボタン を作ります。

このアプリでは、share_plus というパッケージを使って、iOSやAndroidの共有画面を表示します。

たとえば、作品詳細画面やClips画面で Share を押すと、次のような共有画面を開けます。

Shareボタンを押す
↓
iOSやAndroidの共有画面が開く
↓
LINE・メッセージ・メールなどで共有できる

今回のポイントは、単に共有するだけではありません。

iOSでは、共有画面を表示するときに sharePositionOrigin を指定しないと、環境によってエラーになることがあります。

そのため、この教材では、iOSでも安定して共有画面を表示する方法まで学びます。


今回使うパッケージ

共有機能には、share_plus を使います。

pubspec.yaml には、次のように書いてあります。

dependencies:
  flutter:
    sdk: flutter

  share_plus: ^12.0.1

share_plus は、Flutterアプリから端末標準の共有画面を開くためのパッケージです。

アプリ側でLINEやメールアプリを直接作るのではなく、端末に入っている共有先を一覧で表示できます。

Flutterアプリ
↓
share_plus
↓
端末標準の共有画面
↓
LINE / メール / メッセージ / AirDrop など

パッケージを読み込む

share_plus を使うために、main.dart の上部でimportしています。

import 'package:share_plus/share_plus.dart';

このimportがないと、SharePlusShareParams が使えません。

もし次のようなエラーが出た場合は、importが入っているか確認してください。

Undefined name 'SharePlus'
Undefined class 'ShareParams'

共有したい内容

今回のアプリでは、作品タイトルとYouTubeリンクを共有します。

共有文のイメージは、次のような形です。

NETAFLIXで「Squid Game」をチェック!
https://www.youtube.com/watch?v=oqxAJKy0ii4

作品ごとにタイトルとYouTube動画IDが違うので、movie の中にある情報を使って共有文を作ります。

movie.title
movie.youtubeVideoId

つまり、MovieItem のデータを使って、共有内容を作ります。


共有処理を関数にまとめる

今回のコードでは、共有処理を shareNetaflixMovie という関数にまとめています。

Future<void> shareNetaflixMovie({
  required BuildContext context,
  required MovieItem movie,
}) async {
  await SharePlus.instance.share(
    ShareParams(
      text:
          'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
      subject: movie.title,
      sharePositionOrigin: shareOriginFromContext(context),
    ),
  );
}

この関数を作っておくことで、詳細画面でもClips画面でも同じ共有処理を使えます。

MovieDetailPage
↓
shareNetaflixMovieを呼ぶ

ClipsPage
↓
shareNetaflixMovieを呼ぶ

同じ処理を何度も書かずに済むので、コードが整理されます。


Futureとは?

関数の最初に、次のように書かれています。

Future<void> shareNetaflixMovie({

Future は、「すぐには終わらない処理」を表します。

共有画面を開く処理は、端末側の機能を呼び出すため、少し時間がかかることがあります。

そのため、Future を使っています。

void は、「結果として返す値はない」という意味です。

つまり、Future<void> は次のような意味です。

少し時間がかかる処理
でも、処理が終わっても値は返さない

共有画面を開く処理には、よく合う書き方です。


requiredで必要な値を指定する

shareNetaflixMovie は、2つの値を受け取ります。

required BuildContext context,
required MovieItem movie,
役割
context共有画面の表示位置を決めるために使う
movie共有する作品情報を作るために使う

どちらも required がついています。

これは、この関数を呼び出すときに必ず渡す必要があるという意味です。

shareNetaflixMovie(
  context: context,
  movie: movie,
);

context がないと、iOSで共有画面の表示位置を決めにくくなります。

movie がないと、どの作品を共有するのか分かりません。


SharePlus.instance.shareとは?

実際に共有画面を開いているのは、この部分です。

await SharePlus.instance.share(
  ShareParams(
    text:
        'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
    subject: movie.title,
    sharePositionOrigin: shareOriginFromContext(context),
  ),
);

SharePlus.instance.share を実行すると、端末の共有画面が開きます。

その中に ShareParams を渡しています。

ShareParams は、共有する内容や設定をまとめるためのものです。


ShareParamsとは?

ShareParams には、共有するテキストや件名、表示位置などを指定します。

ShareParams(
  text:
      'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
  subject: movie.title,
  sharePositionOrigin: shareOriginFromContext(context),
)

今回指定しているのは、次の3つです。

項目役割
text実際に共有する文章
subjectメールなどで使われる件名
sharePositionOriginiOSで共有画面を出す位置

特に大事なのが、sharePositionOrigin です。

iOSでは、これを指定しておくことで共有画面が安定して表示されます。


textで共有文を作る

共有する本文は、text に書いています。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

ここでは、文字列の中に movie.titlemovie.youtubeVideoId を入れています。

${movie.title}
${movie.youtubeVideoId}

たとえば、movie.titleSquid Gamemovie.youtubeVideoIdoqxAJKy0ii4 の場合、共有文はこうなります。

NETAFLIXで「Squid Game」をチェック!
https://www.youtube.com/watch?v=oqxAJKy0ii4

作品ごとに共有文が自動で変わるのがポイントです。


\nで改行する

共有文の中には、\n が入っています。

'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}'

\n は改行を表します。

つまり、共有文は1行ではなく、2行になります。

NETAFLIXで「作品名」をチェック!
YouTubeのURL

共有するときは、作品名とURLが分かれていた方が読みやすくなります。


subjectで件名を指定する

subject には、作品タイトルを入れています。

subject: movie.title,

subject は、共有先によって使われ方が変わります。

たとえば、メールで共有する場合は、件名として使われることがあります。

件名:Squid Game
本文:NETAFLIXで「Squid Game」をチェック!
...

すべての共有先で必ず表示されるわけではありませんが、入れておくとメール共有などで自然になります。


iOSで大事なsharePositionOrigin

今回の共有機能で特に大事なのが、次の部分です。

sharePositionOrigin: shareOriginFromContext(context),

iOS、特にiPadでは、共有画面をどこから表示するのかを指定する必要があります。

これがない、または不正な値になると、次のようなエラーが出ることがあります。

PlatformException
sharePositionOrigin: argument must be set

このエラーは、共有画面を出す位置が正しく指定されていないときに起こります。

そのため、sharePositionOrigin を必ず指定するようにしています。


shareOriginFromContextとは?

sharePositionOrigin に渡しているのが、shareOriginFromContext(context) です。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

この関数は、共有画面を表示するための位置情報を作っています。

戻り値は Rect です。

Rect shareOriginFromContext(BuildContext context)

Rect は、四角い範囲を表すデータです。

iOSでは、この四角い範囲をもとに共有画面の表示位置を決めます。


MediaQuery.sizeOf(context)とは?

関数の中で、まず画面サイズを取得しています。

final size = MediaQuery.sizeOf(context);

MediaQuery.sizeOf(context) を使うと、現在の画面サイズが分かります。

たとえば、スマホ画面なら、横幅や高さを取得できます。

画面の横幅
画面の高さ

このサイズを使って、共有画面の表示位置を作ります。


Rect.fromLTWHとは?

次に使っているのが、Rect.fromLTWH です。

Rect.fromLTWH(
  1,
  1,
  size.width <= 2 ? 1 : size.width - 2,
  size.height <= 2 ? 1 : size.height - 2,
);

Rect.fromLTWH は、四角形の位置と大きさを作るための書き方です。

LTWH は、次の意味です。

文字意味
LLeft。左からの位置
TTop。上からの位置
WWidth。横幅
HHeight。高さ

今回の場合は、画面全体に近い範囲を指定しています。

左:1
上:1
幅:画面幅 - 2
高さ:画面高さ - 2

完全に0から始めるのではなく、少しだけ内側にしています。


なぜ0ではなく1にするのか

Rect.fromLTWH の最初の2つは、左と上の位置です。

1,
1,

本当は 0, 0 でも画面左上を表せそうですが、共有処理では {0, 0}, {0, 0} のように、幅や高さが0になってしまうとエラーになることがあります。

そのため、少し内側の 1, 1 から始めています。

完全な0ではなく
少し内側の1から始める

これにより、iOSで「表示位置が0です」という扱いになりにくくなります。


幅と高さを0にしない工夫

次の部分も大切です。

size.width <= 2 ? 1 : size.width - 2,
size.height <= 2 ? 1 : size.height - 2,

これは、三項演算子です。

意味は次の通りです。

画面幅が2以下なら、幅は1にする
そうでなければ、画面幅 - 2にする

画面高さが2以下なら、高さは1にする
そうでなければ、画面高さ - 2にする

つまり、幅や高さが0にならないようにしています。

sharePositionOrigin では、幅と高さが0だとエラーになることがあります。

そのため、最低でも 1 になるようにしています。


共有関数の流れを整理しよう

shareNetaflixMovie の流れは、次の通りです。

Shareボタンを押す
↓
shareNetaflixMovieが呼ばれる
↓
movie.titleとmovie.youtubeVideoIdで共有文を作る
↓
shareOriginFromContextで表示位置を作る
↓
SharePlus.instance.shareを実行する
↓
iOSやAndroidの共有画面が開く

このように、共有内容とiOS対策を1つの関数にまとめています。


詳細画面から共有する

作品詳細画面では、Shareボタンから次のように呼び出しています。

DetailAction(
  icon: Icons.share_outlined,
  label: 'Share',
  active: false,
  onTap: () {
    shareNetaflixMovie(
      context: context,
      movie: movie,
    );
  },
),

ここで、今表示している作品データ movie を渡しています。

movie: movie

そのため、詳細画面で Squid Game を見ている場合は、Squid Game の共有文が作られます。

NETAFLIXで「Squid Game」をチェック!
https://www.youtube.com/watch?v=oqxAJKy0ii4

Clips画面から共有する

Clips画面でも、同じ共有関数を使っています。

void shareMovie(MovieItem movie) {
  shareNetaflixMovie(
    context: context,
    movie: movie,
  );
}

そして、ClipMoviePage に渡しています。

onTapShare: () => shareMovie(item),

Clips画面では、縦スクロールで表示中の作品を共有したいので、item を渡しています。

final item = movies[movieIndex];

つまり、今表示している作品をそのまま共有できます。


共通関数にするメリット

詳細画面とClips画面で、別々に共有処理を書くこともできます。

しかし、そうすると同じようなコードが増えてしまいます。

詳細画面用の共有処理
Clips画面用の共有処理
Search画面用の共有処理

これでは、あとで共有文を変えたいときに、いろいろな場所を直す必要があります。

そこで、shareNetaflixMovie にまとめています。

共有処理は1か所にまとめる
↓
どの画面からも同じ関数を呼ぶ
↓
修正が楽になる

たとえば、共有文を変更したい場合も、shareNetaflixMovie の中だけを直せば済みます。


ここまでのまとめ

ここまでで、share_plus を使って共有画面を開く基本を確認しました。

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

  • share_plus を使うと、端末標準の共有画面を表示できる。
  • pubspec.yamlshare_plus: ^12.0.1 を追加する。
  • main.dartpackage:share_plus/share_plus.dart をimportする。
  • SharePlus.instance.share で共有画面を開く。
  • ShareParams に共有する内容をまとめる。
  • text には共有本文を入れる。
  • subject には件名として作品タイトルを入れる。
  • movie.titlemovie.youtubeVideoId を使うと、作品ごとに共有文を変えられる。
  • sharePositionOrigin を指定すると、iOSで共有画面が安定しやすい。
  • shareOriginFromContext で、幅と高さが0にならない Rect を作っている。
  • 共有処理を shareNetaflixMovie にまとめると、詳細画面やClips画面から使い回せる。

sharePositionOriginがないと起きやすいエラー

iOSで共有機能を使うとき、次のようなエラーが出ることがあります。

PlatformException
sharePositionOrigin: argument must be set

これは、共有画面を表示する位置が正しく指定されていないときに起こります。

特にiPadでは、共有メニューがポップオーバーとして表示されるため、「どの位置から共有画面を出すのか」が必要になります。

そのため、このアプリでは sharePositionOrigin を指定しています。

sharePositionOrigin: shareOriginFromContext(context),

shareOriginFromContextを使う理由

共有画面を表示する位置は、Rect という四角形の情報で渡します。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

この関数では、画面サイズをもとに、共有画面の表示位置を作っています。

ポイントは、幅や高さが 0 にならないようにしていることです。

size.width <= 2 ? 1 : size.width - 2
size.height <= 2 ? 1 : size.height - 2

これにより、共有画面を表示するときの位置情報が安定します。


Rectとは?

Rect は、画面上の四角い範囲を表すデータです。

Rect.fromLTWH(
  left,
  top,
  width,
  height,
)

LTWH は、次の意味です。

項目意味
LLeft。左からの位置
TTop。上からの位置
WWidth。横幅
HHeight。高さ

今回のコードでは、画面全体に近い範囲を指定しています。

Rect.fromLTWH(
  1,
  1,
  size.width <= 2 ? 1 : size.width - 2,
  size.height <= 2 ? 1 : size.height - 2,
);

完全な 0, 0, 0, 0 にならないようにしているのが大切です。


awaitを使う理由

共有処理では、次のように await を使っています。

await SharePlus.instance.share(
  ShareParams(
    text:
        'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
    subject: movie.title,
    sharePositionOrigin: shareOriginFromContext(context),
  ),
);

await は、処理が終わるまで待つための書き方です。

共有画面を開く処理は、端末側の共有機能を呼び出すため、すぐに終わるとは限りません。

そのため、asyncawait を使っています。

Future<void> shareNetaflixMovie(...) async {
  await ...
}

アプリ開発では、外部機能を呼び出す処理や通信処理では、Futureasyncawait がよく出てきます。


Shareボタンを押したときの流れ

詳細画面でShareボタンを押したときの流れを確認しましょう。

DetailAction(
  icon: Icons.share_outlined,
  label: 'Share',
  active: false,
  onTap: () {
    shareNetaflixMovie(
      context: context,
      movie: movie,
    );
  },
),

流れは次の通りです。

Shareボタンを押す
↓
onTapが実行される
↓
shareNetaflixMovieが呼ばれる
↓
共有文を作る
↓
sharePositionOriginを作る
↓
SharePlus.instance.shareを実行する
↓
端末の共有画面が表示される

このように、ボタン側では関数を呼ぶだけにしておくと、コードが読みやすくなります。


Clips画面のShareボタンでも同じ処理を使う

Clips画面でも、同じ共有関数を使っています。

void shareMovie(MovieItem movie) {
  shareNetaflixMovie(
    context: context,
    movie: movie,
  );
}

そして、ClipMoviePage に渡しています。

onTapShare: () => shareMovie(item),

これにより、Clips画面で今表示している作品を共有できます。

詳細画面でもClips画面でも、共有処理は同じです。

詳細画面のShare
↓
shareNetaflixMovie

Clips画面のShare
↓
shareNetaflixMovie

共通関数にしておくことで、共有文やエラー対策を1か所で管理できます。


共有文をカスタマイズしてみよう

共有文は、text の部分で作っています。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

たとえば、もう少し自然な文章にしたい場合は、次のようにできます。

text:
    'この作品、気になりました。\nNETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文は、アプリの雰囲気に合わせて調整できます。

授業用アプリなら、次のような文でもよいです。

text:
    '授業で作成したNETAFLIXアプリから共有しています。\n作品名:${movie.title}\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

URLの作り方

YouTubeのURLは、次の形で作っています。

'https://www.youtube.com/watch?v=${movie.youtubeVideoId}'

movie.youtubeVideoId には、動画IDだけが入っています。

たとえば、次のようなIDです。

oqxAJKy0ii4

それをURLの後ろに入れることで、YouTubeの動画リンクになります。

https://www.youtube.com/watch?v=oqxAJKy0ii4

このように、固定のURLと作品ごとの動画IDを組み合わせています。


共有処理を直接ボタンに書かない理由

ボタンの中に、直接 SharePlus.instance.share を書くこともできます。

onTap: () {
  SharePlus.instance.share(...);
}

ただし、この書き方だと、共有処理を使う場所が増えたときにコードが重複します。

詳細画面のShareボタン
Clips画面のShareボタン
Search画面のShareボタン

それぞれに同じコードを書くと、あとで修正が大変になります。

そのため、共有処理は関数にまとめます。

shareNetaflixMovie(
  context: context,
  movie: movie,
);

このようにしておくと、共有文を変えたいときも、shareNetaflixMovie の中だけを直せば済みます。


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

Q. SharePlusやShareParamsが見つかりません。

まず、pubspec.yamlshare_plus が入っているか確認してください。

share_plus: ^12.0.1

そのあと、ターミナルで次のコマンドを実行します。

flutter pub get

さらに、main.dart の上部にimportがあるか確認します。

import 'package:share_plus/share_plus.dart';

Q. iOSでPlatformExceptionが出ます。

sharePositionOrigin が指定されているか確認してください。

sharePositionOrigin: shareOriginFromContext(context),

また、shareOriginFromContext の戻り値で、幅と高さが 0 にならないようにしてください。

size.width <= 2 ? 1 : size.width - 2
size.height <= 2 ? 1 : size.height - 2

Rect.fromLTWH(0, 0, 0, 0) のような値になると、iOSでエラーになることがあります。


Q. 共有文が作品ごとに変わりません。

共有文の中で、固定のタイトルを書いていないか確認してください。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

movie.titlemovie.youtubeVideoId を使うことで、作品ごとに共有内容が変わります。


Q. Clips画面で違う作品が共有されます。

onTapShare に、今表示している item を渡しているか確認してください。

onTapShare: () => shareMovie(item),

item は、現在のページで表示している作品です。

final item = movies[movieIndex];

この item を共有することで、Clips画面で表示中の作品を正しく共有できます。


Q. 共有画面が出ません。

まず、Shareボタンの onTap が動いているか確認しましょう。

onTap: () {
  shareNetaflixMovie(
    context: context,
    movie: movie,
  );
}

次に、shareNetaflixMovie の中で SharePlus.instance.share が呼ばれているか確認してください。

await SharePlus.instance.share(
  ShareParams(
    text: ...,
    subject: ...,
    sharePositionOrigin: ...,
  ),
);

エミュレーターやシミュレーターでは、共有先アプリが少なく、見え方が実機と違う場合があります。


チャレンジ

チャレンジ1:共有文を日本語らしく変更しよう

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

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

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

text:
    'この作品、ちょっと気になりました。\nNETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有画面で表示される文章が変わるか確認してください。


チャレンジ2:件名に「予告編」を追加しよう

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

subject: movie.title,

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

subject: '${movie.title} の予告編',

メール共有などで件名が変わるか確認してください。


チャレンジ3:共有文にカテゴリを追加しよう

次のコードを変更して、作品カテゴリも共有文に入れてみましょう。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

変更例はこちらです。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nカテゴリ:${movie.category}\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文にカテゴリが入るか確認してください。


チャレンジ4:共有関数の名前を意味が分かりやすい名前に変えてみよう

現在の関数名は次の通りです。

shareNetaflixMovie

たとえば、次のような名前に変えることもできます。

shareMovieTrailer

ただし、関数名を変えた場合は、呼び出している場所もすべて変更する必要があります。

shareMovieTrailer(
  context: context,
  movie: movie,
);

関数名は、何をする関数なのか分かる名前にすると、あとから読みやすくなります。


チャレンジの答え

チャレンジ1の答え

変更前:

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

変更後:

text:
    'この作品、ちょっと気になりました。\nNETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文の最初に、自然な一文が追加されます。


チャレンジ2の答え

変更前:

subject: movie.title,

変更後:

subject: '${movie.title} の予告編',

メール共有などで、件名に「作品名 の予告編」と表示されます。


チャレンジ3の答え

変更前:

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

変更後:

text:
    'NETAFLIXで「${movie.title}」をチェック!\nカテゴリ:${movie.category}\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文に作品カテゴリが追加されます。


チャレンジ4の答え

変更前:

Future<void> shareNetaflixMovie({
  required BuildContext context,
  required MovieItem movie,
}) async {
  ...
}

変更後:

Future<void> shareMovieTrailer({
  required BuildContext context,
  required MovieItem movie,
}) async {
  ...
}

呼び出し側も、次のように変更します。

shareMovieTrailer(
  context: context,
  movie: movie,
);

関数名を変えるときは、定義部分と呼び出し部分の両方をそろえる必要があります。


この節のまとめ

この節では、share_plus を使って、iOSやAndroidの共有画面を表示する方法を学びました。

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

  • share_plus を使うと、端末標準の共有画面を開ける。
  • SharePlus.instance.share で共有処理を実行する。
  • ShareParams に共有文、件名、表示位置をまとめて渡す。
  • text には実際に共有する本文を入れる。
  • subject にはメールなどで使われる件名を入れる。
  • movie.titlemovie.youtubeVideoId を使うと、作品ごとに共有文を作れる。
  • \n を使うと、共有文の中で改行できる。
  • iOSでは sharePositionOrigin を指定しておくと共有画面が安定しやすい。
  • Rect.fromLTWH で共有画面の表示位置を作る。
  • 幅や高さが 0 にならないようにすることが大切。
  • 共有処理を shareNetaflixMovie のような関数にまとめると、詳細画面やClips画面から使い回せる。
  • 共通関数にしておくと、あとから共有文やiOS対策をまとめて修正できる。

次のステップ

次の節では、iOS共有エラー対策として、sharePositionOrigin をもう少し詳しく見ていきます。

なぜ {0, 0}, {0, 0} のような位置情報でエラーが起きるのか、そして今回の shareOriginFromContext がどのようにそれを防いでいるのかを、より丁寧に分解していきます。

教材トップへ戻る