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

一般に提供されているほとんどの 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 で作成されたサムネイル プロパティ画像のグリッドが表示されています。

6c26142b52c51285.png

この Codelab で作成するバージョンのアプリには、視覚的に凝ったところはありません。このバージョンは、インターネットに接続し、ウェブサービスを使用して未加工のプロパティ データをダウンロードする、アプリのネットワーク レイヤ部分に焦点を当てています。データの取得と解析が適切に行われたことを確認するため、バックエンド サーバーから受信した写真の数をテキストビューに出力するだけです。

e98a0641540fcb73.png

スターター コードをダウンロードする

この Codelab では、この Codelab で学んだ機能を使って拡張するためのスターター コードが提供されています。スターター コードには、以前の Codelab で学んだコードと学んでいないコードの両方が含まれている可能性があります。学んでいないコードの詳細については、この後の Codelab で説明します。

GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-mars-photos-app になります。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
  2. パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
  3. ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを表示して、アプリがどのように実装されているかを確認します。

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

  1. Android Studio で、ダウンロードしたプロジェクトを開きます。プロジェクトのフォルダ名は android-basics-kotlin-mars-photos-app です。スターター コードのフォルダ構造は下記のように表示されます。
  2. [Android] ペインで、[app] > [java] を展開します。アプリに overview という名前のパッケージ フォルダがあることに注目してください。これがアプリの UI レイヤです。

39027f91862361f1.png

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

4886b6b36023a53f.png

  1. ファイルを参照してスターター コードを理解してください。レイアウト ファイルの場合は、右上の [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 からの statusTextView にバインドします。

MainActivity.kt: このアクティビティの唯一のタスクは、アクティビティのレイアウト activity_main を読み込むことです。

layout/activity_main.xml: これは、fragment_overview を指す単一の FragmentContainerView を持つメイン アクティビティ レイアウトです。アプリが起動されると、Overview フラグメントがインスタンス化されます。

この Codelab では、バックエンド サーバーと通信して必要なデータを取得するネットワーク サービスのレイヤを作成します。この機能を実装するため、Retrofit というサードパーティ ライブラリを使用します。詳しくは、後ほど説明します。ViewModel はネットワーク レイヤと直接通信するので、アプリの他の部分はこの実装に対して透過的です。

d5a05ab8fd5ff011.png

OverviewViewModel は、火星写真データを取得するためにネットワーク呼び出しを行う役割を担います。ViewModel では、データが変更されたときにアプリ UI を更新するため、ライフサイクル対応のデータ バインディングで LiveData を使用します。

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

b3ab0ee52bfd791e.png

4a23a1ba3307b2a5.png

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

9fb57e255df97a4d.png

6da405d572445df9.png

通常、ウェブサービスからのレスポンスは、XML や JSON などの一般的なウェブ形式(Key-Value ペアで構造化データを表す形式)のいずれかで書式設定されます。JSON について詳しくは、この後のタスクをご覧ください。

このタスクでは、サーバーへのネットワーク接続を確立し、サーバーと通信して、JSON レスポンスを受信します。演習用にすでに作成されているバックエンド サーバーを使用します。この Codelab では、サードパーティ ライブラリである Retrofit ライブラリを使用して、バックエンド サーバーと通信します。

外部ライブラリ

外部ライブラリまたはサードパーティ ライブラリは、コア Android API の拡張機能に似ています。その多くはオープンソースであり、コミュニティで開発され、全世界の大規模な Android コミュニティからの共同の貢献によって維持されています。それらを利用して、Android デベロッパーはより優れたアプリを開発できます。

Retrofit ライブラリ

この Codelab で RESTful な Mars ウェブサービスと通信するために使用する Retrofit ライブラリは、適切なサポートとメンテナンスが行われているライブラリの良い例です。それを確認するには、GitHub ページにアクセスして、オープンされている問題(一部は機能リクエスト)とクローズされた問題をチェックします。デベロッパーが定期的に問題を解決し、機能リクエストに対応していれば、そのライブラリは適切に管理されており、アプリで使用する有力な候補になることがわかります。Retrofit には、ドキュメント ページもあります。

Retrofit ライブラリはバックエンドと通信します。アプリで渡すパラメータに基づいて、ウェブサービスの URI を作成します。詳しくは、この後のセクションで説明します。

c9e1034e86327abd.png

Retrofit の依存関係を追加する

Android Gradle を使用すると、プロジェクトに外部ライブラリを追加できます。ライブラリの依存関係に加えて、ライブラリがホストされているリポジトリも含める必要があります。Jetpack ライブラリの ViewModelLiveData などの Google ライブラリは、Google リポジトリでホストされています。ほとんどのコミュニティ ライブラリは、Retrofit のように JCenter でホストされています。

  1. プロジェクトの最上位レベルの build.gradle(Project: MarsPhotos) ファイルを開きます。repositories ブロックの下にリストされているリポジトリに注意してください。google()jcenter() の 2 つのリポジトリがあります。
repositories {
   google()
   jcenter()
}
  1. モジュール レベルの Gradle ファイル build.gradle (Module: MarsPhots.app) を開きます。
  2. 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 つのライブラリは連携して機能します。

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

Java 8 言語機能のサポートを追加する

Retrofit2 を含む多くのサードパーティ ライブラリは、Java 8 言語機能を使用します。Android Gradle プラグインは、特定の Java 8 言語機能を使用するための組み込みのサポートを提供します。

  1. 組み込みの機能を使用するには、モジュールの 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 は、このサービスを呼び出して使用するコードを作成します。コードには、バックグラウンド スレッドでリクエストを実行するなどの重要な詳細情報が含まれています。

64fe0b5717a31f71.png

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

  • ネットワーク レイヤ(MarsApiService クラス)を作成します。
  • ベース URL とコンバータ ファクトリを使用して、Retrofit オブジェクトを作成します。
  • Retrofit がウェブサーバーと通信する方法を記述するインターフェースを作成します。
  • Retrofit サービスを作成し、API サービスのインスタンスをアプリの他の部分に公開します。

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

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

メッセージが表示されたら、retrofit2.Retrofit をインポートします。

  1. Retrofit には、ウェブサービスのベース URI と、ウェブサービス API をビルドするためのコンバータ ファクトリが必要です。コンバータは、ウェブサービスから返されたデータをどのように処理するかを Retrofit に伝えます。この演習では、Retrofit はウェブサービスから JSON レスポンスを取得し、String として返す必要があります。Retrofit には、文字列およびその他のプリミティブ型をサポートする ScalarsConverter があるため、ScalarsConverterFactory のインスタンスを使ってビルダーで addConverterFactory() を呼び出します。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())

メッセージが表示されたら、retrofit2.converter.scalars.ScalarsConverterFactory をインポートします。

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

リクエストされたら、retrofit2.http.GET をインポートします。

  1. 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 つだけ必要とします。したがって、オブジェクト宣言を使用して、アプリの他の部分にサービスを公開します。

  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 オブジェクトを使用します。

OverviewViewModel でウェブサービスを呼び出す

このステップでは、Retrofit サービスを呼び出して、返された JSON 文字列を処理する getMarsPhotos() メソッドを実装します。

ViewModelScope

ViewModelScope は、アプリの ViewModel ごとに定義される組み込みコルーチン スコープです。このスコープ内で起動されたすべてのコルーチンは、ViewModel がクリアされると自動的にキャンセルされます。

ViewModelScope を使用してコルーチンを起動し、バックグラウンドで Retrofit ネットワーク呼び出しを実行します。

  1. MarsApiService で、getPhotos() を suspend 関数にします。これにより、コルーチン内からこのメソッドを呼び出すことが可能になります。
@GET("photos")
suspend fun getPhotos(): String
  1. overview/OverviewViewModel を開きます。下にスクロールして getMarsPhotos() メソッドを表示します。ステータス レスポンスを "Set the Mars API Response here!". に設定している行を削除します。これでメソッド getMarsPhotos() は空になります。
private fun getMarsPhotos() {

}
  1. getMarsPhotos() 内で、viewModelScope.launch を使用してコルーチンを起動します。
private fun getMarsPhotos() {
    viewModelScope.launch {
    }
}

メッセージが表示されたら、androidx.lifecycle.viewModelScopekotlinx.coroutines.launch をインポートします。

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

メッセージが表示されたら、com.example.android.marsphotos.network.MarsApi をインポートします。

  1. バックエンド サーバーから受信した結果を _status.value. に割り当てます。
 val listResult = MarsApi.retrofitService.getProperties()
 _status.value = 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 権限がない可能性を示しています。次のタスクでは、インターネット権限をアプリに追加して、この問題を解決します。

Android の権限

Android における権限の目的は、Android ユーザーのプライバシーを保護することです。Android アプリは、連絡先や通話履歴などの機密ユーザーデータと、特定のシステム機能(カメラやインターネット)にアクセスする権限を宣言またはリクエストする必要があります。

アプリがインターネットにアクセスするには、INTERNET 権限が必要です。インターネットに接続すると、セキュリティ上の問題が生じるおそれがあります。そのため、アプリはデフォルトではインターネットに接続しません。アプリがインターネットにアクセスするには、その必要性を明示的に宣言する必要があります。この権限は通常の権限と見なされます。Android の権限とその種類について詳しくは、ドキュメントをご覧ください。

このステップでは、AndroidManifest ファイルに <uses-permission> タグを含めることにより、アプリが必要とする権限を宣言します。

  1. manifests/AndroidManifest.xml を開きます。次の行を <application> タグの直前に追加します。
<uses-permission android:name="android.permission.INTERNET" />
  1. アプリをコンパイルして再度実行します。インターネット接続が成功すると、火星写真に関するデータを含む JSON テキストが表示されます。JSON 形式については、この後で詳しく説明します。

