Aplikácia bude uchovávať zoznam linkov na wikipédiu a po kliknutí bude možné otvoriť danú stránku v prehliadači.
Ukážeme si vytvorenie novej aktivity a prepojenie na databázu. Okrem toho bude viacero nových vecí z kotlinu a funkcionálneho prístupu k programovaniu.
Na začiatok si vytvoríme nový projekt s empty views aktivitou.
RecyclerView
združuje viaceré View
do jedného ViewGroup
a poskytuje zobrazenie dynamického zoznamu. Aj keď máme veľký zoznam nejakých položiek, tak sa vytvorí len zopár widgetov (View
), ktoré sa potom pri scrollovaní recyklujú.
<androidx.constraintlayout.widget.ConstraintLayout
...
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
V hlavnej aktivite je potrebné nastaviť pre RecyclerView
dve veci:
LinearLayoutManager
ich bude dávať za sebou. Alternatíva je použiť mriežkové layouty.override fun onCreate(savedInstanceState: Bundle?) {
...
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = WeblinksAdapter()
}
Adaptér je spojený s triedou ViewHolder
. Trieda ViewHolder
určuje zobrazenie jednej položky v zozname. Túto triedu zvykneme vytvárať ako vnútornú triedu v adaptéri, ktorý na nej závisí (všimnite si <>
zátvorky v hlavičke triedy). Pre lepšiu čitateľnosť zatiaľ nechávame neimplementované metódy.
Adaptér ma tri funkcie:
onCreateViewHolder
- vytvorí layout jednej položkyonBindViewHolder
- spojí jednu položku s konkrétnymi dátami. Na vstupe funkcie je viewHolder
a pozícia v zozname.getItemCount
- množstvo dát, veľkosť zoznamuDáta si v tejto aplikácii najprv vytvoríme manuálne a neskôr budú vytiahnuté z databázy.
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class WeblinksAdapter :
RecyclerView.Adapter<WeblinksAdapter.WeblinksViewHolder>() {
class WeblinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeblinksViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: WeblinksViewHolder, position: Int) {
TODO("Not yet implemented")
}
override fun getItemCount(): Int {
TODO("Not yet implemented")
}
}
Ak by stále nebol jasný zápis tried v Kotline - tu je prepísaná trieda WeblinksViewHolder
v jave:
class WeblinksViewHolder extends RecyclerView.ViewHolder {
public WeblinksViewHolder(View itemView) {
super(itemView);
}
}
Skúsme si spustiť aplikáciu. Spadne s výnimkou (resp. chybou), ktorú si vieme pozrieť v logcat.
Kotlin umožňuje kompilovať kód, aj keď niektoré funkcie ešte nie sú doprogramované a nepotrebujeme pri tom dávať defaultný return 0
,return null
a pod. Namiesto toho vieme použiť TODO
a následne si tieto TODO vyhľadávať s pomocou nástroja v Android Studio (nájdeme to v ponuke vľavo ako je prehliadač súborov).
Dáta budú názvy stránok na wikipédii. Na vytvorenie vlastného listu (zatiaľ listOf
stringov) môžete kliknúť na Random article na wikipédii a skopírovať niekoľko názvov.
import android.widget.TextView
import android.view.LayoutInflater
class WeblinksAdapter :
RecyclerView.Adapter<WeblinksAdapter.WeblinksViewHolder>() {
val weblinks = listOf(
"André Planson",
"Elena Zamura",
"Walter K. Singleton",
"1980 Major League Baseball draft",
"Ōi Junction",
"Pixham",
"Hergenroth",
"2009 in Azerbaijan",
"Pujan Uparkoti",
"Cambria, Minnesota"
)
class WeblinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeblinksViewHolder {
val singleItemLayout = LayoutInflater.from(parent.context)
.inflate(
android.R.layout.simple_list_item_1,
parent, false
)
return WeblinksViewHolder(singleItemLayout)
}
override fun onBindViewHolder(holder: WeblinksViewHolder, position: Int) {
holder.textView.text = weblinks[position]
}
override fun getItemCount() = weblinks.size
}
Vysvetlenie jednotlivých implementácii:
WeblinksViewHolder
- stačí do inštančnej premennej (property) vytiahnúť TextView
. Budeme neskôr vytvárať aj vlastný layout pre položky v zozname. Na začiatok stačí použiť preddefinovaný layout od androidu. XML súbor simple_list_item_1
obsahuje jeden textView
s ID text1
. V tomto prípade ale berieme android.R.id.
namiesto R.id.
onCreateViewHolder
- vytvárame viewHolder
nafúknutím nejakého xml layoutu. LayoutInflater
vie vytvoriť z XML súboru potrebné veci. Parametre nie sú podstatné, iba android.R.layout.simple_list_item_1
.onBindViewHolder
- nastaví do textView
príslušný string zo zoznamu.getItemCount
- v tomto prípade triviálne.Aplikáciu spustíme a uvidíme zoznam. Môžete si vyskúšať otočiť obrazovku alebo pridať výrazne viac stringov (môžu sa opakovať) - všimnite si, že sa dá zoznam scrollovať. Ak vidíte na obrazovke naraz 10 položiek a máte ich v zozname 1000, tak reálne máte v aplikácii zhruba do 15 view
a pri posúvaní sa iba nastavujú hodnoty jednotlivých widgetov (cez viewHolder
).
Slovom data
prispôsobím triedu, ktorej hlavnou úlohou je držať dáta. Podmienkou je mať aspoň jeden parameter v konštruktore a všetky parametre musia byť aj property (povinné val
alebo var
).
V kotline získame v data class okrem properties deklarovaných už v konštruktore aj:
equals()
a hashCode()
toString()
na základe propertiescomponentN()
- na základe čoho ide urobiť destructuring declarationcopy()
Úprava weblinku je veľmi jednoduchá. Zatiaľ iba s jednou premennou.
data class Weblink(val title: String)
Takéto triedy môžeme pripojiť k iným a zahrnúť ich do súboru s inou triedou. V našom prípade k adaptéru alebo aktivite. Tentokrát ale nechajme veci osobitne pre lepšiu prehľadnosť - 1 trieda 1 súbor.
Trieda Record
v Jave existuje od verzie 14. Umožňuje jednoduchší zápis pre triedy, ktoré obaľujú dáta. Deklaruje sa napr. takto:
public record Point(int x, int y){}
Tento kód automatický zahŕňa inštančné premenné, konštruktor, hashCode, equals a toString. Viac napr. tu alebo tu. Record v Jave je cielený iba na immutable premenné. Data class v kotline umožňuje aj val
aj var
premenné. Okrem toho je tam viac rozdielov (pre nás menej podstatných).
map()
- aplikuje lambda funkciu na všetky prvky a vráti zoznam lambda výsledkov. Alternatíva mapIndexed()
má v lambde 2 parametre - index a hodnotu.zip()
- kombinuje dve kolekcie do listu párov (+ existuje unzip()
funkcia). Trieda Pair
obaľuje dvojicu hodnôt. Dá sa urobiť infix zápis: listA zip listB
associate()
- vyrobí mapu, associateWith()
a associateBy()
určujú, či pôvodné prvky z kolekcie sú kľúčom alebo hodnotou v mape.flatten()
- vytvorí list zo všetkých prvkov vnorených kolekciíjoinToString()
- na vytvorenie prispôsobeného stringu, parametre separator, prefix, suffix, limitMember funkcie - to čo už je definované, napr. isEmpty()
, get(i)
. Môžeme ich nazvať inštančné metódy.
Extension funkcie:
O high-order funkciách a lambdách si môžete prečítať viac. Budeme sa tomu venovať v ďalšej aplikácii. Namiesto venovania sa teórii, skúsme experimentálne.
Zoznam stringov linkov wikipédie premenujme na weblinkStrings
. Pomocou funkcie map
vieme vytvoriť novú kolekciu, kde každý prvok vznikne transformáciou prvku z inej kolekcie. Teda na vstupe funkcie (to medzi kučeravými zátvorkami) je string a na výstupe je nový objekt. Ak má lambda funkcia iba jeden parameter, tak ho písať nemusíme (ani tú šípku ->
) a použijeme premennú it
. Ak píšeme lambda funkcie v novom riadku, tak android studio nam poradí, aký typ je it
.
val weblinks = weblinkStrings.map {
Weblink(title = it)
}
Alternatívny zápis:
val weblinks = weblinkStrings.map {
nazov -> Weblink(title = nazov)
}
Môžete si skúsiť na to ešte aplikovať nejakú ďalšiu funkciu, napr. .sortedBy{ it.title }
.
LayoutInflater
môžeme prepísať pomocou scope funkcie let
. Všimnime si, že sa tam tiež používa it
. Scope funkcie boli v predošlej aplikácii popísané.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeblinksViewHolder {
return LayoutInflater.from(parent.context)
.inflate(
android.R.layout.simple_list_item_1,
parent, false
).let {
WeblinksViewHolder(it)
}
}
Ešte jednu drobnosť - minimalizujeme množstvo kódu vo funkcii onBindViewHolder
a presunieme zodpovednosť na ViewHolder
:
class WeblinksAdapter ...
class WeblinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(android.R.id.text1)
fun bind(weblink: Weblink) {
textView.text = weblink.title
}
}
...
override fun onBindViewHolder(holder: WeblinksViewHolder, position: Int) {
holder.bind(weblinks[position])
}
...
}
Môžeme si všimnúť, že Android Studio nás upozorní, že premenná weblinksStrings
môže byť private
. Modifikátor private
v Kotline neslúži iba na zapúzdrenie ako v Jave, ale vieme ním oddeliť pomocné premenné.
Tiež si môžeme všimnúť, že zoznam (list) vieme dopytovať s použitím zátvoriek ako v Jave []
.
Chceme reagovať na kliknutie na nejakú položku v RecyclerView
- tentokrát zobraziť toast
, ale potom aj pracovať s databázou, resp. otvoriť wikipédiu. Kliknutie vieme zaznamenať v WeblinksViewHolder
triede, čo je súčasť WeblinksAdapter
, ale na toast potrebujeme tzv. context
, čo je v našom prípade MainActivity
.
Nasledujúci koncept si v ďalšej aplikácii urobíme funkcionálne s lambda výrazom. Teraz pre lepšie pochopenie po starom pomocou rozhrania:
interface OnWeblinkClickListener {
fun onWeblinkClick(weblink: Weblink)
}
Je potrebné vyriešiť nasledovné veci:
onWeblinkClick
? TextView
má listener na kliknutie. Keď sa urobí kliknutie na widget, tak príslušný ViewHolder
môže oznámiť udalosť cez volanie funkcie onWeblinkClick
.object
).WeblinksAdapter
. Môže preto posunúť informáciu ďalej cez setter, resp. konštruktor. Adaptér potom posúva odkaz ďalej na WeblinksViewHolder
....
class WeblinksAdapter(private val listener: OnWeblinkClickListener) :
RecyclerView.Adapter<WeblinksAdapter.WeblinksViewHolder>() {
...
class WeblinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(android.R.id.text1)
fun bind(weblink: Weblink, listener: OnWeblinkClickListener) {
textView.text = weblink.title
textView.setOnClickListener {
listener.onWeblinkClick(weblink)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeblinksViewHolder {
return LayoutInflater.from(parent.context)
.inflate(
android.R.layout.simple_list_item_1,
parent, false
).let {
WeblinksViewHolder(it)
}
}
override fun onBindViewHolder(holder: WeblinksViewHolder, position: Int) {
holder.bind(weblinks[position], listener)
}
...
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = WeblinksAdapter(object : OnWeblinkClickListener {
override fun onWeblinkClick(weblink: Weblink) {
Toast.makeText(this@MainActivity, weblink.toString(), Toast.LENGTH_SHORT).show()
}
})
}
}
Pri vytvorení Toast
je this
označením inštancie vnútornej anonymnej triedy. Avšak inštancia aktivity je viditeľná a takýto this
je dostupný cez this@MainActivity
.
Rozhranie je možné implementovať aj iným spôsobom, napr. vytvorením samostatnej triedy alebo tým, že aktivita bude implementovať to rozhranie. Pri iných spôsoboch netreba zabúdať, že potrebujeme mať referenciu na aktivitu, aby sme mali context pomocou ktorého sa vytvára toast.
Intent v androide je objekt, ktorý obsahuje správu pre systém, aby vykonal nejakú akciu v inom komponente. Reálne to znamená, že ide o spustenie inej aktivity, ale aj service alebo doručenie broadcastu. V tejto aplikácii budeme spúšťať nové aktivity. Žiadna aktivita nemá právo spúšťať inú - iba požiada Android o spustenie.
Intent obsahuje dáta a akciu. Môže byť explicitný (povieme akú triedu spúšťame) alebo implicitný (popíše akciu a nechá systém nech vyberie - ten to môže nechať na používateľa). V tejto aplikácii si ukážeme obidva spôsoby. Začneme implicitným.
Najprv upravme triedu Weblink
, aby obsahovala url na wikipediu.
data class Weblink(
val title: String,
val url: String = "https://en.wikipedia.org/wiki/" + title.replace(' ', '_')
)
V hlavnej aktivite vyrobíme intent, ktorému nastavíme akciu ACTION_VIEW
a dáta budú URL. Toto je implicitný intent, kde sa systém rozhodne ako urobí zobrazenie = spustí prehliadač.
startActivity
pošle intent systému, ktorý spustí danú akciu.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = WeblinksAdapter(object : OnWeblinkClickListener {
override fun onWeblinkClick(weblink: Weblink) {
with(Intent()) {
action = Intent.ACTION_VIEW
data = Uri.parse(weblink.url)
startActivity(this)
}
}
})
}
}
Novú aktivitu DetailActivity.kt
si vytvorme cez File -> New -> Activity -> Empty Views Activity
. Dostaneme automaticky aj zápis v manifest.xml
a layout k tomu.
Layout si upravme tak, aby sme mali jediný TextView
, ktorý bude mať ID android:id="@+id/text_view_detail"
ViewBinding je spôsob ako urobiť prepojenie medzi widgetmi z XML (všetky, ktoré majú definované ID) a premennými v OOP. Potrebujeme upraviť gradle script. Po úprave klikneme hore na SYNC NOW
.
android {
...
buildFeatures {
viewBinding = true
}
}
V aktivite nastavujeme layout trochu iným spôsobom. Avšak máme cez binding
k dispozícii v premenných všetky widgety daného layoutu. Trieda ActivityDetailBinding
je generovaná automaticky na základe XML súboru.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import sk.upjs.vma.weblinks.databinding.ActivityDetailBinding
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Chceme využiť novú aktivitu, aby po kliknutí na položku v zozname MainActivity
boli zobrazené informácie o danej položke v inej aktivite DetailActivity
. Informáciu o weblinku budeme posielať v intente. Intent môže obsahovať dáta, avšak predtým potrebujeme vedieť zabaliť objekt do intentu. Aktivity si priamo nevedia posielať dáta (je možné využiť nejakú formu uloženia údajov - to bude neskôr).
Aplikujeme rozhranie Serializable
. Upozornenie: data class
nemá v tomto prípade {}
(aj keď môže mať), preto pozor, kde dávame : Serializable
.
import java.io.Serializable
data class Weblink(
val title: String,
val url: String = "https://en.wikipedia.org/wiki/" + title.replace(' ', '_')
) : Serializable
Upravíme rozhranie, aby sme rozlíšíli dve udalosti:
interface OnWeblinkClickListener {
fun onWeblinkClick(weblink: Weblink)
fun onWeblinkLongClick(weblink: Weblink)
}
V adaptéri pridáme setOnLongClickListener
, ktorý sleduje dlhý klik na TextView
. Táto metóda potrebuje vrátiť boolean
. Všimnite si v kóde, že v lambda výraze return
nepíšeme, je tam iba true
.
fun bind(weblink: Weblink, listener: OnWeblinkClickListener) {
textView.text = weblink.title
textView.setOnClickListener {
listener.onWeblinkClick(weblink)
}
textView.setOnLongClickListener {
listener.onWeblinkLongClick(weblink)
true
}
}
V hlavnej aktivite doplníme implementáciu po dlhom kliknutí. Keďže sme v anonymnej triede, tak je pre nás daná trieda this
. Táto trieda je vnorená do hlavnej aktivity a k nej sa vieme dostať cez this@MainActivity
- takto rozlíšime, ktorý this
potrebujeme.
Explicitný intent berie kontext a triedu, ktorú má spustiť (DetailActivity::class.java
). Do intentu doplníme extra informáciu o weblinku. Extra
je mapa, ktorú intent obsahuje - podobne ako SharedPreferences
a Bundle
.
Môžeme použiť ďalšiu scope funkciu apply
. Podobá sa na with
ale teraz nie je intent v oblých zátvorkach ako argument, ale na daný objekt ešte aplikujeme nasledovný kód.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = WeblinksAdapter(object : OnWeblinkClickListener {
override fun onWeblinkClick(weblink: Weblink) {
Intent(this@MainActivity, DetailActivity::class.java)
.apply {
putExtra("weblink", weblink)
startActivity(this)
}
}
override fun onWeblinkLongClick(weblink: Weblink) {
with(Intent()) {
action = Intent.ACTION_VIEW
data = Uri.parse(weblink.url)
startActivity(this)
}
}
})
}
}
V druhej aktivite vytiahneme z intentu informáciu, ktorú zobrazíme. Využijeme viewBinding
na získanie referencie na TextView
.
String (kľúč v mape Extra
) "weblink"
by bolo vhodnejšie extrahovať do nejakej premennej.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import sk.upjs.vma.weblinks.databinding.ActivityDetailBinding
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val weblink = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra("weblink", Weblink::class.java)
} else {
intent.getSerializableExtra("weblink") as Weblink
}
binding.textViewDetail.text = weblink?.url
}
}
Použitá funkcia getSerializableExtra
je deprecated, čo nám umožní pozrieť sa na niektoré zaujímavé veci.
Ak chceme použiť novú verziu tejto metódy (s dvoma parametrami), tak potrebujeme novšiu verziu Androidu.
V build.gradle (Module :app)
resp. v nastaveniach projektu nájdeme 3 čísla:
compileSdk
- táto verzia sa použije pri kompilácii kóduminSdk
- minimálna podporovaná verzia, ak má telefon staršiu verziu Androidu, nebude umožnená inštalácia aplikácietargetSdk
- určuje voči akej najnovšej verzii je aplikácia testovanáViac o verziách SDK nájdeme pri prvej aplikácii Hangman v Jave.
V kóde DetailActivity
si všimneme:
if
pri priradení do premennejSDK
Pred napojením na databázu ešte implementujeme odstraňovanie zo zoznamu. Pridávanie a úpravu nebudeme robiť.
Swipe gestá vieme implementovať pomocou ItemTouchHelper
a príslušného Callback
. Použijeme object
ako anonymnú triedu. SimpleCallback
vyžaduje v parametroch dragDirs
a swipeDirs
- umožňuje reagovať na presunutie a na potiahnutie. Chce to čísla - v jednom čísle sa kódujú smery pohybu. Ale nemusíme riešiť binárne čísla a na miesto toho dať 0
ak to nepotrebujeme a pomocou or
pridať konštanty pre smery potiahnutia, ktoré nás zaujímajú. Chceme swipe doľava a doprava.
Vo funkcii onSwipe
povieme adaptéru, že odstraňujeme položku na danom indexe.
...
import androidx.recyclerview.widget.ItemTouchHelper
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val adapter = WeblinksAdapter(object : OnWeblinkClickListener {
...
})
recyclerView.adapter = adapter
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val index = viewHolder.adapterPosition
adapter.remove(index)
}
})
itemTouchHelper.attachToRecyclerView(recyclerView)
}
}
V adaptéri implementujeme metódu remove
, kde odstránime položku z kolekcie. Okrem toho musíme notifikovať RecyclerView
, že sa zmenili dáta a je potrebné prekresliť zoznam. Môžete si vyskúšať tento riadok vynechať a sledovať správanie. Prípadne si pozrite aké iné funkcie začínajúce na notify...
existujú. Ak neviem povedať konkrétnejšiu informáciu, urobíme notifyDataSetChanged()
, ale vždy je lepšie volať viac špecifickú metódu.
Samozrejme, zoznam nie je možné meniť. Preto pri mapovaní stringov na objekty pretypujeme výsledok na MutableList
, aby to povolilo meniť kolekciu.
Odstránime tiež funkciu sortedBy
, ktorá spôsobuje problémy pri odstraňovaní.
package sk.upjs.vma.weblinks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class WeblinksAdapter(val listener: OnWeblinkClickListener) :
RecyclerView.Adapter<WeblinksAdapter.WeblinksViewHolder>() {
...
val weblinks = weblinkStrings.map {
Weblink(title = it)
} as MutableList
...
fun remove(index: Int) {
weblinks.removeAt(index)
notifyItemRemoved(index)
}
}
V ďalšom samostatnom codelab-e bude aplikácia Weblink
doplnená o databázu.