[Android] 026. Paging3

024. Room(DB)の内容にもっとも簡素なPagingの機能を追加してみました

developerの説明は以下
https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ja

依存関係追加

以下をProject Structureなどでimplementationします
Paging3を使うためにandroidx.pagingは3.0.0以上を指定してください(ここでは3.2.0)

androidx.room:room-paging
androidx.paging:paging-runtime-ktx
androidx.paging:paging-compose

追加後以下のようにバージョンを指定しなおします

[versions]
androidx-room = "2.5.2"
androidx-paging = "3.2.0"

[libraries]
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging" }
androidx-paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "androidx-paging" }

実装

以下を追加

@Dao
interface DataDao {
    @Query("SELECT * FROM data_table ORDER BY id ASC")
    fun pagingSource(): PagingSource<Int, Data>

    ...
}
interface DataRepository {
    fun getPagingStream(): Flow<PagingData<Data>>

    ...
}
class DefaultDataRepository(private val dao: DataDao) : DataRepository {
    override fun getPagingStream(): Flow<PagingData<Data>> {
        return Pager(
            config = PagingConfig(pageSize = PAGE_SIZE, enablePlaceholders = false),
            pagingSourceFactory = { dao.pagingSource() }
        ).flow
    }
    
    ...

    companion object {
        const val PAGE_SIZE = 20
    }
}
class DataListViewModel(private val repo: DataRepository) : ViewModel() {
    val pagingDataFlow = repo.getPagingStream().cachedIn(viewModelScope)

    ...
}

Viewを以下のように書き換えます
LazyColumnを一番下までスクロールさせるとitemCountが20ずつ増えることが確認できると思います

LazyColumnのitemsにitems(dataList)といった設定はPaging3.2.0でできなくなってます
https://developer.android.com/jetpack/androidx/releases/paging

@Composable
fun DataListScreen(
    viewModel: DataListViewModel = androidx.lifecycle.viewmodel.compose.viewModel {
        val application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY) as Application)
        DataListViewModel(application.container.dataRepository)
    },
    modifier: Modifier = Modifier
) {
    val dataList = viewModel.pagingDataFlow.collectAsLazyPagingItems()
    val names = listOf("alpha", "bravo", "charlie", "echo")
    val listState = rememberLazyListState()

    // テスト用にとりあえずDBに追加
    LaunchedEffect(Unit) {
        repeat(100) {
            val index = (0..3).random()
            viewModel.insertData(Data(name = names[index]))
        }
    }

    LaunchedEffect(listState) {
        snapshotFlow { listState.layoutInfo.visibleItemsInfo.size }
            .collect {
                Log.d("App", "${dataList.itemCount}")
            }
    }

    Column(modifier) {
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = {
                viewModel.deleteAll()
            }) {
                Text("Cleanup")
            }
        }

        LazyColumn(
            state = listState
        ) {
            items(
                count = dataList.itemCount,
                key = dataList.itemKey { it.id },
            ) { index ->
                val data = dataList[index]
                ListItem(
                    headlineContent = {
                        Text("$index. ${data?.name}(${data?.id})")
                    },
                )
            }
        }
    }
}

検索対応

paging使っていても検索で絞り込みできます

pagingSourceのSQLにLIKEを追加してそれに対応した引数を各個に追加
検索用のUIを追加してます

@Dao
interface DataDao {
    @Query("SELECT * FROM data_table WHERE name LIKE '%' || :search || '%' ORDER BY id ASC")
    fun pagingSource(search: String): PagingSource<Int, Data>
}
class DataRepository(private val dao: DataDao) {
    fun getPagingSource(search: String) = dao.pagingSource(search)
}
class DataListViewModel(private val repo: DataRepository) : ViewModel() {
    private val _search = MutableStateFlow("")
    private val search = _search.asStateFlow().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(),
        initialValue = "",
    )
    @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
    val pagingDataFlow = search.debounce(300.milliseconds).flatMapLatest { query ->
        Pager(
            config = PagingConfig(pageSize = 10, enablePlaceholders = false),
            pagingSourceFactory = { repo.getPagingSource(query) }
        ).flow.cachedIn(viewModelScope)
    }

    fun search(query: String) {
        _search.value = query
    }
}
@Composable
fun DataListScreen(
    viewModel: DataListViewModel = androidx.lifecycle.viewmodel.compose.viewModel {
        val application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY) as Application)
        DataListViewModel(application.container.dataRepository)
    },
    modifier: Modifier = Modifier
) {
    val dataList = viewModel.pagingDataFlow.collectAsLazyPagingItems()
    val names = listOf("alpha", "bravo", "charlie", "echo")
    val listState = rememberLazyListState()

    // テスト用にとりあえずDBに追加
    LaunchedEffect(Unit) {
        repeat(100) {
            val index = (0..3).random()
            viewModel.insertData(Data(name = names[index]))
        }
    }

    var query by remember { mutableStateOf("") }
    Column(modifier) {
        Row(
            Modifier.padding(horizontal = 8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(value = query, onValueChange = { query = it }, Modifier.weight(1f))

            Button(onClick = {
                viewModel.search(query)
            }) {
                Text("search")
            }
        }

        LazyColumn(
            state = listState
        ) {
            items(
                count = dataList.itemCount,
                key = dataList.itemKey { it.id },
            ) { index ->
                val data = dataList[index]
                ListItem(
                    headlineContent = {
                        Text("$index. ${data?.name}(${data?.id})")
                    },
                )
            }
        }
    }
}

Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023