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.