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.