В этой статье мы рассмотрим некоторые из наиболее полезных аспектов языка Kotlin при разработке для Android.
Как работать с фрагментами
В следующих разделах на примерах Fragment показаны некоторые из лучших функций Kotlin.
Наследование
Вы можете объявить класс в Kotlin с помощью ключевого слова class. В примере ниже LoginFragment – это подкласс Fragment. Чтобы указать наследование, используйте оператор : между подклассом и его родительским классом:
class LoginFragment : Fragment()
В этом объявлении класса LoginFragment отвечает за вызов конструктора своего надкласса Fragment.
В классе LoginFragment можно переопределить ряд обратных вызовов жизненного цикла, чтобы реагировать на изменения состояния в Fragment. Чтобы переопределить функцию, используйте ключевое слово override, как показано в следующем примере:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
Чтобы сослаться на функцию в родительском классе, используйте ключевое слово super, как показано в следующем примере:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
Допустимость значения NULL и инициализация
В предыдущих примерах некоторые параметры в переопределенных методах имеют типы с суффиксом в виде вопросительного знака ?. Это указывает на то, что аргументы,
переданные для этих параметров, могут быть нулевыми. Обязательно обрабатывайте допустимость значения NULL безопасным образом.
В Kotlin вы должны инициализировать свойства объекта при его объявлении.
Это означает, что при получении экземпляра класса вы можете сразу же ссылаться на любое из его доступных свойств. Однако объекты View в Fragment не готовы к заполнению до вызова Fragment#onCreateView, поэтому вам нужно отложить инициализацию свойств для View.
lateinit позволяет отложить инициализацию свойства. При использовании lateinit инициализируйте ресурс как можно раньше.
В следующем примере показано, как использовать lateinit для назначения объектов View в 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
Чтобы отслеживать события кликов в Android, реализуйте интерфейс OnClickListener. Объекты Button содержат функцию setOnClickListener(), которая принимает реализацию OnClickListener.
OnClickListener содержит один абстрактный метод onClick(), который необходимо реализовать. Поскольку setOnClickListener() всегда принимает OnClickListener в качестве аргумента, а OnClickListener всегда имеет один и тот же абстрактный метод, эту реализацию можно представить с помощью анонимной функции в Kotlin. Этот процесс называется преобразованием единого абстрактного метода, или преобразованием SAM.
Преобразование SAM может сделать ваш код значительно чище. В примере ниже показано, как использовать преобразование SAM, чтобы реализовать OnClickListener для 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)
}
}
Код в анонимной функции, переданной в setOnClickListener(), выполняется, когда пользователь нажимает loginButton.
Сопутствующие объекты
Объекты-компаньоны предоставляют механизм для определения переменных или функций, которые концептуально связаны с типом, но не привязаны к конкретному объекту. Объекты-компаньоны похожи на использование ключевого слова static в Java для переменных и методов.
В примере ниже TAG – это константа String. Вам не нужен уникальный экземпляр String для каждого экземпляра LoginFragment, поэтому его следует определить в сопутствующем объекте:
class LoginFragment : Fragment() {
...
companion object {
private const val TAG = "LoginFragment"
}
}
Вы можете определить TAG на верхнем уровне файла, но в файле также может быть большое количество переменных, функций и классов, которые также определены на верхнем уровне. Объекты-компаньоны помогают связывать переменные, функции и определение класса без ссылки на какой-либо конкретный экземпляр этого класса.
Делегирование ресурса
При инициализации свойств вы можете повторять некоторые из наиболее распространенных шаблонов Android, например доступ к ViewModel в Fragment. Чтобы избежать избыточного дублирования кода, можно использовать синтаксис делегирования свойств Kotlin.
private val viewModel: LoginViewModel by viewModels()
Делегирование свойств позволяет использовать одну и ту же реализацию в разных частях приложения. В Android KTX есть несколько делегатов свойств.
Например, viewModels возвращает ViewModel, относящийся к текущему Fragment.
Делегирование свойств использует рефлексию, что добавляет некоторые накладные расходы на производительность. Однако это компенсируется лаконичным синтаксисом, который экономит время разработки.
Допускает значение NULL
В Kotlin действуют строгие правила допустимости значения NULL, которые обеспечивают типобезопасность в приложении. По умолчанию ссылки на объекты в Kotlin не могут содержать значения NULL. Чтобы присвоить переменной значение NULL, необходимо объявить тип переменной, допускающий значение NULL, добавив ? в конец базового типа.
Например, следующее выражение недопустимо в Kotlin. name имеет тип String и не может иметь значение null:
val name: String = null
Чтобы разрешить значение null, необходимо использовать тип String, допускающий значение null, String?, как показано в следующем примере:
val name: String? = null
Совместимость
Строгие правила Kotlin делают код более безопасным и лаконичным. Эти правила снижают вероятность возникновения NullPointerException, которая может привести к сбою приложения. Кроме того, они уменьшают количество проверок на null, которые вам нужно сделать в коде.
Часто при написании приложения для Android вам также приходится вызывать код, написанный не на Kotlin, поскольку большинство API Android написаны на языке программирования Java.
Допустимость значения NULL – это ключевая область, в которой поведение Java и Kotlin различается. В Java синтаксис допустимости значения NULL менее строгий.
Например, у класса Account есть несколько свойств, в том числе свойство String с названием name. В Java нет правил Kotlin в отношении допустимости значения NULL. Вместо этого используются необязательные аннотации о допустимости значения NULL, которые явно указывают, можно ли присвоить значение NULL.
Поскольку фреймворк Android написан в основном на Java, вы можете столкнуться с этим сценарием при вызове API без аннотаций допустимости значения NULL.
Типы платформ
Если вы используете Kotlin для ссылки на элемент name без аннотации, определенный в классе Java Account, компилятор не знает, сопоставляется ли String с String или String? в Kotlin. Эта неоднозначность представлена в виде типа платформы String!.
String! не имеет особого значения для компилятора Kotlin. String! может представлять собой String или String?, и компилятор позволяет присвоить значение любого типа. Обратите внимание, что если вы представите тип как String и назначите нулевое значение, то рискуете получить ошибку NullPointerException.
Чтобы решить эту проблему, используйте аннотации допустимости значения NULL при написании кода на Java. Эти аннотации помогают разработчикам, использующим как Java, так и Kotlin.
Например, вот как класс Account определяется в Java:
public class Account implements Parcelable {
public final String name;
public final String type;
private final @Nullable String accessId;
...
}
Одна из переменных-членов, accessId, аннотирована с помощью @Nullable, что указывает на то, что она может содержать нулевое значение. Kotlin будет обрабатывать accessId как String?.
Чтобы указать, что переменная никогда не может быть нулевой, используйте аннотацию @NonNull:
public class Account implements Parcelable {
public final @NonNull String name;
...
}
В этом случае name считается типом String, не допускающим значения NULL, в Kotlin.
Аннотации допустимости значения NULL включены во все новые и многие существующие API Android. Во многие библиотеки Java были добавлены аннотации о допустимости значения NULL, чтобы улучшить поддержку разработчиков, использующих Kotlin и Java.
Обработка допустимости значения NULL
Если вы не уверены в типе Java, считайте, что он допускает значение NULL.
Например, элемент name класса Account не аннотирован, поэтому его следует считать допускающим значение NULL String?.
Если вы хотите обрезать name, чтобы его значение не включало начальные или конечные пробелы, вы можете использовать функцию trim Kotlin. Безопасно подстричь String? можно несколькими способами. Один из способов – использовать оператор утверждения ненулевого значения !!, как показано в следующем примере:
val account = Account("name", "type")
val accountName = account.name!!.trim()
Оператор !! обрабатывает все, что находится слева от него, как ненулевое значение, поэтому в данном случае вы обрабатываете name как ненулевое значение String. Если результат выражения слева от него равен null, то приложение выдает ошибку NullPointerException.
Этот оператор прост и удобен, но его следует использовать с осторожностью, поскольку он может повторно ввести экземпляры NullPointerException в ваш код.
Более безопасный вариант – использовать оператор безопасного вызова ?., как показано в следующем примере:
val account = Account("name", "type")
val accountName = account.name?.trim()
Если name не равно null, то результатом name?.trim() будет значение name без начальных и конечных пробелов. Если name равно нулю, то результатом name?.trim() будет null. Это означает, что при выполнении этого оператора ваше приложение никогда не сможет выдать NullPointerException.
Хотя оператор безопасного вызова спасает вас от потенциальной ошибки NullPointerException,
он передает нулевое значение следующему оператору. Вместо этого вы можете сразу обрабатывать нулевые значения, используя оператор Элвиса (?:), как показано в следующем примере:
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
Если результат выражения слева от оператора Элвиса равен null, то переменной accountName присваивается значение справа. Этот метод полезен для предоставления значения по умолчанию, которое в противном случае было бы нулевым.
Вы также можете использовать оператор Элвиса, чтобы досрочно вернуть значение из функции, как показано в следующем примере:
fun validateAccount(account: Account?) {
val accountName = account?.name?.trim() ?: "Default name"
// account cannot be null beyond this point
account ?: return
...
}
Изменения в Android API
Android API все лучше поддерживают Kotlin. Многие из наиболее распространенных API Android, включая AppCompatActivity и Fragment, содержат аннотации допустимости значения NULL, а для некоторых вызовов, таких как Fragment#getContext, есть более удобные альтернативы Kotlin.
Например, при обращении к Context из Fragment почти всегда возвращается ненулевое значение, поскольку большинство вызовов, выполняемых в Fragment, происходит, когда Fragment прикреплен к Activity (подклассу Context). Тем не менее Fragment#getContext не всегда возвращает ненулевое значение, поскольку в некоторых сценариях Fragment не прикреплен к Activity. Таким образом, тип возвращаемого значения Fragment#getContext может быть нулевым.
Поскольку Context, возвращаемый из Fragment#getContext, может иметь значение NULL (и помечен как @Nullable), в коде Kotlin его нужно обрабатывать как Context?.
Это означает, что перед доступом к свойствам и функциям необходимо применить один из ранее упомянутых операторов для обработки допустимости значения NULL. Для некоторых из этих сценариев в Android есть альтернативные API, которые обеспечивают такое удобство.
Например, Fragment#requireContext возвращает ненулевое значение Context и выдает IllegalStateException, если вызывается, когда Context равно нулю. Таким образом, вы можете рассматривать результирующий Context как ненулевой без необходимости использования операторов безопасного вызова или обходных путей.
Инициализация ресурса
Свойства в Kotlin не инициализируются по умолчанию. Они должны быть инициализированы при инициализации содержащего их класса.
Инициализировать свойства можно несколькими способами. В примере ниже показано, как инициализировать переменную index, присвоив ей значение в объявлении класса.
class LoginFragment : Fragment() {
val index: Int = 12
}
Инициализацию также можно определить в блоке инициализатора:
class LoginFragment : Fragment() {
val index: Int
init {
index = 12
}
}
В приведенных выше примерах index инициализируется при создании LoginFragment.
Однако у вас могут быть свойства, которые нельзя инициализировать во время создания объекта. Например, если вы хотите сослаться на View из Fragment, то сначала нужно развернуть макет. При создании Fragment раздувание не происходит. Вместо этого он разворачивается при вызове Fragment#onCreateView.
Один из способов решения этой проблемы – объявить представление как допускающее значение NULL и инициализировать его как можно скорее, как показано в следующем примере:
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)
}
}
Это работает как ожидалось, но теперь вам нужно управлять допустимостью значения NULL для View при каждой ссылке на него. Лучше использовать lateinit для инициализации View, как показано в примере ниже.
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)
}
}
Ключевое слово lateinit позволяет избежать инициализации свойства при создании объекта. Если на свойство ссылаются до его инициализации, Kotlin выдает ошибку UninitializedPropertyAccessException, поэтому обязательно инициализируйте свойство как можно скорее.