Utilizzare i modelli Kotlin comuni con Android

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

Utilizzare i fragment

Le sezioni seguenti utilizzano esempi Fragment per evidenziare 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 al costruttore della superclasse Fragment.

All'interno di LoginFragment, puoi eseguire l'override di una serie di callback del ciclo di vita per rispondere alle modifiche dello 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 principale, utilizza la parola chiave super, come mostrato nell'esempio seguente:

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

Supporto di valori Null e inizializzazione

Negli esempi precedenti, alcuni parametri nei metodi sottoposti a override hanno tipi con il suffisso di un punto interrogativo ?. Ciò indica che gli argomenti trasmessi per questi parametri possono essere nulli. Assicurati di gestire la loro nullabilità in modo sicuro.

In Kotlin, devi inizializzare le proprietà di un oggetto quando lo dichiari. Ciò implica che quando ottieni un'istanza di una classe, puoi fare riferimento immediatamente a una qualsiasi delle sue proprietà accessibili. Gli oggetti View in un Fragment, tuttavia, non sono pronti per essere gonfiati fino alla chiamata di Fragment#onCreateView, quindi è necessario un modo per posticipare l'inizializzazione delle proprietà per un View.

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

Il seguente esempio 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 rilevare gli eventi di clic in Android implementando l'interfaccia OnClickListener. Gli oggetti Button contengono una funzione setOnClickListener() che accetta un'implementazione di OnClickListener.

OnClickListener ha un solo metodo astratto, onClick(), che devi implementare. Poiché setOnClickListener() accetta sempre un OnClickListener come argomento e poiché OnClickListener ha sempre lo stesso metodo astratto singolo, questa implementazione può essere rappresentata utilizzando una funzione anonima in Kotlin. Questa procedura è nota come conversione del metodo astratto singolo o conversione SAM.

La conversione SAM può rendere il codice molto più pulito. 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 passata a setOnClickListener() viene eseguito quando un utente fa clic su loginButton.

Oggetti companion

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

Nel seguente esempio, TAG è una costante String. Non hai bisogno di un'istanza univoca di String per ogni istanza di LoginFragment, quindi devi definirla in un oggetto complementare:

class LoginFragment : Fragment() {

    ...

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

Potresti definire TAG a livello superiore del file, ma il file potrebbe anche avere un numero elevato di variabili, funzioni e classi definite anche a livello superiore. Gli oggetti companion aiutano a connettere variabili, funzioni e la definizione della classe senza fare riferimento a un'istanza particolare di quella classe.

Delega della proprietà

Quando inizializzi le proprietà, potresti ripetere alcuni pattern più comuni di Android, ad esempio l'accesso a un ViewModel all'interno di un Fragment. Per evitare codice duplicato in eccesso, puoi utilizzare la sintassi di delegazione di proprietà di Kotlin.

private val viewModel: LoginViewModel by viewModels()

La delega di proprietà fornisce un'implementazione comune che puoi riutilizzare in tutta l'app. Android KTX fornisce alcuni delegati di proprietà. viewModels, ad esempio, recupera un ViewModel con ambito limitato all'Fragment corrente.

La delega delle proprietà utilizza la reflection, che aggiunge un overhead delle prestazioni. Il compromesso è una sintassi concisa che consente di risparmiare tempo di sviluppo.

Supporto di valori Null

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

Ad esempio, la seguente espressione non è valida in Kotlin. name è di tipo String e non è annullabile:

val name: String = null

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

val name: String? = null

Interoperabilità

Le regole rigorose di Kotlin rendono il codice più sicuro e conciso. Queste regole riducono le probabilità di avere un NullPointerException che causerebbe l'arresto anomalo della tua app. Inoltre, riducono il numero di controlli nulli che devi eseguire nel codice.

Spesso, quando scrivi un'app per Android, devi anche chiamare codice non Kotlin, in quanto la maggior parte delle API Android è scritta nel linguaggio di programmazione Java.

La nullabilità è un'area chiave in cui Java e Kotlin differiscono nel comportamento. Java è meno rigido con la sintassi di nullabilità.

Ad esempio, la classe Account ha alcune proprietà, tra cui una proprietà String denominata name. Java non ha le regole di Kotlin relative alla nullabilità, ma si basa su annotazioni di nullabilità facoltative per dichiarare esplicitamente se è possibile assegnare un valore nullo.

Poiché il framework Android è scritto principalmente in Java, potresti riscontrare questo scenario quando chiami le API senza annotazioni di nullabilità.

Tipi di piattaforma

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

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

Per risolvere questo problema, devi utilizzare le annotazioni di nullabilità ogni volta che scrivi codice in Java. Queste annotazioni sono utili sia per gli sviluppatori Java che per quelli Kotlin.

Ad esempio, ecco la classe Account come è 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, a indicare che può contenere un valore nullo. Kotlin tratterebbe quindi accessId come String?.

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

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

In questo scenario, name è considerato un String non nullabile in Kotlin.

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

Gestione dell'annullabilità

Se non hai la certezza di un tipo Java, devi considerarlo annullabile. Ad esempio, il membro name della classe Account non è annotato, quindi devi supporre che sia un String? che accetta valori nulli.

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

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

L'operatore !! considera tutto ciò che si trova sul lato sinistro come non nullo, quindi in questo caso, name viene considerato come un String non nullo. Se il risultato dell'espressione alla sua sinistra è null, la tua app genera un errore NullPointerException. Questo operatore è rapido e semplice, ma deve essere utilizzato con parsimonia, in quanto può reintrodurre istanze di NullPointerException nel codice.

Una scelta più sicura è utilizzare l'operatore safe-call, ?., come mostrato nell'esempio seguente:

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

Se name non è null, il risultato di name?.trim() è un valore del nome senza spazi bianchi iniziali o finali. Se name è null, il risultato di name?.trim() è null. Ciò significa che la tua app non può mai generare un NullPointerException durante l'esecuzione di questa istruzione.

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

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

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

Puoi anche utilizzare l'operatore Elvis per uscire anticipatamente 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 alle API Android

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

Ad esempio, l'accesso a Context di un Fragment è quasi sempre non nullo, poiché la maggior parte delle chiamate effettuate in un Fragment si verifica mentre il Fragment è collegato a un Activity (una sottoclasse di Context). Detto questo, Fragment#getContext non restituisce sempre un valore non nullo, poiché esistono scenari in cui un Fragment non è collegato a un Activity. Pertanto, il tipo restituito di Fragment#getContext è annullabile.

Poiché Context restituito da Fragment#getContext è annullabile (ed è annotato come @Nullable), devi considerarlo come Context? nel tuo codice Kotlin. Ciò significa applicare uno degli operatori menzionati in precedenza per gestire la nullabilità 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 Context non nullo e genera un IllegalStateException se chiamato quando un Context sarebbe nullo. In questo modo, puoi trattare il Context risultante come non nullo senza la necessità di operatori di chiamata sicura o soluzioni alternative.

Inizializzazione della proprietà

Le proprietà in Kotlin non vengono inizializzate per impostazione predefinita. Devono essere inizializzati quando viene inizializzata la classe che li contiene.

Puoi inizializzare le proprietà in diversi modi. L'esempio seguente mostra come inizializzare una variabile index assegnandole un valore nella dichiarazione di 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 costruito un LoginFragment.

Tuttavia, potresti avere alcune proprietà che non possono essere inizializzate durante la costruzione dell'oggetto. Ad esempio, potresti voler fare riferimento a un View all'interno di un Fragment, il che significa che il layout deve essere gonfiato per primo. L'inflazione non si verifica quando viene costruito un Fragment. Al contrario, viene gonfiato quando chiami Fragment#onCreateView.

Un modo per risolvere questo scenario è dichiarare la visualizzazione come Nullable 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)
    }
}

Sebbene funzioni come previsto, ora devi gestire l'annullabilità di View ogni volta che 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 l'inizializzazione di una proprietà quando viene costruito un oggetto. Se viene fatto riferimento alla proprietà prima dell'inizializzazione, Kotlin genera un errore UninitializedPropertyAccessException, quindi assicurati di inizializzare la proprietà il prima possibile.