안드로이드에서 Coroutine과 Retrofit2로 비동기적으로 국가 리스트와 국기 이미지를 가져와 화면에 보여주는 예제를 소개합니다.
예제는 AndroidCoroutinesRetrofitMVVM에 있고, 예제를 실행하면 다음과 같이 국가와 국기 리스트가 보입니다.
- 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)
}
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력