Coil 라이브러리를 사용해 이미지를 다운로드, 디코드, 캐시할 수 있다.
Coil에는 이미지의 URL, 이미지를 올릴 ImageView 이 두가지가 필요하다.
dependecies
에 추가한다. Coil 다큐먼트 페이지에서 최신 버전을 확인한다.
implementation "io.coil-kt:coil:1.4.0"
Coil 라이브러리는 mavenCentral() 리포지토리를 사용한다.
repositories {
google()
jcenter()
mavenCentral()
}
MarsPhoto
객체를 저장할 MutableLiveData
유형의 _photos
를 추가한다._photos.value = MarsApi.retrofitSerice.getPhotos()[0]
BindingAdapter는 뷰의 속성에 대한 맞춤 setter를 만드는데 사용한다.
예를 들어 일반적으로 text 속성을 수정할 때 setText
메서드를 사용하는데 이런 setter를 정의하는 것.
먼저 overview 패키지 아래 BindingAdapter.kt
파일을 생성한다. 이 파일은 앱 전반에 사용하는 Binding Adapter에 대한 내용을 갖는다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)
}
}
imageUrl
속성이 있는 경우 이 결합 어댑터를 실행하도록 데이터 결합에 지시한다.@BindingAdapter
속성 이름을 매개변수로 사용한다.bindImage
의 매개변수를 보면 첫 번째는 타겟 뷰, 두 번째는 속성에 설정되는 값.let
은 범위 함수로, 호출 체인의 결과에서 함수 하나 이상을 호출하는 데 사용한다.?.
와 함께 사용해 객체가 null이 아닌 경우에만 실행된다.imgUri
메서드로 URL을 Uri 객체로 변환한다.load()
가 Coil 라이브러리에 속한다.grid_view_item.xml
을 수정한다.
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
ImageView의 imageUrl 속성에 binding adapter 사용
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
다음과 같이 placeholder와 error에 해당 이미지를 작성해 로딩 중이나 이미지 에러 발생시 표시할 이미지를 설정한다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?){
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri){
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
그리드 이미지로 사진을 표시하려면 RecyclerView를 사용하도록 코드를 업데이트해야 한다.
photos를 List<MarsPhoto>로 변경해 목록을 받는다.
try catch문에서도 _photos.value = MarsApi.retrofitService.getPhotos()
로 목록을 할당한다.
<data> 태그로 MarsPhoto 유형의 photo변수를 추가하고, imageUrl 속성을 app:imageUrl="@{photo.imgSrcUrl}"
로 수정한다.
ListAdapter
는 RecyclerView.Adapter
의 서브 클래스로 백그라운드 스레드의 목록 간 차이를 계산하는 작업을 포함한다. 여기서 ListAdapter
의 DiffUtil
을 사용하는데, DiffUtil
을 사용하면 리사이클러뷰의 일부 항목이 변경될 때마다 전체 목록이 새로고침되지 않는다는 점이다. 변경된 항목만 바뀐다.
PhotoGridAdapter
라는 새 Kotlin 클래스에 ListAdapter를 구현한다.
class PhotoGridAdapter : ListAdapter<MarsPhoto, PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
두 개 메서드를 implement한다.
override fun onCreateViewHolder( parent: ViewGroup, viewType: Int): MarsPhotoViewHolder {
return MarsPhotoViewHolder(
GridViewItemBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: MarsPhotoViewHolder, position: Int) {
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
}
onCreateViewHolder()
와 onBindViewHolder()
에서 ViewHolder 클래스가 필요하다.
executePendingBindings()
를 호출하면 바로 업데이트가 된다.
class MarsPhotoViewHolder(
private var binding: GridViewItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(marsPhoto: MarsPhoto) {
binding.photo = marsPhoto
// This is important, because it forces the data binding to execute immediately,
// which allows the RecyclerView to make the correct view size measurements
binding.executePendingBindings()
}
}
DiffCallback
companion object가 필요하다. 여기서 두 화성 사진 객체를 비교한다.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
}
BindingAdapter
를 사용하여 MarsPhoto 객체 목록으로 PhotoGridAdapter를 초기화한다.
BindingAdapter
를 사용하여 RecyclerView
데이터를 설정하면 데이터 결합이 자동으로 MarsPhoto
객체 목록의 LiveData
를 관찰한다. 그래서 MarsPhoto
목록이 변경되면 결합 어댑터가 자동으로 호출된다.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
fragment_overview.xml
의 리사이클러뷰에 listData
속성을 추가하고, viewModel.photos
로 설정한다.
app:listData="@{viewModel.photos}"
OverviewFragment
의 리사이클러뷰 어댑터를 PhotoGridAdapter
로 변경한다. binding.photosGrid.adapter = PhotoGridAdapter()
웹 요청의 상태를 표시하는 속성을 만든다. 로드, 성공, 실패 등
enum
은 상수 집합을 저장하는 데이터 타입이다. 다음과 같이 세 상태를 정의하고,
enum class MarsApiStatus { LOADING, ERROR, DONE }
getMarsPhotos()
에서 각 상태를 설정한다. ERROR
인 경우 _photos
도 빈 배열로 설정한다.
private fun getMarsPhotos() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
}
}
ImageView 값과 MarsApiStatus 값을 인수로 사용하는 bindStatus()라는 새 결합 어댑터를 추가한다.
when문을 사용해 각 상태에 따라 동작을 설정한다.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, status: MarsApiStatus?) {
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
}
}
로그를 출력해서 디버깅 하는 방법도 있지만 중단점을 찍어서 순서대로 실행되는 과정을 보는 것도 디버깅에 효과적이다.
빨간색 중단점을 찍고, 중단점 위에서 오른쪽 마우스를 클릭하면 중단 조건을 설정할 수 있다. 반복문에서 i번째 반복문을 디버깅 하고싶을 때 i번째까지 단계별로 실행하지 않고도 조건이 충족되는 시점에 멈추도록 할 수 있다.
디버깅하는 동안 특정 값을 모니터링하려는 경우 Variables 탭에서 검색하지 않고 Watches에 항목을 추가해서 특적 변수를 모니터링할 수 있다. 디버깅 뷰의 변수 창에 Watches라는 빈 창이 있는데 여기에서 New Watch를 추가할 수 있다.