Search code examples
androidandroid-paging-3kotlin-coroutineskotlin-stateflow

Pagination with Room not able to merge Flow<PagingData<Model>> with other flows properly


I'm trying to use Paging 3 library to get Flow<PagingData<Scan>> from Room, then check if item was selected or not in the recyclerview so I'm mapping this class to another class called ScanMapper. For achieving this mapping, whenever user marked an item as selected, I updated Map inside a MutableStateFlow<Map<Int, State>>. Here the Map looks up an Index(Int) to get the State, State is just an enum class to represent State UNINITIALISED, USELECTED and SELECTED.

I am setting the value of the Map to a StateFlow<Map<Int,State>>. The problem is, I am trying to combine the Flow<PagingData<Scan>> with the StateFlow<Map<Int,State>> in order to also pass the State as a parameter to the ScanMapper class since this State is taken from the StateFlow<Map<Int,State>> and is not a part of the original Scan class. But the PagingDataAdapter seems to always get the State UNINITIALISED despite when I'm marking item as selected on item click using the markSelected(scanId: Int) function.

Kindly tell me what I am missing here.

UPDATE

I was able to achieve the functionality that i wanted by using a Flow<List<Scan>> and removing the usage of Paging 3 library using a recycler adapter with DiffUtils. Though this is not the actual solution since it eliminated the pagination using paging 3 library, but the following changes allowed me to perform item selection:

Updated Dao

@Query("SELECT * FROM scan ORDER BY timeOfStorage DESC")
fun getAllScansList(): Flow<List<Scan>>

Updated Function in ViewModel

fun getData(context: Context): Flow<List<ScanMapper>> {

    val dao = ScanDatabase.invoke(context).getScanDao()

    return combine(
        dao.getAllScansList(),
        myMap
    ) { scan, stateMap ->
        scan.map {
            it.toScanMapper(stateMap[it.id] ?: State.UNKNOWN)
        }
    }.flatMapLatest {
        flow {
            emit(it)
        }
    }
}

Updated fragment code

mainViewModel.getData(requireContext()).collect {

    adapter.asyncListDiffer.submitList(it as MutableList)
}

Original Question

My Scan class:

@Entity(tableName = "scan")
@Parcelize
data class Scan (
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0,
    val name: String,
    val recognisedText: String,
    val timeOfStorage: Long,
    val filename: String
): Parcelable {

}

My ScanMapper class:

data class ScanMapper(
    val id: Int,
    val name: String,
    val recognisedText: String,
    val timeOfStorage: Long,
    val filename: String,
    val isSelected: State
)

enum class State{
    UNINITIALISED,
    SELECTED,
    UNSELECTED,
    UNKNOWN
}

Converter for Scan to ScanMapper and vice-versa

fun ScanMapper.mapToScan() =
Scan(
    id = id,
    name = name,
    recognisedText = recognisedText,
    timeOfStorage = timeOfStorage,
    filename = filename
)

fun Scan.toScanMapper(isSelected: State) =
ScanMapper(
    id = id,
    name = name,
    recognisedText = recognisedText,
    timeOfStorage = timeOfStorage,
    filename = filename,
    isSelected = isSelected
)

My PagingSource inside Dao

@Query("SELECT * FROM scan ORDER BY timeOfStorage DESC")
fun getAllScans(): PagingSource<Int,Scan>

@Query("SELECT id FROM scan")
fun getAllIds(): List<Int>

Getting flow using getScansFlow()

fun getScansFlow(context: Context): Flow<PagingData<Scan>> {

    val scanDao = ScanDatabase.invoke(context).getScanDao()

    return Pager(
        config = PagingConfig(
            pageSize = 10,
            maxSize = 30,
            enablePlaceholders = false
        ),
        pagingSourceFactory = { scanDao.getAllScans() }
    ).flow.cachedIn(viewModelScope)

}

Initialising the Map for ids present in Room

fun initList() =
       viewModelScope.launch {

    var idList: List<Int> = listOf()

    withContext(Dispatchers.IO) {

        val task = async {
            return@async ScanDatabase.invoke(context).getScanDao().getAllIds()
        }
        idList = task.await()
    }

    val map = _myMap.value.toMutableMap()

    for (id in idList) {
        map[id] = State.UNINITIALISED
    }

    _myMap.value = map

}

Function in ViewModel that gets the flow and combines them

    fun getScansSyncFlow(context: Context) {

    viewModelScope.launch {

        combine(
            getScansFlow(context),
            myMap
        ) { scan, stateMap ->
            scan.map {
                it.toScanMapper(stateMap[it.id] ?: State.UNKNOWN)
            }
        }.collect{
           _myFlow.emit(it) 
        }
    }
  }

myFlow is a StateFlow<PagingData<ScanMapper>?>.

This is how I am updating the Map when an item gets selected by the user in recyclerview.

fun markSelected(scanId: Int) {

    val map = _myMap.value.toMutableMap()
    map[scanId] = State.SELECTED
    _myMap.value = map
}

Then, I am collecting this myFlow in my fragment like this:

lifecycleScope.launchWhenStarted {

mainViewModel.getScansSyncFlow(requireContext())

mainViewModel.myFlow.collect { data ->

      data?.let {
        
          adapter.submitData(it)
       }
    }
}

Solution

  • The immediate issue I see is that you are using collect instead of collectLatest. Since submitData does not return, you will never receive updates from your Flow.

    mainViewModel.myFlow.collectLatest {
      adapter.submitData(it)
    }