Display an internet image
Step 1: Glide 디펜던시 추가하기
dependencies {
...
implementation "com.github.bumptech.glide:glide:$version_glide"
}
version_glide는 프로젝트 Gradle 파일에 이미 따로 정의되어 있다.
Step 2: 뷰 모델 업데이트 하기
OverviewViewModel.kt
class OverviewViewModel : ViewModel() {
...
private val _property = MutableLiveData<MarsProperty>() // internal(mutable)
val property: LiveData<MarsProperty> // external(immutable)
get() = _property
...
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getProperties()
_response.value = "Success: ${listResult.size} Mars properties retrieved"
if (listResult.size > 0) {
_property.value = listResult[0]
}
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
}
fragment_overview.xml
<TextView
...
android:text="@{viewModel.property.imgSrcUrl}"
...
Step 3: 바인딩 어댑터를 생성하고 Glide 호출하기
BindingAdapters.kt
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide.with(imgView.context)
.load(imgUri)
.into(imgView)
}
}
@BindingAdapter 어노테이션은 데이터 바인딩에게 XML item이 imageUrl 속성을 가질 때 이 바인딩 어댑터가 실행되는 것을 원한다고 알려준다.
이미지를 가져오는 서버가 보안성이 있는 방식을 요구하기 때문에 최종 Uri 오브젝트가 HTTPS 방식을 사용하려고 한다. HTTPS 방식을 사용하려면 buildUpon().scheme("https")를 toUri 빌더에 덧붙인다. toUri() 메서드는 Android KTX core 라이브러리로부터 상속 받은 Kotlin 함수이므로 String 클래스의 일부처럼 보인다.
Step 4: 레이아웃과 프래그먼트 업데이트 하기
grid_view_item.xml
<data>
<variable
name="viewModel"
type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
<ImageView
...
app:imageUrl="@{viewModel.property.imgSrcUrl}"
...
OverviewFragment.kt
// val binding = FragmentOverviewBinding.inflate(inflater)
val binding = GridViewItemBinding.inflate(inflater)
Step 5: 간단한 로딩 이미지와 에러 이미지 추가하기
ic_broken_image.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
...
android:tint="#A9A9AC"
...>
...
</vector>
android:tint 속성을 사용해 아이콘에 색깔을 입힌 것을 알 수 있다.
loading_animation.xml
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/loading_img"
android:pivotX="50%"
android:pivotY="50%" />
<animated-rotate> 태그로 정의된 애니메이션이다.
BindingAdapters.kt
...
Glide.with(imgView.context)
.load(imgUri)
.apply(RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(imgView)
...
Display a grid of images with a RecyclerView
Step 1: 뷰 모델 업데이트 하기
OverviewViewModel.kt
class OverviewViewModel : ViewModel() {
...
private val _properties = MutableLiveData<List<MarsProperty>>() // internal(mutable)
val properties: LiveData<List<MarsProperty>> // external(immutable)
get() = _properties
...
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
_properties.value = MarsApi.retrofitService.getProperties()
_response.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
}
Step 2: 레이아웃과 프래그먼트 업데이트 하기
grid_view_item.xml
<layout ...>
<data>
<variable
name="property"
type="com.example.android.marsrealestate.network.MarsProperty" />
</data>
<ImageView
...
app:imageUrl="@{property.imgSrcUrl}"
... />
</layout>
OverviewFragment.kt
val binding = FragmentOverviewBinding.inflate(inflater)
fragment_overview.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.marsrealestate.MainActivity">
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>
Step 3: photo grid adapter 추가하기
PhotoGridAdapter.kt
class PhotoGridAdapter : 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()
}
}
}
PhotoGridAdapter는 ListAdapter를 상속하고 ListAdapter의 생성자는 리스트 아이템 타입, 뷰 홀더, 그리고 DiffUtil.ItemCallback 구현을 필요로 한다.
PhotoGridAdapter를 클릭하고 Control+i를 누르면 ListAdapter의 메서드를 구현할 수 있다. DiffCallback의 메서드도 같은 방법으로 구현할 수 있다.
executePendingBindings()는 업데이트가 즉시 실행되도록 한다.
Step 4: 바인딩 어댑터를 추가하고 연결하기
BindingAdapters.kt
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsProperty>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
as를 사용해 recyclerView.adapter를 PhotoGridAdapter로 형변환 한다.
submitList()는 새 리스트가 있을 때 RecyclerView에게 알려준다.
fragment_overview.xml
<androidx.recyclerview.widget.RecyclerView
...
app:listData="@{viewModel.properties}"
...
OverviewFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
...
binding.photosGrid.adapter = PhotoGridAdapter()
setHasOptionsMenu(true)
return binding.root
}
스크롤을 내리다 보면 로딩 애니메이션을 볼 수 있다.
Add error handling in RecyclerView
Step 1: 뷰 모델에 상태 추가하기
OverviewViewModel.kt
enum class MarsApiStatus { LOADING, ERROR, DONE }
/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {
// The internal MutableLiveData String that stores the most recent status
private val _status = MutableLiveData<MarsApiStatus>()
// The external immutable LiveData for the status MarsApiStatus
val status: LiveData<MarsApiStatus>
get() = _status
...
/**
* Sets the value of the status LiveData to the Mars API status.
*/
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_properties.value = MarsApi.retrofitService.getProperties()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_properties.value = ArrayList()
}
}
}
}
Step 2: 상태 ImageView를 위해 바인딩 어댑터 추가하기
BindingAdapters.kt
...
@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
}
}
}
Step 3: 레이아웃에 상태 ImageView 추가하기
fragment_overview.xml
<layout ...>
...
<androidx.constraintlayout.widget.ConstraintLayout
...>
...
<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}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
app:marsApiStatus 속성은 뷰 모델의 상태 값이 변할 때 BindingAdapter를 호출한다.
Homework
Answer these questions
Q1. 1 / Q2. 2 / Q3. 4
'안드로이드 Android > Android Study Jams' 카테고리의 다른 글
9. Repository and workManager - Add a Repository (0) | 2021.01.29 |
---|---|
8. Connect to the internet - Filter and create views with internet data (0) | 2021.01.28 |
8. Connect to the internet - Get data from the internet (0) | 2021.01.28 |
7. Databases and RecyclerView - RecyclerView Fundamentals (0) | 2021.01.21 |
7. Databases and RecyclerView - Use LiveData to control Button states (0) | 2021.01.21 |