refaktoryzacja w Kotlinie,

Z tego modułu dowiesz się, jak przekonwertować kod z kodu Java na Kotlin. Dowiesz się też, na czym polegają konwencje językowe Kotlina i jak upewnić się, że kod, który piszesz, jest z nimi zgodny.

To ćwiczenie z programowania jest przeznaczone dla każdego dewelopera, który używa języka Java i rozważa migrację projektu do Kotlina. Rozpoczniemy od kilku kursów Java, które zostaną przekształcone w Kotlin za pomocą IDE. Przyjrzymy się teraz przekonwertowanemu kodowi i zobaczymy, jak możemy go ulepszyć, zwiększając jego idiomatic unikanie typowych problemów.

Czego się nauczysz

Dowiedz się, jak przekonwertować kod Java na Kotlin. Dzięki temu poznasz funkcje i koncepcje języka Kotlin:

  • Obsługa wartości null
  • Implementowanie singli
  • Klasy danych
  • Obsługa ciągów tekstowych
  • Operator Elvisa
  • Niszczenie
  • Właściwości i zasoby zapasowe
  • Domyślne argumenty i parametry nazwane
  • Praca z kolekcjami
  • Funkcje rozszerzeń
  • Funkcje i parametry najwyższego poziomu
  • Słowa kluczowe: let, apply, with i run

Założenia

Musisz znać język Java.

Czego potrzebujesz

Tworzenie nowego projektu

Jeśli używasz IntelliJ IDEA, utwórz nowy projekt w Javie z kotlin/JVM.

Jeśli używasz Androida Studio, utwórz nowy projekt bez aktywności.

Kod

Utworzymy obiekt modelu User i klasę jednotonową Repository, która działa z obiektami User i wyświetla listy użytkowników i sformatowane nazwy użytkowników.

Utwórz nowy plik User.java o nazwie app/java/<nazwa_pakietu> i wklej ten kod:

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;
    }

}

W zależności od typu projektu zaimportuj androidx.annotation.Nullable, jeśli używasz projektu na Androida lub org.jetbrains.annotations.Nullable.

Utwórz nowy plik o nazwie Repository.java i wklej ten kod:

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;
    }
}

Nasz IDE może nieźle radzić sobie z automatycznym refaktoryzacją kodu Java do kodu Kotlin, ale czasami wymaga to trochę pomocy. Zaczniemy od przeanalizowania kodu refaktorowego, by dowiedzieć się, jak i dlaczego został on poddany refaktoryzacji w ten sposób.

Otwórz plik User.java i przekonwertuj go na Kotlin: Pasek menu -> Kod -> Przekonwertuj plik Java na Kotlin.

Jeśli IDE wyświetla komunikat o konieczności korekty po konwersji, naciśnij Tak.

Powinien wyświetlić się ten kod Kotlin:

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

Nazwa usługi User.java została zmieniona na User.kt. Pliki Kotlin mają rozszerzenie .kt.

Na zajęciach w języku Java User mamy 2 usługi: firstName i lastName. Każda z nich korzystała z metody getter i seter, co zmienia wartość. Słowo kluczowe Kotlin dla zmiennych zmiennego to var, więc osoba dokonująca konwersji używa var w każdej z tych właściwości. Gdyby nasze usługi Java miały tylko metody getter, byłyby stałe i zostały zadeklarowane jako zmienne val. val jest podobne do słowa kluczowego final w Javie.

Jedną z głównych różnic między Kotlin a Java jest to, że Kotlin wyraźnie określa, czy zmienna może zaakceptować wartość null. Aby to zrobić, należy dodać deklarację typu „?”.

Ponieważ firstName i lastName oznaczyliśmy jako wartości null, automatyczne konwertery automatycznie oznaczyły właściwości jako null i String?. Jeśli oznaczysz członków Java jako „null” (za pomocą org.jetbrains.annotations.NotNull lub androidx.annotation.NonNull), użytkownik dokonujący konwersji rozpozna to i zmieni też pola w kotlinie jako puste.

Podstawowy refaktoryzacja jest już wykonana. Możemy jednak napisać to w bardziej identyczny sposób. Zobaczmy, jak to zrobić.

Klasa danych

Klasa User zawiera tylko dane. Kotlin ma słowo kluczowe dla zajęć o tej roli: data. Jeśli oznaczysz te zajęcia jako klasy data, kompilator sam utworzy dla Ciebie klasy getter i setery. Będzie też funkcja equals(), hashCode() i toString().

Dodajmy słowo kluczowe data do naszej klasy User:

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

Kotlin, podobnie jak Java, może mieć główny konstruktor i co najmniej jeden konstruktor dodatkowy. Ten w powyższym przykładzie jest głównym konstruktorem klasy użytkownika. Jeśli przekonwertujesz klasę Java, która ma wiele konstruktorów, moduł konwersji automatycznie utworzy też wiele konstruktorów w Kotlinie. Są one definiowane za pomocą słowa kluczowego constructor.

Jeśli chcesz utworzyć instancję tej klasy, wykonaj następujące czynności:

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

Równość

Kotlin stosuje dwa rodzaje równości:

  • Równość strukturalna korzysta z operatora == i wywołuje polecenie equals(), aby określić, czy 2 wystąpienia są równe.
  • Równość referencyjna korzysta z operatora === i sprawdza, czy 2 odwołania wskazują ten sam obiekt.

Właściwości zdefiniowane w podstawowym konstruktorze klasy danych będą używane do kontroli strukturalnej równości.

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

W Kotlin możemy przypisywać wartości domyślne do argumentów w wywołaniach funkcji. W przypadku pominięcia argumentu używana jest wartość domyślna. Kotlin to również konstrukcje, więc możemy użyć domyślnych argumentów, aby określić, że domyślna wartość lastName to null. W tym celu przypisujemy numer null do organizacji 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")

Parametry funkcji możesz nazwać podczas wywoływania funkcji:

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

Załóżmy, że firstName ma wartość domyślną null, a lastName nie. W tym przypadku parametr domyślny jest poprzedzony parametrem bez wartości domyślnej, więc trzeba wywołać funkcję z argumentami nazwanymi:

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")

Zanim przejdziesz dalej, upewnij się, że klasa User jest klasą data. Przekonwertujmy klasę Repository na Kotlin. Wynik automatycznej konwersji powinien wyglądać tak:

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)
   }
}

Zobaczmy, co zrobiła automatyczna konwersja:

  • Dodano blok init (Repository.kt#L50)
  • Pole static jest teraz częścią bloku companion object (Repozytorium.kt#L33)
  • Lista users ma wartość null, ponieważ obiekt nie został utworzony w momencie deklaracji (Repository.kt#L7).
  • Metoda getFormattedUserNames() jest teraz usługą o nazwie formattedUserNames (Repository.kt#L11)
  • iteracja nad listą użytkowników (która początkowo była częścią getFormattedUserNames() ) ma inną składnię niż Java (Repository.kt#L15)

Zanim przejdziemy dalej, nieco uporządkujemy kod. Jak widać, użytkownik dokonujący konwersji przekształcił naszą listę users w możliwą do zmodyfikowania listę zawierającą obiekty null. Lista może mieć wartość null, ale nie może zawierać użytkowników o wartości null. Wykonajmy więc te czynności:

  • Usuń ? z User? w deklaracji typu users
  • Parametr getUsers powinien zwrócić wartość List<User>?

Automatycznie konwertuje również niepotrzebnie podzielone na 2 wiersze deklaracje zmiennych użytkownika i zmiennych zdefiniowanych w bloku init. Każda deklaracja zmiennej znajduje się w jednym wierszu. Kod powinien wyglądać tak:

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)
    }
}

Blokowanie Init

W Kotlin główny konstruktor nie może zawierać żadnego kodu, więc kod inicjowania jest umieszczany w blokach init. Ich funkcje są takie same.

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)
    }
}

Znaczna część kodu init obsługuje właściwości inicjowania. Możesz to zrobić w deklaracji właściwości. Na przykład w klasie w Kotlin klasy Repository widzimy, że właściwość użytkowników została zainicjowana w deklaracji.

private var users: MutableList<User>? = null

Usługi i metody static Kotlin

W języku Java używamy słowa kluczowego static w polach lub funkcjach, aby wskazywać, że element należy do klasy, ale nie do wystąpienia klasy. Dlatego utworzyliśmy pole statyczne INSTANCE w klasie Repository. Odpowiednikiem Kotlina jest blok companion object. Następnie trzeba zadeklarować pola statyczne i funkcje statyczne. Użytkownik, który dokonał konwersji, utworzył i przeniósł pole INSTANCE.

Obsługa jednotonów

Potrzebujemy tylko jednego wystąpienia klasy Repository, dlatego użyliśmy wzorca pojedynczego adresowania w Javie. Korzystając z Kotlin, możesz wymusić stosowanie tego wzorca na poziomie kompilacji, zastępując słowo kluczowe class wartością object.

Usuń konstruktor prywatny oraz obiekt towarzyszący i zastąp definicję klasy 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)
    }
}

Gdy używasz klasy object, funkcje i właściwości są wywoływane bezpośrednio w obiekcie, na przykład:

val users = Repository.users

Nękanie

Kotlin umożliwia zniszczenie obiektu w wielu zmiennych przy użyciu składni zwanej destrukcją zniszczenia. Tworzymy wiele zmiennych i możemy ich używać niezależnie.

Na przykład klasy danych obsługują zniszczenie, aby móc zniszczyć obiekt User w pętli for do (firstName, lastName). Dzięki temu możemy pracować bezpośrednio z wartościami firstName i lastName. Zaktualizujmy pętlę for w ten sposób:

 
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)
}

Podczas konwertowania klasy Repository na Kotlin lista użytkowników, którzy dokonali konwersji, została uznana za unieważnioną, ponieważ nie została ona zainicjowana do obiektu podczas jego rejestrowania. Przy wszystkich zastosowaniach obiektu users używany jest niepusty operator asercji !!. Konwertuje dowolną zmienną na typ inny niż null i tworzy wyjątek, jeśli wartość jest pusta. Korzystanie z narzędzia !! wiąże się z ryzykiem odrzucenia wyjątków w czasie działania.

Zamiast tego lepiej jest użyć wartości null. Aby to zrobić, użyj jednej z tych metod:

  • Przeprowadzam kontrolę o wartości null (if (users != null) {...})
  • Stosowanie operatora Elvisa ?: (opisane później w ćwiczeniach z programowania)
  • Korzystanie z niektórych funkcji standardowych Kotlin (opisanych w dalszej części ćwiczenia z programowania)

W naszym przypadku lista użytkowników nie musi być null, bo inicjuje się zaraz po utworzeniu obiektu, więc możemy za jego pomocą natychmiast utworzyć instancję.

Podczas tworzenia wystąpień typów kolekcji Kotlin udostępnia kilka funkcji pomocniczych, aby kod był bardziej czytelny i elastyczny. Oto nazwa domeny MutableList dla users:

private var users: MutableList<User>? = null

Dla uproszczenia możemy użyć funkcji mutableListOf(), przekazać typ elementu listy, usunąć wywołanie konstruktora ArrayList z bloku init i usunąć konkretną deklarację typu właściwości users.

private val users = mutableListOf<User>()

Zmieniliśmy też zmienną var val, ponieważ użytkownicy będą zawierać stałe odniesienie do listy użytkowników. Pamiętaj, że odniesienie nie może być stałe, ale lista jest stała (możesz dodawać i usuwać elementy).

Dzięki tym zmianom nasza właściwość users nie jest już pusta i możemy usunąć wszystkie niepotrzebne wystąpienia operatora !!.

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

Poza tym ze względu na to, że zmienna „user” została już zainicjowana, musimy ją usunąć z bloku 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)
}

Zarówno wartość lastName, jak i firstName może wynosić null, dlatego podczas tworzenia listy sformatowanych nazw użytkowników musimy użyć wartości null. Jeśli chcemy wyświetlać "Unknown", jeśli brakuje którejś z tych nazw, możemy usunąć ją z deklaracji ? bez wartości null.

val name: String

Jeśli lastName ma wartość NULL, name jest firstName lub "Unknown":

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

Możesz to zapisać w sposób bardziej idiotyczny, używając operatora Elvisa ?:. Operator elvis zwróci wyrażenie po lewej stronie, jeśli nie jest puste (lub nie ma wartości null).

Jeśli ten kod nie jest pusty, zwracany jest kod user.firstName. Jeśli user.firstName ma wartość NULL, wyrażenie zwraca wartość po prawej stronie, "Unknown":

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

Kotlin ułatwia współpracę z String w szablonach String. Szablony ciągów umożliwiają odwoływanie się do zmiennych w deklaracjach ciągu.

Automatyczna konwersja dokonała połączenia imienia i nazwiska, by odwoływały się do nazwy zmiennej bezpośrednio w ciągu, używając symbolu $ i umieszczając wyrażenie między { } .

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

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

W kodzie zastąp ciąg znaków ciągiem:

name = "$firstName $lastName"

W Kotlin if, when, for i while to wyrażenia – zwracają wartość. IDE pokazuje nawet ostrzeżenie, że przypisanie powinno zostać usunięte z if:

Zobaczmy sugestię IDE i cofnij przypisanie dla obu instrukcji if. Ostatni wiersz instrukcji „if” zostanie przypisany. W ten sposób można łatwo zainicjować wartość nazwy w tym bloku:

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

Następnie pojawi się ostrzeżenie, że można dołączyć deklarację name. Zastosujmy to też. Ponieważ typ zmiennej nazwy może być wydychany, możemy usunąć deklarację konkretnego typu. Teraz formattedUserNames wygląda tak:

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
   }

Przyjrzyjmy się bliżej getrowi formattedUserNames i zobaczmy, jak możemy zwiększyć jego idiomatykę. Teraz kod wykonuje te czynności:

  • Tworzy nową listę ciągów znaków
  • Przeglądanie list użytkowników
  • Tworzy sformatowaną nazwę każdego użytkownika na podstawie jego imienia i nazwiska
  • Zwraca nowo utworzoną listę
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 udostępnia obszerną listę przekształceń kolekcji, które przyspieszają i zwiększają możliwości programowania dzięki wykorzystaniu możliwości interfejsu Java Collection API. Jedną z nich jest funkcja map. Ta funkcja zwraca nową listę zawierającą wyniki zastosowania określonej funkcji przekształcenia do każdego elementu na oryginalnej liście. Dlatego zamiast tworzyć nową listę i szybko przeglądać listę użytkowników, możemy użyć funkcji map i przeprowadzić działanie logiczne w pętli for w treści map. Domyślnie nazwa bieżącego elementu listy używanego w polu map to it, ale aby można było odczytać, it można zastąpić własną nazwą zmiennej. W naszym przypadku nazwijmy go 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
            }
        }

Aby jeszcze bardziej uprościć to działanie, możemy całkowicie usunąć zmienną 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"
                }
            }
        }

Wykryliśmy, że automatyczna konwertacja zastąpiła funkcję getFormattedUserNames() właściwością formattedUserNames, która ma niestandardowy moduł getter. Pod maską Kotlin nadal generuje metodę getFormattedUserNames(), która zwraca List.

W Javie ujawnimy właściwości klasy za pomocą funkcji getter i seter. Kotlin umożliwia nam rozróżnianie między klasami wyrażonymi polami i funkcjami, które klasa może wykonywać w ramach funkcji wyrażonych za pomocą funkcji. W naszym przypadku klasa Repository jest bardzo prosta i nie wykonuje żadnych działań, więc ma tylko pola.

Logika, która została wywołana w funkcji Java getFormattedUserNames() jest teraz wyzwalana podczas pobierania metody formattedUserNames usługi Kotlin.

Chociaż nie mamy pola odpowiadającego właściwości formattedUserNames, Kotlin udostępnia automatyczne pole o nazwie field , do którego mamy dostęp w razie potrzeby od niestandardowych nadawców i osób określających.

Potrzebujemy czasem dodatkowych funkcji, których nie oferuje pole do automatycznego tworzenia kopii zapasowych. Przeanalizujmy to poniżej.

W klasie Repository znajduje się zmienna lista użytkowników, które są udostępniany przez funkcję getUsers() wygenerowaną na podstawie kodu Java:

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

Problem polega na tym, że gdy zwracamy wartość users, każdy klient klasy Repozytorium może modyfikować naszą listę użytkowników – nie jest to dobry pomysł. Rozwiążmy ten problem, korzystając z usługi zapasowej.

Najpierw zmień nazwę elementu users na _users. Teraz dodaj stałą właściwość publiczną, która zwraca listę użytkowników. Nazwijmy ją users:

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

Po wprowadzeniu tej zmiany prywatna usługa _users stanie się usługą zapasową dla publicznej usługi users. Poza klasą Repository nie można modyfikować listy _users, ponieważ dostęp do niej mają tylko users.

Pełny kod:

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)
    }
}

Obecnie klasa Repository wie, jak obliczyć sformatowaną nazwę użytkownika obiektu User. Jeśli jednak chcesz wykorzystać tę samą zasadę formatowania w innych zajęciach, musisz ją skopiować i wkleić lub przenieść do klasy User.

Kotlin umożliwia deklarowanie funkcji i właściwości poza dowolną klasą, obiektem lub interfejsem. Na przykład funkcja mutableListOf(), która została użyta do utworzenia nowego wystąpienia obiektu List, jest zdefiniowana bezpośrednio w Collections.kt w Bibliotece standardowej.

Gdy potrzebujesz obsługi jakiejś funkcji w języku Java, prawdopodobnie tworzysz klasę Util i deklarujesz ją jako funkcję statyczną. W Kotlin możesz zadeklarować funkcje najwyższego poziomu bez klasy. Kotlin umożliwia też jednak tworzenie funkcji rozszerzeń. Są to funkcje, które rozszerzają określony typ, ale są zadeklarowane poza typem. Mają więc podobne zainteresowania.

Widoczność funkcji i rozszerzeń rozszerzeń można ograniczyć, stosując modyfikatory widoczności. Ograniczają one użycie tylko do klas, które wymagają rozszerzeń, i nie będą zaśmiecać przestrzeni nazw.

W przypadku klasy User możemy dodać funkcję rozszerzenia do obliczania sformatowanej nazwy lub przechowywać nazwę sformatowaną we właściwości rozszerzenia. Możesz go dodać poza klasą Repository w tym samym pliku:

// 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

Następnie możemy używać funkcji i właściwości rozszerzenia w taki sposób, jakby były one częścią klasy User.

Sformatowana nazwa jest właściwością użytkownika, a nie klasy Repository, dlatego użyjemy rozszerzenia. Plik Repository wygląda teraz tak:

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)
    }
}

Biblioteka standardowa Kotlin używa funkcji rozszerzeń do rozszerzania funkcjonalności niektórych interfejsów API Java. Wiele funkcji Iterable i Collection implementujemy jako funkcje rozszerzeń. Na przykład funkcja map użyta w poprzednim kroku to funkcja rozszerzenia w usłudze Iterable.

W kodzie zajęć Repository dodaliśmy kilka obiektów użytkownika do listy _users. Dzięki funkcjom zakresu te wywołania mogą być bardziej idiotyczne.

Aby wykonać kod tylko w kontekście konkretnego obiektu, bez konieczności uzyskania dostępu do obiektu na podstawie jego nazwy, Kotlin utworzył 5 funkcji zakresu: let, apply, with, run i also. Wszystkie te funkcje są krótkie i obszerne, mają odbiornik (this), mogą mieć argument (it) i mogą zwracać wartość. To, której z nich użyjesz, zależy od tego, co chcesz osiągnąć.

Oto przydatna ściągawka, która to pomoże:

Konfigurujemy obiekt _users w Repository, więc za pomocą funkcji apply możemy zwiększyć kod idiomatyczny:

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)
    }
 }

W tym ćwiczeniu omówiliśmy podstawy potrzebne do refaktoryzacji kodu z Javy do Kotlina. Ten współczynnik refaktoryzacji jest niezależny od platformy deweloperskiej, dzięki czemu masz pewność, że kod jest idiomatyczny.

Idiomatyczna Kotlin sprawia, że pisanie kodu jest krótkie i ciekawe. Kotlin oferuje wiele funkcji, dzięki którym Twój kod może być bardziej bezpieczny, zwięzły i czytelny. Możemy na przykład zoptymalizować klasę Repository, tworząc listę _users z użytkownikami bezpośrednio w deklaracji, pozbywając się bloku init:

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

omówiliśmy szeroką gamę tematów: od obsługi wartości null, singleton, Strings i collection po takie funkcje jak rozszerzenie, funkcje najwyższego poziomu, właściwości i zakresy. Przeszliśmy z dwóch klas Java na dwie klasy Kotlin, które wyglądają teraz tak:

User.kt

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

Repository.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 }
}

Oto kierunek rozwoju funkcji Java oraz ich mapowanie na Kotlin:

Java

Kotlin

final obiekt

val obiekt

equals()

==

==

===

Klasa, która zawiera tylko dane

Klasa data

Zdarzenie w konstruktorze

Inicjacja w bloku init

static pól i funkcji

pola i funkcje zadeklarowane w elemencie companion object

Klasa Singleton

object

Aby dowiedzieć się więcej o Kotlin i sposobie korzystania z niego na swojej platformie, sprawdź te zasoby: