V posledním díle mini série o knihovně Anko si ukážeme, jak psát layout v Anko DSL ve fragmentu. Taky se naučíme pracovat s RecyclerView komponentou, opět za použití DSL definice.
Úvod do knihovny Anko pro Android (4/4)
V prvním článku jsme si řekli o vytvoření UI, které jsme definovali jako samostatné komponenty (AnkoComponent) a naučili jsme se je používat uvnitř aktivit. Ve druhém článku jsme přidali do naší demo aplikace trochu té logiky. Ve třetím článku jsme si naše UI vylepšili.
A v tomto posledním článku si ukážeme, jak použít AnkoComponent
i pro definici UI layout ve fragmentech. Naučíme se definici custom view pro dialog a používání FAB tlačítka. Na závěr si se podíváme na použití RecyclerView
komponenty, kde si opět vytvoříme UI layout v Anko DSL.
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
Fragmenty
V předchozích článcích jsme si vytvořili velice jednoduchou Sign In funkci jako aktivitu. Teď přidáme novou funkčnost; přidávání poznámek. Po správném zadání uživatelského jména (frosty) a hesla (snowman) bude flow aplikace přesměrováno do nové funkce pro možnost přidávání poznámek. Poznámku bude možno přidat po stisknutí FAB tlačítka a vyplnění textu poznámky v dialogu.
Výsledná aplikace pak bude vypadat takto:
Vytvoříme nový package notes s těmito třídami:
- NotesFragment
- NotesView
- ContainerActivity
- ContainerView
NotesFragment
bude sloužit k zobrazení a zadání nových poznámek. UI layout bude definován v Anko komponentě, v tomto případě v NotesView
.
Jako první vytvoříme NotesFragment
třídu dědící od Fragment ze supportu knihovny:
class NotesFragment : Fragment()
Pak vytvoříme Anko komponentu NotesView
, abychom mohli použít její referenci v NotesFragmentu
:
class NotesView : AnkoComponent<NotesFragment>
Vraťme se zpět do NotesFragment
třídy. V předchozích článcích jsme se naučili, jak vytvořit aktivitu a nastavit v ní content view jako referenci do UI vytvořeného v Anko komponentě. A to všechno ve funkci onCreate
:
class SignInActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) SingInView().setContentView(this) } }
Jak tohoto ale docílíme ve fragmentu? Skoro stejně jednoduše jako u aktivity. Ve funkci onCreateView
vytvoříme instanci Anko komponenty (NotesView) a přes funkci createView
vytvoříme Anko Context. Tam nadefinujeme referenci na owner pro NotesView
.
class NotesFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return NotesView().createView(AnkoContext.Companion.create(inflater.context, this, false)) } }
Máme připravenou “hrubou stavbu” pro funkci přidání nové poznámky a její zobrazení v listu. Musíme však ještě zavést fragment a říct systému, kdy a za jakých podmínek ho má zobrazit.
ContainerActivity bude obsahovat frame layout pro vložení a zobrazení fragmentu s poznámkami, jehož definici provedeme v samostatné třídě ContainerView.
class ContainerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ContainerView().setContentView(this) if (savedInstanceState == null) { supportFragmentManager.beginTransaction().replace(R.id.container, NotesFragment()).commit() } } }
class ContainerView : AnkoComponent<ContainerActivity> { override fun createView(ui: AnkoContext<ContainerActivity>) = with(ui) { frameLayout { id = R.id.container } } }
V prvních dvou článcích jsme se věnovali funkci pro přihlášení uživatele do aplikace. V případě správného přihlášení přesměrujeme nyní uživatele do ContainerActivity:
startActivity<ContainerActivity>()
class SignInActivity : AppCompatActivity() { // ... fun authorizeUser(username: String, password: String) { doAsync { // ... activityUiThread { if (authorized) { startActivity<ContainerActivity>() } else { view.showAccessDeniedAlertDialog() } } } } }
Pojďme teď nadefinovat základní UI layout v NotesView
. Jako první vytvoříme RelativeLayout
a přidáme FAB tlačítko. Pro tyto účely však musíme přidat pár nových závislostí:
implementation com.android.support:design:27.0.2 implementation org.jetbrains.anko:anko-design:0.9.1 implementation org.jetbrains.anko:anko-appcompat-v7:0.9.1
V prvním článku jsme si vytvořili projekt se strukturou, ve které všechny dependence definujeme, v dependencies.gradle a v app/build.gradle se na tyto dependence odkážeme. Samozřejmě, pokud používáte svoji strukturu, pak si závislosti přidejte rovnou do app/build.gradle souboru.
/** * UI layout definition for a [MainActivity] * * @author eMan s.r.o. * @see[AnkoComponent] * @see[MainActivity] */ class NotesView : AnkoComponent<NotesFragment> { override fun createView(ui: AnkoContext<NotesFragment>) = with(ui) { relativeLayout { lparams(width = matchParent, height = matchParent) floatingActionButton { imageResource = android.R.drawable.ic_input_add }.lparams { gravity = Gravity.BOTTOM or Gravity.END margin = dip(10) alignParentBottom() alignParentEnd() alignParentRight() } } } }
Pokud se podíváte do kódu či na obrázek, pak vidíte, že jsme vytvořili jednoduchý layout (RelativeLayout) s FAB tlačítkem. Podobně jako u tlačítek a jiných UI komponent máme i u FAB k dispozici onClick dsl blok. Přidáme zobrazení dialogu, ve kterém umožníme uživateli naší aplikace zadat novou poznámku:
class NotesView : AnkoComponent<NotesFragment> { private lateinit var ankoContext: AnkoContext<NotesFragment> override fun createView(ui: AnkoContext<NotesFragment>) = with(ui) { ankoContext = ui relativeLayout { lparams(width = matchParent, height = matchParent) floatingActionButton { imageResource = android.R.drawable.ic_input_add onClick { showAddNewNoteDialog() } }.lparams { gravity = Gravity.BOTTOM or Gravity.END margin = dip(10) alignParentBottom() alignParentEnd() alignParentRight() } } } }
Můžete si všimnout, že jsme na řádku č. 3 a č. 6 nadefinovali anko context
jako globální propertu, kterou využijeme ihned v metodě pro vytvoření alert dialogu pro zadání poznámky:
private fun showAddNewNoteDialog() { with(ankoContext) { alert { title(R.string.notes_dialog_new_note_title) positiveButton(R.string.notes_dialog_new_note_button_positive) { // TODO } negativeButton(R.string.notes_dialog_new_note_button_negative) } }.show() }
Poznámka:
Nově jsme přidali následující texty:
<string name="notes_dialog_new_note_title">Add a New Note</string> <string name="notes_dialog_new_note_button_positive">Add</string> <string name="notes_dialog_new_note_button_negative">Cancel</string> <string name="notes_dialog_new_note_hint">Write your note here ...</string>
V našem novém alert dialogu zobrazujeme title a tlačítka pro přidání a zavření dialogu. Jak ale přidáme EditText
? Alert DSL blok obsahuje možnost přidat vlastní UI layout za použití customView {}
bloku:
alert { title(R.string.notes_dialog_new_note_title) customView { verticalLayout { lparams(width = matchParent, height = matchParent) val noteEditText = editText { hintResource = R.string.notes_dialog_new_note_hint }.lparams(width = matchParent, height = wrapContent) { topMargin = dip(15) bottomMargin = dip(15) leftMargin = dip(20) rightMargin = dip(20) } positiveButton(R.string.notes_dialog_new_note_button_positive) { // TODO } negativeButton(R.string.notes_dialog_new_note_button_negative) } } } }.show()
Nadpis v tom dialogu nevypadá asi úplně nejlíp… Co kdybychom využili toolbar komponentu z app-compat knihovny?
alert { customView { verticalLayout { // ... toolbar { titleResource = R.string.notes_dialog_new_note_title backgroundColor = ContextCompat.getColor(ctx, R.color.colorPrimary) setTitleTextColor(ContextCompat.getColor(ctx, android.R.color.white)) } // ... } } }.show()
Paráda! Uživatel může nyní zapsat svoji poznámku, a i ten nadpis v dialogu vypadá o něco lépe. Ale moment, po stisknutí tlačítka ADD nám dialog nic nedělá… No zapomněli jsme definovat onClick
listener. Jaký bude tedy use-case pro přidání poznámky?
Uživatel zadá novou poznámku a po stisknutí tlačítka ADD se nejprve zkontroluje, jestli poznámka obsahuje nějaký text. Pokud ne, dojde k zobrazení toast zprávy. V tom lepším případě se poznámka přidá do adapteru, který bude využíván RecyclerView
komponentou. Abychom mohli zareagovat například na změnu orientace zařízení, pak list poznámek bude držen jako reference uvnitř NotesFragment
.
RecyclerView
V této části se naučíme pracovat s RecyclerView komponentou za použití Anko DSL layoutu.
Nejdříve však musíme přidat závislosti na RecyclerView a CardView komponenty a k nim odpovídající Anko knihovny:
implementation “com.android.support:recyclerview-v7:27.0.2” implementation “com.android.support:cardview-v7:27.0.2” implementation "org.jetbrains.anko:anko-recyclerview-v7:0.9.1" implementation "org.jetbrains.anko:anko-cardview-v7:0.9.1"
Klasický layout pro ViewHolder by vypadal takto:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="10dp"> <TextView android:id="@+id/txtNote" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"/> </LinearLayout> </android.support.v7.widget.CardView> </LinearLayout>
My však chceme mít všechno v rámci Anko DSL. Vytvoříme si tedy novou třídu NotesItemView
, kde na základě výše uvedeného XML přepíšeme layout do DSL.
package cz.eman.android.sample.anko.notes.adapter import android.support.v4.widget.TextViewCompat import android.view.ViewGroup import cz.eman.android.sample.anko.R import org.jetbrains.anko.* import org.jetbrains.anko.cardview.v7.cardView /** * UI represents a single item contains a note. * * @author eMan s.r.o. * @see[AnkoComponent] */ class NotesItemView : AnkoComponent<ViewGroup> { override fun createView(ui: AnkoContext<ViewGroup>) = with(ui) { verticalLayout { lparams(width = matchParent, height = wrapContent) cardView { verticalLayout { lparams(width = matchParent, height = wrapContent) padding = dip(10) textView { id = R.id.txtNote TextViewCompat.setTextAppearance(this, R.style.Base_TextAppearance_AppCompat_Medium) }.lparams(width = matchParent, height = wrapContent) } }.lparams(width = matchParent, height = matchParent) { margin = dip(5) } } } }
Na vytvoření layoutu není celkem nic zvláštního a nového. Můžete si však všimnout, že vlastníkem (owner) dané komponenty není fragment ani aktivita, protože dané view je pro view holder a nepotřebujeme ani přistupovat k referenci na adapter. Proto nejjednodušším způsobem je nastavit context jako ownera, případně jako v našem případě ViewGroup
. Existuje však i elegantnější řešení, které je nicméně nad rámec této série. Můžete si vyzkoušet varianty popsané v ticketu zde.
O něco zajímavější je pak vytvoření adapteru – NotesAdapter
:
package cz.eman.android.sample.anko.notes.adapter import android.support.v7.widget.RecyclerView import android.view.View import android.view.ViewGroup import kotlinx.android.synthetic.main.view_notes_item.view.* import org.jetbrains.anko.AnkoContext /** * An adapter which contains list of all available notes. * * @author vsouhrada (vaclav.souhrada@eman.cz) * @see[RecyclerView.Adapter] */ class NotesAdapter(private var notes: MutableList<String>) : RecyclerView.Adapter<NotesAdapter.NotesViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotesViewHolder { return NotesViewHolder(NotesItemView().createView(AnkoContext.Companion.create(parent.context, parent))) } override fun onBindViewHolder(holder: NotesViewHolder?, position: Int) { holder?.bindItem(notes[position]) } override fun getItemCount() = notes.size fun updateNotes(notes: MutableList<String>) { this.notes = notes notifyDataSetChanged() } fun addNote(note: String) { notes.add(note) notifyItemChanged(notes.size - 1) } /** * Describes an item note view about its place within the RecyclerView. * * @author eMan s.r.o. * @see[RecyclerView.ViewHolder] */ inner class NotesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bindItem(note: String) { itemView.txtNote.text = note } } }
Ve funkci onCreateViewHolder(...)
vytváříme instanci již implementované Anko komponenty NotesItemView. Jako parenta jsme nastavili parent view group a použili jsme ho i jako referenci na kontext. Podobně jako u fragmentu tak vytvoříme Anko komponentu obsahující náš layout zapsaný v DSL.
Funkce updateNotes(notes: MutableList<String>)
slouží k vložení všech dostupných poznámek a jejich zobrazení v RecyclerView.
Funkce addNote(note: String)
pak přidá novou poznámku do seznamu. Tato funkce bude volána po stisknutí tlačítka ADD z dialogu pro přidání poznámky.
Nyní už nám zbývá “jenom” přidat definici RecyclerView v NotesView
a akci pro přidání poznámky:
package cz.eman.android.sample.anko.notes import android.support.v4.content.ContextCompat import android.support.v7.widget.LinearLayoutManager import android.view.Gravity import cz.eman.android.sample.anko.R import cz.eman.android.sample.anko.notes.adapter.NotesAdapter import org.jetbrains.anko.* import org.jetbrains.anko.appcompat.v7.titleResource import org.jetbrains.anko.appcompat.v7.toolbar import org.jetbrains.anko.design.floatingActionButton import org.jetbrains.anko.recyclerview.v7.recyclerView /** * UI layout definition for a [NotesFragment] * * @author eMan s.r.o. * @see[AnkoComponent] * @see[NotesFragment] */ class NotesView(private val notes: MutableList<String>) : AnkoComponent<NotesFragment> { private lateinit var ankoContext: AnkoContext<NotesFragment> private lateinit var notesAdapter: NotesAdapter override fun createView(ui: AnkoContext<NotesFragment>) = with(ui) { ankoContext = ui notesAdapter = NotesAdapter(notes) relativeLayout { lparams(width = matchParent, height = matchParent) recyclerView { layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) adapter = notesAdapter }.lparams(width = matchParent, height = wrapContent) { alignParentTop() } floatingActionButton { imageResource = android.R.drawable.ic_input_add onClick { showAddNewNoteDialog() } }.lparams { gravity = Gravity.BOTTOM or Gravity.END margin = dip(10) alignParentBottom() alignParentEnd() alignParentRight() } } } fun updateNotes(notes: MutableList<String>) { notesAdapter.updateNotes(notes) } private fun showAddNewNoteDialog() { with(ankoContext) { alert { customView { verticalLayout { lparams(width = matchParent, height = matchParent) toolbar { titleResource = R.string.notes_dialog_new_note_title backgroundColor = ContextCompat.getColor(ctx, R.color.colorPrimary) setTitleTextColor(ContextCompat.getColor(ctx, android.R.color.white)) } val noteEditText = editText { hintResource = R.string.notes_dialog_new_note_hint }.lparams(width = matchParent, height = wrapContent) { topMargin = dip(15) bottomMargin = dip(15) leftMargin = dip(20) rightMargin = dip(20) } positiveButton(R.string.notes_dialog_new_note_button_positive) { val note = noteEditText.text.toString() if (note.isNotEmpty()) { notesAdapter.addNote(noteEditText.text.toString()) } else { toast(R.string.notes_dialog_error_empty_note) } } negativeButton(R.string.notes_dialog_new_note_button_negative) } } } }.show() } }
- Na řádku č. 25 jsme nadefinovali novou propertu pro
NotesAdapter
a jeho instance je pak vytvořena na řádku č. 29. RecyclerView
jsme nadefinovali na řádku 34.–39. s parametrem reverseLayout=true, tak abychom měli vždy nejnovější poznámku na prvním místě.- Funkcí
updateNotes(notes: MutableList<String>)
jsou předány všechny dostupné poznámky, například po otočení zařízení. - Na řádcích 83.–88. jsme přidali kontrolu, jestli zadaná poznámka není prázdný text. Pokud ano, pak se zobrazí zpráva ve formě toastu uživateli. Když je vše v pořádku, zavoláme funkci
addNote()
na adapteru, která přidá novou poznámku a notifikuje, že došlo ke změně dat.
Ještě jsme přidali nové “idčko” a text:
<item name="txtNote" type="id"/> <string name="notes_dialog_error_empty_note">Your note cannot be empty!!!</string
Shrnutí
Tak a jsme na konci. V rámci celé mini série o knihovně Anko jsme si prošli základní funkce, které nám tato knihovna přináší. Zaměřili jsme se na to jak využít Anko pro jednoduché zobrazování dialogů, toastů a práci na pozadí (v background threadu).
Zajímavou částí byla ukázka toho, jak můžeme díky této knihovně definovat UI layouty přímo v kódu s využitím AnkoComponent namísto standardní definice v XML souboru.
V této poslední části jsme si pak ukázali, jak lze využít Anko komponenta pro práci s fragmenty, jak můžeme pracovat s FAB buttonem a dialogem s custom view obsahujícím toolbar. V závěru jsme se naučili, jak používat RecyclerView
a hlavně jak nadefinovat layout pro ViewHolder
v DSL. Demo aplikace vytvořená v rámci této série je pak dostupná na našem GitHubu.
Protože vývoj knihovny neustále pokračuje, tak během této série došlo k přidání nových funkčností, například podpora Kotlin Coroutines pro práci na pozadí. Proto bude následovat ještě jeden samostatný článek, který vám představí aktuální novinky a změny v Anko knihovně od verze 0.10+.
Doufám, že se vám celá série líbila a že jste si také z toho odnesli něco nového. Budu se těšit u dalších článků nejen ze světa Kotlinu.