인터넷에서 이미지 로드 및 표시

1. 시작하기 전에

소개

이전 Codelab에서는 저장소 패턴을 사용하여 웹 서비스에서 데이터를 가져오고 응답을 Kotlin 객체로 파싱하는 방법을 배웠습니다. 이 지식을 기반으로 이 Codelab에서는 웹 URL에서 사진을 로드하고 표시합니다. 또한 LazyVerticalGrid를 빌드하고 이 뷰를 사용해 개요 페이지에 이미지 그리드를 표시하는 방법을 다시 확인합니다.

기본 요건

  • Retrofit 라이브러리와 Gson 라이브러리를 사용하여 REST 웹 서비스에서 JSON을 검색하고 이 데이터를 Kotlin 객체로 파싱하는 방법에 관한 지식
  • REST 웹 서비스에 관한 지식
  • 데이터 레이어 및 저장소 등의 Android 아키텍처 구성요소에 관한 지식
  • 종속 항목 삽입에 관한 지식
  • ViewModelViewModelProvider.Factory에 관한 지식
  • 앱의 코루틴 구현에 관한 지식
  • 저장소 패턴에 관한 지식

학습할 내용

  • Coil 라이브러리를 사용하여 웹 URL에서 이미지를 로드하고 표시하는 방법
  • LazyVerticalGrid를 사용하여 이미지 그리드를 표시하는 방법
  • 이미지를 다운로드하고 표시할 때 발생할 수 있는 오류를 처리하는 방법

빌드할 항목

  • 화성 데이터에서 이미지 URL을 가져오도록 Mars Photos 앱을 수정하고 Coil을 사용해 이 이미지를 로드하고 표시합니다.
  • 앱에 로드 애니메이션과 오류 아이콘을 추가합니다.
  • 앱에 상태 및 오류 처리를 추가합니다.

필요한 항목

  • 최신 버전의 Chrome과 같은 최신 웹브라우저가 설치된 컴퓨터
  • REST 웹 서비스가 포함된 Mars Photos 앱의 시작 코드

2. 앱 개요

이 Codelab에서는 이전 Codelab의 Mars Photos 앱을 계속 사용합니다. Mars Photos 앱은 웹 서비스에 연결하여 Gson을 사용해 검색된 Kotlin 객체 수를 가져와 표시합니다. 이 Kotlin 객체에는 NASA의 화성 탐사 로버가 캡처한 화성 표면의 실제 사진 URL이 포함되어 있습니다.

a59e55909b6e9213.png

이 Codelab에서 빌드하는 버전의 앱은 화성 사진을 이미지 그리드로 표시합니다. 이미지는 앱이 웹 서비스에서 검색하는 데이터의 일부입니다. 앱은 Coil 라이브러리를 사용하여 이미지를 로드해 표시하고 LazyVerticalGrid를 사용하여 이미지의 그리드 레이아웃을 만듭니다. 또한 앱은 오류 메시지를 표시하여 네트워크 오류를 적절하게 처리합니다.

68f4ff12cc1e2d81.png

시작 코드 가져오기

시작하려면 시작 코드를 다운로드하세요.

또는 코드에 관한 GitHub 저장소를 클론해도 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout coil-starter

Mars Photos GitHub 저장소에서 코드를 찾아볼 수 있습니다.

3. 다운로드한 이미지 표시

웹 URL에서 사진을 표시하는 것은 간단해 보일 수도 있지만 제대로 작동하려면 엔지니어링이 상당히 필요합니다. 이미지를 다운로드하고, 내부적으로 저장(캐시)하고, 압축 형식에서 Android가 사용할 수 있는 이미지로 디코딩해야 합니다. 메모리 내 캐시나 저장소 기반 캐시 또는 두 캐시 모두에 이미지를 캐시할 수 있습니다. UI가 응답성을 유지하기 위해 이 모든 작업은 우선순위가 낮은 백그라운드 스레드에서 이루어져야 합니다. 또한 최상의 네트워크 및 CPU 성능을 위해 둘 이상의 이미지를 한 번에 가져오고 디코딩하는 것이 좋습니다.

