Android Kotlin 기초 08.2: 인터넷에서 이미지 로드 및 표시

이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.

소개

이전 Codelab에서는 웹 서비스에서 데이터를 가져와 응답을 데이터 객체로 파싱하는 방법을 배웠습니다. 이 지식을 기반으로 이 Codelab에서는 웹 URL에서 사진을 로드하고 표시합니다. 또한 RecyclerView를 빌드하고 이 뷰를 사용해 개요 페이지에 이미지 그리드를 표시하는 방법을 다시 확인합니다.

기본 요건

  • 프래그먼트를 만들고 사용하는 방법
  • 뷰 모델, 뷰 모델 팩토리, 변환, LiveData 등의 아키텍처 구성요소를 사용하는 방법
  • Retrofit 라이브러리와 Moshi 라이브러리를 사용하여 REST 웹 서비스에서 JSON을 검색하고 이 데이터를 Kotlin 객체로 파싱하는 방법
  • RecyclerView로 그리드 레이아웃을 구성하는 방법
  • Adapter, ViewHolder, DiffUtil의 작동 방식

학습할 내용

  • Glide 라이브러리를 사용하여 웹 URL에서 이미지를 로드하고 표시하는 방법
  • RecyclerView 및 그리드 어댑터를 사용하여 이미지 그리드를 표시하는 방법
  • 이미지를 다운로드하고 표시할 때 발생할 수 있는 오류를 처리하는 방법

실행할 작업

  • 화성 속성 데이터에서 이미지 URL을 가져오도록 MarsRealEstate 앱을 수정하고 Glide를 사용하여 해당 이미지를 로드하고 표시합니다.
  • 앱에 로드 애니메이션과 오류 아이콘을 추가합니다.
  • RecyclerView를 사용하여 화성 속성 이미지의 그리드를 표시합니다.
  • RecyclerView에 상태 및 오류 처리를 추가합니다.

이 Codelab 및 관련 Codelab에서는 화성 판매의 속성을 표시하는 MarsRealEstate라는 앱을 사용합니다. 앱이 인터넷 서버에 연결하여 가격 및 부동산 매매 또는 임대 가능 여부 등의 세부정보 데이터를 검색하고 표시합니다. 각 속성을 나타내는 이미지는 NASA의 화성 탐사 로봇이 화성에서 촬영한 실제 사진입니다.

이 Codelab에서 빌드하는 앱 버전은 이미지 그리드를 표시하는 개요 페이지에 채워집니다. 이미지는 앱이 화성 부동산 웹 서비스에서 가져오는 부동산 데이터의 일부입니다. 앱은 Glide 라이브러리를 사용하여 이미지를 로드하고 표시하며 RecyclerView는 이미지의 그리드 레이아웃을 만듭니다. 또한, 앱은 네트워크 오류를 적절히 처리합니다.

웹 URL에서 사진을 표시하는 것은 간단해 보일 수도 있지만 제대로 작동하려면 엔지니어링이 상당히 필요합니다. 이미지는 Android에서 사용할 수 있는 이미지로 다운로드, 버퍼링, 디코딩해야 합니다. 이미지는 메모리 내 캐시나 저장소 기반 캐시 또는 두 캐시 모두에 캐시해야 합니다. UI가 응답성을 유지하기 위해 이 모든 작업은 우선순위가 낮은 백그라운드 스레드에서 이루어져야 합니다. 또한 최상의 네트워크 및 CPU 성능을 위해 둘 이상의 이미지를 한 번에 가져오고 디코딩하는 것이 좋습니다. 네트워크에서 효과적으로 이미지를 로드하는 방법을 배우는 것은 그 자체로 Codelab이 될 수 있습니다.

다행히 커뮤니티에서 개발한 Glide라는 라이브러리를 사용하여 이미지를 다운로드하고 버퍼링 및 디코딩하고 캐시할 수 있습니다. Glide는 처음부터 이 모든 작업을 해야 했던 것보다 훨씬 적은 수고를 덜어줍니다.

Glide에는 기본적으로 다음 두 가지가 필요합니다.

  • 로드 및 표시하려는 이미지의 URL.
  • 이미지를 표시하는 ImageView 객체

이 작업에서는 Glide를 사용하여 부동산 웹 서비스의 단일 이미지를 표시하는 방법을 알아봅니다. 웹 서비스에서 반환하는 속성 목록에 첫 번째 화성 속성을 나타내는 이미지를 표시합니다. 다음은 전과 후의 스크린샷입니다.

1단계: Glide 종속 항목 추가하기

  1. 이전 Codelab의 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 클래스를 업데이트합니다.

  1. overview/OverviewViewModel.kt를 엽니다. _responseLiveData 바로 아래에 단일 MarsProperty 객체의 내부 (변경 가능) 및 외부 (변경 불가능) 실시간 데이터를 모두 추가합니다.

    요청을 받으면 MarsProperty 클래스 (com.example.android.marsrealestate.network.MarsProperty)를 가져옵니다.
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. getMarsRealEstateProperties() 메서드에서 try/catch {} 블록 내에 있는 줄을 _response.value의 속성 수로 설정하는 줄을 찾습니다. 아래와 같이 테스트를 추가합니다. MarsProperty 객체를 사용할 수 있는 경우 이 테스트는 _property LiveData 값을 listResult의 첫 번째 속성으로 설정합니다.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

이제 전체 try/catch {} 블록은 다음과 같습니다.

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. res/layout/fragment_overview.xml 파일을 엽니다. <TextView> 요소에서 android:text를 변경하여 property LiveDataimgSrcUrl 구성요소에 바인딩합니다.
android:text="@{viewModel.property.imgSrcUrl}"
  1. 앱을 실행합니다. TextView는 첫 번째 화성 속성에 이미지의 URL만 표시합니다. 지금까지 이 URL의 뷰 모델과 실시간 데이터를 설정했습니다.

3단계: 결합 어댑터 만들기 및 Glide 호출

이제 표시할 이미지의 URL이 생겼으므로 Glide를 사용하여 이미지를 로드하세요. 이 단계에서는 결합 어댑터를 사용하여 ImageView와 연결된 XML 속성에서 URL을 가져오고 Glide를 사용하여 이미지를 로드합니다. 결합 어댑터는 데이터가 변경될 때 맞춤 동작을 제공하기 위해 뷰와 결합된 데이터 사이에 있는 확장 메서드입니다. 이 경우 맞춤 동작은 Glide를 호출하여 URL에서 ImageView로 이미지를 로드하는 것입니다.

  1. BindingAdapters.kt를 엽니다. 이 파일은 앱 전반에 사용하는 결합 어댑터를 보유하게 됩니다.
  2. 매개변수로 ImageViewString을 사용하는 bindImage() 함수를 만듭니다. 함수에 @BindingAdapter 주석을 추가합니다. @BindingAdapter 주석은 XML 항목에 imageUrl 속성이 있을 때 이 결합 어댑터가 실행되기를 원하는 데이터 결합을 알려 줍니다.

    요청을 받으면 androidx.databinding.BindingAdapterandroid.widget.ImageView를 가져옵니다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. bindImage() 함수 내에서 imgUrl 인수의 let {} 블록을 추가합니다.
imgUrl?.let { 
}
  1. let {} 블록 안에 다음 줄을 추가하여 XML에서 URL 문자열을 Uri 객체로 변환합니다. 요청이 있으면 androidx.core.net.toUri를 가져옵니다.

    이미지를 가져오는 서버에 이 체계가 필요하므로 최종 Uri 객체가 HTTPS 스키마를 사용해야 합니다. HTTPS 스키마를 사용하려면 buildUpon.scheme("https")toUri 빌더에 추가합니다. toUri() 메서드는 Android KTX 핵심 라이브러리의 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를 엽니다. 이 파일은 Codelab의 뒷부분에서 RecyclerView의 각 항목에 사용할 레이아웃 리소스 파일입니다. 여기에서 일시적으로 이 이미지를 사용하여 단일 이미지만 표시합니다.
  2. <ImageView> 요소 위에 데이터 결합의 <data> 요소를 추가하고 OverviewViewModel 클래스에 결합합니다.
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. 새 이미지 로드 결합 어댑터를 사용하도록 ImageView 요소에 app:imageUrl 속성을 추가합니다.
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. overview/OverviewFragment.kt를 엽니다. onCreateView() 메서드에서 FragmentOverviewBinding 클래스를 확장하고 결합 변수에 할당하는 줄을 주석 처리합니다. 이 오류는 일시적이며 나중에 다시 확인합니다.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. 대신 GridViewItemBinding 클래스를 확장하는 줄을 추가합니다. 요청이 있는 경우 com.example.android.marsrealestate. databinding.GridViewItemBinding을 가져옵니다.
val binding = GridViewItemBinding.inflate(inflater)
  1. 앱을 실행합니다. 이제 결과 목록의 첫 번째 MarsProperty에서 이미지 사진이 표시됩니다.

5단계: 간단한 로드 및 오류 이미지 추가

Glide는 이미지를 로드하는 동안 자리표시자 이미지를 표시하고 로드 실패 시(예: 이미지가 없거나 손상된 경우) 오류 이미지를 표시함으로써 사용자 경험을 개선할 수 있습니다. 이 단계에서는 이러한 기능을 결합 어댑터 및 레이아웃에 추가합니다.

  1. res/drawable/ic_broken_image.xml을 열고 오른쪽에서 Preview 탭을 클릭합니다. 오류 이미지의 경우 내장된 아이콘 라이브러리에서 사용할 수 있는 손상 이미지 아이콘을 사용합니다. 이 벡터 드로어블은 android:tint 속성을 사용하여 아이콘 색상을 회색으로 지정합니다.

  1. res/drawable/loading_animation.xml를 엽니다. 이 드로어블은 <animate-rotate> 태그로 정의된 애니메이션입니다. 애니메이션은 이미지 드로어블 loading_img.xml을 중심점을 축으로 회전시킵니다. (이 애니메이션이 미리보기에 표시되지 않습니다.)

  1. BindingAdapters.kt 파일로 돌아갑니다. bindImage() 메서드에서 Glide.with() 호출을 업데이트하여 load()into() 사이에 apply() 함수를 호출합니다. 요청이 있는 경우 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가 속성 이미지를 다운로드하고 표시할 때 로드 이미지가 잠시 표시될 수도 있습니다. 그러나 네트워크를 사용 중지해도 손상 이미지 아이콘은 아직 표시되지 않습니다. 이 부분은 Codelab의 마지막 부분에서 수정합니다.

이제 앱이 인터넷에서 속성 정보를 로드합니다. 첫 번째 MarsProperty 목록 항목의 데이터를 사용하여 뷰 모델에 LiveData 속성을 만들고 이 속성 데이터의 이미지 URL을 사용하여 ImageView를 채웠습니다. 하지만 앱이 이미지 그리드를 표시하는 것이 목표이므로 GridLayoutManager와 함께 RecyclerView를 사용하는 것이 좋습니다.

1단계: 뷰 모델 업데이트하기

이제 뷰 모델에는 웹 서비스의 응답 목록에 있는 첫 번째 객체인 MarsProperty 객체 하나를 보유하는 _property LiveData가 있습니다. 이 단계에서는 MarsProperty 객체의 전체 목록을 보유하도록 LiveData를 변경합니다.

  1. overview/OverviewViewModel.kt를 엽니다.
  2. 비공개 _property 변수를 _properties로 변경합니다. 유형을 MarsProperty 객체 목록으로 변경합니다.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. 외부 property 실시간 데이터를 properties로 바꿉니다. 여기에서도 LiveData 유형에 목록을 추가합니다.
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. 아래로 스크롤하여 getMarsRealEstateProperties() 메서드를 찾습니다. try {} 블록 내에서 이전 작업에서 추가한 전체 테스트를 아래와 같은 줄로 바꿉니다. listResult 변수는 MarsProperty 객체 목록을 보유하고 있으므로 성공적인 응답을 테스트하는 대신 _properties.value에 할당할 수 있습니다.
_properties.value = listResult

이제 전체 try/catch 블록은 다음과 같습니다.

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

2단계: 레이아웃 및 프래그먼트 업데이트하기

다음 단계는 단일 이미지 뷰가 아닌 recycler 뷰와 그리드 레이아웃을 사용하도록 앱의 레이아웃과 프래그먼트를 변경하는 것입니다.

  1. res/layout/gridview_item.xml를 엽니다. 데이터 결합을 OverviewViewModel에서 MarsProperty로 변경하고 변수의 이름을 "property"로 바꿉니다.
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. <ImageView>에서 MarsProperty 객체의 이미지 URL을 참조하도록 app:imageUrl 속성을 변경합니다.
app:imageUrl="@{property.imgSrcUrl}"
  1. overview/OverviewFragment.kt를 엽니다. onCreateview()에서 FragmentOverviewBinding을 확장하는 줄의 주석 처리를 삭제합니다. GridViewBinding을 확장하는 줄을 삭제하거나 주석 처리합니다. 이렇게 변경하면 마지막 작업에서 적용한 임시 변경사항이 실행취소됩니다.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. res/layout/fragment_overview.xml를 엽니다. 전체 <TextView> 요소를 삭제합니다.
  2. 대신 단일 항목에 GridLayoutManagergrid_view_item 레이아웃을 사용하는 다음 <RecyclerView> 요소를 추가합니다.
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            app:layoutManager=
               "androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:spanCount="2"
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />

3단계: 사진 그리드 어댑터 추가하기

이제 fragment_overview 레이아웃에는 RecyclerView이 있고 grid_view_item 레이아웃에 단일 ImageView가 있습니다. 이 단계에서는 RecyclerView 어댑터를 통해 데이터를 RecyclerView에 바인딩합니다.

  1. overview/PhotoGridAdapter.kt를 엽니다.
  2. 아래와 같이 생성자 매개변수를 사용하여 PhotoGridAdapter 클래스를 만듭니다. PhotoGridAdapter 클래스는 ListAdapter를 확장합니다. 이 생성자에는 목록 항목 유형, 뷰 홀더, DiffUtil.ItemCallback 구현이 필요합니다.

    요청에 따라 androidx.recyclerview.widget.ListAdaptercom.example.android.marsrealestate.network.MarsProperty 클래스를 가져옵니다. 다음 단계에서는 이 생성자에 누락되어 오류를 생성하는 다른 부분을 구현합니다.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. PhotoGridAdapter 클래스의 아무 곳이나 클릭하고 Control+i을 눌러 onCreateViewHolder() 메서드, onBindViewHolder()ListAdapter 메서드를 구현합니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. PhotoGridAdapter 클래스 정의 끝에서 방금 추가한 메서드 뒤에 아래와 같이 DiffCallback의 컴패니언 객체 정의를 추가합니다.

    요청을 받으면 androidx.recyclerview.widget.DiffUtil을 가져옵니다.

    DiffCallback 객체는 비교하려는 객체 유형(MarsProperty)으로 DiffUtil.ItemCallback를 확장합니다.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Control+i를 눌러 이 객체의 비교기 메서드 areItemsTheSame()areContentsTheSame()을 구현합니다.
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. areItemsTheSame() 메서드의 경우 TODO를 삭제합니다. oldItemnewItem의 객체 참조가 동일한 경우 true를 반환하는 Kotlin의 참조 동등 연산자 (===)를 사용합니다.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. areContentsTheSame()의 경우 oldItemnewItem의 ID에만 표준 동등 연산자를 사용합니다.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. PhotoGridAdapter 클래스 내에서 컴패니언 객체 아래에 RecyclerView.ViewHolderMarsPropertyViewHolder을 확장하는 내부 클래스 정의를 추가합니다.

    요청이 있으면 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.databinding.GridViewItemBinding를 가져옵니다.

    레이아웃에 MarsProperty를 바인딩하려면 GridViewItemBinding 변수가 필요하므로, 변수를 MarsPropertyViewHolder에 전달합니다. 기본 ViewHolder 클래스는 생성자에 뷰가 필요하므로 결합 루트 뷰를 전달합니다.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. MarsPropertyViewHolder에서 MarsProperty 객체를 인수로 사용하고 binding.property를 이 객체로 설정하는 bind() 메서드를 만듭니다. 속성을 설정한 후 executePendingBindings()를 호출하면 업데이트가 즉시 실행됩니다.
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. onCreateViewHolder()에서 TODO를 삭제하고 아래의 줄을 추가합니다. 요청이 있는 경우 android.view.LayoutInflater을 가져옵니다.

    onCreateViewHolder() 메서드는 GridViewItemBinding를 확장하고 상위 ViewGroup 컨텍스트의 LayoutInflater를 사용하여 생성된 새 MarsPropertyViewHolder를 반환해야 합니다.
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. onBindViewHolder() 메서드에서 TODO를 삭제하고 아래의 줄을 추가합니다. 여기서 getItem()를 호출하여 현재 RecyclerView 위치와 연결된 MarsProperty 객체를 가져온 다음 이 속성을 MarsPropertyViewHolderbind() 메서드에 전달합니다.
val marsProperty = getItem(position)
holder.bind(marsProperty)

4단계: 결합 어댑터 추가 및 부분 연결하기

