Как с треском провалить миграцию с Java на Kotlin в Android приложении


С тех пор, как Google объявила об официальной поддержке Kotlin в Android, всё больше разработчиков хотят использовать его в своих новых и существующих проектах. Поскольку я также большой поклонник Kotlin, я не мог дождаться, когда смогу использовать Kotlin в своём рабочем проекте. В конце концов, Kotlin полностью совместим с Java, и все разработчики просто в восторге от этого. Так что же может пойти не так?


Ну, на самом деле многое может пойти не так. Меня просто пугает то, что на странице официальной документации Android «Начало работы с Kotlin» написано, что если вы хотите перенести существующее приложение на Kotlin, вы должны просто начать писать модульные тесты, а затем, после небольшого опыта работы с этим языком, вы должны писать новый код на Kotlin, а существующий Java-код просто конвертировать.


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


Познакомьтесь с моим маленьким проектом


У меня был проект, который я создал более года назад на Java, и мне нужно было немного его изменить и добавить один новый экран. Я подумал, что это отличная возможность проверить, действительно ли миграция существующего проекта на Kotlin очень проста, как все говорят. Исходя из рекомендаций официальной документации, я начал с написания на Kotlin модульных тестов. Новые классы я тоже писал на Kotlin и конвертировал некоторые существующие. Казалось, что всё замечательно, но через некоторое время я обнаружил одну неприятную проблему.


Kotlin + Lombok = :(


В моем приложении я использовал библиотеку Lombok для генерации геттеров и сеттеров. Lombok — это обработчик аннотаций для javac. К сожалению, компилятор kotlin использует javac без обработки аннотаций [источник]. Это означает, что методы, созданные при помощи Lombok, будут недоступны в Kotlin:


Методы, созданные при помощи Lombok, будут недоступны в Kotlin


Но вы можете сказать: «Хорошо, это будет не очень круто, но в принципе можно вручную создать геттеры и сеттеры для тех полей, которые будут необходимы в коде Kotlin. В конце концов, не нужно этого делать во всём проекте сразу».


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


Kotlin + Lombok + Dagger 2 = :(((


В моём приложении используется Dagger 2 для внедрения зависимостей. При создании нового экрана я обычно создаю структуру MVP: Activity, Presenter, Component, Module и Contract. Все зависимости для презентора внедряются с помощью Dagger. Activity вызывает DaggerSomeComponent.builder().(...).build().inject(this), чтобы внедрить презентор с необходимыми для себя зависимостями.


Использование Dagger 2 вместе с Kotlin — не проблема. Только перед этим нужно применить kapt-плагин, который создает необходимые самогенерируемые классы для Dagger.


И вот здесь всё начинает разваливаться


Без kapt-плагина я не мог использовать сгенерированные Dagger-классы в файлах Kotlin. Но после того, как я добавил этот плагин, все методы, созданные Lombok, исчезли!


До применения kapt-плагина:


До применения kapt-плагина


После применения kapt-плагина:


После применения kapt-плагина


И, к сожалению, решения этой проблемы нет. Вы можете применять либо только kapt-плагин, либо только Lombok. К счастью, поскольку это был всего лишь мой маленький проект, я просто удалил Lombok и сам написал геттеры и сеттеры. Но в этом проекте было всего около 50 сгенерированных методов. В проекте, который мы поддерживаем на работе, у нас их около тысячи. Удаление Lombok из этого приложения просто невозможно.


Также, очевидно, что отказ от kapt-плагина — не выход из ситуации. Без него вы не сможете использовать Kotlin в классах, где используется Dagger. В моем случае я должен был бы реализовать Activity, Component и Module на Java, и только Contract и Presenter могли быть написаны на Kotlin. А смешивание файлов Java и Kotlin — это определенно не здорово. Вместо плавного перехода с Java на Kotlin вы просто создали бы большой беспорядок.


Вот так выглядела бы эта ужасная полиглотная MVP-структура без kapt-плагина:


Ужасная полиглотная MVP-структура без kapt-плагина


Но я всё же хочу перейти на Kotlin. Что мне делать?


Один из способов — использовать разные модули. Kotlin не увидит методы, которые Lombok будет генерировать в исходном коде, но будет видеть их в байт-коде.


Лично мне кажется, что это и есть самый предпочтительный путь. Если вы выводите Kotlin в отдельные зависимые модули для каждой фичи, вы уменьшаете риск таких проблем с совместимостью, с которыми столкнулся я, или более сложных, перечисленных в официальном гайде.


И это ещё не всё. В смешивании файлов Kotlin и Java без чёткого разделения есть много других недостатков. Это заставляет всех разработчиков, работающих с вами над проектом, знать оба языка. Также это уменьшает читаемость кода и может привести к увеличению времени сборки [источник].


Кратко


  • методы, сгенерированные Lombok, не видны в Kotlin;
  • использование kapt-плагина ломает Lombok;
  • без kapt-плагина вы не cможете использовать самогенерируемые классы для Dagger в Kotlin, что означает, что вам всё равно придётся писать новый код на Java;
  • способ решить эту проблему — вывести Kotlin в отдельные модули;
  • смешивание файлов Kotlin и Java в больших проектах без чёткого разделения может привести к неожиданным проблемам совместимости.

Комментарии (24)


  1. semyong
    06.09.2018 16:00

    Можно применить delombok и постепенно переписывать на kotlin.


  1. r_ii
    06.09.2018 17:35

    А мне вот интересно зачем тривиальные геттеры/сеттеры?


    1. Guitariz
      06.09.2018 17:46

      безопасность обращения к переменным.


      1. r_ii
        06.09.2018 18:05

        И в чем же безопасность проявляется? Ну возьмем например класс Movie из второго рисунка. Чем тривиальный геттер безопаснее public final поля?


        1. Bringoff
          06.09.2018 18:10
          +1

          Как вариант, поля не final, потому что где-то внутри класса модифицируются (мы ведь не видим весь класс, верно?), но снаружи менять их по очевидным причинам нельзя.


        1. Guitariz
          06.09.2018 18:13

          final — ничем. Но только если final. Lombok все же неамного шире.


      1. svr_91
        07.09.2018 08:27

        Вот интересно, что в C++ даже в Core Guidelines призывают не использовать геттеры/сеттеры, а использовать структуры (CoreGuidelines)
        В результате, в классах есть ну максимум один геттер/сеттер, а все остальное — это нормальные методы с нормальными именами, выполняющие нетривиальную функциональность. Неужели в яве с этим такие проблемы?


    1. AlexanderG
      06.09.2018 17:51
      +1

      ИМХО, один из важнейших недостатков Java в сравнении с C# это отстутствие автопропертей. Весь этот геттеро-сеттерный бойлерплейт дико замусоривает классы и по размеру сравним иной раз с кодом, реализующим функциональность.


      1. Bringoff
        06.09.2018 18:11
        +1

        В общем-то, это как раз одна из причин, почему люди выбирают Kotlin.


      1. franzose
        07.09.2018 01:01

        В Kotlin есть data-классы, можно было воспользоваться ими)


  1. Bringoff
    06.09.2018 18:14

    Мораль проста — не используйте Lombok. Я сейчас на большом проекте, где он используется, и за полгода им так и не проникся. В тривиальных случаях он помогает избавиться от нескольких строк бойлерплейта, но как-то это недостаточно весомая причина для +1 источника магии в коде.


    1. DrAlan
      06.09.2018 22:52

      Возможно в вашем проекте он не так уж и обходим.
      Например у меня много микро-сервисов. В не которых из них могут быть поджо на десятки полей. Классы могут инжектиться до десятком (очень редко конечно) сервисов.
      В первом случае писать толпу гетр сетеров во втором случае писать конструктор на несколько параметров и поддерживать его при изменениях кода.
      А так же отличный фукционал генерации статик конструкторов и билдеров.
      В целом на ломбок очень даже подходит на бэкенд задачах с толпой логики и энтетей.


      1. franzose
        07.09.2018 01:04

        Так ведь вместо кучки геттеров-сеттеров можно использовать public поля.


      1. Maccimo
        08.09.2018 00:27
        +1

        писать толпу гетр сетеров

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


  1. Prokky
    06.09.2018 20:06
    +1

    Пример на скриншоте c Movie по идее должен отлично заменяться data классом


    1. mitgard
      06.09.2018 22:52
      +1

      Как я понимаю, автор воспользовался котлином как Java с улучшенным синтаксисом, без его плюшек, хотя я могу ошибаться, я не пишу на котлине.


      1. Djaler
        07.09.2018 22:49

        Этот класс написан на джаве, не на котлине


  1. gildor
    06.09.2018 20:09

    В смешивании файлов Kotlin и Java без чёткого разделения есть много других недостатков.

    Это каких? Могли бы привести конкретные примеры? Собственно для таких юзкейсов Котлин и создавался и отлично с ними работает.
    Упомянутое замедление сборки может произойти конечно (из-за дополнительного анализа java кода компилятором Kotlin), но оно не критичное и на глаз его сложно заметить.


    Это заставляет всех разработчиков, работающих с вами над проектом, знать оба языка.

    Ну это не выглядит проблемой, без знания Java в Android разработке всяко никак, ну а обучить Java разработчика котлину на приемлемом уровне дело недели-двух


  1. franzose
    07.09.2018 00:56

    «pet project» — это не проект про домашних животных, это «детище, любимый проект». Хотя в контексте статьи ваш перевод этой фразы получился очень даже к месту))


    1. Devcolibri Автор
      07.09.2018 08:34

      Спасибо, поправили)


  1. yanex
    07.09.2018 08:34

    А сможете выложить этот игрушечный проект к нам в ишью-трекер (https://kotl.in/issue)?
    Мы бы поисследовали и, вероятно, исправили бы этот баг (впрочем, есть подозрение в неверной конфигурации проекта).


    1. Devcolibri Автор
      07.09.2018 08:35

      Это перевод статьи, поэтому, к сожалению, нет такой возможности.

      Напишите автору оригинальной статьи, думаю он с радостью это сделает.


      1. taujavarob
        07.09.2018 21:20

        Опять 25! Не видят люди никаких плашек! Мозг не приучен. Поди.


  1. anonymous
    08.09.2018 18:08
    -1

    Ужас какой. Какие геттеры/сеттеры?

    data class Movie(
        val id: Long,
        val title: String,
        val posterUrl: String
    )
    

    Всё! У вашего entity теперь есть и get (чтобы были set нужно val поменять на var), а также equals(), hashcode(), toString() и clone(). Метод clone() — правильный подход. Должны быть лишь геттеры. Хочешь менять — создавай клон!
    Перед тем, как перетаскивать проект с Java на Kotlin стоит Kotlin изучить.