In 2019, Google stated that Kotlin is the preferred language for Android development. Does that mean Kotlin is going to replace Java for Android development? Actually, itโs already happening. Stack Overflowโs 2019 Developer Survey discovered that Kotlin is among the top four most loved languages.
In this article, we explore the advantages of Kotlin over Java and overview seven Kotlin features that make development faster and more fun. This information will be useful for Android developers who are considering using Kotlin programming language features and want to know more about them.
Contents:
Kotlin vs Java comparison
Since its release in 1996, Java has become one of the most popular languages in the world according to the PopularitY of Programming Language index. But today, Java gets a fair amount of criticism for its design flaws, forced object-oriented programming, and issues with security and performance.
In 2015, JetBrains released Kotlin โ a new open-source object-oriented programming language designed as a better version of Java. Since then, thereโs been an ongoing argument between fans of both languages as to which is better. Letโs try to figure out if Kotlin will completely substitute Java one day and when itโs better to choose one language over the other.
Letโs see why itโs beneficial to use Kotlin:
- Full interoperability with Java. When working with Kotlin, you can freely call Java code and Java interfaces. You can also use Java frameworks, libraries, etc. And if you decide to implement Kotlin in an existing Java project, you donโt need to rework any existing code.
- Intuitive and laconic syntax. Kotlin code is simple and understandable. Developers that have never studied Kotlin can easily conduct a code review. For an experienced developer, learning the syntax of this language takes several hours.
- Backend support. Kotlin has ktor, http4k, and other native frameworks for backend developers. Spring Framework 5.0 and higher also supports Kotlin.
- Compatibility with all Android versions. Being a primary language for Android development, Kotlin is supported by all Android versions, while other languages have some restrictions. For example, most Java 8 APIs are supported only starting from Android Nougat (API level 24).
With such benefits, Kotlin proves to be a promising, vibrant, and rapidly developing programming language. However, Java has several benefits of its own:
- Fast and stable compilation. In clean builds, Java compiles 10โ15% faster than Kotlin. Also, the Java compiler is more stable. However, the compilation speed during partial builds is almost identical.
- Robust ecosystem. Over the years, the Java community has become one of the largest, most supportive, and most well-equipped. Java developers can study thousands of tutorials, guides, and books that can help them solve any issue.
- Flexibility. Java can run anywhere, from a virtual machine to a browser window.
Code speaks louder than words, so letโs compare the following features in Kotlin and Java:
- Data classes
- Null safety
- Extension functions
- Sealed classes
- Higher-order functions and inline modifiers
- Standard functions
- Delegated properties
Looking to develop a custom mobile app?
Turn your idea into a feature-rich mobile application that stands out. Trust Aprioritโs experienced developers to create a seamless, high-performance app for your business.
Function-to-function comparison
Kotlin developers claim their language has unique functionality that makes development easier and faster than with Java. Letโs take a look at Kotlin features and possible alternatives provided by Java.
Check out this comparison from the Kotlin documentation to find out more about Kotlin functions that are not available in Java.
Both languages provide a great number of benefits to developers, but they serve different purposes. If you have a complex application in mind, Java provides you with the knowledge, tools, and speed to develop it. But if you need to build an Android app and have strict time limitations (or simply want to make your life easier), itโs best to go with Kotlin.
Now letโs find out how to use some of Kotlinโs best features and what benefits they provide.
1. Data classes
The main purpose of data classes in Kotlin is to hold data, i.e. your data models, in the domain layer. To illustrate the benefit of Kotlin data classes, letโs first consider the following class written in Java:
public class City {
private String name;
private String country;
private String imageUrl;
private String description;
public City(String name, String country, String imageUrl, String description) {
this.name = name;
this.country = country;
this.imageUrl = imageUrl;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
City city = (City) o;
return Objects.equals(name, city.name) &&
Objects.equals(country, city.country) &&
Objects.equals(imageUrl, city.imageUrl) &&
Objects.equals(description, city.description);
}
@Override
public int hashCode() {
return Objects.hash(name, country, imageUrl, description);
}
@Override
public String toString() {
return "City{" +
"name='" + name + '\'' +
", country='" + country + '\'' +
", imageUrl='" + imageUrl + '\'' +
", description='" + description + '\'' +
'}';
}
Quite a lot of code for a class that holds only four strings, isnโt it? However, you canโt avoid creating such boilerplate code in Java. This is what coding in Java is like. Now letโs look at the implementation of the same class in Kotlin:
data class City (val name: String, val country: String, val imageUrl: String, val description: String)
Thatโs pretty much it; just one line of code! The Kotlin compiler will generate the rest for you. Itโs worth mentioning that weโll get the same functionality as in the Java example and even more! Data classes in Kotlin also make it easier to copy an object while altering some fields and keeping the rest unchanged.
Each data class
object comes with a default copy function that can be used to create exact or modified copies of the original object while keeping it intact:
val london = City(name = "London", country = "England", imageUrl = "https://londonImagePath.com", description = "Long description of London")
val liverpool = london.copy(name = "Liverpool", imageUrl = "https://liverpoolImagePath.com", description = "Long description of Liverpool")
Notice that we didnโt pass the country
value when creating liverpool
, and it will remain unchanged (England
like in the first data class).
While Kotlin offers a simple and convenient way of creating data classes, there are some restrictions you should keep in mind:
- The primary constructor needs to have at least one parameter.
- All primary constructor parameters need to be marked as
val
orvar
. - Data classes cannot be abstract, open, sealed, or inner.
- Data classes donโt play well with inheritance: thereโs no way to implement
equals()
correctly in a hierarchy of non-abstract classes.
Read also
Comparison of Kotlin and Java for Reactive Programming
Discover whether Kotlin or Java is better suited for writing asynchronous code in Android and JVM applications. With our insights, you can enhance your reactive programming skills and choose the best toolset for your projects.
2. Null safety
Unlike Java, Kotlin differentiates nullable and non-nullable types. This is why Kotlin helps you avoid a NullPointerException
during compilation and saves you a lot of nerves.
By default, Kotlin assumes that a variable of any reference type canโt be null
. Letโs try to assign null
as a city
value:
var city: City = City(name = "London", country = "England", imageUrl = "https://londonImagePath.com", description = "Long description of London")
city = null
Such code simply will not compile, and weโll see the message โNull cannot be a value of a non-null type String.โ If you really need to declare a nullable type, you simply need to add a question mark to the type:
var city: City? = null
This code will compile without errors. But now every time you want to access city
, you should check that itโs not null
first:
var city: City? = null
......
var cityName: String? = null
if (city != null) {
cityName = city.name
}
Luckily, Kotlin supports Safe Calls that allow us not to write boilerplate code. So if you want to access the name field in a safe way, you need to add a question mark before the dot:
var city: City? = null
......
var cityName: String? = city?.name
Moreover, you can make this code even safer using the Elvis operator (?:
):
var city: City? = null
......
var cityName: String = city?.name ?: ""
The Elvis operator works like this: If the left side is not null
, then it returns the result; otherwise, it returns the value on the right. Now you have a non-nullable cityName
value which is safer than the previous version.
Kotlin has a way to access the nullable field directly without checks. Thereโs a special syntax for this โ two exclamation marks before the dot. But itโs rather dangerous and should be used wisely because if we use !!
to access a field that doesnโt exist, the program will crash.
Hereโs an example of using such syntax:
var city: City? = null
......
var cityName: String = city!!.name
In this case, if the reference contains null
, the result will contain a NullPointerException
.
Read also
Pentesting Android Applications: Tools and Step-by-Step Instructions
Looking to enhance your mobile security practices? Learn how to improve the security of your Android applications through penetration testing based on Apriorit step-by-step instructions to identify vulnerabilities in your app.
3. Extension functions
At least once, every developer has had to write a utility method for some framework class that they couldnโt extend due to a lack of control. Hereโs an example of the utility method from an Android project:
public static void makeVisibleOrGone(View view, boolean visible) {
view.setVisibility(value ? View.VISIBLE : View.GONE);
}
In this example, the View is instantiated by the framework, so itโs out of our control. The code given above has to be written quite often, which is why itโs wrapped in the makeVisibleOrGone method. With Kotlin, you can call this method in a much simpler manner than in Java.
Like any modern language (and unlike Java), Kotlin supports extension functions. They offer a way of extending the existing functionality of a class without needing to inherit it or use design patterns like Decorator. To create such a function, you need to declare a regular function and prefix its with the receiver type and a dot:
fun View.makeVisibleOrGone(visible: Boolean) {
this.visibility = if (visible) View.VISIBLE else View.GONE
}
Now, you can call the method declared above on every instance of the View class as if it were implemented inside the class by calling
view.makeVisibleOrGone(false)
Letโs take a look at the decompiled Java version of this extension function:
public static final void makeVisibleOrGone(@NotNull View $receiver, @NotNull Boolean visible) {
...
}
And hereโs the key thing about extension functions: theyโre always dispatched statically. They donโt modify classes they extend. The called extension function is determined by the type of expression on which the function is invoked.
Hereโs a code snippet to illustrate this:
open class City (val name: String, val country: String)
class Capital(name: String, country: String) : City(name, country)
fun City.isCapital(): Boolean {
return false
}
fun Capital.isCapital(): Boolean {
return true
}
fun isCapitalCity(city: City): Boolean {
return city.isCapital()
}
.............
println(Capital("London", "Great Britain").isCapital())
The result of this call is false
. Why? Because the extension function being called depends only on the declared type of the city
parameter, which is an instance of the City
class.
4. Sealed classes
A sealed class represents structured and restricted class hierarchies that contain values assigned with only one type from a limited set of types. A sealed class is abstract and canโt be directly instantiated. Itโs similar to the enum class with one exception: subclasses of sealed class may have multiple instances containing states. Letโs look at the difference between these two classes.
Let’s assume we have an Android application with a ModelโViewโViewModel architecture. This application has events (e.g. showing an error, navigating to another screen) in the ViewModel that need to be passed to the View. We can use enum classes to describe instances that represent these events:
public enum CitiesViewEvent {
SHOW_ERROR,
SHOW_ANOTHER_SCREEN
}
class CitiesViewModel {
val viewEvent: SingleLiveEvent<citiesviewevent> = SingleLiveEvent()
........
private fun showErrorMessage() {
viewEvent.value = CitiesViewEvent.SHOW_ERROR
}
}
class CitiesView {
private fun subscribeForModel() {
.....
viewModel.viewEvent.observe(this, Observer { event ->
when (event) {
CitiesViewEvent.SHOW_ERROR -> showErrorSnackbar()
.........
}
})
}
}
While this solution works, itโs rather limited. In most cases, we would want to pass some parameters with the event (e.g. an error message string, some argument for the screen to open). It’s difficult to do this because enums cannot form hierarchies with other classes.
In our case, itโs best to use sealed classes instead. Letโs rewrite the above example using sealed classes instead of enums:
sealed class CitiesViewEvent {
class ShowError(val message: String): CitiesViewEvent()
class ShowCityDetails(val city: City): CitiesViewEvent()
}
class CitiesViewModel {
val viewEvent: SingleLiveEvent<CitiesViewEvent> = SingleLiveEvent()
........
fun onCityClicked(city: City) {
viewEvent.value = CitiesViewEvent.ShowCityDetails(city)
}
private fun showErrorMessage(stringResId: Int) {
viewEvent.value = CitiesViewEvent.ShowError(resources.getString(stringResId))
}
}
class CitiesView {
private fun subscribeForModel() {
.....
viewModel.viewEvent.observe(this, Observer { event ->
when (event) {
is CitiesViewEvent.ShowError -> showSnackbar(event.message)
is CitiesViewEvent.ShowCityDetails -> showCityDetails(event.city)
}
})
}
}
Itโs very convenient to use sealed classes in conjunction with the when
expression because in that case the compiler tracks the is
checks and applies smart casts. So when you access the event variable in the above example, itโs already been cast to the right type.
Related project
Building a Complex Parental Control App for Android
Discover how our team developed a flexible Android parental control app that meets both client needs and end-user expectations. We managed to build a comprehensive internet protection solution for mobile devices that helped our client drive their sales.
5. Higher-order functions and inline modifiers
In contrast to Java, Kotlin has built-in support for higher-order functions. These are functions that take other functions as arguments or return a function. Consequently, Kotlin has a special type that represents a function:
(SomeParameterType) -> ReturnType
(SomeParameterType1, SomeParameterType2) -> ReturnType
(SomeParameterType1, SomeParameterType2, SomeParameterType3) -> ReturnType
......
The closest analog of a function type in Java is a functional interface. There are several ways to obtain an instance of a Kotlin function type. First, you can use a lambda expression. Itโs essentially an undeclared function that passes as an expression, and itโs called with the following code:
val sum = { a: Int, b: Int -> a + b }
Also, you can call for an anonymous function โ a function with an alternative syntax to a lambda expression. But contrary to a lambda expression, an anonymous function specifies the return type. Hereโs how you can call one:
val sum = fun(a: Int, b: Int): Int = a + b
Finally, you can use a callable reference to an existing declaration. For example, we can get a reference of the getSize function of the list of City
instances, store it in a callable variable, and then call that variable as if it were a list method:
val cities = listOf<City>()
val citiesCountFunc = c::size
val citiesCount = citiesCountFunc()
Now letโs take a look at a higher-order function from the Kotlin standard library. Filter is one of the most popular functions for dealing with a collection of objects:
val cities = listOf<city>(City(name = "Liverpool", country = "Great Britain"), Capital(name = "London", country = "Great Britain"))
val predicate = { city: City -> city.isCapital() }
val capitals = cities.filter(predicate)
This function has the following definition:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
The Filter function returns a list of elements that match the given predicate, which, in turn, is also a function. We passed the predicate function into the Filter function so it can be executed later.
You may be wondering what the inline keyword is in the function signature above. The goal of inlining is to reduce the runtime overhead created by lambda expressions and function references. Keep in mind that every function type is implemented as an interface, meaning that every lambda expression will actually be compiled as an anonymous class under the hood.
6. Standard functions: run, apply, with, let, also
While inlining could be a topic for a separate article, letโs look closer at functional programming features in Kotlin. In order to make it easier to understand some of the standard functions, weโll first define them all:
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
The run, apply, with, let, and also functions do similar things: they take the receiver argument and the block of code and then execute the provided block of code on the provided receiver. You can call all of them using the Safe calls we discussed earlier. But itโs obvious that they differ in signature and implementation and were designed to be used in different situations. Letโs take a closer look at each of them.
run
You can use the run function whenever you want to limit the scope of several variables or to compute some value and return it. Hereโs an example of using this function:
..................
intent.extras?.run {
if (containsKey(CITY_EXTRA_KEY)) {
viewModel.init(Parcels.unwrap(getParcelable(CITY_EXTRA_KEY)))
}
}
In this example, if the extras
isnโt null
, the run function executes the code block in lambda on the Bundle
object. Here, containsKey(String key)
and getParcelable(String key)
are Bundle class methods.
apply
The apply function is useful when you arenโt going to access any functions of the receiver within your block and also want to return the same receiver. For example, you can initialize objects with this function:
fun getCallingIntent(activity: Activity, city: City): Intent {
return Intent(activity, CityDetailsActivity::class.java)
.apply { putExtra(CITY_EXTRA_KEY, Parcels.wrap(city)) }
}
In the example above, the apply function puts the City
object in the Intent
extras before returning it.
with
When you need to access receiver properties and arenโt particularly interested in the result, itโs appropriate to use the with function:
with(cities_loading_progressbar) {
if (isLoading) show()
else hide()
}
This function is also useful when you need to introduce a helper object with properties or functions required for calculating a value.
let
The most popular use case of the let function is to execute some code block if a receiver is not null
:
localDataSource.getCities()?.let { emitter.onSuccess(it) }
In our example, the let block will only be executed if the cities
collection is not null
and the variable it
inside lambda is actually a receiver.
also
If you arenโt going to access or mutate receiver parameters, use the also function. This function is very handy when you need to execute side effects on an object or validate its data before assigning it to a property. For example, when you need to swap variables, you can do this:
var a = 1
var b = 2
a = b.also { b = a }
As a result, the values of variables a and b will be swapped.
Read also
Building a Transparent Encryption Framework to Secure Android Applications: A Step-by-Step Guide
Learn how to use transparent encryption in Android apps to protect your sensitive data even on rooted devices. This step-by-step guide walks you through creating a reliable encryption framework, helping to ensure security of your mobile app.
7. Delegated properties
The main idea behind property delegation is that the class owning the property can delegate the property accessor to a delegate object instead of handling it by itself. According to Kotlin documentation, delegating properties is appropriate (but not limited to) the following situations:
- Lazy properties: the value gets computed only upon the first access
- Observable properties: listeners get notified about changes to this property
- Storing properties in a map instead of in a separate field for each property
Letโs look at an example of property delegation:
val viewEvent: SingleLiveEvent<CitiesViewEvent> by lazy { SingleLiveEvent<CitiesViewEvent>() }
fun onCityClicked(city: City) {
viewEvent.value = CitiesViewEvent.ShowCityDetails(city)
}
In this example, the instance of the SingleLiveEvent
will not be instantiated until someone attempts to access the viewEvent
variable. The lazy
delegate is provided by the Kotlin standard library.
But letโs consider another delegation thatโs also quite popular:
val cities: List<City> by Delegates.observable(listOf()) { prop, old, new ->
println("$old -> $new")
}
Each time the cities
property is updated, the code inside the delegateโs lambda will be executed so you can detect that the cities
collection was changed and react accordingly.
Conclusion
Kotlin is a powerful and elegant language that can make the life of Java and Android developers a lot easier. Itโs modern and easy to learn, and the features of Kotlin make development faster and safer. And with the support of Google, this language has all the chances to dominate Android development. At Apriorit, weโve already used Kotlin for mobile development.
Elevate your mobile app strategy!
From concept to launch, weโll help you build a mobile app that meets your unique business needs.