Room + Paging 완전 가이드 — PagingSource/RemoteMediator/오프라인 캐시
Source: Dev.to
위에 제공된 Source 링크만 그대로 두고, 번역이 필요한 본문 내용을 알려주시면 해당 부분을 한국어로 번역해 드리겠습니다.
(코드 블록이나 URL은 그대로 유지하고, 마크다운 형식과 기술 용어는 원본 그대로 유지합니다.)
이 문서에서 배울 수 있는 것
DAO
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY createdAt DESC")
fun getArticlesPaging(): PagingSource
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(articles: List)
@Query("DELETE FROM articles")
suspend fun clearAll()
}
RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator @Inject constructor(
private val api: ArticleApi,
private val db: AppDatabase
) : RemoteMediator() {
override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.page + 1
}
}
return try {
val response = api.getArticles(page = page, pageSize = state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.articleDao().clearAll()
}
db.articleDao().insertAll(response.map { it.copy(page = page) })
}
MediatorResult.Success(endOfPaginationReached = response.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
Repository
class ArticleRepository @Inject constructor(
private val db: AppDatabase,
private val remoteMediator: ArticleRemoteMediator
) {
@OptIn(ExperimentalPagingApi::class)
fun getArticles(): Flow> {
return Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5),
remoteMediator = remoteMediator,
pagingSourceFactory = { db.articleDao().getArticlesPaging() }
).flow
}
}
ViewModel
@HiltViewModel
class ArticleViewModel @Inject constructor(
repository: ArticleRepository
) : ViewModel() {
val articles = repository.getArticles().cachedIn(viewModelScope)
}
Compose UI
@Composable
fun ArticleList(viewModel: ArticleViewModel = hiltViewModel()) {
val articles = viewModel.articles.collectAsLazyPagingItems()
LazyColumn {
items(articles.itemCount) { index ->
articles[index]?.let { article ->
ListItem(
headlineContent = { Text(article.title) },
supportingContent = { Text(article.summary) }
)
}
}
when (articles.loadState.append) {
is LoadState.Loading -> item {
CircularProgressIndicator(
Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
is LoadState.Error -> item {
Text("読み込みエラー", Modifier.padding(16.dp))
}
else -> {}
}
}
}
컴포넌트와 역할
| 컴포넌트 | 역할 |
|---|---|
| PagingSource | 페이지 단위 데이터 가져오기 |
| RemoteMediator | API → DB 동기화 |
| Pager | PagingData 생성 |
| collectAsLazyPagingItems | Compose 연동 |
- Room DAO가 PagingSource를 직접 반환
- RemoteMediator를 통해 API → 로컬 DB 캐시
- 오프라인에서도 DB에서 표시 가능
- cachedIn(viewModelScope)로 재구성 시 재조회 방지
템플릿 공개
8가지 Android 앱 템플릿(Paging 지원)을 공개하고 있습니다。
템플릿 목록 → Gumroad
관련 기사
- Paging3
- Room/Flow
- 오프라인 퍼스트
Gumroad에서 8개의 Android 앱 템플릿(Room DB, Material3, MVVM)을 제공합니다.
템플릿 둘러보기 → Gumroad