インターネットからデータを取得する

1. 始める前に

市販されている Android アプリの大半は、ネットワーク オペレーション(バックエンド サーバーからメール、メッセージ、またはその他の情報を取得するなど)を実行するために、インターネットに接続します。Gmail、YouTube、Google フォトは、インターネットに接続してユーザーデータを表示するアプリの例です。

この Codelab では、コミュニティで開発されたオープンソースのライブラリを使用して、バックエンド サーバーからデータを取得するデータレイヤを構築します。この方法は、データの取得を大幅に簡素化します。また、Android のベスト プラクティス(バックグラウンド スレッドでオペレーションを実行するなど)に沿ってアプリを構築するのに役立ちます。さらに、インターネットが低速な場合や利用できない場合は、エラー メッセージを表示します。これにより、ネットワーク接続に関する問題について常にユーザーに通知できます。

前提条件

  • コンポーズ可能な関数の作成方法についての基本的な知識。
  • Android アーキテクチャ コンポーネントである ViewModel の使用方法についての基本的な知識。
  • 長時間実行タスクでコルーチンを使用する方法についての基本的な知識。
  • build.gradle.kts に依存関係を追加する方法についての基本的な知識。

学習内容

  • REST ウェブサービスとは何か。
  • Retrofit ライブラリを使用し、インターネット上の REST ウェブサービスに接続してレスポンスを取得する方法。
  • シリアル化(kotlinx.serialization)ライブラリを使用し、JSON レスポンスを解析してデータ オブジェクトに変換する方法。

演習内容

  • スターター アプリに変更を加え、ウェブサービス API リクエストを送信してレスポンスを処理します。
  • Retrofit ライブラリを使用して、アプリにデータレイヤを実装します。
  • kotlinx.serializationkotlinx.serialization ライブラリを使用して、ウェブサービスからの JSON レスポンスを解析してアプリのデータ オブジェクトのリストに変換し、UI 状態にアタッチします。
  • コルーチンに対する Retrofit のサポートを使用して、コードを簡素化します。

必要なもの

  • Android Studio がインストールされているパソコン
  • Mars Photos アプリのスターター コード

2. アプリの概要

Mars Photos という名前のアプリを編集して、火星の地表の画像を表示します。このアプリは、ウェブサービスに接続して火星の写真を取得し、表示します。画像は、NASA の火星探査機が撮影した火星の実際の写真です。次の画像は、画像のグリッドを表示する最終的なアプリのスクリーンショットです。

68f4ff12cc1e2d81.png

この Codelab で作成するバージョンのアプリに、視覚的に凝ったところはありません。この Codelab では、アプリのデータレイヤの部分に焦点を絞っています。これは、インターネットに接続し、ウェブサービスを使用して未加工のプロパティ データをダウンロードする部分です。アプリがこのデータを正しく取得して解析できるようにするため、バックエンド サーバーから受信した写真の数を Text コンポーザブルに出力できます。

a59e55909b6e9213.png

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 リポジトリで確認できます。

スターター コードを実行する

  1. Android Studio で、ダウンロードしたプロジェクトを開きます。プロジェクトのフォルダ名は basic-android-kotlin-compose-training-mars-photos です。
  2. [Android] ペインで、[app] > [kotlin + java] を開きます。アプリに ui という名前のパッケージ フォルダがあることに注目してください。これがアプリの UI レイヤです。

de3d8666ecee9d1c.png

  1. アプリを実行します。アプリをコンパイルして実行すると、プレースホルダ テキストが中央にある次のような画面が表示されます。この Codelab の最後では、このプレースホルダ テキストを更新して、取得した写真の数に置き換えます。

95328ffbc9d7104b.png

スターター コードのチュートリアル

ここでは、プロジェクトの構造を把握します。プロジェクトに含まれる重要なファイルとフォルダの説明を以下に示します。

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 はデータレイヤと通信します。アプリの他の部分はこの実装に対して透過的です。

76551dbe9fc943aa.png

MarsViewModel は、火星写真データを取得するためにネットワーク呼び出しを行う役割を担います。ViewModel では、データが変更されたときに MutableState を使用してアプリの UI を更新します。

5. ウェブサービスと Retrofit

火星写真データはウェブサーバーに格納されています。このデータをアプリに取り込むには、インターネット上のサーバーとの接続を確立して通信する必要があります。

301162f0dca12fcf.png7ced9b4ca9c65af3.png

今日の多くのウェブサーバーは、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 を含むレスポンスをアプリに返します。

5bbeef4ded3e84cf.png83e8a6eb79249ebe.png

ウェブサービスからのレスポンスは、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 を指定する必要があります。このトピックについては、後で詳しく説明します。

26043df178401c6a.png

Retrofit の依存関係を追加する

Android Gradle を使用して、プロジェクトに外部ライブラリを追加できます。ライブラリの依存関係に加えて、ライブラリがホストされているリポジトリも含める必要があります。

  1. モジュール レベルの Gradle ファイル build.gradle.kts (Module :app) を開きます。
  2. 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 については後で学習します。

  1. [Sync Now] をクリックして、新しい依存関係でプロジェクトを再ビルドします。

6. インターネットに接続する

Retrofit ライブラリを使用して、Mars ウェブサービスと通信し、未加工の JSON レスポンスを String として表示します。プレースホルダ Text は、返された JSON レスポンス文字列か、または接続エラーを示すメッセージを表示します。

Retrofit は、ウェブサービスからのコンテンツに基づいて、アプリ用のネットワーク API を作成します。ウェブサービスからデータを取得し、データのデコード方法を認識している別個のコンバータ ライブラリを経由して、String のようなオブジェクトの形式でデータを返します。Retrofit には、XML や JSON などのよく使用されるデータ形式のサポートが組み込まれています。最後に Retrofit は、このサービスを呼び出して使用するコードを作成します。コードには、重要な詳細情報(バックグラウンド スレッドでのリクエストの実行など)が含まれています。

8c3a5c3249570e57.png

このタスクでは、ViewModel がウェブサービスとの通信に使用する Mars Photos プロジェクトにデータレイヤを追加します。Retrofit サービス API を実装するには、次の手順を実施します。

  • データソースである MarsApiService クラスを作成します。
  • ベース URL とコンバータ ファクトリを使用して、文字列を変換する Retrofit オブジェクトを作成します。
  • Retrofit がウェブサーバーと通信する方法を記述するインターフェースを作成します。
  • Retrofit サービスを作成し、API サービスのインスタンスをアプリの他の部分に公開します。

上記の手順を実装します。

  1. Android プロジェクト ペインでパッケージ com.example.marsphotos を右クリックし、[New] > [Package] を選択します。
  2. ポップアップで、提案されたパッケージ名の末尾に「network」を追加します。
  3. 新しいパッケージの下に新しい Kotlin ファイルを作成し、MarsApiService という名前を付けます。
  4. network/MarsApiService.kt を開きます。
  5. ウェブサービスのベース URL を表す次の定数を追加します。
private const val BASE_URL = 
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. この定数のすぐ下に、Retrofit オブジェクトを作成するための Retrofit ビルダーを追加します。
import retrofit2.Retrofit

private val retrofit = Retrofit.Builder()

Retrofit は、ウェブサービスのベース URI と、ウェブサービス API を構築するためのコンバータ ファクトリを必要とします。コンバータは、ウェブサービスから返されたデータをどのように処理するかを Retrofit に伝えます。この演習では、Retrofit はウェブサービスから JSON レスポンスを取得し、String として返す必要があります。Retrofit には、文字列とその他のプリミティブ型をサポートする ScalarsConverter があります。

  1. ScalarsConverterFactory のインスタンスを使用して、ビルダーで addConverterFactory() を呼び出します。
import retrofit2.converter.scalars.ScalarsConverterFactory

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. baseUrl() メソッドを使用して、ウェブサービスのベース URL を追加します。
  2. build() を呼び出して Retrofit オブジェクトを作成します。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. Retrofit ビルダーに対する呼び出しの下に、MarsApiService という名前のインターフェースを定義します。このインターフェースで、Retrofit が HTTP リクエストを使用してウェブサーバーと通信する方法を定義します。
interface MarsApiService {
}
  1. ウェブサービスからレスポンス文字列を取得するために、getPhotos() という名前の関数を MarsApiService インターフェースに追加します。
interface MarsApiService {    
    fun getPhotos()
}
  1. これが GET リクエストであることを Retrofit に伝えるために @GET アノテーションを使用し、そのウェブサービス メソッド用のエンドポイントを指定します。この演習では、エンドポイントは photos です。前のタスクで説明したように、この Codelab では /photos エンドポイントを使用します。
import retrofit2.http.GET


interface MarsApiService {
    @GET("photos") 
    fun getPhotos()
}

getPhotos() メソッドが呼び出されると、Retrofit はリクエストの開始に使用するベース URL(Retrofit ビルダーで定義した URL)の末尾にエンドポイント photos を追加します。

  1. 関数の戻り値の型を 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 つだけであるため、オブジェクト宣言を使用してアプリの他の部分にサービスを公開します。

  1. MarsApiService インターフェース宣言の外部で、MarsApi という公開オブジェクトを定義して Retrofit サービスを初期化します。このオブジェクトは、アプリの他の部分がアクセスできる公開シングルトン オブジェクトです。
object MarsApi {}
  1. MarsApi オブジェクト宣言内に、遅延初期化される MarsApiService 型の Retrofit オブジェクト プロパティを retrofitService という名前で追加します。この遅延初期化により、プロパティが最初に使用されるときに初期化されるようにします。エラーはこの後のステップで修正するので、無視してください。
object MarsApi {
    val retrofitService : MarsApiService by lazy {}
}
  1. 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 に属しているため、アプリで構成の変更が発生してもリクエストは続行されます。

  1. MarsApiService.kt ファイルで、getPhotos() を非同期にして呼び出し元のスレッドをブロックしないようにするため、suspend 関数にします。この関数は、viewModelScope 内から呼び出すことができます。
@GET("photos")
suspend fun getPhotos(): String
  1. ui/screens/MarsViewModel.kt ファイルを開きます。下にスクロールして getMarsPhotos() メソッドを表示します。ステータス レスポンスを "Set the Mars API Response here!" に設定する行を削除して、メソッド getMarsPhotos() を空にします。
private fun getMarsPhotos() {}
  1. getMarsPhotos() 内で、viewModelScope.launch を使用してコルーチンを起動します。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch


private fun getMarsPhotos() {
    viewModelScope.launch {}
}
  1. viewModelScope 内で、シングルトン オブジェクト MarsApi を使用して、retrofitService インターフェースの getPhotos() メソッドを呼び出します。返されたレスポンスを、listResult という名前の val に保存します。
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. バックエンド サーバーから受信した結果を marsUiState に割り当てます。marsUiState は、最新のウェブ リクエストのステータスを表す可変状態オブジェクトです。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
  1. アプリを実行します。アプリはすぐに閉じ、エラー ポップアップが表示される場合もあれば、表示されない場合もあります。これはアプリがクラッシュしたことを示しています。
  2. 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> タグを含めることにより、アプリが必要とする権限を宣言します。

  1. manifests/AndroidManifest.xml を開きます。次の行を <application> タグの直前に追加します。
<uses-permission android:name="android.permission.INTERNET" />
  1. アプリをコンパイルして再度実行します。

インターネット接続が確立されていれば、火星の写真に関するデータを含む JSON テキストが表示されます。画像レコードごとに id と img_src が繰り返されていることに注目してください。JSON 形式については、この Codelab で後ほど詳しく説明します。

b82ddb79eff61995.png

  1. デバイスまたはエミュレータの戻るボタンをタップして、アプリを閉じます。

例外処理

コードにはバグがあります。バグを表示するには、次の手順を実施します。

  1. ネットワーク接続エラーをシミュレートするため、デバイスまたはエミュレータを機内モードにします。
  2. 最近使ったアプリのメニューからアプリを再度開きます。または、Android Studio からアプリを実行します。
  3. 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 ブロックを実行してエラーからの回復処理を行います。

  1. getMarsPhotos() の launch ブロック内で、例外を処理する try ブロックを追加して MarsApi 呼び出しを囲みます。
  2. try ブロックの後に catch ブロックを追加します。
import java.io.IOException


viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}
  1. アプリを再度実行します。今回はアプリがクラッシュしません。

状態 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 に変更して、ウェブ レスポンスのオブジェクトを作成します。

  1. ui/MarsViewModel.kt ファイルを開きます。import ステートメントの後に MarsUiState シール インターフェースを追加します。これにより、MarsUiState オブジェクトが網羅的な値を持つことができます。
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. MarsViewModel クラス内の marsUiState の定義を更新します。型を MarsUiState に変更し、そのデフォルト値を MarsUiState.Loading に設定します。marsUiState への書き込みを保護するため、セッターを非公開にします。
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  1. 下にスクロールして getMarsPhotos() メソッドを表示します。marsUiState の値を MarsUiState.Success に更新して listResult を渡します。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
  1. catch ブロック内で、エラー レスポンスを処理します。MarsUiState を Error に設定します。
catch (e: IOException) {
   marsUiState = MarsUiState.Error
}
  1. marsUiState の代入を try-catch ブロックの外に出すことができます。完成した関数のコードは次のようになります。
private fun getMarsPhotos() {
   viewModelScope.launch {
       marsUiState = try {
           val listResult = MarsApi.retrofitService.getPhotos()
           MarsUiState.Success(listResult)
       } catch (e: IOException) {
           MarsUiState.Error
       }
   }
}
  1. 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()
        )
    }
}
  1. 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())
    }
}
  1. res/drawable/loading_animation.xml を開きます。このドローアブルは、中心点の周りで画像ドローアブル(loading_img.xml)が回転するアニメーションです(アニメーションはプレビューでは確認できません)。

92a448fa23b6d1df.png

  1. 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)
    )
}
  1. 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))
    }
}
  1. 機内モードをオンにして、アプリを再度実行します。今回は、アプリは突然終了せず、次のエラー メッセージを表示します。

28ba37928e0a9334.png

  1. スマートフォンまたはエミュレータで機内モードをオフにします。アプリを実行してテストします。すべてが正しく動作し、JSON 文字列が表示されることを確認します。

8. kotlinx.serialization を使用して JSON レスポンスを解析する

JSON

通常、リクエストしたデータは、XML や JSON などの一般的なデータ形式のいずれかで書式設定されます。個々の呼び出しは構造化データを返すので、アプリはレスポンスからデータを読み取るためにデータの構造を認識している必要があります。

たとえば、このアプリは、サーバー https://android-kotlin-fun-mars-server.appspot.com/photos からデータを取得します。この URL をブラウザに入力すると、火星の地表の ID と画像 URL のリストが JSON 形式で表示されます。

サンプル JSON レスポンスの構造

Key-Value と JSON オブジェクトの表示

JSON レスポンスの構造には、以下の特徴があります。

  • JSON レスポンスは角かっこで囲まれた配列です。配列には JSON オブジェクトが含まれています。
  • JSON オブジェクトは中かっこで囲まれています。
  • 各 JSON オブジェクトには、カンマで区切られた Key-Value ペアのセットが含まれています。
  • ペアのキーと値はコロンで区切られています。
  • 名前は引用符で囲まれています。
  • 値は、数値、文字列、ブール値、配列、オブジェクト(JSON オブジェクト)、null のいずれかです。

たとえば、img_src は URL で、文字列です。この URL をウェブブラウザに貼り付けると、火星の地表の画像が表示されます。

b4f9f196c64f02c3.png

このアプリでは、現在 Mars ウェブサービスから JSON レスポンスを取得しています。これはスタートとしては上々です。しかし、画像を表示するために実際に必要なのは Kotlin オブジェクトであり、長い JSON 文字列ではありません。このプロセスは「シリアル化解除」と呼ばれます。

「シリアル化」は、アプリによって使用されるデータを、ネットワーク経由で転送できる形式に変換するプロセスです。「シリアル化解除」は、「シリアル化」とは反対に、外部ソース(サーバーなど)からデータを読み取ってランタイム オブジェクトに変換するプロセスです。これらは両方とも、ネットワーク経由でデータを交換するアプリのほとんどに不可欠なコンポーネントです。

kotlinx.serialization は、JSON 文字列を Kotlin オブジェクトに変換するライブラリ セットを備えています。コミュニティで開発されたサードパーティ ライブラリとして、Retrofit と連携する Kotlin Serialization Converter があります。

このタスクでは、kotlinx.serialization ライブラリを使用して、ウェブサービスからの JSON レスポンスを解析し、火星の写真を表す有用な Kotlin オブジェクトに変換します。アプリを変更して、未加工の JSON を表示する代わりに、返された火星の写真の数を表示するようにします。

kotlinx.serialization ライブラリの依存関係を追加する

  1. build.gradle.kts (Module :app) を開きます。
  2. plugins ブロックに kotlinx serialization プラグインを追加します。
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
  1. dependencies セクションで、kotlinx.serialization の依存関係を含めるために次のコードを追加します。この依存関係により、Kotlin プロジェクトで JSON シリアル化が可能になります。
// Kotlin serialization 
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
  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")
  1. [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 を作成します。

  1. network パッケージを右クリックして、[New] > [Kotlin File/Class] を選択します。
  2. ダイアログで [Class] を選択し、クラスの名前として「MarsPhoto」と入力します。この操作により、MarsPhoto.kt という名前の新しいファイルが network パッケージに作成されます。
  3. クラス定義の前に data キーワードを追加して、MarsPhoto をデータクラスにします。
  4. 中かっこ {} をかっこ () に変更します。データクラスには少なくとも 1 つのプロパティが定義されている必要があるため、この変更を行うとエラーになります。
data class MarsPhoto()
  1. 次のプロパティを MarsPhoto クラス定義に追加します。
data class MarsPhoto(
    val id: String,  val img_src: String
)
  1. 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 にマッピングできます。

  1. img_src キーの行を下記の行に置き換えます。
import kotlinx.serialization.SerialName

@SerialName(value = "img_src") 
val imgSrc: String

MarsApiService と MarsViewModel を更新する

このタスクでは、kotlinx.serialization コンバータを使用して、JSON オブジェクトを Kotlin オブジェクトに変換します。

  1. network/MarsApiService.kt を開きます。
  2. ScalarsConverterFactory の未解決の参照エラーに注目してください。これらのエラーの原因は、前のセクションで行った Retrofit の依存関係の変更です。
  3. ScalarConverterFactory のインポートを削除します。その他のエラーは後で修正します。

削除する行:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 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 オブジェクトのリストを返すようリクエストできます。

  1. String を返す代わりに MarsPhoto オブジェクトのリストを返すように、Retrofit の MarsApiService インターフェースを更新します。
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}
  1. viewModel に同様の変更を加えます。MarsViewModel.kt を開き、下にスクロールして getMarsPhotos() メソッドを表示します。

getMarsPhotos() メソッドの listResult が List<MarsPhoto> になり、String ではなくなりました。このリストのサイズは、受信されて解析された写真の数です。

  1. 取得された写真の数を出力するため、marsUiState を次のように更新します。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. デバイスまたはエミュレータで機内モードがオフになっていることを確認します。アプリをコンパイルして実行します。

今回は、長い JSON 文字列ではなく、ウェブサービスから返されたプロパティの数を示すメッセージが表示されます。

a59e55909b6e9213.png

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 アーキテクチャを提供するウェブサービスを RESTfulRESTful サービスと呼びます。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.serializationkotlinx.serialization は、JSON レスポンスのキーを、同じ名前を持つデータ オブジェクトのプロパティとマッチングします。
  • 異なるプロパティ名をキーで使用するには、そのプロパティに @SerialName アノテーションを付けて JSON キー value を指定します。

11. 関連リンク

Android デベロッパー ドキュメント:

Kotlin ドキュメント:

その他: