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

タスク:インターネットの画像を表示する

ウェブURLから画像を表示するというのは簡単に聞こえるかもしれませんが、実際に機能させるには多くの工数が必要になります。画像はダウンロードされ、バッファリングされ、圧縮形式からAndroidが使用できる形式にデコードされる必要があります。また、画像はインメモリキャッシュかストレージベースのキャッシュ、またはその両方にキャッシュされるべきです。

これらはUIが反応しなくなるのを防ぐために、バックグラウンドスレッドで低い優先順位で行われなければなりません。また、最適なネットワークおよびCPUパフォーマンスのために、一度に一枚以上の画像を読み込み、デコードするのが好ましいです。

幸いにもコミュニティによって開発されたGlideというライブラリを使って画像をダウンロード、バッファリング、デコード、およびキャッシュすることができます。Glideはスクラッチからこれら全てをやる手間を省いてくれます。

Grlideライブラリは以下の二つを必要とします。

  • 読み取り、表示した画像のURL
  • 画像を表示するためのImageViewオブジェクト

このタスクでは、Glideを使って不動産ウェブサービスから一枚の画像を表示する方法を学習します。ウェブサービスから返される物件のリストの最初の火星物件を表す画像を表示します。
以下はビフォーとアフターのスクリーンショットです。

  

ステップ1:Glideの依存関係を追加する

  1. 前回の記事で使ったMarsRealEstateアプリを開いてください。(アプリがない場合はMarsRealEstateNetworkからダウンロードすることができます)
  2. 現在の状態を確認するためにアプリを起動してください。(火星上の仮想的に利用可能な物件の詳細テキストが表示されます)
  3. build.gradle(Module: app)を開いてください。
  4. dependenciesセクションの中に、以下のGlideライブラリ用のコードを追加してください。
implementation "com.github.bumptech.glide:glide:$version_glide"

バージョン番号が既にプロジェクトのGradleファイルに別途定義されていることを確認してください。

  1. Sync Nowをクリックして新規依存関係のもとでプロジェクトをリビルドできるようにしてください。

ステップ2:ビューモデルをアップデートする

次にOverviewViewModelクラスをアップデートして、火星の物件用のLiveDataを含ませます。

  1. overview/OverviewViewModel.ktを開いてください。_response用のLiveDataのすぐ下に内部(mutable)と外部(mutableでない)LiveDataをMarsPropertyオブジェクト用に追加してください。

要求されたらMarsPropertyクラス(com.example.android.marsrealestate.network.MarsProperty)をインポートしてください。

private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. getMarsRealEstateProperties()メソッド中の、try{}ブロック内のコードを以下のコードで置き換えてください。
val listResult = MarsApi.retrofitService.getProperties()
_property.value = MarsApi.retrofitService.getProperties()
.get(0)
  1. catch{}ブロック内のコードを以下のコードで置き換えてください。
_response.value = "Failure: ${e.message}"

最終的なtry/catch{}ブロックは以下のようになります。

try {
   val listResult = MarsApi.retrofitService.getProperties()
   _property.value = MarsApi.retrofitService.getProperties().get(0)
   _response.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}
  1. res/layout/fragment_overview.xmlファイルを開いてください。<TextView>要素の中の、android:textを変更して、property LiveDataのimgSrcUrlコンポーネントにバインドします。
android:text="@{viewModel.property.imgSrcUrl}"
  1. アプリを起動してください。TextViewは火星物件の一番最初の画像のURLのみを表示します。ここまでやったことは全てURL用のビューモデルとLiveDataのセットアップ作業です。

ステップ3:バインディングアダプターの作成とGlideの呼び出し

ここまでで表示する画像のURLを手に入れることができたので、ここからはGlideを使って画像を読み込みます。このステップでは、ImageViewに紐づけられたXML属性からURLを取得するためにバインディングアダプターを使い、その画像を読み込むためにGlideを使います。
バインディングアダプターはビューとバインドされたデータ間にある拡張メソッドで、データが変わったときに独自の振る舞いをさせるために使われます。今回の場合は、独自の振る舞いとはURLからImageViewに画像を読み込ませるためにGlideを呼び出すことです。

  1. BindingAdapters.ktを開いてください。このファイルにはアプリ全体で使用するバインディングアダプターが保持されています。
  2. ImageViewとStringを引数に受け取るbindImage()関数を作成してください。この関数を@BindingAdapterでアノテーションしてください。
    @BindingAdapterアノテーションはデータバインディングに、XMLのアイテムがimageUrl属性を持っている際に、このバインディングアダプターが実行されるべきということを伝えます。

要求されたら、androidx.databinding.BindingAdapterとandroid.widget.ImageViewをインポートしてください。

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. bindImage()関数内に、imgUrl引数用のlet{}ブロックを追加してください。
imgUrl?.let { 
}
  1. let{}ブロック内に、以下のURL string(XMLから取得したもの)をUriオブジェクトに変換するコードを追加してください。要求されたら、androidx.core.net.toUriをインポートしてください。

HTTPSスキームを利用するためにUriオブジェクトが必要になります。画像を引っ張て来ているサーバーがそのスキームを要求するためです。HTTPSスキームを利用するために、buildUpon.scheme(“https”)をtoUriビルダーに付け加えています。
toUri()メソッドはAndroid KTX coreライブラリからのKotlin拡張関数なので、Stringクラスの一部のように扱えます。

val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. さらにlet{}内で、Glide.with()を呼び出して、UriオブジェクトからImageViewに画像を読み込ませます。要求されたらcom.bumptech.glide.Glideをインポートしてください。
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

ステップ4:レイアウトとフラグメントを更新する

Glideは既に画像を読み込んでいますが、今のところ何も表示されません。次のステップはレイアウトとフラグメントを画像を表示するためのImageViewを用いて更新します。

  1. res/layout/gridview_item.xmlを開いてください。これは後にRecyclerView内のそれぞれのアイテム用に使うレイアウトリソースファイルです。ここでは一時的に一つの画像を表示するために使用します。
  2. <ImageView>要素の上に、データバインディング用の<data>要素を追加し、OverviewViewModelクラスとバインドしてください。
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. app:imageUrl属性をImageView要素に追加し、新しい画像読み込みバインディングアダプターを使用します。
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. overview/OverviewFragment.ktを開いてください。onCreateView()メソッド内のFragmentOvervieBindingクラスをインフレートし、それをbinding変数に代入している行をコメントアウトしてください。これは一時的なもので、後に修正します。
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. 代わりにGridViewItemBindingクラスをインフレートするコードを追加してください。
    要求されたら、com.example.android.marsrealestate.databinding.GridViewItemBindingをインポートしてください。

Note: この変更によってAndroid Studioがデータバインディングエラーを表示することがあります。このエラーを解消するには、アプリをクリーンアップし、リビルドしてください。

val binding = GridViewItemBinding.inflate(inflater)
  1. アプリを起動してください。リストの最初のMarsPropertyの画像が表示されます。

ステップ5:シンプルなローディング画像とエラー画像を追加する

画像の読み込み中や読み込みが失敗した際にプレースホルダー画像を表示することによって、Glideのユーザーエクスペリエンスは向上させることができます。このステップでは、その機能をバインディングアダプターとレイアウトに追加していきます。

  1. res/drawable/ic_broken_image.xmlを開き、右側のデザインタブを津クリックしてください。エラー画像用には組み込みアイコンライブラリで取得可能なこのbroken-imageアイコンを使用します。このヴェクター画像はアイコンをグレーにするためにandroid:tint属性を使用しています。
  1. res/drawable/loading_animation.xmlを開いてください。この画像は<animate-rotate>タグによって定義されたアニメーションです。アニメーションによってloading_img.xml画像が回転します。
    (プレビューでアニメーションは確認できません)
  1. BindingAdapters.ktファイルに戻ってください。bindImage()メソッド内のGlide.with()の呼び出しを更新して、apply()関数をload()とinto()の間で呼び出してください。
    要求されたら、com.bumptech.glide.request.RequestOptionsをインポートしてください。

このコードは画像の読み込み中にプレースホルダー画像(loading_animation)をセットしています。また画像の読み込みが失敗した場合用の画像(broken_image)もセットしています。
最終的なbindImage()メソッドは以下のようになります。

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = 
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  1. アプリを起動してください。ネットワーク接続のスピードによりますが、Glideが画像をダウンロードし表示する際に、読み込み画像が表示されます。しかしネットワーク接続をオフにしたとしても、broken-imageアイコンは表示されません。これについては記事の最後で解消します。