1. Introduction
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.
What you will build
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:
|
What you'll learn
- How to use the Android Studio's Java to Kotlin converter.
- How to write code using Kotlin syntax.
- How to use Kotlin lambda expressions and extension functions to reduce boilerplate code and avoid common errors.
- How to use built-in standard library functions to extend existing Java class functions.
- How Kotlin can help avoid the Billion Dollar Mistake, the
NullPointerException
.
What you'll need
- Latest version of Android Studio
- An Android device or emulator to run the app on
- The sample code (downloaded in the next step)
- Basic knowledge of developing Android apps in the Java language
2. Getting set up
Download the Code
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/android/codelab-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-starter contains the starter app
- Steps 1 and 2 are the introduction and getting started steps, so there are no folders for these ones.
- MyAddressBook-stepN folders contain the app in the finished state after step N.
- The final app can be found in the MyAddressBook-final folder.
App Overview
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.
- Open MyAddressBook-starter in Android Studio.
- Run it.
Contact.java
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.
ContactsActivity.java
The ContactsActivity shows a RecyclerView that displays an ArrayList of Contact objects. You can add data in two ways:
- Pressing the Floating Action Button will open up a New Contact dialog, allowing you to enter contact information.
- You can generate a list of 20 mock contacts quickly by selecting the
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:
- The first and last name fields must not be empty.
- The email field must contain an email address using a valid format.
- Attempting to save an invalid contact will show an error in a Toast message.
Configure Kotlin
Before you can use the Kotlin tools and methods, you must configure Kotlin in your project.
- In Android Studio, select Tools > Kotlin > Configure Kotlin Plugin Updates. In the window select Stable channel and click on Check for updates now. If there are any updates available click Install.
- In Android Studio, select Tools > Kotlin > Configure Kotlin in Project. If a window titled Choose Configurator appears, select Android with Gradle, make sure All modules is selected, and click OK.
Android Studio will add the required dependencies in both the app level and the project level build.gradle files.
- You will be prompted to sync the project with Gradle as the build.gradle files have changed. Once the sync is complete, move to the next step and write some Kotlin code.
3. Kotlin Conversion Basics
Convert the Contact POJO to a Kotlin Data Class
Problems with the Contact class file
The Contact.java class should look pretty familiar, as it contains standard POJO code:
- Private fields, in this case three strings.
- A constructor to initially set those fields.
- Getters for the fields you want to obtain externally, in this case all of the fields.
- Setters for the fields you want to set externally, in this case just the
email
field.
This kind of object can cause some problems to the unwary developer, because it leaves a few questions open:
- Nullability: which of these fields can be
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 passnull
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. - Mutability: which of these fields might change? Because only the email field has a setter defined, the implication is that only that field is mutable.
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.
Use the converter
- Select Contact.java in the Project pane.
- Select Code > Convert Java File to Kotlin File.
- A dialog appears that warns you that there is code in the rest of the project that may require some corrections to work with the conversion (the Contact class is used in the activity). Click OK to make those changes.
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:
- Declares a class called
Contact
. - Declares three properties for the class: two read-only fields for the first and last name, and one mutable variable for the email address.
- The
firstName
andlastName
properties are guaranteed never to benull
, since they are of typeString
. The converter can guess this because the Java code did not have setters or secondary constructors that don't set all three properties. - The email field may be
null
, since it is of typeString?
.
Kotlin Data Classes
- Run the app, and use the
Generate
menu option in the app to create some contacts. If you want to reset the app and clear your contacts, chooseClear
from the Options menu. - In your code, the
generateContacts()
method uses thetoString()
method to log each contact being created. Check the logs to see the output of this statement. The defaulttoString()
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
- In your Contact.kt file, add the
data
keyword after theinternal
visibility modifier.
internal data class Contact (val firstName: String, val lastName: String, var email: String?)
- Run the app again and generate contacts. Check your logs for the much more informative output.
Convert ContactsActivity to Kotlin
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.
- Run the converter on ContactsActivity, using the same process as you did for the Contact.java file (Code > Convert Java File to Kotlin File).
The lateinit keyword
- Note that the conversion process changed the Java member variables such as
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.
- Add the
lateinit
keyword after theprivate
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>
- Remove the
Boolean
property type from themEntryValid
member variable (and the preceding colon), because Kotlin can infer the type from the assignment:
private var mEntryValid = false
- Remove all of the occurrences of the !! operator. You can select all occurrences of an existing selection by pressing selecting Edit > Find> Select all occurrences and pressing the backspace key.
Clean up ContactsActivity
Your activity should now work as expected. There are a few changes left to finish cleaning up the converted activity:
- In the ViewHolder inner class, the
nameLabel
variable is faintly underlined. Select the variable and press on the orange lightbulb and select Join declaration and assignment. - Repeat step 1 for the
emailLabel
variable.
4. Lambdas & Standard Library Extensions
Add lambdas for validation
Kotlin provides support for lambdas, which are functions that are not declared, but passed immediately as an expression. They have the following syntax:
- A lambda expression is always surrounded by curly braces { }.
- An arrow (->) separates the function parameters from the function definition.
- The function's parameters (if any) are declared before the -> . Parameter types may be omitted if they can be inferred.
- The body of the function is defined after ->.
{ 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.
- In the
afterTextChanged()
method, remove the three booleans that check the validity of the three fields. - Create an immutable variable that stores a lambda expression called notEmpty. The lambda expression takes a TextView as a parameter (EditText inherits from TextView) and returns a Boolean:
val notEmpty: (TextView) -> Boolean
- Assign a lambda expression to
notEmpty
that returnstrue
if the TextView'stext
property is not empty using the KotlinisNotEmpty()
method:
val notEmpty: (TextView) -> Boolean = { textView -> textView.text.isNotEmpty() }
- If a lambda expression only has a single parameter, it can be omitted and replaced with the
it
keyword. Remove thetextView
parameter and replace its reference withit
in the lambda body:
val notEmpty: (TextView) -> Boolean = { it.text.isNotEmpty() }
- Create another lambda with the same signature, and assign it to an isEmail variable. This function checks if the TextView's text property matches the email pattern:
val isEmail: (TextView) -> Boolean = { Patterns.EMAIL_ADDRESS .matcher(it.text).matches() }
- In each call to
EditText.setCompoundDrawablesWithIntrinsicBounds()
, remove the deleted boolean inside the if/else statement:
mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null, if () passIcon else failIcon, null)
- Replace it with the appropriate validation lambda expression and pass in the EditText that needs to be validated:
mFirstNameEdit.setCompoundDrawablesWithIntrinsicBounds(null, null, if (notEmpty(mFirstNameEdit)) passIcon else failIcon, null)
- Change the
mEntryValid
boolean to callnotEmpty()
on the first and last name EditTexts, and callisEmail()
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.
Add sorting and reduce boilerplate with standard extension functions
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.
Add sorting
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
.
- Create two new menu items in
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" />
- In the
onOptionsItemSelected()
method ofContactsActivity.kt
, add two more cases to thewhen
statement (similar to theswitch
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 }
- The list will be reordered, so notify the adapter that the dataset has changed, and return true inside the first case of the
onOptionsItemSelected()
method:
{ mContacts.sortBy { it.firstName } mAdapter.notifyDataSetChanged() return true }
- Copy the code to the "Sort by Last Name" case, changing the lambda expression to return the last name of the passed in contact:
{ mContacts.sortBy { it.lastName } mAdapter.notifyDataSetChanged() return true }
- Run the app. You can now sort the contacts by first and last name from the options menu!
Replace for loops with standard library extensions
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.
- In the
loadContacts()
method, set the cursor on thefor
keyword. - Press the orange light bulb to show the quick fix menu and choose Replace with mapTo(){}.
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).
- Inline this call into the return statement, as the contacts variable is not useful anymore:
private fun loadContacts(): ArrayList<Contact> { val contactSet = mPrefs.getStringSet(CONTACT_KEY, HashSet())!! return contactSet.mapTo(ArrayList<Contact>()) { Gson().fromJson(it, Contact::class.java) } }
- Remove the
<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) }
- In the
saveContacts()
method, set the cursor on the underlined for loop definition. - Select the orange bulb icon to show the quick fix menu and choose Replace with map{}.toSet().
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.
5. Custom Extension Functions
EditText validation extension function
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).
- Create a new Kotlin file by selecting your package directory in the project browser and clicking on File > New > Kotlin File/Class. Call it
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.
- In the
Extensions.kt
file, create an Extension function on EditText calledEditText
.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 thenotEmpty
andisEmail
validators you created). This extensions function also returns a Boolean:
internal fun EditText.validateWith (passIcon: Drawable?, failIcon: Drawable?, validator: (TextView) -> Boolean): Boolean { }
- Define the method to return the result of the validator function. You can pass
this
as a parameter to the validator, since the function is an extension on EditText, sothis
is the instance that the method is called on:
internal fun EditText.validateWith (passIcon: Drawable?, failIcon: Drawable?, validator: (TextView) -> Boolean): Boolean { return validator(this) }
- In the
validateWith()
method, callsetCompoundDrawablesWithIntrinsicBounds()
, passing innull
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)
- Back in the
ContactsActivity
, in theafterTextChanged()
method, remove all three calls tosetCompoundDrawablesWithIntrinsicBounds()
. - Change the assignment of the
mEntryValid
variable to callvalidateWith()
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.
- Change the
notEmpty
andisEmail
type declaration to be an extension of TextView and remove the parameter. Remove theit
parameter reference, and use the text property directly:
val notEmpty: TextView.() -> Boolean = { text.isNotEmpty() } val isEmail: TextView.() -> Boolean = { Patterns.EMAIL_ADDRESS.matcher(text).matches() }
- In the
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 thethis
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.
- Add the
inline
keyword to thevalidateWith()
method declaration.
Default Arguments
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.
- In the
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 {
- In the
afterTextChanged()
method, remove the drawable variables and the first two arguments of each call tovalidateWith()
. You must preface remaining argument withvalidator =
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.
6. Congratulations!
What we've covered
- How to use the Android Studio's Java to Kotlin converter.
- How to write code using Kotlin syntax.
- How to use Kotlin lambda expressions and extension functions to reduce boilerplate code and avoid common errors.
- How to use built-in standard library functions to extend existing Java class functions.
Reference
Notable Kotlin syntax changes
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.
Kotlin Properties
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:
- In the
setupRecyclerView()
method, therecyclerView.setAdapter(mAdapter)
method is replaced withrecyclerView.adapter = mAdapter
andviewHolder.getPosition()
withviewHolder.position
. - The EditText
setText()
and,getText()
methods are replaced throughout with thetext
property. ThesetEnabled()
method is replaced with theisEnabled
property. - The size of the
mContacts
list is accessed withmContacts.size
throughout. - You can access one of the items in the
mContacts
list usingmContacts[index]
instead ofmContacts.get(index)
.
Lambda Expressions
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) }
Destructured declarations
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
Where to learn more
- Kotlin Koans interactive tutorials
- Kotlin website tutorial section
- Kotlin reference
- Kotlin and Android