안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제

안드로이드에서 Coroutine과 Retrofit2로 비동기적으로 국가 리스트와 국기 이미지를 가져와 화면에 보여주는 예제를 소개합니다.

예제는 AndroidCoroutinesRetrofitMVVM에 있고, 예제를 실행하면 다음과 같이 국가와 국기 리스트가 보입니다.

Android Coroutines Retrofit MVVM

  • Coroutine으로 비동기 요청
  • Retrofit으로 통신하여 국가와 국기 이미지를 가져옴
  • MVVM으로 데이터를 UI에 보여줌

1. Coroutine, Retrofit2 의존성

다음과 같이 build.gradle에 Coroutine과 Retrofit2의 의존성을 추가합니다. 또한 이미지를 로드할 때 사용할 glide를 추가합니다.

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를 이용하여 국가 리스트를 보여줍니다. 아래 두개 파일을 추가합니다.

아래와 같이 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)
}
Loading script...
codechachaCopyright ©2019 codechacha