[안드로이드 코드랩] Android Kotlin Fundamentals:8.2 Loading and displaying images from the Internet

홍석규·2022년 2월 21일
0

지난 코드랩에서 retrofit을 이용한 api 호출방법, Moshi 라이브러리를 이용한 json → 코틀린 객체 역직렬화, api 호출 코루틴으로 리팩토링 등을 진행했다. 이제는 서버로부터 받는 응답값을 파싱 해 사용해보자.

무엇을 배우는가

  • 현재 코드랩에서 사용하고 있는 api url은 img url을 응답값으로 내려준다. 이번 챕터에서는 Glide Library를 이용해 web url로부터 image를 가져오고 표시하는 방법 을 배우게 된다.
  • 또한 image를 내려받고 ui에 표시할 때 발생할 수 있는 에러들을 핸들링 하는 방법을 배우게 된다.

Display an internet image

서버로부터 사진을 받아 ui에 표시하는 과정은 상당히 직관적으로 보이지만, 실제로는 몇가지 작업이 필요하다.

이미지를 서버로부터 다운 받아야하고 내부적으로 저장 한 다음, 압축되어 있는 포맷에서 안드로이드가 사용할 수 있는 이미지 포맷으로 변경하는 과정이 필요하다

  • 이미지는 메모리 상에 저장되거나, 스토리지 기반 저장소에 저장되거나, 상황에 따라 둘다 저장이 필요할 수도 있다. 해당 과정들은 모두 백그라운드 스레드에서 수행 되어야 ui 스레드가 버벅 거리지 않을것이다.
  • 또한 network 통신과 cpu 성능을 최대화 하기 위해 한번에 한개 이상의 이미지를 받아와서 디코딩 하는것이 좋을것이다. 이번 챕터에서 어떻게 효율적으로 이미지를 서버로부터 받아올 수 있을지 배울 수 있다.

Glide

글라이드는 안드로이드에서 사용할 수 있는 빠르고 효율적인 미디어 관리 및 이미지 로딩 프레임워크다. 미디어 디코딩, 메모리 또는 디스크 저장, 그리고 리소스 풀링등을 간단하고 쉽게 사용할 수 있도록 인터페이스화 시켜두었다.

글라이드는 기본적으로 아래 2가지를 필요로한다.

  • 다운로드 받아야 할 이미지가 있는 url
  • 실제로 이미지를 화면에 표시하기 위해 사용되는 ImageView 객체

Glide 의존성 추가하기

gradle 파일에 아래처럼 의존성을 추가하자.

  • implementation "com.github.bumptech.glide:glide:$version_glide"

ViewModel 수정

우선 Glide를 사용 해보기 위해 api가 내려주는 image url list중에 첫번째 데이터를 이용해 화면에 띄워보는 작업을 해보자. 아래 코드를 추가하자.

private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  • 뷰모델을 사용할때 구글에서 가장 권장하는 방법이다. 데이터의 변경이 가능한 LiveData는 MutableLiveData로 감싸서 private으로 선언한다. 그리고 일반 LiveData로 감싸서 ViewModel외부에서 참조할 경우 read만 가능 하게 선언 하는것이다.
    • 이렇게 사용하면 외부에서 직접 변수를 참조해서 데이터의 변경을 발생 시키지 않을 수 있다.

지난 학습 내용에서 코루틴으로 적용 해두었던 api 호출 로직의 응답부분에 다음처럼 추가해주자.

val listResult = MarsApi.retrofitService.getProperties()
if (listResult.size > 0) {
	_property.value = listResult[0]
}
  • 비동기로 api 호출한 데이터가 코루틴에 의해 listResult로 값이 들어올 경우 listResult.size가 0보다 크면 첫번째 데이터를 MutableLiveData에 넣어준다.

Databinding을 해둔 fragment_overview의 TextView를 다음처럼 변경하자

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.property.imgSrcUrl}"
  • 이렇게 되면 TextView가 ViewModel의 liveData를 observing하게 되고 liveData 갱신이 발생할 경우 text에 자동으로 viewModel.property.imgSrcUrl 값이 들어가게 될 것이다.

바인딩 어댑터 생성, Glide 호출하기

화면에 url이 잘 표시된다면 이제 해당 url을 이용해 이미지 데이터를 받아서 화면에 뿌려줄 것이다. 이번 코드랩에서는 binding Adapter 를 이용해서 ImageView의 xml 속성에서 url을 가져올 수 있게 구현 할것이다.

  • binding Adapter는 view의 속성을 커스텀으로 추가해 줄 수 있고 DataBinding과 함께 사용한다면 상당히 편리하다.(binding Adapter에 대해 추후 따로 공부하고 정리 해 볼 계획이다.)
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  • @BindingAdapter 어노테이션을 통해 어떤 속성으로 사용할 것인지를 정의해준다.
  • BindingAdapter 메서드의 파라미터가 2개 있는데 첫번째는 ImageView, 두번째는 서버로부터 이미지를 받아올 imgUrl 데이터다.

이렇게 정의 해주면 실제로 다음과 같이 사용 가능하다.

app:imageUrl="@{viewModel.property.imgSrcUrl}"

viewModel.propery.imgSrcUrl은 String value이며 해당 value가 bindImage 메서드의 imgUrl 파라미터로 전달되어 bindImage 메서드가 동작하는 로직이다.

bindImage메서드 내에서 imgUrl을 이용해 서버에 image data를 호출하는 로직을 추가 해주자.

fun bindingImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

        Glide.with(imgView.context)
            .load(imgUri)
            .into(imgView)
    }
}
  • 먼저 Url 스트링을 그대로 사용할 수 없고 Uri 객체로 변환 시켜주는 과정이 필요하다.
  • Glide 라이브러리를 이용해서 imgUrl을 이용해 img를 load하고, 받아온 값을 imgView에 넣어주는 로직을 추가해준다.
    • 메서드 체이닝 형식으로 되어있고 코드 가독성이 상당히 좋다.

loading, error image 추가하기

Glide는 이미지를 서버로부터 받아오는 동안 placeHolder 이미지를 표시하고, 로드 실패시 에러 이미지를 표시하는 기능을 지원해준다. Glide를 이용한 로직을 변경해보자.

Glide.with(imgView.context)
     .load(imgUri)
     .apply(RequestOptions()
     .placeholder(R.drawable.loading_animation)
     .error(R.drawable.ic_broken_image))
     .into(imgView)
  • 단순히 호출하고 저장하는 로직이외에 apply메서드가 추가되었다.

  • apply메서드는 요청시 옵션을 추가해줄 수 있게 해준다. 파라미터로 RequestOptions 객체를 전달 해주면 내부적으로 requestOptions을 적용 시키는 로직을 확인할 수 있다.

  • api 호출중 일때 표시할 placeholder img와 error가 발생한 경우 표시할 img를 추가해줬다.

Display a grid of image with a RecyclerView

이제 리싸이클러뷰를 이용해서 이미지를 그리드뷰로 표시해보자. 먼저 overView 화면에서 그리드형태로 데이터를 표시하기 위해 viewModel에 List 타입의 라이브데이터를 생성한다.

private val _properties = MutableLiveData<List<MarsProperty>>() 
val properties: LiveData<List<MarsProperty>>
        get() = _properties

그리고 받아온 첫번째 데이터만 갱신해주고 있던 try{} 블럭내의 코드를 List로 받아올 수 있게 변경해준다.

try {
    _properties.value = MarsApi.retrofitService.getProperties()
    _response.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}
  • 이제 retrofit을 이용해 받아온 MarsProperty타입의 데이터들을 담은 라이브데이터를 이용해서 화면에 표시해 보자.

그리드 어댑터 만들기 (리싸이클러뷰)

xml layout을 다루는 코드는 정리에서 제외했다.

지난 코드랩에서 학습 했던 내용을 다시 복습한다는 느낌으로 Diffutils를 이용하는 리싸이클러뷰 어댑터를 생성하자

class PhotoGridAdapter : androidx.recyclerview.widget.ListAdapter<MarsProperty, PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
        return MarsPropertyViewHolder(GridViewItemBinding.inflate(LayoutInflater.from(parent.context)))
    }

    override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
        val marsProperty = getItem(position)
        holder.bind(marsProperty)

    }

    companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
        override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
            return oldItem.id == newItem.id
        }
    }

    class MarsPropertyViewHolder(private var binding: GridViewItemBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(marsProperty: MarsProperty) {
            binding.property = marsProperty
            binding.executePendingBindings()
        }
    }
}
  • 리싸이클러뷰의 onCreateViewHolder에서는 ViewHolder 객체로 사용할 MarsPropertyViewHolder 객체를 생성하고 반환해준다. 내부적으로 데이터바인딩을 사용하고 있기 때문에 Databinding inflate 과정도 추가해준다.
  • onBindViewHolder에서는 onCreateViewHolder에서 생성된 ViewHolder 객체에 데이터를 바인딩 하는 과정을 거친다. getItem(position) 메서드를 호출하면 데이터 리스트를 ListAdpater에서 관리 해주기 때문에 position에 해당하는 MarsProperty 객체를 얻을 수 있다.
  • DiffCallback 객체는 DiffUtil을 이용해 리싸이클러뷰 데이터를 업데이트 할 때 데이터의 일치 여부를 어댑터가 판단할 수 있는 기준을 정의해준다. areItemsTheSame이 true를 반환 하면 areContentsTheSame 메서드가 호출된다.

이제 리싸이클러뷰에서 사용할 수 있게 BindingAdapter를 만들어주자.

@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
		val adapter = recyclerView.adapter as PhotoGridAdapter
		adapter.submitList(data)
}
  • listData 라는 BindingAdapter를 생성해줬다. 첫번째 파라미터는 해당 바인딩 어댑터 메서드가 적용될 뷰를 넘겨줘야하며, 두번째는 바인딩 어댑터 메서드에서 사용할 데이터를 넘겨줘야한다.
  • PhtoGridAdapter를 가져와서 submitList(data)를 호출할경우 adapter에서 DiffCallback을 이용해 필요한 데이터만 새로 갱신하는 작업을 거친다.
  • xml 코드를 정리에서 빼버려서 위 바인딩 어댑터 메서드 사용 방법을 간단하게 적어두면 다음과 같다.
    <RecyclerView>
    ....
    	app:listData = "@{viewModel.properties}"
    ...
    
    </RecyclerView>
    • viewMode의 properties는 라이브데이터다. 위에서 try catch 구문으로 감싼 retrofit api 호출의 결과값이 properties에 담기게 되며 RecycierView에서 데이터를 옵저빙 해둔다.
    • properties데이터가 갱신이 되면 데이터변화가 발생해서 listData로 정의한 bindingAdapter 메서드가 동작한다.
    • bindRecyclerView 메서드의 data파라미터로 viewModel.properties가 들어가게 되고 해당 데이터를 adapter.submitList(data) 를 통해 리싸이클러뷰 어댑터에게 데이터변경을 알려주게 되는것이다.
profile
학습한 내용을 공유하고 기록합니다.

0개의 댓글