Reactive programming allows us to develop flexible, independent, and scalable solutions — if we choose the right tools. In this article, we explore whether Java or Kotlin is more suitable for writing asynchronous code for Android and Java Virtual Machine (JVM) applications. We explain the pros and cons of these languages and compare their effectiveness for reactive programming with ReactiveX.
This article will be useful for developers who want to improve their reactive programming skills and choose the best toolset for Android development.
Reactive programming allows developers to create responsive, flexible, sustainable, and message-driven applications. This type of programming relies on asynchronous data streams that can contain all sorts of static and dynamic data: variables, user input, cache data, data structures, etc. With reactive programming, if data in a stream changes, all elements that use that stream will take those changes into account.
Reactive programming is especially useful for:
- large development teams where developers write functionalities independently
- projects that contain massive volumes of data
- multi-user projects
- artificial intelligence and machine learning algorithms.
When using reactive programming principles to create Android applications, developers usually choose between Java and Kotlin. Let’s see why these languages are so attractive.
Java is an object-oriented programming language created more than 20 years ago that was the go-to choice for decades. Until recently, it also was the only possible option for Android development. Most of Java’s success can be explained by its core principles:
- Simplicity, which comes from a lack of low-level facilities and automatic memory management
- A robust toolset in the form of libraries and frameworks that make it easy and quick to develop new programs
- Portability between JVMs, as a compiled Java program doesn’t depend on computer hardware and can be executed in any JVM environment
- High performance due to features like just-in-time compilation and generational garbage collection
On the other hand, being an old C-like language, Java has several drawbacks when used for reactive programming:
- Lack of clarity. Even though Java offers rudimentary type inference when declaring local variables or creating templated values, this isn’t enough to keep clear the reactive streams that operate on a rich domain of complex data.
- Weak modeling options when compared to Kotlin.
- Requires a lot of boilerplate code to model concepts that Kotlin offers out of the box.
- Requires the RxJava library, which extends Java’s capabilities for composing asynchronous and event-based programs for JVMs. The Java Development Kit (JDK) 8 has several features for reactive programming, but they aren’t enough for comfortable development.
Kotlin is a statically typed programming language developed for the JVM. It provides developers with ways of writing more concise, robust, and fun code compared to Java. This language became popular because of characteristics like:
- Java interoperability, allowing developers to call Java functions from Kotlin code. The .kt files containing Kotlin source code can coexist with .java files in one project and compile just fine.
- Null safety, which eliminates null references. In Kotlin, any variable is non-nullable by default, but you can assign the
- Сonvenient features such as type inference, concise and expressive syntax, and a rich built-in library of utility functions make development faster and easier.
But as a relatively new language, Kotlin also has several disadvantages:
- Slower compilation compared to Java.
- Smaller developer community, which can be an issue when you need to find a solution for a rare issue. Kotlin documentation, however comprehensive, doesn’t contain answers to all questions.
- Synchronization-related bugs may appear if you use both dedicated APIs for reactive programming (such as ReactiveX) and built-in coroutines for asynchronous programming.
Kotlin can also use RxJava for reactive programming. There’s a dedicated RxKotlin library, but it consists mostly of additional functions like subscribeBy. Core reactive functions for reactive programming can be called from RxJava. Also, Kotlin has built-in functionality for asynchronous programming.
Thanks to the abovementioned advantages, Kotlin has quickly become a popular language. In 2019, Google even declared it a preferred language for Android development. But is it really that good? Check out our function-to-function comparison of Java and Kotlin to find out.
Now, let’s see how ReactiveX works and how it can help you write applications in Java or Kotlin.
ReactiveX is an API for asynchronous programming with observable streams. It provides developers with all the tools needed for reactive programming, transforming the information within a program into data streams. Data-carrying messages (or events) in these streams originate from various Observable (or Flowable) objects and are consumed by Subscriber objects.
The ReactiveX API provides multiple tools for controlling event streams and transforming the data that moves through these streams from Observables to Subscribers. Data carried by events can be transformed by the map function before it reaches the subscriber. It’s also possible to combine or separate event streams using functions like zip and merge.
These transformations, as well as the actions performed by Subscribers, can be carried out asynchronously thanks to a variety of Schedulers. You can use Schedulers to make sure that code will be safely executed in the corresponding thread pool.
ReactiveX tools also allow dependencies between objects to be looser by implementing an Event Bus — a boilerplate publisher/subscriber implementation that organizes communication between program components in such a way that they can react to any event in the program without being dependent on other components. This instrument is especially useful when the lifecycles of components are independent.
For example, an Activity of an Android application can gather a user's input, create a
message object, and push it into an Event Bus. This
message will then be processed by whatever is listening to such events. With an Event Bus, the Activity that posted the message may be closed before the message is processed and it won’t affect the processing routine. The result can be posted to the same or to a parallel Event Bus.
ReactiveX is especially helpful for large development teams, as it makes merge conflicts impossible. Developers can all agree on the set of data streams that should exist within the program, define the format for messages, and then implement functionality independently.
Even though ReactiveX is a powerful and useful tool, however, it should be used wisely. Events in ReactiveX have temporal characteristics, meaning that messages come in a particular order and at a certain time. The general rule of thumb is that if data doesn’t have temporal characteristics, it probably should not be passed through reactive streams. For example, a pure function that only takes an input and produces an output without fetching additional data from anywhere should not be augmented with reactive functionality.
A downside of using ReactiveX features is the possibility to introduce unnecessary complexity and bugs into the program. It’s best to use simple function calls whenever possible to avoid synchronization problems and keep code clear and simple.
With this in mind, let’s compare the capabilities of Java and Kotlin with ReactiveX and the RxJava library.
Let's say we want to write a program that receives commands from a remote server and executes them. The commands come in the form of messages received as raw strings and have to be parsed and represented as objects in the program’s data model. To compare the effectiveness of Kotlin and Java for this task, we’ll provide code in both languages.
The implementation seems straightforward. We just have to implement Connection, Parser, and Handler classes. The Connection class will receive the raw messages, parse them using the Parser's functionality, and then pass them to the Handler, which will do the actual work.
But how flexible will this system be? What if the connection type changes? Or the program has to receive commands and handle messages from multiple connections? To accommodate these scenarios, the set of commands may expand, and the Handler class will have to be split into multiple Handlers, thus bloating all the Connection classes.
A good way of organizing communication between the Connection and Handler implementations in this case is to use an Event Bus.
Messages constitute a well-defined closed set. They can be represented in the form of a tagged union — a data structure that allows for creating a closed set of options that a value of that type can have. In Kotlin, we can implement commands in the form of messages using sealed classes:
Any class can inherit anything from a sealed class — even another sealed class. The only limitation is that all the classes have to be located in the same file. This allows the compiler and developers to know exactly how many possible outcomes there are and to handle them accordingly.
In Java, we can try to achieve the same result by somewhat abusing the visibility modifiers and doing the compiler’s work:
It’s impossible to inherit from the Command class outside the file it’s declared in. But the code above still isn’t going to accomplish everything that the Kotlin example does. For example, we can’t use Java's
switch statement the same way as Kotlin's
when expression to match and handle all possible variants. In Kotlin, handling will look like this:
In this case, Kotlin's when command isn’t only syntactic sugar but a way of verifying that all cases are handled. If they aren’t handled, the program won’t even compile.
To do this in Java, we’ll have to use cascades of if statements and verbosely check the type of command instance. For example:
Having defined the set of commands, we can create an Event Bus to pass these messages through. The
bus will be implemented as a wrapper over the PublishSubject class object, which can be both an Observable and a Subscriber. The wrapper class is going to provide two methods: sink for posting messages and source for receiving them.
Bus object is going to be used by multiple classes at the same time, we have to make sure they’re using the same
Bus object by creating either a static or a singleton class. In Kotlin, we can use the object keyword to easily declare singleton classes:
The Java implementation is a little more verbose:
The code snippets above may seem equivalent, but the Java code has a flaw. If
null is passed to the sink method, the program will crash because RxJava methods don’t allow
null values in their arguments. In Kotlin, all types are non-nullable by default if not specified otherwise. Consider the following lines:
These two variables have distinct types:
Command?. It’s impossible to assign the
Command? value to a variable of the
Command type because
Command? may not contain anything. The compiler guarantees that all values of the
Command type are well defined and can be used safely. And this is exactly why the Kotlin implementation of the
Bus object is completely safe. Implementing it in Java requires additional null checks.
After fixing issues with
null values, we’re done defining the in-program communication interface.
As we mentioned earlier, the implementation of an application can be easily parallelized between developers because Connections only post parsed messages to the
Bus object and Handlers only read messages from it.
In Kotlin, implementing the Connection class looks like this:
Parser is a function that can convert a raw string into an instance of
Command?. We use Kotlin's ?. syntax on the parser's result to filter the non-null values and post them to the
Bus object. If the parser fails to convert the received string, it will return
null. This case will be handled by the code following the so-called Elvis operator (
Compare this to the Java implementation:
Now we need to implement Handlers. Our implementations in Kotlin and Java do the same thing: subscribe to the
Bus, filter the objects they can handle, and then process the values that pass through the filter. In Kotlin, adding Handlers looks like this:
Here’s the same implementation in Java:
To achieve this behavior conveniently and concisely, we used the Flowable.filter method in both examples above. It takes a predicate function as an argument and applies it to all the values received from upstream. A value is passed downstream only if its predicate is evaluated as
true. Then we use the Flowable.subscribe method to add a custom callback to the upstream Flowable object. Flowable.subscribe also takes a function as an argument. This function is invoked whenever an event reaches the subscriber. The event's value is passed to the function as an argument.
With that, our program is finished both in Kotlin and Java. We have implemented a system where each component depends only on the communication interface — the
Bus object. All it takes to extend such a system is to implement the required functionality and configure its relationships with the
Comparing the implementations in both languages, we can see that the one written in Kotlin has several advantages:
- It requires less code (and, therefore, less time) to implement the needed functionality.
- There’s no need to perform null checks.
- There’s explicit support for the functional programming paradigm, which means there are tagged unions, first-class functions, algebraic types, and more.
- Out-of-the-box data classes require next to no effort to define the program’s object domain.
- Built-in support for singleton classes makes it easy to share functionality and states between different parts of the program.
Reactive programming is becoming more and more popular in various areas of development, including in Android development. This approach allows for managing vast amounts of data, reducing complicated passages of code to a couple of lines, creating independent modules, and more. But in order to experience reactive programming in its full glory, you need to choose the right programming language.
Modern languages like Kotlin offer developers the best tools borrowed from different programming paradigms. Rust, Golang, Swift, and Kotlin each combine the flexibility of the traditional imperative style of programming with the robustness and expressiveness of the functional paradigm.
Even though Java is undergoing a modernization process, it’s still far behind Kotlin in terms of flexibility. Moreover, using Java for Android development deprives developers of lots of modern features, since Android supports only JDK 7 along with some JDK 8 features. And features for reactive programming are only supported in JDK 8.
At Apriorit, we have an expert team of Android developers that can create solutions of any complexity written in Kotlin, Java, and many other programming languages. Contact us if you have a challenging task in mind!