こんな経験ないですか?
- 大規模なデータのリストを表示したいけどやり方がわからない
- すべて読みこんでからリスト表示すると非効率そう
ページネーションって何?
これらの課題を解決する技術がページネーション
Paging 3
Android
での実装はPaging3
ライブラリを用いる
登場人物は以下の通り
リポジトリレイヤ
-
PagingSource
- 特定のページクエリのデータチャンクを読み込むためのクラス
ViewModelレイヤ
-
Pager
-
PagingSource
からページングされたデータのストリーム(Flow<PagingData>
)を生成する
-
-
Flow<PagingData>
- ページングされたデータの非同期ストリーム。データのロード状態、エラー、およびページネーション情報をUIに提供する。
UIレイヤ
-
LazyPagingItems
-
PagingData
のフローから取得したデータを表示するためのクラス
-
データの流れ
APIデータ
→PagingSource
→Pager
→Flow<PagingData>
→LazyPagingItems
それぞれの役割
APIデータ
→PagingSource
データソースを定義
API通信したpage番号ごとにアイテムのページを読み込む実装を行う
kotlinコルーチンを使用するにはPagingSourceクラスを直接使う
返り値は PagingSource<Int, HogeDataClass>()
getRefreshKey
とload
を実装しなければならない
getRefreshKey
リフレッシュ(データの再読み込み)を行う際に、どのキー(ページ番号)からリフレッシュを開始するかを決定するために使用される
load
Web API やローカルストレージからデータを取得し、LoadResult 型で返す
key
-
prevKey
:- 現在のページよりも前のページのキーを表す
-
if (page == 1) null else page - 1
という条件式で計算される- これは、最初のページ(ページ番号1)の場合、前のページが存在しないため
null
を設定し、それ以外のページでは現在のページ番号から1を引いた値を設定する
- これは、最初のページ(ページ番号1)の場合、前のページが存在しないため
-
nextKey
:- 現在のページよりも後のページのキーを表す
-
if (characterPage.characters.isEmpty()) null else page + 1
という条件式で計算される- これは、現在のページのキャラクターリストが空の場合、次のページが存在しないため
null
を設定し、それ以外の場合は現在のページ番号に1を足した値を設定する
- これは、現在のページのキャラクターリストが空の場合、次のページが存在しないため
class CharacterPagingSource(private val ktorClient: KtorClient) :
PagingSource<Int, Character>() {
override fun getRefreshKey(state: PagingState<Int, Character>): Int? {
// アンカー位置(最後にアクセスされたアイテムの位置)から最も近いページ番号を返す
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Character> {
// 最初のロードは1ページ目がロードされる
val page = params.key ?: 1
return try {
when (val result = ktorClient.getCharacterByPage(page)) {
is ApiOperation.Success -> {
val characterPage = result.data
LoadResult.Page(
data = characterPage.characters,
prevKey = if (page == 1) null else page - 1,
nextKey = if (characterPage.characters.isEmpty()) null else page + 1
)
}
is ApiOperation.Failure -> LoadResult.Error(result.exception)
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
class CharacterRepositoryImpl @Inject constructor(
private val ktorClient: KtorClient,
) : CharacterRepository {
override fun getCharacterPagingSource(): PagingSource<Int, Character> {
return CharacterPagingSource(ktorClient)
}
}
PagingSource
→Pager
→Flow<PagingData>
ポイント
-
config
でページングの設定を行う -
pagingSourceFactory
でデータソースを管理 -
Pager
の.flow
プロパティを呼び出すとFlow<PagingData>
というデータストリームが生成される -
.cachedIn(viewModelScope)
で画面回転などの設定変更時にデータの再ロードを防ぐ
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val repository: CharacterRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow<HomeScreenViewState>(HomeScreenViewState.Loading)
val viewState: StateFlow<HomeScreenViewState> = _viewState.asStateFlow()
init {
loadCharacters()
}
private fun loadCharacters() {
viewModelScope.launch {
try {
val pagingSource = repository.getCharacterPagingSource()
_viewState.update {
HomeScreenViewState.GridDisplay(
characters = Pager(
config = PagingConfig(enablePlaceholders = false, pageSize = 20),
pagingSourceFactory = { pagingSource }
).flow.cachedIn(viewModelScope)
)
}
} catch (e: Exception) {
_viewState.update { HomeScreenViewState.Error(e.message ?: "Unknown error") }
}
}
}
}
Flow<PagingData>
→ LazyPagingItems
@Composable
fun HomeScreen(
viewModel: HomeScreenViewModel = hiltViewModel(),
) {
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
when (val state = viewState) {
is HomeScreenViewState.Loading -> {
LoadingState()
}
is HomeScreenViewState.GridDisplay -> {
Log.d("HomeScreen", "viewState in GridDisplay: $state")
val characters = state.characters.collectAsLazyPagingItems()
CharacterGrid(characters = characters)
}
is HomeScreenViewState.Error -> {
val errorMessage = state.errorMessage
Text(text = "Error: $errorMessage")
}
}
}
Flow<PagingData<Character>>
を.collectAsLazyPagingItems()
に変更
@Composable
fun CharacterGrid(
characters: LazyPagingItems<Character>,
) {
val navController = LocalNavController.current
LazyVerticalGrid(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
columns = GridCells.Fixed(2),
) {
items(characters.itemCount) { index ->
val character = characters[index]
character?.let {
CharacterGridItem(
modifier = Modifier,
character = it,
onCharacterSelected = { navController.navigateToCharacterDetailsScreen(it.id) }
)
}
}
}
}
itemに流し込んで終了
参考文献
ページング ライブラリの概要 | Android Developers
Jetpack ComposeとPaging3で実現する Endless Grid Design
【Android】Paging 3で実装するEndless Grid Design - Qiita