Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

12. Error Handling in Practice
Written by Alex Sullivan & Junior Bontognali

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Life would be great if we lived in a perfect world, but unfortunately things frequently don’t go as expected. Even the best RxJava developers can’t avoid encountering errors, so they need to know how to deal with them gracefully and efficiently. In this chapter, you’ll learn how to deal with errors, how to manage error recovery through retries, or just surrender yourself to the universe and let the errors go.

Getting started

The app you’ll be creating for this chapter is a weather app. It will allow a user to type in a city name and see the weather for that city. It will also allow the user to use their current location as the trigger to fetch weather details. To accomplish all of this, you’ll use the OpenWeatherMap API.

Before continuing, make sure you have a valid OpenWeatherMap API Key http://openweathermap.org. If you don’t already have a key, you can sign up for one at https://home.openweathermap.org/users/sign_up.

Once you’ve completed the sign-up process, visit the dedicated page for API keys at https://home.openweathermap.org/api_keys and generate a new one.

Open the starter project in Android Studio. In the starter project, open the WeatherApi.kt file, take the key you generated above and replace the placeholder in the following location:

val apiKey =
    BehaviorSubject.createDefault("INSERT_YOUR_API_KEY_HERE")

Once that’s done, run the app. When prompted, grant the app permission to use the device’s location. After you grant permission, you’ll see the following screen:

Try entering some text into the top EditText box at the top of the screen where it says Current Location. You should see the weather details change. You should also see a nice image in the center of the app indicating what the current weather is. For example, if it’s snowing outside, you’ll see a cloud with some snow underneath. Brrrr!

If you instead see nothing show up, then that might mean you hit an error. Make sure the API key you entered is valid and that the city name you entered is a real city. If you just created your account, make sure you check your email to confirm your email address. You’ll have to re-run the app if it did experience an error when making the initial API call. Not a great user experience, right?

This good news is you’re going to fix that user experience!

Before you start diving into managing errors, it’s a good idea to get acquainted with the code for the app. Open the WeatherViewModel and look around. It takes one argument:

private val lastKnownLocation: Maybe<Location>

lastKnownLocation is a Maybe representing the last known location of the user. If you’re interested in learning about how the app creates a Maybe out of the last known location, take a look at the lastKnownLocation method in the X.kt file.

In addition to the lastKnownLocation constructor parameter, WeatherViewModel exposes two public methods that WeatherActivity uses to notify the ViewModel of clicks on the location button and text change events:

fun locationClicked() = locationClicks.onNext(Unit)

fun cityNameChanged(name: CharSequence) =
  cityNameChanges.onNext(name)

These methods pipe their relevant values into a couple of PublishSubjects that are defined at the top of the file:

private val locationClicks = PublishSubject.create<Unit>()
private val cityNameChanges =
  PublishSubject.create<CharSequence>()

Now you can easily represent users actions as streams. Hooray!

In the init block, WeatherViewModel uses the Observable.merge function to merge the two subjects built from locationClicks and cityNameChanges to create a final Observable that will emit Weather updates to the weatherLiveData object.

Notice that the locationObservable declaration uses the onErrorReturnItem() method to default to an empty instance of the Weather object if the stream emits any errors.

Sure, it’s a nice, compact, single line, but it doesn’t make for a great UX. You can do way better!

Managing errors

Errors are an inevitable part of any app. Unfortunately, no one can guarantee an app will never error out, so you will always need some type of error-handling mechanism.

Handling errors with catch

Now that you know about the types of errors you can encounter, it’s time to see how to handle those errors. The most basic way is to use one of the onError. operators. The onError operators works much like the try-catch flow in plain Kotlin.

public final Observable<T> onErrorResumeWith(
  @NonNull ObservableSource<? extends T> fallback
)

public final Observable<T> onErrorReturnItem(final T item)

Avoiding a common pitfall

Errors propagate through the Observable’s chain, so an Observable will forward an error that happens at the beginning of an Observable chain to the final subscription if there aren’t any handling operators in place.

.subscribeBy(
  onError = {
    Log.e("Weather", "Error: $it")
  },
  onNext = {
    weatherLiveData.postValue(it)
  }
)
E/Weather: Error: java.lang.IllegalStateException: Not Found

Catching errors

Now, revert the changes you just made so that the Observable.merge() call is using a single line subscribe() and the textObservable is again returning an empty instance of Weather if it encounters an error.

private val cache = mutableMapOf<String, Weather>()
.flatMapSingle { cityName ->
  WeatherApi.getWeather(cityName.toString())
    .doOnSuccess { cache[cityName.toString()] = it }
}
private fun getWeatherForLocationName(
    name: String
): Single<Weather> {
}
return WeatherApi.getWeather(name)
  .doOnSuccess { cache[name] = it }
.onErrorReturn {
  cache[name] ?: Weather.empty
}
.flatMapSingle { getWeatherForLocationName(it.toString()) }

Retrying on error

Catching an error is just one way you can handle errors in RxJava. You can also handle errors with retry().

Retry operators

There are three basic types of retry() operators. The first one is the most basic:

public final Observable<T> retry()
//.onErrorReturn {
//  cache[name] ?: Weather.empty
//}
.retry()
public final Observable<T> retry(long times)
private fun getWeatherForLocationName(name: String): Single<Weather> {
  return WeatherApi.getWeather(name)
    .doOnSuccess { cache[name] = it }
    .retry(3)
    .onErrorReturn {
      cache[name] ?: Weather.empty
    }
}

Advanced retries

The last operator, retryWhen(), is suited for advanced retry situations. This error handling operator is considered one of the most powerful:

public final Observable<T> retryWhen(
    Function<? super Observable<Throwable>,
        ? extends ObservableSource<?>> handler
)
subscription -> error
delay and retry after 1 second

subscription -> error
delay and retry after 2 seconds

subscription -> error
delay and retry after 3 seconds

subscription -> error
delay and retry after 4 seconds
private val maxAttempts = 4
.retryWhen { errors: Flowable<Throwable> ->

}
errors.flatMap { Flowable.timer(1, TimeUnit.SECONDS) }
errors
  .scan(1) { count, _ ->
    count + 1
  }
  .flatMap { Flowable.timer(it.toLong(), TimeUnit.SECONDS) }
.scan(1) { count, _ ->
  count + 1
}
.scan(1) { count, error ->
  if (count > maxAttempts) {
    throw error
  }
  count + 1
}

Errors as objects

As you go deeper into the world of reactive and functional programming, it can become painful to keep dealing with Throwables and exceptions for expected results. It often makes more sense to treat an exception as something that your program could not have imagined, and thus does not know how to handle.

Modeling a network error

Open WeatherApi and look at the bottom of the file. You should see two unused sealed classes:

sealed class NetworkResult {
  class Success(val weather: Weather) : NetworkResult()
  class Failure(val error: NetworkError) : NetworkResult()
}

sealed class NetworkError : Exception() {
  object ServerFailure : NetworkError()
  object CityNotFound : NetworkError()
}
private fun mapWeatherResponse(
    response: Response<WeatherNetworkModel>
): NetworkResult {
  return when (response.code()) {
    // 1
    in 200..300 -> {
      val body = response.body()
      if (body != null) {
        NetworkResult.Success(
            body.toWeather().copy(icon = iconNameToChar(
                body.weather.first().icon)))
      } else {
        NetworkResult.Failure(NetworkError.ServerFailure)
      }
    }
    // 2
    in 400..500 -> NetworkResult.Failure(
        NetworkError.CityNotFound)
    // 3
    else -> NetworkResult.Failure(NetworkError.ServerFailure)
  }
}
fun getWeather(city: String): Single<NetworkResult> {
  return weather.getWeather(city, apiKey.value)
      .map(this::mapWeatherResponse)
}

fun getWeather(location: Location): Single<NetworkResult> {
  return weather.getWeather(
      location.latitude, location.longitude, apiKey.value)
      .map(this::mapWeatherResponse)
}
.onErrorReturnItem(Weather.empty)
.onErrorReturnItem(
    WeatherApi.NetworkResult.Success(Weather.empty))
Single<WeatherApi.NetworkResult>
.onErrorReturn {
  val cachedItem = cache[name] ?: Weather.empty
  WeatherApi.NetworkResult.Success(cachedItem)
}
private fun showNetworkResult(
    networkResult: WeatherApi.NetworkResult
) {
  when (networkResult) {
    // 1
    is WeatherApi.NetworkResult.Success -> {
      cache[networkResult.weather.cityName] =
          networkResult.weather
      weatherLiveData.postValue(networkResult.weather)
    }
    // 2
    is WeatherApi.NetworkResult.Failure -> {
      when (networkResult.error) {
        WeatherApi.NetworkError.ServerFailure ->
            errorLiveData.postValue("Server Failure")
        WeatherApi.NetworkError.CityNotFound ->
            errorLiveData.postValue("City Not Found")
      }
    }
  }
}
.subscribe(this::showNetworkResult)

Challenges

Challenge 1: Reacting to an invalid API key

Recall that, earlier in the chapter, you started using the NetworkResult object to encapsulate both success and errors from the network. You’re currently interpreting values between 400 and 500 as “city not found” errors, but that’s not actually the case.

Challenge 2: Use retryWhen on restored connectivity

In this challenge you need to handle the condition of an unavailable internet connection.

Key points

  • Errors are an inevitable part of any app. You will always need some type of error-handling mechanism.
  • No internet connection is a common error. If the app needs an internet connection to retrieve and process the data, but the device is offline, you need to be able to detect this and respond appropriately.
  • Invalid input is a common error. Sometimes you require a certain form of input, but the user might enter something entirely different. Perhaps you have a phone number field in your app, but the user ignores that requirement and enters letters instead of digits.
  • API error or HTTP error is a common error. Errors from an API can vary widely. They can arrive as a standard HTTP error (response code from 400 to 500), or as errors in the response, such as using the status field in a JSON response.
  • In RxJava, error handling is part of the framework and can be handled in two ways: onError (return a default value) and retry (Retry for a limited or unlimited number of times).

Where to go from here?

In this chapter, you were introduced to error handling using retry() and onErrorReturn(). The way you handle errors in your app really depends on what kind of project you’re building. When handling errors, design and architecture come in play, and creating the wrong handling strategy might compromise your project and result in re-writing portions of your code.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now