I've been trying to figure this out still new to RXJava and still not liking it, rather use Livedata and coroutines but anyway.. I have this:
Single.just(entity)
.map {
insertDb(it)
return#map it
}
.doOnSubscribe { Timber.d("Updating in database") }
.doOnSuccess { Timber.d("Added row in database") }
.doOnError { Timber.e(it, "Unable to insert object in db") }
fun insertDb(entity: T) {
try {
// basic update or insert database.begingTransaction() and endTransaction()
} catch (e: SQLiteDatabaseLockedException) {}
}
So when the error of DB locked occurs I want to catch it and retry the insert using the Single with a retryWhen(). The examples I read are very convoluted and not quite what I want. And don't worry about the locked DB stuff this is just an example of how I would catch DB errors.
You can use a PublishProcessor:
val retryProcessor = PublishProcessor.create<Unit>()
Single.just("Entity")
.map { insertDB(it) }
.doOnError { Log.e(TAG, "Error") }
.retryWhen { retryProcessor }
.subscribe { entity -> Log.i(TAG, "Success: $entity") }
Insert to DB:
fun insertDB(entity: String): String {
// Insert to DB
return entity
}
Whenever you want to perform a retry, call onNext on the PublishProcessor:
retryProcessor.onNext(Unit)
If you just want to retry when a specific exception occurs in the stream, the retry(N) operator would probably be better here. (Where N is the max number of times you want to retry the subscription).
Single.just(entity)
.map {
insertDb(it)
return#map it
}
.retry(1) { e -> e is SQLiteDatabaseLockedException }
You also don't want to catch the exception in your insertDb() function, let it fall into the error stream so you can catch it inside your retry function.
You might also might want to consider returning a Single or Completable from insertDb(), then you call it like insertDb(entity).retry(1)... without using that awkward map that returns itself.
Your insertDb() function would become something like this:
fun <T> insertDb(entity: T): Completable {
return Completable.fromCallable {
// Insert operation
}
}
Related
I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}
I'm trying to implement One Tap, so I have created a function that looks like this:
override suspend fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
emit(Result.Error(e.message!!))
}
}
If I use flow and try to emit the result, my app crashed with the following message:
Flow exception transparency is violated:
StandaloneCoroutine has completed normally; but then emission attempt of value 'Error(message=StandaloneCoroutine has completed normally)' has been detected.
However, if change the code to:
override suspend fun oneTapSgnInWithGoogle() = channelFlow {
try {
send(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
send(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
send(Result.Error(e.message!!))
}
}
And I use channelFlow and try to send the result, the app isn't crashing but I still get the error message saying:
StandaloneCoroutine has completed normally
How can I emit the result correctly and get rid of this error message?
P.S. In my ViewModel class I use:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
viewModelScope.launch {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
}
This is not a good practice to launch a coroutine in liveData block. liveData block is a suspend lambda, you can collect values directly in it without launching a coroutine:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
In your case liveData block has already finished execution (and corresponding coroutine, in which liveData block is executed) when you try to emit a value to LiveData. The solution above should solve the problem.
Usually I'm returning from my dao suspend function:
#Dao
interface DataDao {
#Query("SELECT * FROM data")
fun getAllData(): List<Data>
}
And handle the call within the repository:
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val cacheResult = safeDatabaseCall(dispatcher = Dispatchers.IO) { dataDao.getAllData() }
//handle cacheResult, convert to DataState, emit DataState values
}.flowOn(Dispatchers.IO)
}
With generic fun:
suspend fun <T> safeDatabaseCall(
dispatcher: CoroutineDispatcher,
cacheCall: suspend () -> T?
): CacheResult<T?> {
return withContext(dispatcher) {
try {
withTimeout(10000L) {
CacheResult.Success(cacheCall.invoke())
}
} catch (t: Throwable) {
when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
}
}
}
The problem is that I want return fun getAllData(): Flow<List<Data>> instead of fun getAllData(): List<Data> In order to get immediate updates, But if I'm returning Flow from the Dao, I can't handle the call with safe call and catch errors.
I thought about collecting the data, but if i'm collecting the data the call already done without error handling
Basically I need the cache result return CacheResult<Data> and not CacheResult<Flow<Data>>
How can I solve the problem And make a generic safeDatabaseCall while returning Flow from Dao?
So if I understand correctly you just want to handle the query and return of information safely in a flow. My only question is around the types. I can sorta assume Data DataState and CacheResult are not the same types so I use a "magic" function that converts the intermediary values to the correct one. You will need to adjust accordingly
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val result = safeDatabaseCall(dispatcher = Dispatchers.IO) {
dataDao.getAllData()
}
// Emit the result
emit(result)
}.catch { t : Throwable ->
// Do our transformation like before
val result = when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
// And because catch is actually extending a FlowCollector
// We can emit the result in the stream
emit(result)
}.map { cacheResult ->
convertToDataOrDataState(cacheResult)
}
You shouldn't need flowOn with a dispatcher here since the work inside this flow doesn't require thread dispatching
to Dispatcher.IO. The code we are putting in our flow, is purely exception handling and invoking a function. The only place that seems to require any manual dispatch changing is, safeDatabaseCall(). I am not familiar with this function but if it does exist and takes a dispatcher for the result of actualing making the db calls on an IO thread, then all should be good without flowOn. Otherwise you will be switching dispatchers from original dispatcher -> IO and then to IO again. It's not much but the extra no-op context switch doesn't add anything other than confusion later on.
The flow itself traps any upstream issues and you then make them part of the resulting flow
I'm having a weird problem with my repository implementation. Every time I call my function that's supposed to get data from the database and update the database with a network call, I receive multiple results from my database observer.
override fun getApplianceControls(
serialNumber: SerialNumber
): Flowable<ApplianceControlState> {
val subject = BehaviorProcessor.create<ApplianceControlState>()
controlsDao.get(serialNumber.serial)
.map { controls ->
ApplianceControlState.Loaded(controls.toDomainModel())
}
.subscribe(subject)
controlApi.getApplianceControls(serialNumber.serial)
.flatMapObservable<ApplianceControlState> { response ->
val entities = response.toEntity(serialNumber)
// Store the fetched controls on the database.
controlsDao.insert(entities).andThen(
// Return an empty observable because the db will take care of emitting latest values.
Observable.create { }
)
}
.onErrorResumeNext { error: Throwable ->
Observable.create { emitter -> emitter.onNext(ApplianceControlState.Error(error)) }
}
.subscribeOn(backgroundScheduler)
.subscribe()
return subject.distinctUntilChanged()
}
#Dao
interface ApplianceControlsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(controls: List<TemperatureControlEntity>): Completable
#Query("SELECT * FROM control_temperature WHERE serial = :serial")
fun get(serial: String): Flowable<List<TemperatureControlEntity>>
}
Basically, if I call getApplianceControls once, I get desired result. Then I call again, with another serial number, which is empty and I get the empty array. But then I call a third time, but with the same serial number as the first time and I get a mix of correct results and empty array after the insert call is made.
Like this:
1st call, to serial number "123" -> Loaded([control1, control2, control3])
2nd call, to serial number "000" -> Loaded([])
3rd call, to serial number "123" -> Loaded([control1, control2, control3]), Loaded([]), Loaded([control1, control2, control3])
If I remove the db insert from the api response, it works fine. Everything weird occurs after insert is called.
Edit: getApplianceControls() is called from the ViewModel.
fun loadApplianceControls(serialNumber: SerialNumber) {
Log.i("Loading appliance controls")
applianceControlRepository.getApplianceControls(serialNumber)
.subscribeOn(backgroundScheduler)
.observeOn(mainScheduler)
.subscribeBy(
onError = { error ->
Log.e("Error $error")
},
onNext = { controlState ->
_controlsLiveData.value = controlState
}
).addTo(disposeBag)
}
As i mention in comment you have 2 subscriptions that are not unsubscribed anywhere, it could cause memory leak (it doesn't dispose when subject is disposed), also with such implementation you ignore API errors.
i'd try to change it to:
override fun getApplianceControls(serialNumber: SerialNumber): Flowable<ApplianceControlState> {
val dbObservable = controlsDao.get(serialNumber.serial)
.map { controls ->
ApplianceControlState.Loaded(controls.toDomainModel())
}
val apiObservable = controlApi.getApplianceControls(serialNumber.serial)
.map { response ->
val entities = response.toEntity(serialNumber)
// Store the fetched controls on the database.
controlsDao.insert(entities).andThen( Unit )
}
.toObservable()
.startWith(Unit)
return Observables.combineLatest(dbObservable, apiObservable) { dbData, _ -> dbData }
// apiObservable emits are ignored, but it will by subscribed with dbObservable and Errors are not ignored
.onErrorResumeNext { error: Throwable ->
Observable.create { emitter -> emitter.onNext(ApplianceControlState.Error(error)) }
}
.subscribeOn(backgroundScheduler)
//observeOn main Thread
.distinctUntilChanged()
}
I'm not sure if it solves the original issue. But if so - the issue is in flatMapObservable
ALSO would be useful to see controlApi.getApplianceControls() implementation.
Trying to test new Android Room librarty with RxJava adapter. And I want to handle result if my query returns 0 objects from DB:
So here is DAO method:
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Flowable<AuthResponse>
And how I handle it:
database.authDao()
.getAuthInfo()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.switchIfEmpty { Log.d(TAG, "IS EMPTY") }
.firstOrError()
.subscribe(
{ authResponse -> Log.d(TAG, authResponse.token) },
{ error -> Log.d(TAG, error.message) })
My DB is empty, so I expect .switchIfEmty() to work, but none of handling methods is firing. Neither .subscribe() nor .switchIfEmpty()
Db Flowables are observable (so they keep dispatching if database changes) so it never completes. You can try returning List<AuthResponse>. We've considered back porting an optional but decided not to do it, at least for now. Instead, we'll probably add support for Optional in different known libraries.
In version 1.0.0-alpha5, room added support of Maybe and Single to DAOs, so now you can write something like
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Maybe<AuthResponse>
You can read more about it here
switchIfEmpty takes as parameter a Publisher<AuthResponse>. Through SAM-conversion your given anonymous function is turned into this class. However it does not follow the behavior expected from a Publisher so it will not work as expected.
Replace it with a correct implementation like Flowable.empty().doOnSubscribe { Log.d(TAG, "IS EMPTY") } and it should work.
You could use some wrapper for result. For example:
public Single<QueryResult<Transaction>> getTransaction(long id) {
return createSingle(() -> database.getTransactionDao().getTransaction(id))
.map(QueryResult::new);
}
public class QueryResult<D> {
public D data;
public QueryResult() {}
public QueryResult(D data) {
this.data = data;
}
public boolean isEmpty(){
return data != null;
}
}
protected <T> Single<T> createSingle(final Callable<T> func) {
return Single.create(emitter -> {
try {
T result = func.call();
emitter.onSuccess(result);
} catch (Exception ex) {
Log.e("TAG", "Error of operation with db");
}
});
}
And use it like 'Single' in this case you will get result in any case. Use:
dbStorage.getTransaction(selectedCoin.getId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(r -> {
if(!r.isEmpty()){
// we have some data from DB
} else {
}
})