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 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);
}
}
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.
class WeblinksAdapter :
RecyclerView.Adapter<WeblinksAdapter.WeblinksViewHolder>() {
val weblinks = listOf(
"Euler's rotation theorem",
"Walter K. Singleton",
"Hokutoumi Nobuyoshi",
"Powmill",
"2007 Maldivian constitutional referendum",
"Greg Tagert",
)
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ť 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.
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 listBassociate() - 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])
}
...
}
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(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()
}
})
}
}
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 doreč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).
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 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, ale potrebujeme vedieť zabaliť objekt do intentu.
Aplikujeme Serializable. Použitá funkcia je deprecated. V nových verziách sa tam uvádza aj typ avšak na to by sme museli meniť verzie Androidu, čo teraz ignorujeme. 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) {
...
}
override fun onWeblinkLongClick(weblink: Weblink) {
Intent(this@MainActivity, DetailActivity::class.java)
.apply {
putExtra("weblink", weblink)
startActivity(this)
}
}
})
}
}
V druhej aktivite vytiahneme z intentu informáciu, ktorú zobrazíme. Využijeme viewBinding na získanie referencie na TextView.
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 = intent.getSerializableExtra("weblink") as Weblink
binding.textViewDetail.text = weblink.url
}
}
Do pôvodnej aktivity sa vieme dostať cez Back button na telefóne. Nasledovným kódom pridáme šípku späť do ActionBar - čo je ten horný panel, kde je názov aplikácie. V ďalšej aplikácii v tomto priestore urobíme viac vecí.
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
...
}
}
V manifeste musíme označiť rodičovskú aktivitu (android:parentActivityName), aby sme dodali význam šípke.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
...>
<activity
android:name=".DetailActivity"
android:parentActivityName=".MainActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Môžete si vyskúšať pridať Log do onCreate v hlavnej aktivite. Je rozdiel ak použijeme back button na telefóne a na action bare?
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.
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)
}
}
Android používa SQLite databázu a prístup k nej zabezpečuje cez Room knižnicu. V ďalšej aplikácii bude použité dependency injection Hilt na redukciu boilerplate kódu.
Stručný popis k jednotlivým commitom:
build.gradle sa doplnia dependencies na príslušné knižniceWeblink je entitou. V DAO je urobených viac metód na ukážku.suspend a Flow. Vytvorenie jednotlivých prepojení cez Application.ViewModel, ktorý má dlhšiu životnosť ako aktivita. Spúštanie korutín.UUID na String, aby bol ako reťazec uložený aj v DB.