I am trying to use RxJava with FirebaseRemoteConfig but not sure how to make the two work, tried using Completable but I get an error The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with.
I have this problem where I need to fetch from RemoteConfig then initialize the String variable from MySingleton.class with this latest config to be use later. The said String variable must not be null or empty so the flow would be.
During Splash
Call fetchAndActivate
Listen for both OnSuccess and OnFailure
If success initialize the static String variable with the latest config
If failed try to use old/cached configs
Cache might not exist yet specially on first run so check if the static String variable is empty
If empty show AlertDialog for retry.
If not proceed to main activity.
What I am trying to do is to use RxJava and listen for OnSuccess or OnFailure listeners, which I can probably apply as well when getting just a single document when using Firebase Firestore in the future.
How can I do this?
So far this is what I got
class RemoteConfig {
companion object {
private val remoteConfig: FirebaseRemoteConfig by lazy {
FirebaseRemoteConfig.getInstance()
}
private val remoteConfigSettings: FirebaseRemoteConfigSettings by lazy {
FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(1800)
.build()
}
fun init(context: Context): Completable {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Completable.create {
fetchConfig(context)
}
}
private fun fetchConfig(context: Context): Completable {
return Completable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
emitter.onComplete()
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
emitter.onError(it.cause!!)
FirebaseCrashlytics.getInstance().recordException(it)
}
}
}
private fun assignSource(context: Context) {
Singleton.staticVariable=
remoteConfig.getString(context.getString(R.string.key))
}
}
}
Splash activity
Completable.mergeArray(
RemoteConfig.init(this).subscribeOn(Schedulers.io()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(5, TimeUnit.SECONDS)
.subscribe({
proceedToMain()
}, {
if (Singleton.staticVariable.isEmpty())
AlertDialog.Builder(this)
.setMessage(it.message)
.setPositiveButton("Retry"
) { dialog, _ ->
run {
dialog.dismiss()
fetchConfig()
}
}
.setNegativeButton("Exit"
) { dialog, _ ->
dialog.dismiss()
finish()
}
.setCancelable(false)
.show()
else
proceedToMain()
})
Manage to make it work it seems all I need is to use tryOnError since it will automatically handle the case when the emitter get disposed/canceled or no longer available. Instead of Completable using just Observable is okay too.
fun init(context: Context): Observable<Boolean> {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Observable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
Log.wtf("CONFIG", "SUCCESS")
emitter.onNext(it)
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
Log.wtf("CONFIG", "FAILED")
emitter.tryOnError(it.cause!!) // Try to throw an error if emitter still available or if the sequence is not cancelled/disposed
FirebaseCrashlytics.getInstance().recordException(it)
}.addOnCompleteListener {
emitter.onComplete()
Log.wtf("CONFIG", "COMPLETE")
}
}
}
Splash
cryptonHadItsChance = RemoteConfig.init(this)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
proceedToMain()
}, {
//Only show dialog if necessary field is not available and activity still running
if (Singleton.staticVariable.isEmpty() && !isFinishing && !isDestroyed)
alertDialog.show()
else
proceedToMain()
})
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 have created the following extension function :
fun <T> Flow<T>.handleErrors(showError: Boolean = false, retry: Boolean = false,
navigateBack: Boolean = true): Flow<T> =
catch { throwable ->
var message: String? = null
if (showError) {
when (throwable) {
is HttpException -> {
postEvent(EventType(retry))
}
}
}
The extension function then posts the throwable type to a Base Activity and based on the event type posted a relevant dialog is displayed.
If the event is a retry, I would like to retry the failed flow.
For example if the HTTP exception is a 400, and I would like to retry the failed call when retry is selected on the dialog.
Is it possible to add callback to a Kotlin Flow, that has failed and can be called, from a different activity or fragment?
I don't think you want to retry in a separate block, you can organize your code like this
fun presentDialog(onClick: (Boolean) -> Unit) {
// some code to compile you dialog / install callbacks / show it
onClick(true) // user-click-retry
}
suspend fun main() {
val source = flow {
while (true) {
emit(
if (Math.random() > 0.5) Result.success(100) else Result.failure(IllegalArgumentException("woo"))
)
}
}
source.collect { result ->
suspendCancellableCoroutine<Unit> { cont ->
result.onSuccess {
println("we are done")
}.onFailure {
presentDialog { choice ->
if (choice) {
cont.resumeWith(Result.success(Unit))
} else {
println("we are done")
}
}
}
}
}
}
now some explanations
as you already know, flow is cold,if you don't collect it, it will never produce, as a result, if your collect block does not return, the remaining thunk in flow builder after emit will not get executed,
you suspend the execution of flow builder by calling suspendCoroutine in collect block, if an HTTP error occurs, you show your dialog, and resume the execution according to user response, if no error happens or users just don't click retry, leave everything alone. the sample code above is somehow misleading, for a general case, you don't supply a retry mechanism when everything goes fine, so the while true block could change to
flow {
do {
val response = response()
emit(response)
} while(response.isFailure)
}.collect {
it.onSuccess { println("of-coz-we-are-done") }.onFailure {
// suspend execution and show dialog
// resume execution when user click retry
}
}
which may comfort you that the flow has an end, but actually it is basically the same
With RxJava we can do something like this:
BaseViewModel
protected void subscribe(Completable completable, MutableLiveData<Response> response) {
mDisposable.add(
completable.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnSubscribe(disposable -> response.setValue(Response.loading()))
.doFinally(() -> response.setValue(Response.idle()))
.subscribe(
() -> response.setValue(Response.success(true)),
e -> response.setValue(Response.error(e))
)
);
}
protected <T> void subscribe(Single<T> single, MutableLiveData<Response> response) {
mDisposable.add(
single.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnSubscribe(disposable -> response.setValue(Response.loading()))
.doFinally(() -> response.setValue(Response.idle()))
.subscribe(
result -> response.setValue(Response.success(result)),
e -> response.setValue(Response.error(e))
)
);
}
Then, from repository we getting Single/Complete and pass it to our custom subscribe(), then we get generic Result with data(optional), very easy way to work with asynchronous requests.
How we can abstract coroutines with similar structure, instead of write Launch in every method in ViewModel and try/catch error manually?
Instead of closely following the code you already have with minimal adaptations, I suggest you review your design altogether when migrating to coroutines.
One important principle embedded into coroutines is structured concurrency. This isn't just about the coroutine scopes and cancellation, it is also about the use of futures by any name (be it CompletionStage, Deferred, Task, Single or any other). According to structured concurrency, a future is basically equivalent to a live thread that has no defined scope. You should avoid them.
Instead you should have clearly delineated places in the code that launch new concurrent work contained within a single top-level block of code provided at the launch site.
So far, that implies that you do have a launch block at each entry point into your code from the Android framework, and that's a lot of places due to the nature of the callback-oriented programming model.
However, everything within that block should be coded according to structured concurrency. If you have just one network call to make, your code is entirely sequential: make the call, get the response, process it. The network calls themselves become suspend functions that complete with the result of the call and do not accept callbacks. All the traditional design patterns from the world of blocking calls apply here.
See here for an intro to using coroutines with LiveData, it may help you map your design to the coroutine-oriented one:
https://developer.android.com/topic/libraries/architecture/coroutines#livedata
You are probably looking for something like this
CoroutineWrapper
fun <T> ViewModel.apiCx(context: CoroutineContext = Dispatchers.Default, init: suspend CxWrapper<T>.() -> Unit) {
val wrap = CxWrapper<T>(context)
wrap.launch {
try {
init.invoke(wrap)
callCx(wrap)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun <T> callCx(wrap: CxWrapper<T>) {
val response: Response<T>? = wrap.request
response?.let {
if (it.isSuccessful) {
wrap.success(it.body())
} else {
wrap.fail(Pair(it.code(), it.message()))
}
}
}
class CxWrapper<T>(override val coroutineContext: CoroutineContext) : CoroutineScope {
var request: Response<T>? = null
internal var success: (T?) -> Unit = {}
internal var fail: (Pair<Int, String?>) -> Unit = {}
fun success(onSuccess: (T?) -> Unit) {
success = onSuccess
}
fun error(onError: (Pair<Int, String?>) -> Unit) {
fail = onError
}
}
you can have this as a separate helper class and to use this from your ViewModel
apiCx<YourModelClass> {
request = yourApiCall()
success { yourModelClass ->
Log.d(TAG, "success")
}
error {
Log.e(TAG, "error")
}
}
You would just do the same, just adapted to coroutines. Just replace the different stream types with the suspension methods you need.
protected inline fun <T> MutableLiveData<Response>.subscribe(single: suspend () -> T) {
viewModelScope.launch {
try {
value = Response.loading()
value = withContext(Dispatchers.IO) {
Response.success(single())
}
} catch(e: Throwable) {
value = Response.error(e)
} finally {
value = Response.idle()
}
}
To use it just call with the livedata as receiver
responseLiveData.subscribe<T> {
singleFromRepo()
}
responseLiveData.subscribe<Unit> {
completableFromRepo()
}
Problem
The issue with reactive programming patterns for one-time events is that they may be re-emitted to the subscriber after the initial one-time event has occurred.
For LiveData the SingleLiveEvent provides a solution using an EventObserver which may also be applied to Kotlin Flow.
Question
Can an AsyncSubject observable be created to handle the case of the SingleLiveEvent in RxJava? The main issue seems to be if there a way for an AsyncSubject to be manually "re-opened" to re-emit data after onComplete is called?
Potential solution
AsyncSubject seems like a potential solution for RxJava, without creating an EventObserver, as the documentation states that it will only publish it when the sequence is completed.
Implementation - Loading status sample
A loading boolean is emitted from the ViewModel method initFeed and view effect state to the view, a fragment in this case. The loading boolean works as expected on the initialization of the fragment and ViewModel sending true via onNext, and completing with onComplete on either a successful or erroneous attempt.
However, the attempt to re-emit a value fails when for example a swipe to refresh initiates the same initFeed method. It seems that onNext cannot be used after onComplete is called for the same object.
SomeViewEffect.kt
data class _FeedViewEffect(
val _isLoading: AsyncSubject<Boolean> = AsyncSubject.create(),
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: AsyncSubject<Boolean> = _viewEffect._isLoading
}
SomeViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> {
Log.v(LOG_TAG, "initFeed ${LOADING.name}")
_viewEffect._isLoading.onNext(true)
}
SUCCESS -> {
Log.v(LOG_TAG, "initFeed ${SUCCESS.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewState._feed.onNext(results.data)
}
ERROR -> {
Log.v(LOG_TAG, "initFeed ${ERROR.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewEffect._isError.onNext(true)
}
}
}
disposables.add(disposable)
}
SomeFragment.kt
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading) progressBar.visibility = VISIBLE
else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable, isErrorDisposable)
}
It is not very clear why you need AsyncSubject which emits only last event. Did you try to use Behavior or Publish Processor for this situation?
Use an Event Wrapper
An AsyncSubject does not seem to be a suitable solution to handle one-time occurrence emissions from an Observable to a Subscriber. After onComplete is called an AsyncSubject can not "re-open" to emit future one-time events.
Using an event wrapper such as an Event, as outlined in LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) is the best approach.
FeedViewEffect.kt
data class _FeedViewEffect(
val _isLoading: BehaviorSubject<Event<Boolean>> = BehaviorSubject.create()
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: BehaviorSubject<Event<Boolean>> = _viewEffect._isLoading
}
FeedViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> _viewEffect._isLoading.onNext(Event(true))
SUCCESS -> _viewEffect._isLoading.onNext(Event(false))
ERROR -> _viewEffect._isLoading.onNext(Event(false))
}
}
disposables.add(disposable)
}
FeedFragment.kt
#ExperimentalCoroutinesApi
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading.getContentIfNotHandled() == true) {
progressBar.visibility = VISIBLE
} else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable)
}
I am using Fuel and Rxjava to make network calls. I have set my base URL to localhost, which at isn't serving anything. I want to be able to handle network errors so I can show some sort of error message on the UI to the user.
Here is an example of my GET request
fun getRandom(take: Int, responseHandler: (result: WikiResult) -> Unit?) {
Urls.getRandomURl(take)
.httpGet()
.timeout(timeout)
.timeoutRead(readTimeout)
.rx_object(WikipediaDataDeserializer())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
val statusCode = result.component2()?.response?.statusCode
when(statusCode) {
-1 -> e(statusCode.toString(), result.component2()?.cause.toString())
else -> {
val (data, _) = result
responseHandler.invoke(data as WikiResult)
}
}
}, {
error -> e(error.cause.toString())
})
}
And on my fragment I am calling the above function in a async task
private fun getRandomArticles() {
refresher?.isRefreshing = true
wikiManager?.getRandom(15, { wikiResult ->
adapter.currentResults.clear()
adapter.currentResults.addAll(wikiResult.query!!.pages)
onUiThread {
adapter.notifyDataSetChanged()
refresher?.isRefreshing = false
}
})
}
private fun reportException(e: Throwable) {
refresher?.isRefreshing = false
val builder = AlertDialog.Builder(activity)
builder.setMessage(e.message).setTitle("Error")
val dialog = builder.create()
dialog.show()
}
So I get a network error java.net.ConnectException: Failed to connect to localhost/127.0.0.1:80
I want to be able to get this on my fragment and display an error on the fragment. Not sure what the best approach is for this.
You can see the full project code on here
https://github.com/limpep/android-kotlin-wikipedia
under branch feature/rxjava
any help would be much appreciated
It is hard to say how to write good code for this in your structure because your code is not very clear in its separation and it is not necessary to use AsyncTask and runOnUIThread when you are already using .subscribeOn() and .observeOn() on your observable.
Maybe this would be a better basis for structure:
fun getRandom(take: Int): Single<WikiResult> {
return Urls.getRandomURl(take)
.httpGet()
.timeout(timeout)
.timeoutRead(readTimeout)
.rx_object(WikipediaDataDeserializer())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map{ if(it.component2()!=null) throw it.component2() else it.component1() as WikiResult }
}
private fun getRandomArticles() {
refresher?.isRefreshing = true
getRandom().doOnCompleted{refresher?.isRefreshing = false}
.subscribe(this::handleResponse,this::reportException)
}
private fun handleResponse(wikiResult:WikiResult){
adapter.currentResults.clear()
adapter.currentResults.addAll(wikiResult.query!!.pages)
adapter.notifyDataSetChanged()
}