PagingとStateFlowとを使ってフィルタリストを作成する

本稿が初投稿になります。
Compose UIでPagingデータを表示する際、フィルタリストを作成するのにちょっと苦労したので、覚え書きとして、こちらに問題と解決策を記します。

# 要約
　Pagingを読み込む条件を変更した時、LazyColumnに表示しているデータが更新されませんでした。原因は、PagingデータがFlowで読み込まれていたためです。そこでStateFlowを使用し、Pagingデータを状態として受け取ることで、Pagingデータが変更された際の再描画を実装しました。

# Pagingの基礎
　Compose UIでPagingを実装する方法は[公式ページ](https://developer.android.com/topic/libraries/architecture/paging/v3-overview)に書かれていますので、ここでは省略して記述します。
　まずPagingSourceでデータを読み込む処理を実装します。データベースから順々にデータを読み込む処理です。次に、PagingSourceを使ってローディングデータをPagerとして受け取ります。ViewModelを使うアーキテクチャでは、次のようにViewModel内でPagerを取得します。
```
class HomeViewModel(application: Application): ViewModel(){
    private val applicationContext = application
    ...
    val pager = Pager (
        config = PagingConfig (
            pageSize = PAGE_SIZE,
            enablePlaceholders = false
        ),
        pagingSourceFactory = {
            homePagingSource (applicationContext)
        }
    )
    ...
    companion object {
        const val PAGE_SIZE = 10
    }
}
```
　Compose UIではFlowとしてPagingデータを取得できます。
```
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
    ...
    val pagingData = viewModel.pager.flow.collectAsLazyPagingItems()
    ...
}
```
　このpagingDataはLazyPagingItemsというFlow Objectです。これをLazyColumnで表示することで、スクロールするごとにデータが順々に読み込まれる仕組みができます。以上が基礎的なPagingの実装です。


# Pagingでフィルタ機能を実装する
　さて、ここからが本題です。例えば食品の在庫データを表示するリストを作成するとして、次のイメージのように"果物"だけを表示するフィルタを実装したい場合を考えます。
| "全て"フィルタ | "果物"フィルタ |
|:-----------:|:------------:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940877/e6a1ac19-3b6b-318b-362c-82e528343621.png" width="200">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940877/defb0974-0344-2256-006e-eb1dfebf1c1d.png" width="200">|

　次のようにPagingSourceに読み込み条件selectionを追加します。
```
val pager = Pager (
    config = PagingConfig (
        pageSize = PAGE_SIZE,
        enablePlaceholders = false
    ),
    pagingSourceFactory = {
        HomePagingSource (
            applicationContext,
            selection // null or "fruit"
        )
    }
)
```
　`HomePagingSource`ではROOMを使ってデータの読み込みをしています。selectionはデータを読み込む条件、すなわちフィルタとお考えください。SQLiteでのWHERE句にあたります。
```
@Dao
interface productDao {
    @Query("SELECT * FROM product_table WHERE (:category IS NULL OR category = :selection)")
    suspend fun getItems(selection: String?): List<ProductItem>
}
```
　このselectionをSelectedButtonなどによるユーザ入力で切り替えたいのですが、前項の`pager.flow.collectAsLazyPagingItems()`でデータを取得していると、表示が切り替わりません。なぜなら、pagerでデータを取得する時はFlowを使っているからです。
　[リファレンス](https://developer.android.com/develop/ui/compose/lifecycle?hl=ja)にあるように、Compose UIが再描画されるのは、状態（StateまたはStateFlow）が変わったときです。Flowが変化しても再描画が起こりません。

# 解決
　上述のように、普通に`pager.flow.collectAsLazyPagingItems()`でPagingデータを取得すると、フィルタを切り替えた時にリスト表示が再描画されません。そこでStateの仕組みをPagingに取り入れる必要があります。
　いくつか方法があるとは思いますが、ここではStateFlowを使った方法を紹介します。次のようにPagerをStateFlowで取得するように変更します。
```
class HomeViewModel(application: Application): ViewModel(){
    private val applicationContext = application
    ...
    private val _filter = MutableStateFlow<String?>(null)
    fun changeFilter(selection: String?) {
        _filter.value = selection
    }

    private val _pager = MutableStateFlow(getPager(null))
    val pager: StateFlow<Pager<Int, ProductData>> = _pager  

    private fun getPager(filter: String?){
        Pager (
            config = PagingConfig (
                pageSize = PAGE_SIZE,
                enablePlaceholders = false
            ),
            pagingSourceFactory = {
                HomePagingSource (
                    applicationContext,
                    filter
                )
            }
        )
    }
    
    init {
        // ここがミソです.
        // フィルタが変わったときだけpagerが更新されるようにします.
        _filter.collectLatest { latestFilter ->
            _pager.update { getPager(latestFilter) }
        }
    }
    ...
    companion object {
        const val PAGE_SIZE = 10
    }
}
```
　そしてUIでは次のように`pager`を監視します。
```
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
    ...
    // 状態selectionStateがSegmentedButtonの切り替えで変化する
    var selection = when (selectionState) {
            0 -> null
            1 -> "fruit"
        }

    viewModel.changeFilter(selection)
    val productPager by viewModel.pager.collectAsState()
    val productData = productPager.flow.collectAsLazyPagingItems()

    ...
}
```
　これでPagerがStateとして扱われるので、フィルタを切り替えるたびにリストが再描画されるようになります。