Result class is RxJava’s soulmate

Why Result class is the perfect LiveData alternative for Android and the best use cases you should be considering.

July 20, 2020
By
Nakarin Jupattanakul

{% c-block language="JavaScript" %}
struct AppSettings {
   // Update the respective value in UserDefault
   static var isDarkModeEnabled = false {
       didSet {
           UserDefaults.standard.setValue(isDarkModeEnabled, forKey:
"is_dark_mode_enabled")
       }
   }
}
{% c-block-end %}

The `Result` class is a particular usage of Kotlin sealed class introduced in `Android` samples. In fact, an introduction video to Kotlin sealed class on the official “Android Developers” channel on YouTube, uses the  `Result` class as an example usage of Kotlin sealed class. If you haven’t seen this particular usage, it would be worthwhile to watch the video.

If you have already seen this usage, you may already be aware of how this `Result` class could be a perfect match to `LiveData`. As `LiveData` is an observable of one data type, using the `Result` class enables the ability to combine data and errors within one observable.

Considering another observable source, a `LiveData` alternative, Rx, the mentioned benefit may seem unnecessary, as it already has the ability to propagate errors directly to its observer with its `error` block. That being said, the benefit of using `Result` class with Rx comes to light with Rx’s advantage over `LiveData` in the ability to combine/chain observables.

Let’s see the following example.

{% c-block language="JavaScript" %}
class GetLatestNewsUseCase {
   fun execute(): Flowable<Result<List<News>>> {
       return NewsRepository().getLatestNews()
              .map {
                 if (it is Result.Success) {
                     It
                 } else if (it.exception is SystemException) {
                     val error = it.exception
                     when (error.code) {
                       50001 -> {
                       Result.Error(MembershipNotFoundException())
                       }
                       50002 -> {
                       Result.Error(SubscriptionExpiredException())
                       }
                     }
                } else {
                    Result.Error(UnknownException())
                }
            }
   }
}
{% c-block-end %}

And a function in presentation layer

{% c-block language="JavaScript" %}
fun getLatestNews() {
   GetLatestNewsUseCase().execute()
       .observeOn(AndroidSchedulers.mainThread())
       .doOnNext {
           if (it is Result.Success) {
               populate(it.data)
           } else {
               when(it.exception) {
                   is MembershipNotFoundException -> {
                       showSignUpPopup()
                   }
                   is SubscriptionExpiredException -> {
                       showReSubscriptionPopup()
                   }
               }
           }
        }.subscribeOn(Schedulers.io())
        .subscribe()
}
{% c-block-end %}

Assuming an exception is thrown in `NewsRepository`, being able to easily pass the error upstream one level at a time enables applications of operation on the error object. In the example, SystemException can be mapped to Business error corresponding to its error code within `UseCase` class, exactly where business logic should be applied. The Presentation layer then consumes the result and renders UI based on the error type.

Let’s take a look at another example.

{% c-block language="JavaScript" %}
class NewsLocalDataStore {
   fun saveNews(newsList: List<News>): Completable {
       return NewsDao().insert(newsList)
   }
}
{% c-block-end %}

Here we have a `CompletableSource`. If we’d like to use the `Result` class to help propagate error upstream, we may choose to convert it to a `SingleSource`. However, you would notice that the `Result` class requires data for its `Success` subclass but the `CompletableSource` is only concerned with completion and does not emit data.

What we could do is create another type of `Result` to handle this case. We would then have 2 types of `Result`. One that it’s `Success` subclass has data and another one that does not. Let’s call the one with data, `QueryResult`, and another one, `TransactionResult`.

{% c-block language="JavaScript" %}
sealed class QueryResult<out R> {
   data class Success<out T>(val data: T) : Result<T>()
   data class Error(val exception: Exception) : Result<Nothing>()
}
sealed class TransactionResult {
   class Success : TransactionResult()
   data class Error(val exception: Exception) : TransactionResult()
}
{% c-block-end %}

We can now convert the `CompletableSource` to `SingleSource`.

{% c-block language="JavaScript" %}
class NewsLocalDataStore {
   fun saveNews(newsList: List<News>): Single<TransactionResult> {
     return try {
              NewsDao().insert(newsList)
                  .toSingle { TransactionResult.Success() }
            } catch (e: Exception) {
              Single.just(TransactionResult.Error(DBException(e)))
            }
   }
}
{% c-block-end %}

Let’s see these 2 types of `Result` in action.

{% c-block language="JavaScript" %}
class NewsRepository {
   fun getLatestNews(): Flowable<QueryResult<List<News>>> {
     return NewsLocalDataStore().getLatestNews()
    }
   fun syncLatestNews(): Single<TransactionResult> {
      return NewsRemoteDataStore().getLatestNews()
         .flatMap {
            if (it is QueryResult.Success) {
                NewsLocalDataStore().saveNews(it.data)
            } else {
                Single.just(TransactionResult.Error(it.exception))
            }
         }
   }
}
{% c-block-end %}

In the example, there are `getLatestNews()` function that observes News objects locally and `syncLatestNews()` function that fetch the latest news and save them to the database.

Suppose `getLatestNews()` function emits an error when the result of the local query is empty, and how about we create a function called `getSyncLatestNews()` that satisfies the following requirements:

  1. Emit News list whichever first, cache or remote result.
  2. Do not emit any error until remote call is completed.

{% c-block language="JavaScript" %}
fun getSyncLatestNews(): Flowable<QueryResult<List<News>> {
   return getSyncData(syncLatestNews(), getLatestNews())
}
fun <T> getSyncData(
   syncingSource: Single<TransactionResult>,
   querySource: Flowable<QueryResult<T>>
): Flowable<QueryResult<T>> {
   var shouldIgnoreResult = false
   return Flowable.combineLatest(

   syncingSource.toFlowable()
       .startWith(TransactionResult.Error(Exception())),
   querySource,
   BiFunction {
         syncResult: TransactionResult,
         queryResult: QueryResult<T> ->
   if (syncResult.failed && queryResult.succeeded) {
       shouldIgnoreResult = false
       queryResult
   } else if (syncResult.succeeded && queryResult.succeeded) {
       shouldIgnoreResult = false
       queryResult
   } else if (syncResult.succeeded && queryResult.failed) {
       shouldIgnoreResult = true
       queryResult
   } else {
       if (shouldIgnoreResult) {
           shouldIgnoreResult = false
           queryResult
       } else {
           shouldIgnoreResult = true
           queryResult
       }
   }}).filter {
       !shouldIgnoreResult
   }
}
{% c-block-end %}

What’s being done here is the flag `shouldIgnoreResult` is being used to filter for results that satisfy the requirements. Bringing both data and error to the same code block allows us to define which states of the combination of success/error results satisfy the requirements. Whereas the implementation and readability would be more complicated if data and error were to be in separate code blocks.

Join the Amity team!

We’re always seeking ambitious, passionate and community-driven candidates to join us.