Search code examples
androidandroid-jetpack-composeandroid-viewmodelkotlin-stateflow

Jetpack Compose, fetch and add more items to LazyColumn using StateFlow


I'm have a LazyColumn that renders a list of items. However, I now want to fetch more items to add to my lazy list. I don't want to re-render items that have already been rendered in the LazyColumn, I just want to add the new items.

How do I do this with a StateFlow? I need to pass a page String to fetch the next group of items, but how do I pass a page into the repository.getContent() method?

class FeedViewModel(
    private val resources: Resources,
    private val repository: FeedRepository
) : ViewModel() {

// I need to pass a parameter to `repository.getContent()` to get the next block of items

    private val _uiState: StateFlow<UiState> = repository.getContent()
        .map { content ->
            UiState.Ready(content)
        }.catch { cause ->
            UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
            initialValue = UiState.Loading
        )
    val uiState: StateFlow<UiState>
        get() = _uiState

And in my UI, I have this code to observe the flow and render the LazyColumn:

    val lifecycleAwareUiStateFlow: Flow<UiState> = remember(viewModel.uiState, lifecycleOwner) {
        viewModel.uiState.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
    }

    val uiState: UiState by lifecycleAwareUiStateFlow.collectAsState(initial = UiState.Loading)

@Composable
fun FeedLazyColumn(
    posts: List<Post> = listOf(),
    scrollState: LazyListState
) {

    LazyColumn(
        modifier = Modifier.padding(vertical = 4.dp),
        state = scrollState
    ) {
 
        // how to add more posts???
        items(items = posts) { post ->
            Card(post) 
        }
    }
}

I do realize there is a Paging library for Compose, but I'm trying to implement something similar, except the user is in charge of whether or not to load next items.

This is the desired behavior:


Solution

  • I was able to solve this by adding the new posts to the old posts before emitting it. See comments below for the relevant lines.

    private val _content = MutableStateFlow<Content>(Content())
        private val _uiState: StateFlow<UiState> = repository.getContent()
            .mapLatest { content ->
                _content.value = _content.value + content // ADDED THIS
                UiState.Ready(_content.value)
            }.catch { cause ->
                UiState.Error(cause.message ?: resources.getString(R.string.error_generic))
            }.stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(stopTimeoutMillis = SUBSCRIBE_TIMEOUT_FOR_CONFIG_CHANGE),
                initialValue = UiState.Loading
            )
    
    private operator fun Content.plus(content: Content): Content = Content(
        posts = this.posts + content.posts,
        youTubeNextPageToken = content.youTubeNextPageToken
    )
    
    class YouTubeDataSource(private val apiService: YouTubeApiService) :
        RemoteDataSource<YouTubeResponse> {
    
        private val nextPageToken = MutableStateFlow<String?>(null)
    
        fun setNextPageToken(nextPageToken: String) {
            this.nextPageToken.value = nextPageToken
        }
    
        override fun getContent(): Flow<YouTubeResponse> = flow {
            // retrigger emit when nextPageToken changes
            nextPageToken.collect {
                emit(apiService.getYouTubeSnippets(it))
            }
        }
    }