Úvod do knihovny Anko pro Android (2/4)

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.

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.

Václav Souhrada
Kotlin & Android Developer at eMan, Czech Kotlin User Group Founder and Organizer

RSS