소개
이전 Codelab에서는 웹 서비스에서 데이터를 가져와 응답을 Kotlin 객체로 파싱하는 방법을 배웠습니다. 이 지식을 기반으로 이 Codelab에서는 웹 URL에서 사진을 로드하고 표시합니다. 또한 RecyclerView를 빌드하고 이 뷰를 사용해 개요 페이지에 이미지 그리드를 표시하는 방법을 다시 확인합니다.
기본 요건
- 프래그먼트를 만들고 사용하는 방법
- Retrofit 라이브러리와 Moshi 라이브러리를 사용하여 REST 웹 서비스에서 JSON을 검색하고 이 데이터를 Kotlin 객체로 파싱하는 방법
RecyclerView로 그리드 레이아웃을 구성하는 방법Adapter,ViewHolder,DiffUtil의 작동 방식
학습할 내용
- Coil 라이브러리를 사용하여 웹 URL에서 이미지를 로드하고 표시하는 방법
RecyclerView및 그리드 어댑터를 사용하여 이미지 그리드를 표시하는 방법- 이미지를 다운로드하고 표시할 때 발생할 수 있는 오류를 처리하는 방법
빌드할 항목
- 화성 데이터에서 이미지 URL을 가져오도록 MarsPhotos 앱을 수정하고 Coil을 사용해 이 이미지를 로드하고 표시합니다.
- 앱에 로드 애니메이션과 오류 아이콘을 추가합니다.
RecyclerView를 사용하여 화성 이미지의 그리드를 표시합니다.RecyclerView에 상태 및 오류 처리를 추가합니다.
필요한 항목
- 최신 버전의 Chrome과 같은 최신 웹브라우저가 설치된 컴퓨터
- 컴퓨터에서 인터넷 액세스가 가능해야 함
이 Codelab에서는 이전 Codelab의 MarsPhotos 앱을 계속 사용하여 작업합니다. MarsPhotos 앱은 웹 서비스에 연결하여 Retrofit을 사용해 검색된 Kotlin 객체 수를 가져와 표시합니다. 이 Kotlin 객체에는 NASA의 화성 탐사 로봇이 화성 표면에서 촬영한 실제 사진의 URL이 포함되어 있습니다.
이 Codelab에서 빌드하는 버전의 앱은 화성 사진을 이미지 그리드로 보여주는 개요 페이지를 채웁니다. 이미지는 앱이 Mars 웹 서비스에서 검색한 데이터의 일부입니다. 앱은 Coil 라이브러리를 사용하여 이미지를 로드해 표시하고 RecyclerView를 사용하여 이미지의 그리드 레이아웃을 만듭니다. 또한, 앱은 네트워크 오류를 적절히 처리합니다.

웹 URL에서 사진을 표시하는 것은 간단해 보일 수도 있지만 제대로 작동하려면 엔지니어링이 상당히 필요합니다. 이미지를 다운로드하고, 내부적으로 저장하고, 압축 형식에서 Android가 사용할 수 있는 이미지로 디코딩해야 합니다. 이미지는 메모리 내 캐시나 저장소 기반 캐시 또는 두 캐시 모두에 캐시해야 합니다. UI가 응답성을 유지하기 위해 이 모든 작업은 우선순위가 낮은 백그라운드 스레드에서 이루어져야 합니다. 또한 최상의 네트워크 및 CPU 성능을 위해 둘 이상의 이미지를 한 번에 가져오고 디코딩하는 것이 좋습니다.
다행히 커뮤니티에서 개발한 Coil이라는 라이브러리를 사용하여 이미지를 다운로드하고 버퍼링 및 디코딩하고 캐시할 수 있습니다. Coil을 사용하지 않으면 해야 할 작업이 훨씬 더 많습니다.
Coil에는 기본적으로 다음 두 가지가 필요합니다.
- 로드하고 표시할 이미지의 URL
- 이미지를 실제로 표시하는
ImageView객체
이 작업에서는 Coil을 사용하여 Mars 웹 서비스의 단일 이미지를 표시하는 방법을 알아봅니다. 웹 서비스에서 반환되는 사진 목록에 있는 첫 번째 화성 사진의 이미지를 표시합니다. 다음은 전과 후의 스크린샷입니다.
|
|
Coil 종속 항목 추가하기
- 이전 Codelab의 MarsPhotos 솔루션 앱을 엽니다.
- 앱을 실행하여 어떻게 되는지 확인합니다. (검색한 화성 사진의 총 개수가 표시됨)
- build.gradle (Module: app)을 엽니다.
dependencies섹션에서 다음과 같은 Coil 라이브러리 줄을 추가합니다.
// Coil
implementation "io.coil-kt:coil:1.1.1"
Coil 문서 페이지에서 최신 버전의 라이브러리를 확인하고 업데이트하세요.
- Coil 라이브러리는
mavenCentral()저장소에서 호스팅되어 제공됩니다. build.gradle (Project: MarsPhotos)의 맨 위repositories블록에mavenCentral()을 추가합니다.
repositories {
google()
jcenter()
mavenCentral()
}
- Sync Now를 클릭하여 새 종속 항목으로 프로젝트를 다시 빌드합니다.
ViewModel 업데이트하기
이 단계에서는 LiveData 속성을 OverviewViewModel 클래스에 추가하여 수신된 Kotlin 객체인 MarsPhoto를 저장합니다.
overview/OverviewViewModel.kt를 엽니다._status속성 선언 바로 아래에 단일MarsPhoto객체를 저장할 수 있는MutableLiveData유형의 새로운 변경 가능 속성_photos를 추가합니다.
private val _photos = MutableLiveData<MarsPhoto>()
요청이 있는 경우 com.example.android.marsphotos.network.MarsPhoto를 가져옵니다.
_photos선언 바로 아래에LiveData<MarsPhoto>유형의photos라는 공개 지원 필드를 추가합니다.
val photos: LiveData<MarsPhoto> = _photos
getMarsPhotos()메서드의try{}블록 내에서 웹 서비스에서 검색되는 데이터를listResult.로 설정하는 줄을 찾습니다.
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- 검색되는 첫 번째 화성 사진을 새 변수
_photos에 할당합니다.listResult를_photos.value로 변경합니다. 색인0에 첫 번째 사진 URL을 할당합니다. 이렇게 하면 오류가 발생하며, 이 오류는 나중에 해결합니다.
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
...
}
- 다음 줄에서
status.value를 다음과 같이 업데이트합니다.listResult대신 새 속성의 데이터를 사용합니다. 사진 목록의 첫 번째 이미지 URL을 표시합니다.
try {
...
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 이제 전체
try{}블록은 다음과 같습니다.
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 앱을 실행합니다.
TextView가 이제 첫 번째 화성 사진의 URL을 표시합니다. 지금까지ViewModel을 설정하고 이 URL의LiveData를 설정했습니다.

결합 어댑터 사용하기
결합 어댑터는 뷰의 맞춤 속성을 위한 맞춤 setter를 만드는 데 사용되는 주석 처리된 메서드입니다.
일반적으로 XML에서 android:text="Sample Text" 코드를 사용하여 속성을 설정하는 경우 Android 시스템은 setText(String: text) 메서드를 통해 설정되는 text 속성과 같은 이름의 setter를 자동으로 찾습니다. setText(String: text) 메서드는 Android 프레임워크에서 제공하는 일부 뷰의 setter 메서드입니다. 결합 어댑터를 사용하여 유사한 동작을 맞춤설정할 수 있습니다. 데이터 결합 라이브러리에서 호출되는 맞춤 속성과 맞춤 로직을 제공할 수 있습니다.
예:
단순히 이미지 뷰에서 드로어블 이미지를 설정하는 setter를 호출하는 것보다 더 복잡한 무언가를 하려면 인터넷에서 UI 스레드(기본 스레드)의 로드 이미지를 가져오는 것을 고려합니다. 먼저 맞춤 속성을 선택하여 ImageView에 이미지를 할당합니다. 다음 예에서는 imageUrl입니다.
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"/>
코드를 추가하지 않으면 시스템이 ImageView에서 setImageUrl(String) 메서드를 찾으며, 찾지 못해 오류가 발생합니다. 프레임워크에서 제공되지 않는 맞춤 속성이기 때문입니다. app:imageUrl 속성을 구현하여 ImageView로 설정하는 방법을 만들어야 합니다. 이렇게 하려면 결합 어댑터(주석 처리된 메서드)를 사용합니다.
결합 어댑터의 예:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
// Load the image in the background using Coil.
}
}
}
@BindingAdapter 주석은 속성 이름을 매개변수로 사용합니다.
bindImage 메서드에서 첫 번째 메서드 매개변수는 타겟 뷰의 유형이고 두 번째 매개변수는 속성에 설정되는 값입니다.
메서드 내부에서 Coil 라이브러리는 UI 스레드에서 이미지를 로드하여 ImageView로 설정합니다.
결합 어댑터 만들기 및 Coil 사용하기
com.example.android.marsphotos패키지에서BindingAdapters라는 Kotlin 파일을 만듭니다. 이 파일은 앱 전반에 사용하는 결합 어댑터를 보유하게 됩니다.

BindingAdapters.kt에서 매개변수로ImageView및String을 사용하는bindImage()함수를 만듭니다.
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
요청이 있는 경우 android.widget.ImageView를 가져옵니다.
- 함수에
@BindingAdapter주석을 추가합니다.@BindingAdapter주석은 뷰 항목에imageUrl속성이 있는 경우 이 결합 어댑터를 실행하도록 데이터 결합에 지시합니다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
요청이 있는 경우 androidx.databinding.BindingAdapter를 가져옵니다.
let 범위 함수
let은 Kotlin의 범위 함수 중 하나로, 이 함수를 사용하여 객체의 컨텍스트 내에서 코드 블록을 실행할 수 있습니다. Kotlin에는 5가지 범위 함수가 있습니다. 자세히 알아보려면 문서를 참고하세요.
사용:
let은 호출 체인의 결과에서 함수 하나 이상을 호출하는 데 사용됩니다.
let 함수는 안전 호출 연산자( ?.)와 함께 객체에서 null 안전 연산을 실행하는 데 사용됩니다. 이 경우 let 코드 블록은 객체가 null이 아닌 경우에만 실행됩니다.
bindImage()함수 내부에서 안전 호출 연산자를 사용하여let{}블록을imageURL인수에 추가합니다.
imgUrl?.let {
}
let{}블록 내부에서toUri()메서드를 사용해 URL 문자열을Uri객체로 변환하도록 다음 줄을 추가합니다. HTTPS 스키마를 사용하려면buildUpon.scheme("https")을toUri빌더에 추가합니다.build()를 호출하여 객체를 빌드합니다.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
요청이 있는 경우 androidx.core.net.toUri를 가져옵니다.
let{}블록 내부에서imgUri선언 다음에 Coil의load(){}를 사용하여imgUri객체에서imgView로 이미지를 로드합니다.
imgView.load(imgUri) {
}
요청이 있는 경우 coil.load를 가져옵니다.
- 전체 메서드는 다음과 같습니다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)
}
}
레이아웃 및 프래그먼트 업데이트하기
이전 섹션에서 Coil 이미지 라이브러리를 사용하여 이미지를 로드했습니다. 이미지를 화면에서 보기 위한 다음 단계는 ImageView를 새 속성으로 업데이트하여 단일 이미지를 표시하는 것입니다.
Codelab 후반에 res/layout/grid_view_item.xml을 RecyclerView의 각 그리드 항목을 위한 레이아웃 리소스 파일로 사용합니다. 이 작업에서는 이 파일을 임시로 사용하여 이전 작업에서 검색한 이미지 URL을 사용하여 이미지를 표시합니다. 임시로 fragment_overview.xml 대신 이 레이아웃 파일을 사용하게 됩니다.
res/layout/grid_view_item.xml을 엽니다.<ImageView>요소 위에 데이터 결합의<data>요소를 추가하고OverviewViewModel클래스에 결합합니다.
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
- 새 이미지 로드 결합 어댑터를 사용하도록
ImageView요소에app:imageUrl속성을 추가합니다.photos에는MarsPhotos가 서버에서 검색한 목록이 포함된다는 점을 기억하세요. 첫 번째 항목 URL을imageUrl속성에 할당합니다.
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
... />
overview/OverviewFragment.kt를 엽니다.onCreateView()메서드에서FragmentOverviewBinding클래스를 확장하고 결합 변수에 할당하는 줄을 주석 처리합니다. 이 줄을 삭제한 것을 이유로 오류가 표시됩니다. 이 오류는 일시적이며 나중에 수정합니다.
//val binding = FragmentOverviewBinding.inflate(inflater)
fragment_overview.xml.대신grid_view_item.xml을 사용합니다. 다음 줄을 추가하여GridViewItemBinding클래스를 대신 확장합니다.
val binding = GridViewItemBinding.inflate(inflater)
요청이 있는 경우 com.example.android.marsphotos. databinding.GridViewItemBinding을 가져옵니다.
- 앱을 실행합니다. 이제 단일 화성 이미지가 표시됩니다.

로드 이미지와 오류 이미지 추가하기
Coil을 사용하면 이미지를 로드하는 동안 자리표시자 이미지를 표시하고 로드 실패 시(예: 이미지가 없거나 손상된 경우) 오류 이미지를 표시함으로써 사용자 경험을 개선할 수 있습니다. 이 단계에서는 이러한 기능을 결합 어댑터에 추가합니다.
res/drawable/ic_broken_image.xml을 열고 오른쪽에서 Design 탭을 클릭합니다. 오류 이미지의 경우 내장된 아이콘 라이브러리에서 사용할 수 있는 손상 이미지 아이콘을 사용합니다. 이 벡터 드로어블은android:tint속성을 사용하여 아이콘 색상을 회색으로 지정합니다.

res/drawable/loading_animation.xml을 엽니다. 이 드로어블은 이미지 드로어블loading_img.xml을 중심점을 축으로 회전시키는 애니메이션입니다. (이 애니메이션이 미리보기에 표시되지 않습니다.)

BindingAdapters.kt파일로 돌아갑니다.bindImage()메서드에서 다음과 같이imgView.load(imgUri)호출을 업데이트하여 후행 람다를 추가합니다. 이 코드는 로드하는 동안 사용할 자리표시자 로드 이미지(loading_animation드로어블)를 설정합니다. 또한 이 코드는 이미지를 로드하지 못한 경우 사용할 이미지(broken_image드로어블)를 설정합니다.
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
- 이제 전체
bindImage()메서드는 다음과 같습니다.
@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)
}
}
}
- 앱을 실행합니다. 네트워크 연결 속도에 따라 Glide가 속성 이미지를 다운로드하고 표시할 때 로드 이미지가 잠시 표시될 수도 있습니다. 그러나 네트워크를 사용 중지해도 손상 이미지 아이콘은 아직 표시되지 않습니다. 이 부분은 Codelab의 마지막 작업에서 수정합니다.

overview/OverviewFragment.kt에서 적용한 임시 변경사항을 되돌립니다.onCreateview()메서드에서FragmentOverviewBinding을 확장하는 줄의 주석 처리를 삭제합니다.GridViewIteMBinding을 확장하는 줄을 삭제하거나 주석 처리합니다.
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
이제 앱이 인터넷에서 화성 사진을 로드합니다. 첫 번째 MarsPhoto 목록 항목의 데이터를 사용하여 ViewModel에 LiveData 속성을 만들고 이 화성 사진 데이터의 이미지 URL을 사용하여 ImageView를 채웠습니다. 하지만 앱이 이미지 그리드를 표시하는 것이 목표이므로, 이 작업에서는 그리드 레이아웃 관리자와 함께 RecyclerView를 사용하여 이미지 그리드를 표시합니다.
뷰 모델 업데이트하기
이전 작업에서는 OverviewViewModel에서 웹 서비스의 응답 목록에 있는 첫 번째 객체인 MarsPhoto 객체 하나를 보유하는 LiveData 객체 _photos를 추가했습니다. 이 단계에서는 MarsPhoto 객체의 전체 목록을 보유하도록 이 LiveData를 변경합니다.
overview/OverviewViewModel.kt를 엽니다._photos유형을MarsPhoto객체 목록으로 변경합니다.
private val _photos = MutableLiveData<List<MarsPhoto>>()
- 또한 지원 속성
photos유형을List<MarsPhoto>유형으로 바꿉니다.
val photos: LiveData<List<MarsPhoto>> = _photos
getMarsPhotos()메서드 내에서 아래로 스크롤하여try {}블록을 찾습니다.MarsApi.retrofitService.getPhotos()는
MarsPhoto 객체의 목록을 반환하며, 이 목록을 _photos.value에 할당하면 됩니다.
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
- 이제 전체
try/catch블록은 다음과 같습니다.
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
그리드 레이아웃
RecyclerView의 GridLayoutManager는 아래와 같이 데이터를 스크롤 가능한 그리드로 배치합니다.

디자인 관점에서 볼 때 그리드 레이아웃은 아이콘이나 이미지로 표현할 수 있는 목록(예: 화성 사진 탐색 앱 내의 목록)에 가장 적합합니다.
그리드 레이아웃에 항목을 배치하는 방법
그리드 레이아웃은 항목을 행과 열의 그리드로 정렬합니다. 세로 스크롤을 사용하는 경우 기본적으로 행의 각 항목은 '스팬' 하나를 차지합니다. 한 항목이 여러 스팬을 차지할 수 있습니다. 아래 예의 경우 한 스팬이 한 열의 너비, 즉 3과 동일합니다.
아래의 두 예에서 각 행은 스팬 세 개로 구성됩니다. 기본적으로 GridLayoutManager는 개발자가 지정한 스팬 수까지 각 항목을 한 스팬에 배치합니다. 스팬 수에 도달하면 다음 줄로 넘어갑니다.
|
|
Recyclerview 추가하기
이 단계에서는 단일 이미지 뷰가 아닌 그리드 레이아웃과 함께 Recycler 뷰를 사용하도록 앱의 레이아웃을 변경합니다.
layout/gridview_item.xml을 엽니다.viewModel데이터 변수를 삭제합니다.<data>태그 내부에MarsPhoto유형의 다음photo변수를 추가합니다.
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
<ImageView>에서MarsPhoto객체의 이미지 URL을 참조하도록app:imageUrl속성을 변경합니다. 이렇게 변경하면 이 이전 작업에서 적용한 임시 변경사항이 실행취소됩니다.
app:imageUrl="@{photo.imgSrcUrl}"
layout/fragment_overview.xml을 엽니다. 전체<TextView>요소를 삭제합니다.- 대신 다음
<RecyclerView>요소를 추가합니다. ID를photos_grid로 설정하고width속성과height속성을0dp로 설정하여 상위ConstraintLayout을 채웁니다. 그리드 레이아웃을 사용할 것이므로layoutManager속성을androidx.recyclerview.widget.GridLayoutManager로 설정합니다. 열이 두 개가 되도록spanCount를2로 설정합니다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
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" />
- 위의 코드가 Design 뷰에서 어떻게 표시되는지 미리보려면
tools:itemCount를 사용하여 레이아웃에 표시되는 항목의 수를16으로 설정합니다.itemCount속성은 Layout Editor에서 Preview 창에 렌더링해야 하는 항목의 수를 지정합니다.tools:listitem을 사용하여 목록 항목의 레이아웃을grid_view_item으로 설정합니다.
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
- Design 뷰로 전환하면 다음 스크린샷과 같은 미리보기가 표시됩니다. 화성 사진은 아니지만 recyclerview 그리드 레이아웃이 어떻게 표시되는지 보여줍니다. 미리보기에서는
recyclerview의 모든 단일 그리드 항목에 패딩과grid_view_item레이아웃을 사용합니다.

- 머티리얼 디자인 가이드라인에 따라 목록의 상단, 하단, 측면에
8dp의 공간이 있어야 하고 항목 사이에는4dp의 공간이 있어야 합니다. 이렇게 하려면fragment_overview.xml레이아웃과gridview_item.xml레이아웃의 패딩 조합을 사용하면 됩니다.

layout/gridview_item.xml을 엽니다.padding속성에는 이미 항목 외부와 콘텐츠 사이에2dp의 패딩이 있습니다. 이에 따라 항목 콘텐츠 사이에4dp의 공간이, 그리고 바깥 가장자리를 따라2dp의 공간이 확보됩니다. 다시 말해서, 디자인 가이드라인과 일치하도록 바깥 가장자리에 추가로6dp의 패딩이 필요합니다.layout/fragment_overview.xml로 돌아갑니다.RecyclerView의6dp패딩을 추가하여 가이드라인에 따라 외부에8dp를, 내부에4dp를 확보합니다.
<androidx.recyclerview.widget.RecyclerView
...
android:padding="6dp"
... />
- 전체
<RecyclerView>요소는 다음과 같습니다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
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" />
사진 그리드 어댑터 추가하기
이제 fragment_overview 레이아웃에 그리드 레이아웃이 포함된 RecyclerView가 있습니다. 이 단계에서는 웹 서버에서 검색한 데이터를 RecyclerView 어댑터를 통해 RecyclerView에 결합합니다.
ListAdapter(복습)
ListAdapter는 RecyclerView.Adapter 클래스의 서브클래스로, 백그라운드 스레드의 목록 간 차이를 계산하는 작업을 포함하여 목록 데이터를 RecyclerView에 표시하기 위한 것입니다.
이 앱에서는 ListAdapter.의 DiffUtil 구현을 사용합니다. DiffUtil을 사용할 때 이점은 RecyclerView에서 일부 항목이 추가되거나 삭제 또는 변경될 때마다 전체 목록이 새로고침되지 않는다는 점입니다. 변경된 항목만 새로고침됩니다.
앱에 ListAdapter를 추가합니다.
overview패키지에서PhotoGridAdapter.kt라는 새 Kotlin 클래스를 만듭니다.- 아래와 같이 생성자 매개변수를 사용하여
ListAdapter의PhotoGridAdapter클래스를 확장합니다.PhotoGridAdapter클래스는ListAdapter를 확장합니다. 이 생성자에는 목록 항목 유형, 뷰 홀더,DiffUtil.ItemCallback구현이 필요합니다.
class PhotoGridAdapter : ListAdapter<MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
요청이 있는 경우 androidx.recyclerview.widget.ListAdapter 클래스와 com.example.android.marsphoto.network.MarsPhoto 클래스를 가져옵니다. 다음 단계에서는 이 생성자에 누락되어 오류를 생성하는 다른 부분을 구현합니다.
- 위의 오류를 해결하기 위해 이 단계에서 필요한 메서드를 추가한 후 이 작업의 후반부에서 구현합니다.
PhotoGridAdapter클래스를 클릭하고 빨간색 전구를 클릭한 다음 드롭다운 메뉴에서 Implement members를 선택합니다. 표시되는 팝업에서ListAdapter메서드onCreateViewHolder(),onBindViewHolder()를 선택합니다. 이 작업을 마칠 때 수정되는 오류가 Android 스튜디오에 계속 표시됩니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPhotoViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPhotoViewHolder, position: Int) {
TODO("Not yet implemented")
}
onCreateViewHolder 메서드와 onBindViewHolder 메서드를 구현하려면 다음 단계에서 추가할 MarsPhotoViewHolder가 필요합니다.
PhotoGridAdapter내부에RecyclerView.ViewHolder를 확장하는MarsPhotoViewHolder의 내부 클래스 정의를 추가합니다.MarsPhoto를 레이아웃에 결합하기 위한GridViewItemBinding변수가 필요하므로, 이 변수를MarsPhotoViewHolder에 전달합니다. 기본ViewHolder클래스는 생성자에 뷰가 있어야 합니다. 이 클래스를 결합 루트 뷰에 전달합니다.
class MarsPhotoViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
요청이 있는 경우 androidx.recyclerview.widget.RecyclerView 및 com.example.android.marsrealestate.databinding.GridViewItemBinding을 가져옵니다.
MarsPhotoViewHolder에서MarsPhoto객체를 인수로 사용하고binding.property를 이 객체로 설정하는bind()메서드를 만듭니다. 속성을 설정한 후executePendingBindings()를 호출하면 업데이트가 즉시 실행됩니다.
fun bind(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
binding.executePendingBindings()
}
onCreateViewHolder()의PhotoGridAdapter클래스 내부에서 TODO를 삭제하고 아래의 줄을 추가합니다.onCreateViewHolder()메서드는GridViewItemBinding을 확장하고 상위ViewGroup컨텍스트의LayoutInflater를 사용하여 생성된 새MarsPhotoViewHolder를 반환해야 합니다.
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
요청이 있는 경우 android.view.LayoutInflater를 가져옵니다.
onBindViewHolder()메서드에서 TODO를 삭제하고 아래의 줄을 추가합니다. 여기서getItem()을 호출하여 현재RecyclerView위치와 연결된MarsPhoto객체를 가져온 다음 이 속성을MarsPhotoViewHolder의bind()메서드에 전달합니다.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
PhotoGridAdapter내부에 아래와 같이DiffCallback의 컴패니언 객체 정의를 추가합니다.
DiffCallback객체는 비교할 일반 객체 유형MarsPhoto로DiffUtil.ItemCallback을 확장합니다. 이 구현 내부에서 두 화성 사진 객체를 비교합니다.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}
요청이 있는 경우 androidx.recyclerview.widget.DiffUtil을 가져옵니다.
- 빨간색 전구를 눌러
DiffCallback객체의 비교기 메서드areItemsTheSame()및areContentsTheSame()을 구현합니다.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented") }
areItemsTheSame()메서드에서TODO를 삭제합니다. 이 메서드는DiffUtil에서 호출되어 두 객체가 동일한 항목을 나타내는지 여부를 확인합니다.DiffUtil은 이 메서드를 사용하여 새MarsPhoto객체가 이전MarsPhoto객체와 동일한지 확인합니다. 모든 항목(MarsPhoto객체)의 ID는 고유합니다.oldItem과newItem의 ID를 비교하여 결과를 반환합니다.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
areContentsTheSame()에서TODO를 삭제합니다. 이 메서드는 두 항목의 데이터가 동일한지 확인하려고 할 때DiffUtil에서 호출됩니다. MarsPhoto에서 중요한 데이터는 이미지 URL입니다.oldItem과newItem의 URL을 비교하여 결과를 반환합니다.
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
오류 없이 앱을 컴파일하고 실행할 수 있어야 하지만, 에뮬레이터에 빈 화면이 표시됩니다. recyclerview가 준비되었지만 뷰에 전달된 데이터가 없기 때문입니다. 이 데이터는 다음 단계에서 구현합니다.
결합 어댑터를 추가하고 부분 연결하기
이 단계에서는 BindingAdapter를 사용하여 MarsPhoto 객체 목록으로 PhotoGridAdapter를 초기화합니다. BindingAdapter를 사용하여 RecyclerView 데이터를 설정하면 데이터 결합이 자동으로 MarsPhoto 객체 목록의 LiveData를 관찰합니다. 그런 다음 MarsPhoto 목록이 변경되면 결합 어댑터가 자동으로 호출됩니다.
BindingAdapters.kt를 엽니다.- 파일 끝에
RecyclerView와MarsPhoto객체 목록을 인수로 사용하는bindRecyclerView()메서드를 추가합니다. 이 메서드에listData속성이 포함된@BindingAdapter주석을 추가합니다.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
요청이 있는 경우 androidx.recyclerview.widget.RecyclerView 및 com.example.android.marsphotos.network.MarsPhoto를 가져옵니다.
bindRecyclerView()함수 내부에서recyclerView.adapter를PhotoGridAdapter로 변환하여 새val속성adapter.에 할당합니다.
val adapter = recyclerView.adapter as PhotoGridAdapter
bindRecyclerView()함수 끝부분에서 화성 사진 목록 데이터가 포함된adapter.submitList()를 호출합니다. 그러면 새 목록을 사용할 수 있을 때RecyclerView에 알려줍니다.
adapter.submitList(data)
요청이 있는 경우 com.example.android.marsrealestate.overview.PhotoGridAdapter를 가져옵니다.
- 전체
bindRecyclerView결합 어댑터는 다음과 같습니다.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- 모든 항목을 연결하려면
res/layout/fragment_overview.xml을 엽니다.RecyclerView요소에app:listData속성을 추가하고 데이터 결합을 사용하여 이 속성을viewmodel.photos로 설정합니다. 이전 작업에서ImageView에 실행한 작업과 유사합니다.
app:listData="@{viewModel.photos}"
overview/OverviewFragment.kt를 엽니다.onCreateView()의return문 바로 앞에서binding.photosGrid의RecyclerView어댑터를 새PhotoGridAdapter객체로 초기화합니다.
binding.photosGrid.adapter = PhotoGridAdapter()
- 앱을 실행합니다. 화성 이미지를 스크롤하는 그리드가 표시됩니다. 스크롤하면 새 이미지가 표시되지만 약간 이상해 보입니다. 스크롤하는 동안
RecyclerView의 상단과 하단에 패딩이 유지되므로 목록이 작업 모음 아래에서 스크롤되는 것처럼 보이지 않습니다.

- 수정하려면 android:clipToPadding 속성을 사용해 내부 콘텐츠를 패딩에 맞춰 자르지 않도록
RecyclerView에 알려야 합니다. 그러면 스크롤 뷰가 패딩 영역 안에 그려집니다.layout/fragment_overview.xml로 돌아갑니다.RecyclerView의android:clipToPadding속성을 추가하고false로 설정합니다.
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- 앱을 실행합니다. 예상대로 이미지 자체가 표시되기 전에 로드 진행률 아이콘이 표시되는 것도 볼 수 있습니다. 이 아이콘은 Coil 이미지 라이브러리에 전달한 자리표시자 로드 이미지입니다.

- 앱이 실행되는 동안 비행기 모드를 사용 설정합니다. 에뮬레이터에서 이미지를 스크롤합니다. 아직 로드되지 않은 이미지는 손상 이미지 아이콘으로 표시됩니다. 이 아이콘은 네트워크 오류가 발생하거나 이미지를 가져올 수 없을 때 표시하도록 Coil 이미지 라이브러리에 전달한 이미지 드로어블입니다.

축하합니다. 거의 완료되었습니다. 다음이자 마지막 작업에서는 앱에 더 많은 오류 처리를 추가하여 사용자 경험을 더욱 개선합니다.
이미지를 가져올 수 없을 때 MarsPhotos 앱은 손상 이미지 아이콘을 표시합니다. 하지만 네트워크가 없으면 앱에서 빈 화면이 표시됩니다. 빈 화면은 다음 단계에서 확인합니다.
- 기기나 에뮬레이터에서 비행기 모드를 사용 설정합니다. Android 스튜디오에서 앱을 실행합니다. 빈 화면이 표시됩니다.

이는 만족스러운 사용자 경험이 아닙니다. 이 작업에서는 기본 오류 처리를 추가하여 사용자가 현재 상황을 더 잘 파악하도록 합니다. 앱은 인터넷을 사용할 수 없는 경우 연결 오류 아이콘을 표시하고 MarsPhoto 목록을 가져오는 동안에는 로드 애니메이션을 표시합니다.
ViewModel에 상태 추가하기
이 작업에서는 OverviewViewModel에서 웹 요청의 상태를 나타내는 속성을 만듭니다. 로드, 성공, 실패 등 세 가지 상태를 고려합니다. 로드 상태는 데이터를 기다리는 동안 발생합니다. 성공 상태는 웹 서비스에서 데이터를 성공적으로 검색했음을 나타냅니다. 실패 상태는 네트워크 오류나 연결 오류를 나타냅니다.
Kotlin의 enum 클래스
애플리케이션에서 이 세 가지 상태를 나타내려면 enum을 사용합니다. enum은 열거의 단축형으로, 컬렉션의 모든 항목을 순서가 지정된 목록으로 나열한다는 의미입니다. 각 enum 상수는 enum 클래스의 객체입니다.
Kotlin에서 enum은 상수 집합을 보유할 수 있는 데이터 유형입니다. 아래와 같이 클래스 정의 앞에 키워드 enum을 추가하여 정의합니다. 열거형 상수는 쉼표로 구분됩니다.
정의:
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
사용:
var direction = Direction.NORTH;
위와 같이 enum 객체 참조는 클래스 이름 뒤에 점(.) 연산자와 상수 이름을 사용하여 처리할 수 있습니다.
ViewModel에서 상태 값과 함께 enum 클래스 정의를 추가합니다.
overview/OverviewViewModel.kt를 엽니다. 파일 상단에서(가져오기 뒤, 클래스 정의 앞에)enum을 추가하여 사용 가능한 모든 상태를 나타냅니다.
enum class MarsApiStatus { LOADING, ERROR, DONE }
_status속성과status속성의 정의로 스크롤하고 유형을String에서MarsApiStatus. MarsApiStatus로 변경합니다. MarsApiStatus는 이전 단계에서 정의한 enum 클래스입니다.
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
getMarsPhotos()메서드에서"Success: ..."문자열을MarsApiStatus.DONE상태로,"Failure..."문자열을MarsApiStatus.ERROR로 변경합니다.
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception)
_status.value = MarsApiStatus.ERROR
}
- 상태를
try {}블록 위에서MarsApiStatus.LOADING으로 설정합니다. 이 상태는 코루틴을 실행하는 동안 데이터를 기다릴 때 초기 상태입니다. 이제 전체viewModelScope.launch{}블록은 다음과 같습니다.
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
}
catch {}블록에서 오류 상태 다음에_photos를 빈 목록으로 설정합니다. 이렇게 하면 Recycler 뷰가 삭제됩니다.
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
- 전체
getMarsPhotos()메서드는 다음과 같습니다.
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()
}
}
}
상태를 나타내는 enum 상태를 정의하고 코루틴 시작 시 로드 상태를 설정했습니다. 앱이 완료되어 웹 서버에서 데이터를 검색하면 완료로 설정되고, 예외가 있으면 오류로 설정됩니다. 다음 작업에서는 결합 어댑터를 사용하여 해당하는 아이콘을 표시합니다.
상태 ImageView용 결합 어댑터 추가하기
일련의 enum 상태를 사용하여 OverviewViewModel에서 MarsApiStatus를 설정했습니다. 이 단계에서는 상태를 앱에 표시합니다. ImageView에 결합 어댑터를 사용하여 로드 상태 및 오류 상태의 아이콘을 표시합니다. 앱이 로드 상태이거나 오류 상태일 때 ImageView가 표시됩니다. 앱에서 로드가 완료되면 ImageView가 표시되지 않습니다.
BindingAdapters.kt를 열고 파일의 끝으로 스크롤하여 다른 어댑터를 추가합니다.ImageView값과MarsApiStatus값을 인수로 사용하는bindStatus()라는 새 결합 어댑터를 추가합니다. 맞춤 속성marsApiStatus를 매개변수로 전달하는@BindingAdapter주석을 메서드에 추가합니다.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
요청이 있는 경우 com.example.android.marsrealestate.overview.MarsApiStatus를 가져옵니다.
bindStatus()메서드 내부에when {}블록을 추가하여 서로 다른 상태 간에 전환합니다.
when (status) {
}
when {}내부에 로드 상태(MarsApiStatus.LOADING)의 사례를 추가합니다. 이 상태의 경우ImageView를 visible로 설정하고 로드 애니메이션에 할당합니다. 이전 작업에서 Coil에 사용한 것과 동일한 애니메이션 드로어블입니다.
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
요청이 있는 경우 android.view.View를 가져옵니다.
- 오류 상태(
MarsApiStatus.ERROR)의 사례를 추가합니다.LOADING상태의 경우와 유사하게 상태ImageView를 visible로 설정하고 연결 오류 드로어블을 사용합니다.
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- 완료 상태(
MarsApiStatus.DONE)의 사례를 추가합니다. 여기서는 성공적인 응답이 있으므로 상태ImageView의 공개 상태를View.GONE으로 설정하여 숨깁니다.
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
상태 이미지 뷰의 결합 어댑터를 설정했습니다. 다음 단계에서는 새 결합 어댑터를 사용하는 이미지 뷰를 추가합니다.
상태 ImageView 추가하기
이 단계에서는 이전에 정의한 상태를 표시하는 이미지 뷰를 fragment_overview.xml에 추가합니다.
res/layout/fragment_overview.xml을 엽니다.ConstraintLayout의RecyclerView요소 아래에 다음과 같이ImageView를 추가합니다.
<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}" />
위의 ImageView에는 RecyclerView와 동일한 제약 조건이 있습니다. 그러나 이미지를 늘려 뷰를 채우는 대신, 너비와 높이가 wrap_content를 사용하여 이미지를 중앙에 배치합니다. 또한 app:marsApiStatus 속성이 viewModel.status로 설정되어 있어, ViewModel의 상태 속성이 변경되면 BindingAdapter가 호출됩니다.
- 위 코드를 테스트하려면 에뮬레이터나 기기에서 비행기 모드를 사용 설정하여 네트워크 연결 오류를 시뮬레이션합니다. 앱을 컴파일하고 실행하면 오류 이미지가 표시됩니다.

- Back 버튼을 탭하여 앱을 닫고 비행기 모드를 사용 중지합니다. 최근 항목 화면을 사용하여 앱을 반환합니다. 네트워크 연결 속도에 따라 앱이 웹 서비스를 쿼리할 때 이미지 로드가 시작되기 전에 로드 스피너가 아주 잠시 표시될 수도 있습니다.
이 Codelab을 완료하고 MarsPhotos 앱을 빌드한 것을 축하합니다! 이제 가족과 친구들에게 실제 화성 사진이 담긴 앱을 자랑하세요.
이 Codelab의 솔루션 코드는 아래 표시된 프로젝트에 있습니다. main 분기를 사용하여 코드를 가져오거나 다운로드하세요.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼
을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.
- Coil 라이브러리는 앱에서 이미지 다운로드, 버퍼링, 디코딩, 캐시와 같은 이미지 관리 프로세스를 단순화합니다.
- 결합 어댑터는 뷰와 이 뷰에 결합된 데이터 사이에 있는 확장 메서드입니다. 결합 어댑터는 데이터가 변경될 때(예: Coil을 호출하여 URL에서
ImageView로 이미지 로드하기) 맞춤 동작을 제공합니다. - 결합 어댑터는
@BindingAdapter주석이 추가된 확장 메서드입니다. - 이미지의 그리드를 표시하려면
GridLayoutManager와 함께RecyclerView를 사용합니다. - 변경 시 속성 목록을 업데이트하려면
RecyclerView와 레이아웃 사이에 결합 어댑터를 사용합니다.
Android 개발자 문서:
기타:



