Введение в Kotlin: программирование Android для людей
Опубликовано: 2022-03-11В идеальном мире Android основной язык Java действительно современный, понятный и элегантный. Вы можете писать меньше, делая больше, и всякий раз, когда появляется новая функция, разработчики могут использовать ее, просто увеличивая версию в Gradle. Затем, создавая очень хорошее приложение, оно кажется полностью тестируемым, расширяемым и ремонтопригодным. Наша деятельность не слишком велика и сложна, мы можем изменить источники данных с базы данных на веб без тонны различий и так далее. Звучит здорово, правда? К сожалению, мир Android не так идеален. Google по-прежнему стремится к совершенству, но все мы знаем, что идеальных миров не существует. Таким образом, мы должны помочь себе в этом великом путешествии в мире Android.
Что такое Kotlin и почему вы должны его использовать?
Итак, первый язык. Я думаю, что Java не является мастером элегантности или ясности, она не современна и не выразительна (и я думаю, вы согласны). Недостатком является то, что ниже Android N мы по-прежнему ограничены Java 6 (включая некоторые небольшие части Java 7). Разработчики также могут подключать RetroLambda для использования лямбда-выражений в своем коде, что очень полезно при использовании RxJava. Выше Android N мы можем использовать некоторые новые функции Java 8, но это все та же старая, тяжелая Java. Очень часто я слышу, как разработчики Android говорят: «Хотелось бы, чтобы Android поддерживал более приятный язык, как iOS со Swift». А что, если бы я сказал вам, что вы можете использовать очень хороший, простой язык с нулевой безопасностью, лямбда-выражениями и множеством других приятных новых функций? Добро пожаловать в Котлин.
Что такое Котлин?
Kotlin — это новый язык (иногда называемый Swift для Android), разработанный командой JetBrains, и сейчас его версия 1.0.2. Что делает его полезным в разработке Android, так это то, что он компилируется в байт-код JVM, а также может быть скомпилирован в JavaScript. Он полностью совместим с Java, а код Kotlin можно просто преобразовать в код Java и наоборот (есть плагин от JetBrains). Это означает, что Kotlin может использовать любой фреймворк, библиотеку и т. д., написанные на Java. На Android он интегрируется с помощью Gradle. Если у вас есть существующее приложение для Android и вы хотите реализовать новую функцию на Kotlin, не переписывая все приложение, просто начните писать на Kotlin, и все заработает.
Но каковы «замечательные новые функции»? Позвольте мне перечислить несколько:
Необязательные и именованные параметры функции
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }
Мы можем вызывать метод createDate по-разному
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'
Нулевая безопасность
Если переменная может быть нулевой, код не скомпилируется, если мы не заставим их сделать это. В следующем коде будет ошибка — nullableVar может быть нулевым:
var nullableVar: String? = “”; nullableVar.length;
Чтобы скомпилировать, мы должны проверить, не является ли оно нулевым:
if(nullableVar){ nullableVar.length }
Или, короче:
nullableVar?.length
Таким образом, если nullableVar имеет значение null, ничего не происходит. В противном случае мы можем пометить переменную как недопустимую, без вопросительного знака после типа:
var nonNullableVar: String = “”; nonNullableVar.length;
Этот код компилируется, и если мы хотим присвоить значение null переменной nonNullableVar, компилятор выдаст ошибку.
Также есть очень полезный оператор Elvis:
var stringLength = nullableVar?.length ?: 0
Затем, когда nullableVar имеет значение null (поэтому nullableVar?.length возвращает null), stringLength будет иметь значение 0.
Изменяемые и неизменяемые переменные
В приведенном выше примере я использую var при определении переменной. Это изменчиво, мы можем переназначить его, когда захотим. Если мы хотим, чтобы эта переменная была неизменной (во многих случаях мы должны), мы используем val (как значение, а не переменную):
val immutable: Int = 1
После этого компилятор не позволит нам переназначить в immutable.
лямбды
Мы все знаем, что такое лямбда, поэтому здесь я просто покажу, как мы можем использовать ее в Котлине:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})
Или, если функция является единственным или последним аргументом:
button.setOnClickListener { Log.d("Kotlin","Click")}
Расширения
Расширения — очень полезная функция языка, благодаря которой мы можем «расширять» существующие классы, даже если они являются окончательными или у нас нет доступа к их исходному коду.
Например, чтобы получить строковое значение из текста редактирования, вместо того, чтобы каждый раз писать editText.text.toString(), мы можем написать функцию:
fun EditText.textValue(): String{ return text.toString() }
Или короче:
fun EditText.textValue() = text.toString()
А теперь с каждым экземпляром EditText:
editText.textValue()
Или мы можем добавить свойство, возвращающее то же самое:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}
Перегрузка оператора
Иногда полезно, если мы хотим добавить, умножить или сравнить объекты. Kotlin позволяет перегружать бинарные операторы (плюс, минус, plusAssign, диапазон и т. д.), операторы массива (получить, установить, получить диапазон, установить диапазон), а также операции равенства и унарные операции (+a, -a и т. д.).
Класс данных
Сколько строк кода нужно для реализации класса User на Java с тремя свойствами: copy, equals, hashCode и toString? В Kaotlin вам нужна только одна строка:
data class User(val name: String, val surname: String, val age: Int)
Этот класс данных предоставляет методы equals(), hashCode() и copy(), а также toString(), который печатает User как:
User(name=John, surname=Doe, age=23)
Классы данных также предоставляют некоторые другие полезные функции и свойства, которые вы можете увидеть в документации Kotlin.
Расширения Анко
Вы используете расширения Butterknife или Android, не так ли? Что, если вам даже не нужно использовать эту библиотеку, а после объявления представлений в XML просто используйте ее из кода по ее идентификатору (как с XAML в C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" />
loginBtn.setOnClickListener{}
У Kotlin есть очень полезные расширения Anko, и с этим вам не нужно сообщать вашей активности, что такое loginBtn, она знает это, просто «импортируя» xml:
import kotlinx.android.synthetic.main.activity_main.*
В Anko есть много других полезных вещей, в том числе запуск активностей, показ тостов и так далее. Это не основная цель Anko — он предназначен для простого создания макетов из кода. Так что если вам нужно создать макет программно, это лучший способ.
Это лишь краткий обзор Котлина. Я рекомендую прочитать блог Антонио Лейвы и его книгу — Kotlin for Android Developers, и, конечно же, официальный сайт Kotlin.
Что такое MVP и почему?
Красивого, мощного и ясного языка недостаточно. Очень легко писать беспорядочные приложения на любом языке без хорошей архитектуры. Разработчики Android (в основном начинающие, но также и более продвинутые) часто возлагают на Activity ответственность за все, что их окружает. Activity (или Fragment, или другое представление) загружает данные, отправляет для сохранения, представляет их, отвечает на действия пользователя, редактирует данные, управляет всеми дочерними представлениями. . . а зачастую и многое другое. Это слишком много для таких нестабильных объектов, как Activity или Fragments (достаточно повернуть экран, и Activity говорит «До свидания….»).
Очень хорошая идея — изолировать обязанности от представлений и сделать их настолько глупыми, насколько это возможно. Представления (действия, фрагменты, настраиваемые представления или что-либо еще, представляющее данные на экране) должны нести ответственность только за управление своими подпредставлениями. В представлениях должны быть ведущие, которые будут общаться с моделью и подсказывать, что им делать. Это, вкратце, паттерн Model-View-Presenter (для меня он должен называться Model-Presenter-View, чтобы показать связи между слоями).
«Эй, я знаю что-то подобное, и это называется MVC!» - ты не думал? Нет, MVP — это не то же самое, что MVC. В шаблоне MVC ваше представление может взаимодействовать с моделью. При использовании MVP вы не разрешаете какую-либо связь между этими двумя уровнями — единственный способ взаимодействия View с Model — через Presenter. Единственное, что View знает о модели, это структура данных. View знает, как, например, отобразить пользователя, но не знает, когда. Вот простой пример:
View знает: «Я Activity, у меня есть два EditTexts и одна кнопка. Когда кто-то нажимает кнопку, я должен сообщить об этом своему ведущему и передать ему значения EditTexts. И все, я могу спать, пока следующий щелчок или ведущий не скажет мне, что делать».
Presenter знает, что где-то есть View, и знает, какие операции этот View может выполнять. Он также знает, что когда он получает две строки, он должен создать пользователя из этих двух строк и отправить данные в модель для сохранения, а в случае успешного сохранения сообщить представлению «Показать информацию об успехе».
Модель просто знает, где находятся данные, где их нужно сохранить и какие операции следует выполнить с данными.
Приложения, написанные на MVP, легко тестировать, поддерживать и использовать повторно. Чистый ведущий ничего не должен знать о платформе Android. Это должен быть чистый класс Java (или Kotlin, в нашем случае). Благодаря этому мы можем повторно использовать наш презентатор в других проектах. Мы также можем легко писать модульные тесты, тестируя по отдельности Model, View и Presenter.
Небольшое отступление: MVP должен быть частью Чистой Архитектуры дяди Боба, чтобы сделать приложения еще более гибкими и красивыми. Постараюсь написать об этом в следующий раз.
Пример приложения с MVP и Kotlin
Достаточно теории, давайте посмотрим код! Хорошо, давайте попробуем создать простое приложение. Основная цель этого приложения — создать пользователя. Первый экран будет иметь два EditTexts (Имя и Фамилия) и одну кнопку (Сохранить). После ввода имени и фамилии и нажатия «Сохранить» приложение должно показать «Пользователь сохранен» и перейти к следующему экрану, где представлены сохраненные имя и фамилия. Когда имя или фамилия пусты, приложение не должно сохранять пользователя и показывать ошибку, указывающую, что не так.
Первое, что нужно сделать после создания проекта Android Studio, — настроить Kotlin. Вы должны установить плагин Kotlin, а после перезагрузки в меню «Инструменты»> «Kotlin» вы можете нажать «Настроить Kotlin в проекте». IDE добавит зависимости Kotlin в Gradle. Если у вас есть какой-либо существующий код, вы можете легко преобразовать его в Kotlin (Ctrl+Shift+Alt+K или Код > Преобразовать файл Java в Kotlin). Если что-то не так и проект не компилируется, или Gradle не видит Kotlin, вы можете проверить код приложения, доступный на GitHub.
Теперь, когда у нас есть проект, давайте начнем с создания нашего первого представления — CreateUserView. Это представление должно иметь функциональные возможности, упомянутые ранее, поэтому мы можем написать для него интерфейс:

interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }
Как видите, Kotlin похож на Java в объявлении функций. Все это функции, которые ничего не возвращают, а последние имеют один параметр. В этом разница, тип параметра идет после имени. Интерфейс View не из Android — это наш простой, пустой интерфейс:
interface View
Интерфейс Basic Presenter должен иметь свойство типа View и, по крайней мере, в методе (например, onDestroy), где для этого свойства будет установлено значение null:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }
Здесь вы можете увидеть еще одну особенность Kotlin — вы можете объявлять свойства в интерфейсах, а также реализовывать там методы.
Наш CreateUserView должен взаимодействовать с CreateUserPresenter. Единственная дополнительная функция, которая нужна этому Презентатору, это saveUser с двумя строковыми аргументами:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }
Нам также нужно определение модели — это упомянутый ранее класс данных:
data class User(val name: String, val surname: String)
После объявления всех интерфейсов мы можем приступить к реализации.
CreateUserPresenter будет реализован в CreateUserPresenterImpl:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }
Первая строка с определением класса:
CreateUserPresenterImpl(override var view: CreateUserView?)
Является конструктором, мы используем его для назначения свойства представления, определенного в интерфейсе.
MainActivity, которая является нашей реализацией CreateUserView, нуждается в ссылке на CreateUserPresenter:
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }
В начале класса мы определили нашего ведущего:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }
Он определен как неизменяемый (val) и создается ленивым делегатом, который будет назначен при первой необходимости. Более того, мы уверены, что оно не будет нулевым (без вопросительного знака после определения).
Когда пользователь нажимает кнопку «Сохранить», View отправляет информацию Presenter со значениями EditTexts. Когда это произойдет, пользователь должен быть сохранен, поэтому мы должны реализовать метод saveUser в Presenter (и некоторые функции модели):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }
Когда пользователь создается, он отправляется в UserValidator для проверки правильности. Затем, в соответствии с результатом проверки, вызывается правильный метод. Конструкция when() {} аналогична конструкции switch/case в Java. Но он более мощный — Kotlin позволяет использовать не только enum или int в «case», но и диапазоны, строки или типы объектов. Он должен содержать все возможности или иметь выражение else. Здесь он охватывает все значения UserError.
Используя view?.showEmptyNameError() (со знаком вопроса после представления), мы защищены от NullPointer. View можно обнулить в методе onDestroy, и с такой конструкцией ничего не произойдет.
Когда в модели User нет ошибок, он указывает UserStore сохранить ее, а затем дает указание View показать успех и показать подробности.
Как упоминалось ранее, мы должны реализовать некоторые вещи модели:
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }
Самое интересное здесь — это UserValidator. Используя объектное слово, мы можем создать одноэлементный класс, не беспокоясь о потоках, частных конструкторах и так далее.
Следующее - в методе validateUser(user) есть выражение with(user) {}. Код внутри такого блока выполняется в контексте объекта, переданного с именем и фамилией, является свойствами пользователя.
Есть и еще одна мелочь. Весь приведенный выше код, от enum до UserValidator, определение помещается в один файл (определение класса User также находится здесь). Kotlin не заставляет вас иметь каждый публичный класс в одном файле (или именовать класс точно так же, как файл). Таким образом, если у вас есть короткие фрагменты связанного кода (классы данных, расширения, функции, константы — Kotlin не требует класса для функции или константы), вы можете поместить его в один файл, а не распространять по всем файлам в проекте.
Когда пользователь сохранен, наше приложение должно отображать это. Нам нужен еще один View — это может быть любой Android View, пользовательский View, Fragment или Activity. Я выбрал Активность.
Итак, давайте определим интерфейс UserDetailsView. Он может показать пользователя, но также должен показывать ошибку, когда пользователь отсутствует:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }
Далее, UserDetailsPresenter. У него должно быть пользовательское свойство:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }
Этот интерфейс будет реализован в UserDetailsPresenterImpl. Он должен переопределить свойство пользователя. Каждый раз, когда назначается это свойство, пользователь должен обновляться в представлении. Для этого мы можем использовать установщик свойств:
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }
Реализация UserDetailsView, UserDetailsActivity, очень проста. Как и раньше, у нас есть объект презентатора, созданный путем отложенной загрузки. Пользователь для отображения должен передаваться через намерение. На данный момент есть одна небольшая проблема, и мы решим ее через минуту. Когда у нас есть пользователь из намерения, View должен назначить его своему докладчику. После этого пользователь обновится на экране, или, если он равен null, появится ошибка (и активность завершится, но ведущий об этом не знает):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }
Передача объектов через намерения требует, чтобы этот объект реализовывал интерфейс Parcelable. Это очень «грязная» работа. Лично я ненавижу делать это из-за всех СОЗДАТЕЛЕЙ, свойств, сохранения, восстановления и так далее. К счастью, для Kotlin есть подходящий плагин Parcelable. После его установки мы можем сгенерировать Parcelable одним щелчком мыши.
Последнее, что нужно сделать, это реализовать showUserDetails(user: User) в нашей MainActivity:
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }
И это все.
У нас есть простое приложение, которое сохраняет пользователя (на самом деле оно не сохраняется, но мы можем добавить эту функцию, не касаясь презентатора или представления) и отображает его на экране. В будущем, если мы захотим изменить способ представления пользователя на экране, например, с двух действий на два фрагмента в одном действии или два пользовательских представления, изменения будут только в классах представлений. Конечно, если мы не изменим функционал или структуру модели. Ведущий, который не знает, что такое View, не нуждается в каких-либо изменениях.
Что дальше?
В нашем приложении Presenter создается каждый раз при создании действия. Этот подход или его противоположность, если Presenter должен сохраняться в экземплярах активности, является предметом многочисленных дискуссий в Интернете. Для меня это зависит от приложения, его потребностей и разработчика. Иногда лучше уничтожить ведущего, иногда нет. Если вы решите сохранить его, очень интересным методом является использование для этого LoaderManager.
Как упоминалось ранее, MVP должен быть частью чистой архитектуры дяди Боба. Более того, хорошие разработчики должны использовать Dagger для внедрения зависимостей презентаторов в действия. Это также помогает поддерживать, тестировать и повторно использовать код в будущем. В настоящее время Kotlin очень хорошо работает с Dagger (до официального релиза это было не так просто), а также с другими полезными библиотеками Android.
Заворачивать
Для меня Kotlin — отличный язык. Он современный, ясный и выразительный, но все еще разрабатывается великими людьми. И мы можем использовать любую новую версию на любом устройстве и версии Android. Что бы меня ни злило в Java, Kotlin становится лучше.
Конечно, как я уже сказал, нет ничего идеального. У Kotlin также есть некоторые недостатки. Новейшие версии плагинов Gradle (в основном альфа- или бета-версии) плохо работают с этим языком. Многие люди жалуются, что время сборки немного больше, чем у чистой Java, а apks имеют дополнительные мегабайты. Но Android Studio и Gradle все еще совершенствуются, и в телефонах появляется все больше места для приложений. Вот почему я считаю, что Kotlin может быть очень хорошим языком для каждого разработчика Android. Просто попробуйте и поделитесь в разделе комментариев ниже, что вы думаете.
Исходный код примера приложения доступен на Github: github.com/tomaszczura/AndroidMVPKotlin.