Duolingo для Android был Java-приложением в течение первых пяти лет своего существования. Теперь, еще два года спустя, это 100% Kotlin! Эта миграция на Kotlin оказалась для нас огромным успехом с точки зрения удобства сопровождения кода и счастья разработчиков.
В интернете уже есть много ресурсов для изучения Kotlin, поэтому в этом посте мы сосредоточимся на собственном опыте развертывания Kotlin-кода для миллионов пользователей.
Почему Kotlin
Когда мы впервые начали рассматривать Kotlin в начале 2018 года, поддержка Android в нем появилась менее года назад. Еще не было понятно, достигнет ли Kotlin такого уровня популярности, который у него есть сейчас, и станет ли он предпочтительным языком Android-разработки у Google.
Основные преимущества, которые мы ожидали:
- Продуктивность. Kotlin гораздо менее многословен, чем Java, что делает его более быстрым и простым как для написания, так и (что более важно!) поддержки. Его бесшовная совместимость с Java и консервативный подход к добавлению новых языковых функций позволяют легко освоиться любому Android-разработчику.
- Стабильность. В нашем Android-репозитории более 100 коммитов из Java-разработки вида «Исправление сбоя NullPointerException». Функции безопасности Kotlin не позволяют большему количеству исключений достигать пользователей и дают нам возможность сосредоточиться на других проблемах во время проверки кода, поскольку существует гораздо меньше шаблонного кода, который нужно проверять.
- Счастье разработчиков. Kotlin стал одним из самых любимых языков пользователей Stack Overflow в 2018 году, уступив только Rust. Наши собственные разработчики положительно отреагировали на аналогичные языковые изменения на двух наших других основных платформах: мы перешли на Swift в нашем приложении для iOS и полностью переписали duolingo.com на TypeScript.
Конечно, были и некоторые риски, в первую очередь из-за того, что миграция на Kotlin может не окупить своих затрат в будущем времени разработки. Другая проблема заключалась в том, что Kotlin, как CoffeeScript, может в конечном итоге устареть из-за бэкпорт-улучшений того самого языка, который он сам намеревался улучшить.
В конечном итоге наши разработчики Android единодушно решили, что преимущества были достаточно велики, чтобы оправдать политику написания всего нового кода Android на Kotlin, хотя мы еще не были готовы к полной миграции всего существующего кода.
Ускорение разработчиков
Все разработчики Android в Duolingo встречаются раз в две недели для обсуждения последних и предстоящих изменений платформы, неофициальных постмортемов, ответов и вопросов. Первые несколько этих собраний были посвящены вводным презентациям Kotlin, основанным на таких источниках, как официальное руководство по языку, Kotlin Koans, официальные документы для Android и шпаргалкам MindOrks.
Каждому разработчику Android был назначен определенный код Java для переноса на Kotlin. Мы создали новую роль «Проверяльщик Kotlin» (Kotlin checker), чтобы более опытные разработчики Kotlin могли поделиться передовым опытом во время проверки кода; членов в этой роли постепенно становилось все больше, пока в нее не вошли все наши Android-разработчики, и необходимость в ней не исчезла.
Инструментарий Kotlin
С самого начала мы всегда обеспечивали согласованность кода, докеризируя наш инструментарий Kotlin и применяя его при проверке статусов предварительных коммитов и пул реквестов в GitHub.
Мы связываем весь код Kotlin с помощью детектов, инспекций IntelliJ, Android Lint и нашего собственного линтера на основе регулярных выражений Splinter.
Для автоматического форматирования кода мы запускаем ktlint как часть общего pre-commit хука, используемого всеми репозиториями в компании. (Другим основным претендентом был форматер IntelliJ, который, как мы обнаружили, медленнее и немного более привередлив в работе в Docker.)
Как только мы переработали примерно 10% Java, мы удалили PMD, SpotBugs и большинство проверок из нашего конвейера CI. Продолжение работы этих специфичных для Java инструментов замедлило бы нашу скорость разработки, но не принесло бы большой пользы.
Преобразование старого Java-кода
Чтобы сделать code review переноса кода в Kotlin настолько безболезненным, насколько это возможно, мы рекомендуем обрабатывать каждый исходный файл в своем собственном пул-запросе, содержащем как минимум три отдельных коммита:
- Запустить автоконвертер IDE. Этот коммит отвечает за перенос большей части кода и не нуждается в тщательном рассмотрении, поскольку в целом он безопасен в отношении ошибок выполнения, хотя может привести к ошибкам во время компиляции.
- Исправить ошибки компиляции. Исправления обычно просты в реализации, например, добавление аннотаций @JvmStatic, где это необходимо.
- Рефакторинг. Автор должен сделать код более идиоматическим в Kotlin, например, используя sumBy вместо цикла for.
Мы обнаружили, что преобразование файла Java в Kotlin уменьшило количество строк в среднем примерно на 30%, а в некоторых случаях даже на 90%!
Хотя перенос старого кода удобно укладывался в рамки роли Android-инженера нашей платформы, мы ожидали, что нашим продуктовым командам будет сложнее расставить приоритеты. Мы призвали разработчиков продуктовых команд конвертировать свои наиболее часто используемые файлы, когда у них появляется свободное время, и — в соответствии с духом Duolingo — мы геймифицировали этот процесс, проводя конкурс с ежедневной таблицей лидеров. В итоге разработчики продукта обеспечили около половины всех конверсий.
Миграция на Kotlin: камни преткновения
Инструментальная экосистема Kotlin намного меньше, чем Java. Она более чем достаточна для наших нужд — мы анализируем наш Kotlin-код примерно так же агрессивно, как мы уже работали с Java.
Время от времени мы по-прежнему получаем NullPointerExceptions и IllegalArgumentExceptions от сторонних зависимостей Java (таких, как сама платформа Android), которые не следуют наилучшей практике использования аннотаций nullability, не оставляя компилятору Kotlin никакой возможности узнать, возвращаются ли параметры методов или что возвращаемые значения могут быть нулевыми Эта ситуация улучшается с течением времени, когда Google возвращается и аннотирует свои общедоступные API.
Kotlin до сих пор не имеет встроенной поддержки некоторых функций Java — от необычных (вызывающих статические защищенные методы суперкласса) до мистических (квалифицированные вызовы конструктора суперкласса), но подобные проблемы достаточно легко обойти.
Миграция на Kotlin: результаты
Количество строк в нашей кодовой базе Android росло на 46% год от года, пока мы не начали использовать Kotlin в начале 2018 года. Два года, много новых функций в продукте и более чем вдвое больше активных разработчиков — наша кодовая база сейчас почти такого же размера, как была два года назад!
Счастье Android-разработчиков, согласно NPS, увеличилось на 129 пунктов за это время, причем большинство разработчиков назвали Kotlin (и наш инструментарий для него) главным фактором.
Теперь мы также поддерживаем Kotlin, наряду с Python и Java, как главный язык для создания backend служб в Duolingo, что потребовало лишь небольших дополнительных усилий, поскольку мы можем повторно использовать код Java из наших существующих сервисов и инструментарий Kotlin из нашего Android-репозитория.
В целом мы очень рады, что перешли на Kotlin, и рады, что его использование продолжает расти как внутри нашей компании, так и во всей индустрии программного обеспечения!