f7ba3feaf864d4cf.png

  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
    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 ブロックが実行され、アプリが突然終了する代わりにエラーからの回復処理が行われます。

  1. overview/OverviewViewModel.kt を開きます。下にスクロールして getMarsPhotos() メソッドを表示します。launch ブロック内に、MarsApi 呼び出しを囲む try ブロックを追加して、例外を処理できるようにします。try ブロックの後に catch ブロックを追加します。
viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       _status.value = listResult
   } catch (e: Exception) {

   }
}
  1. catch {} ブロック内で、エラー レスポンスを処理します。e.message_status.value に設定して、ユーザーにエラー メッセージを表示します。
catch (e: Exception) {
   _status.value = "Failure: ${e.message}"
}
  1. 機内モードをオンにして、アプリを再度実行します。今回は、アプリは突然終了せず、代わりにエラー メッセージが表示されます。

2fbc318b4fff2f34.png

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

JSON

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

たとえば、このアプリは、次のサーバーからデータを取得します: https://android-kotlin-fun-mars-server.appspot.com/photos。この URL をブラウザに入力すると、

火星表面の ID と画像 URL のリストが JSON 形式で表示されます。

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

68fdfa54410ee03e.png

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

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

17116bdeb21fec0d.png

これで、Mars ウェブサービスから JSON レスポンスを取得できます。スタートとしては上々です。しかし、実際に必要なのは Kotlin オブジェクトであり、長い JSON 文字列ではありません。そこで、Moshi という外部ライブラリを利用します。これは、JSON 文字列を Kotlin オブジェクトに変換する Android JSON パーサーです。Retrofit は Moshi と連携するコンバータを備えているため、ここでの目的に最適のライブラリです。

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

Moshi ライブラリの依存関係を追加する

  1. build.gradle(モジュール: app)を開きます。
  2. dependencies セクションに下記のコードを追加して、Moshi の依存関係を含めます。この依存関係により、Kotlin をサポートする Moshi JSON ライブラリのサポートが追加されます。
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
  1. 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'
  1. [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 を作成します。

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

  1. img_src キーの行を下記の行に置き換えます。リクエストされたら、com.squareup.moshi.Json をインポートします。
@Json(name = "img_src") val imgSrcUrl: String

MarsApiService と OverviewViewModel を更新する

このタスクでは、Retrofit ビルダーの場合と同様に、Moshi ビルダーを使用して Moshi オブジェクトを作成します。

ScalarsConverterFactoryKotlinJsonAdapterFactory に置き換え、Moshi を使用して JSON レスポンスを Kotlin オブジェクトに変換できることを Retrofit に知らせます。その後、Moshi オブジェクトを使用するようにネットワーク API と ViewModel を更新します。

  1. network/MarsApiService.kt を開きます。ScalarsConverterFactory の未解決の参照エラーに注意してください。これは、前のステップで行った Retrofit 依存関係の変更が原因です。ScalarConverterFactory のインポートを削除します。他のエラーはこの後すぐ修正します。

以下を削除します。

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. ファイルの先頭で、Retrofit ビルダーの直前に次のコードを追加して、Retrofit オブジェクトに似た Moshi オブジェクトを作成します。
private val moshi = Moshi.Builder()

リクエストされたら、com.squareup.moshi.Moshicom.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory をインポートします。

  1. Moshi のアノテーションを Kotlin で正しく動作させるため、Moshi ビルダーで KotlinJsonAdapterFactory を追加してから build() を呼び出します。
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()
  1. retrofit オブジェクト宣言で、ScalarConverterFactory の代わりに MoshiConverterFactory を使用するように Retrofit ビルダーを変更し、先ほど作成した moshi インスタンスを渡します。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

リクエストされたら、retrofit2.converter.moshi.MoshiConverterFactory をインポートします。

  1. MoshiConverterFactory を配置したので、JSON 文字列を返すのではなく、JSON 配列から MarsPhoto オブジェクトのリストを返すよう Retrofit にリクエストできます。MarsApiService インターフェースを更新して、Retrofit が String を返すのではなく、MarsPhoto オブジェクトのリストを返すようにします。
interface MarsApiService {
   @GET("photo")
   fun getPhotos(): List<MarsPhoto>
}
  1. viewModel に同様の変更を加えて、OverviewViewModel.kt を開きます。下にスクロールして getMarsPhotos() メソッドを表示します。
  2. getMarsPhotos() メソッドの listResult は、String ではなく List<MarsPhoto> になっています。リストのサイズは、受信されて解析された写真の数です。取得された写真の数を出力するには、次のように _status.value を更新します。
_status.value = "Success: ${listResult.size} Mars photos retrieved"

メッセージが表示されたら、com.example.android.marsphotos.network.MarsPhoto をインポートします。

  1. デバイスまたはエミュレータで機内モードがオフになっていることを確認します。アプリをコンパイルして実行します。今回は、長い JSON 文字列ではなく、ウェブサービスから返されたプロパティの数がメッセージに示されます。

7da53c64bd36fe74.png

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 ドキュメント:

その他: