Reactive Abstractions in Android World

August 26, 2018

Who knows how many test suites were not created because of a classic parry.

It cannot be tested — it uses a platform call!

Well, it is not actually true all the time. Unit testing something that only a human eye and neural networks can catch — like animations — doesn’t make sense. On the other hand, retrying a network request on a re-established connection can and should be tested. As a bonus, it is possible to gain a couple of perks.

Theory

The main advice I can give Android developers about testing — start thinking about the codebase as a platform-agnostic environment. Not in a ridiculous way but in a more pragmatic one — otherwise, it is too easy to slip on a dark cross-platform path. Associating the codebase with the JVM platform and not specifically Android is a better idea. Framework-related interactions can be plugged-in as composable blocks.

Another advice — embrace abstractions available on hand. Samples in this article are based on RxJava but it is possible to replace it with Future, Promise or Kotlin coroutines. This is especially useful with async + await type of interactions. Believe me, it is not wise to do everything from scratch if there is a good tech available. Moreover, it is easier to maintain consistency in the codebase if each component speaks using the same constructs. Do not repeat the Tower of Babel fall.

Practice

Lke it or not — platform interactions leak into the business logic. It is understandable — the environment capabilities should be embraced.

At the same time, it is essential to test the logic of the final product — otherwise, there will be no product at all, only issues and undefined behavior.

Fortunately enough, the mankind have dealt with such issues for a while. Let’s use a weight measurement example. How much does a brick weight? Well, certainly less than a space station and more than an atom. That characteristic does not really help when it is necessary to transport a number of bricks. Will a car break under a million bricks? That’s why humanity created abstractions, such as grams, kilograms and tons. It is possible to take this completely (but collectively) made up measurement unit and apply it everywhere. Well, except three countries that do not use metric system.

Software development provides means to create abstractions easy-as. It is not necessary to create an ISO committee to create one — a programming language is enough. Android is not an exception — and it never was.

Connectivity

It can be useful to retry stalled network requests when OS reconnects to a network access point. In fact, ConnectivityManager was there for centuries:

The primary responsibilities of this class are to: monitor network connections (Wi-Fi, GPRS, UMTS, etc)…

The usage of the potential abstraction looks like this.

disposable += connectivity.available.subscribe(refresh)

Seems like a single stream will be enough. Let’s do it!

interface Connectivity {

    val available: Observable<Unit>

    class AndroidConnectivity(context: AndroidContext) : Connectivity {

        private val manager = context.systemService<ConnectivityManager>()

        override val available = Observable.merge(current, updates)

        private val current = Observable
            .fromCallable { manager.isDefaultNetworkActive() }
            .filter { it == true }
            .map { Unit }

        private val updates = Observable.create<Unit> { emitter ->
            val listener = OnNetworkActiveListener { emitter.onNext(Unit) }

            manager.addDefaultNetworkActiveListener(listener)
            emitter.setCancellable { manager.removeDefaultNetworkActiveListener(listener) }
        }
    }
}

Marvelous! A couple of things to notice here.

The Observable itself handles proper Listener setting on subscription and unsetting on unsubscription. It uses the Dispose pattern, i. e. the RxJava Disposable. Since all Observable behave the same the end-user of the Connectivity will work with it as with any other Observable.

Since there is an interface it is easy to provide a Test* implementation for unit tests. Just a single call to simulate the real-world behavior and that’s it — it is possible to test code which works with the platform framework.

class TestConnectivity : Connectivity {
    override val available = PublishSubject.create<Unit>()
}
context("connectivity becomes available") {

    beforeEach {
        connectivity.available.onNext(Unit)
    }

    it("refreshes") {
        verify(refresh).run()
    }
}

Launching

What about starting something via Intent? The operation itself is trivial, but it is necessary to keep in mind that there might be an ActivityNotFoundException. This exception is being thrown when nobody can handle our intention.

disposable += launcher.launch(Request.ShareText("Ping!"))
    .observeOn(mainThread)
    .subscribe {
        when (result) {
            Result.Success -> view.showSuccessAlert()
            Result.Failure -> view.showFailureAlert()
        }
    }

To achieve this we are going to create our own abstraction over Intent. Doing so will hide the complexity and will help to avoid a copy-paste of the same Intent building over and over again.

interface Launcher {

    sealed class Request {
        data class ShareText(val text: String) : Request()
    }

    enum class Result { Success, Failure }

    fun launch(request: Request): Single<Result>

    class AndroidLauncher(
            private val application: Application,
            private val mainScheduler: Scheduler
    ) : Launcher {

        override fun launch(request: Request) = application.currentActivity
                .take(1)
                .singleOrError()
                .map { activity ->
                    try {
                        activity.startActivity(createIntent(request))
                        Result.Success
                    } catch (e: ActivityNotFoundException) {
                        Result.Failure
                    }
                }
                .subscribeOn(mainScheduler)

        private inline fun createIntent(request: Request) = when (request) {
            is Request.ShareText -> {
                val intent = Intent(Intent.ACTION_SEND).apply {
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    type = "text/plain"
                    putExtra(Intent.EXTRA_TEXT, request.text)
                }

                Intent.createChooser(intent, /* chooser title */ null)
            }
        }
    }
}

We are controlling the thread under the hood so the consumer does not need to know implementation details.

An attentive reader might notice that we are using another abstraction — Application#currentActivity. It is trivial to implement using ActivityLifecycleCallbacks and Connectivity-like approach to listeners and callbacks.

Of course, we can test the behavior.

class TestLauncher: Launcher {
    val result = SingleSubject.create<Result>()

    override fun launch(request: Request) = result
}
context("launch result is success") {

    beforeEach {
        launcher.result.onSuccess(Result.Success)
    }

    it("shows success alert") {
        verify(view).showSuccessAlert()
    }
}

context("launch result is failure") {

    beforeEach {
        launcher.result.onSuccess(Result.Failure)
    }

    it("shows failure alert") {
        verify(view).showFailureAlert()
    }
}

Google Play Services

The concept works well not only with the platform but with all third-party information sources — such as various SDK. In such cases, it is possible to improve the external API — for example, provide proper nullability handling if the SDK is not annotated with @Nullable and @NonNull.

Let’s say it is necessary to show a warning if Google Play Services is not installed.

disposable += googlePlayServices.available
    .filter { it == false }
    .observeOn(mainThread)
    .subscribe(view::showGooglePlayServicesWarningAlert)

Implementation:

interface GooglePlayServices {

    val available: Single<Boolean>

    class PackagedGooglePlayServices(
        context: AndroidContext,
        ioScheduler: Scheduler
    ) : GooglePlayServices {

        override val available = Single
            .fromCallable { GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) }
            .map { it == ConnectionResult.SUCCESS }
            .subscribeOn(ioScheduler)
    }
}

The work is done on IO thread since retrieving the information can take an undefined amount of time.

As always, it is possible to test the behavior without mocking static GoogleApiAvailability getInstance() method.

class TestGooglePlayServices : GooglePlayServices {
    override val available = SingleSubject.create<Boolean>()
}
context("Google Play Services is available") {

    beforeEach {
        googlePlayServices.available.onSuccess(true)
    }

    it("does not show warning alert") {
        verifyNever(view).showGooglePlayServicesWarningAlert()
    }
}

context("Google Play Services is not available") {

    beforeEach {
        googlePlayServices.available.onSuccess(false)
    }

    it("shows warning alert") {
        verify(view).showGooglePlayServicesWarningAlert()
    }
}

Retrospective

The beautiful thing about abstractions is that the idea is universal and can be applied everywhere:

At the same time, using a high-level abstraction — like the reactive approach (RxJava in particular) — brings a couple of benefits.

The ironic thing about abstractions is that developers create them all the time for their own domain. But… tend to avoid it for external sources. Do not make this mistake.


Thanks to Artem Zinnatullin for the review!