Рефакторинг в Котлин

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

Эта лаборатория кода подходит для любого разработчика, использующего Java, который рассматривает возможность переноса своего проекта на Kotlin. Мы начнем с пары классов Java, которые вы преобразуете в Kotlin с помощью IDE. Затем мы взглянем на преобразованный код и посмотрим, как мы можем его улучшить, сделав его более идиоматичным и избежав распространенных ошибок.

Что вы узнаете

Вы узнаете, как конвертировать Java в Kotlin. При этом вы изучите следующие функции и концепции языка Kotlin:

  • Обработка обнуляемости
  • Реализация синглетонов
  • Классы данных
  • Обработка строк
  • Элвис оператор
  • Разрушение
  • Свойства и резервные свойства
  • Аргументы по умолчанию и именованные параметры
  • Работа с коллекциями
  • Функции расширения
  • Функции и параметры верхнего уровня
  • let , apply , with и run ключевые слова

Предположения

Вы уже должны быть знакомы с Java.

Что вам понадобится

Создать новый проект

Если вы используете IntelliJ IDEA, создайте новый проект Java с помощью Kotlin/JVM.

Если вы используете Android Studio, создайте новый проект без активности.

Код

Мы создадим объект модели User и одноэлементный класс Repository , который работает с объектами User и предоставляет списки пользователей и отформатированные имена пользователей.

Создайте новый файл с именем User.java в папке app/java/< yourpackagename > и вставьте в него следующий код:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

В зависимости от типа вашего проекта импортируйте androidx.annotation.Nullable , если вы используете проект Android, или org.jetbrains.annotations.Nullable противном случае.

Создайте новый файл с именем Repository.java и вставьте в него следующий код:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

Наша IDE может довольно хорошо выполнять автоматический рефакторинг кода Java в код Kotlin, но иногда требуется небольшая помощь. Сначала мы сделаем это, а затем пройдемся по рефакторингу кода, чтобы понять, как и почему он был рефакторинг таким образом.

Перейдите к файлу User.java и преобразуйте его в Kotlin: Строка меню -> Код -> Преобразовать файл Java в файл Kotlin .

Если ваша среда разработки предложит внести исправления после преобразования, нажмите « Да ».

Вы должны увидеть следующий код Kotlin:

class User(var firstName: String?, var lastName: String?)

Обратите внимание, что User.java был переименован в User.kt Файлы Kotlin имеют расширение .kt.

В нашем классе Java User у нас было два свойства: firstName и lastName . У каждого был метод получения и установки, что делало его значение изменяемым. Ключевое слово Kotlin для изменяемых переменных — var , поэтому преобразователь использует var для каждого из этих свойств. Если бы у наших свойств Java были только геттеры, они были бы неизменяемыми и были бы объявлены как переменные val . val похож на ключевое слово final в Java.

Одно из ключевых различий между Kotlin и Java заключается в том, что Kotlin явно указывает, может ли переменная принимать нулевое значение. Он делает это, добавляя ` ? ` к объявлению типа.

Поскольку мы пометили имя и lastName как обнуляемые, firstName автоматически пометил свойства как обнуляемые с помощью String? . Если вы аннотируете свои элементы Java как ненулевые (используя org.jetbrains.annotations.NotNull или androidx.annotation.NonNull ), конвертер распознает это и сделает поля ненулевыми также в Kotlin.

Базовый рефакторинг уже сделан. Но мы можем написать это более идиоматично. Посмотрим, как.

Класс данных

Наш класс User содержит только данные. В Kotlin есть ключевое слово для классов с этой ролью: data . Пометив этот класс как класс data , компилятор автоматически создаст для нас геттеры и сеттеры. Он также получит функции equals() , hashCode() и toString() .

Давайте добавим ключевое слово data в наш класс User :

data class User(var firstName: String, var lastName: String)

Kotlin, как и Java, может иметь первичный конструктор и один или несколько вторичных конструкторов. Тот, что в приведенном выше примере, является основным конструктором класса User. Если вы конвертируете класс Java с несколькими конструкторами, конвертер также автоматически создаст несколько конструкторов в Kotlin. Они определяются с помощью ключевого слова constructor .

Если мы хотим создать экземпляр этого класса, мы можем сделать это так:

val user1 = User("Jane", "Doe")

Равенство

В Kotlin есть два типа равенства:

  • Структурное равенство использует оператор == и вызывает equals() , чтобы определить, равны ли два экземпляра.
  • Ссылочное равенство использует оператор === и проверяет, указывают ли две ссылки на один и тот же объект.

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

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

В Kotlin мы можем присваивать значения по умолчанию аргументам в вызовах функций. Значение по умолчанию используется, когда аргумент опущен. В Kotlin конструкторы также являются функциями, поэтому мы можем использовать аргументы по умолчанию, чтобы указать, что значение по умолчанию для lastName равно null . Для этого мы просто присваиваем null для lastName .

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("John", "Doe")

Параметры функции могут быть названы при вызове функции:

val john = User(firstName = "John", lastName = "Doe") 

В качестве другого варианта использования предположим, что firstName имеет значение null в качестве значения по умолчанию, а lastName — нет. В этом случае, поскольку параметр по умолчанию будет предшествовать параметру без значения по умолчанию, вам придется вызывать функцию с именованными аргументами:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Прежде чем двигаться вперед, убедитесь, что ваш класс User является классом data . Давайте конвертируем класс Repository в Kotlin. Результат автоматического преобразования должен выглядеть так:

import java.util.*

class Repository private constructor() {
   private var users: MutableList<User?>? = null
   fun getUsers(): List<User?>? {
       return users
   }

   val formattedUserNames: List<String?>
       get() {
           val userNames: MutableList<String?> =
               ArrayList(users!!.size)
           for (user in users) {
               var name: String
               name = if (user!!.lastName != null) {
                   if (user!!.firstName != null) {
                       user!!.firstName + " " + user!!.lastName
                   } else {
                       user!!.lastName
                   }
               } else if (user!!.firstName != null) {
                   user!!.firstName
               } else {
                   "Unknown"
               }
               userNames.add(name)
           }
           return userNames
       }

   companion object {
       private var INSTANCE: Repository? = null
       val instance: Repository?
           get() {
               if (INSTANCE == null) {
                   synchronized(Repository::class.java) {
                       if (INSTANCE == null) {
                           INSTANCE =
                               Repository()
                       }
                   }
               }
               return INSTANCE
           }
   }

   // keeping the constructor private to enforce the usage of getInstance
   init {
       val user1 = User("Jane", "")
       val user2 = User("John", null)
       val user3 = User("Anne", "Doe")
       users = ArrayList<Any?>()
       users.add(user1)
       users.add(user2)
       users.add(user3)
   }
}

Посмотрим, что сделал автоматический преобразователь:

  • Добавлен блок init (Repository.kt#L50)
  • static поле теперь является частью блока companion object (Repository.kt#L33).
  • Список users может иметь значение null, поскольку экземпляр объекта не был создан во время объявления (Repository.kt#L7)
  • Метод getFormattedUserNames() теперь является свойством formattedUserNames (Repository.kt#L11).
  • Итерация по списку пользователей (который изначально был частью getFormattedUserNames( )) имеет синтаксис, отличный от синтаксиса Java (Repository.kt#L15).

Прежде чем идти дальше, давайте немного почистим код. Мы видим, что преобразователь заставил наших users перечислить изменяемый список, содержащий объекты, допускающие значение NULL. Хотя список действительно может быть нулевым, допустим, что он не может содержать нулевых пользователей. Итак, давайте сделаем следующее:

  • Удалить ? в User? в объявлении типа users
  • getUsers должен возвращать List<User>?

Автоконвертер также без необходимости разделил на 2 строки объявления переменных пользовательских переменных и переменных, определенных в блоке инициализации. Давайте поместим объявление каждой переменной в одну строку. Вот как должен выглядеть наш код:

class Repository private constructor() {
    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Блок инициализации

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

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Большая часть кода init обрабатывает инициализацию свойств. Это также можно сделать в декларации свойства. Например, в версии нашего класса Repository для Kotlin мы видим, что свойство users было инициализировано в объявлении.

private var users: MutableList<User>? = null

static свойства и методы Kotlin

В Java мы используем ключевое слово static для полей или функций, чтобы сказать, что они принадлежат классу, но не экземпляру класса. Вот почему мы создали статическое поле INSTANCE в нашем классе Repository . Эквивалентом Kotlin для этого является companion object блок. Здесь вы также должны объявить статические поля и статические функции. Преобразователь создал и переместил сюда поле INSTANCE .

Обработка синглетонов

Поскольку нам нужен только один экземпляр класса Repository , мы использовали шаблон singleton в Java. С Kotlin вы можете применить этот шаблон на уровне компилятора, заменив ключевое слово class на object .

Удалите частный конструктор и сопутствующий объект и замените определение класса на object Repository .

object Repository {

    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

При использовании класса object мы просто вызываем функции и свойства непосредственно на объекте, например:

val users = Repository.users

Разрушение

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

Например, классы данных поддерживают деструктурирование, поэтому мы можем деструктурировать объект User в цикле for на (firstName, lastName) . Это позволяет нам работать напрямую со значениями firstName и lastName . Обновим цикл for следующим образом:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

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

Вместо этого предпочтите обрабатывать значение NULL с помощью одного из следующих методов:

  • Выполнение нулевой проверки ( if (users != null) {...} )
  • Использование оператора elvis ?: (описано позже в лаборатории кода)
  • Использование некоторых стандартных функций Kotlin (рассмотренных позже в кодовой лаборатории)

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

При создании экземпляров типов коллекций Kotlin предоставляет несколько вспомогательных функций, чтобы сделать ваш код более читабельным и гибким. Здесь мы используем MutableList для users :

private var users: MutableList<User>? = null

Для простоты мы можем использовать mutableListOf() , предоставить тип элемента списка, удалить вызов конструктора ArrayList из блока init и удалить явное объявление типа свойства users .

private val users = mutableListOf<User>()

Мы также изменили var на val, потому что users будет содержать неизменяемую ссылку на список пользователей. Обратите внимание, что ссылка неизменяема, но сам список изменяем (вы можете добавлять или удалять элементы).

С этими изменениями наше свойство users теперь не равно null, и мы можем удалить все лишнее !! вхождения оператора.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Кроме того, поскольку переменная users уже инициализирована, мы должны удалить инициализацию из блока init :

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Так как как lastName , так и firstName могут быть null , нам нужно обрабатывать допустимость значений NULL при построении списка отформатированных имен пользователей. Поскольку мы хотим отображать "Unknown" , если какое-либо имя отсутствует, мы можем сделать имя ненулевым, удалив ? из объявления типа.

val name: String

Если lastName имеет значение null, name является либо firstName , либо "Unknown" :

if (lastName != null) {
    if (firstName != null) {
        name = "$firstName $lastName"
    } else {
        name = lastName
    }
} else if (firstName != null) {
    name = firstName
} else {
    name = "Unknown"
}

Это можно записать более идиоматически, используя оператор Элвиса ?: . Оператор elvis вернет выражение в левой части, если оно не равно нулю, или выражение в правой части, если левая часть равна нулю.

Итак, в следующем коде возвращается user.firstName , если оно не равно null. Если user.firstName имеет значение null, выражение возвращает значение справа "Unknown" :

if (lastName != null) {
    ...
} else {
    name = firstName ?: "Unknown"
}

Kotlin упрощает работу со String с помощью шаблонов String . Строковые шаблоны позволяют ссылаться на переменные внутри строковых объявлений.

Автоматический преобразователь обновил конкатенацию имени и фамилии, чтобы ссылаться на имя переменной непосредственно в строке с помощью символа $ и поместил выражение между { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

В коде замените конкатенацию строк на:

name = "$firstName $lastName"

В Kotlin выражения if , when , for и while возвращают значение. Ваша IDE даже показывает предупреждение о том, что присваивание должно быть удалено из if :

Давайте последуем предложению IDE и удалим назначение для обоих операторов if . Последняя строка оператора if будет присвоена. Таким образом, становится яснее, что единственная цель этого блока — инициализировать значение имени:

name = if (lastName != null) {
    if (firstName != null) {
        "$firstName $lastName"
    } else {
        lastName
    }
} else {
   firstName ?: "Unknown"
}

Далее мы получим предупреждение о том, что объявление name может быть присоединено к присваиванию. Применим и это. Поскольку тип переменной имени можно вывести, мы можем удалить явное объявление типа. Теперь наши formattedUserNames выглядят так:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(users.size)
       for ((firstName, lastName) in users) {
           val name = if (lastName != null) {
               if (firstName != null) {
                   "$firstName $lastName"
               } else {
                   lastName
               }
           } else {
               firstName ?: "Unknown"
           }
           userNames.add(name)
       }
       return userNames
   }

Давайте подробнее рассмотрим геттер formattedUserNames и посмотрим, как мы можем сделать его более идиоматичным. Прямо сейчас код делает следующее:

  • Создает новый список строк
  • Итерации по списку пользователей
  • Создает форматированное имя для каждого пользователя на основе имени и фамилии пользователя.
  • Возвращает только что созданный список
val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }

                userNames.add(name)
            }
            return userNames
        }

Kotlin предоставляет обширный список преобразований коллекций , которые делают разработку быстрее и безопаснее за счет расширения возможностей API коллекций Java. Одна из них — функция map . Эта функция возвращает новый список, содержащий результаты применения данной функции преобразования к каждому элементу исходного списка. Таким образом, вместо того, чтобы создавать новый список и перебирать список пользователей вручную, мы можем использовать функцию map и переместить логику, которая была у нас в цикле for , внутрь тела map . По умолчанию имя текущего элемента списка, используемого в map , это it , но для удобства чтения вы можете заменить it своим собственным именем переменной. В нашем случае назовем его user :

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Чтобы упростить это еще больше, мы можем полностью удалить переменную name :

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

Мы увидели, что автоматический конвертер заменил getFormattedUserNames() свойством formattedUserNames , которое имеет собственный геттер. Под капотом Kotlin по-прежнему генерирует метод getFormattedUserNames() , который возвращает List .

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

Логика, которая была запущена в функции Java getFormattedUserNames() , теперь запускается при вызове геттера свойства formattedUserNames Kotlin.

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

Однако иногда нам нужны некоторые дополнительные функции, которые не предоставляет автоматическое резервное поле. Давайте рассмотрим пример ниже.

Внутри нашего класса Repository у нас есть изменяемый список пользователей, который отображается в функции getUsers() , сгенерированной из нашего Java-кода:

fun getUsers(): List<User>? {
    return users
}

Проблема здесь в том, что возвращая users любой потребитель класса Repository может изменить наш список пользователей — не очень хорошая идея! Давайте исправим это, используя резервное свойство.

Во-первых, давайте переименуем users в _users . Теперь добавьте общедоступное неизменяемое свойство, которое возвращает список пользователей. Назовем это users :

private val _users = mutableListOf<User>()
val users: List<User>
      get() = _users

С этим изменением частное свойство _users становится вспомогательным свойством для общедоступного свойства users . Вне класса Repository список _users нельзя изменить, поскольку потребители класса могут получить доступ к списку только через users .

Полный код:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Прямо сейчас класс Repository знает, как вычислить отформатированное имя пользователя для объекта User . Но если мы хотим повторно использовать ту же логику форматирования в других классах, нам нужно либо скопировать и вставить ее, либо переместить в класс User .

Kotlin предоставляет возможность объявлять функции и свойства вне любого класса, объекта или интерфейса. Например, mutableListOf() , которую мы использовали для создания нового экземпляра List , определена непосредственно в Collections.kt из стандартной библиотеки.

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

Видимость функций и свойств расширения можно ограничить с помощью модификаторов видимости. Они ограничивают использование только классами, которым нужны расширения, и не загрязняют пространство имен.

Для класса User мы можем либо добавить функцию расширения, которая вычисляет отформатированное имя, либо мы можем хранить отформатированное имя в свойстве расширения. Его можно добавить вне класса Repository , в том же файле:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Затем мы можем использовать функции расширения и свойства, как если бы они были частью класса User .

Поскольку отформатированное имя является свойством пользователя, а не функцией класса Repository , воспользуемся свойством расширения. Наш файл Repository теперь выглядит так:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Стандартная библиотека Kotlin использует функции расширения для расширения функциональности нескольких API Java; многие функции Iterable и Collection реализованы как функции расширения. Например, функция map , которую мы использовали на предыдущем шаге, является функцией расширения для Iterable .

В коде нашего класса Repository мы добавляем несколько пользовательских объектов в список _users . Эти вызовы можно сделать более идиоматическими с помощью функций области видимости.

Чтобы выполнять код только в контексте определенного объекта, без необходимости доступа к объекту на основе его имени, Kotlin создал 5 функций области видимости: let , apply , with , run и also . Короткие и мощные, все эти функции имеют приемник ( this ), могут иметь аргумент ( it ) и могут возвращать значение. Вы решите, какой из них использовать, в зависимости от того, чего вы хотите достичь.

Вот удобная шпаргалка, которая поможет вам запомнить это:

Поскольку мы настраиваем наш объект _users в нашем Repository , мы можем сделать код более идиоматичным, используя функцию apply :

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

В этой лаборатории кода мы рассмотрели основы, необходимые для начала рефакторинга кода с Java на Kotlin. Этот рефакторинг не зависит от вашей платформы разработки и помогает гарантировать, что код, который вы пишете, идиоматичен.

Идиоматический Kotlin делает написание кода коротким и приятным. Со всеми функциями, которые предоставляет Kotlin, существует множество способов сделать ваш код более безопасным, лаконичным и читабельным. Например, мы можем даже оптимизировать наш класс Repository , создав экземпляр списка _users с пользователями непосредственно в объявлении, избавившись от блока init :

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Мы рассмотрели широкий спектр тем, от обработки значений NULL, синглетонов, строк и коллекций до таких тем, как функции расширения, функции верхнего уровня, свойства и функции области видимости. Мы перешли от двух классов Java к двум классам Kotlin, которые теперь выглядят так:

User.kt

class User(var firstName: String?, var lastName: String?)

Репозиторий.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Вот TL; DR функций Java и их сопоставление с Kotlin:

Ява

Котлин

final объект

объект val

equals()

==

==

===

Класс, который просто хранит данные

класс data

Инициализация в конструкторе

Инициализация в блоке init

static поля и функции

поля и функции, объявленные в companion object

Синглтон класс

object

Чтобы узнать больше о Kotlin и о том, как использовать его на своей платформе, ознакомьтесь со следующими ресурсами: