Ukážeme si pár triků, jak si usnadnit práci s toasty a dialogy. Čeká nás taky spouštění aktivit a dostaneme se i k dalším užitečným vychytávkám, co můžeme v knihovně Anko najít. Tento článek navazuje na první díl, kde jsme položili základy, na kterých teď budeme stavět naši demo aplikaci.
Úvod do knihovny Anko pro Android (2/4)
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.