- PVSM.RU - https://www.pvsm.ru -

Чему я научился, конвертируя проект в Kotlin при помощи Android Studio

К большой моей радости, мне наконец выдалась возможность поработать с популярным языком Kotlin — конвертировать простенькое приложение из Java при помощи инструмента Convert Java File to Kotlin [1] из Android Studio. Я опробовал язык и хотел бы рассказать о своем опыте.

Я быстро убедился, что этот инструмент конвертирует большую часть классов в Java практически безукоризненно. Но кое-где пришлось подчистить за ним код, и в процессе я выучил несколько новых ключевых слов!

Ниже я поделюсь своими наблюдениями. Прежде, чем мы начнем, замечу: если вы в какой-то момент захотите взглянуть, что происходит «под капотом», Android Studio позволяет отслеживать все процессы; просто перейдите в панели по следующему пути: Tools → Kotlin → Show Kotlin Bytecode.

Чему я научился, конвертируя проект в Kotlin при помощи Android Studio - 1

Константа long

Первое изменение я сперва даже не заметил — настолько оно было незначительным. Волшебным образом конвертер заменил константу long в одном из классов на int и преобразовывал ее обратно в long при каждом обращении. Брр!

companion object {
    private val TIMER_DELAY = 3000
}
//...
handler.postDelayed({
    //...
}, TIMER_DELAY.toLong())

Хорошая новость: Константа все равно распознавалась благодаря ключевому слову val.

Плохая новость: многие процессы сопровождались ненужными преобразованиями. Я ожидал, что безопасность типов в Kotlin будет на более высоком уровне, что там все будет реализовано лучше. Может быть, я переоценил, насколько умен этот конвертер?

Решение оказалось простым: нужно было просто добавить «L» в конце объявления переменой (примерно как в Java).

companion object {
    private val TIMER_DELAY = 3000L
}
//...
handler.postDelayed({
    //...
}, TIMER_DELAY)

Лучше поздно, чем никогда

Одно из главных преимуществ Kotlin — безопасность null [2], которая устраняет угрозу возникновения нулевых ссылок. Осуществляется это при помощи системы типов, которая различает ссылки, допускающие и не допускающие значение null. В большинстве случаев для вас предпочтительнее ссылки, не допускающие значения null, с которыми нет риска столкнуться с NPE (Null Pointer Exceptions). Однако в некоторых ситуациях нулевые ссылки могут быть полезны, например, при инициализации из события onClick(), такого как AsyncTask.

Существует несколько способов наладить работу с нулевыми ссылками:

  1. Старые-добрые операторы if, которые будут проверять свойства на наличие нулевых ссылок, прежде чем дать к ним доступ (Java должен был вас уже к ним приучить).
  2. Крутой Safe Call Operator [3] (синтаксис ?.), который проводит за вас проверку на нулевые значения в фоновом режиме. Если объект — нулевая ссылка, то он возвращает ноль (не NPE). Никаких больше надоедливых операторов if!
  3. Насильственное возвращение NPE при помощи оператора !!.. В этом случае вы фактически пишете знакомый по Java код и вам необходимо вернуться к первому шагу.

Определить, на каком именно паттерне остановиться, чтобы обеспечить безопасность null — непростая задача, поэтому конвертер по умолчанию выбирает самое простое решение (третье), позволяя разработчику справляться с проблемой оптимальным для его кейса образом.

Я понимал, что разрешить коду Kotlin выбрасывать null pointer exception — это как-то идет вразрез с преимуществами, которые дает данный язык, и стал копать глубже в надежде найти решение, которое оказалось бы лучше уже имеющихся.

Так я обнаружил мощное ключевое слово lateinit. При помощи lateinit в Kotlin можно инициализировать ненулевые свойства [4] после вызова конструктора, что дает возможность вообще отойти от нулевых свойств.

Это значит, что я получаю все плюсы второго подхода без необходимости прописывать дополнительные «?.». Я просто обращаюсь с методами так, будто они в принципе не бывают нулевыми, не тратя время на шаблонные проверки и используя тот синтаксис, к которому привык.

Использование lateinit — это простой способ убрать операторы!!! из кода на Kotlin. Если вам интересны другие советы о том, как от них избавиться и сделать код аккуратнее, рекомендую пост David Vávra [5].

Internal и его внутренний мир

Так как конвертацию я проводил от класса к классу, мне стало интересно, как уже конвертированные классы будут взаимодействовать с теми, которые пока еще остаются на Java. Я читал, что Kotlin идеально совместим с Java [6], так что, по логике вещей, все должно было бы работать без видимых изменений.

У меня был публичный метод в одном фрагменте, который конвертировался в функцию internal [7] в Kotlin. На Java у него не было никаких модификаторов доступа [8], и, соответственно, он был package private.

public class ErrorFragment extends Fragment {
    void setErrorContent() {
        //...
    }
}

Конвертер заметил отсутствие модификаторов доступа и решил, что метод должен быть видимым только в пределах модуля / пакета, применив ключевое слово internal, чтобы задать параметры видимости [9].

class ErrorFragment : Fragment() {
    internal fun setErrorContent() {
        //...
    }
}

Что означает это новое ключевое слово? Заглянув в декомпилированный биткод мы тут же увидим, что название метода из setErrorContent() превратилось в setErrorContent$production_sources_for_module_app().

public final void setErrorContent$production_sources_for_module_app() {
   //...
}

Хорошая новость: в других классах Kotlin достаточно знать исходное название метода.

mErrorFragment.setErrorContent()

Kotlin сам переведет его в сгенерированное имя. Если снова взглянуть на декомпилированный код, можно увидеть, как осуществлялся перевод.

// Accesses the ErrorFragment instance and invokes the actual method
ErrorActivity.access$getMErrorFragment$p(ErrorActivity.this)
    .setErrorContent$production_sources_for_module_app();

Таким образом, Kotlin разбирается с изменениями в названиях своими силами. А как насчет остальных классов на Java?

Из Java класса вызвать метод errorFragment.setErrorContent() нельзя — ведь этого «внутреннего» метода на самом деле не существует (так как изменилось название).

Метод setErrorContent() теперь невидим для классов на Java, как можно увидеть и в API, и в окошке Intellisense в Android Studio. Так что придется использовать сгенерированное (и очень громоздкое) название метода.

Чему я научился, конвертируя проект в Kotlin при помощи Android Studio - 2

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

Сложности с компаньоном

Kotlin не допускает публичных статических переменных и методов [10], которые столь типичны для Java. Вместо этого не предлагает такой концепт, как объект-компаньон [11], который отвечает за поведение статических объектов и интерфейсов в Java.

Если вы создаете константу в классе на Java [12], а затем конвертируете его в Kotlin, то конвертер не распознает, что переменная static final должна применяться как константа, что может привести к помехам в совместимости Java и Kotlin.

Когда вам нужна константа в классе Java, вы создаете переменную static final:

public class DetailsActivity extends Activity {
    public static final String SHARED_ELEMENT_NAME = "hero";
    public static final String MOVIE = "Movie";

    //...

}

Как видите, после конвертирования, все они оказались в классе компаньона:

class DetailsActivity : Activity() {

    companion object {
        val SHARED_ELEMENT_NAME = "hero"
        val MOVIE = "Movie"
    }

    //...
}

Когда их используют другие классы Kotlin, все происходит так, как и следовало ожидать:

val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra(DetailsActivity.MOVIE, item)

Однако, так как Kotlin, конвертируя константу, помещает ее в собственный класс компаньона, доступ к таким константам из Java класса не интуитивен.

intent.putExtra(DetailsActivity.Companion.getMOVIE(), item)

Декомпилируя класс в Kotlin, мы можем заметить, что константы стали приватными и раскрываются через класс-оболочку компаньона.

public final class DetailsActivity extends Activity {
   @NotNull
   private static final String SHARED_ELEMENT_NAME = "hero";
   @NotNull
   private static final String MOVIE = "Movie";
   public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
   //...

   public static final class Companion {
      @NotNull
      public final String getSHARED_ELEMENT_NAME() {
         return DetailsActivity.SHARED_ELEMENT_NAME;
      }

      @NotNull
      public final String getMOVIE() {
         return DetailsActivity.MOVIE;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

В результате код получается куда сложнее, чем хотелось бы.

Хорошая новость: частично исправить положение и добиться желаемого поведения мы можем, введя ключевое слово const [13] в класс компаньона.

class DetailsActivity : Activity() {

    companion object {
        const val SHARED_ELEMENT_NAME = "hero"
        const val MOVIE = "Movie"
    }

    //...
}

Теперь, если взглянуть на декомпилированный код, мы увидим наши константы! Но увы, в конечном счете мы все равно создаем пустой класс компаньона.

public final class DetailsActivity extends Activity {
   @NotNull
   public static final String SHARED_ELEMENT_NAME = "hero";
   @NotNull
   public static final String MOVIE = "Movie";
   public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
   //...
   public static final class Companion {
      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

Зато доступ из Java классов происходит по обычной схеме!

Прошу заметить, что это метод работает только для примитивов и строк [13]. Чтобы узнать больше о не-примитивах, почитайте JvmField [14] и статью Kotlin’s hidden costs [15].

Чему я научился, конвертируя проект в Kotlin при помощи Android Studio - 3

Циклы, и как Kotlin их совершенствует

По умолчанию Kotlin конвертирует циклы в диапазоне с границами 0..N-1, чем затрудняет сопровождение кода, увеличивая вероятность возникновения ошибок на единицу [16].

В моем коде, к примеру, был вложенный цикл for, чтобы добавлять карты в каждый ряд — самый обычный пример цикла for на Android.

for (int i = 0; i < NUM_ROWS; i++) {
    //...
    for (int j = 0; j < NUM_COLS; j++) {
        //...
    }
    //...
}

Конвертирование прошло без особых ухищрений.

for (i in 0..NUM_ROWS - 1) {
    //...
    for (j in 0..NUM_COLS - 1) {
        //...
    }
    //...
}

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

Как пишет в своем блоге Dan Lew [17], у Kotlin функция диапазона инклюзивна по умолчанию. Однако, ознакомившись с характеристиками диапазона [18] у Kotlin, я нашел их очень хорошо проработанными и гибкими. Мы можем упростить код и сделать его читабельнее, воспользовавшись возможностями, которые они предлагают.

for (i in 0 until NUM_ROWS) {
    //...
    for (j in 0 until NUM_COLS) {
        //...
    }
    //...
}

Функция until делает циклы неинклюзивными и более простыми для чтения. Можно наконец выкинуть все эти нелепые -1 из головы!

Полезные советы для продвинутых

Для ленивых

Иногда бывает полезно лениво загрузить переменную member. Представьте, что у вас класс типа singleton, который управляет списком данных. Каждый раз создавать этот список заново нет необходимости, так что мы зачастую обращаемся к ленивому геттеру [19]. Паттерн получается в таком духе:

public static List<Movie> getList() {
    if (list == null) {
        list = createMovies();
    }
    return list;
}

Если конвертер попытается конвертировать этот паттерн, код не скомпилируется, так как list прописан как неизменяемый, при том что у createMovies() изменяемый тип возвращаемого значения. Компилятор не позволит вернуть изменяемый объект, если сигнатура метода задает неизменяемый.

Чему я научился, конвертируя проект в Kotlin при помощи Android Studio - 4

Это очень мощный паттерн для делегирования загрузки объекта, поэтому Kotlin подключает особую функцию, lazy [20], чтобы упростить загрузку ленивым способом. С ее помощью код компилируется.

val list: List<Movie> by lazy {
    createMovies()
}

Так как последняя строка — это возвращаемый объект, теперь мы можем создавать объект, который требует меньше кода, чтобы лениво его загрузить!

Деструктуризация

Если вам приходилось деструктурировать массивы или объекты на javascript [21], то объявления по деструктуризации [22] покажутся вам знакомыми.

На Java мы постоянно создаем и перемещаем объекты. Однако в некоторых случаях нам нужно буквально несколько свойств объекта, и бывает жаль времени на то, чтобы извлекать их в переменные. Если же речь идет о большом количестве свойств, проще получить к ним доступ через геттер. Например, так:

final Movie movie = (Movie) getActivity()
        .getIntent().getSerializableExtra(DetailsActivity.MOVIE);

// Access properties from getters
mMediaPlayerGlue.setTitle(movie.getTitle());
mMediaPlayerGlue.setArtist(movie.getDescription());
mMediaPlayerGlue.setVideoUrl(movie.getVideoUrl());

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

val (_, title, description, _, _, videoUrl) = activity
        .intent.getSerializableExtra(DetailsActivity.MOVIE) as Movie

// Access properties via variables
mMediaPlayerGlue.setTitle(title)
mMediaPlayerGlue.setArtist(description)
mMediaPlayerGlue.setVideoUrl(videoUrl)

Не приходится удивляться, что в декомпилированном коде методы у нас ссылаются на геттеры в классах данных.

Serializable var10000 = this.getActivity().getIntent().getSerializableExtra("Movie");
Movie var5 = (Movie)var10000;
String title = var5.component2();
String description = var5.component3();
String videoUrl = var5.component6();

Конвертер оказался достаточно умным, чтобы упростить код путем деструктуризации объекта. Тем не менее, я бы посоветовал почитать про лямбды и деструктуризацию [23]. В Java 8 существует распространенная практика заключать параметры лямбда-функции в скобки, если их больше одного, но в Kotlin это может быть интерпретировано как деструктуризация.

Заключение

Использование инструмента для конвертирования в Android Studio стало для меня отличным первым шагом в освоении Kotlin. Но, проглядев некоторые участки полученного кода, я был вынужден начать глубже вникать в этот язык, чтобы найти более эффективные способы писать на нем.

Хорошо, что меня предупредили: после конвертации код нужно обязательно вычитать. Иначе на Kotlin у меня получилось бы нечто маловразумительное! Хотя, честно говоря, у меня и на Java с этим не лучше.

Если вы хотите узнать другую полезную информацию о Kotlin для начинающих, советую прочитать этот пост [24] и посмотреть видео [25].

Автор: nanton

Источник [26]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/259912

Ссылки в тексте:

[1] Convert Java File to Kotlin: https://developer.android.com/kotlin/get-started.html#convert-to-kotlin-code

[2] безопасность null: https://kotlinlang.org/docs/reference/null-safety.html

[3] Safe Call Operator: https://kotlinlang.org/docs/reference/null-safety.html#safe-calls

[4] инициализировать ненулевые свойства: https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties

[5] пост David Vávra: https://android.jlelse.eu/how-to-remove-all-from-your-kotlin-code-87dc2c9767fb?gi=26743d7b8c09

[6] Kotlin идеально совместим с Java: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html

[7] internal: https://kotlinlang.org/docs/reference/visibility-modifiers.html

[8] модификаторов доступа: https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html

[9] параметры видимости: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#visibility

[10] не допускает публичных статических переменных и методов: https://kotlinlang.org/docs/reference/classes.html#companion-objects

[11] объект-компаньон: https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects

[12] в классе на Java: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#static-fields

[13] const: https://kotlinlang.org/docs/reference/properties.html#compile-time-constants

[14] JvmField: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#instance-fields

[15] Kotlin’s hidden costs: https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62

[16] ошибок на единицу: https://en.wikipedia.org/wiki/Off-by-one_error

[17] Dan Lew: http://blog.danlew.net/2017/06/05/musings-on-kotlin-ranges/

[18] характеристиками диапазона: https://kotlinlang.org/docs/reference/ranges.html

[19] ленивому геттеру: https://en.wikipedia.org/wiki/Lazy_initialization#Java

[20] lazy: https://kotlinlang.org/docs/reference/delegated-properties.html#lazy

[21] javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

[22] объявления по деструктуризации: https://kotlinlang.org/docs/reference/multi-declarations.html

[23] про лямбды и деструктуризацию: https://kotlinlang.org/docs/reference/multi-declarations.html#destructuring-in-lambdas-since-11

[24] этот пост: https://developer.android.com/kotlin/get-started.html

[25] видео: https://www.youtube.com/watch?v=czKo-jPVweg

[26] Источник: https://habrahabr.ru/post/332598/