(5)-2 현재 상영작 그리기 - 포스터 그리기 및 pagination

HEYDAY7·2022년 10월 17일
0

mappers.kt 분리

지난 작업에서 삽질을 했던 mapper 쪽을 먼저 건들여보기로했다. 그런데 문뜩 정말 그냥 코드만 분리하면 안되는건가라는 단순한 생각이 들어 mapper들만 mappers.kt라는 파일을 만들어 분리해봤다..... 지난 날의 삽질이 아주 한심해지는 느낌으로 그냥 바로 해결됐다. 괜시리 mapper를 만들어 주입하겠다~ 이런 있어보이는 것만 하려하다 그랬던 것 같다. 그래서 뭐 일단 기분좋고 깔끔하게 코드를 분리할 수 있게 되었다.

## mappers.kt
fun MovieDTO.toMovie() = Movie(
    adult,
    id,
    imdbId,
    originalLanguage,
    originalTitle,
    overview,
    popularity,
    posterPath,
    productionCompanies.map { it.toCompany() },
    productionCountries.map { it.toCountry() },
    releaseDate,
    runtime,
    spokenLanguages.map { it.toLanguage() },
    status,
    title
)

fun CompanyDTO.toCompany() = Company(
    name, id, logoPath, originCountry
)

fun CountryDTO.toCountry() = Country(
    isoId, name
)

fun LanguageDTO.toLanguage() = Language(
    isoId, name
)

// SimpleMovie
fun SimpleMovieDTO.toSimpleMovie() = SimpleMovie(
    posterPath,
    adult,
    overview,
    releaseDate,
    genreIds,
    id,
    originalTitle,
    originalLanguage,
    title,
    popularity
)

// Pagination
fun <T, R> PaginationDTO<T>.toPagination(transformer: (T) -> (R)) = Pagination(
    page,
    results.map { transformer(it) },
    totalPages,
    totalResults
)

Flow 적용하기

Flow는 데이터스트림이며, 코루틴(Coroutine)상에서 리액티브 프로그래밍을 지원하기 위한 구성요소라고 한다. 이에 대한 정보는 따로 글을 작성하겠고, 지금은 단순히 적용만 한다.
코드적으로 적용할 것은 단순하게 return 타입을 flow로 바꿔주기만 하면 된다.

## MovieRepository.kt
// suspend를 제거하고 리턴 타입을 Flow로 한번 감싸줬다.
fun getMovieNowPlaying(
        page: Int,
        region:String = "KR",
        language: String = "ko"
    ): Flow<Pagination<SimpleMovie>>
    

## MovieRepositoryImpl.kt
// suspend를 제거하고 flow로 코드를 감싸준 뒤, api 결과물 자체를 emit!! 해준다.
// 이 emit을 꼭 해줘야 하며, 뒤에 붙은 flowOn은 Flow 정리 글에서 따로 다루겠다.
override fun getMovieNowPlaying(
        page: Int,
        region:String,
        language: String
    ): Flow<Pagination<SimpleMovie>> = flow {
        emit(
            movieApi.getMovieNowPlaying(page, region, language).toPagination { it.toSimpleMovie() }
        )
    }.flowOn(Dispatchers.IO)

요렇게 flow를 적용하고 나면 데이터를 불러와 사용하는 곳에서도 수정이 필요하다.

## RealHomeViewModel.kt
 movieRepository.getMovieNowPlaying(page = 1, language = "ko")
 	.onEach { pagination ->
    	_state.update {
        	it.copy(
            	moviesNowPlaying = pagination.results
            )
        }
    }
    .catch { e -> e.printStackTrace() }
    .launchIn(viewModelScope)

Network Image 불러오기

영화 포스터를 불러와 그리기 위해선 url로 이미지를 그릴 수 있어야 하는데, 이 때 사용할 수 있는 좋은 library로 coil이 있다. 이미지, 비디오와 같은 미디어를 사용하는데 있어 큰 도움이 되니 참고해보자.

## build.gradle

implementation("io.coil-kt:coil-compose:2.2.2")


## NetworkImage.kt
// 이렇게 coil에서 지원하는 rememberAsyncImagePainter를 이용해 쉽게 그릴 수 있다.

@Composable
fun NetworkImage(
    modifier: Modifier,
    imageUrl: String,
    contentDescription: String? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
) {
    Image(
        painter = rememberAsyncImagePainter(imageUrl),
        contentDescription = contentDescription,
        modifier = modifier,
        alignment = alignment,
        contentScale = contentScale,
        alpha = alpha
    )
}

이렇게 NetworkImage composable을 만들어 imageUrl을 넣어주면 원하는 사진을 불러와 그릴 수 있다.

Pagination 적용하기

모든 리스트들에 기초가 될 수 있는 pagination을 적용해보자. 방식은 여러개가 있겠지만, 이번에 사용하는 현재 상영작 api는 page, total_page를 제공하므로 이를 사용하는 방식으로 작업하였다. 다만 코드는 글이 너무 길어질 거 같아 PaginationState만 적어두고, 나머지는 commit에서 확인해보면 좋을 것 같다.

## HomeViewModel.kt

    data class NowPlayingPaginationState(
        val results: List<SimpleMovie> = emptyList(),
        val isLoading: Boolean = false,
        val isLoadingMore: Boolean = false,
        val page: Int = 1,
        val totalPages: Int = 1,
    ) {
        val paginationEnabled = !isLoadingMore && page < totalPages
    }

이와 같은 PaginationState를 따로 만들어 viewModel에서 state로 가지고 있으면서 pagination을 관리하게 작성했다. 구현 방식을 설명해두면 다음과 같다.
1. viewmodel이 init 될 때 첫 page에 대해서 결과를 받아오고, 이 때 total_page를 저장해둔다.
2. LazyRow를 활용해 마지막 item이 그려질 때 paginationEnabled = true일 경우 이벤트를 통해 loadMore 을 부른다
3. loadMore에서 (현재) page + 1의 값을 넣어 새 데이터를 받아와 합쳐준다.

여기서 핵심이 하나 있다면, loadMore을 부를때 시작에는 loadingMore = true로, api call이 다 끝난 후에는 loadingMore = false로 update 해줘야 하는 것이다. 그렇게 해야 loadMore가 과도하게 불리는 현상을 방지할 수 있다.

마치며

촬영해서 gif로 변환하다보니 살짝 끊기는 것처럼 보이긴 하지만 실제론 매우 부드럽게 pagination이 되는 것을 볼 수 있다. 이번 현재 상영작 관련 작업 코드는 이 branch에서 볼 수 있다.

profile
(전) Junior Android Developer (현) Backend 이직 준비생

0개의 댓글