Share

engineering

5min read

Maintaining Open-Source RxAndroidBle Library (Introduction)

Maintaining Open-Source RxAndroidBle Library (Introduction)

The User, the Developer and the Maintainer

The end of 2015 was the moment when we started to give back to the Open Source what we had learned during several BLE related projects. We have released the RxAndroidBle library that has wrapped a cumbersome native Android BLE API into handy RxJava Observables. This library has proven to dramatically decrease the time needed for implementing the typical usage of this communication channel.

Maintaining a non-trivial Open Source library for an extended amount of time is quite similar to developing an application module in a long-lasting enterprise project:

  • Users of the API may come with little to no domain knowledge needed to work robustly
  • Developers are expected to support the already developed code for a long time
  • Maintainers, who will eventually come, will also lack the domain and codebase knowledge

During the last eighteen months we have tried different techniques of efficiently sharing information with all the interested parties. The following series of posts will be an account of our experiences. We will discuss what have proved to work best so you could have a better start (and waste less time)!

Two words about RxJava (and other Reactive frameworks)

RxJava is a library coming from the family of several other Functional-Reactive frameworks (RxSwift, RxScala, RxJs—just to name a few) centered around Observables. If you have not heard about the Observable pattern, you can read more about it here. The concept of Observables is very similar to streams—a feature introduced in Java 8. Unfortunately, the streams are not available for Android development.

The most important thing from the developer’s point of view is that the Observable pattern allows for an extremely easy orchestration of work—or to describe it better—designing the flow of data.

Imagine that you have to perform a server API call using an access token in your application. If the access token is rejected then you would need to refresh it and retry the operation.

Using old fashioned callbacks it could look similar to the code below:

class CallbackImplementation {
    void performCall() {
        performServerApiCall(getCurrentAccessToken(), response -> {
            // handle the response
        }, initialThrowable -> {
            if (isAccessDenied(initialThrowable)) {
                performTokenRefresh(newAccessToken -> {
                    // retry the initial call with a new access token
                    performServerApiCall(newAccessToken, response -> {
                        // handle the response
                    }, retryThrowable -> {
                        // handle the retried call error
                    });
                }, refreshThrowable -> {
                    // handle the refresh throwable
                });
            } else {
                // handle the initial throwable
            }
      });
    }
}

As the first example shows, in order to understand the flow, the reader needs to hop several times back and forth to grasp all the relevant information. Whereas when using the RxJava it could look like this (assuming that both performServerApiCall() and performTokenRefresh() are returning Observables):

class RxJavaImplementation {
    void performCall() {
        Observable.defer(() -> performServerApiCall(getCurrentAccessToken()))
            .retryWhen(throwableObservable -> throwableObservable.take(1).flatMap(throwable -> {
                if (isAccessDenied(throwable)) {
                    return performTokenRefresh();
                } else {
                    return Observable.error(throwable);
                }
            }))
            .subscribe(response -> {
                // handle the response
            }, throwable -> {
                // handle the throwable
            });
    }
}

The second fragment of the code has less indents and is quite easy to comprehend thanks to the linear relation between actions. Of course, the above-mentioned fragment could be easier to read thanks to extracting the .retryWhen() parameter and naming it for instance tokenIsRefreshedAfterFirstAccessDenied.

As the complexity grows, the advantage of Reactive-Functional approach is getting more and more visible.

Perspective: the User

When it comes to learning a new library / framework there are two types of users:

  • those who start with reading the documentation
  • those who start with using the API

To be honest, most of the developers I know tend not to dive deep into the documentation when learning the new framework. Usually, when new users approach a library, it is because they want to save the time on learning how to properly use a new API.

rx_screen.png

The question is: how to make it easier for them?

During the development of the Bluetooth libraries we have tested some of the approaches that were presented during conferences all around the world.

A right abstraction

Users who approach the new topic usually try to create a mental model that will fit well into their minds. When creating an RxJava library you sometimes need to change the way you think about a problem to get the best interaction possible.

Raw Android BLE API abstraction looks like this:

  • BluetoothAdapter—the main entry point for all Bluetooth interactions. This is the place to check if the specific Android device is Bluetooth-capable and able to start scanning for nearby Bluetooth devices.
  • BluetoothDevice—an abstraction of a Bluetooth device to connect to.
  • BluetoothGatt—the attribute table of a specific Bluetooth device. GATT stands for Generic Attribute Profile and may contain different services (a.k.a functionalities) which in turn consist of characteristics which may be considered “shelves” for actual data. But let’s not dig into this too deep—this is all we need to know right now.

One of the issues of the native API is that it is inherently stateful. When trying to use the device, one need to scan it first. But if, for some reason, the Bluetooth adapter (radio) gets disabled during that time, the user will not get any information about it, unless they know what they should look for. Similar problems appear during establishing a connection to an already scanned device. When the radio goes off, there will be no information where one would expect it to be—no callback will get called. Even when a connection is already established and the user tries to read or write a particular characteristic still if the radio gets turned off—the error callback does not get called. The same happens if the connection gets broken in the middle of a read operation.

It was obvious that to make the native API work in a user-friendly manner, there is a need to wrap it and route all potential errors to a place where the user expects them to be reported. At the beginning of our work on theRxAndroidBle, we made 3 attempts to design the right abstraction to interact with.

Stay tuned for the next blogpost on designing the abstraction!

Share

Darek

Staff Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!