during the last two days I read a lot about the rxJava retryWhen operator.
Here, here, here and some more I forgot.
But unfortunately I'm not able to get it work.
What I'm trying to achive is, that I make an API call. If the call returns an error, I'm showing a SnackBar to the user with a reload button. If the user clicks this button, I would like to resubscribe to the chain.
Here is my code:
interface RetrofitApi {
#GET("/v1/loadMyData")
fun getMyData(): Single<Response<DataResponse>>
}
Where Response is from retrofit2. I need it to wrap the data class to check if response is successful.
The next fun is called in a repository from a ViewModel:
override fun loadMyData(): Observable<Resource<DataResponse>> {
return retrofitApi
.getMyData()
.compose(getRetryTransformer())
.toObservable()
.compose(getResponseTransformer())
}
Resource is another wrapper for the state of the call (SUCCESS, ERROR, LOADING).
And finally the Transformer:
private fun <Data> getRetryTransformer(): SingleTransformer<Response<Data>, Response<Data>> {
return SingleTransformer { singleResponse ->
singleResponse
.onErrorReturn {
singleResponse.blockingGet()
}
.retryWhen { errors ->
errors.zipWith(retrySubject.toFlowable(BackpressureStrategy.LATEST),
BiFunction<Throwable, Boolean, Flowable<Throwable>> { throwable: Throwable, isRetryEnabled: Boolean ->
if (isRetryEnabled) {
Flowable.just(null)
} else {
Flowable.error(throwable)
}
})
}
}
}
The retrySubject:
private val retrySubject = PublishSubject.create<Boolean>()
And when the user clicks the retry button, I call:
retrySubject.onNext(true)
The problem is now, that the error is not returned to the ViewModel and the SnackBar is never shown. I tried onErrorResumeNext() as well with no success. The whole retryWhen/zipWith part seem to work. Because in the repository there some more API calls with no retry behavior (yet) and there the SnackBar is displayed. That means, I do anther call where the SnackBar is shown -> button click and the retry transform works as expected.
If you need some more information please don't hesitate to ask! Any help is appreciated!
Strange, as soon you do it the right way, it works.
I over read somehow that I need in doOnError{...} to manage to show my Snackbar.
Here is my working retry transformer:
private fun <Data> getRetryTransformer(): SingleTransformer<Response<Data>, Response<Data>> {
return SingleTransformer { singleResponse ->
singleResponse
.doOnError {
errorEventSubject.onNext(it)
}
.retryWhen { errors ->
errors.zipWith(retrySubject.toFlowable(BackpressureStrategy.LATEST),
BiFunction<Throwable, Boolean, Flowable<Throwable>> { throwable: Throwable, isRetryEnabled: Boolean ->
if (isRetryEnabled) {
Flowable.just(throwable)
} else {
Flowable.error(throwable)
}
})
}
}
}
And the chain looks now like this (and I think it's beautiful):
override fun loadMyData(): Observable<Resource<DataResponse>> {
return retrofitApi
.getMyData()
.compose(getRetryTransformer())
.toObservable()
.compose(getResponseTransformer())
}
What else I needed to propagate the error to my ViewModel is a 2nd PublishSubject:
private val errorEventSubject = PublishSubject.create<Throwable>()
And in the ViewModel I observe the changes for it and show the Snackbar.
That's it.
Related
I am observing inside a fragment the events of a sharedflow such as this:
myEvent.collectInLifeCycle(viewLifecycleOwner) { event ->
when (state) {
//check the event. The event emited form onStart is never reached here :(
}
}
Whereas in the viewmodel I have
private val _myEvent = MutableSharedFlow<MyEvent>()
val myEvent: SharedFlow<MyEvent> = _myEvent
fun loadData() =
viewModelScope.launch {
getDataUseCase
.safePrepare(onGenericError = { _event.emit(Event.Error(null)) })
.onStart { _event.emit(Event.Loading) }
.onEach { result ->
result.onSuccess { response ->
_event.emit(Event.Something)
}
}
.launchIn(viewModelScope)
}
So the problem is that only the Event.Something is the one being properly collected from the fragment, whereas _event.emit(Event.Loading) is not being collected... If I debug it goes to the onStart, but it is never called in the fragment.
Your SharedFlow needs to have a replay so that collectors always get at least the most recent value. Otherwise, if you emit to the Flow before the collector is registered, it will never see anything emitted. Do this:
private val _myEvent = MutableSharedFlow<MyEvent>(replay = 1)
Personally, unless I'm missing some detail here that would change my mind, I would simplify all your code to avoid having to manually call loadData(). Something like this but I'm guessing a bit because I don't know all your types and functions.
val myEvent: SharedFlow<MyEvent> = flow {
emit(Event.Loading)
emitAll(
getDataUseCase
.transform { result ->
result.onSuccess { response ->
emit(Event.Something)
}
}
.catch { error -> emit(Event.Error(null)) }
)
}.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)
I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
val result = MutableLiveData<ApiResponse<DisastersDTO?>>()
apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if(response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
return result
}
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
remoteDataSource.getDisastersByFilter(filter).map {
when (it) {
is ApiResponse.Empty -> Resource.Error("Terjadi error")
is ApiResponse.Error -> Resource.Error(it.errorMessage)
is ApiResponse.Loading -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(
DataMapper.disastersResponseToDisasterDomain(
it.data
)
)
}
}
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
useCase.getDisastersByFilter(filter)
Here's the **MapsFragment**
private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
when (it) {
is Resource.Success -> {
Log.d("MapsFragmentFilter", "${it.data}")
it.data?.let { listDisaster ->
if(listDisaster.isNotEmpty()) {
map.clear()
addGeofence(listDisaster)
listDisaster.map { disaster ->
placeMarker(disaster)
addCircle(disaster)
}
}
}
}
is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()
is Resource.Loading -> {}
}
}
Here's the MainActivity that triggers the function to hit API
private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
when (menu.itemId) {
R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
R.id.about_us -> viewModel.getDisasters()
}
binding.drawerLayoutMain.closeDrawers()
true
}
I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.
That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.
I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.
If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)
The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.
I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
What I believe you are doing wrong is using LiveData in the first place while using a retrofit.
You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.
And while calling this function from ViewModel, wrap it with viewModelScope.launch{}
fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
useCase.getDisastersByFilter(filter).collect{
// do something....
// assign the values to MutableLiveData or MutableStateFlows
}
}
You should either be using RxJava or CallbackFlow.
I prefer Flows, given below is an example of how your code might look if you use callback flow.
suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
callbackFlow {
apiService.getDisastersByFilter(filter)
.enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if (response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
trySend(ApiResponse.Success(it))
// result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
// result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
trySend(ApiResponse.Error("Terjadi kesalahan!"))
// result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
trySend(ApiResponse.Error(t.localizedMessage!!))
// result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
awaitClose()
}
The following code is from the project.
The function of tasksRepository.refreshTasks() is to insert data from remote server to local DB, it's a time consuming operation.
In class TasksViewModel, asksRepository.refreshTasks() is wrapped with viewModelScope.launch{}, it means launch and careless.
1: How can I guarantee tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) } to return the latest result?
2: I don't know how distinctUntilChanged() work, will it keep listening to return the latest result in whole Lifecycle ?
3: What's happened if I use tasksRepository.observeTasks().switchMap { filterTasks(it) } instead of tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
Code
class TasksViewModel(..) : ViewModel() {
private val _items: LiveData<List<Task>> = _forceUpdate.switchMap { forceUpdate ->
if (forceUpdate) {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTasks()
_dataLoading.value = false
}
}
tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
}
...
}
class DefaultTasksRepository(...) : TasksRepository {
override suspend fun refreshTask(taskId: String) {
updateTaskFromRemoteDataSource(taskId)
}
private suspend fun updateTasksFromRemoteDataSource() {
val remoteTasks = tasksRemoteDataSource.getTasks()
if (remoteTasks is Success) {
tasksLocalDataSource.deleteAllTasks()
remoteTasks.data.forEach { task ->
tasksLocalDataSource.saveTask(task)
}
} else if (remoteTasks is Result.Error) {
throw remoteTasks.exception
}
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return tasksLocalDataSource.observeTasks()
}
}
switchMap - The returned LiveData delegates to the most recent LiveData created by calling switchMapFunction with the most recent value set to source, without changing the reference. Doc
Yes, it'll keep listening to return the latest result in whole Lifecycle. distinctUntilChanged creates a new LiveData object that does not emit a value until the source LiveData value has been changed. The value is considered changed if equals() yields false.
Yes you can use that too but it'll keep emitting the values even the values are the same as the last emitted value.
e.g. first emitted value is ["aman","bansal"] and the second is the same ["aman","bansal"] which you don't want to emit since the values are same. So you use distinctUntilChanged to make sure it won't emit the same value until changed.
I hope this helped.
I'm learning kotlin in intelij Idea, and I have to make presentation about interfaces. One subject is callback, where can I find information about it? or can you tell me simply, veery simply, what's call back?
fun main() {
val myphone = Myphone()
myphone.phoneOn()
myphone.onClick()
myphone.onTouch()
myphone.openApp()
myphone.closeApp()
}
interface Application {
var AppName: String
fun openApp()
fun closeApp() {
println("$AppName App is closed!")
}
}
interface Button {
var helloMessage: String
fun phoneOn()
fun onClick()
fun onTouch() {
println("The screen was touched!")
}
}
class Myphone: Button, Application {
override var AppName: String = "Facebook"
override fun openApp() {
println("$AppName Is Open!")
}
override var helloMessage: String = "Hello"
override fun onClick() {
println("The screen was clicked!")
}
override fun phoneOn() {
println("$helloMessage !")
}
}
VERY simply: callback means the function, that is executed on the other function's finish or some specific event happening.
fun execute() {
// Some logic
executeAnotherOnFinish();
}
OR
// filter executes only after array converted to list
myIntArray.toList().filter { it > 0 }
OR
myListener.notify()
// Listener class methid
notify() {
// Do some work
executeCallback()
}
Callback is not just Kotlin related, its very common programming technique which is primarily used with asynchronous programming. The simplest explanation is that it is function that will be called back (hence the name) once some asynchronous event has occurred.
Button's onClick function is quite good example of that, we have some logic that we need to execute but we want it to run only when button is clicked so we provide callback which will be called once that button is clicked.
I am trying out kotlin in a home project with Spring webflux and project reactor. I am trying to do a blocking call to the H2 database and I am therefore using the fromCallable method as recommended. To my understanding and experience, fromCallable is supposed to wrap any encountered exception which can then be handled using doOnError, but instead, the error is displayed directly in the console.
fun updateUser(req: ServerRequest): Mono<ServerResponse> =
req.bodyToMono(UserDto::class.java)
.flatMap { userDto -> updateUser(userDto) }
.flatMap { user -> ServerResponse.ok().syncBody(user!!) }
.doOnError { ServerResponse.notFound().build() }
fun updateUser(userDto: UserDto): Mono<User?> =
Mono.fromCallable {
val id = userDto.id.toLong()
userRepository.findByIdOrNull(id) ?:
throw IllegalArgumentException("No user found")
}.subscribeOn(Schedulers.elastic())
If I ask for an Id that does not exist in my database, I would expect a 404 back. Instead, I get a 500 back from the request and the IllegalArgumentException straight into my console in the IDE. If anyone can tell me why this is, or have any info about this, it would be greatly appreciated!
doOnError adds behavior if a mono terminates with an error. In other words, it adds a side effect but doesn't change the stream. Replace doOnError with onErrorResume. onErrorResume it exactly what you need, it subscribes to a fallback publisher if any error occurs.
fun updateUser(req: ServerRequest): Mono<ServerResponse> =
req.bodyToMono(UserDto::class.java)
.flatMap { userDto -> updateUser(userDto) }
.flatMap { user -> ServerResponse.ok().syncBody(user!!) }
.onErrorResume { ServerResponse.notFound().build() } // fallback publisher
.doOnError { println("Failed to perform an update: $it") } // side effect