안드로이드 Paging3 컴포넌트 정복하기 – Part2

지난 Part1 에서는 Paging3 전반적인 내용과 PagingSource를 통해 네트워크로부터 페이징 된 데이터를 로드하고 화면에 나타내는 방법에 대해서 살펴보았다.

이번 포스팅에서는 오프라인 상태 또는 커넥션이 불안정한 상태에서도 앱 사용성을 보장하여 향상된 사용자 경험을 제공하는 방법에 대해서 알아본다. 페이징을 하는 방법은 네트워크와 로컬 데이터베이스로부터 동시에 페이징을 하는 것이다. 이렇게 하면 앱이 로컬 데이터베이스 캐시에서 직접 데이터를 로드하고, 데이터베이스에 더 이상 데이터가 없을 때만 네트워크에 데이터를 요청하게 된다.

기본적인 사용방법

앱이 네트워크 데이터 소스로부터 Post 아이템들을 페이징으로 로드하여 Room 데이터베이스에 로컬 캐시로 저장한다고 가정하자. 다음 나올 그림은 RemoteMediator와 PagingSource가 이 유즈케이스를 충족하기 위해 함께 작동하는 방법을 보여준다.

RemoteMediator는 페이징 된 데이터들을 네트워크로부터 가져와 로컬 데이터베이스에 저장하는 것을 관리한다. 네트워크에서 불러온 데이터를 로드하여 직접적으로 UI에 나타내지 않으며, 앱은 로컬 데이터베이스를 단일 소스 저장소로 사용한다. (예를 들어, Room에 의해 생성된) PagingSource 구현은 로컬 데이터베이스로부터 캐시된 데이터를 불러와 UI에 표현하는 것을 담당한다.

Room entity 생성하기

가장 먼저 해야할 일은 Room 라이브러리를 사용하여 데이터베이스를 정의하고, 네트워크 데이터 소스로 부터 얻은 페이징 된 데이터를 로컬 캐시로 갖도록 하는 것이다. 

Room에 대한 내용은 공식 문서를 확인하자

데이터베이스를 생성했다면, Room에서 사용하는 Entity를 구성하는 것인데 다음과 같이 Long 타입의 id를 기본키로 갖도록 설정했다.

@Entity
data class Post(
        @PrimaryKey val id: Long,
        val date: String,
        val link: String,
        val title: String,
        val content: String,
        val excerpt: String,
        val categories: List<Int>,
        val author: Int
)

그리고 Room 어노테이션 프로세서에 의해 생성되는 PagingSource와 데이터베이스가 가지고 있는 페이징 된 데이터를 쿼리하기 위해 다음과 같이 DAO(Data Access Object)를 정의했다.

@Dao
interface PostDao{

    //전체 Post 목록을 PagingSource로 가져온다.
    @Query("SELECT * FROM post ORDER BY date DESC")
    fun getPosts(): PagingSource<Int, Post>

    //가장 최근 Post를 하나 가져온다.
    @Query("SELECT * FROM post ORDER BY date DESC LIMIT 1")
    fun getLatestPost(): Post?

    //가장 오래 된 Post를 하나 가져온다.
    @Query("SELECT * FROM post ORDER BY date ASC LIMIT 1")
    fun getEarliestPost(): Post?

    //Post 목록 삽입
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(posts : List<Post>)

    // Post 데이터 전체 삭제
    @Query("DELETE FROM post")
    fun deleteAll()

}

RemoteMediator 구현하기

RemoteMediator 는 PagingSource 구성요소와 유사하다. RemoteMediator는 load() 메서드를 가지고 있고, 이를 반드시 재정의 하여 어떻게 데이터를 로딩할 것인지 정의해야 한다. 단지 차이점은 페이징된 데이터를 데이터소스로 부터 가져와 RecyclerView에 로딩하는 것이 아니라, RemoteMediator 객체가 페이징 된 데이터를 네트워크 데이터 소스로 부터 불러오고 데이터베이스에 저장한다는 점이다. 앞에서 나온 그림을 참조하여 이 점을 꼭 명심하자.

다음 나올 예제의 RemoteMediator의 구현은 다음과 같이 2가지 매개변수를 포함하고 있다.

  • db: 로컬 캐시로 사용할 Room 데이터 객체
  • api: 백엔드 서비스를 위한 API 인스턴스 (Retrofit service)

Note:만약 query가 필요하다면 문자열 및 숫자를 매개변수로 추가할 수도 있지만, 이 예제에서는 db와 api만 다룬다.

RemoteMediator<Key, Value> 구현을 생성한다. Key 타입과 Value 타입은 반드시 PagingSource에서 정의한 내용과 같아야한다.(PostDao#getPosts 확인)

@OptIn(ExperimentalPagingApi::class)
class PostRemoteMediator(
        private val db: AppDatabase,
        private val api: PostService
) : RemoteMediator<Int, Post>() {

    override suspend fun load(loadType: LoadType, state: PagingState<Int, Post>): MediatorResult {
        ...
    }
    ...
}

PostRemoteMediator 전체 소스 코드

load() 메서드는 데이터베이스를 갱신하고 PagingSource를 무효화(invalidating)하는 책임이 있다. Room과 같은 Paging3를 지원하는 몇가지 라이브러리는 자동적으로 PagingSource 객체를 무효화하는 구현을 제공한다.

load() 메서드는 다음 두가지 매개변수를 취한다

  • LoadType: REFRESH, APPEND 또는 PREPEND 같은 로드 유형을 알려준다.
  • PagingState: 지금까지 로딩 된 페이지들에 대한 정보, 가장 최근에 엑세스한 인덱스 그리고 페이징 스트림을 초기화 하는데 사용했었던 PagingConfig를 포함한다.

load() 메서드에서 반환되는 값은 MediatorResult 객체다. MediatorResult는 MediatorResult.Error 거나 MediatorResult.Success 둘 중 하나다. MediatorResult.Error는 에러 정보를 담고 있고, MediatorResult.Success는 로드할 데이터가 더 있는지 여부를 나타내는 신호를 포함한다.

load() 메서드는 반드시 다음 단계들을 수행해야 한다.

  1. load type 및 지금까지 로드된 데이터에 의존하여 네트워크로부터 어떤 페이지를 로드 할 지 결정한다.
  2. 네트워크 요청을 트리거 한다.
  3. 로드 작업 결과에 따라 다음과 같은 액션을 수행한다
    – 로드가 성공했고 받은 아이템 목록이 비어있지 않다면, 아이템 목록을 데이터베이스에 저장하고 MediatorResult.Success(endOfPaginationReached = false)를 반환한다.
    – 로드가 성공했고 받은 아이템 목록이 비어 있다면, MediatorResult.Success(endOfPaginationReached = true)를 반환한다.
    – 만약 네트워크 요청으로 인해 에러가 발생한다면 MediatorResult.Error를 반환한다.

Pager 생성하기

드디어 Pager 인스턴스를 만들 준비가 되었다. Pager를 통해 페이징 된 데이터를 받을 수 있는 스트림을 설정할 수 있다. Pager 인스턴스를 생성하는 것은 Part1에서 나온 네트워크 데이터 소스로 부터 간단히 Pager를 생성하는 방식과 비슷하지만 두가지 다른 점이 있다.

  • PagingSource 생성자를 직접적으로 전달하는 대신에, 반드시 DAO로 부터 PagingSource를 반환하는 query 메서드를 제공해야 한다.
  • 매개변수 “remoteMediator”로 RemoteMediator 구현 인스턴스를 제공해야 한다.
class PostRepository @Inject constructor(
        private val db: AppDatabase,
        private val api: PostService
) {
    fun getPosts(pageSize: Int) = Pager(
            config = PagingConfig(pageSize = pageSize, enablePlaceholders = false),
            remoteMediator = PostRemoteMediator(db, api)

    ) {
        db.postDao().getPosts()
    }.flow

}

샘플 앱 및 소스코드

최대한 구글에서 가이드 하는 방식대로 Paging3 라이브러리를 사용하는 샘플 앱을 만들었다. 이 앱은 찰스의 안드로이드 블로그(https://charlezz.com)의 게시물을 페이징하여 불러와 보여주는 단순한 앱이다. Room과 Paging3 만으로 간단히 리소스를 효율적으로 사용하며, 오프라인 모드를 지원하는 앱을 만들 수 있어서 놀라웠다. 

샘플 앱은 구글 플레이스토어에서 다운로드 할 수 있으며, 소스코드는 github 에서 확인 가능하다.

카테고리: 미분류

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.