一般に提供されているほとんどの Android アプリは、インターネットに接続してネットワーク オペレーションを行います。たとえば、バックエンド サーバーからメールやメッセージなどの情報を取得します。Gmail、YouTube、Google フォトは、インターネットに接続してユーザーデータを表示するアプリの例です。
この Codelab では、オープンソースの開発済みライブラリを使用してネットワーク レイヤを構築し、バックエンド サーバーからデータを取得します。この方法により、データ取得が大幅に簡素化され、Android のベスト プラクティス(バックグラウンド スレッドでのオペレーションの実行など)に沿ってアプリを構築できます。また、インターネットが低速な場合や利用できない場合は、アプリのユーザー インターフェースを更新して、ネットワーク接続の問題に関する情報を常にユーザーに提供するようにします。
前提となる知識
- フラグメントの作成方法と使用方法。
- Android アーキテクチャ コンポーネント
ViewModel
およびLiveData
の使用方法。 - Gradle ファイルに依存関係を追加する方法。
学習内容
- REST ウェブサービスとは何か。
- Retrofit ライブラリを使用して、インターネット上の REST ウェブサービスに接続し、レスポンスを取得する方法。
- Moshi ライブラリを使用して、JSON レスポンスを解析し、データ オブジェクトに変換する方法。
演習内容
- スターター アプリを変更して、ウェブサービス API リクエストを送信し、レスポンスを処理します。
- Retrofit ライブラリを使用して、アプリにネットワーク レイヤを実装します。
- Moshi ライブラリを使用して、ウェブサービスからの JSON レスポンスを解析し、アプリの
LiveData
オブジェクトに変換します。 - コルーチンに対する Retrofit のサポートを使用して、コードを簡素化します。
必要なもの
- Android Studio がインストールされているパソコン
- MarsPhotos アプリのスターター コード
この演習では、火星表面の画像を表示する MarsPhotos というスターター アプリを使用します。このアプリは、ウェブサービスに接続して火星の写真を取得し、表示します。画像は、NASA の火星探査機が撮影した火星の実際の写真です。最終的なアプリのスクリーンショットを次に示します。RecyclerView
で作成されたサムネイル プロパティ画像のグリッドが表示されています。
この Codelab で作成するバージョンのアプリには、視覚的に凝ったところはありません。このバージョンは、インターネットに接続し、ウェブサービスを使用して未加工のプロパティ データをダウンロードする、アプリのネットワーク レイヤ部分に焦点を当てています。データの取得と解析が適切に行われたことを確認するため、バックエンド サーバーから受信した写真の数をテキストビューに出力するだけです。
スターター コードをダウンロードする
この Codelab では、この Codelab で学んだ機能を使って拡張するためのスターター コードが提供されています。スターター コードには、以前の Codelab で学んだコードと学んでいないコードの両方が含まれている可能性があります。学んでいないコードの詳細については、この後の Codelab で説明します。
GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-mars-photos-app
になります。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開かれるまで待ちます。
- 実行ボタン をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを表示して、アプリがどのように実装されているかを確認します。
スターター コードを実行する
- Android Studio で、ダウンロードしたプロジェクトを開きます。プロジェクトのフォルダ名は
android-basics-kotlin-mars-photos-app
です。スターター コードのフォルダ構造は下記のように表示されます。 - [Android] ペインで、[app] > [java] を展開します。アプリに
overview
という名前のパッケージ フォルダがあることに注目してください。これがアプリの UI レイヤです。
- アプリを実行します。アプリをコンパイルして実行すると、プレースホルダ テキストが中央にある次のような画面が表示されます。この Codelab を終了する時点では、取得した写真の数を使ってこのプレースホルダ テキストを更新します。
- ファイルを参照してスターター コードを理解してください。レイアウト ファイルの場合は、右上の [Split] オプションを使用して、レイアウトと XML のプレビューを同時に表示できます。
スターター コードを確認する
このタスクでは、プロジェクトの構造を把握します。プロジェクト内の重要なファイルとフォルダを見ていきましょう。
OverviewFragment:
- これは、
MainActivity
内に表示されるフラグメントです。このフラグメントには、前のステップで見たプレースホルダ テキストが表示されます。 - 次の Codelab では、このフラグメントは火星写真のバックエンド サーバーから受信したデータを表示します。
- このクラスは、
OverviewViewModel
オブジェクトへの参照を保持します。 OverviewFragment
にはonCreateView()
関数があります。この関数は、データ バインディングを使用してfragment_overview
レイアウトをインフレートし、バインディング ライフサイクルのオーナーをそれ自身に設定し、バインディング オブジェクト内のviewModel
変数をそれに設定します。- ライフサイクルのオーナーが割り当てられているため、データ バインディングで使用されるすべての
LiveData
の変更が自動的に監視され、それに応じて UI が更新されます。
OverviewViewModel:
- これは、
OverviewFragment
に対応するビューモデルです。 - このクラスには、
_status
という名前のMutableLiveData
プロパティがそのバッキング プロパティとともに含まれています。このプロパティの値を更新すると、画面に表示されるプレースホルダ テキストが更新されます。 getMarsPhotos()
メソッドは、プレースホルダのレスポンスを更新します。Codelab の後半では、このメソッドを使用して、サーバーから取得されたデータを表示します。この Codelab の目標は、インターネットから取得した実際のデータを使用して、ViewModel
内のstatus
LiveData
を更新することです。
res/layout/fragment_overview.xml
:
- このレイアウトはデータ バインディングを使用するように設定されており、単一の
TextView
で構成されています。 OverviewViewModel
変数を宣言した後、ViewModel
からのstatus
をTextView
にバインドします。
MainActivity.kt
: このアクティビティの唯一のタスクは、アクティビティのレイアウト activity_main
を読み込むことです。
layout/activity_main.xml:
これは、fragment_overview
を指す単一の FragmentContainerView
を持つメイン アクティビティ レイアウトです。アプリが起動されると、Overview フラグメントがインスタンス化されます。
この Codelab では、バックエンド サーバーと通信して必要なデータを取得するネットワーク サービスのレイヤを作成します。この機能を実装するため、Retrofit というサードパーティ ライブラリを使用します。詳しくは、後ほど説明します。ViewModel
はネットワーク レイヤと直接通信するので、アプリの他の部分はこの実装に対して透過的です。
OverviewViewModel
は、火星写真データを取得するためにネットワーク呼び出しを行う役割を担います。ViewModel
では、データが変更されたときにアプリ UI を更新するため、ライフサイクル対応のデータ バインディングで LiveData
を使用します。
火星写真データはウェブサーバーに格納されます。このデータをアプリに取り込むには、インターネット上のサーバーとの接続を確立して通信する必要があります。
今日の多くのウェブサーバーは、REST と呼ばれる一般的なステートレス ウェブ アーキテクチャを使用してウェブサービスを実行します。REST とは、REpresentational State Transfer の略です。このアーキテクチャを提供するウェブサービスを、RESTful サービスと呼びます。
リクエストは、URI を介して標準化された方法で RESTful ウェブサービスに送信されます。URI(Uniform Resource Identifier)は、サーバー内のリソースを、場所やアクセス方法に関係なく、名前で識別します。たとえば、このレッスンのアプリでは、次のサーバー URI を使用して画像 URL を取得します(このサーバーは火星の不動産と火星の写真の両方をホストしています)。
android-kotlin-fun-mars-server.appspot.com
Uniform Resource Locator(URL)は、リソースの表現を操作または取得する手段を指定する 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 エンドポイントを使用します。
ウェブサービス リクエスト
個々のウェブサービス リクエストには URI が含まれており、Chrome などのウェブブラウザで使用されるのと同じ HTTP プロトコルを介してサーバーに転送されます。HTTP リクエストには、サーバーに何をすべきかを伝えるオペレーションが含まれています。
一般的な HTTP オペレーションには次のようなものがあります。
- GET: サーバーデータを取得する
- POST または PUT: サーバーに対して新しいデータの追加、作成、更新を行う
- DELETE: サーバーからデータを削除する
アプリは火星写真の情報を得るために HTTP GET リクエストをサーバーに送信し、サーバーは画像 URL を含むレスポンスをアプリに返します。
通常、ウェブサービスからのレスポンスは、XML や JSON などの一般的なウェブ形式(Key-Value ペアで構造化データを表す形式)のいずれかで書式設定されます。JSON について詳しくは、この後のタスクをご覧ください。
このタスクでは、サーバーへのネットワーク接続を確立し、サーバーと通信して、JSON レスポンスを受信します。演習用にすでに作成されているバックエンド サーバーを使用します。この Codelab では、サードパーティ ライブラリである Retrofit ライブラリを使用して、バックエンド サーバーと通信します。
外部ライブラリ
外部ライブラリまたはサードパーティ ライブラリは、コア Android API の拡張機能に似ています。その多くはオープンソースであり、コミュニティで開発され、全世界の大規模な Android コミュニティからの共同の貢献によって維持されています。それらを利用して、Android デベロッパーはより優れたアプリを開発できます。
Retrofit ライブラリ
この Codelab で RESTful な Mars ウェブサービスと通信するために使用する Retrofit ライブラリは、適切なサポートとメンテナンスが行われているライブラリの良い例です。それを確認するには、GitHub ページにアクセスして、オープンされている問題(一部は機能リクエスト)とクローズされた問題をチェックします。デベロッパーが定期的に問題を解決し、機能リクエストに対応していれば、そのライブラリは適切に管理されており、アプリで使用する有力な候補になることがわかります。Retrofit には、ドキュメント ページもあります。
Retrofit ライブラリはバックエンドと通信します。アプリで渡すパラメータに基づいて、ウェブサービスの URI を作成します。詳しくは、この後のセクションで説明します。
Retrofit の依存関係を追加する
Android Gradle を使用すると、プロジェクトに外部ライブラリを追加できます。ライブラリの依存関係に加えて、ライブラリがホストされているリポジトリも含める必要があります。Jetpack ライブラリの ViewModel や LiveData などの Google ライブラリは、Google リポジトリでホストされています。ほとんどのコミュニティ ライブラリは、Retrofit のように JCenter でホストされています。
- プロジェクトの最上位レベルの
build.gradle(Project: MarsPhotos)
ファイルを開きます。repositories
ブロックの下にリストされているリポジトリに注意してください。google()
とjcenter()
の 2 つのリポジトリがあります。
repositories {
google()
jcenter()
}
- モジュール レベルの Gradle ファイル
build.gradle (Module: MarsPhots.app)
を開きます。 dependencies
セクションに、Retrofit ライブラリ用に次の行を追加します。
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
最初の依存関係は Retrofit2 ライブラリ用です。2 番目の依存関係は Retrofit のスカラー コンバータ用です。このコンバータにより、Retrofit は JSON 結果を String
として返すことができます。2 つのライブラリは連携して機能します。
- [Sync Now] をクリックして、新しい依存関係でプロジェクトを再ビルドします。
Java 8 言語機能のサポートを追加する
Retrofit2 を含む多くのサードパーティ ライブラリは、Java 8 言語機能を使用します。Android Gradle プラグインは、特定の Java 8 言語機能を使用するための組み込みのサポートを提供します。
- 組み込みの機能を使用するには、モジュールの
build.gradle
ファイルに次のコードを追加する必要があります。この手順はすでに完了しています。build.gradle(Module: MarsPhotos.app).
に次のコードがあることを確認してください。
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
Retrofit ライブラリを使用して、Mars ウェブサービスと通信し、未加工の JSON レスポンスを String
として表示します。プレースホルダ TextView
は、返された JSON レスポンス文字列か、または接続エラーを示すメッセージを表示します。
Retrofit は、ウェブサービスからのコンテンツに基づいて、アプリ用のネットワーク API を作成します。ウェブサービスからデータを取得し、データのデコード方法を認識している別個のコンバータ ライブラリを経由して、String
のようなオブジェクトの形式でデータを返します。Retrofit には、XML や JSON などの一般的なデータ形式のサポートが組み込まれています。最終的に Retrofit は、このサービスを呼び出して使用するコードを作成します。コードには、バックグラウンド スレッドでリクエストを実行するなどの重要な詳細情報が含まれています。
このタスクでは、ViewModel
がウェブサービスとの通信に使用するネットワーク レイヤを MarsPhotos プロジェクトに追加します。Retrofit サービス API を実装するには、次の手順を実施します。
- ネットワーク レイヤ(
MarsApiService
クラス)を作成します。 - ベース URL とコンバータ ファクトリを使用して、Retrofit オブジェクトを作成します。
- Retrofit がウェブサーバーと通信する方法を記述するインターフェースを作成します。
- Retrofit サービスを作成し、API サービスのインスタンスをアプリの他の部分に公開します。
上記の手順を実装します。
- network という名前の新しいパッケージを作成します。Android プロジェクト ペインで、パッケージ
com.example.android.marsphotos
を右クリックします。[New] > [Package] を選択します。ポップアップで、提案されたパッケージ名の末尾に「network」を追加します。 - 新しいパッケージ network の下に新しい Kotlin ファイルを作成します。
MarsApiService.
という名前を付けます。 network/MarsApiService.kt
を開きます。ウェブサービスのベース URL に次の定数を追加します。
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- その定数のすぐ下に、Retrofit オブジェクトをビルドして作成する Retrofit ビルダーを追加します。
private val retrofit = Retrofit.Builder()
メッセージが表示されたら、retrofit2.Retrofit
をインポートします。
- Retrofit には、ウェブサービスのベース URI と、ウェブサービス API をビルドするためのコンバータ ファクトリが必要です。コンバータは、ウェブサービスから返されたデータをどのように処理するかを Retrofit に伝えます。この演習では、Retrofit はウェブサービスから JSON レスポンスを取得し、
String
として返す必要があります。Retrofit には、文字列およびその他のプリミティブ型をサポートするScalarsConverter
があるため、ScalarsConverterFactory
のインスタンスを使ってビルダーでaddConverterFactory()
を呼び出します。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
メッセージが表示されたら、retrofit2.converter.scalars.ScalarsConverterFactory
をインポートします。
baseUrl()
メソッドを使用して、ウェブサービスのベース URI を追加します。最後に、build()
を呼び出して Retrofit オブジェクトを作成します。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- Retrofit ビルダーの呼び出しの下に、
MarsApiService
という名前のインターフェースを定義します。このインターフェースでは、Retrofit が HTTP リクエストを使用してウェブサーバーと通信する方法を定義します。
interface MarsApiService {
}
MarsApiService
インターフェース内にgetPhotos()
という関数を追加して、ウェブサービスからレスポンス文字列を取得します。
interface MarsApiService {
fun getPhotos()
}
@GET
アノテーションを使用して、これが GET リクエストであることを Retrofit に伝え、そのウェブサービス メソッド用のエンドポイントを指定します。ここでのエンドポイントの名前はphotos
です。前のタスクで説明したように、この Codelab では /photos エンドポイントを使用します。
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
リクエストされたら、retrofit2.http.GET
をインポートします。
getPhotos()
メソッドが呼び出されると、Retrofit はリクエストの開始に使用するベース URL(Retrofit ビルダーで定義した URL)の末尾にエンドポイントphotos
を追加します。関数の戻り値の型をString
に追加します。
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
オブジェクト宣言
Kotlin では、シングルトン オブジェクトの宣言にオブジェクト宣言を使用します。シングルトン パターンを使用すると、オブジェクトのインスタンスが 1 つだけ作成され、そのオブジェクトへのグローバル アクセス ポイントが 1 つであることが保証されます。オブジェクト宣言の初期化はスレッドセーフで、最初のアクセスの際に行われます。
Kotlin では、シングルトンを簡単に宣言できます。オブジェクト宣言とそれに対するアクセスの例を次に示します。オブジェクト宣言では、常に object
キーワードの後に名前を指定します。
例:
// Object declaration
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)
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 オブジェクトを使用します。
OverviewViewModel でウェブサービスを呼び出す
このステップでは、Retrofit サービスを呼び出して、返された JSON 文字列を処理する getMarsPhotos()
メソッドを実装します。
ViewModelScope
ViewModelScope
は、アプリの ViewModel
ごとに定義される組み込みコルーチン スコープです。このスコープ内で起動されたすべてのコルーチンは、ViewModel
がクリアされると自動的にキャンセルされます。
ViewModelScope
を使用してコルーチンを起動し、バックグラウンドで Retrofit ネットワーク呼び出しを実行します。
MarsApiService
で、getPhotos()
を suspend 関数にします。これにより、コルーチン内からこのメソッドを呼び出すことが可能になります。
@GET("photos")
suspend fun getPhotos(): String
overview/OverviewViewModel
を開きます。下にスクロールしてgetMarsPhotos()
メソッドを表示します。ステータス レスポンスを"Set the Mars API Response here!".
に設定している行を削除します。これでメソッドgetMarsPhotos()
は空になります。
private fun getMarsPhotos() {
}
getMarsPhotos()
内で、viewModelScope.launch
を使用してコルーチンを起動します。
private fun getMarsPhotos() {
viewModelScope.launch {
}
}
メッセージが表示されたら、androidx.lifecycle.
viewModelScope
と kotlinx.coroutines.launch
をインポートします。
viewModelScope
内で、シングルトン オブジェクトMarsApi
を使用して、retrofitService
インターフェースのgetPhotos()
メソッドを呼び出します。返されたレスポンスを、listResult
という名前のval
に保存します。
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
メッセージが表示されたら、com.example.android.marsphotos.network.MarsApi
をインポートします。
- バックエンド サーバーから受信した結果を
_status.value.
に割り当てます。
val listResult = MarsApi.retrofitService.getProperties()
_status.value = 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
権限がない可能性を示しています。次のタスクでは、インターネット権限をアプリに追加して、この問題を解決します。
Android の権限
Android における権限の目的は、Android ユーザーのプライバシーを保護することです。Android アプリは、連絡先や通話履歴などの機密ユーザーデータと、特定のシステム機能(カメラやインターネット)にアクセスする権限を宣言またはリクエストする必要があります。
アプリがインターネットにアクセスするには、INTERNET
権限が必要です。インターネットに接続すると、セキュリティ上の問題が生じるおそれがあります。そのため、アプリはデフォルトではインターネットに接続しません。アプリがインターネットにアクセスするには、その必要性を明示的に宣言する必要があります。この権限は通常の権限と見なされます。Android の権限とその種類について詳しくは、ドキュメントをご覧ください。
このステップでは、AndroidManifest
ファイルに <uses-permission>
タグを含めることにより、アプリが必要とする権限を宣言します。
manifests/AndroidManifest.xml
を開きます。次の行を<application>
タグの直前に追加します。
<uses-permission android:name="android.permission.INTERNET" />
- アプリをコンパイルして再度実行します。インターネット接続が成功すると、火星写真に関するデータを含む JSON テキストが表示されます。JSON 形式については、この後で詳しく説明します。
- デバイスまたはエミュレータの戻るボタンをタップして、アプリを閉じます。
- デバイスまたはエミュレータを機内モードにして、ネットワーク接続エラーをシミュレートします。最近使ったアプリのメニューからアプリを再度開きます。または、Android Studio からアプリを再起動します。
- Android Studio で [Logcat] タブをクリックします。次のような致命的な例外がログに含まれていることを確認します。
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.android.marsphotos, PID: 3302 java.net.SocketTimeoutException: timeout ...
このエラー メッセージは、アプリが接続を試行してタイムアウトしたことを示しています。実際のインターネット接続では、このような例外はよく発生します。次のステップでは、このような例外を処理する方法を学びます。
例外処理
例外とは、(コンパイル時ではなく)実行時に発生し、ユーザーへの通知なしに突然アプリを終了させるエラーです。例外はユーザー エクスペリエンスを低下させます。例外処理は、アプリが突然終了することを防ぎ、ユーザー フレンドリーな方法で例外を処理するメカニズムです。
例外の原因としては、単純なゼロ除算やネットワーク内のエラーが考えられます。この種の例外は、前の Codelab で学んだ NumberFormatException
と似ています。
サーバーへの接続時に発生する問題の例:
- 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
ブロックが実行され、アプリが突然終了する代わりにエラーからの回復処理が行われます。
overview/OverviewViewModel.kt
を開きます。下にスクロールしてgetMarsPhotos()
メソッドを表示します。launch ブロック内に、MarsApi
呼び出しを囲むtry
ブロックを追加して、例外を処理できるようにします。try
ブロックの後にcatch
ブロックを追加します。
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = listResult
} catch (e: Exception) {
}
}
catch {}
ブロック内で、エラー レスポンスを処理します。e.message
を_status.
value
に設定して、ユーザーにエラー メッセージを表示します。
catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
- 機内モードをオンにして、アプリを再度実行します。今回は、アプリは突然終了せず、代わりにエラー メッセージが表示されます。
- スマートフォンまたはエミュレータで機内モードをオフにします。アプリを実行してテストします。すべてが正常に機能し、JSON 文字列が表示されることを確認します。
JSON
通常、リクエストしたデータは、XML や JSON などの一般的なデータ形式のいずれかで書式設定されます。個々の呼び出しは構造化データを返すので、アプリはレスポンスからデータを読み取るためにデータの構造を認識する必要があります。
たとえば、このアプリは、次のサーバーからデータを取得します: https://android-kotlin-fun-mars-server.appspot.com/photos。この URL をブラウザに入力すると、
火星表面の ID と画像 URL のリストが JSON 形式で表示されます。
サンプル JSON レスポンスの構造:
- JSON レスポンスは、角かっこで示されている配列です。配列には JSON オブジェクトが含まれています。
- JSON オブジェクトは中かっこで囲まれています。
- 各 JSON オブジェクトには、名前と値のペアのセットが含まれています。名前と値はコロンで区切られています。
- 名前は引用符で囲まれています。
- 値は、数値、文字列、ブール値、配列、オブジェクト(JSON オブジェクト)、null のいずれかです。
たとえば、img_src
は URL で、文字列です。この URL をウェブブラウザに貼り付けると、火星表面の画像が表示されます。
これで、Mars ウェブサービスから JSON レスポンスを取得できます。スタートとしては上々です。しかし、実際に必要なのは Kotlin オブジェクトであり、長い JSON 文字列ではありません。そこで、Moshi という外部ライブラリを利用します。これは、JSON 文字列を Kotlin オブジェクトに変換する Android JSON パーサーです。Retrofit は Moshi と連携するコンバータを備えているため、ここでの目的に最適のライブラリです。
このタスクでは、Moshi ライブラリと Retrofit を併用して、ウェブサービスからの JSON レスポンスを解析し、火星写真を表す有用な Kotlin オブジェクトに変換します。アプリを変更して、未加工の JSON を表示する代わりに、返された火星写真の数を表示するようにします。
Moshi ライブラリの依存関係を追加する
- build.gradle(モジュール: app)を開きます。
- dependencies セクションに下記のコードを追加して、Moshi の依存関係を含めます。この依存関係により、Kotlin をサポートする Moshi JSON ライブラリのサポートが追加されます。
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
dependencies
ブロックで Retrofit スカラー コンバータの行を見つけ、converter-moshi
を使用するように依存関係を変更します。
変更前
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
変更後
// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
- [Sync Now] をクリックして、新しい依存関係でプロジェクトを再ビルドします。
火星写真データクラスを実装する
ウェブサービスから取得した JSON レスポンスのサンプル エントリは次のようになります。これは前に見たものと似ています。
[{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]
上記の例では、個々の火星写真エントリに次の JSON 形式の Key-Value ペアが含まれています。
id
: プロパティの ID(文字列)。" "
でラップされているため、型はString
ではなくInteger
です。img_src
: 画像の URL(文字列)。
Moshi は、この JSON データを解析して Kotlin オブジェクトに変換します。そのためには、Moshi は解析結果を格納する Kotlin データクラスを持つ必要があります。そこで、このステップではデータクラス 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
クラスの各変数は、JSON オブジェクトのキー名に対応することに注意してください。特定の JSON レスポンスに含まれる型をマッチングするには、すべての値に対して String
オブジェクトを使用します。
Moshi は JSON を解析して名前でキーをマッチングし、データ オブジェクトに適切な値を入力します。
@Json アノテーション
JSON レスポンスのキー名に対応する Kotlin プロパティがわかりづらい場合や、推奨されるコーディング スタイルと一致しない場合があります。たとえば、JSON ファイルでは img_src
キーにアンダースコアを使用しますが、プロパティに関する Kotlin の命名規則では大文字と小文字(キャメルケース)を使用します。
JSON レスポンスのキー名とは異なるデータクラスの変数名を使用するには、@Json
アノテーションを使用します。この例では、データクラスの変数名は imgSrcUrl
です。@Json(name = "img_src")
を使用して、変数を JSON 属性 img_src
にマッピングできます。
img_src
キーの行を下記の行に置き換えます。リクエストされたら、com.squareup.moshi.Json
をインポートします。
@Json(name = "img_src") val imgSrcUrl: String
MarsApiService と OverviewViewModel を更新する
このタスクでは、Retrofit ビルダーの場合と同様に、Moshi ビルダーを使用して Moshi オブジェクトを作成します。
ScalarsConverterFactory
を KotlinJsonAdapterFactory
に置き換え、Moshi を使用して JSON レスポンスを Kotlin オブジェクトに変換できることを Retrofit に知らせます。その後、Moshi オブジェクトを使用するようにネットワーク API と ViewModel
を更新します。
network/MarsApiService.kt
を開きます。ScalarsConverterFactory
の未解決の参照エラーに注意してください。これは、前のステップで行った Retrofit 依存関係の変更が原因です。ScalarConverterFactory
のインポートを削除します。他のエラーはこの後すぐ修正します。
以下を削除します。
import retrofit2.converter.scalars.ScalarsConverterFactory
- ファイルの先頭で、Retrofit ビルダーの直前に次のコードを追加して、Retrofit オブジェクトに似た Moshi オブジェクトを作成します。
private val moshi = Moshi.Builder()
リクエストされたら、com.squareup.moshi.Moshi
と com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
をインポートします。
- Moshi のアノテーションを Kotlin で正しく動作させるため、Moshi ビルダーで
KotlinJsonAdapterFactory
を追加してからbuild()
を呼び出します。
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
retrofit
オブジェクト宣言で、ScalarConverterFactory
の代わりにMoshiConverterFactory
を使用するように Retrofit ビルダーを変更し、先ほど作成したmoshi
インスタンスを渡します。
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
リクエストされたら、retrofit2.converter.moshi.MoshiConverterFactory
をインポートします。
MoshiConverterFactory
を配置したので、JSON 文字列を返すのではなく、JSON 配列からMarsPhoto
オブジェクトのリストを返すよう Retrofit にリクエストできます。MarsApiService
インターフェースを更新して、Retrofit がString
を返すのではなく、MarsPhoto
オブジェクトのリストを返すようにします。
interface MarsApiService {
@GET("photo")
fun getPhotos(): List<MarsPhoto>
}
viewModel
に同様の変更を加えて、OverviewViewModel.kt
を開きます。下にスクロールしてgetMarsPhotos()
メソッドを表示します。getMarsPhotos()
メソッドのlistResult
は、String
ではなくList<MarsPhoto>
になっています。リストのサイズは、受信されて解析された写真の数です。取得された写真の数を出力するには、次のように_status.
value
を更新します。
_status.value = "Success: ${listResult.size} Mars photos retrieved"
メッセージが表示されたら、com.example.android.marsphotos.network.MarsPhoto
をインポートします。
- デバイスまたはエミュレータで機内モードがオフになっていることを確認します。アプリをコンパイルして実行します。今回は、長い JSON 文字列ではなく、ウェブサービスから返されたプロパティの数がメッセージに示されます。
build.gradle(Module : MarsPhotos.app)
以下の新しい依存関係が含まれているはずです。
dependencies { ... // Moshi implementation 'com.squareup.moshi:moshi-kotlin:1.9.3' // Retrofit with Moshi Converter implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' ... }
Manifests/AndroidManifest.xml
下記のコード スニペットから、インターネット権限を表す <uses-permission..>
コードを追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.marsphotos">
<!-- In order for our app to access the Internet, we need to define this permission. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</application>
</manifest>
network/MarsPhoto.kt
package com.example.android.marsphotos.network
import com.squareup.moshi.Json
/**
* This data class defines a Mars photo which includes an ID, and the image URL.
* The property names of this data class are used by Moshi to match the names of values in JSON.
*/
data class MarsPhoto(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String
)
network/MarsApiService.kt
package com.example.android.marsphotos.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
/**
* Build the Moshi object with Kotlin adapter factory that Retrofit will be using.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* The Retrofit object with the Moshi converter.
*/
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
/**
* A public interface that exposes the [getPhotos] method
*/
interface MarsApiService {
/**
* Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
* The @GET annotation indicates that the "photos" endpoint will be requested with the GET
* HTTP method
*/
@GET("photos")
suspend fun getPhotos() : List<MarsPhoto>
}
/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object MarsApi {
val retrofitService: MarsApiService by lazy { retrofit.create(MarsApiService::class.java) }
}
Overview/OverviewViewModel.kt
package com.example.android.marsphotos.overview
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch
/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {
// The internal MutableLiveData that stores the status of the most recent request
private val _status = MutableLiveData<String>()
// The external immutable LiveData for the request status
val status: LiveData<String> = _status
/**
* Call getMarsPhotos() on init so we can display status immediately.
*/
init {
getMarsPhotos()
}
/**
* Gets Mars photos information from the Mars API Retrofit service and updates the
* [MarsPhoto] [List] [LiveData].
*/
private fun getMarsPhotos() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = "Success: ${listResult.size} Mars photos retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
}
}
}
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 ペアのキーは引用符で囲まれています。値は数値または文字列です。
- Moshi ライブラリは、JSON 文字列を Kotlin オブジェクトに変換する Android JSON パーサーです。Retrofit は Moshi と連携するコンバータを備えています。
- Moshi は、JSON レスポンスのキーを、同じ名前を持つデータ オブジェクトのプロパティとマッチングします。
- 異なる名前のプロパティをキーに使用するには、そのプロパティに
@Json
アノテーションと JSON キー名を付けます。
Android デベロッパー ドキュメント:
Kotlin ドキュメント:
その他: