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

Rozširovanie tried

Aj v Kotline vznikajú triedy rozširovaním iných.

Ak sa nič nenapíše, tak:

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.

Definovanie metód/funkcií

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

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

Prepis rozhrania do Kotlinu nie je extra komplikovaný. Stačí poznať zápis implementovania rozhrania (neriešime či extends alebo implements) a syntax pre funkcie:

Game.kt

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:

Premenné

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ť:

Ak je pri vytváraní premennej aj priradenie, typ nemusíme nutne písať. Príklady:

Čo so statickými premennými?

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ý:

Výsledný kód pre rozhranie vyzerá nasledovne:

Game.kt

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.

Polia a listy

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:

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.

Konštruktory

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.

Cykly

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.

Single expression funkcia

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

Porovnávanie

HangmanGame.kt

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ť:

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:

Pozná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.

Property a konštruktor

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

Game.kt

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:

HangmanGame.kt

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.

Lateinit

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.

Pretypovanie

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:

MainActivity v Kotline

V kóde nižšie vo funkcii onImageClick si všimnite nasledovné veci:

MainActivity.kt

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

Scope funkcie

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.

elvis

Kód na gitlabe

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