V prvním článku jsme si vytvořili Kotlin Android projekt v Android Studiu 3. Taky jsme si ukázali základní použití knihovny Anko a udělali jsme první View, které jsme definovali v samostatné komponentě (AnkoComponent).
V tomto v pořadí druhém článku budeme pokračovat s rozvíjením znalostí o této úžasné knihovně pro vývoj Android aplikací. Dozvíme se například:
- Jak jednodušeji zobrazit toasty a dialogy
- Jak spustit aktivitu
- Jaké další užitečné funkce Anko obsahuje, například spuštění věcí na pozadí nebo v UI vlákně
Přehled dílů série:
1. Vytvoření projektu v Android Studio 3 a první UI
2. Tlačítka, toasty, dialogy, intenty, uživatelská rozhraní a práce na pozadí (background threads)
3. Vylepšujeme naše UI
4. Fragmenty & RecyclerView
Tlačítko onClick a Toast widget
V této části si ukážeme, jak můžeme v Anko zaregistrovat onClick akci k tlačítku, číst uživatelem zadaný text v EditText komponentě a zobrazovat toasty.
Vraťme se k našemu SignInView, které jsme vytvořili v minulém díle. Díky Kotlinu, a hlavně Anko knihovně, už nemůže být přidání onClick akce jednodušší:
button.setOnClickListener {
Toast.makeText(act, "Hello, ${username.text}!", Toast.LENGTH_SHORT).show()
}
Pokud však budeme vytvářet UI za pomocí Anko DSL (přímo v kódu namísto v XML), pak náš kód může vypadat ještě mnohem lépe:
button {
onClick { toast("Hello ${username.text}") }
}
Jak jste si mohli všimnout, pro každý ze dvou případů jsme použili jiné definování zobrazení toast komponenty. V prvním případě zobrazujeme toast klasickým Android způsobem. V druhém případě jsme už využili extension funkci z Anko knihovny pro zobrazení toastu. Tím pádem se vyhneme časté chybě, tedy tomu, že zapomeneme zavolat metodu show(). V obou případech se nám tak zobrazí toast s časovým zobrazením nastaveným na „SHORT“. Pokud chcete zobrazit „LONG“toast, pak Anko nabízí extension funkci s celkem logickým názvem:
longToast("Hello ${username.text}")
Nyní se zaměříme na EditText pro zadávání username. Po stisknutí tlačítka Sign In zobrazíme toast obsahující text, který uživatel zadal do pole username.
Nejdříve musíme získat referenci na username:
override fun createView(ui: AnkoContext<SignInActivity>) = with(ui) {
val username = editText {
...
}
}
Poté můžeme přidat onClick DSL blok do Sign In tlačítka a zobrazit text v daném toastu:
override fun createView(ui: AnkoContext<SignInActivity>) = with(ui) {
val username = editText {
...
}
...
button {
...
onClick { toast("Hello ${username.text}") }
}
}
Prozatím máme kód hotový, tak si spustíme aplikaci a otestujeme si to. Jestli jste všechno udělali správně, uvidíte něco jako na následujícím obrázku (za předpokladu, že jste zadali text Anko):
Jen pro kontrolu, váš kód v SignInView.kt by měl vypadat takto:
class SingInView : AnkoComponent<SignInActivity> {
override fun createView(ui: AnkoContext<SignInActivity>) = with(ui) {
verticalLayout {
lparams(width = matchParent, height = matchParent)
val username = editText {
id = R.id.usernameEditText
hintResource = R.string.sign_in_username
textSize = 24f
}.lparams(width = matchParent, height = wrapContent)
editText {
id = R.id.passwordEditText
hintResource = R.string.signIn_password
textSize = 24f
}.lparams(width = matchParent, height = wrapContent)
button {
id = R.id.signIn_button
textResource = R.string.signIn_button
onClick { toast("Hello ${username.text}") }
}.lparams(width = matchParent, height = wrapContent)
}
}
}
Vytvoření Sign In Business logiky
Teď budeme vyvářet první business logiku v rámci série článků o knihovně Anko. Nejdřív si ale připravíme doménové objekty.
Pro náš zatím jediný proces Sign In vytvoříme nový Kotlin soubor a pojmenujeme si ho jako DomainObjects.kt. Tam budeme definovat všechny naše POJO objekty jako datové třídy (data class). Tento soubor umístíme do nového package pojmenovaného jako model (v sign_in package). Následně v tomto souboru vytvoříme první datovou třídu pod názvem AuthCredentials s těmito argumenty:
data class AuthCredentials(val username: String,
val password: String)
Vytvoříme další package bl (jako business logic, případně si ho pojmenujte, jak uznáte za vhodné), nové rozhraní ISignInBL.kt a jeho implementaci SignInBL.kt.
V ISignInBL.kt rozhraní nadefinujeme funkci checkUserCredentials:
interface ISignInBL {
fun checkUserCredentials(credentials: AuthCredentials): Boolean
}
Následně tuto funkci nadefinujeme v SignInBL.kt, kde provedeme kontrolu uživatelského jména a hesla. Pro náš demo projekt nebudeme implementovat žádnou perzistentní vrstvu (to si ukážeme v budoucích článcích o Kotlinu, respektive o knihovně Requery). Kontrolu proto provedeme tak, že uživatel bude puštěn dál, pokud zadá uživatelské jméno = frosty a heslo = snowman.
class SignInBL : ISignInBL {
override fun checkUserCredentials(credentials: AuthCredentials): Boolean {
return ("frosty".equals(credentials.username) && "snowman".equals(credentials.password))
}
}
Super, máme připravenou velice jednoduchou business logiku pro sing in proces. V dalším kroku musíme přidat získání zadané hodnoty z username a password edit textů a jejich kontrolu. Ta se bude hodit v případě, že uživatel naší aplikace kolonky nevyplní. Poté využijeme business logiku pro kontrolu uživatelského přístupu (voláním checkUserCredentials funkce).
Kontrola v Sign In formuláři
Jak jsme si už řekli, budeme přidávat kontrolu vstupních polí (username a password) v Sign In formuláři. Jako první na seznamu je získání reference na námi vytvořené EditText komponenty (to samé jsme udělali v úvodu článku pro username):
val username = editText {
id = R.id.usernameEditText
hintResource = R.string.sign_in_username
textSize = 24f
}.lparams(width = matchParent, height = wrapContent)
val password = editText {
id = R.id.passwordEditText
hintResource = R.string.signIn_password
textSize = 24f
}.lparams(width = matchParent, height = wrapContent)
Následně vytvoříme privátní funkci handleOnSignInButtonPressed s těmito argumenty:
- ui: AnkoContext
- username: String
- password: String
Tou nahradíme náš „Hello“toast (onClick DSL blok přidaný k Sign In tlačítku):
button {
...
onClick {
handleOnSignInButtonPressed(
ui = ui,
username = username.text.toString(),
password = password.text.toString())
}
}.lparams(width = matchParent, height = wrapContent)
Alert Dialog
Anko obsahuje DSL blok pro zobrazování dialogů:
alert(message = "Hello, I'm Alert Dialog", title = "Cool Title") {
yesButton { toast("YES pressed!") }
noButton { toast("NO pressed!") }
}.show()
Celkem jednoduché, co říkáte?
Už víme, jak v Anko zobrazit Alert dialog. Nic nám tedy nebrání si nově nabytou znalost vyzkoušet v naší demo aplikaci. Kód pro zobrazení dialogu umístíme do funkce handleOnSignInButtonPressed, kde zkontrolujeme správnost zadaných uživatelských dat, jinak řečeno zjistíme, jestli jsou uživatelem kolonky vyplněné:
private fun handleOnSignInButtonPressed(ui: AnkoContext<SignInActivity>, username: String, password: String) {
if (username.isBlank() || password.isBlank()) {
with(ui) {
alert(title = R.string.sigIn_alert_invalid_user_title,
message = R.string.sigIn_alert_invalid_user_message) {
positiveButton(R.string.dialog_button_close) {}
}.show()
}
} else {
// TODO
}
Pokud jste zkoumali daný kód podrobněji, pak jste si určitě všimli, že používáme funkci isBlank(). Tato funkce se nachází ve stdlib Kotlin knihovny a slouží ke kontrole. Díky ní se dozvíme, jestli je daný text prázdný, nebo jestli obsahuje bílé znaky. Taky jsme použili výraz with{}, kam jsme umístili definici Alert dialogu. Argument ui používáme proto, že definujeme UI v Anko komponentě a potřebujeme mít referenci na context této komponenty, abychom mohli využívat extension funkce z Anko.
private fun handleOnSignInButtonPressed(ui: AnkoContext<SignInActivity>, username: String, password: String) {
if (username.isBlank() or password.isBlank()) {
with(ui) {
alert(title = R.string.sigIn_alert_invalid_user_title,
message = R.string.sigIn_alert_invalid_user_message) {
positiveButton(R.string.dialog_button_close) {}
}.show()
}
} else {
// TODO
}
}
Spustíme aplikaci a vyzkoušíme náš nový dialog. Ten by se měl zobrazit, pokud uživatel nevyplnil username nebo password a přesto stiskl Sign In tlačítko:
Málem bych zapomněl ještě ukázat definici nových textových řetězců:
<!-- strings.xml -->
<resources>
...
<string name="sigIn_alert_invalid_user_title">Sign In Failed! </string>
<string name="sigIn_alert_invalid_user_message">Invalid username or password.
</string>
<string name="dialog_button_close">Close</string>
</resources>
Zobrazujeme aktivity, fragmenty z Anko komponenty
Předpokládám, že většina z vás v reálných aplikacích pro oddělení logiky od definice UI z aktivit či fragmentů používá MVP nebo něco podobného, případně už nové Architecture Components představené na Google I/O 17. V naší demo aplikaci nic z toho používat nebudeme. Pokud by vás ale zajímal jeden z MVP frameworků, který se jmenuje Mosby, můžete se podívat na můj projekt na githubu, kotlin-anko-demo, kde jsem zkoušel spojit Kotlin, Anko, Mosby a další knihovny.
Pro komunikaci mezi aktivitami, fragmenty a dalšími komponentami používám na reálných projektech RxJava & RxKotlin například implementací RxBus. V našem demu však budeme používat komunikaci mezi aktivitou a UI skrze volání Anko komponenty z příslušné aktivity.
Jak víte z prvního článku, když vytváříme AnkoComponent<T>, musíme definovat generický typ T jakožto vlastníka té dané komponenty. Díky tomu můžeme z komponenty přistupovat k aktivitě, která je s komponentou svázaná, tedy owner. Z komponenty SignInView se tak na aktivitu dostaneme voláním třeba ui.owner.someFunction(...).
Otevřete si třídu SignInActivity.kt a přidejte novou funkci authorizeUser:
class SignInActivity : AppCompatActivity() {
...
fun authorizeUser(username: String, password: String) {
// TODO
}
}
Vraťme se zpět do SignInView.kt, kde jsme nechali TODO v else větvi z předešlé implementace. Uvnitř funkce handleOnSignInButtonPressed zavoláme nově vytvořenou funkci authorizeUser:
// SignInView.kt
private fun handleOnSignInButtonPressed(ui: AnkoContext<SignInActivity>, username: String, password: String) {
if (username.isBlank() or password.isBlank()) {
with(ui) {
alert(title = R.string.sigIn_alert_invalid_user_title,
message = R.string.sigIn_alert_invalid_user_message) {
positiveButton(R.string.dialog_button_close) {}
}.show()
}
} else {
ui.owner.authorizeUser(username, password)
}
}
Teď se vrátíme zpátky do SignInActivity.kt, kde si ukážeme, jak využitím Anko knihovny můžeme jednoduše spustit kód na pozadí (background vlákno).
Asynchronní volání
Každý z vás jistě ví, jaké je to peklo používat AsyncTask v Androidu. Nebo je potřeba napsat hodně omáčky okolo. Anko knihovna nabízí velmi jednoduchý způsob, jak spustit operaci na pozadí:
// Example from Anko documentation
doAsync {
// Long background task
uiThread {
result.text = "Done"
}
}
Poznámka: Ukázky se vztahují k verzi 0.9, v novějších verzích knihovny můžeme používat Kotlin Courotines, ale o nich budu mluvit třeba někdy jindy.
Pokud potřebujete asynchronně zavolat jinou funkci, pak má Anko funkci doAsyncResult(function_call_here):
doAsyncResult { callSomething() }
Dokonce můžete použít vlastní instanci exekutoru pro vykonání operace na pozadí:
// Example from Anko documentation
val executor = Executors.newScheduledThreadPool(4)
doAsync(executor) {
// Some task
}
Víme o další skvělé části knihovny Anko, tak si ji pojďme vyzkoušet.
V SignInActivity kontrolujeme správnost uživatelského jména a hesla zadaného uživatelem v Sign In formuláři. V reálné aplikaci bychom měli tyto údaje uložené například v databázi, ale tou se v tomto článku zabývat nebudeme. Anko samozřejmě obsahuje řadu extension metod pro práci s SQLite, ale o tom třeba jindy. Kontrolu správnosti uživatele provedeme v background vlákně (asynchronně):
class SignInActivity : AppCompatActivity() {
...
fun authorizeUser(username: String, password: String) {
doAsync {
val authorized = signInBL.checkUserCredentials(
AuthCredentials(username = username, password = password))
activityUiThread {
if (authorized) {
toast("Signed!!!")
} else {
view.showAccessDeniedAlertDialog()
}
}
}
}
Co vlastně v tomto kódu děláme? Na pozadí provádíme kontrolu uživatelských dat voláním příslušné funkce, kterou jsme definovali v business logice. Výsledek kontroly pak zpracováváme v UI vlákně aktivity. Pokud je vše v pořádku, zobrazíme toast, jinak se zobrazí alert dialog.
Poznámka: uiThread() volání se v Anko chová trošku odlišně, podle toho, jestli voláte například z aktivity. V tom případě se kód v lambdě nevykoná, pokud metoda isFinishing() na dané metodě vrací true. Pokud si to ale v aktivitě z nějakého důvodu přejete obejít, pak můžete použít kontext a zavolat na něm ctx.uiThread { }.
Aktuální kód SignInActivity by měl být:
package com.example.android.anko.sample.sign_in
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.example.android.anko.sample.sign_in.bl.ISignInBL
import com.example.android.anko.sample.sign_in.bl.SignInBL
import com.example.android.anko.sample.sign_in.model.AuthCredentials
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.setContentView
import org.jetbrains.anko.toast
class SignInActivity : AppCompatActivity() {
private val signInBL: ISignInBL = SignInBL()
private lateinit var view: SingInView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
view = SingInView()
view.setContentView(this)
}
fun authorizeUser(username: String, password: String) {
doAsync {
val authorized = signInBL.checkUserCredentials(AuthCredentials(username = username, password = password))
activityUiThread {
if (authorized) toast("Signed!!!") else view.showAccessDeniedAlertDialog()
}
}
}
}
V dalším kroku se vrátíme k SignInView.kt, tady nám totiž chybí implementovat funkci showAccessDeniedAlertDialog, kterou jsme v předchozím kroku přidali do aktivity:
fun showAccessDeniedAlertDialog() {
with(ankoContext) {
alert(title = R.string.sigIn_alert_access_denied_title,
message = R.string.sigIn_alert_access_denied_msg) {
positiveButton(R.string.dialog_button_close) {}
}.show()
}
}
<!-- strings.xml --> <string name="sigIn_alert_access_denied_title">Access Denied</string> <string name="sigIn_alert_access_denied_msg">You do not have permission to access this application!</string>
V metodě používám proměnnou ankoContext, ale kde se vzala? Jedná se o globální proměnnou s definicí Anko kontextu (předtím ui: AnkoContext<SignInActivity>), tak, abychom k ní měli přistup i z dalších funkcí:
class SingInView : AnkoComponent<SignInActivity> {
private lateinit var ankoContext: AnkoContext<SignInActivity>
override fun createView(ui: AnkoContext<SignInActivity>) =
with(ui) { {
ankoContext = ui
...
}
}
Intenty
V této kapitole bych vám rád ukázal, jak můžeme využít Anko knihovnu pro práci s intenty.
// Took it from original Anko documentation // Make a call makeCall(number) // Send a text sendSMS(number, [text]) //Browse the web browse(url) //Share some text share(text, [subject]) //Send a email email(email, [subject], [text]) //Arguments in square brackets ([]) are optional
Pokud se podíváte do zdrojových kódů Anko knihovny, tak tyto funkce nejsou nic jiného než extension funkce:
fun Context.makeCall(number: String): Boolean {
try {
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$number"))
startActivity(intent)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
Extension funkce z Anko můžeme použít pro práci s intenty, tak si to pojďme hned vyzkoušet v našem demo projektu. Takže jak spustit SignInActivity z MainActivity třídy?
Pokud nepotřebujeme při startu aktivity zadat žádné flagy či parametry, můžeme jednoduše napsat:
startActivity(Intent(this, SignInActivity::class.java))
Pokud ale potřebujeme nastavit parametry, pak by náš kód bez použití Anko knihovny vypadal takto:
val intent = Intent(this, SignInActivity::class.java)
intent.putExtra("checkUser", true)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
Využitím extension funkcí z Anko knihovny můžeme tento kód upravit:
startActivity(
intentFor<SignInActivity>("checkUser" to true).singleTop())
V naší demo aplikaci ale žádné argumenty či flagy nastavovat nebudeme, takže náš výsledný kód pro MainActivity je následující:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intentFor<SignInActivity>())
finish()
}
}
Shrnutí 2. části
V tomto druhém díle ze série o knihovně Anko jsme si řekli, jak jednodušeji zobrazovat na Androidu toasty a dialogy. Ukázali jsme si, jak definovat různé akce/action listenery pro UI komponenty. V našem případě to byla onClick akce přidaná na Sign In tlačítko.
Dále jsme zkusili interakci mezi AnkoComponent (SignInView) a aktivitou a asynchronní volání a spuštění operací na pozadí.
V příštím článku, tentokrát s pořadovým číslem tři, se vrátíme k UI a budeme si hrát s jeho vylepšením.






