(Ritiro programmato) Conversione in Kotlin

1. Ti diamo il benvenuto.

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.

Cosa imparerai

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 supporto
  • 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

2. Preparazione

Creare un nuovo progetto

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

Se utilizzi Android Studio, crea un nuovo progetto con il modello Nessuna attività. Scegli Kotlin come linguaggio del progetto. L'SDK minima può avere qualsiasi valore, non influirà sul risultato.

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

}

Noterai che il tuo IDE ti comunica che @Nullable non è definito. Importa androidx.annotation.Nullable se utilizzi Android Studio o org.jetbrains.annotations.Nullable se utilizzi IntelliJ.

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

3. Dichiarazione di nullabilità, val, var e classi di dati

Il nostro IDE è in grado di convertire automaticamente il codice Java in codice Kotlin, ma a volte ha bisogno di un piccolo aiuto. Lasciamo che il nostro IDE esegua un primo passaggio per la conversione. Poi esamineremo il codice risultante per capire come e perché è stato convertito 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 apportare una correzione dopo la conversione, premi .

e6f96eace5dabe5f.png

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, 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 di sola lettura 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. Per farlo, aggiunge un ? alla dichiarazione del tipo.

Poiché abbiamo contrassegnato firstName e lastName come nullabili, lo strumento di conversione automatica 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.

La conversione di base è già stata eseguita. Ma possiamo scriverlo in modo più idiomatico. Vediamo come.

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

Uguaglianza

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

4. Argomenti predefiniti, argomenti denominati

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("Joe", "Doe")

Kotlin ti consente di etichettare gli argomenti 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 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")

I valori predefiniti sono un concetto importante e spesso utilizzato nel codice Kotlin. Nel nostro codelab vogliamo specificare sempre il nome e il cognome in una dichiarazione di oggetto User, quindi non abbiamo bisogno di valori predefiniti.

5. Inizializzazione di oggetti, oggetti complementari e singleton

Prima di continuare il codelab, assicurati che la classe User sia una classe data. Ora 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:

  • L'elenco di users è annullabile perché l'oggetto non è stato istanziato al momento della dichiarazione
  • Le funzioni in Kotlin come getUsers() vengono dichiarate con il modificatore fun
  • Il metodo getFormattedUserNames() ora è una proprietà denominata formattedUserNames
  • L'iterazione sull'elenco di utenti (che inizialmente faceva parte di getFormattedUserNames() ha una sintassi diversa da quella Java
  • Il campo static ora fa parte di un blocco companion object
  • È stato aggiunto un blocco init

Prima di andare avanti, puliamo un po' il codice. Se esaminiamo il costruttore, notiamo che il convertitore ha reso l'elenco users un elenco modificabile che contiene oggetti Nullable. Anche se l'elenco può essere nullo, supponiamo che non possa contenere utenti nulli. Quindi, segui questi passaggi:

  • Rimuovi ? in User? all'interno della dichiarazione di tipo users
  • Rimuovi l'attributo ? in User? per il tipo di reso getUsers() in modo che restituisca List<User>?

Init block

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

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. 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 il blocco dell'oggetto companion e ha spostato qui il campo INSTANCE.

Gestione dei singleton

Poiché abbiamo bisogno di 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 sostituisci la definizione della classe con object Repository. Rimuovi anche l'oggetto companion.

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 formattedUserNames = Repository.formattedUserNames

Tieni presente che se una proprietà non ha un modificatore di visibilità, è pubblica per impostazione predefinita, come nel caso della proprietà formattedUserNames nell'oggetto Repository.

6. Gestione dell'annullabilità

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. Di conseguenza, per tutti gli utilizzi dell'oggetto users, è necessario utilizzare l'operatore di asserzione non null !!. Vedrai users!! e user!! in tutto il codice convertito. L'operatore !! converte qualsiasi variabile in un tipo non null, in modo da poter accedere alle proprietà o chiamare funzioni. Tuttavia, verrà generata un'eccezione se il valore della variabile è effettivamente null. Se utilizzi !!, 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 (nel blocco init). Pertanto, possiamo istanziare direttamente l'oggetto users 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() e fornire il tipo di elemento dell'elenco. mutableListOf<User>() crea un elenco vuoto che può contenere oggetti User. Poiché il tipo di dati della variabile ora può essere dedotto dal compilatore, rimuovi la dichiarazione di tipo esplicita della proprietà users.

private val users = mutableListOf<User>()

Abbiamo anche modificato var in val perché gli utenti conterranno un riferimento di sola lettura all'elenco degli utenti. Tieni presente che il riferimento è di sola lettura, quindi non può mai puntare a un nuovo elenco, ma l'elenco stesso è ancora modificabile (puoi aggiungere o rimuovere elementi).

Poiché la variabile users è già inizializzata, rimuovi questa inizializzazione dal blocco init:

users = ArrayList<Any?>()

Il blocco init dovrebbe avere il seguente aspetto:

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

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

Con queste modifiche, la nostra proprietà users ora non è nulla e possiamo rimuovere tutte le occorrenze non necessarie dell'operatore !!. Tieni presente che in Android Studio continuerai a visualizzare errori di compilazione, ma continua con i passaggi successivi dei codelab per risolverli.

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

Inoltre, per il valore userNames, se specifichi che il tipo di ArrayList è Strings, puoi rimuovere il tipo esplicito nella dichiarazione perché verrà dedotto.

val userNames = ArrayList<String>(users.size)

Destrutturazione

Kotlin consente di destrutturare 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 data 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. Aggiorna il ciclo for come mostrato di seguito. Sostituisci tutte le istanze di user.firstName con firstName e sostituisci user.lastName con lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if expression

I nomi nell'elenco userNames non sono ancora nel formato che vogliamo. Poiché sia lastName che firstName possono essere null, dobbiamo gestire la nullabilità quando creiamo l'elenco dei nomi utente formattati. Vogliamo visualizzare "Unknown" se manca uno dei due nomi. Poiché la variabile name non verrà modificata dopo l'impostazione, possiamo utilizzare val anziché var. Apporta prima questa modifica.

val name: String

Dai un'occhiata al codice che imposta la variabile name. Potrebbe sembrarti una novità vedere una variabile impostata in modo che sia uguale a un blocco di codice if / else. Ciò è consentito perché in Kotlin if e when sono espressioni, ovvero restituiscono un valore. L'ultima riga dell'estratto conto if verrà assegnata a name. L'unico scopo di questo blocco è inizializzare il valore name.

In sostanza, la logica presentata qui prevede che se lastName è null, name sia impostato su firstName o "Unknown".

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

Operatore Elvis

Questo codice 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, firstName viene restituito se non è nullo. Se firstName è null, l'espressione restituisce il valore a destra , "Unknown":

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

7. Modelli di stringa

Kotlin semplifica l'utilizzo delle String con i modelli di stringa. I modelli di stringa ti consentono di fare riferimento alle variabili all'interno delle dichiarazioni di stringa utilizzando il simbolo $ prima della variabile. Puoi anche inserire un'espressione all'interno di una dichiarazione di stringa, inserendola tra parentesi graffe {} e utilizzando il simbolo $ prima. Esempio: ${user.firstName}.

Il tuo codice attualmente utilizza la concatenazione di stringhe per combinare firstName e lastName nel nome utente.

if (firstName != null) {
    firstName + " " + lastName
}

Sostituisci invece la concatenazione di stringhe con:

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

L'utilizzo di modelli di stringa può semplificare il codice.

L'IDE mostrerà avvisi se esiste un modo più idiomatico per scrivere il codice. Noterai una sottolineatura ondulata nel codice e, se passi il mouse sopra, vedrai un suggerimento su come eseguire il refactoring del codice.

Al momento, dovresti visualizzare un avviso che indica che la dichiarazione name può essere unita all'assegnazione. Applichiamolo. Poiché il tipo della variabile name può essere dedotto, possiamo rimuovere la dichiarazione esplicita del tipo String. Ora il nostro formattedUserNames ha il seguente aspetto:

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
    }

Possiamo apportare un'ulteriore modifica. La nostra logica dell'interfaccia utente mostra "Unknown" nel caso in cui manchino il nome e il cognome, pertanto non supportiamo gli oggetti nulli. Pertanto, per il tipo di dati formattedUserNames sostituisci List<String?> con List<String>.

val formattedUserNames: List<String>

8. Operazioni sulle raccolte

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
  • Itera 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 ampio elenco 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 nome della tua 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
            }
        }

Tieni presente che utilizziamo l'operatore Elvis per restituire "Unknown" se user.lastName è null, poiché user.lastName è di tipo String? e per name è necessario un String.

...
else {
    user.lastName ?: "Unknown"
}
...

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

9. Proprietà e proprietà di supporto

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, esporremmo 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.

Sebbene 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 supporto automatico non fornisce.

Vediamo un esempio.

All'interno della 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
}

Poiché non volevamo che i chiamanti della classe Repository modificassero l'elenco degli utenti, abbiamo creato la funzione getUsers() che restituisce un List<User> di sola lettura. Con Kotlin, preferiamo utilizzare le proprietà anziché le funzioni in questi casi. Più precisamente, esporremo un List<User> di sola lettura supportato da un mutableListOf<User>.

Innanzitutto, rinominiamo users in _users. Evidenzia il nome della variabile, fai clic con il tasto destro del mouse per Refactor > Rename (Refattorizza > Rinomina) la variabile. Poi aggiungi una proprietà pubblica di sola lettura che restituisce un elenco di utenti. Chiamiamolo users:

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

A questo punto, puoi eliminare il metodo getUsers().

Con la modifica riportata sopra, 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.

Quando users viene chiamato dal codice Kotlin, viene utilizzata l'implementazione List della libreria standard Kotlin, in cui l'elenco non è modificabile. Se users viene chiamato da Java, viene utilizzata l'implementazione java.util.List, in cui l'elenco è modificabile e sono disponibili operazioni come add() e remove().

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

10. Funzioni e proprietà di primo livello e di estensione

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 List è già definita in Collections.kt della libreria standard Kotlin.

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 avere una classe. Tuttavia, Kotlin offre anche la possibilità di creare funzioni di estensione. Si tratta di funzioni che estendono un determinato tipo, ma sono dichiarate al di fuori del tipo.

La visibilità delle proprietà e delle funzioni di estensione 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à della classe User 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 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.

11. Funzioni di ambito: let, apply, with, run, also

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

Per eseguire il codice solo nel contesto di un oggetto specifico, senza dover accedere all'oggetto in base al suo nome, Kotlin offre cinque funzioni di ambito: let, apply, with, run e also. Queste funzioni rendono il codice più facile da leggere e più conciso. Tutte le funzioni di ambito hanno un ricevitore (this), possono avere un argomento (it) e possono restituire un valore.

Ecco una pratica scheda di riferimento per aiutarti a ricordare quando utilizzare ogni funzione:

6b9283d411fb6e7b.png

Poiché stiamo configurando l'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)
    }
 }

12. Conclusione

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

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

data 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 a 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: