Android Kotlin基礎講座 08.2:インターネットから画像をロードし表示する

タスク:RecyclerViewにエラー処理を追加する

MarsRealEstateアプリは画像が読み込めなかった場合、破損画像のアイコンを表示します。しかしネットワーク接続がない場合、アプリは空白の画面を表示します。

これはベストなユーザーエクスペリエンスとは言えません。このタスクでは、ユーザーに何が起こっているのかをより分かりやすく伝えるための基本的なエラー処理を追加します。インターネットが利用可能でない際には、アプリは接続エラーアイコンを表示します。MarsPropertyリストの読み込み中には読み込み中アニメーションが表示されます。

ステップ1:ビューモデルに状態を追加する

初めに、ビューモデル内にウェブリクエストの状態を表すためのLiveDataを作成します。考えられる状態として、読み込み中、成功、失敗の三つの状態があります。読み込み中の状態はawait()の呼び出し内のデータを待機している間にことを意味します。

  1. overview/OverviewViewModel.ktを開いてください。ファイルのトップ(インポート文の後、クラス定義の前)に全ての状態を表すためのenumを追加してください。
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. OverviewViewModelクラス全体の内部および外部の_response LiveDataの名前を全て_statusに変更してください。この記事の前半で、_peroperties LiveDataを追加したので、完全なウェブサービスレスポンスはもう使われません。ここでは現在の状態をトラックするためにLiveDataが必要になるので、既存の変数を改名するだけで大丈夫です。

またString型からMarsApiStatus型に変更してください。

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. getMarsRealEstateProperties()メソッドまでスクロールし、ここも同様に_responseを_statusに変更してください。”Success” stringをMarsApiStatus.DONEに変更し、”Failure”はMarsApiStatus.ERRORに変更してください。
  2. try{}ブロックの前でstatusにMarsApiStaus.LOADINGをセットしてください。これはコルーチンが実行され、データを待っている間の内部状態です。最終的なtry/catch{}ブロックは以下のようになります。
_status.value = MarsApiStatus.LOADING
try {
   _properties.value = MarsApi.retrofitService.getProperties()
   _status.value = MarsApiStatus.DONE
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. catch{]ブロック内のエラー状態の後で、_properties LiveDataに空のリストをセットしてください。これによってRecyclerViewがクリアされます。
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

ステップ2:状態を表すImageView用のバインディングアダプターを追加する

ここまででビューモデル内に状態を持つことができましたが、これは状態をセットしたに過ぎません。これはアプリそのものに出現させるにはどうしたらよいでしょうか。このステップでは、読み込み中とエラー状態を表すアイコンを表示するために、データバインディングに接続されたImageViewを使います。
アプリが読み込み状態、またはエラー状態であるとき、ImageViewは可視化されます。アプリが読み込みを完了したとき、ImageViewは見えなくなります。

  1. BindingAdapters.ktを開いてください。ImageViewとMarsApiStatusを引数にとるbindStaus()というバインディングアダプターを追加してください。
    要求されたら、com.example.android.marsrealestate.overview.MarsApiStatusをインポートしてください。
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. bindStatus()メソッド内にwhen{]を追加し、状態によって処理を切り替えられるようにします。
when (status) {

}
  1. when{}の中に読み込み中状態(MarsApiStaus.LOADING)の場合の処理を追加します。この状態のときには、ImageViewを可視化し、それに読み込み中アニメーションを代入してください。これは前のタスクのGlide用に用いたアニメーション画像と同じものです。
    要求されたら、android.view.Viewをインポートしてください。
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. エラー状態(MarsApiStatus.ERROR)の場合の処理も追加します。読み込み中状態用に行ったことと同じく、ImageViewを可視化し、接続エラー画像を再利用しましょう。
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 完了状態(MarsApiStatus.Done)の場合の処理も追加します。ここでは成功した結果があるので、ImageViewを見え無くして隠します。(ImageViewのvisibilityをオフにする)
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

ステップ3:状態ImageViewをレイアウトに追加する

  1. res/layout/fragment_overview.xmlを開いてください。RecyclerView要素の下、ConstraintLayout内に以下の用にImageViewを追加してください。

このImageViewはRecyclerViewと同じ制約を持っています。しかし、width(幅)とheight(高さ)にはwrap_contentが使われ、画像がビューを満たすように拡大するのではなく、中央に配置されるようになっています。
またapp:marsApiStatus属性を確認してみてください。これはビューモデル内の状態(status)のプロパティが変わったときにBindingAdapterを呼び出します。

<ImageView
   android:id="@+id/status_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:marsApiStatus="@{viewModel.status}" />
  1. エミュレーター、または実機の機内モードをオンにして、ネットワーク接続がない場合をシミュレーションできるようにしてください。
    コンパイルしてアプリを起動してください。エラー画像が表示されることを確認してください。
  1. 戻るボタンを押してアプリを閉じてください。機内モードをオフにしてください。最近使ったアプリからアプリに戻ってください。ネットワーク接続速度によりますが、アプリがウェブサービスにクエリを送信し、画像の読み込みが始まる前に読み込み中の画像が表示されます。

完成済みプロジェクト

お疲れさまでした。完成済みプロジェクトは以下からダウンロードできます。

MarsRealEstateGrid