Friday, October 13, 2017

Kotlin From Scratch: Advanced Properties and Classes

Kotlin From Scratch: Advanced Properties and Classes

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. 

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).

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).  

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. 

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. 

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:

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.

We just insert the inline modifier before the variable modifier: var or valHere's the bytecode generated for this inline property:

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

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). 

As you can see, we need to explicitly code the class property accessors: the getter and setter, as well as hashcodeequals, 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.

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 equalshashcodetoStringcopy, 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. 

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. 

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: 

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. 

One cool thing about the copy method in Kotlin is the ability to change properties during copying. 

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.

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.

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. 

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. 

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. 

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. 

To retrieve an enum value based on its name (just like in Java), we do this:

Or we can use the Kotlin enumValueOf<T>() helper method to access constants in a generic way:

Also, we can get all the values (like for a Java enum) like this:

Finally, we can use the Kotlin enumValues<T>() helper method to get all enum entries in a generic way:

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. 

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:

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. 

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 CircleTriangle, 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 or final 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). 

A sealed class and its subclasses are really handy in a when expression. For example:

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:

The code wouldn't compile, because we have not included all possible cases. We'd have the following error:

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 SDK
    Concurrency in RxJava 2
    Chike Mgbemena
  • Android SDK
    Sending Data With Retrofit 2 HTTP Client for Android
    Chike Mgbemena
  • Android SDK
    Java vs. Kotlin: Should You Be Using Kotlin for Android Development?
    Jessica Thornsby
  • Android SDK
    How to Create an Android Chat App Using Firebase
    Ashraff Hathibelagal

No comments:

Post a Comment