At I/O 2017, Google announced official support for Kotlin for developing Android apps. Kotlin is a language developed by Jetbrains, with a quickly growing developer base because of its concise, elegant syntax, and 100% interoperability with Java.
In this codelab, you will convert an address book application written in the Java language to Kotlin. In doing so, you will see how Kotlin can:
|
NullPointerException
.Click the following link to download all the code for this codelab:
Alternatively, you can also find it in this github repository:
$ git clone https://github.com/googlecodelabs/android-using-kotlin
Unpack the downloaded zip file. The zip file contains a root folder (android-using-kotlin-master
), which includes a folder for each step of this codelab.
MyAddressBook is a simplified address book app that displays a list of contacts in a RecyclerView. You'll be converting all of the Java code into Kotlin, so take a moment to familiarize yourself with the existing code in the MyAddressBook-starter app.
The Contact class is a simple Java class that contains data. It is used to model a single contact entry and contains three fields: first name, last name and email.
The ContactsActivity shows a RecyclerView that displays an ArrayList of Contact objects. You can add data in two ways:
Generate
option in the options menu, which will use an included JSON file to generate Contact objects.Once you have some contacts, you can swipe on a row to delete that contact, or else clear the whole list by selecting Clear in the options menu.
You can also tap on a row to edit that contact entry. In the editing dialog the first and last name fields are disabled, since for this tutorial app, these fields are immutable (only getters are provided in the Contact class), but the email field is modifiable.
The New Contact dialog performs validation in the following ways:
Before you can use the Kotlin tools and methods, you must configure Kotlin in your project.
Android Studio will add the required dependencies in both the app level and the project level build.gradle files.
The Contact.java class should look pretty familiar, as it contains standard POJO code:
email
field.This kind of object can cause some problems to the unwary developer, because it leaves a few questions open:
null
? The fact that the first name and last name can only be set through the constructor and don't have setter methods implies that they are meant to be non nullable, but this is not a guarantee: one could still pass null
into the constructor for one of the fields. The email does have a setter, so for this one there is no way to know whether it should be nullable or not.The fact that the Java language does not force you to consider the potential null
cases, as well as the setting of fields meant to be read-only, can lead to runtime errors such as the dreaded NullPointerException
.
Kotlin helps to solve these problems by forcing the developer to think about them while the class is being written, and not at runtime.
The converter runs and changes the Contact.java file extension to .kt. All of the code for the Contact class reduces to the following single line:
internal class Contact(val firstName: String, val lastName: String, var email: String?)
In summary, this class declaration does the following:
Contact
.firstName
and lastName
properties are guaranteed never to be null
, since they are of type String
. The converter can guess this because the Java code did not have setters or secondary constructors that don't set all three properties.null
, since it is of type String?
. Generate
menu option in the app to create some contacts. If you want to reset the app and clear your contacts, choose Clear
from the Options menu. generateContacts()
method uses the toString()
method to log each contact being created. Check the logs to see the output of this statement. The default toString()
method uses the object's hashcode to identify the object, but it doesn't give you any useful information about the object's contents:generateContacts: com.example.android.myaddressbook.Contact@d293353
data
keyword after the internal
visibility modifier. internal data class Contact (val firstName: String, val lastName: String, var email: String?)
The next step is to convert the ContactsActivity to Kotlin. In this process you will learn about some of the limitations of the converter, and some new Kotlin keywords and syntax.
mContacts
into Kotlin properties. private var mContacts: ArrayList<Contact>? = null
All of these properties are marked as nullable except the boolean, since they are not initialized with any values until onCreate()
is executed and are therefore null
when the activity is created.
This is not ideal, since anytime you want to use a method or set a property, you will have to check if the reference is null first (otherwise the compiler will throw an error to avoid a possible NullPointerException).
In Android apps, you usually initialize member variables in an activity lifecycle method, such as onCreate()
, rather than when the instance is instantiated.
Fortunately, Kotlin has a keyword for precisely this situation: lateinit
. Use this keyword when you know the value of a property will not be null
once it is initialized.
lateinit
keyword after the private
visibility modifier to all of the member variables except the initialized boolean. Remove the null assignment, and change the type to be not nullable by removing the ?
.private lateinit var mContacts: ArrayList<Contact>
Boolean
property type from the mEntryValid
member variable (and the preceding colon), because Kotlin can infer the type from the assignment:private var mEntryValid = false
Your activity should now work as expected. There are a few changes left to finish cleaning up the converted activity:
nameLabel
variable is faintly underlined. Select the variable and press on the orange lightbulb and select Join declaration and assignment.emailLabel
variable.Kotlin provides support for lambdas, which are functions that are not declared, but passed immediately as an expression. They have the following syntax:
{ x: Int, y: Int -> x + y }
You can then store these expressions in a variable and reuse them.
In this step, you will modify the afterTextChanged()
method to use lambda expressions to validate the user input when adding or modifying a contact. This method uses the setCompoundDrawablesWithIntrinsicBounds()
method to set the pass or fail drawable on the right side of the EditText.
You will create two lambda expressions for validating the user input and store them as variables.
afterTextChanged()
method, remove the three booleans that check the validity of the three fields.val notEmpty: (TextView) -> Boolean
notEmpty
that returns true
if the TextView's text
property is not empty using the Kotlin isNotEmpty()
method:val notEmpty: (TextView) -> Boolean = { textView -> textView.text.isNotEmpty() }
it
keyword. Remove the textView
parameter and replace its reference with it
in the lambda body:val notEmpty: (TextView) -> Boolean = { it.text.isNotEmpty() }
val isEmail: (TextView) -> Boolean = { Patterns.EMAIL_ADDRESS .matcher(it.text).matches() }
EditText.setCompoundDrawablesWithIntrinsicBounds()
, remove the deleted boolean inside the if/else statement:mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null, if () passIcon else failIcon, null)
mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null, if (notEmpty(mFirstNameEdit)) passIcon else failIcon, null)
mEntryValid
boolean to call notEmpty()
on the first and last name EditTexts, and call isEmail()
on the email EditText:mEntryValid = notEmpty(mFirstNameEdit) and notEmpty(mLastNameEdit) and isEmail(mEmailEdit)
Although these changes have not reduced the code much, it is possible to see how these validators can be reused. Using lambda expressions becomes even more useful in combination with higher-order functions, which are functions that take other functions as arguments, which will be discussed in the next section.
One of the main features of the Kotlin language is extensions, or the ability to add functionality to any external classes (ones that you didn't create). This helps avoid the need for "utility" classes that wrap unmodifiable Java or Android framework classes. For example, if you wanted to add a method to ArrayList to extract any strings that only had integers, you could add an extension function to ArrayList to do so without creating any subclasses.
The standard Kotlin library includes a number of extensions to commonly used Java classes.
In this step, you'll use standard library extension functions to add a sort option to the options menu, allowing the contacts to be sorted by first and last name.
The Kotlin standard library includes the sortBy()
extension function for mutable lists, including ArrayLists, that takes a "selector" function as a parameter. This is an example of a higher-order function, a function that takes another function as parameter. The role of this passed in function is to define a natural sort order for the list of arbitrary objects. The sortBy()
method iterates through each item of the list it is called on, and performs the selector function on the list item to obtain a value it knows how to sort. Usually, the selector function returns one of the fields of the object that implements the Comparable interface, such as a String
or Int
.
res/menu/menu_contacts.xml
, to sort the contacts by first name and last name:<item android:id="@+id/action_sort_first" android:orderInCategory="100" android:title="Sort by First Name" app:showAsAction="never" /> <item android:id="@+id/action_sort_last" android:orderInCategory="100" android:title="Sort by Last Name" app:showAsAction="never" />
onOptionsItemSelected()
method of ContactsActivity.kt
, add two more cases to the when
statement (similar to the switch
in Java), using the IDs for the cases:R.id.action_sort_first -> {} R.id.action_sort_last -> {}
For R.id.action_sort_first
call the sortBy()
method on the mContacts
list. Pass in a lambda expression that takes a Contact
object as a parameter and returns its first name property. Because the contact is the only parameter, the contact can be referred to as it
:
mContacts.sortBy { it.firstName }
onOptionsItemSelected()
method: { mContacts.sortBy { it.firstName } mAdapter.notifyDataSetChanged() return true }
{ mContacts.sortBy { it.lastName } mAdapter.notifyDataSetChanged() return true }
The Kotlin standard library adds many extension functions to Collections such as Lists, Sets, and Maps, to allow conversion between them. In this step, you'll simplify the save and load contacts methods to use the conversion extensions.
loadContacts()
method, set the cursor on the for
keyword.Android Studio will change the for loop into the mapTo(){}
function, another higher-order function that takes two arguments: the collection to turn the receiver parameter (The class you are extending) into, and a lambda expression that specifies how to convert the items of the set into items of the list. Note the it
notation in the lambda, referring to the single passed in parameter (the item in the list).
private fun loadContacts(): ArrayList<Contact> { val contactSet = mPrefs.getStringSet(CONTACT_KEY, HashSet())!! return contactSet.mapTo(ArrayList<Contact>()) { Gson().fromJson(it, Contact::class.java) } }
<Contact>
parameterization in the first argument, as it can be inferred by the compiler from the lambda in the second argument:return contactSet.mapTo(ArrayList()) { Gson() .fromJson(it, Contact::class.java) }
saveContacts()
method, set the cursor on the underlined for loop definition.Again, Android Studio replaces the loop with an extension function: this time map{}
, which performs the passed in function on each item in the list (Uses GSON to convert it to a string) and then converts the result to a Set using the toSet()
extension method.
The Kotlin standard library is full of extension functions, particularly higher-order ones that add functionality to existing data structures by allowing you to pass in lambda expressions as parameters. You can also create your own higher-order extension functions, as you'll see in the next section.
The afterTextChanged()
method, which validates the values when you add a new contact, is still longer than it needs to be. It has repeated calls to set the validation drawables, and for each call you have to access the EditText instance twice: once to set the drawable and once to check if the input is valid. You also have to check the validation lambda expressions again to set the mEntryValid
boolean.
In this task, you will create an extension function on EditText that performs validation (checks the input and sets the drawable).
Extensions
and make sure the Kind field says File.To create an Extensions function, you use the same syntax as for a regular function, except you preface the function name with the class you wish to extend and a period.
Extensions.kt
file, create an Extension function on EditText called EditText
.validateWith()
. It takes three arguments: a drawable for the case where the input is valid, a drawable for when the input is invalid, and a validator function that takes a TextView as a parameter and returns a Boolean (like the notEmpty
and isEmail
validators you created). This extensions function also returns a Boolean:internal fun EditText.validateWith (passIcon: Drawable?, failIcon: Drawable?, validator: (TextView) -> Boolean): Boolean { }
this
as a parameter to the validator, since the function is an extension on EditText, so this
is the instance that the method is called on:internal fun EditText.validateWith (passIcon: Drawable?, failIcon: Drawable?, validator: (TextView) -> Boolean): Boolean { return validator(this) }
validateWith()
method, call setCompoundDrawablesWithIntrinsicBounds()
, passing in null
for the top, left, and bottom drawables, and using the if/else syntax with the passed in validator function to select either the pass or fail drawable:setCompoundDrawablesWithIntrinsicBounds(null, null, if (validator(this)) passIcon else failIcon, null)
ContactsActivity
, in the afterTextChanged()
method, remove all three calls to setCompoundDrawablesWithIntrinsicBounds()
. mEntryValid
variable to call validateWith()
on all three EditTexts, passing in the pass and fail icons, as well as the appropriate validator:mEntryValid = mFirstNameEdit.validateWith(passIcon, failIcon, notEmpty) and mLastNameEdit.validateWith(passIcon, failIcon, notEmpty) and mEmailEdit.validateWith(passIcon, failIcon, isEmail)
You can take this one step further by changing the type of notEmpty
and isEmail
lambda expression to be extensions of the TextView class, rather than passing in an instance of TextView. This way, inside the lambda expression, you are a member the of TextView instance and can therefore call TextView methods and properties without referencing the instance at all.
notEmpty
and isEmail
type declaration to be an extension of TextView and remove the parameter. Remove the it
parameter reference, and use the text property directly:val notEmpty: TextView.() -> Boolean = { text.isNotEmpty() } val isEmail: TextView.() -> Boolean = { Patterns.EMAIL_ADDRESS.matcher(text).matches() }
validateWith()
method in the Extensions.kt file, make the third parameter (the validator function) extend the TextView type rather than have it passed in, and remove the this
parameter inside the validator method call:internal fun EditText.validateWith(passIcon: Drawable?, failIcon: Drawable?, validator: TextView.() -> Boolean): Boolean { setCompoundDrawablesWithIntrinsicBounds(null, null, if (validator()) passIcon else failIcon, null) return validator() }
When you use a higher-order function, the generated Java bytecode creates an instance of an anonymous class, and calls the passed in function as a member of the anonymous class. This creates performance overhead, as the class needs to be loaded into memory. In the above example, every call to validateWith()
in the activity will create an anonymous inner class that wraps the validator function, and calls it when it is needed.
Most of the time, the main reason for using a higher-order function is to specify a call order or location, as in the above example, where the passed in function must be called to determine which drawable to load and again to determine the return Boolean. To prevent these anonymous class instances from being created, you can use the inline
keyword when defining a higher-order function. In this case, the body of the inlined function gets copied to the location where it is called and no instance is created.
inline
keyword to the validateWith()
method declaration.Kotlin provides the ability to declare default values for parameters of a function. Then, when calling the function, you can omit the parameters that use their default values, and only pass in the values that you choose using the <variableName> = <value>
syntax.
validateWith()
method in the Extensions.kt file, set the pass and fail drawables as defaults for the first two parameters:internal inline fun EditText.validateWith (passIcon: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_pass), failIcon: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_fail), validator: TextView.() -> Boolean): Boolean {
afterTextChanged()
method, remove the drawable variables and the first two arguments of each call to validateWith()
. You must preface remaining argument with validator =
to let the compiler know which argument you are passing in: mEntryValid = mFirstNameEdit.validateWith(validator = notEmpty) and mLastNameEdit.validateWith(validator = notEmpty) and mEmailEdit.validateWith(validator = isEmail)
The afterTextChanged()
method is now much easier to read: it declares two lambda expressions for validation, and passes them in the validateWith()
extension function using the default pass and fail drawables.
The Kotlin converter changes a lot of the code to use Kotlin specific syntax. The following section will point out some of these changes that the converter made to the MyAddressBook app.
In Kotlin, you can access properties of objects directly, using the object.property
syntax, without the need for an access method (getters and setters in Java). You can see this in action in many places throughout the ContactsActivity class:
setupRecyclerView()
method, the recyclerView.setAdapter(mAdapter)
method is replaced with recyclerView.adapter = mAdapter
and viewHolder.getPosition()
with viewHolder.position
.setText()
and, getText()
methods are replaced throughout with the text
property. The setEnabled()
method is replaced with the isEnabled
property.mContacts
list is accessed with mContacts.size
throughout.mContacts
list using mContacts[index]
instead of mContacts.get(index)
.Kotlin supports lambda expressions, which is a function that is not declared before it is used, but can be passed immediately as an expression.
{ a, b -> a.length < b.length }
This is especially useful wherever you would use an anonymous inner class that implements a single abstract method, such as inside a setOnClickListener()
method on a view. In this case, you can pass a lambda expression directly instead of the anonymous object. The converter does this automatically for every OnClickListener
in the ContactsActivity, for example:
fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showAddContactDialog(-1); } });
turns into
fab.setOnClickListener { showAddContactDialog(-1) }
Sometimes it is convenient to destructure an object into a number of variables. For example, it is common in the onBindViewHolder()
method of adapter classes to use the fields of an object to populate the ViewHolder with data.
This is made easier in Kotlin with by using destructured declarations, such as the ones the converter creates automatically in the onBindViewHolder()
declaration. The order of the deconstructed elements the same as in the original class declaration:
val (firstName, lastName, email) = mContacts[position]
You can then use each property to populate the view:
val fullName = "$firstName $lastName" holder.nameLabel.text = fullName holder.emailLabel.text = email