1. 始める前に
市販されている Android アプリの大半は、ネットワーク オペレーション(バックエンド サーバーからメール、メッセージ、またはその他の情報を取得するなど)を実行するために、インターネットに接続します。Gmail、YouTube、Google フォトは、インターネットに接続してユーザーデータを表示するアプリの例です。
この Codelab では、コミュニティで開発されたオープンソースのライブラリを使用して、バックエンド サーバーからデータを取得するデータレイヤを構築します。この方法は、データの取得を大幅に簡素化します。また、Android のベスト プラクティス(バックグラウンド スレッドでオペレーションを実行するなど)に沿ってアプリを構築するのに役立ちます。さらに、インターネットが低速な場合や利用できない場合は、エラー メッセージを表示します。これにより、ネットワーク接続に関する問題について常にユーザーに通知できます。
前提条件
- コンポーズ可能な関数の作成方法についての基本的な知識。
- Android アーキテクチャ コンポーネントである
ViewModel
の使用方法についての基本的な知識。 - 長時間実行タスクでコルーチンを使用する方法についての基本的な知識。
build.gradle.kts
に依存関係を追加する方法についての基本的な知識。
学習内容
- REST ウェブサービスとは何か。
- Retrofit ライブラリを使用し、インターネット上の REST ウェブサービスに接続してレスポンスを取得する方法。
- シリアル化(kotlinx.serialization)ライブラリを使用し、JSON レスポンスを解析してデータ オブジェクトに変換する方法。
演習内容
- スターター アプリに変更を加え、ウェブサービス API リクエストを送信してレスポンスを処理します。
- Retrofit ライブラリを使用して、アプリにデータレイヤを実装します。
- kotlinx.serialization ライブラリを使用して、ウェブサービスからの JSON レスポンスを解析してアプリのデータ オブジェクトのリストに変換し、UI 状態にアタッチします。
- コルーチンに対する Retrofit のサポートを使用して、コードを簡素化します。
必要なもの
- Android Studio がインストールされているパソコン
- Mars Photos アプリのスターター コード
2. アプリの概要
Mars Photos という名前のアプリを編集して、火星の地表の画像を表示します。このアプリは、ウェブサービスに接続して火星の写真を取得し、表示します。画像は、NASA の火星探査機が撮影した火星の実際の写真です。次の画像は、画像のグリッドを表示する最終的なアプリのスクリーンショットです。
この Codelab で作成するバージョンのアプリに、視覚的に凝ったところはありません。この Codelab では、アプリのデータレイヤの部分に焦点を絞っています。これは、インターネットに接続し、ウェブサービスを使用して未加工のプロパティ データをダウンロードする部分です。アプリがこのデータを正しく取得して解析できるようにするため、バックエンド サーバーから受信した写真の数を Text
コンポーザブルに出力できます。
3. Mars Photos スターター アプリを確認する
スターター コードをダウンロードする
まず、スターター コードをダウンロードします。
または、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 starter
コードは Mars Photos
GitHub リポジトリで確認できます。
スターター コードを実行する
- Android Studio で、ダウンロードしたプロジェクトを開きます。プロジェクトのフォルダ名は
basic-android-kotlin-compose-training-mars-photos
です。 - [Android] ペインで、[app] > [kotlin + java] を開きます。アプリに
ui
という名前のパッケージ フォルダがあることに注目してください。これがアプリの UI レイヤです。
- アプリを実行します。アプリをコンパイルして実行すると、プレースホルダ テキストが中央にある次のような画面が表示されます。この Codelab の最後では、このプレースホルダ テキストを更新して、取得した写真の数に置き換えます。
スターター コードのチュートリアル
ここでは、プロジェクトの構造を把握します。プロジェクトに含まれる重要なファイルとフォルダの説明を以下に示します。
ui\MarsPhotosApp.kt
:
- このファイルには、コンポーザブル
MarsPhotosApp
が含まれています。このコンポーザブルは、トップ アプリバーやHomeScreen
などのコンポーザブルのコンテンツを画面に表示します。前のステップのプレースホルダ テキストは、このコンポーザブルに表示されます。 - 次の Codelab では、このコンポーザブルは Mars Photos バックエンド サーバーから受信したデータを表示します。
screens\MarsViewModel.kt
:
- このファイルは、
MarsPhotosApp
に対応するビューモデルです。 - このクラスには、
marsUiState
という名前のMutableState
プロパティが含まれています。このプロパティの値を更新すると、画面に表示されるプレースホルダ テキストが更新されます。 getMarsPhotos()
メソッドは、プレースホルダのレスポンスを更新します。この Codelab の後半では、このメソッドを使用して、サーバーから取得したデータを表示します。この Codelab の目標は、インターネットから取得したデータを使用して、ViewModel
内のMutableState
を更新することです。
screens\HomeScreen.kt
:
- このファイルには、
HomeScreen
コンポーザブルとResultScreen
コンポーザブルが含まれています。ResultScreen
は、Text
コンポーザブルにmarsUiState
の値を表示するシンプルなBox
レイアウトを備えています。
MainActivity.kt
:
- このアクティビティの唯一のタスクは、
ViewModel
を読み込んでMarsPhotosApp
コンポーザブルを表示することです。
4. ウェブサービスの概要
この Codelab では、バックエンド サーバーと通信して必要なデータを取得するネットワーク サービスのレイヤを作成します。このタスクを実装するため、Retrofit というサードパーティ ライブラリを使用します。詳細については後で説明します。ViewModel
はデータレイヤと通信します。アプリの他の部分はこの実装に対して透過的です。
MarsViewModel
は、火星写真データを取得するためにネットワーク呼び出しを行う役割を担います。ViewModel
では、データが変更されたときに MutableState
を使用してアプリの UI を更新します。
5. ウェブサービスと Retrofit
火星写真データはウェブサーバーに格納されています。このデータをアプリに取り込むには、インターネット上のサーバーとの接続を確立して通信する必要があります。
今日の多くのウェブサーバーは、REST と呼ばれる一般的なステートレス ウェブ アーキテクチャを使用してウェブサービスを実行します。REST とは、REpresentational State Transfer の略です。このアーキテクチャを提供するウェブサービスを、RESTful サービスと呼びます。
リクエストは、Uniform Resource Identifier(URI)を介して、標準化された方法で RESTful ウェブサービスに送信されます。URI は、サーバー内のリソースを、場所またはアクセス方法に関係なく、名前で識別します。たとえば、このレッスンのアプリでは、次のサーバー URI を使用して画像 URL を取得します(このサーバーは火星の不動産と火星の写真の両方をホストしています)。
android-kotlin-fun-mars-server.appspot.com
URL(Uniform Resource Locator)は URI のサブセットであり、リソースが存在する場所とリソースを取得する仕組みを示します。
例:
次の URL は、利用可能な火星のすべての不動産物件のリストを取得します。
https://android-kotlin-fun-mars-server.appspot.com/realestate
次の URL は、火星の写真のリストを取得します。
https://android-kotlin-fun-mars-server.appspot.com/photos
これらの URL は、ハイパーテキスト転送プロトコル(http:)を介してネットワークから取得できる特定のリソース(/realestate や /photos など)を指します。この Codelab では、/photos エンドポイントを使用します。エンドポイントとは、サーバー上で実行されるウェブサービスにアクセスできる URL です。
ウェブサービス リクエスト
個々のウェブサービス リクエストは URI を含んでおり、Chrome などのウェブブラウザで使用されるのと同じ HTTP プロトコルを介してサーバーに転送されます。HTTP リクエストには、サーバーに何をすべきかを伝えるオペレーションが含まれています。
一般的な HTTP オペレーションには次のようなものがあります。
- GET: サーバーデータを取得します。
- POST: サーバー上に新しいデータを作成します。
- PUT: サーバー上の既存のデータを更新します。
- DELETE: サーバーからデータを削除します。
アプリは火星の写真の情報を得るための HTTP GET リクエストをサーバーに送信し、サーバーはそれに応じて画像 URL を含むレスポンスをアプリに返します。
ウェブサービスからのレスポンスは、XML(eXtensible Markup Language)や JSON(JavaScript Object Notation)などの一般的なデータ形式のいずれかで書式設定されています。JSON 形式では、構造化データを Key-Value ペアで表現します。アプリは JSON を使用して REST API と通信します。JSON については、後のタスクで詳しく説明します。
このタスクでは、サーバーへのネットワーク接続を確立し、サーバーと通信して、JSON レスポンスを受信します。演習用にすでに作成されているバックエンド サーバーを使用します。この Codelab では、サードパーティ ライブラリである Retrofit ライブラリを使用して、バックエンド サーバーと通信します。
外部ライブラリ
外部ライブラリ(つまりサードパーティ ライブラリ)は、コア Android API の拡張機能に似ています。このコースで使用するライブラリは、コミュニティによって開発されたオープンソースのライブラリで、全世界に広がる大規模な Android コミュニティの協力によってメンテナンスされています。これらのリソースは、Android デベロッパーが優れたアプリを開発するために役立ちます。
Retrofit ライブラリ
この Codelab で RESTful な Mars ウェブサービスと通信するために使用する Retrofit ライブラリは、適切なサポートとメンテナンスが行われているライブラリの良い例です。GitHub ページにアクセスしてオープンされている問題とクローズされた問題(問題の中には機能リクエストもあります)をチェックすると、そのことがよくわかります。デベロッパーが継続的に問題を解決し、機能リクエストに対応している場合、ライブラリは適切にメンテナンスされていてアプリで使用する有力な候補になる可能性が高くなります。ライブラリについて詳しくは、Retrofit のドキュメントもご覧ください。
Retrofit ライブラリは REST バックエンドと通信します。このライブラリはコードを生成しますが、演習で渡すパラメータに基づいてウェブサービスの URI を指定する必要があります。このトピックについては、後で詳しく説明します。
Retrofit の依存関係を追加する
Android Gradle を使用して、プロジェクトに外部ライブラリを追加できます。ライブラリの依存関係に加えて、ライブラリがホストされているリポジトリも含める必要があります。
- モジュール レベルの Gradle ファイル
build.gradle.kts (Module :app)
を開きます。 dependencies
セクションに、Retrofit ライブラリ用に以下の行を追加します。
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
2 つのライブラリは連携して機能します。1 つ目の依存関係は Retrofit2 ライブラリ用で、2 つ目の依存関係は Retrofit スカラー コンバータ用です。Retrofit2 は Retrofit ライブラリの更新版です。このスカラー コンバータにより、Retrofit は JSON 結果を String
として返すことができます。JSON は、データを保存してクライアントとサーバー間で転送するための形式です。JSON については後で学習します。
- [Sync Now] をクリックして、新しい依存関係でプロジェクトを再ビルドします。
6. インターネットに接続する
Retrofit ライブラリを使用して、Mars ウェブサービスと通信し、未加工の JSON レスポンスを String
として表示します。プレースホルダ Text
は、返された JSON レスポンス文字列か、または接続エラーを示すメッセージを表示します。
Retrofit は、ウェブサービスからのコンテンツに基づいて、アプリ用のネットワーク API を作成します。ウェブサービスからデータを取得し、データのデコード方法を認識している別個のコンバータ ライブラリを経由して、String
のようなオブジェクトの形式でデータを返します。Retrofit には、XML や JSON などのよく使用されるデータ形式のサポートが組み込まれています。最後に Retrofit は、このサービスを呼び出して使用するコードを作成します。コードには、重要な詳細情報(バックグラウンド スレッドでのリクエストの実行など)が含まれています。
このタスクでは、ViewModel
がウェブサービスとの通信に使用する Mars Photos プロジェクトにデータレイヤを追加します。Retrofit サービス API を実装するには、次の手順を実施します。
- データソースである
MarsApiService
クラスを作成します。 - ベース URL とコンバータ ファクトリを使用して、文字列を変換する Retrofit オブジェクトを作成します。
- Retrofit がウェブサーバーと通信する方法を記述するインターフェースを作成します。
- Retrofit サービスを作成し、API サービスのインスタンスをアプリの他の部分に公開します。
上記の手順を実装します。
- Android プロジェクト ペインでパッケージ com.example.marsphotos を右クリックし、[New] > [Package] を選択します。
- ポップアップで、提案されたパッケージ名の末尾に「network」を追加します。
- 新しいパッケージの下に新しい Kotlin ファイルを作成し、
MarsApiService
という名前を付けます。 network/MarsApiService.kt
を開きます。- ウェブサービスのベース URL を表す次の定数を追加します。
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- この定数のすぐ下に、Retrofit オブジェクトを作成するための Retrofit ビルダーを追加します。
import retrofit2.Retrofit
private val retrofit = Retrofit.Builder()
Retrofit は、ウェブサービスのベース URI と、ウェブサービス API を構築するためのコンバータ ファクトリを必要とします。コンバータは、ウェブサービスから返されたデータをどのように処理するかを Retrofit に伝えます。この演習では、Retrofit はウェブサービスから JSON レスポンスを取得し、String
として返す必要があります。Retrofit には、文字列とその他のプリミティブ型をサポートする ScalarsConverter
があります。
ScalarsConverterFactory
のインスタンスを使用して、ビルダーでaddConverterFactory()
を呼び出します。
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
baseUrl()
メソッドを使用して、ウェブサービスのベース URL を追加します。build()
を呼び出して Retrofit オブジェクトを作成します。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- Retrofit ビルダーに対する呼び出しの下に、
MarsApiService
という名前のインターフェースを定義します。このインターフェースで、Retrofit が HTTP リクエストを使用してウェブサーバーと通信する方法を定義します。
interface MarsApiService {
}
- ウェブサービスからレスポンス文字列を取得するために、
getPhotos()
という名前の関数をMarsApiService
インターフェースに追加します。
interface MarsApiService {
fun getPhotos()
}
- これが GET リクエストであることを Retrofit に伝えるために
@GET
アノテーションを使用し、そのウェブサービス メソッド用のエンドポイントを指定します。この演習では、エンドポイントはphotos
です。前のタスクで説明したように、この Codelab では /photos エンドポイントを使用します。
import retrofit2.http.GET
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
getPhotos()
メソッドが呼び出されると、Retrofit はリクエストの開始に使用するベース URL(Retrofit ビルダーで定義した URL)の末尾にエンドポイント photos
を追加します。
- 関数の戻り値の型を
String
に追加します。
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
オブジェクト宣言
Kotlin では、シングルトン オブジェクトの宣言にオブジェクト宣言を使用します。シングルトン パターンを使用すると、オブジェクトのインスタンスが 1 つだけ作成され、そのオブジェクトへのグローバル アクセス ポイントが 1 つであることが保証されます。オブジェクトの初期化はスレッドセーフで、最初のアクセスの際に行われます。
オブジェクト宣言とそれに対するアクセスの例を次に示します。オブジェクト宣言では、常に object
キーワードの後に名前を指定します。
例:
// Example for Object declaration, do not copy over
object SampleDataProvider {
fun register(provider: SampleProvider) {
// ...
}
// ...
}
// To refer to the object, use its name directly.
SampleDataProvider.register(...)
Retrofit オブジェクトで create()
関数を呼び出すのは、メモリ、速度、パフォーマンスの点で高コストです。アプリに必要な Retrofit API サービスのインスタンスは 1 つだけであるため、オブジェクト宣言を使用してアプリの他の部分にサービスを公開します。
MarsApiService
インターフェース宣言の外部で、MarsApi
という公開オブジェクトを定義して Retrofit サービスを初期化します。このオブジェクトは、アプリの他の部分がアクセスできる公開シングルトン オブジェクトです。
object MarsApi {}
MarsApi
オブジェクト宣言内に、遅延初期化されるMarsApiService
型の Retrofit オブジェクト プロパティをretrofitService
という名前で追加します。この遅延初期化により、プロパティが最初に使用されるときに初期化されるようにします。エラーはこの後のステップで修正するので、無視してください。
object MarsApi {
val retrofitService : MarsApiService by lazy {}
}
MarsApiService
インターフェースでretrofit.create()
メソッドを使用して、retrofitService
変数を初期化します。
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
以上で Retrofit のセットアップは完了です。アプリが MarsApi.retrofitService
を呼び出すたびに、呼び出し元は、最初のアクセスで作成された MarsApiService
を実装する同一のシングルトン Retrofit オブジェクトにアクセスします。次のタスクでは、実装した Retrofit オブジェクトを使用します。
MarsViewModel でウェブサービスを呼び出す
このステップでは、getMarsPhotos()
メソッドを実装します。このメソッドは、REST サービスを呼び出して、返された JSON 文字列を処理します。
ViewModelScope
viewModelScope
は、アプリの ViewModel
ごとに定義される組み込みコルーチン スコープです。このスコープ内で起動されたすべてのコルーチンは、ViewModel
がクリアされると自動的にキャンセルされます。
viewModelScope
を使用すると、コルーチンを起動して、バックグラウンドでウェブサービス リクエストを送信できます。viewModelScope
は ViewModel
に属しているため、アプリで構成の変更が発生してもリクエストは続行されます。
MarsApiService.kt
ファイルで、getPhotos()
を非同期にして呼び出し元のスレッドをブロックしないようにするため、suspend 関数にします。この関数は、viewModelScope
内から呼び出すことができます。
@GET("photos")
suspend fun getPhotos(): String
ui/screens/MarsViewModel.kt
ファイルを開きます。下にスクロールしてgetMarsPhotos()
メソッドを表示します。ステータス レスポンスを"Set the Mars API Response here!"
に設定する行を削除して、メソッドgetMarsPhotos()
を空にします。
private fun getMarsPhotos() {}
getMarsPhotos()
内で、viewModelScope.launch
を使用してコルーチンを起動します。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
private fun getMarsPhotos() {
viewModelScope.launch {}
}
viewModelScope
内で、シングルトン オブジェクトMarsApi
を使用して、retrofitService
インターフェースのgetPhotos()
メソッドを呼び出します。返されたレスポンスを、listResult
という名前のval
に保存します。
import com.example.marsphotos.network.MarsApi
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
- バックエンド サーバーから受信した結果を
marsUiState
に割り当てます。marsUiState
は、最新のウェブ リクエストのステータスを表す可変状態オブジェクトです。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
- アプリを実行します。アプリはすぐに閉じ、エラー ポップアップが表示される場合もあれば、表示されない場合もあります。これはアプリがクラッシュしたことを示しています。
- Android Studio で [Logcat] タブをクリックし、ログ内に「
------- beginning of crash
」のような行で始まるエラーがあることを確認します。
--------- beginning of crash 22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher Process: com.example.android.marsphotos, PID: 22803 java.lang.SecurityException: Permission denied (missing INTERNET permission?) ...
このエラー メッセージは、アプリに INTERNET
権限がない可能性を示しています。次のタスクでは、この問題を解決するためにアプリにインターネット権限を追加する方法について説明します。
7. インターネット権限と例外処理を追加する
Android の権限
Android における権限の目的は、Android ユーザーのプライバシーを保護することです。Android アプリは、連絡先や通話履歴などの機密ユーザーデータと、カメラやインターネットなどの特定のシステム機能にアクセスする権限を宣言またはリクエストする必要があります。
アプリがインターネットにアクセスするには、INTERNET
権限が必要です。インターネットに接続すると、セキュリティ上の問題が生じるおそれがあります。そのため、アプリはデフォルトではインターネットに接続しません。アプリがインターネットにアクセスするには、その必要性を明示的に宣言する必要があります。この宣言は標準の権限と見なされます。Android の権限とその種類の詳細については、Android での権限をご覧ください。
このステップでは、AndroidManifest.xml
ファイルに <uses-permission>
タグを含めることにより、アプリが必要とする権限を宣言します。
manifests/AndroidManifest.xml
を開きます。次の行を<application>
タグの直前に追加します。
<uses-permission android:name="android.permission.INTERNET" />
- アプリをコンパイルして再度実行します。
インターネット接続が確立されていれば、火星の写真に関するデータを含む JSON テキストが表示されます。画像レコードごとに id
と img_src
が繰り返されていることに注目してください。JSON 形式については、この Codelab で後ほど詳しく説明します。
- デバイスまたはエミュレータの戻るボタンをタップして、アプリを閉じます。
例外処理
コードにはバグがあります。バグを表示するには、次の手順を実施します。
- ネットワーク接続エラーをシミュレートするため、デバイスまたはエミュレータを機内モードにします。
- 最近使ったアプリのメニューからアプリを再度開きます。または、Android Studio からアプリを実行します。
- Android Studio で [Logcat] タブをクリックし、次のような致命的な例外がログに含まれていることを確認します。
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.android.marsphotos, PID: 3302
このエラー メッセージは、アプリが接続を試行してタイムアウトしたことを示しています。実際のインターネット接続では、このような例外はよく発生します。権限の問題とは異なり、この種のエラーを修正することはできませんが、処理することはできます。次のステップでは、このような例外を処理する方法を学びます。
例外
例外とは、コンパイル時ではなく実行時に発生し、ユーザーへの通知なしに突然アプリを終了させるエラーです。例外はユーザー エクスペリエンスを低下させます。例外処理は、アプリが突然終了することを防ぎ、例外が発生する状況をユーザー フレンドリーな方法で処理するメカニズムす。
例外の原因には、ゼロ除算やネットワーク接続エラーなどの単純なものがあります。このような例外は、前の Codelab で説明した IllegalArgumentException
と似ています。
サーバーへの接続時に発生する可能性のある問題の例を次に示します。
- API で使用されている URL または URI が正しくない。
- サーバーが利用できないため、アプリがサーバーに接続できなかった。
- ネットワーク レイテンシに問題がある。
- デバイスのインターネット接続が不十分だったか、接続がなかった。
これらの例外はコンパイル時には処理できませんが、実行時には try-catch
ブロックを使用して処理できます。詳細な説明については、例外をご覧ください。
try-catch ブロックの構文例
try {
// some code that can cause an exception.
}
catch (e: SomeException) {
// handle the exception to avoid abrupt termination.
}
try
ブロック内に、例外を予測したコードを追加します。このアプリでは、それはネットワーク呼び出しです。catch
ブロック内に、アプリが突然終了することを防ぐコードを実装する必要があります。例外が発生した場合は、アプリを突然終了する代わりに、catch
ブロックを実行してエラーからの回復処理を行います。
getMarsPhotos()
のlaunch
ブロック内で、例外を処理するtry
ブロックを追加してMarsApi
呼び出しを囲みます。try
ブロックの後にcatch
ブロックを追加します。
import java.io.IOException
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
- アプリを再度実行します。今回はアプリがクラッシュしません。
状態 UI を追加する
MarsViewModel
クラスでは、最新のウェブ リクエストのステータスである marsUiState
が可変状態オブジェクトとして保存されます。しかし、このクラスには、読み込み、成功、失敗という別々のステータスを保存する機能がありません。
- 読み込みステータスは、アプリがデータを待機していることを示します。
- 成功ステータスは、データがウェブサービスから正常に取得されたことを示します。
- エラー ステータスは、ネットワーク エラーまたは接続エラーを示します。
アプリでこれら 3 つの状態を表すには、シール インターフェースを使用します。sealed interface
を使用すると、可能な値を制限することで、状態の管理が簡単になります。Mars Photos アプリでは、marsUiState
ウェブ レスポンスを、読み込み、成功、エラーという 3 つの状態(データクラス オブジェクト)に制限しています。これは次のようなコードになります。
// No need to copy over
sealed interface MarsUiState {
data class Success : MarsUiState
data class Loading : MarsUiState
data class Error : MarsUiState
}
上記のコード スニペットでは、レスポンスが成功したら、サーバーから火星の写真の情報を受け取ります。データを保存するには、Success
データクラスにコンストラクタ パラメータを追加します。
Loading
状態と Error
状態の場合、新しいデータを設定して新しいオブジェクトを作成する必要はありません。単にウェブ レスポンスを渡すだけです。data
クラスを Object
に変更して、ウェブ レスポンスのオブジェクトを作成します。
ui/MarsViewModel.kt
ファイルを開きます。import ステートメントの後にMarsUiState
シール インターフェースを追加します。これにより、MarsUiState
オブジェクトが網羅的な値を持つことができます。
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
object Error : MarsUiState
object Loading : MarsUiState
}
MarsViewModel
クラス内のmarsUiState
の定義を更新します。型をMarsUiState
に変更し、そのデフォルト値をMarsUiState.Loading
に設定します。marsUiState
への書き込みを保護するため、セッターを非公開にします。
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
- 下にスクロールして
getMarsPhotos()
メソッドを表示します。marsUiState
の値をMarsUiState.Success
に更新してlistResult
を渡します。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
catch
ブロック内で、エラー レスポンスを処理します。MarsUiState
をError
に設定します。
catch (e: IOException) {
marsUiState = MarsUiState.Error
}
marsUiState
の代入をtry-catch
ブロックの外に出すことができます。完成した関数のコードは次のようになります。
private fun getMarsPhotos() {
viewModelScope.launch {
marsUiState = try {
val listResult = MarsApi.retrofitService.getPhotos()
MarsUiState.Success(listResult)
} catch (e: IOException) {
MarsUiState.Error
}
}
}
screens/HomeScreen.kt
ファイルで、marsUiState
にwhen
式を追加します。marsUiState
がMarsUiState.Success
の場合は、ResultScreen
を呼び出してmarsUiState.photos
を渡します。エラーはとりあえず無視してください。
import androidx.compose.foundation.layout.fillMaxWidth
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Success -> ResultScreen(
marsUiState.photos, modifier = modifier.fillMaxWidth()
)
}
}
when
ブロック内に、MarsUiState.Loading
とMarsUiState.Error
のチェックを追加します。アプリにLoadingScreen
、ResultScreen
、ErrorScreen
の各コンポーザブルを表示させます。これらは後で実装します。
import androidx.compose.foundation.layout.fillMaxSize
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> ResultScreen(
marsUiState.photos, modifier = modifier.fillMaxWidth()
)
is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
}
}
res/drawable/loading_animation.xml
を開きます。このドローアブルは、中心点の周りで画像ドローアブル(loading_img.xml
)が回転するアニメーションです(アニメーションはプレビューでは確認できません)。
screens/HomeScreen.kt
ファイルのHomeScreen
コンポーザブルの下に、次のコンポーズ可能な関数LoadingScreen
を追加して、読み込み中を表すアニメーションを表示します。loading_img
ドローアブル リソースはスターター コードに含まれています。
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
Image(
modifier = modifier.size(200.dp),
painter = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.loading)
)
}
LoadingScreen
コンポーザブルの下に、次のコンポーズ可能な関数ErrorScreen
を追加して、アプリがエラー メッセージを表示できるようにします。
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
)
Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
}
}
- 機内モードをオンにして、アプリを再度実行します。今回は、アプリは突然終了せず、次のエラー メッセージを表示します。
- スマートフォンまたはエミュレータで機内モードをオフにします。アプリを実行してテストします。すべてが正しく動作し、JSON 文字列が表示されることを確認します。
8. kotlinx.serialization を使用して JSON レスポンスを解析する
JSON
通常、リクエストしたデータは、XML や JSON などの一般的なデータ形式のいずれかで書式設定されます。個々の呼び出しは構造化データを返すので、アプリはレスポンスからデータを読み取るためにデータの構造を認識している必要があります。
たとえば、このアプリは、サーバー https://android-kotlin-fun-mars-server.appspot.com/photos からデータを取得します。この URL をブラウザに入力すると、火星の地表の ID と画像 URL のリストが JSON 形式で表示されます。
サンプル JSON レスポンスの構造
JSON レスポンスの構造には、以下の特徴があります。
- JSON レスポンスは角かっこで囲まれた配列です。配列には JSON オブジェクトが含まれています。
- JSON オブジェクトは中かっこで囲まれています。
- 各 JSON オブジェクトには、カンマで区切られた Key-Value ペアのセットが含まれています。
- ペアのキーと値はコロンで区切られています。
- 名前は引用符で囲まれています。
- 値は、数値、文字列、ブール値、配列、オブジェクト(JSON オブジェクト)、null のいずれかです。
たとえば、img_src
は URL で、文字列です。この URL をウェブブラウザに貼り付けると、火星の地表の画像が表示されます。
このアプリでは、現在 Mars ウェブサービスから JSON レスポンスを取得しています。これはスタートとしては上々です。しかし、画像を表示するために実際に必要なのは Kotlin オブジェクトであり、長い JSON 文字列ではありません。このプロセスは「シリアル化解除」と呼ばれます。
「シリアル化」は、アプリによって使用されるデータを、ネットワーク経由で転送できる形式に変換するプロセスです。「シリアル化解除」は、「シリアル化」とは反対に、外部ソース(サーバーなど)からデータを読み取ってランタイム オブジェクトに変換するプロセスです。これらは両方とも、ネットワーク経由でデータを交換するアプリのほとんどに不可欠なコンポーネントです。
kotlinx.serialization
は、JSON 文字列を Kotlin オブジェクトに変換するライブラリ セットを備えています。コミュニティで開発されたサードパーティ ライブラリとして、Retrofit と連携する Kotlin Serialization Converter があります。
このタスクでは、kotlinx.serialization
ライブラリを使用して、ウェブサービスからの JSON レスポンスを解析し、火星の写真を表す有用な Kotlin オブジェクトに変換します。アプリを変更して、未加工の JSON を表示する代わりに、返された火星の写真の数を表示するようにします。
kotlinx.serialization
ライブラリの依存関係を追加する
build.gradle.kts (Module :app)
を開きます。plugins
ブロックにkotlinx serialization
プラグインを追加します。
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
dependencies
セクションで、kotlinx.serialization
の依存関係を含めるために次のコードを追加します。この依存関係により、Kotlin プロジェクトで JSON シリアル化が可能になります。
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
dependencies
ブロックで Retrofit スカラー コンバータの行を見つけて、kotlinx-serialization-converter
を使用するように変更します。
変更前のコード
// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
変更後のコード
// Retrofit with Kotlin serialization Converter
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
- [Sync Now] をクリックして、新しい依存関係でプロジェクトを再ビルドします。
MarsPhoto データクラスを実装する
ウェブサービスから取得した JSON レスポンスのサンプル エントリは、先ほど示した例と同様で、次のようになります。
[
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]
上記の例では、個々の火星写真エントリに次の JSON 形式の Key-Value ペアが含まれています。
id
: プロパティの ID(文字列)。引用符(" "
)でラップされているため、型はInteger
ではなくString
です。img_src
: 画像の URL(文字列)。
kotlinx.serialization は、この JSON データを解析して Kotlin オブジェクトに変換します。そのためには、解析結果を格納する Kotlin データクラスが kotlinx.serialization に存在する必要があります。このステップでは、データクラス MarsPhoto
を作成します。
- network パッケージを右クリックして、[New] > [Kotlin File/Class] を選択します。
- ダイアログで [Class] を選択し、クラスの名前として「
MarsPhoto
」と入力します。この操作により、MarsPhoto.kt
という名前の新しいファイルがnetwork
パッケージに作成されます。 - クラス定義の前に
data
キーワードを追加して、MarsPhoto
をデータクラスにします。 - 中かっこ
{}
をかっこ()
に変更します。データクラスには少なくとも 1 つのプロパティが定義されている必要があるため、この変更を行うとエラーになります。
data class MarsPhoto()
- 次のプロパティを
MarsPhoto
クラス定義に追加します。
data class MarsPhoto(
val id: String, val img_src: String
)
MarsPhoto
クラスをシリアル化可能にするため、@Serializable
アノテーションを付けます。
import kotlinx.serialization.Serializable
@Serializable
data class MarsPhoto(
val id: String, val img_src: String
)
MarsPhoto
クラスの変数はそれぞれ JSON オブジェクトのキー名に対応することに注目してください。特定の JSON レスポンスに含まれる型をマッチングするには、すべての値に対して String
オブジェクトを使用します。
kotlinx serialization
は、JSON を解析して名前でキーをマッチングし、データ オブジェクトに適切な値を入力します。
@SerialName アノテーション
JSON レスポンスのキー名に対応する Kotlin プロパティがわかりづらい場合や、推奨されるコーディング スタイルと一致しない場合があります。たとえば、JSON ファイルでは img_src
キーにアンダースコアを使用しますが、プロパティに関する Kotlin の命名規則では大文字と小文字(キャメルケース)を使用します。
JSON レスポンスのキー名と異なる変数名をデータクラスで使用するには、@SerialName
アノテーションを使用します。次の例では、データクラスの変数名は imgSrc
です。@SerialName(value = "img_src")
を使用して、変数を JSON 属性 img_src
にマッピングできます。
img_src
キーの行を下記の行に置き換えます。
import kotlinx.serialization.SerialName
@SerialName(value = "img_src")
val imgSrc: String
MarsApiService と MarsViewModel を更新する
このタスクでは、kotlinx.serialization
コンバータを使用して、JSON オブジェクトを Kotlin オブジェクトに変換します。
network/MarsApiService.kt
を開きます。ScalarsConverterFactory
の未解決の参照エラーに注目してください。これらのエラーの原因は、前のセクションで行った Retrofit の依存関係の変更です。ScalarConverterFactory
のインポートを削除します。その他のエラーは後で修正します。
削除する行:
import retrofit2.converter.scalars.ScalarsConverterFactory
retrofit
オブジェクト宣言で、ScalarConverterFactory
の代わりにkotlinx.serialization
を使用するように Retrofit ビルダーを変更します。
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType
private val retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(BASE_URL)
.build()
これで kotlinx.serialization
の配置が済んだので、Retrofit に対し、JSON 文字列を返すのではなく JSON 配列から MarsPhoto
オブジェクトのリストを返すようリクエストできます。
String
を返す代わりにMarsPhoto
オブジェクトのリストを返すように、Retrofit のMarsApiService
インターフェースを更新します。
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
viewModel
に同様の変更を加えます。MarsViewModel.kt
を開き、下にスクロールしてgetMarsPhotos()
メソッドを表示します。
getMarsPhotos()
メソッドの listResult
が List<MarsPhoto>
になり、String
ではなくなりました。このリストのサイズは、受信されて解析された写真の数です。
- 取得された写真の数を出力するため、
marsUiState
を次のように更新します。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
"Success: ${listResult.size} Mars photos retrieved"
)
- デバイスまたはエミュレータで機内モードがオフになっていることを確認します。アプリをコンパイルして実行します。
今回は、長い JSON 文字列ではなく、ウェブサービスから返されたプロパティの数を示すメッセージが表示されます。
9. 解答コード
この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。
$ 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 repo-starter
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab の解答コードを確認する場合は、GitHub で表示します。
10. まとめ
REST ウェブサービス
- ウェブサービスは、インターネット経由で提供されるソフトウェア ベースの機能です。アプリはウェブサービスにリクエストを送信することにより、データを取得できます。
- 一般的なウェブサービスは REST アーキテクチャを使用します。REST アーキテクチャを提供するウェブサービスを RESTful サービスと呼びます。RESTful ウェブサービスは、標準のウェブ コンポーネントとプロトコルを使用して構築されます。
- REST ウェブサービスへのリクエストは、URI を介する標準的な方法で行います。
- アプリでウェブサービスを使用するには、ネットワーク接続を確立してサービスと通信する必要があります。アプリは、レスポンス データを受信して解析し、アプリが使用できる形式に変換する必要があります。
- Retrofit ライブラリは、アプリが REST ウェブサービスにリクエストを送信することを可能にするクライアント ライブラリです。
- コンバータを使用して、ウェブサービスに送信するデータとウェブサービスから返されたデータをどのように処理するかを Retrofit に伝えます。たとえば、
ScalarsConverter
は、ウェブサービスのデータをString
またはその他のプリミティブとして扱います。 - アプリがインターネットに接続できるようにするには、Android マニフェストに
"android.permission.INTERNET"
権限を追加します。 - 遅延初期化では、オブジェクトの作成がそのオブジェクトが初めて使用されるときまで遅延されます。参照は作成されますが、オブジェクトは作成されません。オブジェクトが初めてアクセスされたときに参照が作成され、その後アクセスされるたびに使用されます。
JSON 解析
- 多くの場合、ウェブサービスからのレスポンスは、構造化データを表す一般的な形式である JSON で書式設定されます。
- JSON オブジェクトは Key-Value ペアのコレクションです。
- JSON オブジェクトのコレクションは JSON 配列です。ウェブサービスからのレスポンスは JSON 配列として取得されます。
- Key-Value ペアのキーは引用符で囲まれています。値は数値または文字列です。
- Kotlin では、別個のコンポーネントである kotlinx.serialization でデータのシリアル化ツールを使用できます。kotlinx.serialization は、JSON 文字列を Kotlin オブジェクトに変換するライブラリのセットを備えています。
- コミュニティで開発された Retrofit 用の Kotlin Serialization Converter ライブラリとして、retrofit2-kotlinx-serialization-converter があります。
- kotlinx.serialization は、JSON レスポンスのキーを、同じ名前を持つデータ オブジェクトのプロパティとマッチングします。
- 異なるプロパティ名をキーで使用するには、そのプロパティに
@SerialName
アノテーションを付けて JSON キーvalue
を指定します。
11. 関連リンク
Android デベロッパー ドキュメント:
Kotlin ドキュメント:
その他: