I'm using the following code in my Android app to load data from a remote source into a local database cache.
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchSuccess: () -> Unit = { },
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow {
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.(data))
try {
// this could take a while, I want to keep getting updates meanwhile
saveFetchResult(fetch())
onFetchSuccess()
query().map { Resource.Success(it) }
} catch (t: Throwable) {
onFetchFailed(t)
query().map { Resource.Error(t, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
The query is a database query that keeps emitting database updates through emitAll until we call this method again.
The problem with this setup is that Resource.Loading only contains a "snapshot" of the current data (first()) and we won't receive any database updates until we get to the end of the try/catch block and call emitAll. But I would like to keep receiving database updates while Loading is still in progress. However, I can't just call emitAll on Resource.Loading because it would block the whole Flow.
Is there a way to call emitAll on Loading and then switch to Success/Error once the try block has finished?
I've only done simple testing on this to validate it, but it looks like you can listen to the query and emit any/all data it propagates in a newly launched coroutine based on the outer Flow's context -- the other work in the function will continue, unblocked. Once that other work is done, the coroutine that's listening to the query can be cancelled. For example:
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }
): Flow<Resource<ResultType>> = flow {
emit(Resource.Loading())
val data = query().first()
val flow = if (shouldFetch(data)) {
val flowContext = currentCoroutineContext()
val loading: Job = coroutineScope {
launch(flowContext) {
query().map { Resource.Loading(it) }
.collect { withContext(flowContext) { emit(it) } }
}
}
try {
val request = fetch()
loading.cancel()
saveFetchResult(request)
query().map { Resource.Success(it) }
} catch (throwable: Throwable) {
loading.cancel()
onFetchFailed(throwable)
query().map { Resource.Error(throwable, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
Let me know if this works out!
Related
I'm aware that there are a couple of topics on this but none of them solve my issue.
I'm trying to test an implementation of NetworkBoundResource.
inline fun <ResultType, RequestType, ErrorType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> Response<RequestType>,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Response<*>?, Throwable?) -> ErrorType? = { _, _ -> null },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType, ErrorType>> {
val data = query().first()
emit(Resource.Success(data))
if (shouldFetch(data)) {
val fetchResponse = safeApiCall { fetch() }
val fetchBody = fetchResponse.body()
if (fetchBody != null) {
saveFetchResult(fetchBody)
}
if (!fetchResponse.isSuccessful) {
emit(Resource.Error(onFetchFailed(fetchResponse, null)))
} else {
query().map { emit(Resource.Success(it)) }
}
}
}.catch { throwable ->
emit(Resource.Error(onFetchFailed(null, throwable)))
}.flowOn(coroutineDispatcher)
This works as expected in my use case in production code.
override suspend fun getCategories() = networkBoundResource(
query = {
categoryDao.getAllAsFlow().map { categoryMapper.categoryListFromDataObjectList(it) }
},
fetch = {
categoryServices.getCategories()
},
onFetchFailed = { errorResponse, _ ->
categoryMapper.toError(errorResponse)
},
saveFetchResult = { response ->
// Clear the old items and add the new ones
categoryDao.clearAll()
categoryDao.insertAll(categoryMapper.toDataObjectList(response.data))
},
coroutineDispatcher = dispatchProvider.IO
)
I have my test setup like this (using turbine for flow testing).
#OptIn(ExperimentalCoroutinesApi::class)
class NetworkBoundResourceTests {
data class ResultType(val data: String)
sealed class RequestType {
object Default : RequestType()
}
sealed class ErrorType {
object Default : RequestType()
}
private val dispatchProvider = TestDispatchProviderImpl()
#Test
fun `Test`() = runTest {
val resource = networkBoundResource(
query = { flowOf(ResultType(data = "")) },
fetch = { Response.success(RequestType.Default) },
saveFetchResult = { },
onFetchFailed = { _, _ -> ErrorType.Default },
coroutineDispatcher = dispatchProvider.IO
)
resource.test {
}
}
}
The coroutine dispatcher is set to unconfined through DI/Test dispatcher.
I want to test that;
Emitting first data from query, then query is updated and new data from saveFetchResult then query().map { emit(Resource.Success(it)) } emits the updated data from that save result.
I think I need to do something around a spyk on my flow with MockK but I can't seem to figure it out. query() will always return the same flow of data as it's mocked to do so if I awaitItem() again it returns the same data (as it should) as that's what the mock is setup for.
I've found a way to test this. Not exactly how I imagined it in my head.
#Test
fun `Given should fetch is true and fetch throws exception, When retrieving data, Then cached items emitted and error item after`() =
runTest {
val saveFetchResultAction = mockk<(() -> Unit)>("Save results action")
val fetchErrorAction = mockk<(() -> ErrorType)>("Fetch error action")
every { fetchErrorAction() } answers { ErrorType }
val fetchRequestAction = mockk<(() -> Response<RequestType>)>("Fetch request action")
coEvery { fetchRequestAction() } throws (Exception(""))
networkBoundResource(
query = { flowOf(ResultType) },
fetch = { fetchRequestAction() },
saveFetchResult = { saveFetchResultAction() },
onFetchFailed = { _, _ -> fetchErrorAction() },
shouldFetch = { true },
coroutineDispatcher = dispatchProvider.IO
).test {
// Assert that we've got the cached item
val cacheItem = awaitItem()
assertThat(cacheItem).isInstanceOf(Resource.Success::class.java)
val errorItem = awaitItem()
assertThat(errorItem).isInstanceOf(Resource.Error::class.java)
awaitComplete()
// Verify order & calls
verifyOrder {
fetchRequestAction()
fetchErrorAction()
}
verify(exactly = 1) { fetchErrorAction() }
verify(exactly = 1) { fetchRequestAction() }
verify(exactly = 0) { saveFetchResultAction() }
}
}
I have a situation where I have to execute 3 network requests one after the other collect their results (which are of different types).
Following is the relevant part of the code :
Resource.kt
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Loading<T>(data: T? = null): Resource<T>(data)
class Success<T>(data: T?): Resource<T>(data)
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
}
Repository.kt
override fun getReportData(profileId: Int): Flow<Resource<ProfileReport>> =
flow {
emit(Resource.Loading<ProfileReport>())
var report: ProfileReport? = null
try {
// Api is available as a retrofit implementation
report = api.getReport(profileId).toProfileReport()
} catch (e: HttpException) {
emit(
Resource.Error<ProfileReport>(
message = "An unknown http exception occured"
)
)
}
if (report!= null) {
emit(Resource.Success<ProfileReport>(data = report))
}
}
Say I have 3 such flows to fetch data in my repository and they have different return types (ex: ProfileReport, ProfileInfo, ProfileStatus).
Now in my viewmodel I have a function to execute these flows and perform actions on the values emitted such as :
ViewModel.kt
fun getProfileData(profileId: Int) {
getReportData(profileId)
.onEach { result ->
when (result) {
is Resource.Loading -> {
_loading.value = true
}
is Resource.Error -> {
_loading.value = false
// UI event to display error snackbar
}
is Resource.Success -> {
_loading.value = false
if (result.data != null) {
_report.value = _report.value.copy(
// Use result here
)
}
}
}
}.launchIn(viewModelScope)
}
This works ok for one flow but how can I execute 3 flows one after the other.
That is, execute first one and if its successful, execute second one and so on, and if all of them are successful use the results.
I did it like this :
fun getProfileData(profileId: Int) {
getReportData(profileId)
.onEach { result1 ->
when (result1) {
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
getProfileStatus(profileId)
.onEach { result2 ->
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
getProfileInfo(profileId)
.onEach { result3 ->
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
/*
Finally update viewmodel state
using result1, result2 and result3
*/
}
}.launchIn(viewModelScope)
}
}.launchIn(viewModelScope)
}
}
}.launchIn(viewModelScope)
}
But, this feels too cumbersome and probably there is a better way to chain flows based on success condition and collect results at the end. I checked some ways that use combine() or flatMapMerge() but was unable to use them in this situation.
Is there a way to achieve this? Or is this approach itself wrong from a design perspective maybe?
I think this could be modeled much more cleanly using imperative coroutines than with flows. Since you're overriding functions, this depends on you being able to modify the supertype abstract function signatures.
This solution doesn't use Resource.Loading, so you should remove that to make smart casting easier.
suspend fun getReportData(profileId: Int): Resource<ProfileReport> =
try {
val report = api.getReport(profileId).toProfileReport()
Resource.Success<ProfileReport>(data = report)
} catch (e: HttpException) {
Resource.Error<ProfileReport>(
message = "An unknown http exception occured"
)
}
//.. similar for the other two functions that used to return flows.
fun getProfileData(profileId: Int) {
viewModelScope.launch {
// do stuff to indicate 1st loading state
when(val result = getReportData(profileId)) {
Resource.Error<ProfileReport> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileReport> -> {
// do stuff with result
}
}
// Since we returned when there was error, we know first
// result was successful.
// do stuff to indicate 2nd loading state
when(val result = getProfileStatus(profileId)) {
Resource.Error<ProfileStatus> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileStatus> -> {
// do stuff with result
}
}
// do stuff to indicate 3rd loading state
when(val result = getProfileInfo(profileId)) {
Resource.Error<ProfileInfo> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileInfo> -> {
// do stuff with result
}
}
}
}
If you want to keep your current Flows, you could collect your flows this way to avoid the deep nesting. This works because your source flows are designed to be finite (they aren't repeatedly emitting new values indefinitely, but have only one final result).
fun getProfileData(profileId: Int) = viewModelScope.launch {
var shouldBreak = false
getReportData(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> {
/*do stuff*/
shouldBreak = true
}
is Resource.Success -> { /*do stuff*/ }
}
}
if (shouldBreak) return#launch
getProfileStatus(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> {
/*do stuff*/
shouldBreak = true
}
is Resource.Success -> { /*do stuff*/ }
}
}
if (shouldBreak) return#launch
getProfileInfo(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> { /*do stuff*/ }
is Resource.Success -> { /*do stuff*/ }
}
}
}
I am using the liveData coroutine as follows. My function takes 3 params - accessing database, make a API call and return the API result
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery.invoke().map { Resource.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
emitSource(source)
}
}
I am calling the function as
fun getImages(term: String) = performGetOperation(
databaseQuery = {
localDataSource.getAllImages(term) },
networkCall = {
remoteDataSource.getImages(term) },
saveCallResult = {
val searchedImages = mutableListOf<Images>()
it.query.pages.values.filter {
it.thumbnail != null
}.map {
searchedImages.add(Images(it.pageid, it.thumbnail!!.source, term))
}
localDataSource.insertAll(searchedImages)
}
)
This is my viewmodel class
class ImagesViewModel #Inject constructor(
private val repository: WikiImageRepository
) : ViewModel() {
var images: LiveData<Resource<List<Images>>> = MutableLiveData()
fun fetchImages(search: String) {
images = repository.getImages(search)
}
}
From my fragment I am observing the variable
viewModel.images?.observe(viewLifecycleOwner, Observer {
when (it.status) {
Resource.Status.SUCCESS -> {
println(it)
}
Resource.Status.ERROR ->
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
Resource.Status.LOADING ->
println("loading")
}
})
I have to fetch new data on click of button viewModel.fetchImages(binding.searchEt.text.toString())
Function doesn't gets executed. Is there something I have missed out?
The liveData {} extension function returns an instance of MediatorLiveData
liveData { .. emit(T) } // is a MediatorLiveData which needs a observer to execute
Why is the MediatorLiveData addSource block not executed ?
We need to always observe a MediatorLiveData using a liveData observer else the source block is never executed
So to make the liveData block execute just observe the liveData,
performGetOperation(
databaseQuery = {
localDataSource.getAllImages(term) },
networkCall = {
remoteDataSource.getImages(term) },
saveCallResult = {
localDataSource.insertAll(it)
}
).observe(lifecyleOwner) { // observing the MediatorLiveData is necessary
}
In your case every time you call
images = repository.getImages(search)
a new instance of mediator liveData is created which does not have any observer. The old instance which is observed is ovewritten. You need to observe the new instance of getImages(...) again on button click.
images.observe(lifecycleOwner) { // on button click we observe again.
// your observer code goes here
}
See MediatorLiveData and this
I am accessing the server in my Android app. I want to get a list of my friends and a list of friend requests in different queries. They have to come at the same time. Then I want to show this data on the screen.
I tried to get data from two queries at using flatMap.
interactor.getColleagues() and interactor.getTest() returns the data type Observable<List<Colleagues>>
private fun loadColleaguesEmployer() {
if (disposable?.isDisposed == true) disposable?.dispose()
//запрос на список друзей
interactor.getColleagues(view.getIdUser() ?: preferences.userId)
.subscribeOn(Schedulers.io())
.flatMap {
interactor.getTest().subscribeOn(Schedulers.io())
.doOnNext {
result-> view.showTest(mapper.map(result))
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = { result1 ->
//Обработка списка коллег работодателей
view.showColleagues(mapper.map(result1.filter { data -> data.typeFriend == "Работодатель" }))
},
onError = { it.printStackTrace() }
)
}
I want to get and process data from different queries at the same time.
Combining observable results of multiple async http requests with rxjava's Observable.zip.
public class Statistics {
public static void main(String[] args) {
List<Observable<ObservableHttpResponse>> observableRequests = Arrays.asList(
Http.getAsync("http://localhost:3001/stream"),
Http.getAsync("http://localhost:3002/stream"),
Http.getAsync("http://localhost:3003/stream"),
Http.getAsync("http://localhost:3004/stream"));
List<Observable<Stats>> observableStats = observableRequests.stream()
.map(observableRequest ->
observableRequest.flatMap(response ->
response.getContent()
.map(new EventStreamJsonMapper<>(Stats.class))))
.collect(toList());
Observable<List<Stats>> joinedObservables = Observable.zip(
observableStats.get(0),
observableStats.get(1),
observableStats.get(2),
observableStats.get(3),
Arrays::asList);
// This does not work, as FuncN accepts (Object...) https://github.com/Netflix/RxJava/blob/master/rxjava-core/src/main/java/rx/functions/FuncN.java#L19
// Observable<List<Stats>> joinedObservables = Observable.zip(observableStats, Arrays::asList);
joinedObservables
.take(10)
.subscribe(
(List<Stats> statslist) -> {
System.out.println(statslist);
double average = statslist.stream()
.mapToInt(stats -> stats.ongoingRequests)
.average()
.getAsDouble();
System.out.println("avg: " + average);
},
System.err::println,
Http::shutdown);
}
}
you can do it by simple operation zip like
private fun callRxJava() {
RetrofitBase.getClient(context).create(Services::class.java).getApiName()
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
getObservable()
.flatMap(object : io.reactivex.functions.Function<List<User>, Observable<User>> {
override fun apply(t: List<User>): Observable<User> {
return Observable.fromIterable(t); // returning user one by one from usersList.
} // flatMap - to return users one by one
})
.subscribe(object : Observer<User> {
override fun onSubscribe(d: Disposable) {
showProgressbar()
}
override fun onNext(t: User) {
userList.add(t)
hideProgressBar()
}
override fun onError(e: Throwable) {
Log.e("Error---", e.message)
hideProgressBar()
}
override fun onComplete() {
userAdapter.notifyDataSetChanged()
}
})
}
this function combines your response from 2 queries
private fun getObservable(): Observable<List<User>> {
return Observable.zip(
getCricketFansObservable(),
getFootlaballFansObservable(),
object : BiFunction<List<User>, List<User>, List<User>> {
override fun apply(t1: List<User>, t2: List<User>): List<User> {
val userList = ArrayList<User>()
userList.addAll(t1)
userList.addAll(t2)
return userList
}
})
}
here is example of first observable
fun getCricketFansObservable(): Observable<List<User>> {
return RetrofitBase.getClient(context).create(Services::class.java).getCricketers().subscribeOn(Schedulers.io())
}
If both observables return the same data type and you don't mind mixing of both sources data - consider using Observable.merge()
For example:
Observable.merge(interactor.getColleagues(), interactor.getTest())
.subscribeOn(Schedulers.io())
.subscribe(
(n) -> {/*do on next*/ },
(e) -> { /*do on error*/ });
Note, that .merge() operator doesn't care about emissions order.
Zip combine the emissions of multiple Observables together via a
specified function
You can use Zip (rx Java) http://reactivex.io/documentation/operators/zip.html, some sudo code will be like this -
val firstApiObserver = apIs.hitFirstApiFunction(//api parameters)
val secondApiObserver = apIs.hitSecondApiFunction(//api parameters)
val zip: Single<SubscriptionsZipper>//SubscriptionsZipper is the main model which contains first& second api response model ,
zip = Single.zip(firstApiObserver, secondApiObserver, BiFunction { firstApiResponseModel,secondApiResponseModel -> SubscriptionsZipper(firstApiResponseModelObjectInstance, secondApiResponseModelObjectInstance) })
zip.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(object : SingleObserver<SubscriptionsZipper> {
override fun onSubscribe(d: Disposable) {
compositeDisposable.add(d)
}
override fun onSuccess(subscriptionsZipper: SubscriptionsZipper) {
Utils.hideProgressDialog()
//here you will get both api response together
}
override fun onError(e: Throwable) {
Utils.hideProgressDialog()
}
})
Hope it helps you .
The following code does not compile:
override fun storeConnections(connections: List<Connection>): Observable<List<Connection>> =
Observable.fromCallable<List<Connection>> {
appDao.storeConnections(connections.map {
mapper.toDb(it)})
}
The line with appDao.storeConnections indicates the following error:
Required List!
Found Unit
The storeConnections is done using Room:
#Dao
interface RoomDao {
#Insert(onConflict = REPLACE)
fun storeConnections(linkedInConnection: List<LinkedInConnectionEntity>)
}
The storeConnections is called from my rx stream:
val startPositions = BehaviorSubject.createDefault(0)
startPositions.flatMap { startPos -> App.context.repository.getConnections(startPos) }
.flatMap { connections -> Observable.fromCallable(App.context.repository.storeConnections(connections)) }
.doOnNext { ind -> startPositions.onNext(ind + 1) }
.subscribe({ ind -> println("Index $ind") })
How do I properly implement this fromCallable?
Given your reply to your question:
storeConnections is returning nothing. But I need to wrap it in an observable in order to push it down the stream. So maybe my question is how to wrap an API call with an Observable when that api call returns nothing.
I will answer how you can wrap it in an observable in order to push it down the stream:
.flatMap {
connections ->
App.context.repository.storeConnections(connections)
.andThen(Observable.just(connections))
}
Given that storeConnections returns a Completable:
override fun storeConnections(connections: List<Connection>): Completable =
Completable.fromAction {
appDao.storeConnections(connections.map { mapper.toDb(it) } )
}
}
If storeConnections returns "nothing", you could simply move the Completable.fromAction to your stream:
.flatMap {
connections ->
Completable.fromAction { App.context.repository.storeConnections(connections) }
.andThen(Observable.just(connections))
}
The key to getting it to work is using this:
return#fromCallable connections
So this is the corrected code:
override fun storeConnections(connections: List<Connection>): Observable<List<Connection>> =
Observable.fromCallable<List<Connection>> {
appDao.storeConnections(connections.map {
mapper.toDb(it)
})
return#fromCallable connections
}
And the rx stream that calls it:
val startPositions = BehaviorSubject.createDefault(0)
startPositions.flatMap { startPos -> App.context.repository.getConnections(startPos) }
.flatMap {
connections -> App.context.repository.storeConnections(connections)
}
.doOnNext {
connections -> startPositions.onNext(startPos++)
}
.subscribe({ ind -> println("Index $ind") })