Refactoring a Kotlin

In questo codelab imparerai a convertire il codice da Java a Kotlin. Scoprirai anche quali sono le convenzioni del linguaggio Kotlin e come assicurarti che il codice che scrivi le rispetti.

Questo codelab è adatto a qualsiasi sviluppatore che utilizza Java e che sta valutando la migrazione del proprio progetto a Kotlin. Inizieremo con un paio di classi Java che convertirai in Kotlin utilizzando l'IDE. Poi esamineremo il codice convertito e vedremo come migliorarlo rendendolo più idiomatico ed evitando le insidie più comuni.

Obiettivi didattici

Imparerai a convertire Java in Kotlin. In questo modo, imparerai le seguenti funzionalità e i seguenti concetti del linguaggio Kotlin:

  • Gestione dell'annullabilità
  • Implementare i singleton
  • Classi di dati
  • Gestione delle stringhe
  • Operatore Elvis
  • Destrutturazione
  • Proprietà e proprietà di backup
  • Argomenti predefiniti e parametri denominati
  • Utilizzo delle raccolte
  • Funzioni di estensione
  • Funzioni e parametri di primo livello
  • Parole chiave let, apply, with e run

Presupposti

Dovresti già avere familiarità con Java.

Che cosa ti serve

Creare un nuovo progetto

Se utilizzi IntelliJ IDEA, crea un nuovo progetto Java con Kotlin/JVM.

Se utilizzi Android Studio, crea un nuovo progetto senza attività.

Il codice

Creeremo un oggetto modello User e una classe singleton Repository che funziona con gli oggetti User ed espone elenchi di utenti e nomi utente formattati.

Crea un nuovo file denominato User.java in app/java/<yourpackagename> e incolla il seguente codice:

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

}

A seconda del tipo di progetto, importa androidx.annotation.Nullable se utilizzi un progetto Android o org.jetbrains.annotations.Nullable in caso contrario.

Crea un nuovo file denominato Repository.java e incolla il seguente codice:

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

Il nostro IDE è in grado di eseguire un refactoring automatico del codice Java in codice Kotlin, ma a volte ha bisogno di un piccolo aiuto. Lo faremo prima e poi esamineremo il codice sottoposto a refactoring per capire come e perché è stato eseguito in questo modo.

Vai al file User.java e convertilo in Kotlin: barra dei menu -> Codice -> Converti file Java in file Kotlin.

Se l'IDE ti chiede di correggere dopo la conversione, premi .

Dovresti visualizzare il seguente codice Kotlin: :

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

Tieni presente che User.java è stato rinominato in User.kt. I file Kotlin hanno l'estensione .kt.

Nella nostra classe Java User avevamo due proprietà: firstName e lastName. Ognuno aveva un metodo getter e setter, il che rendeva il suo valore modificabile. La parola chiave di Kotlin per le variabili modificabili è var, quindi il convertitore utilizza var per ciascuna di queste proprietà. Se le nostre proprietà Java avessero solo getter, sarebbero immutabili e sarebbero state dichiarate come variabili val. val è simile alla parola chiave final in Java.

Una delle principali differenze tra Kotlin e Java è che Kotlin specifica esplicitamente se una variabile può accettare un valore nullo. A questo scopo, aggiunge un valore `?` alla dichiarazione del tipo.

Poiché abbiamo contrassegnato firstName e lastName come valori nullabili, il convertitore automatico ha contrassegnato automaticamente le proprietà come nullabili con String?. Se annoti i membri Java come non null (utilizzando org.jetbrains.annotations.NotNull o androidx.annotation.NonNull), il convertitore lo riconoscerà e renderà i campi non null anche in Kotlin.

Il refactoring di base è già stato eseguito. Ma possiamo scriverlo in modo più idiomatico. Vediamo come procedere.

Classe di dati

La nostra classe User contiene solo dati. Kotlin ha una parola chiave per le classi con questo ruolo: data. Se contrassegniamo questa classe come classe data, il compilatore creerà automaticamente i metodi getter e setter. Deriverà anche le funzioni equals(), hashCode() e toString().

Aggiungiamo la parola chiave data al nostro corso User:

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

Kotlin, come Java, può avere un costruttore principale e uno o più costruttori secondari. Quello nell'esempio precedente è il costruttore principale della classe User. Se converti una classe Java con più costruttori, il convertitore creerà automaticamente più costruttori anche in Kotlin. Sono definiti utilizzando la parola chiave constructor.

Se vogliamo creare un'istanza di questa classe, possiamo farlo in questo modo:

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

Equality

Kotlin ha due tipi di uguaglianza:

  • L'uguaglianza strutturale utilizza l'operatore == e chiama equals() per determinare se due istanze sono uguali.
  • L'uguaglianza referenziale utilizza l'operatore === e controlla se due riferimenti puntano allo stesso oggetto.

Le proprietà definite nel costruttore principale della classe di dati verranno utilizzate per i controlli di uguaglianza strutturale.

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

In Kotlin, possiamo assegnare valori predefiniti agli argomenti nelle chiamate di funzioni. Il valore predefinito viene utilizzato quando l'argomento viene omesso. In Kotlin, i costruttori sono anche funzioni, quindi possiamo utilizzare gli argomenti predefiniti per specificare che il valore predefinito di lastName è null. Per farlo, assegniamo null a 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")

I parametri della funzione possono essere denominati quando vengono chiamate le funzioni:

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

Come caso d'uso diverso, supponiamo che firstName abbia null come valore predefinito e lastName no. In questo caso, poiché il parametro predefinito precederebbe un parametro senza valore predefinito, dovresti chiamare la funzione con argomenti denominati:

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

Prima di procedere, assicurati che il tuo corso User sia un corso data. Convertiamo la classe Repository in Kotlin. Il risultato della conversione automatica dovrebbe essere simile al seguente:

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

Vediamo cosa ha fatto il convertitore automatico:

  • È stato aggiunto un blocco init (Repository.kt#L50)
  • Il campo static ora fa parte di un blocco companion object (Repository.kt#L33)
  • L'elenco di users è annullabile perché l'oggetto non è stato istanziato al momento della dichiarazione (Repository.kt#L7)
  • Il metodo getFormattedUserNames() ora è una proprietà denominata formattedUserNames (Repository.kt#L11)
  • L'iterazione sull'elenco di utenti (che inizialmente faceva parte di getFormattedUserNames() ha una sintassi diversa da quella Java (Repository.kt#L15)

Prima di andare avanti, puliamo un po' il codice. Possiamo vedere che il convertitore ha reso la nostra lista users una lista modificabile che contiene oggetti annullabili. Sebbene l'elenco possa essere nullo, supponiamo che non possa contenere utenti nulli. Quindi, segui questi passaggi:

  • Rimuovi ? in User? all'interno della dichiarazione di tipo users
  • getUsers dovrebbe restituire List<User>?

Il convertitore automatico ha anche suddiviso inutilmente in due righe le dichiarazioni delle variabili degli utenti e di quelle definite nel blocco init. Inseriamo ogni dichiarazione di variabile su una riga. Ecco come dovrebbe apparire il nostro codice:

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

Blocco init

In Kotlin, il costruttore principale non può contenere codice, quindi il codice di inizializzazione viene inserito nei blocchi init. La funzionalità è la stessa.

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

Gran parte del codice init gestisce l'inizializzazione delle proprietà. Questa operazione può essere eseguita anche nella dichiarazione della proprietà. Ad esempio, nella versione Kotlin della nostra classe Repository, vediamo che la proprietà users è stata inizializzata nella dichiarazione.

private var users: MutableList<User>? = null

Proprietà e metodi static di Kotlin

In Java, utilizziamo la parola chiave static per i campi o le funzioni per indicare che appartengono a una classe, ma non a un'istanza della classe. Per questo motivo abbiamo creato il campo statico INSTANCE nella nostra classe Repository. L'equivalente Kotlin è il blocco companion object. Qui dichiareresti anche i campi statici e le funzioni statiche. Il convertitore ha creato e spostato il campo INSTANCE qui.

Gestione dei singleton

Poiché abbiamo bisogno di una sola istanza della classe Repository, abbiamo utilizzato il singleton pattern in Java. Con Kotlin, puoi applicare questo pattern a livello di compilatore sostituendo la parola chiave class con object.

Rimuovi il costruttore privato e l'oggetto complementare e sostituisci la definizione della classe con 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)
    }
}

Quando utilizziamo la classe object, chiamiamo direttamente funzioni e proprietà sull'oggetto, in questo modo:

val users = Repository.users

Destrutturazione

Kotlin consente di destrutturare un oggetto in un numero di variabili utilizzando una sintassi chiamata dichiarazione di destrutturazione. Creiamo più variabili e possiamo utilizzarle in modo indipendente.

Ad esempio, le classi di dati supportano la destrutturazione, quindi possiamo destrutturare l'oggetto User nel ciclo for in (firstName, lastName). In questo modo possiamo lavorare direttamente con i valori firstName e lastName. Aggiorniamo il loop for in questo modo:

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

Durante la conversione della classe Repository in Kotlin, il convertitore automatico ha reso la lista di utenti nullable, perché non è stata inizializzata a un oggetto al momento della dichiarazione. Per tutti gli utilizzi dell'oggetto users, viene utilizzato l'operatore di asserzione non null !!. Converte qualsiasi variabile in un tipo non null e genera un'eccezione se il valore è null. Utilizzando !!, rischi che vengano generate eccezioni in fase di runtime.

Preferisci invece gestire la nullabilità utilizzando uno di questi metodi:

  • Eseguire un controllo NULL ( if (users != null) {...} )
  • Utilizzo dell'operatore Elvis ?: (trattato più avanti nel codelab)
  • Utilizzo di alcune delle funzioni standard di Kotlin (trattate più avanti nel codelab)

Nel nostro caso, sappiamo che l'elenco degli utenti non deve essere annullabile, poiché viene inizializzato subito dopo la creazione dell'oggetto, quindi possiamo istanziarlo direttamente quando lo dichiariamo.

Quando crei istanze di tipi di raccolta, Kotlin fornisce diverse funzioni di assistenza per rendere il codice più leggibile e flessibile. Qui utilizziamo un MutableList per users:

private var users: MutableList<User>? = null

Per semplicità, possiamo utilizzare la funzione mutableListOf(), fornire il tipo di elemento dell'elenco, rimuovere la chiamata al costruttore ArrayList dal blocco init e rimuovere la dichiarazione esplicita del tipo della proprietà users.

private val users = mutableListOf<User>()

Abbiamo anche modificato var in val perché gli utenti conterranno un riferimento immutabile all'elenco degli utenti. Tieni presente che il riferimento è immutabile, ma l'elenco stesso è modificabile (puoi aggiungere o rimuovere elementi).

Con queste modifiche, la nostra proprietà users ora non è nulla e possiamo rimuovere tutte le occorrenze non necessarie dell'operatore !!.

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

Inoltre, poiché la variabile users è già inizializzata, dobbiamo rimuovere l'inizializzazione dal blocco 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)
}

Poiché sia lastName che firstName possono essere null, dobbiamo gestire la nullabilità quando creiamo l'elenco dei nomi utente formattati. Poiché vogliamo visualizzare "Unknown" se manca uno dei due nomi, possiamo rendere il nome non nullo rimuovendo ? dalla dichiarazione del tipo.

val name: String

Se lastName è nullo, name è firstName o "Unknown":

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

Può essere scritto in modo più idiomatico utilizzando l'operatore Elvis ?:. L'operatore Elvis restituisce l'espressione sul lato sinistro se non è null o l'espressione sul lato destro se il lato sinistro è null.

Quindi, nel seguente codice user.firstName viene restituito se non è nullo. Se user.firstName è null, l'espressione restituisce il valore a destra , "Unknown":

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

Kotlin semplifica l'utilizzo delle String con i modelli String. I modelli di stringa ti consentono di fare riferimento alle variabili all'interno delle dichiarazioni di stringa.

Il convertitore automatico ha aggiornato la concatenazione del nome e del cognome in modo da fare riferimento al nome della variabile direttamente nella stringa utilizzando il simbolo $ e ha inserito l'espressione tra { } .

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

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

Nel codice, sostituisci la concatenazione di stringhe con:

name = "$firstName $lastName"

In Kotlin if, when, for e while sono espressioni, ovvero restituiscono un valore. Il tuo IDE mostra persino un avviso che indica che l'assegnazione deve essere rimossa da if:

Seguiamo il suggerimento dell'IDE e estraiamo l'assegnazione per entrambe le istruzioni if. Verrà assegnata l'ultima riga dell'istruzione if. In questo modo, è più chiaro che l'unico scopo di questo blocco è inizializzare il valore del nome:

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

Successivamente, riceveremo un avviso che indica che la dichiarazione name può essere unita all'assegnazione. Applichiamo anche questo. Poiché il tipo della variabile name può essere dedotto, possiamo rimuovere la dichiarazione di tipo esplicita. Ora il nostro formattedUserNames ha il seguente aspetto:

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
   }

Diamo un'occhiata più da vicino al getter formattedUserNames e vediamo come possiamo renderlo più idiomatico. Al momento il codice esegue queste operazioni:

  • Crea un nuovo elenco di stringhe
  • Scorre l'elenco degli utenti
  • Crea il nome formattato per ogni utente, in base al nome e al cognome dell'utente
  • Restituisce l'elenco appena creato
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 fornisce un elenco completo di trasformazioni di raccolte che rendono lo sviluppo più rapido e sicuro espandendo le funzionalità dell'API Java Collections. Una di queste è la funzione map. Questa funzione restituisce un nuovo elenco contenente i risultati dell'applicazione della funzione di trasformazione specificata a ogni elemento dell'elenco originale. Quindi, anziché creare un nuovo elenco e scorrere manualmente l'elenco degli utenti, possiamo utilizzare la funzione map e spostare la logica che avevamo nel ciclo for all'interno del corpo map. Per impostazione predefinita, il nome dell'elemento di elenco corrente utilizzato in map è it, ma per una maggiore leggibilità puoi sostituire it con il tuo nome di variabile. Nel nostro caso, chiamiamolo 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
            }
        }

Per semplificare ulteriormente la procedura, possiamo rimuovere completamente la variabile 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"
                }
            }
        }

Abbiamo notato che il convertitore automatico ha sostituito la funzione getFormattedUserNames() con una proprietà chiamata formattedUserNames che ha un getter personalizzato. Sotto il cofano, Kotlin genera comunque un metodo getFormattedUserNames() che restituisce un List.

In Java, esponiamo le proprietà della classe tramite le funzioni getter e setter. Kotlin ci consente di distinguere meglio tra le proprietà di una classe, espresse con i campi, e le funzionalità, ovvero le azioni che una classe può eseguire, espresse con le funzioni. Nel nostro caso, la classe Repository è molto semplice e non esegue alcuna azione, quindi ha solo campi.

La logica attivata nella funzione Java getFormattedUserNames() viene ora attivata quando viene chiamato il getter della proprietà Kotlin formattedUserNames.

Anche se non disponiamo esplicitamente di un campo corrispondente alla proprietà formattedUserNames, Kotlin ci fornisce un campo di supporto automatico denominato field a cui possiamo accedere, se necessario, da getter e setter personalizzati.

A volte, però, vogliamo alcune funzionalità aggiuntive che il campo di backup automatico non fornisce. Vediamo un esempio di seguito.

All'interno della nostra classe Repository, abbiamo un elenco modificabile di utenti che viene esposto nella funzione getUsers() generata dal nostro codice Java:

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

Il problema è che restituendo users qualsiasi consumer della classe Repository può modificare il nostro elenco di utenti, il che non è una buona idea. Risolviamo il problema utilizzando una proprietà di supporto.

Innanzitutto, rinominiamo users in _users. Ora aggiungi una proprietà pubblica e immutabile che restituisce un elenco di utenti. Chiamiamolo users:

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

Con questa modifica, la proprietà privata _users diventa la proprietà di backup per la proprietà pubblica users. Al di fuori della classe Repository, l'elenco _users non è modificabile, in quanto i consumatori della classe possono accedere all'elenco solo tramite users.

Codice completo:

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

Al momento, la classe Repository sa come calcolare il nome utente formattato per un oggetto User. Tuttavia, se vogliamo riutilizzare la stessa logica di formattazione in altri corsi, dobbiamo copiarla e incollarla o spostarla nel corso User.

Kotlin consente di dichiarare funzioni e proprietà al di fuori di qualsiasi classe, oggetto o interfaccia. Ad esempio, la funzione mutableListOf() che abbiamo utilizzato per creare una nuova istanza di un List è definita direttamente in Collections.kt della libreria standard.

In Java, ogni volta che hai bisogno di alcune funzionalità di utilità, molto probabilmente creerai una classe Util e dichiarerai questa funzionalità come funzione statica. In Kotlin puoi dichiarare funzioni di primo livello senza una classe. Tuttavia, Kotlin offre anche la possibilità di creare funzioni di estensione. Si tratta di funzioni che estendono un determinato tipo, ma vengono dichiarate al di fuori del tipo. Pertanto, hanno un'affinità con questo tipo.

La visibilità delle proprietà e delle funzioni delle estensioni può essere limitata utilizzando i modificatori di visibilità. In questo modo, l'utilizzo è limitato solo alle classi che necessitano delle estensioni e lo spazio dei nomi non viene contaminato.

Per la classe User, possiamo aggiungere una funzione di estensione che calcola il nome formattato oppure possiamo memorizzare il nome formattato in una proprietà di estensione. Può essere aggiunto al di fuori della classe Repository, nello stesso file:

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

Possiamo quindi utilizzare le funzioni e le proprietà dell'estensione come se facessero parte della classe User.

Poiché il nome formattato è una proprietà dell'utente e non una funzionalità della classe Repository, utilizziamo la proprietà di estensione. Il nostro file Repository ora ha il seguente aspetto:

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

La libreria standard Kotlin utilizza le funzioni di estensione per estendere la funzionalità di diverse API Java; molte delle funzionalità su Iterable e Collection sono implementate come funzioni di estensione. Ad esempio, la funzione map che abbiamo utilizzato in un passaggio precedente è una funzione di estensione di Iterable.

Nel nostro codice della classe Repository, stiamo aggiungendo diversi oggetti utente all'elenco _users. Queste chiamate possono essere rese più idiomatiche con l'aiuto delle funzioni di ambito.

Per eseguire il codice solo nel contesto di un oggetto specifico, senza dover accedere all'oggetto in base al suo nome, Kotlin ha creato cinque funzioni di ambito: let, apply, with, run e also. Brevi e potenti, tutte queste funzioni hanno un ricevitore (this), possono avere un argomento (it) e possono restituire un valore. Deciderai quale utilizzare in base a ciò che vuoi ottenere.

Ecco un'utile scheda di riferimento per aiutarti a ricordare:

Poiché stiamo configurando il nostro oggetto _users in Repository, possiamo rendere il codice più idiomatico utilizzando la funzione 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)
    }
 }

In questo codelab abbiamo trattato le nozioni di base necessarie per iniziare a eseguire il refactoring del codice da Java a Kotlin. Questo refactoring è indipendente dalla tua piattaforma di sviluppo e contribuisce a garantire che il codice che scrivi sia idiomatico.

Kotlin idiomatico rende la scrittura di codice breve e semplice. Con tutte le funzionalità fornite da Kotlin, ci sono molti modi per rendere il codice più sicuro, conciso e leggibile. Ad esempio, possiamo persino ottimizzare la nostra classe Repository istanziando l'elenco _users con gli utenti direttamente nella dichiarazione, eliminando il blocco init:

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

Abbiamo trattato un'ampia gamma di argomenti, dalla gestione della nullabilità, dei singleton, delle stringhe e delle raccolte ad argomenti come funzioni di estensione, funzioni di primo livello, proprietà e funzioni di ambito. Siamo passati da due classi Java a due classi Kotlin che ora hanno questo aspetto:

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

Ecco un riepilogo delle funzionalità Java e della loro mappatura in Kotlin:

Java

Kotlin

final oggetto

val oggetto

equals()

==

==

===

Classe che contiene solo dati

Classe data

Inizializzazione nel costruttore

Inizializzazione nel blocco init

static campi e funzioni

campi e funzioni dichiarati in un companion object

Classe singleton

object

Per scoprire di più su Kotlin e su come utilizzarlo sulla tua piattaforma, consulta queste risorse: