androidkotlinandroid-paging-3kotlin-stateflow

Android MVI using StateFlow and paging 3


I am trying to implement android MVI architecture using state flow and paging 3 but I got confused when I had a view state which contains paging data.

The problem is that I expose the view state from view model as a state flow object, but now inside that view state I have another flow object which comes from the paging library.

Is it OK to have a flow inside a state flow? and if it’s not what should I do instead?

This is my code for more clarification.

TaskRepository

override fun list(
pageNumber: Int,
pageSize: Int,
groupId: Long?,
query: String
): Flow<PagingData<Task>> {
return Pager(
    config = PagingConfig(
        pageSize = Consts.PageSize,
        maxSize = 200,
        enablePlaceholders = false
    ),
    remoteMediator = TaskRemoteMediator(query, groupId, db, taskApi),
    pagingSourceFactory = {
        TaskDataSource(taskApi, groupId, query)
    }
).flow
}

TaskViewModel

viewModelScope.launch {
try {
    _taskListViewState.emit(TaskListViewState.Loading)
    val tasks = taskRepo.list(1, Consts.PageSize, intent.groupId, "")
    _taskListViewState.emit(TaskListViewState.Data(tasks))
} catch (e: Exception) {
    _taskListViewState.emit(TaskListViewState.Error(R.string.unknown_error))
}
}

TaskListViewState

sealed class TaskListViewState {
object Idle : TaskListViewState()
object Loading : TaskListViewState()
data class Data(val tasks: Flow<PagingData<Task>>) : TaskListViewState()
data class Error(val error: Int) : TaskListViewState()
}

TaskListFragment

private fun observeViewState() {

lifecycleScope.launchWhenStarted {
    viewModel.taskListViewState.collect {
        render(it)
    }
}
}

private fun render(viewState: TaskListViewState) {
Log.d(TAG, "render: $viewState")
when (viewState) {
    is TaskListViewState.Loading -> showLoading()
    is TaskListViewState.Idle -> hildeLoading()
    is TaskListViewState.Error -> {
        hildeLoading()
        showMessage(viewState.error)
    }
    is TaskListViewState.Data -> {
        hildeLoading()
        lifecycleScope.launchWhenCreated {
            viewState.tasks.collectLatest {
                tasksAdapter.submitData(lifecycle, it)
            }

        }
    }
}
}

Solution

  • A bit late but you can modify the classes like below and you wouldn't need flow inside the TaskListViewState.

    TaskListViewState

    sealed class TaskListViewState {
       object Idle : TaskListViewState()
       object Loading : TaskListViewState()
       data class Data(val tasks: PagingData<Task>) : TaskListViewState()
       data class Error(val error: Int) : TaskListViewState()
    }
    

    TaskViewModel

    viewModelScope.launch {
        try {
            _taskListViewState.emit(TaskListViewState.Loading)
            taskRepo.list(1, Consts.PageSize, intent.groupId, "")
              .cacheIn(viewModelScope)
              .collectLatest { pagingData ->
                 _taskListViewState.emit(TaskListViewState.Data(pagingData))
              }
        } catch (e: Exception) {
           _taskListViewState.emit(TaskListViewState.Error(R.string.unknown_error))
        }
    }
    

    TaskListFragment

    private fun observeViewState() {
    
        lifecycleScope.launchWhenStarted {
            viewModel.taskListViewState.collect {
                render(it)
            }
        }
    }
    
    private fun render(viewState: TaskListViewState) {
        Log.d(TAG, "render: $viewState")
        when (viewState) {
            is TaskListViewState.Loading -> showLoading()
            is TaskListViewState.Idle -> hildeLoading()
            is TaskListViewState.Error -> {
                hildeLoading()
                showMessage(viewState.error)
            }
            is TaskListViewState.Data -> {
                hildeLoading()
                tasksAdapter.submitData(lifecycle, viewState.tasks)
            }
        }
    }