DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Room + Paging完全ガイド — PagingSource/RemoteMediator/オフラインキャッシュ

この記事で学べること

Room + Paging(PagingSource、RemoteMediator、オフラインキャッシュ、Compose LazyPaging)を解説します。


Room PagingSource

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY createdAt DESC")
    fun getArticlesPaging(): PagingSource<Int, Article>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(articles: List<Article>)

    @Query("DELETE FROM articles")
    suspend fun clearAll()
}
Enter fullscreen mode Exit fullscreen mode

RemoteMediator

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator @Inject constructor(
    private val api: ArticleApi,
    private val db: AppDatabase
) : RemoteMediator<Int, Article>() {

    override suspend fun load(loadType: LoadType, state: PagingState<Int, Article>): 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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Repository + ViewModel

class ArticleRepository @Inject constructor(
    private val db: AppDatabase,
    private val remoteMediator: ArticleRemoteMediator
) {
    @OptIn(ExperimentalPagingApi::class)
    fun getArticles(): Flow<PagingData<Article>> {
        return Pager(
            config = PagingConfig(pageSize = 20, prefetchDistance = 5),
            remoteMediator = remoteMediator,
            pagingSourceFactory = { db.articleDao().getArticlesPaging() }
        ).flow
    }
}

@HiltViewModel
class ArticleViewModel @Inject constructor(
    repository: ArticleRepository
) : ViewModel() {
    val articles = repository.getArticles().cachedIn(viewModelScope)
}

// Compose
@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 -> {}
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

まとめ

コンポーネント 役割
PagingSource ページ単位データ取得
RemoteMediator API→DB同期
Pager PagingData生成
collectAsLazyPagingItems Compose連携
  • Room DAOがPagingSourceを直接返却
  • RemoteMediatorでAPI→ローカルDBキャッシュ
  • オフラインでもDBから表示可能
  • cachedIn(viewModelScope)で再コンポーズ時の再取得防止

8種類のAndroidアプリテンプレート(Paging対応)を公開しています。

テンプレート一覧Gumroad

関連記事:


I publish 8 Android app templates (Room DB, Material3, MVVM) on Gumroad.

Browse templatesGumroad

Top comments (0)