이 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에서 빌드하는 버전의 앱은 이미지 그리드를 표시하는 개요 페이지를 채웁니다. 이미지는 앱이 Mars 부동산 웹 서비스에서 가져오는 속성 데이터의 일부입니다. 앱은 Glide 라이브러리를 사용하여 이미지를 로드해 표시하고 RecyclerView
를 사용하여 이미지의 그리드 레이아웃을 만듭니다. 또한, 앱은 네트워크 오류를 적절히 처리합니다.
웹 URL에서 사진을 표시하는 것은 간단해 보일 수도 있지만 제대로 작동하려면 엔지니어링이 상당히 필요합니다. 이미지를 다운로드하고, 버퍼링하고, 압축 형식에서 Android가 사용할 수 있는 이미지로 디코딩해야 합니다. 이미지는 메모리 내 캐시나 저장소 기반 캐시 또는 두 캐시 모두에 캐시해야 합니다. UI가 응답성을 유지하기 위해 이 모든 작업은 우선순위가 낮은 백그라운드 스레드에서 이루어져야 합니다. 또한 최상의 네트워크 및 CPU 성능을 위해 둘 이상의 이미지를 한 번에 가져오고 디코딩하는 것이 좋습니다. 네트워크에서 이미지를 효과적으로 로드하는 방법을 배우는 것은 Codelab 자체일 수 있습니다.
다행히 커뮤니티에서 개발한 Glide라는 라이브러리를 사용하여 이미지를 다운로드하고 버퍼링 및 디코딩하고 캐시할 수 있습니다. Glide를 사용하면 이 모든 작업을 처음부터 해야 하는 경우보다 훨씬 적은 작업으로 앱을 만들 수 있습니다.
Glide에는 기본적으로 다음 두 가지가 필요합니다.
- 로드하고 표시할 이미지의 URL입니다.
- 이미지를 표시하는
ImageView
객체
이 작업에서는 Glide를 사용하여 부동산 웹 서비스의 단일 이미지를 표시하는 방법을 알아봅니다. 웹 서비스에서 반환되는 속성 목록에 있는 첫 번째 화성 속성을 나타내는 이미지를 표시합니다. 다음은 전과 후의 스크린샷입니다.
1단계: Glide 종속 항목 추가
- 이전 Codelab에서 MarsRealEstate 앱을 엽니다. (앱이 없는 경우 여기에서 MarsRealEstateNetwork를 다운로드할 수 있습니다.)
- 앱을 실행하여 어떻게 되는지 확인합니다. (화성에서 사용할 수 있는 가상 속성의 텍스트 세부정보를 표시합니다.)
- build.gradle (Module: app)을 엽니다.
dependencies
섹션에서 다음과 같은 Glide 라이브러리 줄을 추가합니다.
implementation "com.github.bumptech.glide:glide:$version_glide"
버전 번호는 이미 프로젝트 Gradle 파일에 별도로 정의되어 있습니다.
- Sync Now를 클릭하여 새 종속 항목으로 프로젝트를 다시 빌드합니다.
2단계: 뷰 모델 업데이트하기
다음으로 단일 화성 속성의 실시간 데이터를 포함하도록 OverviewViewModel
클래스를 업데이트합니다.
overview/OverviewViewModel.kt
를 엽니다._response
의LiveData
바로 아래에 단일MarsProperty
객체의 내부 (변경 가능) 및 외부 (변경 불가능) 라이브 데이터를 모두 추가합니다.
요청이 있는 경우MarsProperty
클래스 (com.example.android.marsrealestate.network.MarsProperty
)를 가져옵니다.
private val _property = MutableLiveData<MarsProperty>()
val property: LiveData<MarsProperty>
get() = _property
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}"
}
res/layout/fragment_overview.xml
파일을 엽니다.<TextView>
요소에서android:text
를 변경하여property
LiveData
의imgSrcUrl
구성요소에 바인딩합니다.
android:text="@{viewModel.property.imgSrcUrl}"
- 앱을 실행합니다.
TextView
에는 첫 번째 화성 속성의 이미지 URL만 표시됩니다. 지금까지 뷰 모델을 설정하고 이 URL의 라이브 데이터를 설정했습니다.
3단계: 바인딩 어댑터 생성 및 Glide 호출
이제 표시할 이미지의 URL이 있으므로 Glide를 사용하여 이미지를 로드할 차례입니다. 이 단계에서는 결합 어댑터를 사용하여 ImageView
와 연결된 XML 속성에서 URL을 가져오고 Glide를 사용하여 이미지를 로드합니다. 결합 어댑터는 데이터가 변경될 때 맞춤 동작을 제공하기 위해 뷰와 결합된 데이터 사이에 있는 확장 프로그램 메서드입니다. 이 경우 맞춤 동작은 Glide를 호출하여 URL에서 ImageView
로 이미지를 로드하는 것입니다.
BindingAdapters.kt
를 엽니다. 이 파일은 앱 전반에 사용하는 결합 어댑터를 보유하게 됩니다.ImageView
및String
을 매개변수로 사용하는bindImage()
함수를 만듭니다. 함수에@BindingAdapter
주석을 추가합니다.@BindingAdapter
주석은 XML 항목에imageUrl
속성이 있는 경우 이 결합 어댑터를 실행하도록 데이터 결합에 지시합니다.
요청이 있는 경우androidx.databinding.BindingAdapter
및android.widget.ImageView
를 가져옵니다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
bindImage()
함수 내부에서imgUrl
인수의let {}
블록을 추가합니다.
imgUrl?.let {
}
let {}
블록 내부에 아래와 같이 XML의 URL 문자열을Uri
객체로 변환하는 줄을 추가합니다. 요청 시androidx.core.net.toUri
을 가져옵니다.
이미지를 가져오는 서버에 HTTPS 스킴이 필요하므로 최종Uri
객체에서 HTTPS 스킴을 사용해야 합니다. HTTPS 스키마를 사용하려면buildUpon.scheme("https")
을toUri
빌더에 추가합니다.toUri()
메서드는 Android KTX 핵심 라이브러리의 Kotlin 확장 함수이므로String
클래스의 일부인 것처럼 보입니다.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
let {}
내에서Glide.with()
를 호출하여Uri
객체에서ImageView
로 이미지를 로드합니다. 요청이 있는 경우com.bumptech.glide.Glide
를 가져옵니다.
Glide.with(imgView.context)
.load(imgUri)
.into(imgView)
4단계: 레이아웃 및 프래그먼트 업데이트하기
Glide가 이미지를 로드했지만 아직 표시되는 항목이 없습니다. 다음 단계는 이미지를 표시하도록 ImageView
로 레이아웃과 프래그먼트를 업데이트하는 것입니다.
res/layout/gridview_item.xml
를 엽니다. 이 파일은 Codelab의 후반부에서RecyclerView
의 각 항목에 사용할 레이아웃 리소스 파일입니다. 여기서는 단일 이미지만 표시하기 위해 일시적으로 사용합니다.<ImageView>
요소 위에 데이터 결합의<data>
요소를 추가하고OverviewViewModel
클래스에 결합합니다.
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
- 새 이미지 로드 결합 어댑터를 사용하도록
ImageView
요소에app:imageUrl
속성을 추가합니다.
app:imageUrl="@{viewModel.property.imgSrcUrl}"
overview/OverviewFragment.kt
를 엽니다.onCreateView()
메서드에서FragmentOverviewBinding
클래스를 확장하고 결합 변수에 할당하는 줄을 주석 처리합니다. 이 오류는 일시적이며 나중에 다시 돌아옵니다.
//val binding = FragmentOverviewBinding.inflate(inflater)
- 다음 줄을 추가하여
GridViewItemBinding
클래스를 대신 확장합니다. 요청이 있는 경우com.example.android.marsrealestate. databinding.GridViewItemBinding
을 가져옵니다.
val binding = GridViewItemBinding.inflate(inflater)
- 앱을 실행합니다. 이제 결과 목록의 첫 번째
MarsProperty
에 있는 이미지의 사진이 표시됩니다.
5단계: 간단한 로드 및 오류 이미지 추가
Glide는 이미지를 로드하는 동안 자리표시자 이미지를 표시하고 로드 실패 시(예: 이미지가 없거나 손상된 경우) 오류 이미지를 표시함으로써 사용자 경험을 개선할 수 있습니다. 이 단계에서는 이러한 기능을 결합 어댑터와 레이아웃에 추가합니다.
res/drawable/ic_broken_image.xml
을 열고 오른쪽에서 Preview 탭을 클릭합니다. 오류 이미지의 경우 내장된 아이콘 라이브러리에서 사용할 수 있는 손상 이미지 아이콘을 사용합니다. 이 벡터 드로어블은android:tint
속성을 사용하여 아이콘 색상을 회색으로 지정합니다.
res/drawable/loading_animation.xml
를 엽니다. 이 드로어블은<animate-rotate>
태그로 정의된 애니메이션입니다. 애니메이션은 이미지 드로어블loading_img.xml
을 중심점을 축으로 회전시킵니다. (이 애니메이션은 미리보기에 표시되지 않습니다.)
BindingAdapters.kt
파일로 돌아갑니다.bindImage()
메서드에서load()
와into()
사이에apply()
함수를 호출하도록Glide.with()
호출을 업데이트합니다. 요청 시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)
}
}
- 앱을 실행합니다. 네트워크 연결 속도에 따라 Glide가 속성 이미지를 다운로드하고 표시할 때 로드 이미지가 잠시 표시될 수도 있습니다. 그러나 네트워크를 사용 중지해도 손상 이미지 아이콘은 아직 표시되지 않습니다. 이 부분은 Codelab의 마지막 부분에서 수정합니다.
이제 앱이 인터넷에서 숙박 시설 정보를 로드합니다. 첫 번째 MarsProperty
목록 항목의 데이터를 사용하여 뷰 모델에 LiveData
속성을 만들고 이 속성 데이터의 이미지 URL을 사용하여 ImageView
를 채웠습니다. 하지만 앱이 이미지 그리드를 표시하는 것이 목표이므로 GridLayoutManager
와 함께 RecyclerView
를 사용해야 합니다.
1단계: 뷰 모델 업데이트하기
현재 뷰 모델에는 웹 서비스의 응답 목록에 있는 첫 번째 객체인 MarsProperty
객체 하나를 보유하는 _property
LiveData
가 있습니다. 이 단계에서는 MarsProperty
객체의 전체 목록을 보유하도록 LiveData
를 변경합니다.
overview/OverviewViewModel.kt
를 엽니다.- 비공개
_property
변수를_properties
로 변경합니다. 유형을MarsProperty
객체 목록으로 변경합니다.
private val _properties = MutableLiveData<List<MarsProperty>>()
- 외부
property
라이브 데이터를properties
로 바꿉니다. 여기에도 목록을LiveData
유형에 추가합니다.
val properties: LiveData<List<MarsProperty>>
get() = _properties
- 아래로 스크롤하여
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 뷰와 그리드 레이아웃을 사용하도록 앱의 레이아웃과 프래그먼트를 변경합니다.
res/layout/gridview_item.xml
를 엽니다. 데이터 바인딩을OverviewViewModel
에서MarsProperty
로 변경하고 변수 이름을"property"
로 변경합니다.
<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
<ImageView>
에서MarsProperty
객체의 이미지 URL을 참조하도록app:imageUrl
속성을 변경합니다.
app:imageUrl="@{property.imgSrcUrl}"
overview/OverviewFragment.kt
를 엽니다.onCreateview()
에서FragmentOverviewBinding
을 확장하는 줄의 주석 처리를 삭제합니다.GridViewBinding
을 확장하는 줄을 삭제하거나 주석 처리합니다. 이렇게 변경하면 마지막 작업에서 적용한 임시 변경사항이 실행취소됩니다.
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
res/layout/fragment_overview.xml
를 엽니다. 전체<TextView>
요소를 삭제합니다.- 대신 다음
<RecyclerView>
요소를 추가하세요. 이 요소는 단일 항목에GridLayoutManager
및grid_view_item
레이아웃을 사용합니다.
<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
에 바인딩합니다.
overview/PhotoGridAdapter.kt
를 엽니다.- 아래와 같이 생성자 매개변수를 사용하여
PhotoGridAdapter
클래스를 만듭니다.PhotoGridAdapter
클래스는ListAdapter
를 확장합니다. 이 생성자에는 목록 항목 유형, 뷰 홀더,DiffUtil.ItemCallback
구현이 필요합니다.
요청이 있는 경우androidx.recyclerview.widget.ListAdapter
및com.example.android.marsrealestate.network.MarsProperty
클래스를 가져옵니다. 다음 단계에서는 이 생성자에 누락되어 오류를 생성하는 다른 부분을 구현합니다.
class PhotoGridAdapter : ListAdapter<MarsProperty,
PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
}
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")
}
PhotoGridAdapter
클래스 정의 끝에 방금 추가한 메서드 뒤에 아래와 같이DiffCallback
의 컴패니언 객체 정의를 추가합니다.
요청이 있는 경우androidx.recyclerview.widget.DiffUtil
를 가져옵니다.DiffCallback
객체는 비교할 객체 유형MarsProperty
로DiffUtil.ItemCallback
을 확장합니다.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
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") }
areItemsTheSame()
메서드에서 TODO를 삭제합니다.oldItem
및newItem
의 객체 참조가 동일한 경우true
를 반환하는 Kotlin의 참조 동등성 연산자 (===
)를 사용합니다.
override fun areItemsTheSame(oldItem: MarsProperty,
newItem: MarsProperty): Boolean {
return oldItem === newItem
}
areContentsTheSame()
의 경우oldItem
및newItem
의 ID에만 표준 같음 연산자를 사용합니다.
override fun areContentsTheSame(oldItem: MarsProperty,
newItem: MarsProperty): Boolean {
return oldItem.id == newItem.id
}
PhotoGridAdapter
클래스 내에서 컴패니언 객체 아래에RecyclerView.ViewHolder
를 확장하는MarsPropertyViewHolder
의 내부 클래스 정의를 추가합니다.
요청 시androidx.recyclerview.widget.RecyclerView
및com.example.android.marsrealestate.databinding.GridViewItemBinding
을 가져옵니다.MarsProperty
을 레이아웃에 바인딩하기 위한GridViewItemBinding
변수가 필요하므로 이 변수를MarsPropertyViewHolder
에 전달합니다. 기본ViewHolder
클래스는 생성자에 뷰가 있어야 하므로 결합 루트 뷰를 전달합니다.
class MarsPropertyViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
MarsPropertyViewHolder
에서MarsProperty
객체를 인수로 사용하고binding.property
를 이 객체로 설정하는bind()
메서드를 만듭니다. 속성을 설정한 후executePendingBindings()
를 호출하면 업데이트가 즉시 실행됩니다.
fun bind(marsProperty: MarsProperty) {
binding.property = marsProperty
binding.executePendingBindings()
}
onCreateViewHolder()
에서 TODO를 삭제하고 아래의 줄을 추가합니다. 요청이 있는 경우android.view.LayoutInflater
을 가져옵니다.onCreateViewHolder()
메서드는GridViewItemBinding
를 확장하고 상위ViewGroup
컨텍스트의LayoutInflater
를 사용하여 생성된 새MarsPropertyViewHolder
를 반환해야 합니다.
return MarsPropertyViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
onBindViewHolder()
메서드에서 TODO를 삭제하고 아래의 줄을 추가합니다. 여기서getItem()
을 호출하여 현재RecyclerView
위치와 연결된MarsProperty
객체를 가져온 다음 이 속성을MarsPropertyViewHolder
의bind()
메서드에 전달합니다.
val marsProperty = getItem(position)
holder.bind(marsProperty)
4단계: 결합 어댑터 추가 및 부분 연결하기
마지막으로 BindingAdapter
를 사용하여 MarsProperty
객체 목록으로 PhotoGridAdapter
를 초기화합니다. BindingAdapter
를 사용하여 RecyclerView
데이터를 설정하면 데이터 결합이 자동으로 MarsProperty
객체 목록의 LiveData
를 관찰합니다. 그런 다음 MarsProperty
목록이 변경되면 결합 어댑터가 자동으로 호출됩니다.
BindingAdapters.kt
를 엽니다.- 파일 끝에
RecyclerView
와MarsProperty
객체 목록을 인수로 사용하는bindRecyclerView()
메서드를 추가합니다. 이 메서드에@BindingAdapter
주석을 추가합니다.
요청이 있는 경우androidx.recyclerview.widget.RecyclerView
및com.example.android.marsrealestate.network.MarsProperty
를 가져옵니다.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsProperty>?) {
}
bindRecyclerView()
함수 내부에서recyclerView.adapter
를PhotoGridAdapter
로 변환하고 데이터와 함께adapter.submitList()
를 호출합니다. 그러면 새 목록을 사용할 수 있을 때RecyclerView
에 알려줍니다.
요청이 있는 경우 com.example.android.marsrealestate.overview.PhotoGridAdapter
를 가져옵니다.
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
res/layout/fragment_overview.xml
를 엽니다.RecyclerView
요소에app:listData
속성을 추가하고 데이터 결합을 사용하여 이 속성을viewmodel.properties
로 설정합니다.
app:listData="@{viewModel.properties}"
overview/OverviewFragment.kt
를 엽니다.onCreateView()
에서setHasOptionsMenu()
호출 바로 전에binding.photosGrid
의RecyclerView
어댑터를 새PhotoGridAdapter
객체로 초기화합니다.
binding.photosGrid.adapter = PhotoGridAdapter()
- 앱을 실행합니다.
MarsProperty
이미지 그리드가 표시됩니다. 새 이미지를 보기 위해 스크롤하면 앱에서 이미지 자체가 표시되기 전에 로드 진행률 아이콘이 표시됩니다. 비행기 모드를 사용 설정하면 아직 로드되지 않은 이미지가 손상 이미지 아이콘으로 표시됩니다.
이미지를 가져올 수 없을 때 MarsRealEstate 앱은 손상 이미지 아이콘을 표시합니다. 하지만 네트워크가 없으면 앱에서 빈 화면이 표시됩니다.
이는 만족스러운 사용자 환경이 아닙니다. 이 작업에서는 기본 오류 처리를 추가하여 사용자가 현재 상황을 더 잘 파악하도록 합니다. 인터넷을 사용할 수 없는 경우 앱은 연결 오류 아이콘을 표시합니다. 앱은 MarsProperty
목록을 가져오는 동안 로드 애니메이션을 표시합니다.
1단계: 뷰 모델에 상태 추가하기
먼저 뷰 모델에서 LiveData
를 만들어 웹 요청의 상태를 나타냅니다. 로드, 성공, 실패 등 세 가지 상태를 고려합니다. 로드 상태는 await()
호출에서 데이터를 기다리는 동안 발생합니다.
overview/OverviewViewModel.kt
를 엽니다. 파일 상단에서(가져오기 뒤, 클래스 정의 앞에)enum
을 추가하여 사용 가능한 모든 상태를 나타냅니다.
enum class MarsApiStatus { LOADING, ERROR, DONE }
OverviewViewModel
클래스 전체에서 내부 및 외부_response
라이브 데이터 정의를 모두_status
로 바꿉니다. 이 Codelab 앞부분에서_properties
LiveData
지원을 추가했기 때문에 전체 웹 서비스 응답이 사용되지 않았습니다. 여기에서 현재 상태를 추적하려면LiveData
가 필요하므로 기존 변수의 이름을 바꾸면 됩니다.
또한 유형을 String
에서 MarsApiStatus.
로 변경합니다.
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus>
get() = _status
- 아래로 스크롤하여
getMarsRealEstateProperties()
메서드를 찾고 여기에서도_response
을_status
로 업데이트합니다."Success"
문자열을MarsApiStatus.DONE
상태로,"Failure"
문자열을MarsApiStatus.ERROR
로 변경합니다. 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
}
catch {}
블록에서 오류 상태 다음에_properties
LiveData
를 빈 목록으로 설정합니다. 이렇게 하면RecyclerView
가 삭제됩니다.
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_properties.value = ArrayList()
}
2단계: 상태 ImageView용 결합 어댑터 추가하기
이제 뷰 모델에 상태가 있지만 상태 집합일 뿐입니다. 앱 자체에 표시하려면 어떻게 해야 하나요? 이 단계에서는 데이터 결합에 연결된 ImageView
를 사용하여 로드 상태 및 오류 상태의 아이콘을 표시합니다. 앱이 로드 상태이거나 오류 상태일 때 ImageView
가 표시됩니다. 앱에서 로드가 완료되면 ImageView
가 표시되지 않습니다.
BindingAdapters.kt
를 엽니다.ImageView
값과MarsApiStatus
값을 인수로 사용하는bindStatus()
라는 새 결합 어댑터를 추가합니다. 요청이 있는 경우com.example.android.marsrealestate.overview.MarsApiStatus
를 가져옵니다.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
bindStatus()
메서드 내부에when {}
를 추가하여 서로 다른 상태 간에 전환합니다.
when (status) {
}
when {}
내부에 로드 상태(MarsApiStatus.LOADING
)의 사례를 추가합니다. 이 상태의 경우ImageView
를 visible로 설정하고 로드 애니메이션에 할당합니다. 이전 작업에서 Glide에 사용한 것과 동일한 애니메이션 드로어블입니다. 요청이 있는 경우android.view.View
를 가져옵니다.
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
- 오류 상태(
MarsApiStatus.ERROR
)의 사례를 추가합니다.LOADING
상태의 경우와 유사하게 상태ImageView
를 visible로 설정하고 연결 오류 드로어블을 재사용합니다.
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- 완료 상태(
MarsApiStatus.DONE
)의 사례를 추가합니다. 여기서는 성공적인 응답이 있으므로 상태ImageView
의 공개 상태를 사용 중지하여 숨깁니다.
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
3단계: 레이아웃에 상태 ImageView 추가하기
res/layout/fragment_overview.xml
를 엽니다.RecyclerView
요소 아래의ConstraintLayout
내부에 아래와 같이ImageView
를 추가합니다.
이ImageView
에는RecyclerView
와 동일한 제약 조건이 있습니다. 그러나 이미지를 늘려 뷰를 채우는 대신, 너비와 높이가wrap_content
를 사용하여 이미지를 중앙에 배치합니다. 또한app:marsApiStatus
속성이 있어 뷰 모델의 상태 속성이 변경되면 뷰에서BindingAdapter
를 호출합니다.
<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}" />
- 에뮬레이터나 기기에서 비행기 모드를 사용 설정하여 네트워크 연결이 누락된 상황을 시뮬레이션합니다. 앱을 컴파일하고 실행하면 오류 이미지가 표시됩니다.
- 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()
메서드를 호출합니다.
▢ 메서드를 BindingAdapters.kt
라는 Kotlin 파일에 넣습니다.
▢ XML 레이아웃에서 android:adapter
속성을 사용합니다.
▢ 메서드에 @BindingAdapter
주석을 답니다.
다음 강의인
이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.