Skip to content

[Feature Request] Improved Support for Pull to Refresh #124

@alexandercasal

Description

@alexandercasal

Is your feature request related to a problem? Please describe.
The documentation states "Another good use case for fresh() is when a user wants to pull to refresh." While this is correct, there is a drawback to using fresh() for pull to refresh events: We don't get the Loading or Error states from the StoreResponse. This may mean implementing additional logic around showing the user the loading state or error state in the UI when using both stream(), for the primary flow of data, and fresh() for the pull to refresh event.

Describe the solution you'd like
I created the following extensions to be used in a pull refresh scenario and have found they work well. Since pull to refresh is common, perhaps these would be good to integrate into Store somehow?

/**
 * Use to trigger a network refresh for your Store. An example use-case is for pull to refresh functionality.
 * This is best used in conjunction with `Flow.collectRefreshFlow` to prevent this returned Flow from living
 * on longer than necessary and conflicting with your primary Store Flow.
 */
fun <Key : Any, Output : Any> Store<Key, Output>.refreshFlow(key: Key) = stream(StoreRequest.fresh(key))

/**
 * Helper to observe the loading state of a Store refresh call triggered via `Store.refreshFlow`. This Flow will
 * automatically be cancelled after the refresh is successful or has an error.
 *
 * @param checkLoadingState Lambda providing the current StoreResponse for the refresh (Loading, Error, or Data) allowing
 * you to decide when to show/hide your loading indicator.
 */
suspend fun <T> Flow<StoreResponse<T>>.collectRefreshFlow(checkLoadingState: suspend (StoreResponse<T>) -> Unit) {
    onEach { storeResponse ->
        checkLoadingState(storeResponse)
    }.first { storeResponse ->
        storeResponse.isData() || storeResponse.isError()
    }
}

Examples of the solution in use
Below is an example of how refreshFlow is used alongside the main flow:

fun getUserDataFlow(): Flow<StoreResponse<List<UserData>>> {
   return myStore.stream(StoreRequest.cached(Key("123"), refresh = true))
}

fun refreshUserDataFlow(): Flow<StoreResponse<List<UserData>>> {
   return myStore.refreshFlow(Key("123"))
}

And finally an example of how collectRefreshFlow is used in a ViewModel alongside the main flow:

class MyViewModel {
   val userDataLiveData = userRepository.getUserDataFlow()
      .onEach { storeResponse ->
            setLoadingSpinnerVisibility(storeResponse)
            setEmptyStateVisibility(storeResponse)
       }
       .filterIsInstance<StoreResponse.Data<List<UserData>>>()
       .mapLatest { storeResponse ->
            storeResponse.dataOrNull() ?: emptyList()
       }
       .flowOn(Dispatchers.IO)
       .asLiveData().distinctUntilChanged()
   }

   fun onRetryLoadUserData() = viewModelScope.launch {
      if (networkIsConnected()) {
         userRepository.refreshUserDataFlow()
            .collectRefreshFlow { storeResponse -> setLoadingSpinnerVisibility(storeResponse) }
      }
   }
}

Additional context
I feel these two extensions provide more power/flexibility in the pull to refresh scenario than simply calling the suspending fresh() method. Does it seem like these or something similar could fit into the Store API?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions