Kotlin программа для android
Года два назад я обнаружил, что в Android Market нет удобных мне приложений для учета времени, затраченного на разные проекты. Как настоящий лентяй, я решил не искать требуемое, а написать его сам. Это оказалось несколько сложнее, чем казалось вначале, и я все забросил в режиме вялотекущего хобби воплощение моего желания прожило больше года, медленно продвигаясь от идеи к альфа-версии.
И тут вдруг добрые люди предложили мне рассказать, как писать под Android на Kotlin'e. Мне показалось, что это добрый знак, и я быстро переписал приложение на Kotlin, закончил его и выложил в Google Play.
Важное уточнение: эта статья — только про то, как писать под Android на Kotlin'e. Интересно будет только тем, кто хочет попробовать Kotlin, и тем, кто хочет понять, зачем его пробовать. Приложение, которое я написал — очень-очень простое (и само по себе никак не относится к JetBrains). В его коде гарантированно можно что-то улучшить, оно выложено на Github только для примера, а не в качестве образца восхитительного кода. Оно работает, и если вам хочется его попробовать, то поставить можно отсюда. Но еще раз: это — демо, его цель — быть не совершенным приложением, а введением в Kotlin под Андроид.
Опять что-то новое. И зачем мне это знать?
- компактность кода. Быстро писать, легко читать, меньше наведенных ошибок;
- встроенная защита от ошибок, прежде всего — nullability, т. е. требование к программисту явно указывать, что некая переменная может принимать значение null;
- легкость в освоении (по-моему, даже начинающий разработчик на Java освоит Kotlin без труда).
Кстати, на github'e уже более 200 репозиториев с кодом на Kotlin'e, а если говорить про мобильные приложения, то Kotlin использовался при разработке мессенджера Telegram.
Ну ладно, а с чего начать-то?
Для работы потребуется IDE. Я по очевидным причинам предпочитаю IntelliJ IDEA, однако JetBrains делает плагин Kotlin и для Eclipse, а любители vim могут пользоваться отдельным компилятором языка.
Дальше будем предполагать, что все, кто пожелает попробовать Kotlin в деле под Android, будут использовать IntelliJ IDEA. Для работы понадобится установить плагин Kotlin и позаботиться, чтобы на компьютере были gradle и Android SDK (это к Kotlin'y отношения не имеет, это надо для разработки под Android на чем угодно).
Вначале была Java
У меня уже было частично написанное приложение, когда я решил довести его до конца. Все было написано на Java.
Суть приложения: на экране есть список красных задач, если кликнуть по задаче, она зеленеет, и время пошло: это значит, что пользователь работает над задачей. Как прекратил — кликает по ней еще раз, и задача снова краснеет, показывая, как немного времени он на нее потратил. Приложение слепо доверяет пользователю, не проверяя его никак: это вам не «большой брат смотрит за тобой» от oDesk'a.
То, что было написано на Java, я хотел переписать на Kotlin, душа просила кода на 100% на Kotlin, несмотря на то, что в одном проекте можно использовать код на Java и на Kotlin'e одновременно. Оказалось, что с унаследованным кодом все просто: рядом с src/main/java создаем папку src/main/kotlin, и в нее помещаем классы. Файлы в Kotlin'е заканчиваются не на .java, а на .kt. Тут к месту оказался приятный момент: Kotlin не требует соответствия «один класс — один файл». Можно в один файл запихнуть столько классов, сколько хочется по логике приложения. В моем приложении было всего две логических части — работа с базой данных и экранный интерфейс пользователя, так что количество файлов можно было сократить.
Плагин Kotlin к IntelliJ IDEA умеет конвертировать файлы .java в файлы .kt, выполняя аккуратную трансляцию из Java в Kotlin. Можно это делать через правый клик по файлу и контекстное меню, а можно — прямо при копировании кода на Java в файл .kt (плагин спросит, не сконвертировать ли).
Пишем код на Kotlin'e
Код на языке Kotlin компактен: например, класс вместе с конструктором и getters/setters описывается так:
Посмотрим на код, который создает диалог для ввода текста по клику на кнопку.
Согласитесь, код на Kotlin'e читается проще. Для внимательного читателя заметим, что это, конечно, только фрагмент кода, так как при создании диалога нужна не только кнопка «ОК», но и кнопка «Отмена». Полностью код выложен на Github.
Что дает возможность так сокращать код? Например, в Kotlin'e возможна такая форма записи: если где-то в качестве параметра ожидается экземпляр класса с одним абстрактным методом, туда можно просто передать лямбду: именно это и показано в примере выше.
Обратите внимание, что в Kotlin'e можно не указывать тип переменной, так как он будет выведен из правой части присваивания:
Java | Kotlin |
---|---|
AlertDialog.Builder alert = new AlertDialog.Builder (this); | val alert = AlertDialog.Builder(this) |
Интересная особенность синтаксиса в Kotlin'e: если в функцию передаются параметры, и последний параметр — функция, то ее можно вынести за скобки. Именно это мы видим во фрагменте, где в setPositiveButton передается лямбда, которая сработает по нажатию кнопки «ОК»:
То же самое можно записать как
Вы вольны сами выбрать, какой вариант кажется проще для чтения.
Использование библиотек на Java в коде на Kotlin'e
Еще один приятный момент — возможность напрямую использовать библиотеки, написанные на Java, из кода на Kotlin.
Например, чтобы нормально обрабатывать спецсимволы и Unicode в названиях проектов при сохранении их в SQLite, я использовал функции StringEscapeUtils.escapeJava и StringEscapeUtils.unescapeJava из популярной библиотеки, которую достаточно импортировать оператором import (и вписать в зависимости в свойствах проекта):
Возвращаемое значение
В Kotlin в функциональных литералах (a.k.a лямбда-функциях) запрещено использовать return, так как функциональный литерал возвращает значение, вычисленное в последнем выражении. Так, в вызове setOnTouchListener ожидается, что последний параметр вызова — функция, возвращающая boolean. Это значение фактически возвращает gestureDetector.onTouchEvent(aEvent).
В коде функционального литерала на Kotlin нельзя писать return gestureDetector.onTouchEvent(aEvent), результат вызова gestureDetector.onTouchEvent(aEvent) и так будет возвращен.
На самом деле есть специальные случаи, когда в функциональном литерале следует использовать нелокальный return. Если интересно, подробности есть в документации по Kotlin
Аналогично работает возврат значений функциями, определенными через знак " java">override fun getCount() = data.size()
Строковые шаблоны
Чтобы избавиться от длинных строк, трудных для чтения и дающих неоптимальный байт-код, в Kotlin'e применены строковые шаблоны (string templates):
Здесь projectToDelete — это строковая переменная. Синтаксис такого шаблона привычен любому, кто имел дело с переменными среды в UNIX, например. Особенно удобны шаблоны для строк, составленных из текста и значений многих переменных, да еще и с форматированием:
Между прочим, с форматированием по ходу дела вышла интересная история: тип String в Kotlin — свой, и метода format в нем нет, поэтому пришлось импортировать java.lang.String.format явным образом и потом к нему так обращаться. Удивительно, кстати, что ни в Java, ни в Kotlin'e до сих пор нет метода secondsToHumanReadableString, преобразующего целое число секунд в строку формата
when и with
Очень удобно использовать when для множественного выбора и with для сокращения обращений к методам и свойствам — и того, и другого хватает даже в маленьком приложении под Android. В моем случае это давало более легкий в чтении код, например:
Без with получились бы alert.setPositiveButton, alert.setNegativeButton, alert.create, alert.show. When тоже повышает читаемость:
Компиляция с помощью Gradle
Не знаю, есть ли такая проблема у пользователей Android Studio, но у привычного к IntelliJ IDEA человека работа с Gradle может вызвать вопрос, как собрать .apk для релиза, а не для отладки.
Для этого в IDEA есть плагин Gradle, открывается кликом по табу справа:
По умолчанию IDEA собирает *-debug-unsigned.apk, т.е. то, что в Google Play не положишь. Чтобы оно стало *-release-signed.apk, надо сгенерировать ключ для подписи, положить его в keystore, и вписать несколько строк в build.gradle вашего проекта, чтобы Gradle знал, где ключ взять. Для сборки release, выберите в окне Gradle задачу assembleRelease двойным кликом по ней.
Про то, как создавать ключ и подписывать приложение вы либо уже знаете, либо можете прочесть подробнее на stackoverflow.
На заметку
Если в статье какие-то примеры кода показались вам красивыми, то это — несомненная заслуга Натальи Ухорской, которая работает у нас в команде Kotlin. Я очень благодарен Наташе за советы, без которых эта статья оказалась бы короче, а код — менее похожим на классический Kotlin.
Мы будем очень рады, если сэкономленные за счет использования Kotlin часы вы сможете провести на пляже. Хорошего остатка лета!
UPD от 31.07.14, 18:55: поправлены некоторые термины и формулировки.
Очень сложно найти один проект, который охватывал бы всё новое в разработке под Android в Android Studio 3.0, поэтому я решил написать его. В этой статье мы разберём следующее:
- Android Studio 3
- Язык программирования Kotlin
- Варианты сборки
- ConstraintLayout
- Библиотека привязки данных Data Binding
- Архитектура MVVM + паттерн repository (с mapper'ами) + Android Manager Wrappers
- RxJava2 и как это помогает нам в архитектуре
- Dagger 2.11, что такое внедрение зависимости, почему вы должны использовать это.
- Retrofit (Rx Java2)
- Room (Rx Java2)
Каким будет наше приложение?
Наше приложение будет самым простым, которое охватывает все перечисленные выше вещи: у него будет только одна функция, которая извлекает все репозитории пользователя googlesamples из GitHub, сохраняет эти данные в локальной базе данных и показывает их пользователю.
Я попытаюсь объяснить как можно больше строк кода. Вы всегда можете посмотреть код, который я опубликовал на GitHub.
Android Studio
Чтобы установить Android Studio 3, перейдите на эту страницу
Android Studio 3 поддерживает Kotlin. Откройте Create Android Project. Там вы увидите новый флажок с меткой Include Kotlin support. Он выбран по умолчанию. Дважды нажмите кнопку Далее и выберите Empty Activity, затем нажмите Finish.
Поздравляю! Вы сделали первое приложение для Android на Котлине :)
Kotlin
Вы можете видеть MainActivity.kt:
Расширение .kt означает, что файл является файлом Kotlin.
MainActivity: AppCompatActivity() означает, что мы расширяем AppCompatActivity.
Кроме того, все методы должны иметь ключевое слово fun и в Котлине вам не нужно использовать ;, но вы можете, если хотите. Вы должны использовать ключевое слово override, а не аннотацию, как в Java.
Так что же означает ? в savedInstanceState: Bundle?? Это означает, что savedInstanceState может быть типа Bundle или типа null. Kotlin null безопасный язык. Если у вас есть:
вы получите ошибку компиляции, потому что a должна быть инициализированна и это не может быть null. Это означает, что вы должны написать:
Кроме того, вы получите ошибку компиляции, если вы это сделаете:
Чтобы сделать a nullable, вы должны написать:
Почему эта важная особенность языка Котлина? Это помогает нам избежать NPE. Разработчики Android уже устали от NPE. Даже создатель null, сэр Тони Хоар, извинился за изобретение. Предположим, что мы имеем nullable nameTextView. Если переменная равна null, то в следующем коде мы получим NPE:
Но Котлин, на самом деле, хорош, он не позволят нам делать даже такое. Он заставляет нас использовать оператор ? или оператор !!. Если мы используем оператор ?:
Строка будет исполнена только если nameTextView не null. В ином случае, если вы используете оператор !!:
Мы получим NPE если nameTextView null. Это для авантюристов :).
Это было небольшое введение в Kotlin. Когда мы продолжим, я остановлюсь, чтобы описать другой специфический код на Котлине.
2. Build Variants
В разработке часто вы имеете различные окружения. Наиболее стандартным является тестовое и производственное окружение. Эти среды могут отличаться в URL-адресах сервера, иконке, имени, целевом API и т.д. На fleka в каждом проекте у вас есть:
- finalProduction, который отправляется в Google Play Store.
- demoProduction, то есть версия с URL-адресом production сервера с новыми функциями, которые всё ещё не находятся в Google Play Store. Наши клиенты могут установить эту версию рядом с Google Play, чтобы они могли протестировать ее и дать нам обратную связь.
- demoTesting, то же самое, что и demoProduction с тестовым URL-адресом сервера.
- mock, полезен для меня как для разработчика и дизайнера. Иногда у нас есть готовый дизайн, и наш API ещё не готов. Ожидание API, чтобы быть начать разработку — не решение. Этот вариант сборки снабжён поддельными данными, поэтому команда дизайнеров может проверить его и дать нам обратную связь. Очень полезно это не откладывать. Когда API уже готов, мы перемещаем нашу разработку в окружение demoTesting.
В этом приложении мы будем использовать всех их. У них будут отличаться applicationId и имена. В gradle 3.0.0 есть новый API flavorDimension, который позволяет смешивать разновидности продукта, так, например, вы можете смешать разновидности demo и minApi23. В нашем приложении мы будем использовать только «default» flavorDimension. Перейдите в build.gradle для приложения и вставьте этот код внутри android <>
Перейдите в strings.xml и удалите строку app_name, чтобы у нас не было конфликтов. Затем нажмите Sync Now. Если вы перейдете в Build Variants, расположенным слева от экрана, вы увидите 4 варианта сборки, каждый из которых имеет два типа сборки: Debug и Release. Перейдите к варианту сборки demoProduction и запустите его. Затем переключитесь на другой и запустите его. Вы должны увидеть два приложения с разными именами.
3. ConstraintLayout
Если вы откроете activity_main.xml, вы увидите, что этот layout — ConstrainLayout. Если вы когда-либо писали приложение под iOS, вы знаете об AutoLayout. ConstraintLayout действительно похож на него. Они даже используют один и тот же алгоритм Cassowary.
Constraint помогает нам описать связи между View. Для каждого View у вас должно быть 4 Constraint, один для каждой стороны. В данном случае наш View ограничен родителем с каждой стороны.
Если вы передвинете TextView «Hello World» немного вверх во вкладке Design, во вкладке Text появится новая линия:
Вкладки Design и Text синхронизируются. Наши изменения во вкладке Design влияют на xml во вкладке Text и наоборот. Vertical_bias описывает вертикальную тенденцию view его Constraint. Если вы хотите центровать вертикально, используйте:
Давайте сделаем чтобы наш Activity показал только один репозиторий. В нём будут имя репозитория, количество звезд, владелец, и он будет показывать, есть ли у репозитория issues, или нет.
Чтобы получить такой layout, xml должен выглядеть так:
Пусть tools:text вас не смущает. Он просто помогает нам видеть хороший предварительный просмотр макета (layout'а).
Вы можете заметить, что наш макет плоский, ровный. Вложенных макетов нет. Вы должны использовать вложенные макеты как можно реже, поскольку это может повлиять на производительность. Более подробную информацию об этом вы можете найти здесь. Кроме того, ConstraintLayout отлично работает с разными размерами экрана:
и мне кажется, что я могу добиться желаемого результата очень быстро.
Это было небольшое введение в ConstraintLayout. Вы можете найти Google code lab здесь, и документацию о ConstraintLayout на GitHub.
4. Библиотека привязки данных Data Binding
Когда я услышал о библиотеке привязки данных, первое вопрос, который я задал себе: "ButterKnife работает очень хорошо для меня. Кроме того, я использую плагин, который помогает мне получать View из xml. Зачем мне это менять?". Как только я узнал больше о привязке данных, у меня было такое же чувство, какое у меня было, когда я впервые использовал ButterKnife.
Как ButterKnife помогает нам?
ButterKnife помогает нам избавиться от скучного findViewById. Итак, если у вас 5 View, без Butterknife у вас есть 5 + 5 строк, чтобы привязать ваши View. С ButterKnife у вас есть 5 строк. Вот и всё.
Что плохо в ButterKnife?
ButterKnife по-прежнему не решает проблему поддержки кода. Когда я использовал ButterKnife, я часто получал исключение во время выполнения, потому что я удалял View в xml, и не удалял код привязки в классе Activity / Fragment. Кроме того, если вы хотите добавить View в xml, вам нужно снова сделать привязку. Это очень скучно. Вы теряете время на поддерживание связей.
Что насчёт библиотеки привязки данных?
Есть много преимуществ! С помощью библиотеки привязки данных вы можете привязать свои View всего одной строкой кода! Позвольте мне показать вам, как это работает. Давайте добавим библиотеку Data Binding в наш проект:
Обратите внимание, что версия компилятора Data Binding должна совпадать с версией gradle в файле build.gradle проекта:
Нажмите Sync Now. Перейдите в activity_main.xml и оберните ConstraintLayout тегом layout:
Обратите внимание, что вам нужно переместить все xmlns в тег layout. Затем нажмите иконку Build или используйте сочетание клавиш Ctrl + F9 (Cmd + F9 на Mac). Нам нужно собрать проект, чтобы библиотека Data Binding могла сгенерировать класс ActivityMainBinding, который мы будем использовать в нашем классе MainActivity.
Если вы не выполните сборку проекта, вы не увидите класс ActivityMainBinding, потому что он генерируется во время компиляции. Мы все еще не закончили связывание, мы просто сказали, что у нас есть ненулевая переменная типа ActivityMainBinding. Кроме того, как вы можете заметить, я не указал ? в конце типа ActivityMainBinding, и я не инициализировал его. Как это возможно? Модификатор lateinit позволяет нам иметь ненулевые переменные, ожидающие инициализации. Подобно ButterKnife, инициализация привязки должна выполняться в методе onCreate, когда ваш Activity будет готов. Кроме того, вы не должны объявлять привязку в методе onCreate, потому что вы, вероятно, используете его вне области видимости метода onCreate. Наша привязка не должна быть нулевой, поэтому мы используем lateinit. Используя модификатор lateinit, нам не нужно проверять привязку переменной каждый раз, когда мы обращаемся к ней.
Давайте инициализируем нашу переменную binding. Вы должны заменить:
Вот и всё! Вы успешно привязали свои View. Теперь вы можете получить к ним доступ и применить изменения. Например, давайте изменим имя репозитория на «Modern Android Habrahabr Article»:
Как вы можете видеть, мы можем получить доступ ко всем View (у которых есть id, конечно) из activity_main.xml через переменную binding. Вот почему Data Binding лучше, чем ButterKnife.
Getter'ы и Setter'ы в Котлине
Возможно, вы уже заметили, что у нас нет метода .setText (), как в Java. Я хотел бы остановиться здесь, чтобы объяснить, как геттеры и сеттеры работают в Kotlin по сравнению с Java.
Во-первых, вы должны знать, почему мы используем сеттеры и геттеры. Мы используем их, чтобы скрыть переменные класса и разрешить доступ только с помощью методов, чтобы мы могли скрыть элементы класса от клиентов класса и запретить тем же клиентам напрямую изменять наш класс. Предположим, что у нас есть класс Square в Java:
Используя метод setA (), мы запрещаем клиентам класса устанавливать отрицательное значение стороне квадрата, оно не должно быть отрицательным. Используя этот подход, мы должны сделать a приватным, поэтому его нельзя установить напрямую. Это также означает, что клиент нашего класса не может получить a напрямую, поэтому мы должны предоставить getter. Этот getter возвращает a. Если у вас есть 10 переменных с аналогичными требованиями, вам необходимо предоставить 10 геттеров. Написание таких строк — это скучная вещь, в которой мы обычно не используем наш разум.
Kotlin облегчает жизнь нашего разработчика. Если вы вызываете
это не означает, что вы получаете доступ к a непосредственно. Это то же самое, что
в Java. Причина заключается в том, что Kotlin автоматически генерирует геттеры и сеттеры по умолчанию. В Котлине, вы должны указать специальный сеттер или геттер, только если он у вас есть. В противном случае, Kotlin автогенерирует его для вас:
field? Что это? Чтобы было ясно, давайте посмотрим на этот код:
Это означает, что вы вызываете метод set внутри метода set, потому что нет прямого доступа к свойству в мире Kotlin. Это создаст бесконечную рекурсию. Когда вы вызываете a = что-то, он автоматически вызывает метод set.
Надеюсь, теперь понятно, почему вы должны использовать ключевое слово field и как работают сеттеры и геттеры.
Вернемся к нашему коду. Я хотел бы показать вам ещё одну замечательную особенность языка Kotlin, apply:
apply позволяет вам вызывать несколько методов на одном экземпляре.
Мы все еще не закончили привязку данных, есть ещё много дел. Давайте создадим класс модели пользовательского интерфейса для репозитория (этот класс модели пользовательского интерфейса для репозитория GitHub хранит данные, которые должны отображаться, не путайте их с паттерном Repository). Чтобы сделать класс Kotlin, вы должны перейти в New -> Kotlin File / Class:
В Kotlin первичный конструктор является частью заголовка класса. Если вы не хотите предоставлять второй конструктор, это всё! Ваша работа по созданию класса завершена здесь. Нет параметров конструктора для назначений полей, нет геттеров и сеттеров. Целый класс в одной строке!
Вернитесь в класс MainActivity.kt и создайте экземпляр класса Repository:
Как вы можете заметить, для построения объекта не нужно ключевого слова new.
Теперь перейдем к activity_main.xml и добавим тег data:
Мы можем получить доступ к переменной repository, которая является типом Repository в нашем макете. Например, мы можем сделать следующее в TextView с идентификатором repository_name:
В TextView repository_name будет отображаться текст, полученный из свойства repositoryName переменной repository. Остается только связать переменную репозитория от xml до repository из MainActivity.kt.
Нажмите Build, чтобы сгенерировать библиотеку привязки данных для создания необходимых классов, вернитесь в MainActivity и добавить две строки:
Если вы запустите приложение, вы увидите, что в TextView появится «Habrahabr Android Repository Article». Хорошая функция, да? :)
Но что произойдёт, если мы сделаем следующее:
Отобразится ли новый текст через 2 секунды? Нет, не отобразится. Вы должны заново установить значение repository. Что-то вроде этого будет работать:
Но это скучно, если нужно будет делать это каждый раз, когда мы меняем какое-то свойство. Существует лучшее решение, называемое Property Observer.
Давайте сначала опишем, что такое паттерн Observer, нам понадобится это в разделе rxJava:
Property Observer, в нашем случае, представляет собой XML-макет, который будет прослушивать изменения в экземпляре Repository. Таким образом, Repository является наблюдаемым. Например, как только свойство name класса Repository изменяется в экземпляре класса, xml должен обновится без вызова:
Как сделать это с помощью библиотеки привязки данных? Библиотека привязки данных предоставляет нам класс BaseObservable, который должен быть реализован в классе Repository:
BR — это класс, который автоматически генерируется один раз, когда используется аннотация Bindable. Как вы можете видеть, как только новое значение установлено, мы узнаём об этом. Теперь вы можете запустить приложение, и вы увидите, что имя репозитория будет изменено через 2 секунды без повторного вызова функции executePendingBindings ().
Для этой части это всё. В следующей части я напишу о паттерне MVVM, паттерне Repository и об Android Wrapper Managers. Вы можете найти весь код здесь. Эта статья охватывает код до этого коммита.
Внимание! В статье описывается создание приложения версии 1.0. Текущий код в репозитории может отличаться от описываемого в статье.
Наверное, некоторые из вас знают, что помимо языка программирования Kotlin JetBrains также разрабатывает библиотеку Anko, для создания UI приложения, в качестве замены обычным XML-файлам. Мы не будем использовать его в нашем проекте, дабы не ставить в затруднительное положение людей не знакомых с Anko.
Для написания приложений на языке Kotlin, Android Studio нужен специальный плагин. Инструкцию по установке плагина можно найти здесь. Также не забудьте отключить функцию «Instant Run» в настройках Android Studio, т. к. на данный момент она не поддерживается плагином Kotlin.
Для корректной работы генерации кода нужно использовать версию плагина не ниже 1.0.1. Я использовал версию Kotlin 1.0.2 EAP. Вот так выглядит файл build.gradle приложения в моем проекте:
Итак, для начала нам нужно определиться что же мы будем писать? Недолго думая я остановился на приложении-заметках. Название тоже придумалось легко — Notelin. Приложение очень простое и состоит из двух экранов:
— Главный экран — содержит в себе список с заметками
— Экран заметки — здесь можно смотреть/редактировать содержание выбранной заметки
Требования к приложению небольшие:
— Добавление/просмотр/удаление заметки
— Просмотр информации о заметке
— Сортировка заметок по заголовку и по дате
— Поиск по заголовкам заметок
Для работы с базой данных я буду использовать библиотеку Android Active. Урок по работе с ней можно найти по этой ссылке. Для реализации Depency Injection была использована библиотека Dagger 2. На Хабре есть много статей по работе с ней. Основой всего приложения будет библиотека Moxy. С ее помощью мы реализуем паттерн MVP в нашем проекте. Она полностью решает проблемы жизненного цикла, благодаря чему вы можете не переживать о пересоздании компонентов вашего приложения. Также мы воспользуемся набором расширений для языка Kotlin в Android — KAndroid. Про остальные библиотеки я буду рассказывать по ходу дела.
Ниже приведен список зависимостей проекта:
А вот так выглядит список зависимостей приложения:
Обратите внимание, что вместо apt я использую kapt. Это плагин для Gradle, позволяющий аннотировать Kotlin-элементы.
Вот так выглядит структура нашего проекта в конечном варианте:
У заметок будет четыре поля:
- Дата создания
- Дата изменения
- Заголовок
- Текст
Реализуем все это в коде:
По этой модели библиотекой ActiveAndroid будет создана БД, в которой будут храниться наши заметки. Если вы заметили, у нас есть два конструктора: пустой и с параметрами. Первый конструктор будем использовать мы, а второй — ActiveAndroid. Наша модель наследуется от класса Model, благодаря чему мы можем сохранять и удалять наши заметки просто вызывая методы save() и delete(), например:
Но прежде чем использовать нашу модель, нам нужно прописать кое-какие мета-данные в Manifest-файле:
Думаю, все понятно без комментариев. Осталось унаследовать класс Application от com.activeandroid.app.Application:
Чтобы приложение было менее зависимо от БД я создал обертку NoteDao над нашей моделью, в которой будут происходить все операции по созданию, сохранению, обновлению и удалению заметок:
Наверное, вы заметили, что для создания объектов мы не использовали ключевое слово new — это отличие Kotlin от Java.
Также является главным экраном приложения. На нем пользователь может добавить/удалить заметку, просмотреть информацию о заметке, отсортировать их по дате или названию, а также произвести поиск по заголовкам.
Создаем MainView и MainPresenter
Теперь нам нужно перевести все это в код. Для начала создадим интерфейс нашей View:
Далее мы реализуем созданный интерфейс в нашей активити:
Одной из особенностей Kotlin, является то, что наследование и реализация интерфейсов указывается через двоеточие после имени класса. Также не имеет разницы идет название родительского класса перед интерфейсами, после или даже между ними, главное, чтобы класс в списке был один. Т. е. запись выше могла бы выглядеть так:
Если же вы попытаетесь добавить через запятую название еще одного класса, то IDE выдаст ошибку и подчеркнет красной линией название класса, который идет вторым.
Пока оставим методы пустыми. Как видите, активити наследуется от MvpAppCompatActivity. Это нужно для того, чтобы активити могла восстанавливать состояние при повороте экрана.
Создадим класс презентер:
Презентер также наследуется от MvpPresenter, которому мы указываем с какой View мы будем работать.Осталось инжектировать нашу модель в презентер. Для этого мы создаем модуль — поставщика NoteDao:
Создадим Component для инжектирования презентера:
Теперь нам нужно создать статический экземпляр класса AppComponent в классе Application:
Теперь мы можем инжектировать нашу модель в презентере:
Для взаимодействия MainView и MainPresenter нам нужно создать переменную в MainActivity:
Плагин Moxy сам привяжет View к фрагменту и произведет другие необходимые действия.
Создадим разметку экрана со списком и плавающей кнопкой. Файл activity_main.xml:
Для реализации летающей кнопки я использовал библиотеку FloatingActionButton. Google уже добавили FAB в support-библиотеку, поэтому вы можете воспользоваться их решением.
Укажем нашей Activity, какой макет она должна показывать:
Далее нам нужно связать FAB и список, чтобы при прокручивании списка вверх кнопка исчезала:
Нам не нужно писать порядком надоевший findViewById, нужно лишь прописать одну строчку в блоке с import'ами:
Как видите, последний пакет совпадает с названием нашего xml-файла. IDE автоматически инициализирует свойства (property) наших View и их имена совпадают с ID, которые мы указали в разметке.
Давайте реализуем загрузку заметок из БД. Заметки нужно загружать только один раз и использовать их в последующем. В этом нам поможет метод onFirstViewAttach класса MvpPresenter, который вызывается единожды при первой привязке View к презентеру. Далее, сколько бы мы не крутили и вертели нашу Activity, данные будут закешированы в презентере.
Создадим адаптер для нашего списка:
В адаптере мы используем метод formatDate. Он служит для форматирования даты в строку:
Данный метод находится в файле DateUtils.kt и мы можем использовать как обычный статический метод. Отличие от статического метода здесь в том, что метод принадлежит не классу, а пакету и нам не нужно писать имя класса перед названием метода. В аннотации мы указываем название класса, через который мы будем обращаться к методу из Java. Например в Java дынный метод будет вызываться так:
В методе onNotesLoaded нашей Activity мы показываем наши заметки:
Насколько я знаю, для обработки клика по элементам RecycleView не существует «официального» OnItemClickListener. Поэтому мы воспользуемся своим решением:
В методе onCreate нашей Activity пишем:
Функция with позволяет не писать каждый раз имя переменной, а только лишь вызывать методы у объекта, который мы передали в нее. Обратите внимание, что для получения Activity я использовал не просто this, а this@MainActivity. Это связано с тем, что при использовании this в блоке with, возвращается объект, который мы передали в функцию with. При обычном клике по пункту мы переходим на Activity, где мы можем просмотреть текст нашей заметки. При долгом нажатии появляется контекстное меню. Если вы заметили, перед закрывающей скобкой я не написал слово return. Это не ошибка, а особенность языка Kotlin.
Вот что происходит при нажатии на пункт меню в презентере:
Мы еще не создали класс NoteActivity, поэтому компилятор будет выдавать ошибку. Для решения этой проблемы можно создать класс NoteActivity или вовсе закомментировать код внутри метода openNote. Запись NoteActivity::class.java аналогична NoteActivity.class в Java. Также заметьте, что мы обращаемся к списку не через метод get(position), а через квадратные скобки, как к обычному массиву.
При использовании MVP-библиотеки Moxy в своем приложении, нам нужно привыкать, что все действия с View, такие как показ/закрытие диалога и другие, должны проходить через презентер. Изначально это не очень привычно и неудобно, но пользы от этого гораздо больше, т. к. мы можем быть уверены, что при пересоздании Activity наше диалоговое окно никуда не пропадет.
Я не буду показывать код контекстного меню, удаления и показа информации о заметке т. к. статья получается очень большой. Но, думаю, общий смысл вы уловили. Также следует отметить, что метод hideNoteContextDialog у презентера должен вызываться даже при закрытии диалога через кнопку назад или при нажатии на область за границами диалога.
При нажатии на FAB должна создаваться новая заметка:
Для создания новой заметки мы вызываем у презентера функция openNewNote:
Метод openNewNote использует созданный нами ранее openNote, в который мы передаем Context и позицию заметки в списке.
Реализуем поиск по заметкам
Давайте добавим поиск по заметкам. Создайте в папке res/menu файл main.xml:
В MainActivity пишем:
При изменении текста в поле поиска мы передаем строку из поля в презентер, после чего показываем результаты в списке. На самом деле, у SearchView нет метода onQueryChange, его добавила библиотека KAndroid.
Реализуем поиск в презентере:
Обратите внимание, как красиво, в одну строчку мы реализовали поиск по списку с помощью метода filter и лямбд. В Java тот же функционал занял бы 6-7строк. Осталось отобразить результаты поиска:
Реализуем сортировку заметок
И последний этап в создании главного экрана, это сортировка заметок. Добавим в res/menu/main.xml следующие строки:
Теперь нам нужно обработать нажатие на пункты меню:
Оператор when является более функциональным аналогом switch-case в Java. Код сортировки в MainPresenter:
Теперь нам нужно создать экран с содержанием заметки. Здесь пользователь может просмотреть/отредактировать заголовок и текст заметки, сохранить или удалить ее, а также просмотреть информацию о заметке.
Создаем NoteView и NotePresenter
Экран содержит всего лишь три View:
-Заголовок
-Дата последнего изменения
-Текст заметки
А вот и сама разметка:
В начале статьи я мельком упомянул об Anko. Библиотека позволяет существенно сократить код, не теряя при этом в удобочитаемости. Вот так, например, выглядела бы наша разметка при использовании Anko:
Но не будем отвлекаться и приступим к написанию кода. Первым делом нам нужно создать View:
Имплементируем NoteView в NoteActivity:
В onCreate мы извлекаем id заметки, чтобы презентер достал заметку из БД и передал данные во View. Создадим презентер:
Не забудьте добавить в класс AppComponent строку:
Покажем нашу заметку:
Реализуем сохранение заметки
Для сохранения заметки нам нужно выбрать соответствующий пункт в меню. Создайте файл res/menu/note.xml:
Опять же, я не стал приводить код удаления и вывода информации о заметке. При просмотре исходного кода, вы можете заметить, что помимо идентификатора заметки я передал в NoteActivity позицию заметки в списке. Это нужно для того, чтобы при удалении заметки на экране просмотра заметки, она также удалялась из списка. Для реализации этого функционала я использовал EventBus. И опять, я не стал приводить код.
На этом все: заметки добавляются, редактируются и удаляются. Также мы можем осуществить поиск и сортировку заметок. Обязательно посмотрите полный исходный код, ссылку на который я привел в конце статьи, чтобы лучше понять как все устроено.
Конечно же, нельзя забывать о людях, которые помогли мне при написании статьи. Хотел бы выразить благодарность хабраюзерам Юрию Шмакову (@senneco) за помощь с его библиотекой Moxy и за помощь по другим вопросам. Также, хочу сказать спаcибо сотруднику JetBrains Роману Белову (@belovrv) за ревью статьи и за предоставленный код на Anko.
UPD: Еще хотел сказать спасибо Sirikid за EPIC COMMIT, благодаря которому я переделал внушительную часть кода с использованием фич Kotlin'а.
Надеюсь, эта статья смогла убедить вас в том, что писать приложения на Kotlin не трудно, а может даже и легче, чем на Java. Конечно же, могут встречаться и баги, которые сотрудники JetBrains достаточно быстро фиксят. Если у вас появились какие-либо вопросы, вы можете задать их напрямую разработчикам на Slack-канале. Также вы можете почитать статьи о разработке на Kotlin здесь.
В настоящее время мы переживаем бум появления новых технологий и подходов к написанию мобильных приложений. Одной из них является развивающийся SDK от компании JetBrains для мультиплатформенной разработки Kotlin Multiplatfrom (KMP) .
Основная идея KMP, как и других кросс-платформенных SDK — оптимизация разработки путем написания кода один раз и последующего его использования на разных платформах.
Согласно концепции JetBrains, Kotlin Multiplatform не является фреймворком. Это именно SDK, который позволяет создавать модули с общим кодом, подключаемые к нативным приложениям.
Написанный на Kotlin модуль компилируется в JVM байткод для Android и LLVM байткод для iOS.
Этот модуль (Shared, Common) содержит переиспользуемую бизнес-логику. Платформенные модули iOS/Android, к которым подключен Shared/Common, либо используют написанную логику напрямую, либо имплементируют свою реализацию в зависимости от особенностей платформы.
Общая бизнес-логика может включать в себя:
- сервисы для работы с сетью;
- сервисы для работы с БД;
- модели данных.
Также в нее могут входить архитектурные компоненты приложения, напрямую не включающие UI, но с ним взаимодействующие:
- ViewModel;
- Presenter;
- Интеракторы и т.п.
Концепцию Kotlin Multiplatform можно сравнить с реализацией Xamarin Native. Однако, в KMP нет модулей или функционала, реализующих UI. Эта логическая нагрузка ложится на подключенные нативные проекты.
Рассмотрим подход на практике и попробуем написать наше первое приложение Kotlin Multiplatform.
Для начала нам потребуется установить и настроить инструменты:
Мы рассмотрим создание проекта с помощью Intelij IDEA.
Выбираем меню File → New → Create Project:
В появившемся окне выбираем тип проекта Kotlin → Mobile Android/iOS|Gradle
Далее стандартно задаем путь к JDK, имя и расположение проекта
После нажатия кнопки Finish проект сгенерируется и будет почти готов к работе.
Рассмотрим, что у нас получилось:
Мультиплатформенные проекты Kotlin обычно делятся на несколько модулей:
- модуль переиспользуемой бизнес-логики (Shared, commonMain и т.п);
- модуль для IOS приложения (iOSMain, iOSTest);
- модуль для Android приложения (androidMain, androidTest).
В них располагается наша бизнес-логика. Сам код базового примера мы разберем немного позже.
Код нативного Android приложения располагается в каталоге main, как если бы мы создавали проект по шаблону обычного Android.
iOS приложение создается автоматически и располагается в каталоге iOSApp:
Перед тем, как мы проверим работоспособность базового решения, необходимо сделать следующие финальные настройки:
В local.properties зададим путь к SDK Android:
Создадим конфигурацию для работы Android приложения:
Теперь вызовем команду gradle wrapper для сборки нашего модуля общей логики:
После сборки модуль для бизнес-логики для Android приложения доступен в app/build/libs:
Путь к библиотеке прописывается стандартно, в блоке dependencies файла build.gradle:
Теперь наш проект сконфигурирован для запуска Android приложения:
Осталось сделать настройки для запуска приложения iOS.
В файле build.gradle(:app) необходимо изменить настройку архитектура проекта, чтобы наше приложение поддерживало как реальные устройства, так и эмуляторы.
После выполнения сборки создастся фреймворк в app/build/bin/ios:
Intelij IDEA автоматически создает в gradle файле код для генерации, подключения и встраивания фреймворка в IOS проект:
При ручной настройке проекта (например, через Android Studio) этот код потребуется указать самостоятельно.
После синхронизации gradle iOS проект готов к запуску и проверке с помощью XCode.
Проверяем, что у нас получилось. Открываем проект iOS через iosApp.xcodeproj:
Проект имеет стандартную структуру, за исключением раздела app, где мы получаем доступ к коду наших модулей на Kotlin.
Фреймворк действительно подключен автоматически во всех соответствующих разделах проекта:
Запускаем проект на эмуляторе:
Теперь разберем код самого приложения на базовом примере.
Используемую в проекте бизнес-логику можно разделить на:
- переиспользуемую (общую);
- платформенную реализацию.
Переиспользуемая логика располагается в проекте commonMain в каталоге kotlin и разделяется на package. Декларации функций, классов и объектов, обязательных к переопределению, помечаются модификатором expect :
Реализация expect -функционала задается в платформенных модулях и помечается модификатором actual :
Вызов логики производится в нативном проекте:
Все очень просто.
Теперь попробуем по тем же принципам сделать что-то посложнее и поинтереснее. Например, небольшое приложение для получения и отображение списка новостей для iOS и Android.
Приложение будет иметь следующую структуру:
В общей части (Common) расположим бизнес-логику:
- сетевой сервис;
- сервис для запросов новостей.
В модулях iOS/Android приложений оставим только UI компоненты для отображения списка и адаптеры. iOS часть будет написана на Swift, Android – на Kotlin. Здесь в плане работы не будет ничего нового.
Cравнение фреймворков для кроссплатформенной мобильной разработки: React Native, Flutter, Ionic, Xamarin и PhoneGap
Организуем архитектуру приложений по простому паттерну MVP. Презентер, обращающийся к бизнес-логике, также вынесем в Common часть. Также поступим и с протоколом для связи между презентером и экранами UI:
Начнем с бизнес-логики. Т.к весь функционал будет в модуле common, то мы будем использовать в качестве библиотек решения для Kotlin Multiplatform:
1. Ktor – библиотека для работы с сетью и сериализации.
В build.gradle (:app) пропишем следующие зависимости:
Также добавим поддержку плагина сериализации:
2. Kotlin Coroutines – для организации многопоточной работы.
При добавлении зависимости в iOS проект обратите внимание, что версия библиотеки должна быть обязательно native-mt и совместима с версией плагина Kotlin multiplatform.
При организации многопоточности с помощью Coroutines необходимо передавать контекст потока ( CoroutineContext ), в котором логика будет исполняться. Это платформозависимая логика, поэтому используем кастомизацию с помощью expect / actual .
В commonMain создадим Dispatchers.kt, где объявим переменные:
Реализация в androidMain создается легко. Для доступа к соответствующим потокам используем CoroutineDispatchers Main (UI поток) и Default (стандартный для Coroutine ):
С iOS труднее. Та версия Kotlin Native LLVM компилятора, которая используется в Kotlin Multiplatform, не поддерживает background очереди. Это давно известная проблема, которая к сожалению, еще не исправлена
Поэтому попробуем обходной маневр как временное решение проблемы.
Мы создаем свой CoroutineDispatcher , где прописываем выполнение логики в асинхронной очереди dispatch_async .
Также нам понадобится свой scope для работы сетевого клиента:
iOS
Android
Применим это при реализации сетевого клиента на Ktor:
Парсинг реализуем с помощью сериализатора типа KSerializer . В нашем случае это NewsList.serializer() . Пропишем реализацию в сервисе новостей:
Вызывать бизнес-логику будем в презентере. Для полноценной работы с coroutines нам надо будет создать scope:
и добавить его в презентер. Вынесем в базовый класс:
Теперь создадим презентер NewsListPresenter для нашего модуля. В инициализатор передадим defaultDispatcher :
Обратите внимание! Из-за особенностей текущей работы Kotlin Native с многопоточностью в IOS работа с синглтонами может привести к крашу. Поэтому для корректной работы надо добавить аннотацию @ThreadLocal для используемого объекта:
Осталось подключить логику к нативным IOS и Android модулям и обработать ответ от Presenter:
Запускаем сборку common модуля gradle wrapper, чтобы сборки обновились. Проверяем работу приложений:
Однако, если вы ещё лишь задумываетесь о переходе на Kotlin — будь это в рамках разработки новых фич вашего проекта на Java и написания первых тестов к нему, либо разработка нового проекта с чистого листа — сейчас всё ещё прекрасное время для этого. Советы из статьи будут вам полезны на определённых этапах такого переходного периода.
Вероятно, самый закономерный вопрос, который первым делом придет вам в голову в процессе предательства Java в пользу Kotlin, — «с чего же начать?».
Открываем двери для Kotlin
В то время как на просторах интернета, в том числе в официальной документации языка и на платформе для разработчиков, можно найти доступные руководства по созданию Kotlin-проекта и/или настройки Kotlin в Android Studio и Gradle, в этом разделе я сфокусируюсь лишь на первых потенциальных ловушках на вашем пути предательства Джавы.
Если в своём существующем проекте вы используете annotation processing или библиотеки, которые на нём базируются (в большинстве случаев это DI-фреймворки вроде Dagger 2 и других, Data Binding, Butterknife), без предварительных правок в build.gradle файлах на уровне ваших модулей, они просто перестанут собираться без подключённого плагина kapt (собственный annotation processor у Kotlin):
Также чрезвычайно важный шаг — замена всех вхождений annotationProcessor конфигурации в вашем build.gradle на kapt .
Генерация стабов в данный момент поддерживается из-под коробки.
Подготовка сознания к Null Safety
Важное отличие Kotlin от Java — поддержка первым nullable-типов null-safety «из-под коробки». Поэтому не поленитесь начать знакомство с языком с раздела официальной документации, который довольно исчерпывающе раскрывает основные аспекты этого мощного механизма и работы с ним (в том числе при двухстороннем взаимодействии с Java-кодом).
Первое свидание с Kotlin: data classes
До того как Google объявил, что поддерживает Kotlin на официальном уровне, довольно часто можно было встретить среди рекомендаций — начать процесс перехода с написания unit-тестов на этом языке. И пусть этот совет имеет огромный смысл, ведь unit-тесты — действительно наименее агрессивный путь экспансии Kotlin на кодовую базу и при этом свободный от рисков наткнуться на странные ошибки, связанные с interop двух языков. Однако вряд ли вы испытаете катарсис, который способен принести «сладкий» и лаконичный синтаксис языка, подталкивающий к тем же приемам функционального программирования (unit-тесты в большинстве случаев — исключительно императивный стиль). Попробуйте параллельно начать делать вкрапления языка с написания POJO с помощью data-классов, о которых вы наверняка, даже не будучи знакомым с языком на практике, могли слышать раньше.
Предположим, вам нужно написать класс сущности для фильма Movie. Одной строчки кода будет достаточно!
Помимо лаконичности здесь вы получаете:
— Переопределённые методы equals() , hashCode() и toString() под капотом;
— Immutable класс, неявно наследующийся от Any (в отличие от Object в Java) с immutable-полями (но это не точно) и неявными публичными геттерами и сеттерами для каждого. Создание экземпляра такого класса будет выглядеть так (обратите внимание на отсутствие ключевого слова new ):
— Метод copy() , который позволяет клонировать экземпляр данного класса и может быть полезен в том случае, если вы, например, пожелаете создать новый неизменяемый объект на основе существующего, но с отличающимися значениями одного или нескольких полей (при условии, что они не private). Такой подход будет для вас первым шагом навстречу функциональному стилю:
Предупреждение № 1 При создании экземпляра такого класса на Java с помощью copy() вам придётся определить значения для каждого из полей.
Предупреждение № 2 На всякий случай предупрежу, что этот метод недоступен для экземпляров обычных (не data) классов.
— Поддержка значений по умолчанию, которой можно заменить использование Builder-паттерна.
Имейте в виду, что data-классы пока что не могут наследоваться друг от друга. Однако вы можете счесть полезными sealed-классы, неявно абстрактные. Например, в тех случаях, когда вам необходимо определить различные состояния загрузки данных или экрана. Или в любых других ситуациях, где ограниченные иерархии классов будут уместными.
Data-классы + Parcelable
Начиная с версии 1.1.4, больше нет необходимости писать boilerplate-реализации методов parcelable для поддержки де/сериализации ваших объектов, так как за вас это сделает аннотация @Parcelize.
Только не забудьте применить Android Extensions plugin:
И определить значение experimental-флага как true .
Экспериментальный статус расширения (к моменту написания этого материала) указывает на тот факт, что перед вами всё ещё не окончательный вариант этого API. Есть вероятность глубоко зарытых багов в его работе (пока что я с таковыми не сталкивался, но возможно всё). И обновление API в будущем потенциально способно «поломать» ваш код, и вам стоит использовать эту аннотацию в продакшн-коде на свой страх и риск.
Data-классы в сочетании с часто используемыми библиотеками
Если вы используете замечательную Room Persistence Library, вы всё так же при подключённом kapt можете писать data-классы для сущностей вашей базы данных, которые прекрасно работают с Room-аннотациями.
С Nullable-свойствами также нет никаких проблем. Но на тот случай, если вам вдруг станет любопытно так же, как нашей команде в своё время, поддерживает ли Room значения по умолчанию Kotlin, вас ждёт разочарование.
А что насчёт библиотек для сериализации данных?
В случае с одним из самых популярных в этой области GSON, необходимости в дополнительных конвертерах и плагинах нет. Библиотека будет сериализовать ваши POJO с таким же успехом, как и их Java-версии:
Однако так же, как и в случае с Room, значения по умолчанию не поддерживаются.
Для совместимости Jackson c Kotlin вам необходимо добавить зависимость специального модуля.
Аналогичным образом дела обстоят и с Moshi, для совместимости с которым потребуется добавить зависимость:
Сводим Kotlin с Java: Interoperability
Предположим, теперь вы готовы зайти дальше уровня data-классов и начать писать на Kotlin более сложные классы для ваших активити, фрагментов, presenter-, view model-, interactor-, repository- и других классов в зависимости от вашей архитектуры.
Разумеется, с некоторой вероятностью в перспективе вы планируете избавиться от Джавы в вашем проекте чуть более, чем полностью. Но перед этим вам придётся пройти длинный путь устранения конфликтов между языками, которые могут разрушить вашу жизнь (по крайней мере на пару часов).
Так как Kotlin был изначально спроектирован как JVM-язык, полностью совместимый с Java и наоборот, вы без труда можете наследоваться от существующих Java-классов, обращаться к ним и применять Java-аннотации к вашим Kotlin-классам и методам.
Существует несколько НО, образовавшихся под влиянием идеологических различий между двумя языками (о ключевых вы можете прочитать здесь).
Going Static
Например, в Kotlin нет ключевого слова static , просто смиритесь с этим.
Но не стоит отчаиваться, ведь в вашем распоряжении есть companion object . Это механизм объявления объекта внутри класса, способного содержать внутри константы и методы, синтаксически доступные со стороны Java в таком же виде, как и статические поля или методы Java. Но для того, чтобы при компиляции сгенерировались и статический метод класса, в котором находится этот объект, и метод этого объекта сам по себе, пометьте его как @JvmStatic (по умолчанию без этой аннотации метод companion-объекта будет вам доступен с помощью ссылки на Companion инстанс). Эта страница документации подробно раскрывает тему companion objects, создания синглтонов и object expressions в Kotlin.
Но прежде чем вы заключите все ваши константы в companion object-ы по всему проекту, возьмите во внимание тот факт, что такие объекты не так дешевы, как кажутся. Очень рекомендую всем, кто переходит на Kotlin, ознакомиться с циклом статей «Kotlin’s hidden costs» (part 1, part 2, part 3), открывающих обратную сторону (с точки зрения байткода) синтаксического сахара языка. Рекомендую обратить особое внимание на советы об использовании inline-функций для оптимизации производительности ваших лямбда-выражений, а также на советы по избежанию избыточной автоупаковки / автораспаковки «под капотом».
Коллекции
Когда речь идёт о коллекциях, Kotlin полностью полагается на классы стандартной библиотеки Java, расширяя их возможности с помощью дополнительных функций для их объявления ( listOf() , mapOf() , etc) и их модификаций и преобразований. Они довольно часто оказываются полезными и удобными в использовании, и с коллекциями как таковыми в Kotlin в целом всё довольно прозрачно. Ну… почти 😉 Обратите внимание на то, что List, Map и Set — это алиасы для неизменяемых JDK-реализаций коллекций, и попытки изменения их содержимого выльются в UnsupportedOperationException, что логично.
Generics
Обобщения в Kotlin тоже поддерживаются, но существуют важные различия в их реализации между двумя языками, пренебрежение которыми может привести вас к непредвиденным конфузам. Эта разница тщательно описана в документации и множестве тематических статей, поэтому вместо дублирования фактов просто оставлю ссылки на самые, на мой взгляд, ценные статьи, раскрывающие ключевые для дженериков в Kotlin концепции ковариантности, контравариантности и инвариантности:
Возвращаемся к unit-тестам
Если вы работаете по TDD/BDD или хотя бы пишете unit-тесты к уже готовой бизнес-логике вашего приложения (ремарка: всегда, при малейшей возможности, покрывайте ваш код тестами), высока вероятность, что вы будете использовать для этих нужд Mockito.
Однако первым камнем преткновения при написании unit-тестов к Kotlin-классам может оказаться отсутствие поддержки со стороны Mockito из-под коробки создания моков к final-классам. В Kotlin классы всегда final по умолчанию, до тех пор, пока вы явно не обозначите их как open. Согласно «Effective Java», 3rd Edition, Item 19: Design and document for inheritance or else prohibit it, это вполне резонное решение. В данной ситуации вы можете поступить так:
- либо открыть тестируемый класс для наследования с помощью упомянутого модификатора open ;
- либо применить небольшой хак к Mockito, создав файл с названием org.mockito.plugins.MockMaker , обязательно в папке test/resources/mockito-extensions . Внутри он должен содержать строку: mock-maker-inline .
Она подключит плагин, который будет генерировать моки для final-классов, что позволит вам писать к ним жизнеспособные тесты.
Также от себя хочу порекомендовать библиотеку Ника Хаармана mockito-kotlin, которая предоставляет множество полезных вспомогательных функций. Они способны с помощью возможностей Котлина подсластить инициализацию моков, верификацию обращений к ним, etc, с помощью Mockito.
А например, вспомогательные inline-методы, возможностями которых воспользовался автор библиотеки, позволяют определять поведение мока сразу же при его инициализации. Например, можем сразу же задать возвращаемое значение для метода получения фильмов мок-объекта MoviesRepository:
Kotlin Android Extensions
Написание любого класса для Activity мы чаще всего начинаем с определения layout-а внутри onCreate метода. Если вы используете базирующиеся на annotation-processing альтернативы для получения вьюх из xml-файла классическому findViewById -подходу или используете его же, посмотрите в сторону плагина Kotlin Android Extensions (не путайте с Android KTX 🙂 ), в который вы можете с некоторой вероятностью влюбиться с первых же строк кода.
С помощью него вы сможете ссылаться на вьюхи по их идентификаторам, указанным в xml, напрямую, так как доступные синтетические свойства для них уже сгенерированы.
Больше об этом плагине и его подключении можно прочитать, перейдя по этой ссылке.
Примечание И сразу остановлюсь на опасном моменте при обращении к синтетическим свойствам, определяющим идентификаторы вьюх, с которым столкнулась вся наша команда. Если вы используете include-блоки в ваших xml-layouts, убедитесь, что каждый из таких блоков НЕ переопределяет идентификатор корневого layout во включённом layout-файле.
Например, при такой конфигурации вас ждёт неминуемый креш:
В данном случае исходный id включенного TextView (randomTextView) переопределён и таким образом не может быть найден. Оптимальным решением будет вообще при такой возможности оставить саму include-область без идентификатора, чтобы избежать конфузов.
Kotlin и Data Binding
Если вы используете в проекте DataBinding и не имеете возможности быстро мигрировать на Kotlin Android Extensions, вы можете обнаружить множество ошибок компиляции после перехода на kapt.
Чтобы вернуть поддержку совместимости Data Binding с Котлином, необходимо добавить в список зависимости в build.gradle зависимость компилятора:
X.y.z. здесь определяют текущую версию Gradle: они должны совпадать.
Потенциальные ловушки при общении с SDK, написанными на Java
В то время, как вам неизбежно придётся работать с SDK, написанными на Java (включая, собственно, Android SDK), нужно всегда оставаться на стороже, когда речь идёт о nullable аргументах (по умолчанию в Java), открытых для переопределения методов.
Предположим, вам нужно переопределить onActivityResult в вашем Activity.
Замечаете что-нибудь подозрительное в этом сниппете?
Всегда есть шанс случайно упустить оператор ? после типа аргумента метода — в данном случае Intent, — который допускал бы null-значения. Здесь же, с точки зрения Kotlin-кода, data не может быть null ни при каких обстоятельствах, и, вне зависимости от того, укажете ли вы тип Intent как nullable или нет, вы не получите ни предупреждения, ни ошибки от компилятора, так как оба варианта сигнатуры допустимы. Но поскольку получение не пустой data не гарантировано, так как в случаях с SDK вы не можете это проконтролировать, получение null в данном случае приведёт к NPE. В случае, если вы укажете Intent?-тип, любые попытки вызвать метод такого объекта приведут к ошибке на этапе компиляции без должной проверки на null посредством ?-оператора.
Работаем с лямбдами: Android SDK
Как вы могли заметить в одном из сниппетов кода выше, с функции высшего порядка избавляют нас от большого количества бойлерплейт-кода — в том числе, когда речь идёт о реализации собственных click listenerов. Начать постижение дзен в написании красивых и производительных лямбд (и, к примеру, понять, в каких случаях уместно пренебречь фигурными скобками <> для достижения максимальной лаконичности), можно с этой страницы документации.
В этой статье мы сфокусируемся лишь на паре интересных кейсов, при которых лямбды могут значительно упростить (или наоборот, при неведении — усложнить) жизнь.
Предположим, что мы уже используем Kotlin Android Extensions в проекте. Давайте реализуем простые адаптер в связке с ViewHolder для списка фильмов на основе RecyclerView, каждый элемент в котором кликабелен, и выбор каждого должен быть каким-то образом обработан извне.
Вы наверняка заметили (Movie) -> Unit в качестве последнего параметра конструктора. В данном случае Movie выступает в качестве типа получателя, а Unit — в качестве результата выполнения функции и, таким образом, заменяет гипотетический click listener интерфейс, который вам бы пришлось реализовывать, имей вы дело с Java.
Ещё один участок, который мог привлечь ваше внимание, — класс MovieHolder сам по себе. Как видите, он реализует интерфейс LayoutContainer, который преобразовывает ViewHolder класс элемента списка в контейнер и таким образом активирует кеширование для вьюхи элемента (стратегию кеширования вы вполне можете кастомизировать).
Важно Не забудьте переопределить свойство containerView: View в конструкторе вашего ViewHolder.
И, наконец, вы определённо могли заметить выражение с with, которое, на самом деле, представляет из себя вызов одноимённой встроенной функции, позволяющей вам вызывать серию из методов текущего объекта.
Ещё одна ремарка Обратите внимание, что return внутри лямбды возвращает нас не из функции, в которой лямбда вызывается, а из самого лямбда-выражения.
Kotlin vs RxJava(2): And then… Nothing!
Kotlin значительно устраняет многословность RxJava в связке с Java (при условии, что вы не использовали Retrolambda или Jack) и полностью совместим с ней. Однако вселенная — место не самое идеальное, и на ловушки можно наткнуться и в этом тандеме.
Спойлер: всё из-за скобочек <>.
В своё время я стал жертвой печально известной проблемы с andThen оператором, которая подробно описана в статье «Kotlin and Rx2. How I wasted 5 hours because of wrong brackets». Передача лямбды в качестве параметра метода приведёт к разочаровывающему экзистенциальному ничему. Достаточно написать простой тест для выражения вроде
чтобы убедиться в этом воочию: содержимое andThen не выполнится. Всё потому, что пока в случае с большинством операторов вроде flatMap , defer , fromAction и огромного количества других, в качестве их аргументов ожидается действительно лямбда, при такой записи с andThen ожидается Completable/Observable/SingleSource . Проблема решается использованием обыкновенных круглых скобок () вместо фигурных <>.
К моменту публикации статьи этот баг всё ещё живёт застывшим в состоянии «under discussion».
Выводы
Эта статья покрывает лишь небольшой объём распространённых камней преткновения и вопросов, с которыми вы можете столкнуться в процессе перехода с Java на Kotlin. Без достаточного практического опыта он не всегда может оказаться столь же простым, каким кажется со стороны.
Однако, на мой взгляд, он того стоит. Даже решение открыть себя для новый язык программирования, выйти из зоны комфорта Java послужит важной вехой в вашем развитии как Android-разработчика. Так что при любой возможности — плавно, итеративно попробуйте перестроить своё сознание, свой проект на Kotlin. Помните, что все эти маленькие вкрапления боли, описанные в статье, — ничто по сравнению с тем, насколько вам приятно будет писать код на этом языке.
Читайте также: