Refactoring a Kotlin

In questo codelab, imparerai a convertire il codice da Java a Kotlin. Scoprirai anche quali sono le convenzioni linguistiche di Kotlin e come assicurarti che il codice che le scrivi sia seguito.

Questo codelab è adatto a qualsiasi sviluppatore che utilizza Java che sta valutando la migrazione del suo progetto a Kotlin. Inizieremo con un paio di lezioni di Java che tu convertirai in Kotlin utilizzando l'IDE. Poi daremo un'occhiata al codice convertito e vedremo come migliorarlo rendendolo più idiomatico ed evitando gli errori più comuni.

Obiettivi didattici

Imparerai a convertire Java in Kotlin. Durante questa operazione imparerai le seguenti caratteristiche e concetti relativi alla lingua Kotlin:

  • Gestione della nullità
  • Implementare i singleton
  • Classi di dati
  • Gestione delle stringhe
  • Operatore Elvis
  • Struttura
  • Proprietà e proprietà di supporto
  • Argomenti predefiniti e parametri denominati
  • Utilizzare le collezioni
  • Funzioni di estensione
  • Funzioni e parametri di primo livello
  • Parole chiave: let, apply, with e run

Premesse

Dovresti già conoscere Java.

Che cosa ti serve

Creare un nuovo progetto

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

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

Il codice

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

Crea un nuovo file denominato User.java in app/java/<nometuo pacchetto> 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 oppure org.jetbrains.annotations.Nullable se non lo utilizzi.

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 può eseguire correttamente un processo di refactoring automatico del codice Java in codice Kotlin, ma a volte è necessario un piccolo aiuto. Lo faremo prima e poi passeremo in rassegna il codice di refactoring per comprendere come e perché in questo modo è stato eseguito il refactoring.

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

Se l'IDE richiede una correzione dopo la conversione, premi .

Dovresti vedere il seguente codice Kotlin:

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

Tieni presente che il nome 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. Ciascuno aveva un metodo getter e setter, che rendeva il proprio valore modificabile. La parola chiave di Kotlin per le variabili modificabili è var, quindi l'utente che ha completato la conversione utilizza var per ciascuna di queste proprietà. Se le nostre proprietà Java avessero 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 in modo esplicito se una variabile può accettare un valore nullo. Per farlo, aggiungi un elemento "?" alla dichiarazione del tipo.

Poiché abbiamo contrassegnato firstName e lastName come null, l'autore della conversione automatica ha contrassegnato automaticamente le proprietà come null con String?. Se annota i tuoi membri Java come non-null (utilizzando org.jetbrains.annotations.NotNull o androidx.annotation.NonNull), l'utente che ha completato la conversione riconosce anche questo e rende i campi non null in Kotlin.

Il refactoring di base è già stato eseguito. Tuttavia, possiamo farlo in modo più idiomatico. Vediamo come.

Classe dati

Il nostro corso User contiene solo dati. Kotlin ha una parola chiave per le classi con questo ruolo: data. Se contrassegni questa classe come classe data, il compilatore creerà automaticamente getter e setter. Funzionerà anche nelle funzioni equals(), hashCode() e toString().

Aggiungiamo la parola chiave data alla nostra classe 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 che ha più costruttori, l'convertitore creerà automaticamente anche più costruttori in Kotlin. Sono definiti utilizzando la parola chiave constructor.

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

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

Parità

Kotlin ha due tipi di uguaglianza:

  • L'uguaglianza strutturale usa l'operatore == e chiama equals() per determinare se due istanze sono uguali.
  • L'uguaglianza di riferimento utilizza l'operatore === e verifica se due riferimenti rimandano allo stesso oggetto.

Le proprietà definite nel costruttore principale della classe di dati verranno utilizzate per i controlli dell'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 funzione. Il valore predefinito viene utilizzato quando l'argomento viene omesso. In Kotlin anche i costruttori sono funzioni, quindi possiamo utilizzare argomenti predefiniti per specificare che il valore predefinito di lastName è null. Per farlo, basta assegnare 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 delle funzioni possono essere denominati quando chiamano 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 precede un parametro senza valore predefinito, devi 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 la classe User sia un corso data. Convertiamo la classe Repository in Kotlin. Il risultato della conversione automatica dovrebbe avere il seguente aspetto:

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 l'utente che ha completato la conversione:

  • È stato aggiunto un blocco init (Repository.kt#L50)
  • Ora il campo static fa parte di un blocco companion object (Repository.kt#L33)
  • L'elenco di users è nullo poiché l'oggetto non è stato creato un'istanza 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 di Java (Repository.kt#L15)

Prima di continuare, salviamo un po' il codice. Abbiamo notato che l'autore della conversione ha reso il nostro elenco users un elenco modificabile che contiene oggetti non validi. Anche se l'elenco può essere nullo, supponiamo che non possa contenere utenti non validi. Pertanto, eseguiamo le seguenti operazioni:

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

L'autore della conversione automatica divide inutilmente anche in due righe le dichiarazioni delle variabili delle variabili utente e di quelle definite nel blocco init. Mettiamo ogni dichiarazione variabile su una sola riga. Ecco come dovrebbe apparire il 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 di inizializzazione

In Kotlin, il costruttore principale non può contenere alcun 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 le proprietà di inizializzazione. Puoi anche farlo nella dichiarazione della proprietà. Ad esempio, nella versione Kotlin della nostra classe Repository, vediamo che la proprietà utente è stata inizializzata nella dichiarazione.

private var users: MutableList<User>? = null

static Proprietà e metodi 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. Ecco perché abbiamo creato il campo statico INSTANCE nella nostra classe Repository. L'equivalente di Kotlin per questo problema è companion object. Qui dichiari anche i campi statici e le funzioni statiche. L'utente che ha completato la conversione ha creato e spostato qui il campo INSTANCE.

Gestire i singleton

Poiché ci serve una sola istanza della classe Repository, abbiamo utilizzato il pattern singleton 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 companion 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 utilizzi la classe object, chiamiamo funzioni e proprietà direttamente sull'oggetto, in questo modo:

val users = Repository.users

Struttura

Kotlin consente di strutturare un oggetto in una serie 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 strutturazione in modo da poter destrutturare l'oggetto User nel loop di for in (firstName, lastName). In questo modo possiamo lavorare direttamente con i valori firstName e lastName. Aggiorna 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 nulla l'elenco degli utenti, perché non è stato inizializzato in un oggetto quando è stato dichiarato. Per tutti gli utilizzi dell'oggetto users, viene utilizzato l'operatore dell'asserzione non null !!. Converte qualsiasi variabile in un tipo non null e genera un'eccezione se il valore è null. Se utilizzi !!, rischi di generare eccezioni in fase di runtime.

Tuttavia, preferisci gestire la nullità utilizzando uno dei seguenti metodi:

  • Verifica null (if (users != null) {...})
  • Utilizzo dell'operatore Elvis ?: (trattato in seguito nel codelab)
  • Utilizzo di alcune funzioni standard di Kotlin (trattate in seguito nel codelab)

Nel nostro caso, sappiamo che l'elenco degli utenti non deve essere nullo, dato che viene inizializzato subito dopo la creazione dell'oggetto, quindi possiamo creare un'istanza diretta dell'oggetto quando lo dichiariamo.

Durante la creazione di istanze dei tipi di raccolta, Kotlin offre diverse funzioni helper per rendere il codice più leggibile e flessibile. Stiamo utilizzando MutableList per users:

private var users: MutableList<User>? = null

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

private val users = mutableListOf<User>()

Abbiamo inoltre modificato var in val perché gli utenti conterranno un riferimento immutabile all'elenco di 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 è null e possiamo rimuovere tutte le occorrenze dell'operatore !! non necessarie.

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

Inoltre, poiché la variabile utente è 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 sia firstName possono essere null, dobbiamo gestire la nullità quando creiamo l'elenco di nomi utente formattati. Poiché vogliamo mostrare "Unknown" se manca uno dei due nomi, possiamo rendere il nome non nullo rimuovendo ? dalla dichiarazione del tipo.

val name: String

Se lastName è null, 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"
}

Questo può essere scritto in modo più descrittivo utilizzando l'operatore Elvis ?:. L'operatore elvis restituirà 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 sulla destra , "Unknown":

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

Kotlin semplifica la collaborazione con gli String grazie ai modelli di stringhe. I modelli di stringa consentono di fare riferimento alle variabili all'interno delle dichiarazioni stringa.

Il convertitore automatico ha aggiornato la concatenazione del nome e del cognome per fare riferimento al nome della variabile direttamente nella stringa utilizzando il simbolo $ e inserendo 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, che restituiscono un valore. Il tuo IDE mostra persino un avviso indicante che il compito deve essere rimosso dal if:

Segui il suggerimento di IDE e rimuovi il compito per entrambe le istruzioni if. L'ultima riga dell'istruzione if verrà assegnata. In questo modo è più chiaro che il solo scopo di questo blocco è inizializzare il valore del nome:

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

Successivamente, riceverai un avviso che ti informa che la dichiarazione name può essere aggiunta al compito. Viene applicata anche questa impostazione. Poiché è possibile dedurre il tipo della variabile name, possiamo rimuovere la dichiarazione del tipo esplicito. 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 funziona nel seguente modo:

  • Crea un nuovo elenco di stringhe
  • Itera l'elenco di 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 vasto elenco di trasformazioni delle raccolte che rendono lo sviluppo più rapido e sicuro espandendo le funzionalità dell'API Java collections. Una è la funzione map. Questa funzione restituisce un nuovo elenco contenente i risultati dell'applicazione della funzione di trasformazione specificata a ciascun elemento dell'elenco originale. Quindi, anziché creare un nuovo elenco e ripeterlo manualmente, possiamo utilizzare la funzione map e spostare la logica che era presente nel loop di for all'interno del corpo di map. Per impostazione predefinita, il nome dell'elemento dell'elenco corrente utilizzato in map è it, ma per una migliore leggibilità puoi sostituire it con il nome della tua variabile. Nel nostro caso, lo chiamiamo 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 questo passaggio, 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à denominata formattedUserNames con un getter personalizzato. Di seguito viene generato un metodo getFormattedUserNames() che restituisce un List.

In Java, esporremmo le nostre proprietà di classe tramite le funzioni getter e setter. Kotlin ci permette di avere una differenziazione migliore delle proprietà di una classe, espressa con campi e funzionalità, azioni che una classe può fare, espresse con le funzioni. Nel nostro caso, il corso Repository è molto semplice e non esegue alcuna azione, quindi contiene solo campi.

La logica che è stata attivata nella funzione getFormattedUserNames() di Java viene attivata quando si chiama il getter della proprietà Kotlin formattedUserNames.

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

Tuttavia, a volte vogliamo alcune funzionalità aggiuntive che il campo di supporto automatico non fornisce. Diamo un'occhiata a un esempio qui sotto.

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

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

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

Innanzitutto, rinomina users con _users. Ora aggiungi una proprietà immutabile pubblica che restituisce un elenco di utenti. Chiamiamola users:

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

Con questa modifica, la proprietà privata _users diventa la proprietà di supporto della proprietà pubblica users. Al di fuori del corso Repository, l'elenco _users non è modificabile, poiché i consumatori del corso possono accedervi 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)
    }
}

In questo 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 nella classe 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 elemento List è definita direttamente in Collections.kt dalla libreria standard.

In Java, ogni volta che hai bisogno di una funzionalità di utilità, è molto probabile che crei una classe Util e dichiari come funzionalità statica. In Kotlin puoi dichiarare le funzioni di primo livello, senza avere una classe. Tuttavia, Kotlin consente anche di creare funzioni di estensione. Si tratta di funzioni che estendono un determinato tipo, ma che sono dichiarate al di fuori del tipo. Di conseguenza, hanno un'affinità con quel tipo.

La visibilità delle funzioni e delle proprietà delle estensioni può essere limitata utilizzando i modificatori di visibilità. Questi limiti limitano l'utilizzo alle classi che ne hanno bisogno e non influiscono sullo spazio dei nomi.

Per la classe User, possiamo aggiungere una funzione di estensione che calcola il nome formattato oppure possiamo mantenere il nome formattato in una proprietà dell'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 fossero parte della classe User.

Poiché il nome formattato è una proprietà dell'utente e non una funzionalità della classe Repository, utilizziamo la proprietà dell'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 di Kotlin utilizza le funzioni delle estensioni 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 su Iterable.

Nel codice del corso 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 nome, Kotlin ha creato 5 funzioni di ambito: let, apply, with, run e also. Breve e potente, tutte queste funzioni hanno un ricevitore (this), possono avere un argomento (it) e possono restituire un valore. Puoi decidere quale utilizzare, in base agli obiettivi che vuoi raggiungere.

Ecco una scheda di riferimento utile per aiutarti a ricordarla:

Poiché stiamo configurando il nostro oggetto _users nel nostro 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 visto le nozioni di base necessarie per iniziare a refactoring del codice da Java a Kotlin. Questo refactoring è indipendente dalla piattaforma di sviluppo e consente di garantire che il codice scritto sia idiomatico.

Idiomatico Kotlin rende la scrittura di codice breve e piacevole. Con tutte le funzionalità offerte da Kotlin, ci sono molti modi per rendere il tuo codice più sicuro, conciso e più leggibile. Ad esempio, possiamo anche ottimizzare la nostra classe Repository creando un'istanza dell'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 di nullità, singleton, stringhe e raccolte ad argomenti quali le funzioni di estensione, di primo livello, di proprietà e di ambito. Siamo passati da due classi Java a due Kotlin simili alla seguente:

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 TL;DR delle funzionalità Java e la relativa mappatura su Kotlin:

Java

Kotlin

final oggetto

val oggetto

equals()

==

==

===

Classe contenente solo i dati

data classe

Inizializzazione nel costruttore

Inizializzazione nel blocco init

static campi e funzioni

campi e funzioni dichiarati in un elemento companion object

Classe Singleton

object

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