Используйте общие шаблоны Kotlin с Android

В этой теме рассматриваются некоторые из наиболее полезных аспектов языка Kotlin при разработке для Android.

Работа с фрагментами

В следующих разделах используются примеры Fragment , чтобы выделить некоторые из лучших функций 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. Обязательно обрабатывайте их обнуляемость безопасно .

В 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)
    }

    ...
}

Конверсия ЗРК

Вы можете прослушивать события кликов в 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 .

Делегирование свойств использует отражение, что увеличивает производительность. Компромиссом является краткий синтаксис, который экономит время разработки.

Обнуляемость

Kotlin предоставляет строгие правила обнуления, которые обеспечивают безопасность типов во всем вашем приложении. В Kotlin ссылки на объекты по умолчанию не могут содержать нулевые значения. Чтобы присвоить переменной значение NULL, вы должны объявить тип переменной, допускающий значение NULL , добавив ? до конца базового типа.

Например, следующее выражение является незаконным в Котлине. name имеет тип String и не может иметь значение NULL:

val name: String = null

Чтобы разрешить нулевое значение, вы должны использовать тип String , допускающий значение NULL, String? , как показано в следующем примере:

val name: String? = null

Совместимость

Строгие правила Kotlin делают ваш код более безопасным и кратким. Эти правила снижают вероятность возникновения NullPointerException , которое может привести к сбою вашего приложения. Более того, они уменьшают количество проверок на ноль, которые необходимо выполнить в коде.

Часто при написании приложения для Android вам также приходится вызывать код, отличный от Kotlin, поскольку большинство API-интерфейсов Android написаны на языке программирования Java.

Обнуляемость — это ключевая область, в которой поведение Java и Kotlin различается. Java менее строг в отношении синтаксиса, допускающего значение NULL.

Например, класс Account имеет несколько свойств, включая свойство String с name . В Java нет правил Kotlin в отношении возможности обнуления, вместо этого используются необязательные аннотации об обнулении , чтобы явно объявить, можете ли вы присвоить значение NULL.

Поскольку платформа Android написана в основном на Java, вы можете столкнуться с этим сценарием при вызове API без аннотаций, допускающих значение NULL.

Типы платформ

Если вы используете Kotlin для ссылки на неаннотированный элемент name , определенный в классе Account Java, компилятор не знает, сопоставляется ли String со String или со String? в Котлине. Эта неоднозначность представлена ​​через тип платформы String! .

String! не имеет особого значения для компилятора Kotlin. String! может представлять собой String или String? , и компилятор позволяет вам присвоить значение любого типа. Обратите внимание, что вы рискуете создать исключение NullPointerException , если представляете тип как String и присваиваете нулевое значение.

Чтобы решить эту проблему, вам следует использовать аннотации, допускающие значение 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 , указывающей, что она может содержать нулевое значение. Тогда Котлин будет рассматривать accessId как String? .

Чтобы указать, что переменная никогда не может быть нулевой, используйте аннотацию @NonNull :

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

В этом сценарии name в Котлине считается String , не допускающей значения NULL.

Аннотации допускающих значение NULL включены во все новые API Android и во многие существующие API Android. Многие библиотеки Java добавили аннотации об отсутствии значений для лучшей поддержки разработчиков Kotlin и Java.

Обработка возможности обнуления

Если вы не уверены в типе Java, вам следует считать его допускающим значение NULL. Например, член name класса Account не аннотирован, поэтому следует предположить, что это String с нулевым значением.

Если вы хотите обрезать name так, чтобы его значение не включало начальные или конечные пробелы, вы можете использовать функцию trim Kotlin. Вы можете безопасно обрезать String? несколькими разными способами. Один из таких способов — использовать оператор утверждения, не равный нулю , !! , как показано в следующем примере:

val account = Account("name", "type")
val accountName = account.name!!.trim()

!! Оператор рассматривает все, что находится в его левой части, как ненулевое значение, поэтому в этом случае вы рассматриваете name как ненулевую String . Если результат выражения слева от него равен нулю, ваше приложение выдает исключение NullPointerException . Этот оператор работает быстро и легко, но его следует использовать с осторожностью, так как он может повторно ввести экземпляры NullPointerException в ваш код.

Более безопасный выбор — использовать оператор безопасного вызова , ?. , как показано в следующем примере:

val account = Account("name", "type")
val accountName = account.name?.trim()

Если при использовании оператора безопасного вызова name не равно NULL, то результатом name?.trim() будет значение имени без начальных или конечных пробелов. Если name имеет значение null, то результатом name?.trim() будет null . Это означает, что ваше приложение никогда не сможет выдать NullPointerException при выполнении этого оператора.

Хотя оператор безопасного вызова спасает вас от потенциального NullPointerException , он передает нулевое значение следующему оператору. Вместо этого вы можете сразу обработать нулевые случаи, используя оператор Элвиса ( ?: ), как показано в следующем примере:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Если результат выражения в левой части оператора Элвиса равен нулю, то значение в правой части присваивается accountName . Этот метод полезен для предоставления значения по умолчанию, которое в противном случае было бы нулевым.

Вы также можете использовать оператор Элвиса для раннего возврата из функции, как показано в следующем примере:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Изменения API Android

API-интерфейсы Android становятся все более дружественными к Kotlin. Многие из наиболее распространенных API-интерфейсов Android, включая AppCompatActivity и Fragment , содержат аннотации, допускающие значение NULL, а некоторые вызовы, такие как Fragment#getContext имеют более удобные для Kotlin альтернативы.

Например, доступ к Context Fragment почти всегда не равен нулю, поскольку большинство вызовов, которые вы делаете во Fragment происходят, когда Fragment прикреплен к Activity (подклассу Context ). Тем не менее, Fragment#getContext не всегда возвращает ненулевое значение, поскольку существуют сценарии, в которых Fragment не прикреплен к Activity . Таким образом, возвращаемый тип Fragment#getContext имеет значение NULL.

Поскольку Context , возвращаемый из Fragment#getContext имеет значение NULL (и помечен как @Nullable), вы должны рассматривать его как Context? в вашем коде Котлина. Это означает применение одного из ранее упомянутых операторов для устранения возможности обнуления перед доступом к его свойствам и функциям. Для некоторых из этих сценариев Android содержит альтернативные API, обеспечивающие это удобство. Например, Fragment#requireContext возвращает ненулевой Context и выдает исключение IllegalStateException , если вызывается, когда Context будет иметь значение null. Таким образом, вы можете рассматривать полученный 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)
    }
}

Хотя это работает так, как и ожидалось, теперь вы должны управлять обнуляемостью 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 , поэтому обязательно инициализируйте свое свойство как можно скорее.