마지막으로, BindingAdapter를 사용하여 MarsProperty 객체 목록으로 PhotoGridAdapter를 초기화합니다. BindingAdapter를 사용하여 RecyclerView 데이터를 설정하면 데이터 결합이 자동으로 MarsProperty 객체 목록의 LiveData를 관찰합니다. 그런 다음 MarsProperty 목록이 변경되면 결합 어댑터가 자동으로 호출됩니다.

  1. BindingAdapters.kt를 엽니다.
  2. 파일 끝에 RecyclerViewMarsProperty 객체 목록을 인수로 사용하는 bindRecyclerView() 메서드를 추가합니다. 이 메서드에 @BindingAdapter 주석을 답니다.

    요청을 받으면 androidx.recyclerview.widget.RecyclerViewcom.example.android.marsrealestate.network.MarsProperty를 가져옵니다.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. bindRecyclerView() 함수 내에서 recyclerView.adapterPhotoGridAdapter으로 변환하고 데이터를 사용하여 adapter.submitList()를 호출합니다. 그러면 새 목록을 사용할 수 있을 때 RecyclerView에 알려줍니다.

요청이 있는 경우 com.example.android.marsrealestate.overview.PhotoGridAdapter를 가져옵니다.

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. res/layout/fragment_overview.xml를 엽니다. RecyclerView 요소에 app:listData 속성을 추가하고 데이터 결합을 사용하여 이 속성을 viewmodel.properties로 설정합니다.
app:listData="@{viewModel.properties}"
  1. overview/OverviewFragment.kt를 엽니다. onCreateView()setHasOptionsMenu() 호출 직전에 binding.photosGrid에서 RecyclerView 어댑터를 새 PhotoGridAdapter 객체로 초기화합니다.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. 앱을 실행합니다. MarsProperty 이미지의 그리드가 표시됩니다. 스크롤하여 새 이미지를 볼 때 앱은 이미지 자체를 표시하기 전에 로드 진행률 아이콘을 표시합니다. 비행기 모드를 사용 설정하면 아직 로드되지 않은 이미지가 손상 이미지 아이콘으로 표시됩니다.

이미지를 가져올 수 없을 때 MarsRealEstate 앱에 손상 이미지 아이콘이 표시됩니다. 하지만 네트워크가 없으면 앱에서 빈 화면이 표시됩니다.

이는 만족스러운 사용자 경험이 아닙니다. 이 작업에서는 기본 오류 처리를 추가하여 사용자가 현재 상황을 더 잘 파악하도록 합니다. 인터넷을 사용할 수 없는 경우 앱에 연결 오류 아이콘이 표시됩니다. 앱이 MarsProperty 목록을 가져오는 동안 앱은 로드 애니메이션을 표시합니다.

1단계: 뷰 모델에 상태 추가하기

시작하려면 뷰 모델에 LiveData를 만들어 웹 요청의 상태를 나타냅니다. 로드, 성공, 실패 등 세 가지 상태를 고려합니다. 로드 상태는 await() 호출을 위한 데이터를 기다리는 동안 발생합니다.

  1. overview/OverviewViewModel.kt를 엽니다. 파일 상단에서(가져오기 뒤, 클래스 정의 앞에) enum을 추가하여 사용 가능한 모든 상태를 나타냅니다.
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. OverviewViewModel 클래스 내부 및 외부 _response 실시간 데이터 정의의 이름을 _status 모두 바꿉니다. 이 Codelab 앞부분에서 _properties LiveData 지원을 추가했으므로 전체 웹 서비스 응답이 사용되지 않았습니다. 기존 변수의 이름을 바꾸려면 현재 상태를 추적하려면 LiveData이 필요합니다.

또한 유형을 String에서 MarsApiStatus.로 변경합니다.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. getMarsRealEstateProperties() 메서드까지 아래로 스크롤하고 _response_status로 업데이트합니다. "Success" 문자열을 MarsApiStatus.DONE 상태로, "Failure" 문자열을 MarsApiStatus.ERROR로 변경합니다.
  2. await()를 호출하기 전에 try {} 블록 상단에 MarsApiStatus.LOADING 상태를 추가합니다. 이 상태는 코루틴을 실행하는 동안 데이터를 기다릴 때 초기 상태입니다. 이제 전체 try/catch {} 블록은 다음과 같습니다.
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} 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 값을 인수로 사용하는 bindStatus()라는 새 결합 어댑터를 추가합니다. 요청이 있는 경우 com.example.android.marsrealestate.overview.MarsApiStatus를 가져옵니다.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. bindStatus() 메서드 내부에 when {}를 추가하여 서로 다른 상태 간에 전환합니다.
when (status) {

}
  1. when {} 내부에 로드 상태(MarsApiStatus.LOADING)의 사례를 추가합니다. 이 상태의 경우 ImageView를 visible로 설정하고 로드 애니메이션에 할당합니다. 이전 작업에서 Glide에 사용한 것과 동일한 애니메이션 드로어블입니다. 요청이 있는 경우 android.view.View를 가져옵니다.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. 오류 상태(MarsApiStatus.ERROR)의 사례를 추가합니다. LOADING 상태의 경우와 유사하게 상태 ImageView를 visible로 설정하고 연결 오류 드로어블을 재사용합니다.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 완료 상태(MarsApiStatus.DONE)의 사례를 추가합니다. 여기서는 성공적인 응답이 있으므로 상태 ImageView의 공개 상태를 사용 중지하여 숨깁니다.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

3단계: 레이아웃에 ImageView 상태 추가하기

  1. res/layout/fragment_overview.xml를 엽니다. RecyclerView 요소 아래 ConstraintLayout에서 아래와 같이 ImageView를 추가합니다.

    ImageView에는 RecyclerView와 동일한 제약 조건이 있습니다. 그러나 이미지를 늘려 뷰를 채우는 대신, 너비와 높이가 wrap_content를 사용하여 이미지를 중앙에 배치합니다. 또한 뷰 모델의 상태 속성이 변경될 때 뷰가 BindingAdapter를 호출하는 app:marsApiStatus 속성을 확인합니다.
<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. Back 버튼을 탭하여 앱을 닫고 비행기 모드를 사용 중지합니다. 최근 항목 화면을 사용하여 앱을 반환합니다. 네트워크 연결 속도에 따라 앱이 웹 서비스를 쿼리할 때 이미지 로드가 시작되기 전에 로드 스피너가 아주 잠시 표시될 수도 있습니다.

Android 스튜디오 프로젝트: MarsRealEstateGrid

  • 이미지 관리 프로세스를 단순화하려면 Glide 라이브러리를 사용하여 앱에서 이미지를 다운로드, 버퍼링, 디코딩, 캐시하세요.
  • Glide는 인터넷에서 이미지를 로드하기 위해 이미지의 URL과 이미지를 배치할 ImageView 객체, 두 가지가 필요합니다. 이러한 옵션을 지정하려면 Glide와 함께 load()into() 메서드를 사용합니다.
  • 결합 어댑터는 뷰와 이 뷰에 결합된 데이터 사이에 있는 확장 메서드입니다. 결합 어댑터는 데이터가 변경될 때(예: Glide를 호출하여 URL에서 ImageView로 이미지 로드하기) 맞춤 동작을 제공합니다.
  • 결합 어댑터는 @BindingAdapter 주석이 추가된 확장 메서드입니다.
  • Glide 요청에 옵션을 추가하려면 apply() 메서드를 사용합니다. 예를 들어 placeholder()와 함께 apply()를 사용하여 로드 드로어블을 지정하고 error()와 함께 apply()를 사용하여 오류 드로어블을 지정합니다.
  • 이미지의 그리드를 생성하려면 GridLayoutManager와 함께 RecyclerView를 사용합니다.
  • 변경 시 속성 목록을 업데이트하려면 RecyclerView와 레이아웃 사이에 결합 어댑터를 사용합니다.

Udacity 과정:

Android 개발자 문서:

기타:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.

  • 필요한 경우 과제를 할당합니다.
  • 학생에게 과제 과제를 제출하는 방법을 알려주세요.
  • 과제 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.

이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.

답변하기

질문 1

로드된 이미지가 포함될 ImageView을 나타내는 데 사용하는 Glide 메서드는 무엇인가요?

into()

with()

imageview()

apply()

질문 2

Glide가 로드 중일 때 표시할 자리표시자 이미지를 지정하려면 어떻게 해야 하나요?

▢ 드로어블과 함께 into() 메서드를 사용합니다.

RequestOptions()를 사용하고 드로어블로 placeholder() 메서드를 호출합니다.

Glide.placeholder 속성을 드로어블에 할당합니다.

RequestOptions()를 사용하고 드로어블로 loadingImage() 메서드를 호출합니다.

질문 3

메서드가 결합 어댑터임을 나타내려면 어떻게 해야 하나요?

LiveData에서 setBindingAdapter() 메서드를 호출합니다.

▢ Kotlin을 BindingAdapters.kt라는 Kotlin 파일에 넣습니다.

▢ XML 레이아웃에서 android:adapter 속성을 사용합니다.

▢ 메서드에 @BindingAdapter 주석을 답니다.

다음 강의 시작: 8.3 인터넷 데이터를 사용한 필터링 및 세부정보 뷰

이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.