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

Vaclav Souhrada, knihovna Anko, Anko library, Kotlin, Anko

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.

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:

 

fragments Anko knihovna

 

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()
            }
        }
    }

}

 

empty view Anko library fragments

 

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()
}

 

add a note fragments Anko library

 

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()

 

add a note fragments Anko library

 

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()

 

dialogue fragments Anko library

 

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.

 

notes detail Anko library

 

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.

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

RSS