-
Notifications
You must be signed in to change notification settings - Fork 99
2.) Basic Kotlin Features
If you've heard about Kotlin, you've probably heard litanies about "null safety".
While it's still possible to get NPEs if you aren't paying attention:
-
specifying nullable platform-type as non-nullable
-
invoking anything on an uninitialized lateinit variable
-
using
!!
on a nullable and actually null value
It's definitely true that you can reduce the number of necessary null checks just by restricting input arguments to be non-null, and you can also ditch some nested conditions by using safe-call operator (?.
) and the Elvis-operator (?:
, think if-null-then
).
For example, one could write the following Java code:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<Item> items = null;
public void updateItems(List<Item> items) {
this.items = items;
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_item, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(items.get(position));
}
@Override
public int getItemCount() {
return items == null ? 0 : items.size();
}
}
You could first write the following Kotlin code:
class MyAdapter: RecyclerView.Adapter<MyAdapter.ViewHolder>() {
private var items: List<Item>? = null
fun updateItems(items: List<Item>?) {
this.items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent!!.getContext()).inflate(R.layout.my_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items!!.get(position))
}
override fun getItemCount(): Int {
return items?.size ?: 0
}
}
But do we reeeeeally want to enable setting a null
into this adapter? I think not.
class MyAdapter: RecyclerView.Adapter<MyAdapter.ViewHolder>() {
private var items = listOf<Item>()
fun updateItems(items: List<Item>) {
this.items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_item, parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
In Kotlin, if we do a check against the type of a class, we can invoke functions on it without a need to cast it again with as T
.
However, we should also be aware that this only works if the class is not a nullable mutable variable.
private var realm: Realm? = null
private var realmResults: RealmResults<T>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
realm = Realm.getDefaultInstance()
realmResults = realm.where().findAllAsync() // ERROR
// realmResults = realm?.where()?.findAllAsync() // works but it's ugly
}
Because then we'll get an error: smart-casting is impossible, this value could have changed over time
.
This means that calling realm.where
is not possible, because realm
could have potentially been changed to null by another thread. Even if we know this is not the case, Kotlin won't permit this. We'll have to keep a reference to the non-null instance to use it as a non-null value. Or we can specify the object as lateinit
.
private var realm: Realm? = null
private var realmResults: RealmResults<T>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val realm = Realm.getDefaultInstance()
this.realm = realm
val realmResults = realm.where().findAllAsync() // works!
this.realmResults = realmResults
realmResults.addChangeListener(RealmChangeListener { ... }) // also works!
// note: I need to specify the Java interface explicitly, because
// this method actually has multiple overloads.
}
If we know that a property will be initialized only once (but not by the constructor), then we can set it to be a lateinit var
which means "we guarantee that this will be non-null upon any actual access to it".
Please note that incorrect access results in KotlinUninitializedPropertyAccessException
.
private lateinit var realm: Realm
private lateinit var realmResults: RealmResults<T>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
realm = Realm.getDefaultInstance()
realmResults = realm.where().findAllAsync() // works!
realmResults.addChangeListener(RealmChangeListener { ... }) // also works!
}
While it's been used in previous examples, string interpolation is preferred in Kotlin against string concatenation. We can use $
for this.
val hello: String
get() = "Hello $name, your overlord ${overlord.name} has been expecting you."
What's also really nice is that you can use multi-line escaped strings, which lets you easily add a JSON to your project without littering it with escape characters.
val jsonString = """
{
"hello": "world",
"another": {
"field": "field",
"boom": "boom"
}
}
""".trimIndent()
Have you heard about data classes? They're the most talked about feature in Kotlin for its "easy-to-demonstrate boilerplate reduction".
Technically it's true, although it's also a pain that you need to have at least 1 constructor argument to use it.
Either way, the way it works is that if you specify a class
as data class
, then it will generate an equals
, hashCode
and toString
function automatically (along with copy()
).
data class Dog(
val name: String,
val owner: Person
)
However if you are in a pinch and need a data class
but have no arguments, I tend to use the following trick:
@Parcelize
data class LoginKey(val placeholder: String = ""): Parcelable
This is because while using object
for no-arguments is commonly recommended in place of empty data class, it actually has different behavior (the toString()
won't be consistent across processes), see https://youtrack.jetbrains.com/issue/KT-4107
The when
keyword is a "switch-case statement on steroids".
when
statements are super-powerful, because they can be combined with complex conditions, such as:
-
checking if an int is
in
a range ofx..y
-
checking if a class is of a particular type (sealed class) or enum value
-
*Kotlin 1.3: allows creating a variable within the
when
statement
Here are a few examples:
val value = when(number) {
0,1,2,3,4 -> 1.0
in 5..9 -> 0.75
in 10..14 -> 0.5
in 15..19 -> 0.25
20 -> 0.2
else -> 0
}
or
enum class Colors {
RED,
GREEN,
BLUE
}
val color = Colors.RED
when(color) {
Colors.RED -> {
...
}
else ->
...
}
}
Note/Tip: a when {}
expression is forced by the compiler to be exhaustive only if it is used as part of an assignment.
For this, we can use the following trick:
fun Unit.safe() {}
fun Nothing?.safe() {}
fun Any?.safe() {}
in which case it looks as:
when(color) {
Colors.RED -> {
...
}
else ->
...
}
}.safe()
Control statements in Kotlin (such as when
or return
can be used as part of assignments.
We've already seen when {}
, but there are also other tricks one can do.
For example, combining the ?:
operator with return
.
val name = tryGetName() ?: return // returns if `tryGetName()` returned `null`
In Kotlin, if you feel that a given set of arguments is unclear, you can specify the name at the call-site.
But if you have default arguments, then you can "skip" certain arguments while specifying other given arguments.
@JvmOverloads
fun printStrings(first: String = "Hello", second: String = "World") {
println("$first $second")
}
We can call this in any of the following ways:
printStrings()
printStrings("Goodbye", "my dear")
printStrings(first = "Goodbye", second = "my dear")
printStrings(first = "Goodbye")
printStrings(second = "my dear")
If you have added the @JvmOverloads
notation, this will work in Java too!
This also applies to constructor arguments.
We already had the ability to specify a vararg method in Java as void doSomething(String... values)
.
Kotlin has a new syntax for this, and an operator that is worth knowing about.
fun doSomething(vararg values: String)
The values
can be accessed as any array. However, what's important is that if we are passing these values one by one to another vararg
function, then we must use the spread operator *
.
Here's a real-world example to show what that is like.
fun animateTogether(vararg animators: Animator) = AnimatorSet().apply {
playTogether(*animators)
}
In this case, we can see that the animators
passed to animateTogether
are passed one-by-one to the playTogether
vararg function of AnimatorSet
.
In Java, if you add a method to an existing interface, you will break the existing implementation of this interface.
Not so in Kotlin because you can define a default implementation of either a val/var
property or even a method of the interface.
interface Animal {
fun makeSound() { // default implementation
println("Growls.")
}
val genus: String
get() = "" // default implementation
}
class Dog : Animal {
}
fun main() {
val dog = Dog()
dog.makeSound() // prints "Growls."
dog.genus // ""
}
What's more interesting is that this works even before Java 8 (though you opt-in with the annotation @JvmDefault
).
Instead Kotlin Bytecode > Decompile
shows that the compiler is creating a static class Animal.DefaultImpls
Generics are a bit tricky in Kotlin. In Java, we had T
, ?
, ? extends T
and ? super T
.
But in Kotlin, we have the following scenarios:
fun <T: View> findViewById(view: View, @IdRes idRes: Int) = view.findViewById(idRes) as T
val activityType: Class<out Activity> // similar to `? extends T`
get() = when(this) {
CAR_HEADER -> CarHeader::class.java
CAR_VIEW -> CarView::class.java
DATA_VIEW -> DataView::class.java
}
}
val clazz: Class<*> = SomeClass::class.java
Where we can see that a "raw type" can in some cases be replaced with star-projection (if a generic is needed but Any?
does not work), that we can supply bounds for generic method arguments as MyClass<*>
, and that there is in/out
which is sometimes needed (the compiler generally tells you that it is required).