Android - コルーチンとRetrofitによる非同期通信の例

Androidでは、CoroutineとRetrofit2で非同期に国のリストと国旗の画像を取得し、画面に表示する例を紹介します。

例はAndroidCoroutinesRetrofitMVVMにあり、例を実行すると、次のように国と国のリストが表示されます。

Android Coroutines Retrofit MVVM

  • Coroutineによる非同期
  • Retrofitで通信して国と国旗の画像を取得する
  • MVVMでデータをUIに表示する

1. Coroutine、Retrofit2依存

次のように build.gradleにCoroutineとRetrofit2の依存関係を追加します。また、イメージをロードするときに使用するグリッドを追加します。

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'

implementation 'com.github.bumptech.glide:glide:4.8.0'

2. View Layout

RecyclerViewを使用して国のリストを表示します。以下の2つのファイルを追加します。

以下のように activity_main.xml ファイルを追加します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/countriesList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/list_error"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="Error"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/loading_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

以下のように item_country.xml ファイルを追加します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                             android:layout_width="match_parent"
                                             android:layout_height="@dimen/layout_height">
    <ImageView
            android:id="@+id/imageView"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:padding="@dimen/standard_padding"/>

    <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:layout_weight="2"
            android:orientation="vertical">

        <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                style="@style/Title"
                android:text="Country"/>

        <TextView
                android:id="@+id/capital"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                style="@style/Text"
                android:text="Capital"/>
    </LinearLayout>
</LinearLayout>

3. CoroutineとRetrofitに国のリストをインポートする

以下のコードはMainActivityです。このコードからデータを取得して画面に表示します。

すべてのコードはAndroidCoroutinesRetrofitMVVMで確認してください。

以下のコードで

  • viewModel.refresh() を呼び出すと、この関数内で Coroutine と Retrofit と非同期通信を行います
  • RetrofitにデータをインポートするとViewModelに保存され、MainActivityは viewModel.countries.observe()でイベントを受け取ります
  • countriesAdapter.updateCountries(it)のコードで渡されたデータをRecyclerViewに表示します

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: ListViewModel
    private val countriesAdapter = CountryListAdapter(arrayListOf())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
        viewModel.refresh()

        countriesList.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = countriesAdapter
        }

        observeViewModel()
    }

    fun observeViewModel() {
        viewModel.countries.observe(this, Observer {countries ->
            countries?.let {
                countriesList.visibility = View.VISIBLE
                countriesAdapter.updateCountries(it) }
        })

        viewModel.countryLoadError.observe(this, Observer { isError ->
            list_error.visibility = if(isError == "") View.GONE else View.VISIBLE
        })

        viewModel.loading.observe(this, Observer { isLoading ->
            isLoading?.let {
                loading_view.visibility = if(it) View.VISIBLE else View.GONE
                if(it) {
                    list_error.visibility = View.GONE
                    countriesList.visibility = View.GONE
                }
            }
        })
    }
}

以下のコードは ListViewModel.kt ですが、 refresh() が呼び出されると CoroutineScope で Retrofit で実装された countriesService.getCountries() を呼び出します。 withContextを使用して通信が完了するのを待ち、結果が返されるとLiveDataオブジェクト「countries」に保存します。 MainActivityはこのLiveDataをobservingするため、データが保存されるとすぐにイベントを受け取ります。

ListViewModel.kt

class ListViewModel: ViewModel() {

    val countriesService = CountriesService.getCountriesService()
    var job: Job? = null
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        onError("Exception: ${throwable.localizedMessage}")
    }

    val countries = MutableLiveData<List<Country>>()
    val countryLoadError = MutableLiveData<String?>()
    val loading = MutableLiveData<Boolean>()

    fun refresh() {
        fetchCountries()
    }

    private fun fetchCountries() {
        loading.value = true

        job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
            val response = countriesService.getCountries()
            withContext(Dispatchers.Main) {
                if (response.isSuccessful) {
                    countries.value = response.body()
                    countryLoadError.value = ""
                    loading.value = false
                } else {
                    onError("Error : ${response.message()}")
                }
            }
        }
    }

    private fun onError(message: String) {
        countryLoadError.value = message
        loading.value = false
    }

    override fun onCleared() {
        job?.cancel()
    }
}

4. Retrofit API フレーズ

Retrofitで非同期通信を行うときに使用されるファイルは以下の3つです。まず、データクラスとインタフェースを設定し、サービスを作成します。残りはRetrofitでコードを自動的に生成します。

データは https://raw.githubusercontent.com/DevTides/countries/master/countriesV2.jsonから取得されます。このアドレスに基づいて、 BASE_URL@GETで使用されるアドレスが決まります。

CountriesApi.kt

interface CountriesApi {
    @GET("DevTides/countries/master/countriesV2.json")
    suspend fun getCountries(): Response<List<Country>>
}

CountriesService.kt

object CountriesService {

    private val BASE_URL = "https://raw.githubusercontent.com"

    fun getCountriesService(): CountriesApi {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(CountriesApi::class.java)
    }
}

Country.kt

data class Country(
    @SerializedName("name")
    val countryName: String?,

    @SerializedName("capital")
    val capital: String?,

    @SerializedName("flagPNG")
    val flag: String?
)

5. Glideで画像へ

Retrofitは上記のURLからJsonファイルを読み込み、国旗イメージをインポートせずに国旗イメージのURLアドレスを読み込みます。

{
  "capital": "Mariehamn",
  "flagPNG": "https://raw.githubusercontent.com/DevTides/countries/master/ala.png",
  "name": "\u00c5land Islands",
}

Glideを使用してこの画像アドレスを読み、画面に表示させる。

fun ImageView.loadImage(uri: String?) {
    val options = RequestOptions()
        .error(R.mipmap.ic_launcher_round)
    Glide.with(this.context)
        .setDefaultRequestOptions(options)
        .load(uri)
        .into(this)
}
codechachaCopyright ©2019 codechacha