[Android,Paging] Compose에서 Paging Library 구현하기 (2)

Paging Library 구현 2번째 포스팅 입니다.

https://jinudmjournal.tistory.com/131

 

[Android,Paging] Compose에서 Paging Library 구현하기 (1)

Paging 라이브러리 Paging 라이브러리는 대규모 데이터 세트에서 페이징 된 데이터를 로드하여 표시하는 기능을 제공한다. Paging 라이브러리를 사용해서 네트워크 데이터 소스에서 페이징 된 데이

jinudmjournal.tistory.com

 

Paging 구현 순서

이 포스팅에서 사용하는 Paging 구현 순서는 아래와 같다.

1. Data Model을 정의한다. 

   - Entity, Dto, Domain Model, Mappers.. 

2. RoomDB를 구현한다.

3. Retrofit 등 리모트 서버에 데이터를 요청하고 받는 코드를 구현한다.

4. RemoteMediator를 생성한다.

   - 외부 서버에서 받은 데이터를 DB로 넘겨주는 역할을 한다.

   - 페이지 단위의 데이터를 주고 받을 때 DB와 서버 측에서 처리할 일을 구현하는 역할을 한다.

5. Room DB의 Dao와 Repository에서 Pager를 return할 함수를 정의한다.

   - 여기서 정의한 함수는 Flow 타입이어야 한다.

6. ViewModel에서 PagingData를 collect하고, LazyColumn에서 데이터를 보여준다.

 

Paging 라이브러리 구현 순서는 위의 플로우를 따른다.

저번 포스팅에서 개념과 Data Model을 정의하는 방법을 학습했다.

 

2. Room DB 구현

생성한 Data Model을 가지고 RoomDB를 구현해야 하는데, 이전에 작성한 포스팅의 DB형식을 가져왔다.

https://jinudmjournal.tistory.com/130

 

[Android, Room] Room Database 스터디 기록 (2)

Room Database 스터디 기록 (1) https://jinudmjournal.tistory.com/129 [Android, Room] Room Database 스터디 기록 (1) https://developer.android.com/training/data-storage/room?hl=ko#components Room을 사용하여 로컬 데이터베이스에 데이

jinudmjournal.tistory.com

RoomDataBase를 구현하는 ItemDao 인터페이스를 생성한 후,

 

@Dao
interface ItemDao {

    @Insert
    suspend fun upsertAll(items : List<ItemEntity>)

    // key - value 형식
    // key : paging 할 기준 값
    // value : 결과로 받을 타입
    @Query("SELECT * FROM items")
    fun pagingSource() : PagingSource<Int, ItemEntity>


    // 컬럼의 데이터를 가져오는 dao 작성
    // 리턴 타입이 반드시 pagingSource 타입이어야 함
    @Query("SELECT * FROM items ORDER BY timestamp DESC")
    fun getItemPager() : PagingSource<Int, Item>

}

 

dao를 실행하는 역할을 하는 Repository를 선언했다.

 

// PagingData 타입의 데이터를 Flow 타입으로 랩한 값을 리턴
// pageSize 한번의 로딩에 얼마나 많은 아이템을 표시할 지 지정
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
    }
}

 

3. 서버에서 데이터 받아오기

데이터를 저장하기 위한 데이터 모델과 로컬 DB를 활용하기 위한 RoomDB를 생성했다.

이제 서버에서 Retrofit을 활용해서 데이터를 받아오는 작업을 해야한다.

 

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

 

위와 같이 데이터를 받아오는 ApiService 인터페이스를 생성했다.

getItems 함수를 사용해서 서버로 item리스트를 받아오는 요청을 보낸다.

 

4. RemoteMediator

이 함수는 아래의 두 가지 역할을 수행한다.

   - 외부 서버에서 받은 데이터를 DB로 넘겨주는 역할을 한다.

   - 페이지 단위의 데이터를 주고 받을 때 DB와 서버 측에서 처리할 일을 구현하는 역할을 한다.

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

이번 예제에서는 사용할 필요가 없지만, 학습을 위해서 구현했다.

 

@OptIn(ExperimentalPagingApi::class)
class ItemRemoteMediator(

) : RemoteMediator<Int,ItemEntity>(){

    // 가장 중요한 구현
    // 서버에서 어떤 값들을 가지고 와서 어떻게 db에 넣어서 보여줄 지 결정
    // load type에 따라서 구현이 달라짐
    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, ItemEntity>
    ): MediatorResult {
        TODO("Not yet implemented")
    }
}

 

가장 중요한 점은 반드시 load 함수를 구현해야 한다는 것이다.

load 함수의 타입에 따라서 구현이 달라지며,  <APPEND,PREPEND,REFRESH> 3가지 타입을 지정할 수 있다.

자세한 내용은 아래 공식 문서를 참고할 수 있다.

https://developer.android.com/reference/kotlin/androidx/paging/RemoteMediator

 

RemoteMediator  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

 

5. Room의 Dao와 Repository 구현

2번의 과정에서 생성한 기능이다.

RoomDataBase를 구현하며, 핵심 코드는 아래와 같다.

 

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

작성한 쿼리문에 따라 모든 item의 데이터를 가져온다.

반드시 return 타입이 PagingSource이어야 하며, Paging Library가 쿼리에 따라서 알아서 데이터를 로드한다.

 

PagingSource 타입은 Key와 Value로 이루어져 있으며, key를 통해서 서버에 데이터를 요청하는 방식을 결정할 수 있다.

key의 page number를 int를 사용할 경우 index를 설정할 수 있고, String을 지정할 경우 토큰을 넘겨줄 수 있다.

대부분의 경우는 Int 형식으로 page number를 전송한다.

 

value에는 PagingSource에 의해서 DB에서 로딩되어지는 데이터 타입을 지정한다.

위 예시에서는 ItemEntity를 활용했다.

 

 6. Repository에서 Pager를 return할 함수 정의

Repository를 구현하는 코드에서는 PagingData 타입의 데이터를 Flow 타입으로 리턴해야 한다.

 

        return Pager(
            config = PagingConfig(
                pageSize = 20, // 한 번에 읽을 데이터 수
                enablePlaceholders = false
            ),
            pagingSourceFactory = {
                dao.getItemPager()
            }
        ).flow

 

PagingConfig를 통해서 한 번에 읽을 데이터의 수를 지정할 수 있다.

pagingSourceFactory에는 위에서 설정한 Dao의 PagingSource 리턴 함수를 지정한다.

마지막으로 리턴되는 타입에 ".flow" 형식으로 래핑해주어야 한다.

 

7. ViewModel과 LazyColumn 활용

데이터를 받아올 준비를 마쳤으므로, ViewModel을 구현한다.

 

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

 

여기서 중요한 점은 ".cachedIn" 함수이다.

이 함수는 Paging 데이터를 받아오는 함수인 getItemPager에 붙여서 사용하며,

화면전환 등 환경 변화에도 값을 viewModelScope에 별도로 저장해서 데이터를 유지하는 역할을 한다.

이 기능을 통해서 페이지 전환 등의 환경이 변하더라도, 값을 매번 새로 로딩하는 현상을 막을 수 있다.

 

이제 각 데이터를 보여줄 ItemScreen을 생성한다.

간단하게 텍스트를 나타내는 코드를 작성했다.

 

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

 

Composable 함수를 통해서 데이터를 페이징 처리를 거쳐서 보여주는 코드를 작성한다.

".collectAsLazyPagingITems()"를 사용해서 뷰 모델에서 페이징 처리한 데이터를 가져온다.

이 후에 LazyColumn에 해당 데이터를 순서대로 뿌려준다.

만약에 아이템이 null일 경우 else 문을 통해서 로딩 화면이나 placeHolder Ui를 보여줄 수 있다.

 

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

    LazyColumn{
        items(lazyPagingItems){item->
            if (item!=null){
                ItemScreen(item)
            }else{
                // 로딩 or placeHolder Ui
            }
        }
    }

    // item이 LazyColum 에 들어왔을 때 아이템이 있는 곳까지 scroll을 내려주는 역할
    val lazyListState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    LaunchedEffect(lazyPagingItems){
        coroutineScope.launch {
            val lastIndex = lazyPagingItems.itemCount -1
            if (lastIndex>=0){
                lazyListState.scrollToItem(index = lazyPagingItems.itemCount -1 )
            }
        }
    }
}

 

아래의 코드는 item이 새로 들어왔을 때, 아이템이 있는 곳까지 scroll 해주는 역할을 한다.

반드시 필요한 코드는 아니지만, 필요한 상황이 생길 때를 대비해서 구현했다.

인덱스를 계산해서 corutineScope 환경에서 뷰를 스크롤한다.

 

이제 대용량 데이터가 들어온다 하더라도, 데이터를 20개씩 끊어서 불러오는 기능이 적용될 것이다.

 

Paging Library를 구현하는 방법에 대하여 정리했으며, 매우 유용하게 사용할 수 있을 것 같다.