Introduzione a Kotlin: programmazione Android per gli esseri umani
Pubblicato: 2022-03-11In un perfetto mondo Android, il linguaggio principale di Java è davvero moderno, chiaro ed elegante. Puoi scrivere di meno facendo di più e ogni volta che viene visualizzata una nuova funzionalità, gli sviluppatori possono utilizzarla semplicemente aumentando la versione in Gradle. Quindi, durante la creazione di un'app molto bella, sembra completamente testabile, estensibile e gestibile. Le nostre attività non sono troppo grandi e complicate, possiamo cambiare le fonti di dati da database a web senza tonnellate di differenze e così via. Suona benissimo, vero? Sfortunatamente, il mondo Android non è così ideale. Google punta ancora alla perfezione, ma sappiamo tutti che i mondi ideali non esistono. Quindi, dobbiamo aiutare noi stessi in quel grande viaggio nel mondo Android.
Cos'è Kotlin e perché dovresti usarlo?
Quindi, la prima lingua. Penso che Java non sia il maestro dell'eleganza o della chiarezza, e non sia né moderno né espressivo (e immagino che tu sia d'accordo). Lo svantaggio è che al di sotto di Android N, siamo ancora limitati a Java 6 (incluse alcune piccole parti di Java 7). Gli sviluppatori possono anche allegare RetroLambda per utilizzare le espressioni lambda nel loro codice, il che è molto utile durante l'utilizzo di RxJava. Oltre ad Android N, possiamo utilizzare alcune delle nuove funzionalità di Java 8, ma è ancora quel vecchio Java pesante. Molto spesso sento gli sviluppatori Android dire "Vorrei che Android supportasse un linguaggio più carino, come fa iOS con Swift". E se ti dicessi che puoi usare un linguaggio molto carino e semplice, con sicurezza nulla, lambda e molte altre belle nuove funzionalità? Benvenuto in Kotlin.
Cos'è Kotlin?
Kotlin è un nuovo linguaggio (a volte indicato come Swift per Android), sviluppato dal team di JetBrains, ed è ora nella sua versione 1.0.2. Ciò che lo rende utile nello sviluppo di Android è che viene compilato in bytecode JVM e può anche essere compilato in JavaScript. È completamente compatibile con Java e il codice Kotlin può essere semplicemente convertito in codice Java e viceversa (c'è un plug-in di JetBrains). Ciò significa che Kotlin può utilizzare qualsiasi framework, libreria ecc. Scritti in Java. Su Android, si integra con Gradle. Se hai un'app Android esistente e desideri implementare una nuova funzionalità in Kotlin senza riscrivere l'intera app, inizia a scrivere in Kotlin e funzionerà.
Ma quali sono le "grandi novità"? Mi permetto di elencarne alcuni:
Parametri di funzione opzionali e denominati
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }
Possiamo chiamare il metodo createDate in modi diversi
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'
Sicurezza nulla
Se una variabile può essere nulla, il codice non verrà compilato a meno che non venga forzato a crearlo. Il codice seguente avrà un errore: nullableVar potrebbe essere null:
var nullableVar: String? = “”; nullableVar.length;
Per compilare, dobbiamo verificare se non è nullo:
if(nullableVar){ nullableVar.length }
Oppure, più breve:
nullableVar?.length
In questo modo, se nullableVar è null, non accade nulla. Altrimenti, possiamo contrassegnare la variabile come non nullable, senza un punto interrogativo dopo il tipo:
var nonNullableVar: String = “”; nonNullableVar.length;
Questo codice viene compilato e se vogliamo assegnare null a nonNullableVar, il compilatore mostrerà un errore.
C'è anche un operatore Elvis molto utile:
var stringLength = nullableVar?.length ?: 0
Quindi, quando nullableVar è null (quindi nullableVar?.length restituisce null), stringLength avrà valore 0.
Variabili mutabili e immutabili
Nell'esempio sopra, utilizzo var quando definisco una variabile. Questo è mutevole, possiamo riassegnarlo quando vogliamo. Se vogliamo che quella variabile sia immutabile (in molti casi dovremmo), usiamo val (come valore, non variabile):
val immutable: Int = 1
Successivamente, il compilatore non ci consentirà di riassegnare a immutable.
Lambda
Sappiamo tutti cos'è una lambda, quindi qui mostrerò solo come possiamo usarla in Kotlin:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})
O se la funzione è l'unico o l'ultimo argomento:
button.setOnClickListener { Log.d("Kotlin","Click")}
Estensioni
Le estensioni sono una funzionalità del linguaggio molto utile, grazie alla quale possiamo "estendere" le classi esistenti, anche quando sono definitive o non abbiamo accesso al loro codice sorgente.
Ad esempio, per ottenere un valore stringa da edit text, invece di scrivere ogni volta editText.text.toString() possiamo scrivere la funzione:
fun EditText.textValue(): String{ return text.toString() }
O più breve:
fun EditText.textValue() = text.toString()
E ora, con ogni istanza di EditText:
editText.textValue()
Oppure, possiamo aggiungere una proprietà che restituisce lo stesso:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}
Sovraccarico dell'operatore
A volte utile se vogliamo aggiungere, moltiplicare o confrontare oggetti. Kotlin consente l'overloading di operatori binari (più, meno, piùAssegna, intervallo, ecc.), operatori di array (ottenere, impostare, ottenere intervallo, impostare intervallo) e operazioni uguali e unarie (+a, -a, ecc.)
Classe di dati
Quante righe di codice sono necessarie per implementare una classe User in Java con tre proprietà: copy, equals, hashCode e toString? In Kaotlin hai bisogno di una sola riga:
data class User(val name: String, val surname: String, val age: Int)
Questa classe di dati fornisce i metodi equals(), hashCode() e copy() e anche toString(), che stampa User come:
User(name=John, surname=Doe, age=23)
Le classi di dati forniscono anche altre utili funzioni e proprietà, che puoi vedere nella documentazione di Kotlin.
Estensioni Anko
Usi Butterknife o estensioni Android, vero? Cosa succede se non hai nemmeno bisogno di usare questa libreria e dopo aver dichiarato le viste in XML usala semplicemente dal codice in base al suo ID (come con XAML in C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" />
loginBtn.setOnClickListener{}
Kotlin ha estensioni Anko molto utili, e con questo non è necessario dire alla tua attività cos'è loginBtn, lo sa semplicemente "importando" xml:
import kotlinx.android.synthetic.main.activity_main.*
Ci sono molte altre cose utili in Anko, tra cui avviare attività, mostrare brindisi e così via. Questo non è l'obiettivo principale di Anko: è progettato per creare facilmente layout dal codice. Quindi, se è necessario creare un layout a livello di codice, questo è il modo migliore.
Questa è solo una breve panoramica di Kotlin. Consiglio di leggere il blog di Antonio Leiva e il suo libro - Kotlin per sviluppatori Android e, naturalmente, il sito ufficiale di Kotlin.
Cos'è MVP e perché?
Un linguaggio bello, potente e chiaro non basta. È molto facile scrivere app disordinate con ogni lingua senza una buona architettura. Gli sviluppatori Android (per lo più quelli che stanno iniziando, ma anche quelli più avanzati) spesso danno ad Activity la responsabilità di tutto ciò che li circonda. L'attività (o il frammento o un'altra vista) scarica i dati, li invia per salvarli, li presenta, risponde alle interazioni dell'utente, modifica i dati, gestisce tutte le viste secondarie. . . e spesso molto di più. È troppo per oggetti instabili come Attività o Frammenti (è sufficiente ruotare lo schermo e l'Attività dice "Arrivederci...").
Un'ottima idea è isolare le responsabilità dai punti di vista e renderli il più stupidi possibile. Le viste (attività, frammenti, viste personalizzate o qualsiasi altra cosa presenti i dati sullo schermo) dovrebbero essere le uniche responsabili della gestione delle loro viste secondarie. Le visualizzazioni dovrebbero avere relatori, che comunicheranno con il modello e diranno loro cosa dovrebbero fare. Questo, in breve, è il modello Model-View-Presenter (per me, dovrebbe essere chiamato Model-Presenter-View per mostrare le connessioni tra i livelli).
"Ehi, conosco una cosa del genere e si chiama MVC!" - non hai pensato? No, MVP non è la stessa cosa di MVC. Nel modello MVC, la tua vista può comunicare con il modello. Durante l'utilizzo di MVP, non consenti alcuna comunicazione tra questi due livelli: l'unico modo in cui View può comunicare con Model è tramite Presenter. L'unica cosa che View sa di Model può essere la struttura dei dati. View sa come, ad esempio, visualizzare User, ma non sa quando. Ecco un semplice esempio:
View sa “Sono Activity, ho due EditTexts e un Button. Quando qualcuno fa clic sul pulsante, dovrei dirlo al mio presentatore e passargli i valori di EditTexts. E questo è tutto, posso dormire fino al prossimo clic o il presentatore mi dice cosa fare".
Il relatore sa che da qualche parte c'è una vista e sa quali operazioni può eseguire questa vista. Sa anche che quando riceve due stringhe, deve creare User da queste due stringhe e inviare i dati al modello da salvare e, se il salvataggio ha esito positivo, dire alla vista "Mostra informazioni di successo".
Il modello sa solo dove si trovano i dati, dove devono essere salvati e quali operazioni devono essere eseguite sui dati.
Le applicazioni scritte in MVP sono facili da testare, mantenere e riutilizzare. Un presentatore puro non dovrebbe sapere nulla della piattaforma Android. Dovrebbe essere pura classe Java (o Kotlin, nel nostro caso). Grazie a questo possiamo riutilizzare il nostro presentatore in altri progetti. Possiamo anche scrivere facilmente unit test, testare separatamente Model, View e Presenter.
Una piccola digressione: MVP dovrebbe far parte dell'architettura pulita di Uncle Bob per rendere le applicazioni ancora più flessibili e ben architettate. Proverò a scriverlo la prossima volta.
Esempio di app con MVP e Kotlin
Questa è abbastanza teoria, vediamo un po' di codice! Ok, proviamo a creare una semplice app. L'obiettivo principale di questa app è creare utenti. La prima schermata avrà due EditText (Nome e Cognome) e un Pulsante (Salva). Dopo aver inserito nome e cognome e aver fatto clic su "Salva", l'app dovrebbe mostrare "L'utente è salvato" e passare alla schermata successiva, dove vengono presentati nome e cognome salvati. Quando il nome o il cognome è vuoto, l'app non dovrebbe salvare l'utente e mostrare un errore che indica cosa c'è che non va.
La prima cosa dopo aver creato il progetto Android Studio è configurare Kotlin. Dovresti installare il plug-in Kotlin e, dopo il riavvio, in Strumenti> Kotlin puoi fare clic su "Configura Kotlin nel progetto". L'IDE aggiungerà le dipendenze di Kotlin a Gradle. Se hai del codice esistente, puoi convertirlo facilmente in Kotlin (Ctrl+Shift+Alt+K o Codice> Converti file Java in Kotlin). Se qualcosa non va e il progetto non viene compilato, o Gradle non vede Kotlin, puoi controllare il codice dell'app disponibile su GitHub.
Ora che abbiamo un progetto, iniziamo creando la nostra prima vista: CreateUserView. Questa vista dovrebbe avere le funzionalità menzionate in precedenza, quindi possiamo scrivere un'interfaccia per questo:

interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }
Come puoi vedere, Kotlin è simile a Java nella dichiarazione delle funzioni. Sono tutte funzioni che non restituiscono nulla e l'ultima ha un parametro. Questa è la differenza, il tipo di parametro viene dopo il nome. L'interfaccia Visualizza non è di Android: è la nostra interfaccia semplice e vuota:
interface View
L'interfaccia di Basic Presenter dovrebbe avere una proprietà di tipo View e almeno sul metodo (ad esempio suDestroy), dove questa proprietà sarà impostata su null:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }
Qui puoi vedere un'altra funzionalità di Kotlin: puoi dichiarare proprietà nelle interfacce e anche implementare metodi lì.
Il nostro CreateUserView deve comunicare con CreateUserPresenter. L'unica funzione aggiuntiva di cui ha bisogno questo presentatore è saveUser con due argomenti stringa:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }
Abbiamo anche bisogno della definizione del modello - è menzionata in precedenza la classe di dati:
data class User(val name: String, val surname: String)
Dopo aver dichiarato tutte le interfacce, possiamo iniziare a implementare.
CreateUserPresenter sarà implementato in CreateUserPresenterImpl:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }
La prima riga, con definizione di classe:
CreateUserPresenterImpl(override var view: CreateUserView?)
È un costruttore, lo usiamo per assegnare la proprietà della vista, definita nell'interfaccia.
MainActivity, che è la nostra implementazione CreateUserView, necessita di un riferimento a CreateUserPresenter:
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }
All'inizio della lezione, abbiamo definito il nostro presentatore:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }
È definito come immutabile (val) e viene creato dal delegato pigro, che verrà assegnato la prima volta che è necessario. Inoltre, siamo sicuri che non sarà nullo (nessun punto interrogativo dopo la definizione).
Quando l'utente fa clic sul pulsante Salva, Visualizza invia le informazioni al relatore con i valori di EditTexts. Quando ciò accade, l'utente dovrebbe essere salvato, quindi dobbiamo implementare il metodo saveUser in Presenter (e alcune delle funzioni del modello):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }
Quando un utente viene creato, viene inviato a UserValidator per verificarne la validità. Quindi, in base al risultato della convalida, viene chiamato il metodo corretto. Il costrutto when() {} è uguale a switch/case in Java. Ma è più potente: Kotlin consente l'uso non solo di enum o int in 'case', ma anche di intervalli, stringhe o tipi di oggetti. Deve contenere tutte le possibilità o avere un'altra espressione. Qui copre tutti i valori UserError.
Usando view?.showEmptyNameError() (con un punto interrogativo dopo la visualizzazione), siamo protetti da NullPointer. La vista può essere annullata nel metodo onDestroy e con questa costruzione non accadrà nulla.
Quando un modello utente non ha errori, dice a UserStore di salvarlo, quindi indica a View di mostrare il successo e mostrare i dettagli.
Come accennato in precedenza, dobbiamo implementare alcune cose modello:
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }
La cosa più interessante qui è UserValidator. Usando la parola oggetto, possiamo creare una classe singleton, senza preoccupazioni per thread, costruttori privati e così via.
La prossima cosa: nel metodo validateUser(user), c'è l'espressione with(user) {}. Il codice all'interno di tale blocco viene eseguito nel contesto dell'oggetto, passato con nome e cognome sono proprietà dell'utente.
C'è anche un'altra piccola cosa. Tutto il codice sopra, da enum a UserValidator, la definizione è inserita in un file (qui è anche la definizione della classe User). Kotlin non ti obbliga ad avere ogni classe pubblica in un singolo file (o a nominare la classe esattamente come file). Pertanto, se si dispone di alcuni brevi pezzi di codice correlato (classi di dati, estensioni, funzioni, costanti - Kotlin non richiede una classe per funzione o costante), è possibile inserirlo in un unico file invece di diffonderlo in tutti i file del progetto.
Quando un utente viene salvato, la nostra app dovrebbe mostrarlo. Abbiamo bisogno di un'altra vista: può essere qualsiasi vista Android, vista personalizzata, frammento o attività. Ho scelto Attività.
Quindi, definiamo l'interfaccia UserDetailsView. Può mostrare l'utente, ma dovrebbe anche mostrare un errore quando l'utente non è presente:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }
Successivamente, UserDetailsPresenter. Dovrebbe avere una proprietà utente:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }
Questa interfaccia sarà implementata in UserDetailsPresenterImpl. Deve sovrascrivere la proprietà dell'utente. Ogni volta che questa proprietà viene assegnata, l'utente dovrebbe essere aggiornato sulla vista. Possiamo usare un setter di proprietà per questo:
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }
L'implementazione di UserDetailsView, UserDetailsActivity, è molto semplice. Proprio come prima, abbiamo un oggetto presenter creato dal caricamento lento. L'utente da visualizzare deve essere passato tramite intento. C'è un piccolo problema con questo per ora e lo risolveremo in un momento. Quando abbiamo un utente dall'intento, View deve assegnarlo al loro relatore. Successivamente, l'utente verrà aggiornato sullo schermo o, se è nullo, verrà visualizzato l'errore (e l'attività terminerà, ma il presentatore non lo sa):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }
Il passaggio di oggetti tramite intent richiede che questo oggetto implementi l'interfaccia Parcelable. Questo è un lavoro molto "sporco". Personalmente, odio farlo a causa di tutti i CREATOR, le proprietà, il salvataggio, il ripristino e così via. Fortunatamente, esiste un plug-in appropriato, Parcelable per Kotlin. Dopo averlo installato, possiamo generare Parcelable con un solo clic.
L'ultima cosa da fare è implementare showUserDetails (utente: Utente) nella nostra MainActivity:
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }
E questo è tutto.
Abbiamo una semplice app che salva un Utente (in realtà non viene salvato, ma possiamo aggiungere questa funzionalità senza toccare presenter o view) e lo presenta sullo schermo. In futuro, se vogliamo cambiare il modo in cui l'utente viene presentato sullo schermo, ad esempio da due attività a due frammenti in un'attività, o due visualizzazioni personalizzate, le modifiche saranno solo nelle classi Visualizza. Naturalmente, se non cambiamo funzionalità o struttura del modello. Il presentatore, che non sa cosa sia esattamente View, non avrà bisogno di modifiche.
Qual è il prossimo?
Nella nostra app, il presentatore viene creato ogni volta che viene creata un'attività. Questo approccio, o il suo contrario, se Presenter dovesse persistere in tutte le istanze di attività, è oggetto di molte discussioni su Internet. Per me, dipende dall'app, dalle sue esigenze e dallo sviluppatore. A volte è meglio distruggere il presentatore, a volte no. Se decidi di mantenerne uno, una tecnica molto interessante è usare LoaderManager per questo.
Come accennato in precedenza, MVP dovrebbe far parte dell'architettura Clean di Uncle Bob. Inoltre, i buoni sviluppatori dovrebbero usare Dagger per inserire le dipendenze dei presentatori nelle attività. Aiuta anche a mantenere, testare e riutilizzare il codice in futuro. Attualmente, Kotlin funziona molto bene con Dagger (prima del rilascio ufficiale non era così facile), e anche con altre utili librerie Android.
Incartare
Per me, Kotlin è una grande lingua. È moderno, chiaro ed espressivo mentre è ancora sviluppato da persone fantastiche. E possiamo utilizzare qualsiasi nuova versione su qualsiasi dispositivo e versione Android. Qualunque cosa mi faccia arrabbiare con Java, Kotlin migliora.
Naturalmente, come ho detto, niente è l'ideale. Kotlin ha anche alcuni svantaggi. Le versioni più recenti del plugin gradle (principalmente da alpha o beta) non funzionano bene con questa lingua. Molte persone si lamentano del fatto che il tempo di compilazione è un po' più lungo di Java puro e gli apk hanno alcuni MB aggiuntivi. Ma Android Studio e Gradle stanno ancora migliorando e i telefoni hanno sempre più spazio per le app. Ecco perché credo che Kotlin possa essere un linguaggio molto carino per ogni sviluppatore Android. Provalo e condividi nella sezione commenti sotto quello che ne pensi.
Il codice sorgente dell'app di esempio è disponibile su Github: github.com/tomaszczura/AndroidMVPKotlin