Java a Kotlin dokážu fungovať pri sebe. Teda z jedného jazyka je možné volať metódy vytvorené v tom druhom jazyku. Viac technických detailov sa dá nájsť v dokumentácii.
Môžete si pozrieť stručné porovnanie, ktoré veci z Javy chýbajú v Kotline. Nie je ich veľa - napr. kontrolované výnimky, primitívne typy a pod. Opačne je toho viac, čo Java nemá a Kotlin áno.
Vytvorme si teda nový projekt v Android Studio - rovnakým spôsobom ako predošlý (s jednou Empty Views Activity
). Jediný rozdiel je, že si zvolíme jazyk Kotlin.
Manifest
a resources
nezávisia od toho, či je aplikácia v Jave alebo Kotline. Manifest
sme v predošlej aplikácii nemodifikovali, čiže je potrebné iba skopírovať všetky resources
:
/res/drawable-v24/gallows1.png (a všetky ostatné)
/res/layout/activity_main.xml
/res/values/strings.xml
Ak by ste si chceli vyskúšať Kotlin mimo Androidu, ponúkajú interaktívny nástroj na zoznámenie sa s jazykom: Kotlin Koans.
V tejto aplikácii nepokrývame to najdôležitejšie z Kotlinu, ale to, čo si vyžiada aplikácia. Nie všetky veci sú používané rovnako často a majú rovnakú dôležitosť, ale to si všimnete až po niekoľkých naprogramovaných aplikáciach.
Mimochodom v Kotline nepíšeme bodkočiarky.
Preskúmajme kód, ktorý dostaneme v MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
Aj v Kotline vznikajú triedy rozširovaním iných.
Ak sa nič nenapíše, tak:
Object
.Any
- je to podobná trieda ako Object
, ale má iba metódy equals
, hashCode
, toString
.Pre ďalšie pochopenie by sme potrebovali vedieť ako funguje kompilácia Kotlin kódu, null checks a iné veci. Teraz to nie je podstatné.
V kotline rozširovanie tried a implementovanie rozhraní zapisujeme cez :
namiesto extends
a implements
. Ak názov za dvojbodkou má zátvorky ide o rozšírenie triedy MainActivity : AppCompatActivity()
. Neskôr uvidíme HangmanGame: Game
, čo znamená implementovanie rozhrania.
Potrebný keyword fun
. V Kotline sa zvykne hovoriť nie o metódach ale o funkciách. Funkcia je všeobecnejší pojem ako metóda (viac v tomto blogu).
override
je keyword, nie iba anotácia. Teda môže byť funkcia override fun toString(): String {}
.
Návratový typ - z predošlého príkladu je zrejmé, že sa píše za názvom metódy (nie pred ako v jave). Nájdeme ho za znakom :
. Ak sa nenapíše nič fun funkcia()
(obdoba void
v Jave), tak je tam defaultne typ Unit
. V ňom je ukrytý iba objekt Unit
- teda v zásade to na nič nie je a nič to nerobí, ale aspoň sa to dá použiť ako argument pri generikách. Možno si spomeniete na typ Void
v Jave.
Premenné - zapisujú sa vo formáte názov: Typ
. Okrem toho platia nasledovné veci (časť vecí možno poznáte z pythonu):
Int, Char, Long, Boolean, Float, Double, Byte, Short
fun mocnina(zaklad: Int, exponent: Int = 2): Long
. Túto funkciu môžeme volať mocnina(5, 3)
, mocnina(4, 8)
ale namiesto mocnina(7, 2)
môžeme zapísať mocnina(7)
.mocnina(zaklad = 8, exponent=3)
.Stále pozeráme kód, ktorý dostaneme v MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
V Kotline
sa stretneme s výnimkou NullPointerException
výrazne menej často. Navyše je to viac pod kontrolou bez nutnosti všade dávať podmienku na kontrolu, či nejaká premenná je nenullová (úprimne - koľkokrát na to v Jave zabúdame?).
Znak ?
pri premennej označuje, či môže táto premenná nadobudnúť hodnotu null
. Teda v kóde aktivity Bundle?
označuje, že táto hodnota môže byť null
.
Príklad (viac detailov neskôr):
slovo: String
- nemôže byť null
. Kompilačná chyba ak by sme sa o to pokúsili.slovo: String?
- môže byť null
. Avšak volanie surprise.length()
nie je povolené (lebo je šanca, že by to skončilo výnimkou).?.
zaručí bezpečné volanie. slovo?.length()
vráti dĺžku slova, ale ak je slovo
rovné null
, tak vráti null
namiesto vyhodenia výnimky.Prepis rozhrania do Kotlinu nie je extra komplikovaný. Stačí poznať zápis implementovania rozhrania (neriešime či extends
alebo implements
) a syntax pre funkcie:
import java.io.Serializable
interface Game : Serializable {
fun isWon() : Boolean
fun guessedCharacters() : CharSequence
fun getChallengeWord() : String
fun attemptsLeft() : Int
fun guess(characters: Char): Boolean
}
Na doplnenie ďalších vecí - prepis statických premenných potrebujeme ďalšie informácie z Kotlinu:
O typoch premenných sme si už povedali pri funkciách. Ak chceme vytvoriť premennú, tak sa rozlúčime s Java tradíciou Random random = new Random()
.
Pri definovaní premennej sa rozhodujeme medzi val
a var
podľa toho, či hodnotu premennej povolíme v budúcnosti zmeniť:
val
- read-onlyvar
- mutableAk je pri vytváraní premennej aj priradenie, typ nemusíme nutne písať. Príklady:
val retazec = "Hello world"
val counter : String
(keď zapisujeme inštančnú premennú)var counter: Int? = null
V kotline je oddelená trieda a súbor. To znamená, že v súbore .kt
môžeme mať viacero tried a navyše aj kód, ktorý je mimo triedy (top-level funkcie, premenné), čo napr. java neumožňuje.
Premenné (property - onedlho sa dozvieme, čo to je), ktoré sú konštantami môžeme obohatiť o slovo const
ak obsahujú String
alebo niektorú z tried zodpovedajúcu primitívnym typom.
Okrem iného je tam podmienka, že const
(compile-time constant) musí byť top-level premenná alebo súčasť companion object alebo object declaration.
Companion object
Ak v kotline použijeme namiesto class
slovo object
tak v podstate vytvárame singleton. Použitie uvidíme, keď budeme implementovať niečo ako anonymné triedy v jave.
Ak máme takýto objekt v rámci triedy, tak to nazveme companion object
. Dá sa s tým robiť viac vecí. V tomto prípade môžeme urobiť companion object
v rámci rozhrania Game.kt
. Konštanty premenné môžeme vložiť do toho companion object
. Rozdiel medzi top-level a týmto prístupom je nasledovný:
DEFAULT_ATTEMPTS_LEFT
.Game.DEFAULT_ATTEMPTS_LEFT
Výsledný kód pre rozhranie vyzerá nasledovne:
import java.io.Serializable
const val DEFAULT_ATTEMPTS_LEFT = 6
const val UNGUESSED_CHAR = '_'
interface Game : Serializable {
fun isWon() : Boolean
fun guessedCharacters() : CharSequence
fun getChallengeWord() : String
fun attemptsLeft() : Int
fun guess(characters: Char): Boolean
}
Najprv si vysvetlíme všetky novinky z kotlinu. V ďalšom kroku nájdete hotový kód vyskladaný do jednej triedy.
S týmto sa stretneme ešte mnohokrát. V zásade Kotlin používa triedu Array
alebo rozhrania List
a MutableList
. Hlavný rozdiel, ktorý si všimneme je pri listoch, že sú nemenné. Ak chceme meniť obsah, použijeme MutableList
. Navyše vieme všetky tri typy dopytovať ako polia pomocou indexov napr.a[i]
.
Vytvoriť jednotlivé kolekcie môžeme aj nasledovne:
val a = arrayOf(1, 2, 3)
resp. val a = intArrayOf(1, 2, 3)
val a = listOf(1, 2, 3)
val a = mutableListOf(1, 2, 3)
arrayOfNulls
Pri listoch pozor na rozdiel medzi MutableList
vs. List
a na druhej strane val
vs. var
. To druhé hovorí o referenciách, nie obsahu.
Navyše máme mnoho užitočných funkcií. Pri práci s kolekciami narazíme často na lambdu a funkcionálny prístup. Viac v ďalších aplikáciach.
Trieda HangmanGame
má jeden konštruktor, ktorý má parameter typu pole stringov Array
.
V kotline je jeden primárny konštruktor, ktorý je už v hlavičke triedy. Do oblých zátvoriek môžeme vložiť premenné. Ak tam pridáme aj kľúčové slovo val
alebovar
, tak dostaneme aj inštančnú premennú. Rovnako ak rozširujeme triedu, ktorá nemá bezparametrický konštruktor, tak môžeme premennú priamo vložiť do zátvoriek, napr. class Rodic(cislo: Int)
a potom class Trieda(slovo: String, cislo: Int) : Rodic(cislo)
Kód pre primárny konštruktor sa dáva do bloku init{}
.
Ďalšie konštruktory doplníme pomocou slova constructor
.
Kľúčové slovo in
:
for (item in collection)
for (i in 1..3)
for (i in array.indices)
for (i in 6 downto 0 step 2)
Viac v dokumentácii - ranges and progressions.
Zopakujme si - funkcie musia mať návratový typ. Ak tam nie je napísaný, doplní sa Unit
.
Občas ale funkcie sú dosť jednoduché a navyše je jasné, čo to vráti. Ak funkcia vráti iba výraz, môže byť prepísaná z takejto verzie
fun duplikuj(x: Int): Int {
return x * 2
}
na jednu z týchto dvoch (nazýva sa to single expression funkcia)
fun duplikuj(x: Int) = x * 2
fun duplikuj(x: Int): Int = x * 2
equals
==
==
===
import android.os.SystemClock
import kotlin.random.Random
class HangmanGame(words: Array<String>) : Game {
val word: String
// val - do premennej nebudeme priradzovat iny SB, iba volat metody
val wordInProgress: StringBuilder
var attemptsLeft : Int
val startTime: Long
init {
val random = Random.Default
val index = random.nextInt(words.size)
word = words[index]
wordInProgress = StringBuilder()
for (i in words.indices) {
wordInProgress.append(UNGUESSED_CHAR)
}
attemptsLeft = DEFAULT_ATTEMPTS_LEFT
startTime = SystemClock.elapsedRealtime()
}
fun getTime() = SystemClock.elapsedRealtime() - startTime
override fun isWon() = word == wordInProgress.toString()
override fun guessedCharacters() = wordInProgress
override fun getChallengeWord() = word
override fun attemptsLeft() = attemptsLeft
override fun guess(character: Char): Boolean {
var success = false
for (i in word.indices) {
if (word[i] == character){
wordInProgress[i] = character
success = true
}
}
if (!success) {
attemptsLeft--
}
return success
}
}
V jazyku Java na uchovávanie stavu objektu využívame inštančné premenné (v angličtine: fields). Pri rozumnom návrhu triedy je potrebné vytvoriť:
private
Pri metódach v Jave modifikátor public
označuje metódy, ktoré sú sprístupnené navonok a metódy s modifikátorom private
sú len pomocné, čiže pre internú potrebu danej triedy. Premenné sú ale všetky označované private
.
V kotline je field iba súčasťou premennej nazývanej property.
Pri vytvorení premennej:
val
dostávame field + gettervar
dostávame field + getter + setterPoznámky k používaniu:
Dôsledkom je, že modifikátor private
označuje pomocné premenné pre internú potrebu triedy. Oddeľuje sa tiež stav objektu a funkcionalita, keďže nie je potrebné k hodnotám premenných pristupovať cez funkcie.
Ak máme v konštruktore premennú napr.
class Turtle(x: Int, y: Int){
init {
volanieFunkcie(x, y)
}
}
zodpovedá to Java kódu
public class Turtle() {
public Turtle(int x, int y) {
volanieFunkcie(x, y)
}
}
Ak v konštruktore pridáme val
resp. var
, tak automaticky dostaneme property:
class Turtle(val x: Int, val y: Int) { }
čo v jave znamená oveľa viac kódu:
public class Turtle() {
private final int x;
private final int y;
public Turtle(int x, int y) {
this.x = x;
this.y = y;
}
}
Môžeme si vyskúšať pridať java kód pre interface Game
z predošlej aplikácie a po kliknutí vybrať možnosť convert Java file to Kotlin file.
Všimnime si, že 4 z 5 funkcií boli vytvorené ako property (val
namiesto fun
). Property uchováva stav objektu a je možné ich pridať aj do rozhrania, či už ako abstraktné alebo s implementáciou (to je náš prípad).
import java.io.Serializable
interface Game : Serializable {
companion object {
const val DEFAULT_ATTEMPTS_LEFT = 6
const val UNGUESSED_CHAR = '_'
}
val isWon : Boolean
val guessedCharacters : CharSequence
val challengeWord : String
val attemptsLeft : Int
fun guess(character: Char): Boolean
}
Následne upravíme aj implementáciu rozhrania. Zopár poznámok:
isWon
a time
v skutočnosti nepotrebujú uchovávať hodnotu, ale ju priamo sprístupňujú. V jave to je riešené cez metódu. V zásade ale ide o stav hry, čiže je to vhodnejšie mať ako premennú nie funkciu. Z inej triedy s tým pracujeme ako s premennou.guessedCharacters
je vyžadovaná rozhraním. V skutočnosti iba sprístupňuje obsah premennej wordInProgress
, ktorá je iného typu StringBuilder
implementuje CharSequence
attemptsLeft
nemá určený typ, lebo je to jasné pri inicializácii na číselnú hodnotu. Okrem toho má privátny setter. Obsah tejto hodnoty sa má meniť, preto musí byť var
. Zmena môže byť robená iba interne z triedy HangmanGame
.companion object
. Viac o rozdieloch medzi týmto spôsobom a top-level deklaráciou bolo vysvetlené predtým (viď prepis rozhrania Game
do kotlinu)import android.os.SystemClock
import kotlin.random.Random
class HangmanGame(words: Array<String>) : Game {
// val - do premennej nebudeme priradzovat iny SB, iba volat metody
private val wordInProgress: StringBuilder
override val challengeWord: String
override val guessedCharacters: CharSequence
get() = wordInProgress
override val isWon: Boolean
get() = challengeWord == wordInProgress.toString()
override var attemptsLeft = Game.DEFAULT_ATTEMPTS_LEFT
private set
private val startTime: Long
val time : Long
get() = SystemClock.elapsedRealtime() - startTime
//fun getTime() = SystemClock.elapsedRealtime() - startTime
init {
val random = Random.Default
val index = random.nextInt(words.size)
challengeWord = words[index]
wordInProgress = StringBuilder()
for (i in challengeWord.indices) {
wordInProgress.append(Game.UNGUESSED_CHAR)
}
startTime = SystemClock.elapsedRealtime()
}
override fun guess(character: Char): Boolean {
var success = false
for (i in challengeWord.indices) {
if (challengeWord[i] == character){
wordInProgress[i] = character
success = true
}
}
if (!success) {
attemptsLeft--
}
return success
}
}
Môžeme začať prepisovať MainActivity.java
na verziu v Kotline.
Widgety dostaneme do premenných pomocou funkcie findViewById
. Táto funkcia je volaná v metóde onCreate
, keď sa vytvára aktivita a vykresľuje layout. Konštruktory sa v aktivitách nepoužívajú.
Dilema - ak by bol TextView?
, tak umožníme hodnotu null
. Avšak vieme, že tam hodnotu null
mať nechceme a ani nebudeme (máme pod kontrolou id
widgetu). Ak to ale dáme TextView
, tak v čase vytvorenia objektu, ešte pred volaním metódy onCreate
nemáme čo do premennej vložiť.
lateinit
je spôsob ako môžeme odložiť inicializovanie premennej na neskôr. Vyžaduje sa var
, lebo hodnota sa neskôr mení. Nefunguje to pre primitívne typy. Pomocou reflection vieme overiť, či je premenná inicializovaná ::nazovPremennej.isInitialized
.
Pomocou is
vieme overiť typ (obj is String
). V jave to bolo instanceof
. Nazýva sa to type cast a je možné robiť aj negáciu !is
.
Pre val
funguje smartcast: if (x is String && x.length > 0)
- automaticky sa v druhej časti podmienky pracuje s x
ako so Stringom. Android Studio smartcast zvýrazňuje zelenou farbou.
Pretypovanie pomocou as
:
x as String
x as? String
. Nevýhoda je, že typ premennej musí byť nullable (val x: String? = y as? String
)V kóde nižšie vo funkcii onImageClick
si všimnite nasledovné veci:
lateinit
v properties pre Game
a widgetycharRange
a negovaná podmienka if (letter !in 'a'..'z')
. Doteraz sme videli in
v cykle, teraz to je v podmienke. Negáciu dosiahneme !in
.game.isWon
, game.attemptsLeft
. Ale to isté máme aj v inputEditText.text
, kde nepotrebujeme volať getter funkciu.new
. Je to vidieť pri LightingColorFilter
. Teda namiesto Java verzie Turtle turtle = new Turtle()
v kotline zapisujeme var turtle = Turtle()
.package sk.itsovy.android.hangman_kotlin
import android.graphics.Color
import android.graphics.LightingColorFilter
import android.os.Bundle
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var game: Game
private lateinit var textView: TextView
private lateinit var imageGallows: ImageView
private lateinit var inputEditText: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun startNewGame() {
}
fun onImageClick() {
if (game.isWon || game.attemptsLeft == 0) {
startNewGame()
return
}
val text : CharSequence = inputEditText.text
// text.length == 0
if (text.isEmpty()) {
Toast.makeText(this, R.string.insert_a_letter, Toast.LENGTH_SHORT).show();
return
}
val letter = text[0].lowercaseChar()
inputEditText.setText("")
if (letter !in 'a'..'z') {
//if (letter < 'a' || letter > 'z') {
Toast.makeText(this, "PISMENO", Toast.LENGTH_SHORT).show()
return
}
val success = game.guess(letter)
if (success) {
updateText()
if (game.isWon) {
imageGallows.colorFilter = LightingColorFilter(Color.GREEN, Color.BLACK)
if (game is HangmanGame) {
val hangmanGame = game as HangmanGame
val time: Long = hangmanGame.time
updateBestTime(time)
}
}
} else {
updateImage()
if (game.attemptsLeft == 0) {
imageGallows.colorFilter = LightingColorFilter(Color.RED, Color.BLACK)
}
}
}
private fun updateBestTime(time: Long) {
}
private fun updateImage() {
TODO("Not yet implemented")
}
private fun updateText() {
TODO("Not yet implemented")
}
}
Doplníme do hlavnej aktivity dve metódy:
import android.app.AlertDialog
class MainActivity : AppCompatActivity() {
...
private fun updateBestTime(time: Long) {
val pref = getPreferences(MODE_PRIVATE)
val bestTime = pref.getLong("time", Long.MAX_VALUE)
if (time < bestTime) {
val editor = pref.edit()
editor.putLong("time", time)
editor.apply()
}
announceBestTime(time)
}
private fun announceBestTime(time: Long) {
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.best_time)
builder.setMessage(getString(R.string.your_time, time))
builder.create().show()
}
}
Viac detailov v dokumentácii. Je to elegantný spôsob na niektoré programátorské úlohy. Pri prvom stretnutí s týmto konceptom ale môže byť náročné pochopiť ako fungujú a ktorú funkciu kedy použiť. Podstatné je v prom rade porozumieť napísanému kódu.
Vyhneme sa vyrábaniu nových premenných. Context object je this
alebo it
. Lepšie pochopenie budeme mať, keď sa strenteme s lambda výrazmi. Funkcie vytvoria nový dočasný scope, kde je možné pristúpiť k premennej bez jej názvu.
Existuje 5 funkcií : let
, run
, with
, apply
, also
.
with
funkcia funguje štýlom: with this object, do the following. V rámci zátvoriek {}
je this
premenná, na ktorej sme scope funkciu zavolali. Lepšie vidieť na príklade:
class MainActivity : AppCompatActivity() {
...
private fun updateBestTime(time: Long) {
val pref = getPreferences(MODE_PRIVATE)
val bestTime = pref.getLong("time", Long.MAX_VALUE)
if (time < bestTime) {
with (pref.edit()) {
putLong("time", time)
apply()
}
}
announceBestTime(time)
}
private fun announceBestTime(time: Long) {
with (AlertDialog.Builder(this)) {
setTitle(R.string.best_time)
setMessage(getString(R.string.your_time, time))
create().show()
}
}
}
this
je objekt triedy MainActivity
- to sa využíva napr. ako parameter pri konštruktore AlertDialog.Builder
. V rámci scope funkcie with
sa stáva this
iná vec - v prvom prípade objekt triedy SharedPreferences.Editor!
(výkričník neriešime, je k tomu komentár neskôr pri null safety) a v druhom AlertDialog.Builder
.
AndroidStudio nám napíše hint, čo je this
ak to nepíšeme v jednom riadku ale rozdelíme to (tak ako v kóde vyššie).
Doplníme hlavnú aktivitu:
import android.graphics.Color
import android.graphics.LightingColorFilter
import android.os.Bundle
import android.os.PersistableBundle
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
const val BUNDLE_KEY = "game"
class MainActivity : AppCompatActivity() {
private val gallowsIds = intArrayOf(
R.drawable.gallows0,
R.drawable.gallows1,
R.drawable.gallows2,
R.drawable.gallows3,
R.drawable.gallows4,
R.drawable.gallows5,
R.drawable.gallows6
)
private lateinit var game: Game
private lateinit var textView: TextView
private lateinit var imageGallows: ImageView
private lateinit var inputEditText: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textViewGuessedWord)
inputEditText = findViewById(R.id.editTextLetter)
imageGallows = findViewById(R.id.imageViewGallows)
imageGallows.setOnClickListener {
onImageClick()
}
if (savedInstanceState == null) {
startNewGame()
} else {
game = savedInstanceState.getSerializable(BUNDLE_KEY) as Game
updateImage()
updateText()
}
}
private fun startNewGame() {
val words = resources.getStringArray(R.array.dictionary)
game = HangmanGame(words)
updateText()
updateImage()
imageGallows.colorFilter = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable(BUNDLE_KEY, game)
}
fun onImageClick() {
if (game.isWon || game.attemptsLeft == 0) {
startNewGame()
return
}
val text : CharSequence = inputEditText.text
// text.length == 0
if (text.isEmpty()) {
Toast.makeText(this, R.string.insert_a_letter, Toast.LENGTH_SHORT).show();
return
}
val letter = text[0].lowercaseChar()
inputEditText.setText("")
if (letter !in 'a'..'z') {
//if (letter < 'a' || letter > 'z') {
Toast.makeText(this, "PISMENO", Toast.LENGTH_SHORT).show()
return
}
val success = game.guess(letter)
if (success) {
updateText()
if (game.isWon) {
imageGallows.colorFilter = LightingColorFilter(Color.GREEN, Color.BLACK)
if (game is HangmanGame) {
val hangmanGame = game as HangmanGame
val time: Long = hangmanGame.time
updateBestTime(time)
}
}
} else {
updateImage()
if (game.attemptsLeft == 0) {
imageGallows.colorFilter = LightingColorFilter(Color.RED, Color.BLACK)
}
}
}
private fun updateBestTime(time: Long) {
val pref = getPreferences(MODE_PRIVATE)
val bestTime = pref.getLong("time", Long.MAX_VALUE)
if (time < bestTime) {
with (pref.edit()) {
putLong("time", time)
apply()
}
}
announceBestTime(time)
}
private fun announceBestTime(time: Long) {
with (AlertDialog.Builder(this)) {
setTitle(R.string.best_time)
setMessage(getString(R.string.your_time, time))
create().show()
}
}
private fun updateImage() {
val index = Game.DEFAULT_ATTEMPTS_LEFT - game.attemptsLeft
imageGallows.setImageResource(gallowsIds[index])
}
private fun updateText() {
textView.text = game.guessedCharacters
}
}
Zhrnutie o null safety - prakticky si to vyskúšame pri iných aplikáciach. Voliteľne si môžete vyskúšať zmeniť Game
na Game?
v hlavnej aktivite a popasovať sa so zmenami.
Prezentácia, ďalšie aplikácie sú na stránke https://ics.science.upjs.sk/vma/.
Nasleduje aplikácia viac zameraná na Android ako Kotlin (aj keď sa dozvieme viaceré nové veci z kotlinu).