I've inherited this codebase which uses RxJava2 and kotlin with a rather peculiar Result pattern for API calls. i.e. all API calls return Singles with a Result object (which is a sealed class of Success and Error types as shown below) i.e.
sealed class Result<T, E> {
data class Success<T, E>(
val data: T
): Result<T, E>()
data class Error<T, E>(
val error: E
): Result<T, E>()
}
Now I'm trying to chain together a bunch of API calls of these but need to terminate the chain on the first Result.Error in it and continue if not.
The only way I can think of is to zip all of the Singles and then have a zipper function that checks the type of each parameter and returns a Result.Error() with the first error it encounters. i.e. something like,
Singles.zip(
repo1.makeCall1(arg),
repo1.makeCall2(arg2),
repo2.makeCall1(arg3)
) { result1, result2, result3 ->
val data1 = when (result1) {
is Result.Error -> return#zip Result.Error(result1.error)
is Result.Success -> result1.data
}
val data2 = when (result2) {
is Result.Error -> return#zip Result.Error(result2.error)
is Result.Success -> result2.data
}
val data3 = when (result3) {
is Result.Error -> return#zip Result.Error(result3.error)
is Result.Success -> result3.data
}
return#zip Result.Success(MergedData(data1, data2, data3))
}
which works but looks really weird (and feels like a code smell with this huge ass zipper method). Also does not allow me to chain anything more after the last method (that checks if the Result is a Success / Error).
I feel it would be a lot more readable to be able to chain these calls and terminate on the first error but I don't know enough Rx to do this. Is there an operator or an approach that could help make this better?
You can get original Single behavior by reversing what your codebase already does.
Create transformer which will extract data from api call or throw error on error. First error will terminate zip.
public <T, E extends Throwable> SingleTransformer<Result<T, E>, T> transform() {
return source -> source.flatMap(result -> {
if (result instanceof Result.Success)
return Single.just(((Success<T, E>) result).getData());
else
return Single.error(((Error<T, E>) result).getError());
});
}
Use it with repo.makeCall(arg).compose(transform())
Hope it helps.
Out of the box, RxJava would "abort on the first error" because Observable and Single (which is akin to Task/Future/Promise) has "monadic qualities". But as Result<*, *> explicitly makes errors be handled on the "success" path to avoid aborting the stream, we might want to consider a different route than letting Rx go to terminal events - because the existing code expects it to be on the success path. Terminal events should be for "the world is ending" exceptions, not ones we actually expect and can handle.
I had some ideas but I think the only thing you can do is reduce the number of lines it takes to do this instead of flat-out removing it.
Technically we are trying to re-implement the Either<E, T> monad here from Arrow, but knowing that, we can reduce the number of lines with some tricks:
sealed class Result<T, E>(
open val error: E? = null,
open val data: T? = null
) {
data class Success<T>(
override val data: T
): Result<T, Nothing?>()
data class Error<E>(
override val error: E
): Result<Nothing?, E>()
}
fun <E> E.wrapWithError(): Result.Error<E> = Result.Error(this) // similar to `Either.asLeft()`
fun <T> T.wrapWithSuccess(): Result.Success<T> = Result.Success(this) // similar to `Either.asRight()`
fun blah() {
Singles.zip(
repo1.makeCall1(arg),
repo1.makeCall2(arg2),
repo2.makeCall1(arg3)
) { result1, result2, result3 ->
val data1 = result1.data ?: return#zip result1.error.wrapWithError()
val data2 = result2.data ?: return#zip result2.error.wrapWithError()
val data3 = result3.data ?: return#zip result3.error.wrapWithError()
Result.Success(MergedData(data1, data2, data3))
}
}
What do you think about this code block:
Single.zip(
Single.just(Result.Error(error = 9)),
Single.just(Result.Success(data = 10)),
Single.just(Result.Success(data = 11)),
Function3<Result<Int, Int>, Result<Int, Int>, Result<Int, Int>, List<Result<Int, Int>>> { t1, t2, t3 ->
mutableListOf(t1, t2, t3)
})
.map { list ->
list.forEach {
if (it is Result.Error){
return#map it
}
}
return#map Result
} // or do more chain here.
.subscribe()
I combine the results into a list, then map it to your expected result. It's much easier to read.
Related
For start I must say I am begginer in RxJava.
Data class:
#Entity(tableName = "google_book")
data class GoogleBook (
#PrimaryKey(autoGenerate = true) val id: Int=0,
val items: ArrayList<VolumeInfo>)
data class VolumeInfo(val volumeInfo: BookInfo){
data class BookInfo(val title: String, val publisher: String, val description: String, val imageLinks: ImageLinks?)
data class ImageLinks(val smallThumbnail: String?)
}
Function which helps me save data to database:
fun searchBooks(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
bookRepository.getBooksFromApi(query)
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { x ->
x?.let { googleBook ->
searchJob?.cancel()
searchJob = viewModelScope.launch {
bookRepository.deleteGoogleBook()
bookRepository.insertGoogleBook(googleBook)
}
} ?: kotlin.run {
Log.d(TAG, "observeTasks: Error")
}
}
}
}
}
As seen I want to filter list within GoogleBook object by image parameter but It doesnt work. I cannot add filtering for data class ImageLinks so I have no Idea how can I make it right
I am asking mostly about this part:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
Thanks for reading
welcome to RxJava, you gonna love it.
As far as I can tell the issue with your filtering simply relies here:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null })
} // this returns you a new list filtered list here, but does not modify the original one
t // but you return the same data object here, it is not modified at all
}
// also consider naming it bookInfo if it is actually a bookInfo
What you should do is make a copy of your object with the filtered elements, something like this:
fun filterGoogleBookBySmallThumbNail(googleBook: GoogleBook): GoogleBook {
val filteredItems = googleBook.items.filter { it.volumeInfo.imageLinks?.smallThumbnail == null }
return googleBook.copy(items = ArrayList(filteredItems)) // now a new googleBook item is created with the filtered elements
}
// snippet to adjust then
bookRepository.getBooksFromApi(query)
.map { googleBook -> filterGoogleBookBySmallThumbNail(googleBook) }
//...
Some additional notes / suggestions I have:
I don't see you actually disposing of the subscription of the Observable.
bookRepository.getBooksFromApi(query) If this line returns an Observable, even if you cancel the job, you will be still observing that Observable. If it returns a Single then you are in luck, because after one element it is disposed.
To properly dispose, in cancellation you would have to do something like this(still i would recommend the other two rather, just wanted to note the not disposing):
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
val disposable = bookRepository.getBooksFromApi(query)
//...
.subscribe { x ->
//...
}
try {
awaitCancellation() // this actually suspends the coroutine until it is cancelled
} catch (cancellableException: CancellationException) {
disposable.dispose() // this disposes the observable subscription
// that way the coroutine stays alive as long as it's not cancelled, and at that point it actually cleans up the Rx Subscription
}
Seems wasteful that you start a new coroutine job just to do actions
If you want to go the Rx way, you could make the
bookRepository.deleteGoogleBook() and bookRepository.insertGoogleBook(googleBook) Completable, and setup the observable as:
bookRepository.getBooksFromApi(query)
//..
.flatMap {
bookRepository.deleteGoogleBook().andThen(bookRepository.insertGoogleBook(it)).andThen(Observable.just(it))
}
//..subscribeOn
.subscribe()
Seems weird you are mixing coroutine and RX this way
if you don't want to go full Rx, you may consider converting your Observable into a kotlin coroutine Flow, that would be easier to handle with coroutine cancellations and calling suspend functions.
I hope it's helpful
I am working on an android Application and I opted to use Kotlin Result class so as to handle success/failure on my operations. I made the changes to the code, but the tests stop working and I cannot understand why. Here I show you some snippets:
FireStoreClient.kt
suspend fun items(): Result<ItemsResponse>
NetworkDataSource.kt
suspend fun getItems(): List<Item> =
fireStoreClient.items().fold({ it.items.map { item -> item.toDomain() } }, { emptyList() })
NetworkDataSourceTest.kt
#ExperimentalCoroutinesApi
#Test
fun `Check getItems works properly`() = runBlockingTest {
whenever(fireStoreClient.items()).doReturn(success(MOCK_ITEMS_DOCUMENT))
val expectedResult = listOf(
Item(
id = 1,
desc = "Description 1"
),
Item(
id = 2,
desc = "Description 2"
)
)
assertEquals(expectedResult, dataSource.getItems())
}
And this is the exception I am getting right now. Any clue? It appears that the fold() method is not being executed when unit testing.
java.lang.ClassCastException: kotlin.Result cannot be cast to ItemsResponse
at NetworkDataSource.getItems(NetworkDataSource.kt:31)
I've found a different workaround for this result-wrapping issue, for those who don't want to make their own Result type.
This issue appears to happens specifically when using Mockito's .thenReturn on suspend functions. I've found that using .thenAnswer doesn't exhibit the problem.
So instead of writing this in your unit test (changed doReturn to thenReturn here):
whenever(fireStoreClient.items()).thenReturn(success(MOCK_ITEMS_DOCUMENT))
Use:
whenever(fireStoreClient.items()).thenAnswer { success(MOCK_ITEMS_DOCUMENT) }
Edit: I should note that I was still experiencing this issue when running Kotlin 1.5.0.
Edit: On Kotlin 1.5.20 I can use .thenReturn again.
After a deep dive into the problem, finally, I've found a temporary workaround that works in the testing environment. The problem is, somehow the value of the Result object is wrapped by another Result, and we can pull the desired value or exception using reflection.
So, I've created an extension function called mockSafeFold, which implements the fold behavior in normal calls, and acts fine when you are executing unit-tests.
inline fun <R, reified T> Result<T>.mockSafeFold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R
): R = when {
isSuccess -> {
val value = getOrNull()
try {
onSuccess(value as T)
} catch (e: ClassCastException) {
// This block of code is only executed in testing environment, when we are mocking a
// function that returns a `Result` object.
val valueNotNull = value!!
if ((value as Result<*>).isSuccess) {
valueNotNull::class.java.getDeclaredField("value").let {
it.isAccessible = true
it.get(value) as T
}.let(onSuccess)
} else {
valueNotNull::class.java.getDeclaredField("value").let {
it.isAccessible = true
it.get(value)
}.let { failure ->
failure!!::class.java.getDeclaredField("exception").let {
it.isAccessible = true
it.get(failure) as Exception
}
}.let(onFailure)
}
}
}
else -> onFailure(exceptionOrNull() ?: Exception())
}
Then, simply call it instead of fold:
val result: Result = myUseCase(param)
result.mockSafeFold(
onSuccess = { /* do whatever */ },
onFailure = { /* do whatever */ }
)
I had the same issue.
I noticed that my method of injected class which should return Result<List<Any>> returns actually Result<Result<List<Any>>> which causes the ClassCastException. I used the Evaluate Expression option for the result from the method and I got
Success(Success([]))
The app works well but unit tests didn't pass due this problem.
As a temporary solution I built a new simple implementation of Result sealed class with fold() extension function. It should be easy to replace in future to kotlin.Result
Result sealed class:
sealed class Result<T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure<T>(val error: Throwable) : Result<T>()
}
fold() extension function:
inline fun <R, T> Result<T>.fold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R
): R = when (this) {
is Result.Success -> onSuccess(value)
is Result.Failure -> onFailure(error)
}
In my app I have two services and both of them have a method that makes a requests and then returns an Observable of different type.
I want to display in a RecyclerView a list composed of the result of combining these two Observables. I googled about this and found the zip() method that seems to do exactly what I want. I'm trying to implement it but I don't know how to do it correctly.
While I was googling, I came up with this this article which seems to explain it clearly. Even though the author is using Singles while I am using Observables.
As far as I understand how zip() works, I know I have to pass every Observable I want to "zip" and then I must specify a function that will compose my final Observable, right?
This is my code so far:
interface InvitationService {
#GET("foo/{userId}")
fun getFooByUser(#Path("userId") userId: String): Observable<Response<ArrayList<Foo>>>
}
interface InvitationService {
#GET("bar/{userId}")
fun getBarByUser(#Path("userId") userId: String): Observable<Response<ArrayList<Bar>>>
}
class FooRemoteDataSource : FooDataSource {
private var apiService: FooService
fun getFooByUser(userId:String) {
return apiService.getFooByUser(userId)
}
}
class BarRemoteDataSource : BarDataSource {
private var apiService: BarService
fun getBarByUser(userId:String) {
return apiService.getBarByUser(userId)
}
}
class FooRepository(private val remoteDataSource: InvitationRemoteDataSource) : FooDataSource {
override fun getFooByUser(userId: String): Observable<Response<ArrayList<Foo>>> {
return remoteDataSource.getFooByUser(userId)
}
}
class BarRepository(private val remoteDataSource: BarRemoteDataSource) : BarDataSource {
override fun getBarByUser(userId: String): Observable<Response<ArrayList<Bar>>> {
return remoteDataSource.getBarByUser(userId)
}
}
And here is where I'm actually stuck:
class ListPresenter(var listFragment: ListContract.View?,
val fooRepository: FooRepository,
val barRepository: BarRepository) : ListContract.Presenter {
fun start() {
loadList()
}
private fun loadLists() {
//HERE IS WHERE IM STUCK
Observable.zip(fooRepository.getFooByUser(userId).subscribeOn(Schedulers.io()),
barRepository.getBarByUser(userId).subscribeOn(Schedulers.io()),
)
// AFTER 'ZIPPING' THE OBSERVABLES
// I NEED TO UPDATE THE VIEW ACCORDINGLY
}
}
I don't know how to call zip() properly, I know that I must pass a function but I don't get it because in the article linked above the author is using a Function3 because he has 3 Observables.
As I only have 2, I don't know how to do it. If open curly braces after a comma inside the method args, it requires me to return a BiFunction<ArrayList<Foo>, ArrayList<Bar>> which is what I don't know how to specify.
Would someone explain it to me?
For Kotlin you should use RxKotlin rather than RxJava. BiFunction, Function3 come from RxJava. With RxKotlin you can use lambdas instead.
As far as I understand how zip() works, I know I have to pass every Observable I want to "zip" and then I must specify a function that will compose my final Observable, right?
Correct, and here is a minimal example, which demonstrates how to do it.
Example 1
val observable1 = listOf(1, 2, 3).toObservable()
val observable2 = listOf(4, 5, 6).toObservable()
val zipped = Observables.zip(observable1, observable2) { o1, o2 -> o1 * o2}
In this example you have two observables, each emitting integers. You pass them to zip and as third argument a lambda which defines a way to "cobmine them". In this case it multiplies them.
The resulting observable zipped will emit: 4, 10 and 18.
Example 2
Here another example zipping three observables which are not all of the same type:
val obs1 = listOf("on", "tw", "thre").toObservable()
val obs2 = listOf("n", "o", "e").toObservable()
val obs3 = listOf(1, 2, 3).toObservable()
val zipped = Observables.zip(obs1, obs2, obs3) { o1, o2, o3 ->
"$o1$o2 = $o3"
}
Here, each element of the resulting observable will be a string: "one = 1", "two = 2", "three = 3"
Zipping two Observables of different types using BiFunction
override fun getCommoditiesAndAddresses() {
view.showProgress()
view.hideViews()
Observable.zip(Commo24Retrofit.createAuthService(RateAPIService::class.java)
.getCommodities(),
Commo24Retrofit.createAuthService(RateAPIService::class.java)
.getLocations(GetLocationsRequest(getOrgId())),
BiFunction { commodityResponse: GetCommoditiesResponse, locationsResponse: GetLocationsResponse ->
handleCommoditiesAndAddresses(commodityResponse, locationsResponse)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
view.hideProgress()
view.showViews()
view.handleCommodities(it?.commodities)
view.handleLocations(it?.locations)
}, { throwable ->
view.hideProgress()
view.handleFailure(throwable.getErrorMessage(context))
})
}
Look, how I'm handling the response:
private fun handleCommoditiesAndAddresses(commodityResponse: GetCommoditiesResponse, locationsResponse: GetLocationsResponse): CommoditiesAddresses {
return CommoditiesAddresses(commodityResponse.commodityList, locationsResponse.addressList)
}
Here, check the API Service:
interface RateAPIService {
#POST("get-org-address")
fun getLocations(#Body getLocationsRequest: GetLocationsRequest): Observable<GetLocationsResponse>
#POST("get-commodity-list")
fun getCommodities(): Observable<GetCommoditiesResponse>
}
If you have any doubt you can comment it out.
I am trying to consume an API from thesportsdb to display lastmatch from specific league. in my recyclerview I want to show the team badge for every teams but when I request the lastmatch API it didn't include the team badge, only the id for each team and if I want to show the badge it require me to request the team profile which includes the url for the team badge.
Since I am new to rxJava so I am still familiarize myself with it. some posts suggest using flatmap but it kind a difficult for beginner like me to implement it.
this is the retrofitService:
interface FootballRest {
#GET("eventspastleague.php")
fun getLastmatch(#Query("id") id:String) : Flowable<FootballMatch>
#GET("lookupteam.php")
fun getTeam(#Query("id") id:String) : Flowable<Teams>
}
I used repository pattern
class MatchRepositoryImpl(private val footballRest: FootballRest) : MatchRepository {
override fun getFootballMatch(id: String): Flowable<FootballMatch> = footballRest.getLastmatch(id)
override fun getTeams(id: String): Flowable<Teams> =
footballRest.getTeam(id)
}
and this is the presenter who make the call and send the data to the view:
class MainPresenter(val mView : MainContract.View, val matchRepositoryImpl: MatchRepositoryImpl) : MainContract.Presenter{
val compositeDisposable = CompositeDisposable()
val requestMatch = matchRepositoryImpl.getFootballMatch("4328")
val requestTeam = matchRepositoryImpl.getTeams()
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe{
mView.displayFootballMatch(it.events)
})
}
so far I only show the last match result, but I want also to show the badge team on the list.
You could use a map operator combined with lastElement().blockingGet() for the second Observable for this and then return a Pair of results. A simple example could be as follows:
#Test
public fun test1() {
Observable.just(1)
.map {
// here 'it' variable is calculated already so it can be passed to the second observable
val result = Observable.just(2).lastElement().blockingGet()
Pair<Int, Int>(it, result)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { t -> System.out.println("First : " + t?.first + ", second : " + t?.second) }
Thread.sleep(1000)
}
Output
1 2
If your second Observable depends on the result of the first one then just use the it variable inside the map operator and pass it to whatever place it's needed. So, if using the previous example your code could be converted to this:
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328").toObservable(
.map {
// here 'it' variable is calculated already so it can be passed to the second observable
val next = matchRepositoryImpl.getTeams(it).toObservable().lastElement().blockingGet()
Pair<Int, Int>(it, next)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe{ t ->
mView.displayFootballMatch(t.first)
mView.displayBadgeTeam(t.second)
})
}
Rather than use a blockingGet operator its probably easier for you to use a flatmap and return all of this data as a single stream.
You could achieve this by combining the flatmap and zip operator. This would look something like the following, where MatchData holds both the FootballMatch data along with the homeTeam and awayTeam data.
data class MatchData(val footballMatch: FootballMatch, val homeTeam: Teams, val awayTeam: Teams)
Your flatmap operation would then need to invoke the getTeams method for both home and away team which can then be combined with the footballMatch data through the zip oprator.
override fun getFootballMatchData() {
compositeDisposable.add(matchRepositoryImpl.getFootballMatch("4328")
.subscribeOn(Schedulers.io())
.flatMap { footballMatch ->
Flowable.zip(
matchRepositoryImpl.getTeams(footballMatch.idHomeTeam),
matchRepositoryImpl.getTeams(footballMatch.idAwayTeam),
BiFunction { homeTeam: Teams, awayTeam: Teams ->
MatchData(
footballMatch = footballMatch,
homeTeam = homeTeam,
awayTeam = awayTeam)
}
)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
mView.displayFootballMatch(it)
})
}
New at Kotlin here and trying to learn the best way to use the higher order functions and passing lambdas. I've created this method to call an API and return an object created from a string OR return a failure if something went wrong.
fun getDeviceStatus(onSuccess: (Device) -> Unit, onFailure: ((String) -> Unit)? = null) {
FuelClient.get(DEVICE_URL,
success = { responseString ->
val adapter = MoshiUtil.moshi.adapter(Device::class.java)
val deivce= adapter.fromJson(responseString)!!
onSuccess(device)
},
failure = { onFailure?.invoke(it.message!!)})
}
I can use this function fine like so:
DeviceService.getDeviceStatus(
{ w ->
print("device")
},
{ e -> print(e) })
But it bothers me a bit that I can't see the name of the functions to see what each function does. I"m wondering if there is a cleaner/better way to do this, like
DeviceService.getDeviceStatus(){
onSuccess{print("device")}
onFailure{print("error")}
}
or maybe
DeviceService.getDeviceStatus()
.onSuccess{print("device")}
.onFailure{print("error")}
But those gives errors. Any thoughts on how to best handle the onSuccess/onFailure use case that is very common? Thx
You can attach a name to each variable in kotlin. Change your code like this
DeviceService.getDeviceStatus(
onSuccess = { w ->
print("device")
},
onFailure = { e -> print(e) })
For this specific case, when the second lambda is optional, infix functions work very well:
sealed class DeviceStatusResult {
abstract infix fun onFailure(handler: (String) -> Unit)
}
class DeviceStatusSuccess(val device: Device) : DeviceStatusResult() {
override fun onFailure(handler: (String) -> Unit) = Unit
}
class DeviceStatusFailure(val errorMessage: String) : DeviceStatusResult() {
override fun onFailure(handler: (String) -> Unit) = handler(errorMessage)
}
fun getDeviceStatus(onSuccess: (Device) -> Unit): DeviceStatusResult {
// get device status
// if (success)
val device = Device()
onSuccess(device)
return DeviceStatusSuccess(device)
// else
// return DeviceStatusFailure(message)
}
Then it can used like
getDeviceStatus { device ->
println(device)
} onFailure { errorMessage ->
System.err.println(errorMessage)
}
Maybe onFailure should be called orFail or something like that.
It is good when the second argument is optional, but not so much otherwise because it doesn't force the user to actually supply a failure handler. And I don't think it's a good idea because it will be too easy to accidentally omit a failure handler. It's much better to force the user to provide one, even if it happens to be an empty one. Therefore, it is better to use named arguments for this case, even though nothing forces to actually name them.
For example we have a class which needs to have more than one function such as two functions as parameter:
class TestClass internal constructor(
private val onClickShowName: (String) -> Unit,
private val onClickShowSurname: (String) -> Unit
) { //Your work. }
Then you need to create val as TestClass:
class MainActivity {
val mTestClass = TestClass(
onClickShowName = {dataText: String -> Log.i("TEST", dataText)},
onClickShowSurname = {dataText: String -> Log.i("TEST", dataText)}
)
}