Introducere în Kotlin: Programare Android pentru oameni
Publicat: 2022-03-11Într-o lume Android perfectă, limbajul principal al Java este cu adevărat modern, clar și elegant. Puteți scrie mai puțin făcând mai mult și ori de câte ori apare o funcție nouă, dezvoltatorii o pot folosi doar prin creșterea versiunii în Gradle. Apoi, în timp ce creați o aplicație foarte frumoasă, aceasta pare complet testabilă, extensibilă și întreținută. Activitățile noastre nu sunt prea mari și complicate, putem schimba sursele de date de la baza de date la web fără tone de diferențe și așa mai departe. Sună grozav, nu? Din păcate, lumea Android nu este atât de ideală. Google se străduiește în continuare spre perfecțiune, dar știm cu toții că lumi ideale nu există. Astfel, trebuie să ne ajutăm în acea călătorie grozavă în lumea Android.
Ce este Kotlin și de ce ar trebui să-l folosești?
Deci, prima limbă. Cred că Java nu este stăpânul eleganței sau al clarității și nu este nici modern, nici expresiv (și bănuiesc că ești de acord). Dezavantajul este că sub Android N, suntem încă limitați la Java 6 (inclusiv câteva părți mici din Java 7). Dezvoltatorii pot, de asemenea, să atașeze RetroLambda pentru a utiliza expresii lambda în codul lor, ceea ce este foarte util în timpul utilizării RxJava. Deasupra Android N, putem folosi unele dintre noile funcționalități ale Java 8, dar este încă atât de vechi și greu de Java. Foarte des aud dezvoltatorii Android spunând „Mi-aș dori ca Android să accepte un limbaj mai frumos, așa cum face iOS cu Swift”. Și dacă ți-aș spune că poți folosi un limbaj foarte drăguț, simplu, cu siguranță nulă, lambdas și multe alte funcții noi și frumoase? Bun venit la Kotlin.
Ce este Kotlin?
Kotlin este un nou limbaj (uneori denumit Swift pentru Android), dezvoltat de echipa JetBrains și este acum în versiunea sa 1.0.2. Ceea ce îl face util în dezvoltarea Android este că se compilează în bytecode JVM și poate fi compilat și în JavaScript. Este pe deplin compatibil cu Java, iar codul Kotlin poate fi pur și simplu convertit în cod Java și invers (există un plugin de la JetBrains). Aceasta înseamnă că Kotlin poate folosi orice cadru, bibliotecă etc. scrisă în Java. Pe Android, se integrează de Gradle. Dacă aveți o aplicație Android existentă și doriți să implementați o nouă funcție în Kotlin fără a rescrie întreaga aplicație, începeți să scrieți în Kotlin și va funcționa.
Dar care sunt „noile funcții grozave”? Permiteți-mi să enumăr câteva:
Parametrii funcției opționale și numiți
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") }
Putem apela metoda createDate în moduri diferite
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'
Siguranță nulă
Dacă o variabilă poate fi nulă, codul nu se va compila decât dacă îi forțăm să o facă. Următorul cod va avea o eroare - nullableVar poate fi nul:
var nullableVar: String? = “”; nullableVar.length;
Pentru a compila, trebuie să verificăm dacă nu este nul:
if(nullableVar){ nullableVar.length }
Sau, mai scurt:
nullableVar?.length
În acest fel, dacă nullableVar este nul, nu se întâmplă nimic. În caz contrar, putem marca variabila ca nu nulă, fără semn de întrebare după tip:
var nonNullableVar: String = “”; nonNullableVar.length;
Acest cod se compilează, iar dacă vrem să atribuim null lui nonNullableVar, compilatorul va afișa o eroare.
Există, de asemenea, un operator Elvis foarte util:
var stringLength = nullableVar?.length ?: 0
Apoi, când nullableVar este nul (deci nullableVar?.length returnează null), stringLength va avea valoarea 0.
Variabile mutabile și imuabile
În exemplul de mai sus, folosesc var când definesc o variabilă. Acesta este mutabil, îl putem realoca oricând dorim. Dacă dorim ca variabila să fie imuabilă (în multe cazuri ar trebui), folosim val (ca valoare, nu variabilă):
val immutable: Int = 1
După aceea, compilatorul nu ne va permite să reatribuim la imuabil.
Lambda
Știm cu toții ce este un lambda, așa că aici voi arăta cum îl putem folosi în Kotlin:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})
Sau dacă funcția este singurul sau ultimul argument:
button.setOnClickListener { Log.d("Kotlin","Click")}
Extensii
Extensiile sunt o caracteristică de limbaj foarte utilă, datorită căreia putem „extinde” clasele existente, chiar și atunci când sunt finale sau nu avem acces la codul lor sursă.
De exemplu, pentru a obține o valoare șir din textul de editare, în loc să scriem de fiecare dată când editText.text.toString() putem scrie funcția:
fun EditText.textValue(): String{ return text.toString() }
Sau mai scurt:
fun EditText.textValue() = text.toString()
Și acum, cu fiecare instanță de EditText:
editText.textValue()
Sau, putem adăuga o proprietate care returnează aceeași:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}
Supraîncărcarea operatorului
Uneori util dacă vrem să adunăm, să înmulțim sau să comparăm obiecte. Kotlin permite supraîncărcarea operatorilor binari (plus, minus, plusAssign, range etc.), operatorilor matrice (get, set, get range, set range) și operații egale și unare (+a, -a, etc.)
Clasa de date
De câte linii de cod aveți nevoie pentru a implementa o clasă User în Java cu trei proprietăți: copy, equals, hashCode și toString? În Kaotlin aveți nevoie de o singură linie:
data class User(val name: String, val surname: String, val age: Int)
Această clasă de date oferă metode equals(), hashCode() și copy() și, de asemenea, toString(), care imprimă User ca:
User(name=John, surname=Doe, age=23)
Clasele de date oferă și alte funcții și proprietăți utile, pe care le puteți vedea în documentația Kotlin.
Extensii Anko
Folosești extensii Butterknife sau Android, nu-i așa? Ce se întâmplă dacă nici măcar nu trebuie să utilizați această bibliotecă și, după declararea vizualizărilor în XML, utilizați-o din cod după ID-ul său (cum ar fi XAML în C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" />
loginBtn.setOnClickListener{}
Kotlin are extensii Anko foarte utile, iar cu asta nu trebuie să spui activității tale ce este loginBtn, o știe doar „importând” xml:
import kotlinx.android.synthetic.main.activity_main.*
Există multe alte lucruri utile în Anko, inclusiv începerea activităților, arătarea toasturilor și așa mai departe. Acesta nu este scopul principal al Anko - este conceput pentru a crea cu ușurință machete din cod. Deci, dacă trebuie să creați un aspect programatic, acesta este cel mai bun mod.
Aceasta este doar o scurtă vedere a lui Kotlin. Recomand să citești blogul lui Antonio Leiva și cartea lui - Kotlin for Android Developers și, bineînțeles, site-ul oficial Kotlin.
Ce este MVP și de ce?
Un limbaj frumos, puternic și clar nu este suficient. Este foarte ușor să scrii aplicații dezordonate cu fiecare limbă fără o arhitectură bună. Dezvoltatorii Android (în mare parte cei care sunt la început, dar și cei mai avansați) dau adesea responsabilitatea Activity pentru tot ceea ce îi înconjoară. Activitatea (sau Fragmentul sau altă vizualizare) descarcă date, trimite pentru salvare, le prezintă, răspunde la interacțiunile utilizatorilor, editează datele, gestionează toate vizualizările copiilor. . . și adesea mult mai mult. Este prea mult pentru astfel de obiecte instabile precum Activități sau Fragmente (este suficient să rotiți ecranul și Activitatea spune „La revedere….”).
O idee foarte bună este să izolați responsabilitățile de opinii și să le faceți cât mai stupide. Vizualizările (Activități, Fragmente, vizualizări personalizate sau orice altceva prezintă date pe ecran) ar trebui să fie responsabile numai pentru gestionarea subviziunilor lor. Vizualizările ar trebui să aibă prezentatori, care vor comunica cu modelul și să le spună ce ar trebui să facă. Acesta, pe scurt, este modelul Model-View-Presenter (pentru mine, ar trebui să fie numit Model-Presenter-View pentru a afișa conexiunile dintre straturi).
„Hei, știu așa ceva și se numește MVC!” - nu ai crezut? Nu, MVP nu este același lucru cu MVC. În modelul MVC, vizualizarea dvs. poate comunica cu modelul. În timp ce utilizați MVP, nu permiteți nicio comunicare între aceste două straturi - singurul mod în care View poate comunica cu Model este prin Presenter. Singurul lucru pe care View îl știe despre Model poate fi structura datelor. View știe cum să afișeze, de exemplu, utilizatorul, dar nu știe când. Iată un exemplu simplu:
View știe că „Sunt Activitate, am două EditText și un Buton. Când cineva dă clic pe buton, ar trebui să-i spun prezentatorului meu și să-i transmit valorile EditTexts. Și asta e tot, pot dormi până când următorul clic sau prezentatorul îmi spune ce să fac.”
Prezentatorul știe că undeva este o vizualizare și știe ce operațiuni poate efectua această vizualizare. El știe, de asemenea, că atunci când primește două șiruri, ar trebui să creeze User din aceste două șiruri și să trimită date modelului pentru a le salva, iar dacă salvarea are succes, să spună vizualizarea „Afișează informațiile de succes”.
Modelul știe doar unde sunt datele, unde ar trebui să fie salvate și ce operațiuni ar trebui efectuate asupra datelor.
Aplicațiile scrise în MVP sunt ușor de testat, întreținut și reutilizat. Un prezentator pur nu ar trebui să știe nimic despre platforma Android. Ar trebui să fie clasa Java pură (sau Kotlin, în cazul nostru). Datorită acestui fapt, putem reutiliza prezentatorul nostru în alte proiecte. De asemenea, putem scrie cu ușurință teste unitare, testând separat Model, View și Presenter.
O mică digresiune: MVP ar trebui să facă parte din Arhitectura curată a unchiului Bob pentru a face aplicațiile și mai flexibile și mai frumos arhitecturate. Voi încerca să scriu despre asta data viitoare.
Exemplu de aplicație cu MVP și Kotlin
E suficientă teorie, să vedem niște coduri! Bine, să încercăm să creăm o aplicație simplă. Scopul principal al acestei aplicații este de a crea utilizator. Primul ecran va avea două EditTexts (Nume și Prenume) și un Buton (Salvare). După ce ați introdus numele și prenumele și dați clic pe „Salvați”, aplicația ar trebui să arate „Utilizatorul este salvat” și să mergeți la următorul ecran, unde sunt prezentate numele și prenumele salvate. Când numele sau prenumele sunt goale, aplicația nu ar trebui să salveze utilizatorul și să arate o eroare care indică ce este în neregulă.
Primul lucru după crearea proiectului Android Studio este să configurați Kotlin. Ar trebui să instalați pluginul Kotlin și, după repornire, în Instrumente > Kotlin puteți face clic pe „Configurați Kotlin în proiect”. IDE va adăuga dependențe Kotlin la Gradle. Dacă aveți vreun cod existent, îl puteți converti cu ușurință în Kotlin prin (Ctrl+Shift+Alt+K sau Cod > Convertiți fișierul Java în Kotlin). Dacă ceva nu este în regulă și proiectul nu se compila, sau Gradle nu vede Kotlin, puteți verifica codul aplicației disponibil pe GitHub.
Acum că avem un proiect, să începem prin a crea prima noastră vizualizare - CreateUserView. Această vizualizare ar trebui să aibă funcționalitățile menționate mai devreme, astfel încât să putem scrie o interfață pentru asta:

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 */ }
După cum puteți vedea, Kotlin este similar cu Java în declararea funcțiilor. Toate acestea sunt funcții care nu returnează nimic, iar ultimele au un singur parametru. Aceasta este diferența, tipul de parametru vine după nume. Interfața View nu este de la Android - este interfața noastră simplă, goală:
interface View
Interfața prezentatorului de bază ar trebui să aibă o proprietate de tip View și cel puțin pe metoda (onDestroy de exemplu), unde această proprietate va fi setată la null:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }
Aici puteți vedea o altă caracteristică Kotlin - puteți declara proprietăți în interfețe și, de asemenea, puteți implementa metode acolo.
CreateUserView trebuie să comunice cu CreateUserPresenter. Singura funcție suplimentară de care are nevoie acest Prezentator este saveUser cu două argumente șir:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }
Avem nevoie și de definirea modelului - este menționată mai devreme clasa de date:
data class User(val name: String, val surname: String)
După declararea tuturor interfețelor, putem începe implementarea.
CreateUserPresenter va fi implementat în CreateUserPresenterImpl:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }
Prima linie, cu definiția clasei:
CreateUserPresenterImpl(override var view: CreateUserView?)
Este un constructor, îl folosim pentru alocarea proprietății de vizualizare, definite în interfață.
MainActivity, care este implementarea noastră CreateUserView, are nevoie de o referință la 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() } }
La începutul orei, am definit prezentatorul nostru:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }
Este definit ca imuabil (val) și este creat de delegatul leneș, care va fi atribuit prima dată când este necesar. Mai mult, suntem siguri că nu va fi nul (fără semn de întrebare după definiție).
Când utilizatorul face clic pe butonul Salvare, View trimite informații către Prezentator cu valorile EditTexts. Când se întâmplă acest lucru, utilizatorul ar trebui să fie salvat, așa că trebuie să implementăm metoda saveUser în Presenter (și unele dintre funcțiile modelului):
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) } } }
Când un utilizator este creat, acesta este trimis către UserValidator pentru a verifica validitatea. Apoi, în funcție de rezultatul validării, este apelată metoda adecvată. Construcția when() {} este aceeași cu switch/case în Java. Dar este mai puternic - Kotlin permite utilizarea nu numai a enum sau int în „case”, ci și a intervalelor, șirurilor de caractere sau a tipurilor de obiecte. Trebuie să conțină toate posibilitățile sau să aibă o expresie else. Aici, acoperă toate valorile UserError.
Folosind view?.showEmptyNameError() (cu un semn de întrebare după vizualizare), suntem protejați de NullPointer. View poate fi anulat în metoda onDestroy și, cu această construcție, nu se va întâmpla nimic.
Când un model de utilizator nu are erori, acesta îi spune lui UserStore să-l salveze și apoi instrucționează View să arate succesul și să arate detalii.
După cum am menționat mai devreme, trebuie să implementăm câteva lucruri model:
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 } }
Cel mai interesant lucru aici este UserValidator. Folosind cuvântul obiect, putem crea o clasă singleton, fără griji cu privire la fire, constructori privați și așa mai departe.
Următorul lucru - în metoda validateUser(user), există expresia with(user) {}. Codul din cadrul unui astfel de bloc este executat în contextul obiectului, transmis cu numele și prenumele sunt proprietățile utilizatorului.
Există și un alt lucru mic. Tot codul de mai sus, de la enumerare la UserValidator, definiția este plasată într-un singur fișier (definiția clasei de utilizator este și aici). Kotlin nu vă obligă să aveți fiecare clasă publică într-un singur fișier (sau să denumiți clasa exact ca fișier). Astfel, dacă aveți câteva bucăți scurte de cod înrudit (clase de date, extensii, funcții, constante - Kotlin nu necesită clasă pentru funcție sau constantă), îl puteți plasa într-un singur fișier în loc să îl răspândiți prin toate fișierele din proiect.
Când un utilizator este salvat, aplicația noastră ar trebui să afișeze asta. Avem nevoie de o altă vizualizare - poate fi orice vizualizare Android, vizualizare personalizată, fragment sau activitate. Am ales Activitate.
Deci, să definim interfața UserDetailsView. Poate arăta utilizatorul, dar ar trebui să arate și o eroare atunci când utilizatorul nu este prezent:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }
Apoi, UserDetailsPresenter. Ar trebui să aibă o proprietate de utilizator:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }
Această interfață va fi implementată în UserDetailsPresenterImpl. Trebuie să suprascrie proprietatea utilizatorului. De fiecare dată când această proprietate este atribuită, utilizatorul ar trebui să fie reîmprospătat în vizualizare. Putem folosi un setter de proprietăți pentru asta:
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() } } }
Implementarea UserDetailsView, UserDetailsActivity, este foarte simplă. La fel ca și înainte, avem un obiect prezentator creat prin încărcare leneșă. Utilizatorul de afișat ar trebui să fie transmis prin intenție. Există o mică problemă cu asta deocamdată și o vom rezolva într-o clipă. Când avem utilizator din intenție, View trebuie să-l atribuie prezentatorului lor. După aceea, utilizatorul va fi reîmprospătat pe ecran sau, dacă este nul, va apărea eroarea (și activitatea se va termina - dar prezentatorul nu știe despre asta):
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() } }
Trecerea obiectelor prin intents necesită ca acest obiect să implementeze interfața Parcelable. Aceasta este o muncă foarte „murdară”. Personal, urăsc să fac asta din cauza tuturor CREATORILOR, proprietăților, salvării, restaurării și așa mai departe. Din fericire, există un plugin adecvat, Parcelable for Kotlin. După ce l-am instalat, putem genera Parcelable doar cu un singur clic.
Ultimul lucru de făcut este să implementați showUserDetails (utilizator: utilizator) în MainActivity:
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }
Și asta e tot.
Avem o aplicație simplă care salvează un Utilizator (de fapt, nu este salvat, dar putem adăuga această funcționalitate fără să atingem prezentatorul sau vizualizarea) și îl prezintă pe ecran. În viitor, dacă dorim să schimbăm modul în care utilizatorul este prezentat pe ecran, cum ar fi de la două activități la două fragmente într-o activitate sau două vizualizări personalizate, modificările vor fi doar în clasele View. Desigur, dacă nu schimbăm funcționalitatea sau structura modelului. Prezentatorul, care nu știe exact ce este View, nu va avea nevoie de nicio modificare.
Ce urmeaza?
În aplicația noastră, Prezentatorul este creat de fiecare dată când este creată o activitate. Această abordare, sau opusul ei, în cazul în care Prezentatorul ar persista în cazurile de activitate, este un subiect de multe discuții pe internet. Pentru mine, depinde de aplicație, de nevoile acesteia și de dezvoltator. Uneori este mai bine să distrugi prezentatorul, alteori nu. Dacă decideți să persistați unul, o tehnică foarte interesantă este să utilizați LoaderManager pentru asta.
După cum am menționat anterior, MVP ar trebui să facă parte din arhitectura Clean a unchiului Bob. Mai mult, dezvoltatorii buni ar trebui să folosească Dagger pentru a injecta dependențe de prezentatori în activități. De asemenea, ajută la menținerea, testarea și reutilizarea codului în viitor. În prezent, Kotlin funcționează foarte bine cu Dagger (înainte de lansarea oficială nu era atât de ușor), precum și cu alte biblioteci utile Android.
Învelire
Pentru mine, Kotlin este o limbă grozavă. Este modern, clar și expresiv, în timp ce este dezvoltat de oameni grozavi. Și putem folosi orice versiune nouă pe orice dispozitiv și versiune Android. Orice mă enervează pe Java, Kotlin se îmbunătățește.
Desigur, așa cum am spus, nimic nu este ideal. Kotlin au și unele dezavantaje. Cele mai noi versiuni de plugin gradle (în principal din alfa sau beta) nu funcționează bine cu acest limbaj. Mulți oameni se plâng că timpul de construire este puțin mai lung decât Java pur și apk-urile au niște MB-uri suplimentare. Dar Android Studio și Gradle încă se îmbunătățesc, iar telefoanele au tot mai mult spațiu pentru aplicații. De aceea cred că Kotlin poate fi un limbaj foarte frumos pentru fiecare dezvoltator de Android. Încearcă și împărtășește în secțiunea de comentarii de mai jos ceea ce crezi.
Codul sursă al aplicației exemplu este disponibil pe Github: github.com/tomatszczura/AndroidMVPKotlin