다행히 커뮤니티에서 개발한 Coil이라는 라이브러리를 사용하여 이미지를 다운로드하고 버퍼링 및 디코딩하고 캐시할 수 있습니다. Coil을 사용하지 않으면 해야 할 작업이 훨씬 더 많습니다.

Coil에는 기본적으로 다음 두 가지가 필요합니다.

  • 로드하고 표시할 이미지의 URL
  • 이미지를 실제로 표시하는 AsyncImage 컴포저블

이 작업에서는 Coil을 사용하여 Mars 웹 서비스의 단일 이미지를 표시하는 방법을 알아봅니다. 웹 서비스에서 반환되는 사진 목록에 있는 첫 번째 화성 사진의 이미지를 표시합니다. 다음은 전과 후의 스크린샷 이미지입니다.

a59e55909b6e9213.png 1b670f284109bbf5.png

Coil 종속 항목 추가

  1. 저장소 추가 및 수동 DI Codelab의 Mars Photos 솔루션 앱을 엽니다.
  2. 앱을 실행하여 검색한 화성 사진의 수가 표시되는지 확인합니다.
  3. build.gradle.kts (Module :app)를 엽니다.
  4. dependencies 섹션에서 다음과 같은 Coil 라이브러리 줄을 추가합니다.
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")

Coil 문서 페이지에서 최신 버전의 라이브러리를 확인하고 업데이트하세요.

  1. Sync Now를 클릭하여 새 종속 항목으로 프로젝트를 다시 빌드합니다.

이미지 URL 표시

이 단계에서는 첫 번째 화성 사진의 URL을 검색하여 표시합니다.

  1. ui/screens/MarsViewModel.kt에 있는 try 블록 내의 getMarsPhotos() 메서드에서, 웹 서비스에서 검색되는 데이터를 listResult로 설정하는 줄을 찾습니다.
// No need to copy, code is already present
try {
   val listResult = marsPhotosRepository.getMarsPhotos()
   //...
}
  1. listResultresult로 변경하고 검색된 첫 번째 화성 사진을 새 변수 result에 할당하는 방법으로 이 줄을 업데이트합니다. 색인 0에 첫 번째 사진 객체를 할당합니다.
try {
   val result = marsPhotosRepository.getMarsPhotos()[0]
   //...
}
  1. 다음 줄에서 MarsUiState.Success() 함수 호출에 전달된 매개변수를 다음 코드의 문자열로 업데이트합니다. listResult 대신 새 속성의 데이터를 사용합니다. 사진 result의 첫 번째 이미지 URL을 표시합니다.
try {
   ...
   MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}

이제 전체 try 블록은 다음 코드와 같습니다.

marsUiState = try {
   val result = marsPhotosRepository.getMarsPhotos()[0]
   MarsUiState.Success(
       "   First Mars image URL : ${result.imgSrc}"
   )
}
  1. 앱을 실행합니다. Text 컴포저블이 이제 첫 번째 화성 사진의 URL을 표시합니다. 다음 섹션에서는 앱이 이 URL에 이미지를 표시하도록 하는 방법을 설명합니다.

b5daaa892fe8dad7.png

AsyncImage 컴포저블 추가

이 단계에서는 구성 가능한 AsyncImage 함수를 추가하여 하나의 화성 사진을 로드하고 표시합니다. AsyncImage는 이미지 요청을 비동기식으로 실행하고 결과를 렌더링하는 컴포저블입니다.

// Example code, no need to copy over
AsyncImage(
    model = "https://android.com/sample_image.jpg",
    contentDescription = null
)

model 인수는 ImageRequest.data 값 또는 ImageRequest 자체일 수 있습니다. 위의 예에서는 ImageRequest.data 값, 즉 이미지 URL("https://android.com/sample_image.jpg")을 할당합니다. 다음 코드 예는 ImageRequest 자체를 model에 할당하는 방법을 보여줍니다.

// Example code, no need to copy over

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/image.jpg")
        .crossfade(true)
        .build(),
    placeholder = painterResource(R.drawable.placeholder),
    contentDescription = stringResource(R.string.description),
    contentScale = ContentScale.Crop,
    modifier = Modifier.clip(CircleShape)
)

AsyncImage는 표준 이미지 컴포저블과 동일한 인수를 지원합니다. 또한 placeholder/error/fallback 페인터 및 onLoading/onSuccess/onError 콜백을 설정하도록 지원합니다. 위의 코드 예는 원형 자르기와 크로스페이드로 이미지를 로드하고 자리표시자를 설정합니다.

contentDescription은 접근성 서비스에서 이미지가 나타내는 내용을 설명하는 데 사용되는 텍스트를 설정합니다.

코드에 AsyncImage 컴포저블을 추가하여 검색된 첫 번째 화성 사진을 표시합니다.

  1. ui/screens/HomeScreen.kt에서 MarsPhotoModifier를 사용하는 MarsPhotoCard()라는 구성 가능한 새 함수를 추가합니다.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
  1. 구성 가능한 MarsPhotoCard() 함수 내에 다음과 같이 AsyncImage() 함수를 추가합니다.
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.platform.LocalContext

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .build(),
        contentDescription = stringResource(R.string.mars_photo),
        modifier = Modifier.fillMaxWidth()
    )
}

위 코드는 이미지 URL(photo.imgSrc)을 사용하여 ImageRequest를 빌드하여 model 인수에 전달합니다. contentDescription을 사용하여 접근성 리더용 텍스트를 설정합니다.

  1. ImageRequestcrossfade(true)를 추가하여 요청이 성공적으로 완료될 때 크로스페이드 애니메이션이 사용되도록 합니다.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .crossfade(true)
            .build(),
        contentDescription = stringResource(R.string.mars_photo),
        modifier = Modifier.fillMaxWidth()
    )
}
  1. 요청이 완료되면 ResultScreen 컴포저블 대신 MarsPhotoCard 컴포저블을 표시하도록 HomeScreen 컴포저블을 업데이트합니다. 다음 단계에서 유형 불일치 오류를 수정합니다.
@Composable
fun HomeScreen(
    marsUiState: MarsUiState,
    modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
        is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier.fillMaxSize())
        else -> ErrorScreen(modifier = modifier.fillMaxSize())
    }
}
  1. MarsViewModel.kt 파일에서 String 대신 MarsPhoto 객체를 허용하도록 MarsUiState 인터페이스를 업데이트합니다.
sealed interface MarsUiState {
    data class Success(val photos: MarsPhoto) : MarsUiState
    //...
}
  1. 첫 번째 화성 사진 객체를 MarsUiState.Success()에 전달하도록 getMarsPhotos() 함수를 업데이트합니다. result 변수를 삭제합니다.
marsUiState = try {
    MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
  1. 앱을 실행하고 하나의 화성 이미지가 표시되는지 확인합니다.

d4421a2458f38695.png

  1. 화성 사진이 전체 화면을 채우지 않습니다. 화면의 사용 가능한 공간을 채우려면 HomeScreen.ktAsyncImage에서 contentScaleContentScale.Crop으로 설정합니다.
import androidx.compose.ui.layout.ContentScale

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
   AsyncImage(
       model = ImageRequest.Builder(context = LocalContext.current)
           .data(photo.imgSrc)
           .crossfade(true)
           .build(),
       contentDescription = stringResource(R.string.mars_photo),
       contentScale = ContentScale.Crop,
       modifier = modifier,
   )
}
  1. 앱을 실행하고 이미지가 가로, 세로 모두 화면을 채우는지 확인합니다.

1b670f284109bbf5.png

로드 이미지와 오류 이미지 추가

이미지를 로드하는 동안 자리표시자 이미지를 표시하여 앱의 사용자 경험을 개선할 수 있습니다. 또한 이미지 파일 누락이나 손상 같은 문제로 인해 로드되지 않은 경우에도 오류 이미지를 표시할 수 있습니다. 이 섹션에서는 AsyncImage를 사용하여 오류 이미지와 자리표시자 이미지를 모두 추가합니다.

  1. res/drawable/ic_broken_image.xml을 열고 오른쪽에서 Design 탭이나 Split 탭을 클릭합니다. 오류 이미지의 경우 내장된 아이콘 라이브러리에서 사용할 수 있는 손상 이미지 아이콘을 사용합니다. 이 벡터 드로어블은 android:tint 속성을 사용하여 아이콘 색상을 회색으로 지정합니다.

70e008c63a2a1139.png

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

92a448fa23b6d1df.png

  1. HomeScreen.kt 파일로 돌아갑니다. MarsPhotoCard 컴포저블에서 AsyncImage() 호출을 업데이트하여 다음 코드와 같이 errorplaceholder 속성을 추가합니다.
import androidx.compose.ui.res.painterResource

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        // ...
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        // ...
    )
}

이 코드는 로드하는 동안 사용할 자리표시자 로드 이미지(loading_img 드로어블)를 설정합니다. 또한 이미지를 로드하지 못한 경우 사용할 이미지(ic_broken_image 드로어블)를 설정합니다.

이제 전체 MarsPhotoCard 컴포저블은 다음 코드와 같습니다.

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .crossfade(true)
            .build(),
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.mars_photo),
        contentScale = ContentScale.Crop
    )
}
  1. 앱을 실행합니다. 네트워크 연결 속도에 따라 Coil이 속성 이미지를 다운로드하고 표시할 때 로드 이미지가 잠시 표시될 수도 있습니다. 그러나 네트워크를 사용 중지해도 손상 이미지 아이콘은 아직 표시되지 않습니다. 이 부분은 Codelab의 마지막 태스크에서 수정합니다.

d684b0e096e57643.gif

4. LazyVerticalGrid를 사용하여 이미지 그리드 표시하기

이제 앱이 인터넷에서 첫 번째 화성 사진, 즉 첫 번째 MarsPhoto 목록 항목을 로드합니다. 이 화성 사진 데이터의 이미지 URL을 사용하여 AsyncImage를 채웠습니다. 하지만 앱이 이미지 그리드를 표시하는 것이 목표입니다. 이 태스크에서는 그리드 레이아웃 관리자와 함께 LazyVerticalGrid를 사용하여 이미지 그리드를 표시합니다.

지연 그리드

LazyVerticalGrid 컴포저블과 LazyHorizontalGrid 컴포저블은 그리드에 항목을 표시하도록 지원합니다. 지연 세로 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하는 반면, 지연 가로 그리드는 가로축을 중심으로 동일하게 동작합니다.

27680e208333ed5.png

디자인 관점에서 볼 때 그리드 레이아웃은 화성 사진을 아이콘이나 이미지로 표시하는 데 가장 적합합니다.

LazyVerticalGridcolumns 매개변수와 LazyHorizontalGridrows 매개변수는 셀이 열이나 행으로 형성되는 방식을 제어합니다. 다음 코드 예는 항목을 그리드로 표시하고 GridCells.Adaptive를 사용하여 각 열의 너비를 128.dp 이상으로 설정합니다.

// Sample code - No need to copy over

@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 150.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

LazyVerticalGrid를 사용하면 항목의 너비를 지정할 수 있고 그러면 그리드는 가능한 한 많은 열에 맞습니다. 열 수를 계산한 후에 그리드는 남은 너비를 열 간에 균등하게 분배합니다. 이러한 적응형 크기 조절 방법은 다양한 화면 크기에서 항목 집합을 표시하는 데 특히 유용합니다.

이 Codelab에서는 화성 사진을 표시하기 위해 GridCells.Adaptive와 함께 LazyVerticalGrid 컴포저블을 사용하고 각 열의 너비를 150.dp로 설정합니다.

항목 키

사용자가 그리드(LazyColumn 내의 LazyRow)를 스크롤할 때 목록 항목 위치가 변경됩니다. 하지만 방향 변경으로 인해, 또는 항목이 추가되거나 삭제되는 경우 사용자에게 표시되는 행 내의 스크롤 위치가 사라질 수 있습니다. 항목 키를 사용하면 키에 따라 스크롤 위치를 유지할 수 있습니다.

키를 제공하면 Compose가 재정렬을 올바르게 처리할 수 있습니다. 예를 들어 항목에 저장된 상태가 포함되어 있는 경우 키를 설정하면 위치가 변경될 때 Compose가 항목과 함께 이 상태를 옮길 수 있습니다.

LazyVerticalGrid 추가

세로 그리드에 화성 사진 목록을 표시하는 컴포저블을 추가합니다.

  1. HomeScreen.kt 파일에서 MarsPhoto 목록 및 modifier를 인수로 사용하는 PhotosGridScreen()이라는 구성 가능한 새 함수를 만듭니다.
@Composable
fun PhotosGridScreen(
    photos: List<MarsPhoto>,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
  1. PhotosGridScreen 컴포저블 내에 다음 매개변수를 사용하여 LazyVerticalGrid를 추가합니다.
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp

@Composable
fun PhotosGridScreen(
    photos: List<MarsPhoto>,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(150.dp),
        modifier = modifier.padding(horizontal = 4.dp),
        contentPadding = contentPadding,
   ) {
     }
}
  1. 항목 목록을 추가하려면 LazyVerticalGrid 람다 내에서 MarsPhoto 목록 및 항목 키를 photo.id로 전달하는 items() 함수를 호출합니다.
import androidx.compose.foundation.lazy.grid.items

@Composable
fun PhotosGridScreen(
    photos: List<MarsPhoto>,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
) {
   LazyVerticalGrid(
       // ...
   ) {
       items(items = photos, key = { photo -> photo.id }) {
       }
   }
}
  1. 단일 목록 항목에 의해 표시되는 콘텐츠를 추가하려면 items 람다 표현식을 정의합니다. MarsPhotoCard를 호출하여 photo를 전달합니다.
items(items = photos, key = { photo -> photo.id }) {
   photo -> MarsPhotoCard(photo)
}
  1. 요청이 완료되면 MarsPhotoCard 컴포저블 대신 PhotosGridScreen 컴포저블을 표시하도록 HomeScreen 컴포저블을 업데이트합니다.
when (marsUiState) {
       // ...
       is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
       // ...
}
  1. MarsViewModel.kt 파일에서 단일 MarsPhoto 대신 MarsPhoto 객체 목록을 허용하도록 MarsUiState 인터페이스를 업데이트합니다. PhotosGridScreen 컴포저블은 MarsPhoto 객체 목록을 허용합니다.
sealed interface MarsUiState {
    data class Success(val photos: List<MarsPhoto>) : MarsUiState
    //...
}
  1. MarsViewModel.kt 파일에서 화성 사진 객체 목록을 MarsUiState.Success()에 전달하도록 getMarsPhotos() 함수를 업데이트합니다.
marsUiState = try {
    MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
  1. 앱을 실행합니다.

2eaec198c56b5eed.png

각 사진 주위에 패딩이 없으며 사진마다 가로세로 비율이 다릅니다. Card 컴포저블을 추가하여 이러한 문제를 해결할 수 있습니다.

카드 컴포저블 추가

  1. HomeScreen.kt 파일의 MarsPhotoCard 컴포저블에서 AsyncImage 주위에 고도가 8.dpCard를 추가합니다. modifier 인수를 Card 컴포저블에 할당합니다.
import androidx.compose.material.Card
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {

    Card(
        modifier = modifier,
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
    ) {

        AsyncImage(
            model = ImageRequest.Builder(context = LocalContext.current)
                .data(photo.imgSrc)
                .crossfade(true)
                .build(),
            error = painterResource(R.drawable.ic_broken_image),
            placeholder = painterResource(R.drawable.loading_img),
            contentDescription = stringResource(R.string.mars_photo),
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxWidth()
        )
    }
}
  1. 가로세로 비율을 수정하려면 PhotosGridScreen()에서 MarsPhotoCard()의 수정자를 업데이트합니다.
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
   LazyVerticalGrid(
       //...
   ) {
       items(items = photos, key = { photo -> photo.id }) { photo ->
           MarsPhotoCard(
               photo,
               modifier = modifier
                   .padding(4.dp)
                   .fillMaxWidth()
                   .aspectRatio(1.5f)
           )
       }
   }
}
  1. 결과 화면 미리보기를 업데이트하여 PhotosGridScreen()을 미리 봅니다. 빈 이미지 URL을 사용하여 모의 데이터를 처리합니다.
@Preview(showBackground = true)
@Composable
fun PhotosGridScreenPreview() {
   MarsPhotosTheme {
       val mockData = List(10) { MarsPhoto("$it", "") }
       PhotosGridScreen(mockData)
   }
}

모의 데이터의 URL이 비어 있으므로 사진 그리드 미리보기에 이미지가 로드되는 것을 볼 수 있습니다.

이미지 로드 중 사진 그리드 화면 미리보기

  1. 앱을 실행합니다.

b56acd074ce0f9c7.png

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

9b72c1d4206c7331.png

잘하셨습니다. 에뮬레이터나 기기에서 비행기 모드를 사용 설정하여 네트워크 연결 오류를 시뮬레이션했습니다.

5. 재시도 작업 추가

이 섹션에서는 재시도 작업 버튼을 추가하고 이 버튼이 클릭되면 사진을 가져옵니다.

60cdcd42bc540162.png

  1. 오류 화면에 버튼을 추가합니다. HomeScreen.kt 파일에서 retryAction 람다 매개변수와 버튼을 포함하도록 ErrorScreen() 컴포저블을 업데이트합니다.
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
    Column(
        // ...
    ) {
        Image(
            // ...
        )
        Text(//...)
        Button(onClick = retryAction) {
            Text(stringResource(R.string.retry))
        }
    }
}

미리보기 확인

55cf0c45f5be219f.png

  1. 재시도 람다를 전달하도록 HomeScreen() 컴포저블을 업데이트합니다.
@Composable
fun HomeScreen(
   marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
   when (marsUiState) {
       //...

       is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
   }
}
  1. ui/theme/MarsPhotosApp.kt 파일에서 HomeScreen() 함수 호출을 업데이트하여 retryAction 람다 매개변수를 marsViewModel::getMarsPhotos로 설정합니다. 서버에서 화성 사진을 가져옵니다.
HomeScreen(
   marsUiState = marsViewModel.marsUiState,
   retryAction = marsViewModel::getMarsPhotos
)

6. ViewModel 테스트 업데이트

이제 MarsUiStateMarsViewModel이 단일 사진이 아닌 사진 목록을 수용합니다. 현재 상태에서 MarsViewModelTest에서 MarsUiState.Success 데이터 클래스에 문자열 속성이 포함되어야 합니다. 따라서 테스트가 컴파일되지 않습니다. MarsViewModel.marsUiState가 사진 목록이 포함된 Success 상태와 같다고 어설션하도록 marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() 테스트를 업데이트해야 합니다.

  1. rules/MarsViewModelTest.kt 파일을 엽니다.
  2. marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() 테스트에서 assertEquals() 함수 호출을 수정하여 Success 상태(사진 매개변수에 모조 사진 목록 전달)를 marsViewModel.marsUiState와 비교합니다.
@Test
    fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
        runTest {
            val marsViewModel = MarsViewModel(
                marsPhotosRepository = FakeNetworkMarsPhotosRepository()
            )
            assertEquals(
                MarsUiState.Success(FakeDataSource.photosList),
                marsViewModel.marsUiState
            )
        }

이제 테스트가 컴파일 및 실행되고 테스트에 통과합니다.

7. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

이 Codelab의 솔루션 코드는 GitHub에서 확인하세요.

8. 결론

이 Codelab을 완료하고 Mars Photos 앱을 빌드한 것을 축하합니다! 이제 가족과 친구들에게 실제 화성 사진이 담긴 앱을 자랑하세요.

#AndroidBasics를 사용해 작업한 결과물을 소셜 미디어로 공유해 보세요.

9. 자세히 알아보기

Android 개발자 문서:

기타: