androidkotlinandroid-jetpack-composekotlin-flowandroid-paging-3

How to correctly consume flows in the view model?


In my Android app, I am fetching data from Room and displaying it on maps. I came across these two view model approaches to fetch data.

Which one is the more effective and good approach?

Currently, I am fetching data with the second approach. It works fine. Just wondering what are differences between these two are.

First approach:

private val _visibleRegion = MutableStateFlow(defaultVisibleRegion)
private val _zoomLevel = MutableStateFlow(6f)
private val _isFavorite = MutableStateFlow(false)

val mapData: StateFlow<PagingData<MapData>> = combine(
    _visibleRegion,
    _zoomLevel,
    _isFavorite
) { region, zoom, isFav ->
    Triple(region, zoom, isFav)
}.flatMapLatest { (region, zoom, isFav) ->
    repo.getDataInBoundsPaged(
        region.southwest.latitude,
        region.northeast.latitude,
        region.southwest.longitude,
        region.northeast.longitude,
        zoom = zoom,
        isFav = isFav
    )
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = PagingData.empty()
)

fun updateVisibleRegion(region: LatLngBounds) {
    _visibleRegion.value = region
}

fun updateZoomLevel(zoom: Float) {
    _zoomLevel.value = zoom
}

fun toggleFavorite(isFavorite: Boolean) {
    _isFavorite.value = isFavorite
}

vs Second approach:

private val _mapData = MutableStateFlow<PagingData<MapData>>(PagingData.empty())
val mapData: StateFlow<PagingData<MapData>> = _mapData.asStateFlow()

private val defaultVisibleRegion = LatLngBounds(
    LatLng(6.792525080049721, 70.06730787456036),
    LatLng(37.411989227903604, 86.94231156259775)
)

init {
    fetchMapData()
}

fun fetchMapData(
    visibleRegion: LatLngBounds = defaultVisibleRegion,
    zoomLevel: Float = 6f,
    isFavorite: Boolean = false
) {
    viewModelScope.launch {
      
        repo.getDataInBoundsPaged(
            visibleRegion.southwest.latitude,
            visibleRegion.northeast.latitude,
            visibleRegion.southwest.longitude,
            visibleRegion.northeast.longitude,
            zoom = zoomLevel,
            isFav = isFavorite
        )
          
            .cachedIn(viewModelScope)
            .collect {
                _mapData.value = it
             
            }
    }
}

Solution

  • You should avoid collecting flows in the view model. If you only collect in the UI the UI can subscribe and unsubscribe from the StateFlow as needed so the underlying flows can be stopped when not needed. This is automatically done when you collect using collectAsStateWithLifecycle.

    Therefore you shouldn't use the second approach.

    The first approach looks fine, you just might want to also cache the paging data as you do it in the second approach. Also the Triple conversion can be simplified a bit:

    val mapData: StateFlow<PagingData<MapData>> =
        combine(
            _visibleRegion,
            _zoomLevel,
            _isFavorite,
            ::Triple,
        )
        .flatMapLatest { (region, zoom, isFav) ->
            repo.getDataInBoundsPaged(
                region.southwest.latitude,
                region.northeast.latitude,
                region.southwest.longitude,
                region.northeast.longitude,
                zoom = zoom,
                isFav = isFav,
            )
        }
        .cachedIn(viewModelScope)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = PagingData.empty(),
        )