This topic focuses on some of the most useful aspects of the Kotlin language when developing for Android.
Work with fragments
The following sections use Fragment
examples to highlight some of Kotlin's
best features.
Inheritance
You can declare a class in Kotlin with the class
keyword. In the following
example, LoginFragment
is a subclass of Fragment
. You can indicate
inheritance by using the :
operator between the subclass and its parent:
class LoginFragment : Fragment()
In this class declaration, LoginFragment
is responsible for calling the
constructor of its superclass, Fragment
.
Within LoginFragment
, you can override a number of lifecycle callbacks to
respond to state changes in your Fragment
. To override a function, use the
override
keyword, as shown in the following example:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
To reference a function in the parent class, use the super
keyword, as shown
in the following example:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
Nullability and initialization
In the previous examples, some of the parameters in the overridden methods have
types suffixed with a question mark ?
. This indicates that the arguments
passed for these parameters can be null. Be sure to
handle their nullability safely.
In Kotlin, you must initialize an object's properties when declaring the object.
This implies that when you obtain an instance of a class, you can immediately
reference any of its accessible properties. The View
objects in a Fragment
,
however, aren’t ready to be inflated until calling Fragment#onCreateView
, so
you need a way to defer property initialization for a View
.
The lateinit
lets you defer property initialization. When using lateinit
,
you should initialize your property as soon as possible.
The following example demonstrates using lateinit
to assign View
objects in
onViewCreated
:
class LoginFragment : Fragment() {
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var loginButton: Button
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
passwordEditText = view.findViewById(R.id.password_edit_text)
loginButton = view.findViewById(R.id.login_button)
statusTextView = view.findViewById(R.id.status_text_view)
}
...
}
SAM conversion
You can listen for click events in Android by implementing the
OnClickListener
interface. Button
objects contain a setOnClickListener()
function that takes in an implementation of OnClickListener
.
OnClickListener
has a single abstract method, onClick()
, that you must
implement. Because setOnClickListener()
always takes an OnClickListener
as
an argument, and because OnClickListener
always has the same single abstract
method, this implementation can be represented using an anonymous function in
Kotlin. This process is known as
Single Abstract Method conversion,
or SAM conversion.
SAM conversion can make your code considerably cleaner. The following example
shows how to use SAM conversion to implement an OnClickListener
for a
Button
:
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}
The code within the anonymous function passed to setOnClickListener()
executes when a user clicks loginButton
.
Companion objects
Companion objects
provide a mechanism for defining variables or functions that are linked
conceptually to a type but are not tied to a particular object. Companion
objects are similar to using Java's static
keyword for variables and methods.
In the following example, TAG
is a String
constant. You don’t need a unique
instance of the String
for each instance of LoginFragment
, so you should
define it in a companion object:
class LoginFragment : Fragment() {
...
companion object {
private const val TAG = "LoginFragment"
}
}
You could define TAG
at the top level of the file, but the
file might also have a large number of variables, functions, and classes
that are also defined at the top level. Companion objects help to connect
variables, functions, and the class definition without referring to any
particular instance of that class.
Property delegation
When initializing properties, you might repeat some of Android's more common
patterns, such as accessing a ViewModel
within a Fragment
. To avoid excess
duplicate code, you can use Kotlin’s property delegation syntax.
private val viewModel: LoginViewModel by viewModels()
Property delegation provides a common implementation that you can reuse
throughout your app. Android KTX provides some property delegates for you.
viewModels
, for example, retrieves a ViewModel
that is scoped to the
current Fragment
.
Property delegation uses reflection, which adds some performance overhead. The tradeoff is a concise syntax that saves development time.
Nullability
Kotlin provides strict nullability rules that maintain type-safety throughout
your app. In Kotlin, references to objects cannot contain null values by
default. To assign a null value to a variable, you must declare a nullable
variable type by adding ?
to the end of the base type.
As an example, the following expression is illegal in Kotlin. name
is of type
String
and isn't nullable:
val name: String = null
To allow a null value, you must use a nullable String
type, String?
, as
shown in the following example:
val name: String? = null
Interoperability
Kotlin's strict rules make your code safer and more concise. These rules lower
the chances of having a NullPointerException
that would cause your app to
crash. Moreover, they reduce the number of null checks you need to make in your
code.
Often, you must also call into non-Kotlin code when writing an Android app, as most Android APIs are written in the Java programming language.
Nullability is a key area where Java and Kotlin differ in behavior. Java is less strict with nullability syntax.
As an example, the Account
class has a few properties, including a String
property called name
. Java does not have Kotlin’s rules around nullability,
instead relying on optional nullability annotations to explicitly declare
whether you can assign a null value.
Because the Android framework is written primarily in Java, you might run into this scenario when calling into APIs without nullability annotations.
Platform types
If you use Kotlin to reference a unannotated name
member that is defined in a
Java Account
class, the compiler doesn't know whether the String
maps to a
String
or a String?
in Kotlin. This ambiguity is represented via a
platform type, String!
.
String!
has no special meaning to the Kotlin compiler. String!
can represent
either a String
or a String?
, and the compiler lets you assign a value of
either type. Note that you risk throwing a NullPointerException
if you
represent the type as a String
and assign a null value.
To address this issue, you should use nullability annotations whenever you write code in Java. These annotations help both Java and Kotlin developers.
For example, here's the Account
class as it's defined in Java:
public class Account implements Parcelable {
public final String name;
public final String type;
private final @Nullable String accessId;
...
}
One of the member variables, accessId
, is annotated with @Nullable
,
indicating that it can hold a null value. Kotlin would then treat accessId
as a String?
.
To indicate that a variable can never be null, use the @NonNull
annotation:
public class Account implements Parcelable {
public final @NonNull String name;
...
}
In this scenario, name
is considered a non-nullable String
in Kotlin.
Nullability annotations are included in all new Android APIs and many existing Android APIs. Many Java libraries have added nullability annotations to better support both Kotlin and Java developers.
Handling nullability
If you are unsure about a Java type, you should consider it to be nullable.
As an example, the name
member of the Account
class is not annotated, so you
should assume it to be a nullable String
.
If you want to trim name
so that its value does not include leading or
trailing whitespace, you can use Kotlin’s trim
function. You can safely trim a
String?
in a few different ways. One of these ways is to use the not-null
assertion operator, !!
, as shown in the following example:
val account = Account("name", "type")
val accountName = account.name!!.trim()
The !!
operator treats everything on its left-hand side as non-null, so in
this case, you are treating name
as a non-null String
. If the result of the
expression to its left is null, then your app throws a NullPointerException
.
This operator is quick and easy, but it should be used sparingly, as it can
reintroduce instances of NullPointerException
into your code.
A safer choice is to use the safe-call operator, ?.
, as shown in the
following example:
val account = Account("name", "type")
val accountName = account.name?.trim()
Using the safe-call operator, if name
is non-null, then the result of
name?.trim()
is a name value without leading or trailing whitespace. If
name
is null, then the result of name?.trim()
is null
. This means that
your app can never throw a NullPointerException
when executing this statement.
While the safe-call operator saves you from a potential NullPointerException
,
it does pass a null value to the next statement. You can instead handle null
cases immediately by using an Elvis operator (?:
), as shown in the following
example:
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
If the result of the expression on the left-hand side of the Elvis operator is
null, then the value on the right-hand side is assigned to accountName
. This
technique is useful for providing a default value that would otherwise be null.
You can also use the Elvis operator to return from a function early, as shown in the following example:
fun validateAccount(account: Account?) {
val accountName = account?.name?.trim() ?: "Default name"
// account cannot be null beyond this point
account ?: return
...
}
Android API changes
Android APIs are becoming increasingly Kotlin-friendly. Many of Android's
most-common APIs, including AppCompatActivity
and Fragment
, contain
nullability annotations, and certain calls like Fragment#getContext
have
more Kotlin-friendly alternatives.
For example, accessing the Context
of a Fragment
is almost always non-null,
since most of the calls that you make in a Fragment
occur while the Fragment
is attached to an Activity
(a subclass of Context
). That said,
Fragment#getContext
does not always return a non-null value, as there are
scenarios where a Fragment
is not attached to an Activity
. Thus, the return
type of Fragment#getContext
is nullable.
Since the Context
returned from Fragment#getContext
is nullable (and is
annotated as @Nullable), you must treat it as a Context?
in your Kotlin code.
This means applying one of the previously-mentioned operators to address
nullability before accessing its properties and functions. For some of these
scenarios, Android contains alternative APIs that provide this convenience.
Fragment#requireContext
, for example, returns a non-null Context
and throws
an IllegalStateException
if called when a Context
would be null. This way,
you can treat the resulting Context
as non-null without the need for
safe-call operators or workarounds.
Property initialization
Properties in Kotlin are not initialized by default. They must be initialized when their enclosing class is initialized.
You can initialize properties in a few different ways. The following example
shows how to initialize an index
variable by assigning a value to it in the
class declaration:
class LoginFragment : Fragment() {
val index: Int = 12
}
This initialization can also be defined in an initializer block:
class LoginFragment : Fragment() {
val index: Int
init {
index = 12
}
}
In the examples above, index
is initialized when a LoginFragment
is
constructed.
However, you might have some properties that can't be initialized during object
construction. For example, you might want to reference a View
from within a
Fragment
, which means that the layout must be inflated first. Inflation does
not occur when a Fragment
is constructed. Instead, it's inflated when calling
Fragment#onCreateView
.
One way to address this scenario is to declare the view as nullable and initialize it as soon as possible, as shown in the following example:
class LoginFragment : Fragment() {
private var statusTextView: TextView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView?.setText(R.string.auth_failed)
}
}
While this works as expected, you must now manage the nullability of the View
whenever you reference it. A better solution is to use lateinit
for View
initialization, as shown in the following example:
class LoginFragment : Fragment() {
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView.setText(R.string.auth_failed)
}
}
The lateinit
keyword allows you to avoid initializing a property when an
object is constructed. If your property is referenced before being initialized,
Kotlin throws an UninitializedPropertyAccessException
, so be sure to
initialize your property as soon as possible.