본문 바로가기
Android Jetpack Compose/Jetpack Compose

Paging Library 구현 방법 총정리 # Android

by Developer88 2023. 4. 17.
반응형

오늘은 Android Paging Library의 구현방법에 대해서 정리해 보도록 하겠습니다.

 

1. Library Implement

가장 먼저 할 것은 라이브러리 선언인데요.

PagingLibrary는 다음의 것들을 선언해 주면 됩니다.

 

def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-compose:1.0.0-alpha18"

 

PagingLibrary를 어떻게 사용하느냐에 따라 다르지만,

대부분 서버와 로컬 DB를 필요로 하므로,

이를 기준으로 Room 과 Retrofit 에 대해서 라이브러리 설치와 설정이 이미 되어 있어야 합니다. 

 

def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
testImplementation "androidx.room:room-testing:$room_version"

 

특히, room-paging 라이브러리는 implementation 해 두어야 합니다.

이 라이브러리가 구현을 굉장히 간단하게 도와 주기 때문입니다.

 

서버에서 데이터를 받아오기 위해서, 

Retrofit과 OkHttp 라이브러리도 필요합니다.

 

implementation "com.squareup.okhttp3:okhttp:4.10.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

def moshi_version = "1.14.0"
implementation "com.squareup.moshi:moshi:$moshi_version"
implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

 

2. Paging Library 큰그림

2-1. Paging Library

아래는 Android 의 공식문서에 나오는 도표인데요.

PagingLibrary의 큰 그림은,

네트워크 즉 외부 서버에서 데이터를 받아오고,

Single Source of Truth로 불리는 데이터베이스에 저장하고,

이것을 ViewModel을 거쳐 UI인 LazyColum에서 보여주는 것을 구현하는 것 입니다.

이 과정에서 특히, 유저가 스크롤을 하거나, 특정버튼을 누르는 등의 이벤트에 맞추어서,

서버에 데이터를 요청하거나, 디비에 있는 데이터를 필요한 만큼만 받아올 수 있게 해 줍니다.

 

 

 

2-2. 이 글에서의 구현 방향

페이징 라이브러리에서의 구현은,

클라이언트에서 리모트 서버로,

페이지 단위수준의 데이터를 요청하면,

그것을 받아서 Paging 라이브러리로 구현하는 것일 텐데요.

 

제가 구현하려는 슬림한 서버 스펙을 클라이언트와 구현하기 때문에,

다량의 데이터를 요청해 받지도 않고,

서버에서 유저가 요청하면 단 하나의 데이터만을 보내주도록 되어있습니다.

서버비용을 최소화하고, 디비자체를 로컬에만 저장하기 때문인데요.

 

그래서 로컬과 서버가 동기화 될 필요도 없고, 과거의 데이터를 서버에서 가져오지도 않습니다.

그래서 위에서 언급한 구현보다는 훨씬 더 심플하구요.

어떤 면에서는 반쪽자리라고 할 수도 있을 것 같네요.

다만, Paging Library의 구현에 대한 전체적인 그림을 이해하기에 좋은 면도 있을 것 같습니다.

 

추후에, 복잡하게 대량의 데이터를 불러오는 방법도 기회가 되면 정리해 보도록 하겠습니다.

 

3. 구현순서

구현 순서에 대해서 간단히 짚고 넘어가겠습니다.

참고로 RoomDB에 기본적으로 필요한 Entity 나 각종 Dao,

그리고 Retrofit 의 기본적인 사항들에 대해서는 여기서 다루지 않았습니다.

우선은 Paging Library의 구현을 중심으로 정리해 보겠습니다.

 

RoomDB에 관해서는 아래 다른 글을 참조해 구현해 주세요.

>> Room DB 사용방법 총정리 # Android SQLite

 

보통 PageLibrary의 구현 순서는 다음과 같습니다.

  1. Create data models: 데이터모델 정의하기(Entity, Dto, Domain Model, Mappers)
  2. RoomDB 구현하기
  3. Retrofit 등 리모트서버에 요청하고 받아오는 코드 구현
  4. RemoteMediator 생성하기 (이 글에서는 필요하지 않아 생략)
    • 위의 도표에 나오는 것처럼, 외부 서버에서 받은 데이터를 디비로 넘겨주는 역할을 함.
    • 페이지 단위의 데이터를 주고 받을 때 디비와 서버측에서 해야할 것을 구현하는 곳 임
    • 이 글의 언급한 목표를 달성하는데는 필요하지 않아 구현하지 않음.
  5. RoomDB의 dao 와 Repository 에서 Pager 를 리턴할 함수를 정의함. return값은 Flow타입으로 랩함.
  6. ViewModel에서 PagingData를 collect 하고, LazyColumn에서 데이터를 보여줌

 

복잡해 보이지만, 하나씩 정리해 보도록 하겠습니다.

 

4. Entity, Dto, Domain Model 정의하기

4-1. Entity

Entity는 RoomDB의 그 Entity클래스가 맞습니다.

RoomDB를 구현하셨다면, 자연스럽게 아래와 같은 Entity클래스를 가지고 있게 됩니다.

Room뿐만이 아니라, Dao, Database class까지 준비되었겠지요.

Paging 을 구현하기 전 RoomDB를 구현하는 방법도 나쁘지 않습니다.

다만, 미리 어떻게 구현하는지 큰그림을 가지고는 있어야 겠지요.

 

@Entity(tableName = "items")
data class ItemEntity(
    @PrimaryKey val id: Int,
    val name: String
)

 

4-2. Dto

뭔가 어려운 말처럼 보이지만, 

사실은 서버에서 데이터를 받아올 때 사용하는 데이터 클래스일 뿐입니다.

보통 서버에서 데이터를 받을 때,

GSON이나 Moshi를 이용해 JSON 타입의 데이터를 받게 되는데요.

그래서 위의 RoomDB때처럼 서버에서 받는 것을 구현할 때 이미 이것을 다 만듭니다.

그냥 구분하기 위해서 클래스 이름뒤에,

Room의 데이터클래스는 Entity로 붙여주고,

서버에서 받아올 때 사용하는 데이터클래스는 Dto를 붙여줍니다.

 

data class ItemDto(
    val id: Int,
    val name: String
)

 

 

4-3. Domain Model

개념상 앱의 센터가 되는 데이터 클래스가,

바로 이 도메인 모델 클래스입니다.

앱에서 DB의 Entity나 서버의 Dto클래스 만으로 커버하기에 부족할 때가 있기 때문이기도 하구요.

디비와 서버의 데이터클래스로부터 분리하기 위한 목적도 있습니다.

다만, 이 예제에서는 심플하게 하기위해서, 위의 데이터클래스들과 차이가 없다는 것을 보실 수 있습니다.

 

data class Item(
    val id: Int,
    val name: String
)

 

4-4. Mapping 함수

각각의 데이터클래스를 사용할 때마다 변화하는 것은 매우 번거롭고 비효율적입니다.

그래서 대부분 Mapping함수를 별도로 만들어 두어서, 그것을 사용하는데요.

서버의 데이터를 다루는 Dto타입의 데이터를 Entity타입으로 변형하거나,

반대의 경우에 대해서 미리 함수로 만들어 두는 것 입니다.

 

fun ItemDto.toItemEntity(): ItemEntity {
  return ItemEntity(
     id = id,
     name = name
  )
}


fun ItemEntity.toItemDto(): ItemDto {
  return ItemDto(
     id = id,
     name = name
  )
}

 

 

5. RoomData베이스 구현

아래와 같이 Room의 Dao와 데이터베이스를 구현해 줍니다.

아래의 Dao내에서 구현할 것이, 

PagingSourceTime으로 리턴하는 pagingSource 함수인데요.

리턴타입을 보면 key 와 value 로 되어 있는 것을 볼 수 있습니다.

key는 페이징을 할 기준이 되는 값이구요.

그 결과로 받을 타입을 value 타입으로 해주면 됩니다.

정수인 각페이지별로 받을 값의 타입은 RoomDB 의 데이터클래스인 ItemEntity 타입으로 받게 되겠지요.

 

@Dao
interface ItemDao {
    @Insert
    suspend fun upsertAll(items: List<ItemEntity>)

    @Query("SELECT * FROM items")
    fun pagingSource(): PagingSource<Int, ItemEntity>
}

@Database(entities = [ItemEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao
}

 

 

위의 코드만으로 Room을 다 구현하지는 못하구요.

전체적인 룸 구현은 아래 글을 참조해 주세요.

>> Room DB 사용방법 총정리 # Android SQLite

 

6. 서버에서 데이터 받아오기 구현

이제 서버에서 Retrofit 으로 데이터를 받아올 차례입니다.

아래와 같이 API클래스를 정의해 주겠습니다.

 

interface ApiService {
    @GET("items")
    suspend fun getItems(@Query("item") item: Int): List<ItemDto>
}

 

7. RemoteMediator

위에서 언급한 것처럼, 저의 경우 이 클래스를 구현할 필요가 없습니다.

왜냐하면  구현스펙이 너무 간단해서 다음과 같기 때문입니다.

  • 서버에서 예전 값들을 가져와서 보여주지 않고,
  • 서버에 요청을 보내도 하나의 데이터 값만 서버가 응답해서 보내주므로,로컬디비에 그것을 바로 저장해버리면 됩니다.
  • Paging Library는 로컬디비에 담긴 데이터를 유저가 스크롤하면 필요한 만큼 보여주는 용도로 사용됩니다.

이러하므로, 저는 RemoteMediator 클래수를 상속받아 구현을 할 필요가 없었습니다.

그래서 Class 자체도 필요하지 않습니다.

 

다만, 추후에 사용하게 되는 환경이 될수도 있으므로 Remote Mediator에 대해서 간단히 정리하고 넘어가겠습니다.

 

이 클래스는 이름에서 Mediator 가 붙은것에서 알 수 있듯이,

리모트서버와 데이터를 이어주는 역할을 합니다.

 

가장 중요한 부분이 load()함수의 구현인데요.

서버에서 어떤값들을 가지고 와서 어떻게 DB에 넣고, 보여주느냐를 구현하는 것이 핵심입니다.

load함수에서 인자로 넘겨주는 LoadType에는 아래의 3가지가 Enum값으로 들어가 있는데요.

이 3가지 타입의 경우의 수를 구현해 주는 것이 핵심입니다.

 

 

위의 3가지 LoadType에 따라 구현을 해야하는데,

앱과서버에 따라 구현의 경우의 수가 조금씩 다릅니다.

아래와 같이 클래스를 상속받아서, LoadType에 따라서 구현을 해주면 됩니다.

 

class ItemRemoteMediator(
    private val apiService: ApiService,
    private val appDatabase: AppDatabase
): RemoteMediator<Int, ItemEntity>() {
    
    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, ItemEntity>
    ): MediatorResult {
        // 구현할 부분
    }

}

 

6. Dao 와 Repository 구현

이제는 RoomDB에서 데이터를 받아서 PagingData 타입의 Flow로 뿌려줄 Dao와 

이 Dao에 접근할 수 있도록 할 Repository 클래스를 구현해야 합니다.

 

6-1. Dao 작성

Dao는 정말 평범합니다. 그냥 해당 컬럼의 데이터를 다 가져오는 것 입니다.

뭔가 limit이나 offset도 있어야 할 것 같은데요.

그걸 다 PagingLibrary 에서 알아서 해 줍니다.

그래서, 사실 이 라이브러리를 써야 하는 것이기도 하구요.

 

주의할 부분은 Return 타입인데요.

PagingSource 타입이어야만 합니다.

 

@Query("SELECT * FROM items ORDER BY timestamp DESC")
fun getItemPager(): PagingSource<Int, ItemEntity>

 

 

PagingSource 의 타입은 Key와 Value로 되어있는데요.

공식문서에 따르면 다음과 같이 되어있습니다.

첫번째 Key밸류의 타입은, PagingLibrary에서 서버에 요청할 때 어떻게 데이터를 요청하는지를 결정하게 할 수 있는데요.

예를 들어서, page number나 item postition을 위해서 Int를 사용하거나,

String을 이용해서 다음 서버호출해서 응답받을 때 토큰을 넘겨주어야 하는 경우들이 있습니다.

아마도 대부분의 겨우 Int에 해당하겠지요.

 

2번째 Value는 PagingSource에 의해서 DB에서 로딩되어지는 데이터타입입니다.

여기서는 ItemEntity가 됩니다.

 

 

6-2. Repository 구현

이제 dao를 실행할 Repository 를 구현해 주어야 하는데요.

아래와 같이 

PagingData타입의 데이터를 Flow타입으로 랩한 값을 리턴해 줍니다.

여기서 pageSize 한번의 로딩에, 얼마나 많은 아이템을 표시할지를 정하는 것 입니다.

 

리턴되는 값은 flow로 랩해야하는데, ".flow"를 붙여주면 접근해서 감쌀수 있습니다.

 

class ItemRepository(
    private val dao: ItemDao,
    private val apiService: ApiService
) {
    fun getItemPager(): Flow<PagingData<Item>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,   //얼마나 많은 아이템을 표시할 지
                enablePlaceholders = false
            ),
            pagingSourceFactory = {
                dao.getItemPager()
            }
        ).flow
    }
}

 

 

만약 저와 다르게 위에서 remoteMediator를 사용하였다면, 여기서 객체를 만들어 넣어주면 됩니다.

마침, Repository이므로 apiService객체와 dao객체 모두 접근할 수 있습니다

 

7. ViewModel 구현과 Compose UI 에서 사용하기

7-1. Viewmodel 과 LazyColumn

이제 ViewModel에서 아래와 같이 가져와서 사용할 수 있게 됩니다.

items 를 아래와 같이 정의해 주고요.

 

class ItemViewModel(private val repository: ItemRepository) : ViewModel() {
    val items: Flow<PagingData<ItemEntity>> = 
    	repository.getItemPager()
}

 

Jetpack Compose에서,

pagingItems는 collectAsLazyPagingItems()를 사용하여서 가져와 주고요.

LazyColumn에서 넣어서 사용해 주기만 하면 됩니다.

이제 받아서 디비에 들어간 데이터는 알아서 Paging Library 로딩해 주게됩니다.

 

@Composable
fun ItemList(viewModel: ItemViewModel) {
    val lazyPagingItems = viewModel.items.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) { item ->
            if (item != null) {
                ItemScreen(item)
            } else {
                //로딩이나 플레이스홀더 UI
            }
        }
    }
}

@Composable
fun ItemScreen(item: Item) {
    Text(text = item.name)
}

 

 

7-2. CachedIn

getItemPager에 cachedIn 함수를 아래와 같이 사용해주면,

화면전환등 환경변화에도 값을 viewModelScope에 저장해서,

데이터를 유지하도록 도와줍니다.

그럼, 환경이 변화되더라도 매번 값을 로딩할 필요가 없어지겠지요.

 

itemRepository.getItemPager().cachedIn(viewModelScope)

 

8. Scroll 

Item이 LazyColum 에 들어왔을 때,

아이템이 있는 곳 까지 Scroll을 내려주어야 하는데요.

Jetpack Compose의 LaunchedEffect를 이용해서 이것을 실행할 수 있습니다.

 

val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val lazyPagingItems = viewModel.items.collectAsLazyPagingItems()
LaunchedEffect(lazyPagingItems){
        coroutineScope.launch {
            val lastIndex = lazyPagingItems.itemCount - 1
            if (lastIndex >= 0) {
                lazyListState.scrollToItem(index = lazyPagingItems.itemCount - 1)
            }
        }
    }

 

참고로, 만약 구현하고자 하는 것이 채팅리스트와 같은 것이라면, LazyColumn의 reverseLayout = true로주고,

db에서 DESC(내림차순)으로 조회를 해주면 이상없이 동작하는 것을 볼 수 있습니다.

 

이상으로 Paging Library를 구현하는 방법에 대해서 정리해 보았습니다.

 

 

728x90

댓글