1. 始める前に
はじめに
これまでの Codelab では、リポジトリ パターンを使用してウェブサービスからデータを取得し、レスポンスを解析して Kotlin オブジェクトに変換する方法を学びました。この Codelab では、その知識に基づいて、ウェブ URL から写真を読み込んで表示します。また、LazyVerticalGrid
を作成して概要ページに画像のグリッドを表示する方法もおさらいします。
前提条件
- REST ウェブサービスから JSON を取得する方法と、Retrofit および Gson ライブラリを使用してそのデータを解析し、Kotlin オブジェクトに変換する方法に関する知識
- REST ウェブサービスに関する知識
- Android アーキテクチャ コンポーネント(データレイヤやリポジトリなど)に精通していること
- 依存関係インジェクションに関する知識
ViewModel
とViewModelProvider.Factory
に関する知識- アプリのコルーチン実装に関する知識
- リポジトリ パターンの知識
学習内容
- Coil ライブラリを使用してウェブ URL から画像を読み込んで表示する方法。
LazyVerticalGrid
を使用して画像のグリッドを表示する方法。- 画像をダウンロードして表示する際に発生するエラーの処理方法。
作成するアプリの概要
- Mars Photos アプリを変更して、火星のデータから画像 URL を取得し、Coil を使用してその画像を読み込んで表示します。
- 読み込み中のアニメーションとエラーアイコンをアプリに追加します。
- ステータス処理とエラー処理をアプリに追加します。
必要なもの
- 最新のウェブブラウザ(Chrome の最新バージョンなど)を搭載したパソコン
- REST ウェブサービスを使用した Mars Photos アプリのスターター コード
2. アプリの概要
この Codelab では、前の Codelab で作成した Mars Photos アプリを引き続き操作します。Mars Photos アプリは、ウェブサービスに接続し、Gson を使用して取得した Kotlin オブジェクトの数を取得して表示します。これらの Kotlin オブジェクトには、NASA の火星探査機が撮影した火星表面の実際の写真の URL が含まれています。
この Codelab で作成するバージョンのアプリでは、火星の写真が画像のグリッドで表示されます。これらの画像は、アプリがウェブサービスから取得したデータの一部です。アプリは Coil ライブラリを使用して画像の読み込みと表示を行い、LazyVerticalGrid
を使用して画像のグリッド レイアウトを作成します。また、アプリはエラー メッセージを表示して、ネットワーク エラーを適切に処理します。
スターター コードを取得する
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git $ cd basic-android-kotlin-compose-training-mars-photos $ git checkout coil-starter
コードは Mars Photos
GitHub リポジトリで確認できます。
3. ダウンロードした画像を表示する
ウェブ URL からの写真を表示するのは簡単に思えますが、適切に処理するにはかなりの作業が必要です。画像をダウンロードして内部的に保存(キャッシュ)し、圧縮形式から Android が使用できる形式にデコードする必要があります。メモリ内キャッシュとストレージベースのキャッシュの両方またはいずれかに画像を保存できます。これらすべてを優先度の低いバックグラウンド スレッドで行いつつ、UI の応答性を維持する必要があります。また、ネットワークと CPU のパフォーマンスを最適化するため、複数の画像を一度に取得してデコードする必要もあります。
幸いなことに、コミュニティで開発された Coil というライブラリを使用して、画像のダウンロード、バッファリング、デコード、キャッシュ保存を行うことができます。Coil を使用しないと、行うべき作業が格段に増えます。
Coil は、基本的に次の 2 つのものを必要とします。
- 読み込んで表示する画像の URL。
- 実際に画像を表示するための
AsyncImage
コンポーザブル。
このタスクでは、Coil を使用して、Mars ウェブサービスから取得した単一の画像を表示する方法を学びます。ウェブサービスから返された火星写真のリストに含まれる最初の写真の画像を表示します。次の画像は、変更前と変更後のスクリーンショットを示しています。
Coil の依存関係を追加する
- リポジトリと手動 DI の追加 Codelab から Mars Photos アプリの解答コードを開きます。
- アプリを実行して、火星写真の取得数が表示されることを確認します。
- build.gradle.kts (Module :app) を開きます。
dependencies
セクションで、Coil ライブラリ用に次の行を追加します。
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
Coil のドキュメント ページでライブラリの最新バージョンを確認し、アップデートします。
- [Sync Now] をクリックし、新しい依存関係でプロジェクトを再ビルドします。
画像の URL を表示する
このステップでは、最初の火星写真の URL を取得して表示します。
getMarsPhotos()
メソッドのui/screens/MarsViewModel.kt
にあるtry
ブロック内で、ウェブサービスから取得したデータをlistResult
に設定している行を見つけます。
// No need to copy, code is already present
try {
val listResult = marsPhotosRepository.getMarsPhotos()
//...
}
- この行を更新するには、
listResult
をresult
に変更し、取得した最初の火星写真を新しい変数result
に割り当てます。インデックス0
の最初の写真オブジェクトを割り当てます。
try {
val result = marsPhotosRepository.getMarsPhotos()[0]
//...
}
- 次の行で、
MarsUiState.Success()
関数呼び出しに渡すパラメータを、次のコード内の文字列に更新します。listResult
ではなく、新しいプロパティのデータを使用します。写真result
の最初の画像の URL を表示します。
try {
...
MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}
完全な try
ブロックは、次のコードのようになります。
marsUiState = try {
val result = marsPhotosRepository.getMarsPhotos()[0]
MarsUiState.Success(
" First Mars image URL : ${result.imgSrc}"
)
}
- アプリを実行します。
Text
コンポーザブルにより、最初の火星写真の URL が表示されます。次のセクションでは、アプリにこの URL の画像を表示させる方法について説明します。
AsyncImage
コンポーザブルを追加する
このステップでは、AsyncImage
コンポーザブル関数を追加して、1 枚の火星写真を読み込んで表示します。AsyncImage
は、画像リクエストを非同期で実行し、結果をレンダリングするコンポーザブルです。
// Example code, no need to copy over
AsyncImage(
model = "https://android.com/sample_image.jpg",
contentDescription = null
)
model
引数には、ImageRequest.data
値または ImageRequest
のいずれかを指定できます。上記の例では、ImageRequest.data
値、つまり画像の URL("https://android.com/sample_image.jpg"
)を割り当てます。次のサンプルコードは、ImageRequest
自体を model
に割り当てる方法を示しています。
// Example code, no need to copy over
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.placeholder),
contentDescription = stringResource(R.string.description),
contentScale = ContentScale.Crop,
modifier = Modifier.clip(CircleShape)
)
AsyncImage
は、標準の Image コンポーザブルと同じ引数をサポートします。また、placeholder
/ error
/ fallback
ペインタと onLoading
/ onSuccess
/ onError
コールバックの設定もサポートします。上記のサンプルコードでは、画像を円形の切り抜きとクロスフェードで読み込み、プレースホルダを設定しています。
contentDescription
は、この画像が表す内容を示すためにユーザー補助サービスで使用されるテキストを設定します。
AsyncImage
コンポーザブルをコードに追加して、最初に取得された火星写真を表示します。
ui/screens/HomeScreen.kt
に、MarsPhotoCard()
という新しいコンポーズ可能な関数を追加します。これはMarsPhoto
とModifier
を受け取ります。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
- コンポーズ可能な関数
MarsPhotoCard()
内に、次のようにAsyncImage()
関数を追加します。
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.platform.LocalContext
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
上記のコードでは、画像の URL(photo.imgSrc
)を使用して ImageRequest
を作成し、model
引数に渡します。contentDescription
を使用して、ユーザー補助リーダー用のテキストを設定します。
crossfade(true)
をImageRequest
に追加して、リクエストが正常に完了したときにクロスフェード アニメーションを有効にします。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
modifier = Modifier.fillMaxWidth()
)
}
- リクエストが正常に完了したときに
ResultScreen
コンポーザブルではなくMarsPhotoCard
コンポーザブルを表示するようにHomeScreen
コンポーザブルを更新します。型の不一致エラーは次のステップで修正します。
@Composable
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier.fillMaxSize())
else -> ErrorScreen(modifier = modifier.fillMaxSize())
}
}
MarsViewModel.kt
ファイルで、String
ではなくMarsPhoto
オブジェクトを受け入れるようにMarsUiState
インターフェースを更新します。
sealed interface MarsUiState {
data class Success(val photos: MarsPhoto) : MarsUiState
//...
}
- 最初の火星写真オブジェクトを
MarsUiState.Success()
に渡すように、getMarsPhotos()
関数を更新します。result
変数を削除します。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
- アプリを実行し、単一の火星画像が表示されていることを確認します。
- 火星の写真が画面全体に表示されていません。画面上の使用可能なスペースを埋めるには、
HomeScreen.kt
およびAsyncImage
でcontentScale
をContentScale.Crop
に設定します。
import androidx.compose.ui.layout.ContentScale
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = modifier,
)
}
- アプリを実行し、画像が水平方向と垂直方向の両方で画面全体に表示されることを確認します。
読み込み中の画像とエラーの画像を追加する
画像の読み込み時にプレースホルダ画像を表示して、アプリのユーザー エクスペリエンスを改善できます。画像がない、画像が破損していることなどが原因で読み込みが失敗した場合、エラー画像を表示することもできます。このセクションでは、AsyncImage
を使用してエラー画像とプレースホルダ画像の両方を追加します。
res/drawable/ic_broken_image.xml
を開き、右側の [Design] タブまたは [Split] タブをクリックします。エラー画像の場合は、組み込みのアイコン ライブラリにある破損した画像のアイコンを使用します。このベクター型ドローアブルでは、android:tint
属性によりアイコンがグレーに色付けされています。
res/drawable/loading_img.xml
を開きます。このドローアブルは、中心点の周りで画像ドローアブル(loading_img.xml
)が回転するアニメーションです(アニメーションはプレビューでは確認できません)。
HomeScreen.kt
ファイルに戻ります。次のコードに示すように、MarsPhotoCard
コンポーザブルで、AsyncImage()
の呼び出しを更新してerror
属性とplaceholder
属性を追加します。
import androidx.compose.ui.res.painterResource
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
// ...
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
// ...
)
}
このコードは、読み込み時に使用する読み込み中のプレースホルダ画像(loading_img
ドローアブル)を設定します。また、画像の読み込みに失敗した場合に使用する画像(ic_broken_image
ドローアブル)も設定します。
完全な MarsPhotoCard
コンポーザブルは、次のコードのようになります。
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop
)
}
- アプリを実行します。ネットワーク接続の速度によっては、Coil がプロパティ画像をダウンロードして表示する間、一瞬だけ読み込み中の画像が表示されます。しかし、ネットワークを切断しても、破損した画像のアイコンは表示されません。この問題は、Codelab の最後のタスクで修正します。
4. LazyVerticalGrid で画像のグリッドを表示する
以上で、アプリはインターネットから火星写真(最初の MarsPhoto
リストアイテム)を読み込むようになりました。この火星写真データの画像 URL を使用して AsyncImage
にデータを入力しました。しかし、目標はアプリに画像のグリッドを表示させることです。このタスクでは、LazyVerticalGrid
とグリッド レイアウト マネージャーを使用して、画像のグリッドを表示します。
Lazy グリッド
LazyVerticalGrid コンポーザブルと LazyHorizontalGrid コンポーザブルは、アイテムをグリッドに表示するためのサポートを提供しています。Lazy 垂直グリッドは、複数の列にわたって、上下にスクロール可能なコンテナにアイテムを表示し、Lazy 水平グリッドは横軸でそれと同じように動作します。
デザインの観点からすると、グリッド レイアウトは火星写真をアイコンまたは画像として表示するのに最適です。
LazyVerticalGrid
の columns
パラメータと LazyHorizontalGrid
の rows
パラメータは、セルを列または行に配置する方法を制御します。次のサンプルコードでは、GridCells.Adaptive
を使用して各列の幅を 128.dp
以上に設定し、グリッド形式でアイテムを表示しています。
// Sample code - No need to copy over
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
LazyVerticalGrid
を使用するとアイテムの幅を指定でき、グリッドに可能な限り多くの列が収まるようになります。列の数が計算された後、グリッドは残りの幅を列間で均等に分配します。このような適応型のサイズ調整方法は、さまざまな画面サイズでアイテムのセットを表示する場合に特に便利です。
この Codelab では、火星写真を表示するために LazyVerticalGrid
コンポーザブルを使用し、GridCells.Adaptive
の幅を 150.dp
に設定します。
アイテムのキー
ユーザーがグリッド(LazyColumn
内の LazyRow
)をスクロールすると、リストアイテムの位置が変更されます。しかし、画面の向きの変更やアイテムの追加または削除によって、ユーザーに表示される行内のスクロール位置が消える可能性があります。アイテムのキーを使用すると、キーに基づいてスクロール位置を維持できます。
キーを指定することで、Compose が並べ替えを正しく処理できるようになります。たとえば、アイテムに記憶された状態が含まれている場合、キーを設定すると、位置が変更されたときに、Compose がこの状態をアイテムと一緒に移動できるようになります。
LazyVerticalGrid を追加する
火星写真のリストを垂直グリッドで表示するコンポーザブルを追加します。
HomeScreen.kt
ファイルで、PhotosGridScreen()
という名前のコンポーズ可能な関数を新たに作成します。この関数は、MarsPhoto
のリストとmodifier
を引数として受け取ります。
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
PhotosGridScreen
コンポーザブル内に、次のパラメータでLazyVerticalGrid
を追加します。
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
modifier = modifier.padding(horizontal = 4.dp),
contentPadding = contentPadding,
) {
}
}
- アイテムのリストを追加するには、
LazyVerticalGrid
ラムダ内でitems()
関数を呼び出してMarsPhoto
のリストとアイテムキーをphoto.id
として渡します。
import androidx.compose.foundation.lazy.grid.items
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
// ...
) {
items(items = photos, key = { photo -> photo.id }) {
}
}
}
- 単一のリストアイテムで表示されるコンテンツを追加するには、
items
ラムダ式を定義します。MarsPhotoCard
を呼び出してphoto
を渡します。
items(items = photos, key = { photo -> photo.id }) {
photo -> MarsPhotoCard(photo)
}
- リクエストが正常に完了したときに
MarsPhotoCard
コンポーザブルではなくPhotosGridScreen
コンポーザブルを表示するようにHomeScreen
コンポーザブルを更新します。
when (marsUiState) {
// ...
is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
// ...
}
MarsViewModel.kt
ファイルで、単一のMarsPhoto
ではなくMarsPhoto
オブジェクトのリストを受け入れるように、MarsUiState
インターフェースを更新します。PhotosGridScreen
コンポーザブルは、MarsPhoto
オブジェクトのリストを受け入れます。
sealed interface MarsUiState {
data class Success(val photos: List<MarsPhoto>) : MarsUiState
//...
}
MarsViewModel.kt
ファイルで、火星写真オブジェクトのリストをMarsUiState.Success()
に渡すようにgetMarsPhotos()
関数を更新します。
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
- アプリを実行します。
各写真の周囲にパディングはなく、写真ごとにアスペクト比が異なることに注意してください。これらの問題は Card
コンポーザブルを追加することで解決できます。
カード コンポーザブルを追加する
HomeScreen.kt
ファイルのMarsPhotoCard
コンポーザブルで、AsyncImage
の周囲に8.dp
のエレベーションを持つCard
を追加します。modifier
引数をCard
コンポーザブルに割り当てます。
import androidx.compose.material.Card
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(photo.imgSrc)
.crossfade(true)
.build(),
error = painterResource(R.drawable.ic_broken_image),
placeholder = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.mars_photo),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
}
}
- アスペクト比を修正するには、
PhotosGridScreen()
でMarsPhotoCard()
の修飾子を更新します。
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
LazyVerticalGrid(
//...
) {
items(items = photos, key = { photo -> photo.id }) { photo ->
MarsPhotoCard(
photo,
modifier = modifier
.padding(4.dp)
.fillMaxWidth()
.aspectRatio(1.5f)
)
}
}
}
PhotosGridScreen()
をプレビューするように結果画面のプレビューを更新します。空の画像 URL のモックデータ
@Preview(showBackground = true) @Composable fun PhotosGridScreenPreview() { MarsPhotosTheme { val mockData = List(10) { MarsPhoto("$it", "") } PhotosGridScreen(mockData) } }
モックデータには空の URL があるため、写真グリッドのプレビューに画像の読み込みが表示されます。
- アプリを実行します。
- アプリの実行中に機内モードをオンにします。
- エミュレータで画像をスクロールします。まだ読み込まれていない画像が、破損した画像のアイコンとして表示されます。これは、ネットワーク エラーが発生したときや画像を取得できないときに表示するために Coil 画像ライブラリに渡した画像ドローアブルです。
おつかれさまでした。エミュレータまたはデバイスで機内モードをオンにして、ネットワーク接続エラーをシミュレートしました。
5. 再試行アクションを追加する
このセクションでは、再試行アクション ボタンを追加し、ボタンがクリックされたときに写真を取得します。
- エラー画面にボタンを追加します。
HomeScreen.kt
ファイルで、retryAction
ラムダ パラメータとボタンを含めるようにErrorScreen()
コンポーザブルを更新します。
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
Column(
// ...
) {
Image(
// ...
)
Text(//...)
Button(onClick = retryAction) {
Text(stringResource(R.string.retry))
}
}
}
プレビューの確認
- 再試行ラムダを渡すように
HomeScreen()
コンポーザブルを更新します。
@Composable
fun HomeScreen(
marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
when (marsUiState) {
//...
is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
}
}
ui/theme/MarsPhotosApp.kt
ファイルのHomeScreen()
関数呼び出しを更新し、retryAction
ラムダ パラメータをmarsViewModel::getMarsPhotos
に設定します。これにより、サーバーから火星の写真が取得されます。
HomeScreen(
marsUiState = marsViewModel.marsUiState,
retryAction = marsViewModel::getMarsPhotos
)
6. ViewModel テストを更新する
MarsUiState
と MarsViewModel
が、1 枚の写真ではなく、写真のリストに対応するようになりました。現在の状態では、MarsViewModelTest
は MarsUiState.Success
データクラスが文字列プロパティを含むことを想定しています。そのため、テストはコンパイルされません。marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
テストを更新して、MarsViewModel.marsUiState
が写真のリストを含む Success
の状態と等しいことを確認する必要があります。
rules/MarsViewModelTest.kt
ファイルを開きます。marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
テストでは、assertEquals()
関数呼び出しを変更して、Success
状態(架空の写真リストを photos パラメータに渡す)をmarsViewModel.marsUiState
と比較します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest {
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
assertEquals(
MarsUiState.Success(FakeDataSource.photosList),
marsViewModel.marsUiState
)
}
これでテストのコンパイル、実行が完了し、合格できました。
7. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab の解答コードを確認する場合は、GitHub で表示します。
8. まとめ
おつかれさまです。これで Codelab は完了し、Mars Photos アプリが完成しました。アプリを使って家族や友人に実際の火星の写真を見せてあげましょう。
作成したら、#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。
9. 関連リンク
Android デベロッパー ドキュメント:
- リストとグリッド | Jetpack Compose | Android デベロッパー
- Lazy グリッド | Jetpack Compose | Android デベロッパー
- ViewModel の概要
その他: