Kotlin is a modern programming language that compiles to Java bytecode. It is free and open source, and promises to make coding for Android even more fun.
In the previous article, you learned about classes and objects in Kotlin. In this tutorial, we'll continue to learn more about properties and also look into advanced types of classes in Kotlin by exploring the following:
- late-initialized properties
- inline properties
- extension properties
- data, enum, nested, and sealed classes
1. Late-Initialized Properties
We can declare a non-null property in Kotlin as late-initialized. This means that a non-null property won't be initialized at declaration time with a value—actual initialization won't happen via any constructor—but instead, it will be late initialized by a method or dependency injection.
Let's look at an example to understand this unique property modifier.
class Presenter { private var repository: Repository? = null fun initRepository(repo: Repository): Unit { this.repository = repo } } class Repository { fun saveAmount(amount: Double) {} }
In the code above, we declared a mutable nullable repository
property which is of type Repository
—inside the class Presenter
—and we then initialized this property to null during declaration. We have a method initRepository()
in the Presenter
class that reinitializes this property later with an actual Repository
instance. Note that this property can also be assigned a value using a dependency injector like Dagger.
Now for us to invoke methods or properties on this repository
property, we have to do a null check or use the safe call operator. Why? Because the repository
property is of nullable type (Repository?
). (If you need a refresher on nullability in Kotlin, kindly visit Nullability, Loops, and Conditions).
// Inside Presenter class fun save(amount: Double) { repository?.saveAmount(amount) }
To avoid having to do null checks every time we need to invoke a property's method, we can mark that property with the lateinit
modifier—this means we have declared that property (which is an instance of another class) as late-initialized (meaning the property will be initialized later).
class Presenter { private lateinit var repository: Repository //... }
Now, as long as we wait until the property has been given a value, we're safe to access the property's methods without doing any null checks. The property initialization can happen either in a setter method or through dependency injection.
repository.saveAmount(amount)
Note that if we try to access methods of the property before it has been initialized, we will get a kotlin.UninitializedPropertyAccessException
instead of a NullPointerException
. In this case, the exception message will be "lateinit property repository has not been initialized".
Note also the following restrictions placed when delaying a property initialization with lateinit
:
- It must be mutable (declared with
var
). - The property type cannot be a primitive type—for example,
Int
,Double
,Float
, and so on. - The property cannot have a custom getter or setter.
2. Inline Properties
In Advanced Functions, I introduced the inline
modifier for higher-order functions—this helps optimize any higher-order functions that accept a lambda as a parameter.
In Kotlin, we can also use this inline
modifier on properties. Using this modifier will optimize access to the property.
Let's see a practical example.
class Student { val nickName: String get() { println("Nick name retrieved") return "koloCoder" } } fun main(args: Array<String>) { val student = Student() print(student.nickName) }
In the code above, we have a normal property, nickName
, that doesn't have the inline
modifier. If we decompile the code snippet, using the Show Kotlin Bytecode feature (if you're in IntelliJ IDEA or Android Studio, use Tools > Kotlin > Show Kotlin Bytecode), we'll see the following Java code:
public final class Student { @NotNull public final String getNickName() { String var1 = "Nick name retrieved"; System.out.println(var1); return "koloCoder"; } } public final class InlineFunctionKt { public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); Student student = new Student(); String var2 = student.getNickName(); System.out.print(var2); } }
In the generated Java code above (some elements of the generated code were removed for brevity's sake), you can see that inside the main()
method the compiler created a Student
object, called the getNickName()
method, and then printed its return value.
Let's now specify the property as inline
instead, and compare the generated bytecode.
// ... inline val nickName: String // ...
We just insert the inline
modifier before the variable modifier: var
or val
. Here's the bytecode generated for this inline property:
// ... public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); Student student = new Student(); String var3 = "Nick name retrieved"; System.out.println(var3); String var2 = "koloCoder"; System.out.print(var2); } // ...
Again some code was removed, but the key thing to note is the main()
method. The compiler has copied the property get()
function body and pasted it into the call site (this mechanism is similar to inline functions).
Our code has been optimized because of no need to create an object and call the property getter method. But, as discussed in the inline functions post, we would have a larger bytecode than before—so use with caution.
Note also that this mechanism will work for properties that don't have a backing field (remember, a backing field is just a field that is used by properties when you want to modify or use that field data).
3. Extension Properties
In Advanced Functions I also discussed extension functions—these give us the ability to extend a class with new functionality without having to inherit from that class. Kotlin also provides a similar mechanism for properties, called extension properties.
val String.upperCaseFirstLetter: String get() = this.substring(0, 1).toUpperCase().plus(this.substring(1))
In the Advanced Functions post we defined a uppercaseFirstLetter()
extension function with receiver type String
. Here, we've converted it into a top-level extension property instead. Note that you have to define a getter method on your property for this to work.
So with this new knowledge about extension properties, you'll know that if you ever wished that a class should have a property that was not available, you are free to create an extension property of that class.
4. Data Classes
Let's start off with a typical Java class or POJO (Plain Old Java Object).
public class BlogPost { private final String title; private final URI url; private final String description; private final Date publishDate; //.. constructor not included for brevity's sake @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BlogPost blogPost = (BlogPost) o; if (title != null ? !title.equals(blogPost.title) : blogPost.title != null) return false; if (url != null ? !url.equals(blogPost.url) : blogPost.url != null) return false; if (description != null ? !description.equals(blogPost.description) : blogPost.description != null) return false; return publishDate != null ? publishDate.equals(blogPost.publishDate) : blogPost.publishDate == null; } @Override public int hashCode() { int result = title != null ? title.hashCode() : 0; result = 31 * result + (url != null ? url.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (publishDate != null ? publishDate.hashCode() : 0); return result; } @Override public String toString() { return "BlogPost{" + "title='" + title + '\'' + ", url=" + url + ", description='" + description + '\'' + ", publishDate=" + publishDate + '}'; } //.. setters and getters also ignored for brevity's sake }
As you can see, we need to explicitly code the class property accessors: the getter and setter, as well as hashcode
, equals
, and toString
methods (though IntelliJ IDEA, Android Studio, or the AutoValue library can help us generate them). We see this kind of boilerplate code mostly in the data layer of a typical Java project. (I removed the field accessors and constructor for brevity's sake).
The cool thing is that the Kotlin team provided us with the data
modifier for classes to eliminate writing these boilerplate.
Let's now write the preceding code in Kotlin instead.
data class BlogPost(var title: String, var url: URI, var description: String, var publishDate: Date)
Awesome! We just specify the data
modifier before the class
keyword to create a data class—just like what we did in our BlogPost
Kotlin class above. Now the equals
, hashcode
, toString
, copy
, and multiple component methods will be created under the hood for us. Note that a data class can extend other classes (this is a new feature of Kotlin 1.1).
The equals
Method
This method compares two objects for equality, and returns true if they're equal or false otherwise. In other words, it compares if the two class instances contain the same data.
student.equals(student3) // using the == in Kotlin student == student3 // same as using equals()
In Kotlin, using the equality operator ==
will call the equals
method behind the scenes.
The hashCode
Method
This method returns an integer value used for fast storage and retrieval of data stored in a hash-based collection data structure, for example in the HashMap
and HashSet
collection types.
The toString
Method
This method returns a String
representation of an object.
data class Person(var firstName: String, var lastName: String) val person = Person("Chike", "Mgbemena") println(person) // prints "Person(firstName=Chike, lastName=Mgbemena)"
By just calling the class instance, we get a string object returned to us—Kotlin calls the object toString()
under the hood for us. But if we don't put the data
keyword in, see what our object string representation would be:
com.chike.kotlin.classes.Person@2f0e140b
Much less informative!
The copy
Method
This method allows us to create a new instance of an object with all the same property values. In other words, it creates a copy of the object.
val person1 = Person("Chike", "Mgbemena") println(person1) // Person(firstName=Chike, lastName=Mgbemena) val person2 = person1.copy() println(person2) // Person(firstName=Chike, lastName=Mgbemena)
One cool thing about the copy
method in Kotlin is the ability to change properties during copying.
val person3 = person1.copy(lastName = "Onu") println(person3) //Person3(firstName=Chike, lastName=Onu)
If you're a Java coder, this method is similar to the clone()
method you are already familiar with. But the Kotlin copy
method has more powerful features.
Destructive Declaration
In the Person
class, we also have two methods auto-generated for us by the compiler because of the data
keyword placed in the class. These two methods are prefixed with "component", then followed by a number suffix: component1()
, component2()
. Each of these methods represents the individual properties of the type. Note that the suffix corresponds to the order of the properties declared in the primary constructor.
So, in our example calling component1()
will return the first name, and calling component2()
will return the last name.
println(person3.component1()) // Chike println(person3.component2()) // Onu
Calling the properties using this style is difficult to understand and read, though, so calling the property name explicitly is much better. However, these implicitly created properties do have a very useful purpose: they let us do a destructuring declaration, in which we can assign each component to a local variable.
val (firstName, lastName) = Person("Angelina", "Jolie") println(firstName + " " + lastName) // Angelina Jolie
What we have done here is to directly assign the first and second properties (firstName
and lastName
) of the Person
type to the variables firstName
and lastName
respectively. I also discussed this mechanism knows as destructuring declaration in the last section of the Packages and Basic Functions post.
5. Nested Classes
In the More Fun With Functions post, I told you that Kotlin has support for local or nested functions—a function that is declared inside another function. Well, Kotlin also similarly supports nested classes—a class created inside another class.
class OuterClass { class NestedClass { fun nestedClassFunc() { } } }
We even call the nested class's public functions as seen below—a nested class in Kotlin is equivalent to a static
nested class in Java. Note that nested classes can't store a reference to their outer class.
val nestedClass = OuterClass.NestedClass() nestedClass.nestedClassFunc()
We're also free to set the nested class as private—this means we can only create an instance of the NestedClass
within the scope of the OuterClass
.
Inner Class
Inner classes, on the other hand, can reference the outer class it was declared in. To create an inner class, we place the inner
keyword before the class
keyword in a nested class.
class OuterClass() { val oCPropt: String = "Yo" inner class InnerClass { fun innerClassFunc() { val outerClass = this@OuterClass print(outerClass.oCPropt) // prints "Yo" } } }
Here we reference the OuterClass
from the InnerClass
by using this@OuterClass
.
6. Enum Classes
An enum type declares a set of constants represented by identifiers. This special kind of class is created by the keyword enum
that is specified before the class
keyword.
enum class Country { NIGERIA, GHANA, CANADA }
To retrieve an enum value based on its name (just like in Java), we do this:
Country.valueOf("NIGERIA")
Or we can use the Kotlin enumValueOf<T>()
helper method to access constants in a generic way:
enumValueOf<Country>("NIGERIA")
Also, we can get all the values (like for a Java enum) like this:
Country.values()
Finally, we can use the Kotlin enumValues<T>()
helper method to get all enum entries in a generic way:
enumValues<Country>()
This returns an array containing the enum entries.
Enum Constructors
Just like a normal class, the enum
type can have its own constructor with properties associated to each enum constant.
enum class Country(val callingCode: Int) { NIGERIA (234), USA (1), GHANA (233) }
In the Country
enum type primary constructor, we defined the immutable property callingCodes
for each enum constant. In each of the constants, we passed an argument to the constructor.
We can then access the constants property like this:
val country = Country.NIGERIA print(country.callingCode) // 234
7. Sealed Classes
A sealed class in Kotlin is an abstract class (you never intend to create objects from it) which other classes can extend. These subclasses are defined inside the sealed class body—in the same file. Because all of these subclasses are defined inside the sealed class body, we can know all the possible subclasses by simply viewing the file.
Let's see a practical example.
// shape.kt sealed class Shape class Circle : Shape() class Triangle : Shape() class Rectangle: Shape()
To declare a class as sealed, we insert the sealed
modifier before the class
modifier in the class declaration header—in our case, we declared the Shape
class as sealed
. A sealed class is incomplete without its subclasses—just like a typical abstract class—so we have to declare the individual subclasses inside the same file (shape.kt in this case). Note that you can't define a subclass of a sealed class from another file.
In our code above, we have specified that the Shape
class can be extended only by the classes Circle
, Triangle
, and Rectangle
.
Sealed classes in Kotlin have the following additional rules:
- We can add the modifier
abstract
to a sealed class, but this is redundant because sealed classes are abstract by default. - Sealed classes cannot have the
open
orfinal
modifier. - We are also free to declare data classes and objects as subclasses to a sealed class (they still need to be declared in the same file).
- Sealed classes are not allowed to have public constructors—their constructors are private by default.
Classes which extend subclasses of a sealed class can be placed either in the same file or another file. The sealed class subclass has to be marked with the open
modifier (you'll learn more about inheritance in Kotlin in the next post).
// employee.kt sealed class Employee open class Artist : Employee() // musician.kt class Musician : Artist()
A sealed class and its subclasses are really handy in a when
expression. For example:
fun whatIsIt(shape: Shape) = when (shape) { is Circle -> println("A circle") is Triangle -> println("A triangle") is Rectangle -> println("A rectangle") }
Here the compiler is smart to ensure we covered all possible when
cases. That means there is no need to add the else
clause.
If we were to do the following instead:
fun whatIsIt(shape: Shape) = when (shape) { is Circle -> println("A circle") is Triangle -> println("A triangle") }
The code wouldn't compile, because we have not included all possible cases. We'd have the following error:
Kotlin: 'when' expression must be exhaustive, add necessary 'is Rectangle' branch or 'else' branch instead.
So we could either include the is Rectangle
case or include the else
clause to complete the when
expression.
Conclusion
In this tutorial, you learned more about classes in Kotlin. We covered the following about class properties:
- late initialization
- inline properties
- extension properties
Also, you learned about some cool and advanced classes such as data, enum, nested, and sealed classes. In the next tutorial in the Kotlin From Scratch series, you'll be introduced to interfaces and inheritance in Kotlin. See you soon!
To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts!
-
Android SDKConcurrency in RxJava 2
-
Android SDKSending Data With Retrofit 2 HTTP Client for Android
-
Android SDKJava vs. Kotlin: Should You Be Using Kotlin for Android Development?
-
Android SDKHow to Create an Android Chat App Using Firebase
No comments:
Post a Comment