Utilizzare i modelli Kotlin comuni con Android

Questo argomento è incentrato su alcuni degli aspetti più utili del linguaggio Kotlin durante lo sviluppo per Android.

Utilizzare i frammenti

Le seguenti sezioni utilizzano esempi di Fragment per mettere in evidenza alcune delle migliori funzionalità di Kotlin.

Ereditarietà

Puoi dichiarare una classe in Kotlin con la parola chiave class. Nel seguente esempio, LoginFragment è una sottoclasse di Fragment. Puoi indicare l'ereditarietà utilizzando l'operatore : tra la sottoclasse e la relativa classe padre:

class LoginFragment : Fragment()

In questa dichiarazione di classe, LoginFragment è responsabile della chiamata del creatore della sua superclasse, Fragment.

All'interno di LoginFragment, puoi eseguire l'override di un numero di callback del ciclo di vita per rispondere alle modifiche di stato in Fragment. Per eseguire l'override di una funzione, utilizza la parola chiave override, come mostrato nell'esempio seguente:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Per fare riferimento a una funzione nella classe padre, utilizza la parola chiave super, come mostrato nell'esempio seguente:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Nulla e inizializzazione

Negli esempi precedenti, alcuni parametri nei metodi sottoposti a override hanno tipi con un punto interrogativo ? come suffisso. Ciò indica che gli argomenti passati per questi parametri possono essere nulli. Assicurati di gestire l'utilizzo dei valori null in sicurezza.

In Kotlin, devi inizializzare le proprietà di un oggetto quando lo dichiari. Ciò implica che quando si ottiene un'istanza di una classe, puoi fare riferimento immediatamente a qualsiasi delle sue proprietà accessibili. Tuttavia, gli oggetti View in Fragment non sono pronti per essere gonfiati fino alla chiamata Fragment#onCreateView, quindi devi rinviare l'inizializzazione della proprietà per View.

lateinit consente di posticipare l'inizializzazione della proprietà. Quando utilizzi lateinit, devi inizializzare la proprietà appena possibile.

L'esempio seguente mostra l'utilizzo di lateinit per assegnare oggetti View in onViewCreated:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

Conversione SAM

Puoi rimanere in ascolto degli eventi di clic in Android implementando l'interfaccia OnClickListener. Gli oggetti Button contengono una funzione setOnClickListener() che accetta un'implementazione di OnClickListener.

OnClickListener include un singolo metodo astratto, onClick(), che devi implementare. Poiché setOnClickListener() prende sempre un OnClickListener come argomento e poiché OnClickListener ha sempre lo stesso singolo metodo astratto, questa implementazione può essere rappresentata utilizzando una funzione anonima in Kotlin. Questo processo è noto come conversione del metodo Abstract singolo o conversione SAM.

La conversione SAM può rendere il tuo codice notevolmente più chiaro. L'esempio seguente mostra come utilizzare la conversione SAM per implementare un OnClickListener per un Button:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

Il codice all'interno della funzione anonima passato a setOnClickListener() viene eseguito quando un utente fa clic su loginButton.

Oggetti associati

Gli oggetti companion forniscono un meccanismo per definire variabili o funzioni collegate concettualmente a un tipo ma non a un particolare oggetto. Gli oggetti companion sono simili all'utilizzo della parola chiave static di Java per variabili e metodi.

Nell'esempio seguente, TAG è una costante String. Non è necessaria un'istanza univoca di String per ogni istanza di LoginFragment, pertanto devi definirla in un oggetto companion:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

Potresti definire TAG al livello superiore del file, ma il file potrebbe avere anche un numero elevato di variabili, funzioni e classi, che sono definite anche al livello superiore. Gli oggetti companion consentono di collegare variabili, funzioni e definizione della classe senza fare riferimento a una specifica istanza di quella classe.

Delega proprietà

Durante l'inizializzazione delle proprietà, potresti ripetere alcuni dei pattern più comuni di Android, ad esempio accedere a un ViewModel all'interno di un Fragment. Per evitare un eccesso di codice duplicato, puoi utilizzare la sintassi di delega della proprietà di Kotlin.

private val viewModel: LoginViewModel by viewModels()

La delega della proprietà fornisce un'implementazione comune che puoi riutilizzare in tutta l'app. Android KTX fornisce alcuni delegati della proprietà. viewModels, ad esempio, recupera un ViewModel che ha come ambito l'oggetto Fragment corrente.

La delega della proprietà utilizza la riflessione, che comporta un certo overhead per le prestazioni. Il compromesso è una sintassi concisa che consente di risparmiare tempo per lo sviluppo.

Nulla

Kotlin fornisce regole con supporto nullo rigoroso per mantenere la sicurezza dei tipi in tutta l'app. In Kotlin, i riferimenti agli oggetti non possono contenere valori nulli per impostazione predefinita. Per assegnare un valore null a una variabile, devi dichiarare un tipo di variabile nullable aggiungendo ? alla fine del tipo di base.

Ad esempio, la seguente espressione è illegale in Kotlin. name è di tipo String e non può essere nullo:

val name: String = null

Per consentire un valore null, devi utilizzare un tipo String con valore null, String?, come mostrato nell'esempio seguente:

val name: String? = null

Interoperabilità

Le rigide regole di Kotlin rendono il tuo codice più sicuro e conciso. Queste regole riducono le possibilità di avere un NullPointerException che potrebbe causare l'arresto anomalo della tua app. Inoltre, riducono il numero di controlli del valore di null da eseguire nel codice.

Spesso è necessario richiamare codice non Kotlin anche durante la scrittura di un'app per Android, poiché la maggior parte delle API Android è scritta nel linguaggio di programmazione Java.

La capacità di valori null è un'area chiave in cui Java e Kotlin differiscono nel comportamento. Java è meno rigoroso con sintassi nulla.

Ad esempio, la classe Account ha alcune proprietà, tra cui una proprietà String chiamata name. Java non ha regole di Kotlin sull'utilizzo di valori null, ma si basa su annotazioni relative alla possibilità di null facoltative per dichiarare esplicitamente se è possibile assegnare un valore nullo.

Poiché il framework Android è scritto principalmente in Java, potresti riscontrare questo scenario durante la chiamata ad API senza annotazioni con supporto nulla.

Tipi di piattaforma

Se utilizzi Kotlin per fare riferimento a un membro name non annotato definito in una classe Account Java, il compilatore non sa se String è mappato a String o String? in Kotlin. Questa ambiguità è rappresentata tramite un tipo di piattaforma, String!.

String! non ha un significato speciale per il compilatore Kotlin. String! può rappresentare String o String? e il compilatore consente di assegnare uno dei due tipi di valore. Tieni presente che rischi di generare un NullPointerException se rappresenti il tipo come String e assegni un valore nullo.

Per risolvere questo problema, devi utilizzare annotazioni con supporto nulla ogni volta che scrivi codice in Java. Queste annotazioni sono utili per gli sviluppatori Java e Kotlin.

Ad esempio, ecco la classe Account come viene definita in Java:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Una delle variabili membro, accessId, è annotata con @Nullable, che indica che può contenere un valore nullo. Kotlin tratterebbe quindi accessId come un String?.

Per indicare che una variabile non può mai essere nulla, utilizza l'annotazione @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

In questo scenario, name è considerato un String senza valore null in Kotlin.

Le annotazioni di nullità sono incluse in tutte le nuove API Android e in molte API Android esistenti. Molte librerie Java hanno aggiunto annotazioni con supporto null per supportare meglio gli sviluppatori Kotlin e Java.

Gestione dei valori null

Se non sei sicuro di un tipo Java, dovresti considerare che sia null. Ad esempio, il membro name della classe Account non è annotato, quindi dovresti presumere che sia un String con null.

Se vuoi tagliare name in modo che il suo valore non includa spazi vuoti iniziali o finali, puoi utilizzare la funzione trim di Kotlin. Puoi tagliare tranquillamente una String? in diversi modi. Uno di questi modi consiste nell'utilizzare l'operatore di asserzione not-null, !!, come mostrato nell'esempio seguente:

val account = Account("name", "type")
val accountName = account.name!!.trim()

L'operatore !! tratta tutto ciò che si trova sul lato sinistro come non null; pertanto, in questo caso, stai trattando name come un String non null. Se il risultato dell'espressione a sinistra è null, l'app genera un NullPointerException. Questo operatore è facile e veloce, ma deve essere utilizzato con moderazione, in quanto può reintrodurre istanze di NullPointerException nel codice.

Una scelta più sicura è quella di utilizzare l'operatore di chiamata sicura, ?., come mostrato nell'esempio seguente:

val account = Account("name", "type")
val accountName = account.name?.trim()

Utilizzando l'operatore di chiamata sicura, se name è diverso da null, il risultato di name?.trim() sarà un valore nome senza spazi vuoti iniziali o finali. Se name è null, il risultato di name?.trim() è null. Ciò significa che la tua app non potrà mai generare un NullPointerException durante l'esecuzione di questa istruzione.

Mentre l'operatore di chiamata sicura ti salva da un potenziale NullPointerException, passa un valore nullo all'istruzione successiva. Puoi invece gestire immediatamente i casi nulli utilizzando un operatore Elvis (?:), come mostrato nell'esempio seguente:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Se il risultato dell'espressione sul lato sinistro dell'operatore Elvis è nullo, il valore sul lato destro viene assegnato a accountName. Questa tecnica è utile per fornire un valore predefinito che altrimenti sarebbe nullo.

Puoi anche utilizzare l'operatore Elvis per tornare in anticipo da una funzione, come mostrato nell'esempio seguente:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Modifiche all'API Android

Le API Android stanno diventando sempre più compatibili con Kotlin. Molte delle API più comuni di Android, tra cui AppCompatActivity e Fragment, contengono annotazioni di nullità e alcune chiamate come Fragment#getContext hanno più alternative compatibili con Kotlin.

Ad esempio, l'accesso all'elemento Context di un Fragment non è quasi sempre null, poiché la maggior parte delle chiamate che effettui in un Fragment avviene mentre Fragment è associato a Activity (una sottoclasse di Context). Detto questo, Fragment#getContext non restituisce sempre un valore diverso da null, poiché esistono scenari in cui un Fragment non è associato a un Activity. Di conseguenza, il tipo restituito di Fragment#getContext è null.

Poiché il valore Context restituito da Fragment#getContext è null (e viene annotato come @Nullable), devi considerarlo come Context? nel codice Kotlin. Ciò significa applicare uno degli operatori citati in precedenza per risolvere i problemi di nullità prima di accedere alle sue proprietà e funzioni. Per alcuni di questi scenari, Android contiene API alternative che offrono questa comodità. Fragment#requireContext, ad esempio, restituisce un valore Context diverso da null e genera un IllegalStateException se viene chiamato quando Context è nullo. In questo modo, puoi trattare il Context risultante come un valore non null senza la necessità di operatori di chiamata sicura o soluzioni alternative.

Inizializzazione proprietà

Le proprietà in Kotlin non vengono inizializzate per impostazione predefinita. Devono essere inizializzati quando la relativa classe di inclusione viene inizializzata.

Puoi inizializzare le proprietà in diversi modi. L'esempio seguente mostra come inizializzare una variabile index assegnando un valore nella dichiarazione della classe:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Questa inizializzazione può essere definita anche in un blocco di inizializzazione:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

Negli esempi precedenti, index viene inizializzato quando viene creato un LoginFragment.

Tuttavia, potresti avere alcune proprietà che non possono essere inizializzate durante la creazione dell'oggetto. Ad esempio, potresti voler fare riferimento a un elemento View dall'interno di un Fragment, il che significa che il layout deve prima essere gonfiato. L'inflazione non si verifica quando viene creato un Fragment. Al contrario, è aumentato in modo artificioso quando chiami Fragment#onCreateView.

Un modo per risolvere questo scenario è dichiarare la vista come null e inizializzarla il prima possibile, come mostrato nell'esempio seguente:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Anche se questo comportamento funziona come previsto, ora devi gestire il valore nulla di View ogni volta che ne fai riferimento. Una soluzione migliore è utilizzare lateinit per l'inizializzazione di View, come mostrato nell'esempio seguente:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

La parola chiave lateinit ti consente di evitare di inizializzare una proprietà durante la creazione di un oggetto. Se viene fatto riferimento alla proprietà prima di essere inizializzata, Kotlin genera un UninitializedPropertyAccessException, quindi assicurati di inizializzare la proprietà il prima possibile.