Kotlin: druhé dojmy aneb z Javy do Kotlinu, ale zpátky už ne

O prvních dojmech z Kotlinu jsem už blogoval. Poté, co jsem aplikaci Pobyty na Moravě přepsal do Kotlinu kompletně, mohu se podělit i o druhé dojmy. Resumé však bude stejné: studovat a používat Kotlin se rozhodně vyplatí.

Při přepisu aplikace Pobyty na Moravě jsem si nejprve pomohl automatickou konverzí z Javy do Kotlinu nabízenou Android Studiem. Poté jsem zdroják kompletně zrevidoval a většinu částí přepsal ručně. Výsledný kód jsem také zkusil dekompilovat do Javy, abych mohl prozkoumat jeho efektivitu.

V seznamu vlastností, které popisuji, nehledejte systém. Seřadil jsem je spíše podle toho, jak mě oslovily a jak moc je považuji za užitečné.

Null safety - bingo!

Kontrola na null už na úrovni kompilátoru je jednoznačným přínosem. Odpadá nutnost defenzivních kontrol na null prováděných mnohdy za cenu kaskády vnořených if. Také je téměř eliminováno riziko pádu aplikace z důvodů NullPointerException. A samozřejmě, výsledný zdroják je kratší, přičemž efektivita neutrpí.

Vezměme si ukázku několikanásobného použití operátoru pro safe reference zmíněnou v předchozím příspěvku:

val img = post.optJSONObject("thumbnail_images")?.optJSONObject("full")?.optString("url") ?: ""
...
retval.previewImage = img

Jak bude vypadat po dekompilaci do Javy?

Object object = post.optJSONObject("thumbnail_images"); 
if (object == null || (object = object.optJSONObject("full")) == null || (object = object.optString("url")) == null) { 
    object = ""; 
} 
Object img = object; 
... 
retval.setPreviewImage((String)img);

Dalo by se to napsat i trochu efektivněji, ale v zásadě můžeme být spokojeni.

Automatický getter a setter - netřeba tolik datlit

Výhody automatického getteru a setteru jsou patrné na první pohled: odpadá nutnost jejich ručního programování a v případě změny odpadá nutnost měnit zdroják na více místech.

Jak to funguje ve skutečnosti? Vezměme si následující příklad (pro účely blogu zkráceno):

class Category : Serializable {

    var id: Int = 0
    var name: String = ""
}

Třída Category na první pohled vypadá, jako by obsahovala dvě proměnné s přístupem public. Co ale ukáže dekompilátor (opět zkráceno)?

import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

public final class Category implements Serializable {

    private int id;
    @NotNull
    private String name = "";

    public final int getId() {
        return this.id;
    }
  
    public final void setId(int i) {
        this.id = i;
    }
  
    @NotNull
    public final String getName() {
        return this.name;
    }
  
    public final void setName(@NotNull String s) {
        Intrinsics.checkParameterIsNotNull(s, "s");
        this.name = s;
    }  
}

Když pomineme přidané speciální anotace, zjistíme, že se ve skutečnosti jedná o proměnné s přístupem private, které mají public getter a setter. Jediné, co je v kódu navíc, je volání metody Intrinsics.checkParameterIsNotNull(...) u objektových typů. Ta ale pouze provede kontrolu parametru na null a v případě null vyhodí výjimku.

Práce výrazně usnadněná a efektivita v zásadě neutrpí.

Extension funkce - zajímavá myšlenka

Možnost přidat k existující třídě další metodu bez nutnosti vytvářet subclass je také užitečná věc. Ve skutečnosti se jedná o statické metody, kde první z parametrů má typ rozšiřované třídy. Takto nadefinované metody lze volat i na objekt s hodnotou null.

View binding - bingo na druhou!

Srdce Androiďáka plesá radostí. Již není nutno do zblbnutí copypastovat volání metody View.findViewById(...) a následné kontroly správného typu. Kotlin Android Extensions se o to postará sám.

Nejprve přidáme následující řádek do build scriptu:

apply plugin: 'kotlin-android-extensions'

Poté přidáme do zdrojáku jeden z těchto importů, podle toho, jestli k daný layout odpovídá Activity nebo View.

import kotlinx.android.synthetic.main.<layout>.*
import kotlinx.android.synthetic.main.<layout>.view.*

K jednotlivým vizuálním prvkům pak přistupujete jako k proměnným, jejichž názvy odpovídají jednotlivým ID z layoutu. Co je ale nejlepší, tyto proměnné kontroluje IDE. Pokud v daném layoutu ID chybí, kompilátor hlásí error. Pokud ID obsahují pouze některé varianty layoutu (např. pouze vertikální, ale ne horizontální), hlásí kompilátor warning, že dané ID v některých layoutech chybí, tudíž může být null. Trochu nekonzistence s null-safety, ale v pohodě.

Detaily se mi nepodařilo vyzkoumat, mám ale důvod se domnívat, že se zde jedná právě o extension funkce. To je rozdíl oproti knihovně Butterknife, která view binding implementuje anotacemi. Srovnání výhod a nevýhod obou přístupů přenechám někomu jinému.

object - nadeklaruj a hned použij

Často se stává, že potřebujete vytvořit třídu a použít ji jenom jednou. Nemusí se a priori jednat o singleton, stačí jednorázový listener, delegát nebo callback, jehož typ je dán abstraktní třídou nebo rozhraním. K tomu slouží klíčové slovo object, které nadefinuje potřebnou třídu, vytvoří její instanci a přiřadí ji do proměnné. Jednoduché a užitečné.

Singleton s lazy initem - jeden řádek kódu

A propó, singleton. Klíčové slovo object nám nadefinuje singleton samo o sobě. Kotlin má však i řešení situací, kdy je inicializace příliš náročná a chceme ji provést až při prvním přístupu k dané instanci:

val instance by lazy { ImageCache() }

Opět jednoduché, dokonce by to mělo být i thread-safe.

val nebo var - přemýšlej, co děláš

Proměnnou můžeme deklarovat jako val nebo var. Alespoň můžeme už od začátku přemýšlet, jestli potřebujeme její hodnotu měnit nebo ne. Dobrá věc.

Dědění naruby - dobré, ale je potřeba mít na paměti

Zatímco v Javě je dědění implicitně povoleno a v případě potřeby lze zakázat klíčovým slovem final, v Kotlinu je to naopak: dědění je implicitně zakázáno a v případě potřeby lze povolit klíčovým slovem open. To znamená, že když v Kotlinu deklarujeme třídu nebo metodu bez modifikátoru open, je to, jako bychom v Javě tutéž třídu nebo metodu deklarovali s modifikátorem final (potvrzeno dekompilací).

Tento přístup naplňuje zásadu, kterou přinesl Joshua Bloch ve své knize Effective Java: dědění navrhněte a zdokumentujte, nebo je zakažte. Tím, že třídy a metody v Kotlinu jsou implicitně final, získáme větší efektivitu výsledného kódu: JVM zpracovává final kód efektivněji.

Dobrá věc, ale je potřeba ji mít při programování na paměti, aby nás nepříjemně nepřekvapil kompilátor.

Automatický typ proměnné - dvousečná zbraň

Z automatického typu proměnné mám poněkud smíšené pocity. Na jedné straně, když nemusím explicitně uvádět typ proměnné, psaní zdrojáku je výrazně snažší. Na druhou stranu mám dojem, že bez explicitně uvedených typů proměnných je zdroják hůře čitelný.

Vezměme jako příklad metodu pro získání části HTML kódu, která odpovídá stručnému výpisu příspěvku. V ukázce je použita knihovna Jsoup pro zpracování HTML.

fun parseSummary(src: String): String {
    val doc = Jsoup.parseBodyFragment(src)
    val body = doc.body()
    val p = body.select("p").first()
    var retval = p?.text() ?: Jsoup.clean(src, Whitelist.none())
    if (retval.isNotEmpty()) {
        retval = retval.trim()
    }
    return retval
}

Asi odhadnete, že názvy proměnných v ukázce budou odpovídat HTML tagům a že proměnná retval představuje návratovou hodnotu. Ale schválně, jakého jsou proměnné typu? A mohou být null nebo ne? Pojďme si typy explicitně uvést.

fun parseSummary(src: String): String {
    val doc: Document = Jsoup.parseBodyFragment(src)
    val body: Element = doc.body()
    val p: Element? = body.select("p").first()
    var retval : String = p?.text() ?: Jsoup.clean(src, Whitelist.none())
    if (retval.isNotEmpty()) {
        retval = retval.trim()
    }
    return retval
}

Ukecanější, ale dle mého názoru čitelnější. A je také vidět rozdíl, že proměnná body nikdy nebude null, zatímco proměnná p hodnotu null mít může.

Automatické přiřazení typu proměnné je užitečná věc, je však potřeba zvážit budoucí čitelnost zdrojáku. Výrazně však může pomoct funkce "Show local variable type hints" v Android studiu. Typy proměnných za vás vyluští IDE.

Automatické přetypování - užitečná věc

Automatické přetypování po kontrole proměnné na typ jsem využíval pouze do doby, než jsem zaimplementoval view binding. Pak už v aplikaci Pobyty na Moravě nebylo potřebné. Dokážu si ale představit situace, ve kterých ho s výhodou využiju. Je ale nutno mít na paměti čitelnost zdrojáku, čehož se dá dosáhnout volbou odpovídajících názvů proměnných.

companion object - střídmě a s rozvahou

Při studiu Kotlinu určitě zaznamenáte absenci statických proměnných a metod, jak je známe z Javy. Ekvivalenty, které Kotlin nabízí, jsou dva. Každý má své specifické vlastnosti a použití.

Prvním možností je deklarovat proměnné nebo funkce jako top-level. To znamená, že nejsou součástí žádné třídy. Pro příklad si vezměme následující ukázku, řekněme, že bude uložena v souboru Example.kt:

const val MY_CONST : String = "Some value"

fun doSomething() {
...
}

class Example {
...
}

Zdroják z ukázky bude zkompilován do dvou souborů:

  1. Example.class obsahující třídu Example
  2. ExampleKt.class obsahující statickou konstantu MY_CONST a statickou metodu doSomething()

Ostatní zdrojáky vidí top-level prvky na úrovni package. Není proto potřeba importovat celou třídu, stačí importovat package, ve kterém jsou prvky uloženy. Konstantu MY_CONST kompilátor zpracuje stejným způsobem, jako kdybychom ji v Javě nadefinovali s modifikátory static final. Volání metody doSomething() je pak klasickým voláním statické metody. Nevýhodou však je, že metoda doSomething() nejspíš neuvidí privátní prvky třídy Example.

Další možností je použít tzv. companion object. Třída z ukázky pak bude vypadat takto:

class Example {
...
    companion object {
        val MY_CONST : String = "Some value"
        
        fun doSomething() {
        ...
        }
    }
}

Funkce a proměnné deklarované v companion objektu jsou na úrovni Kotlinu přístupné jako statické prvky třídy Example. Zdroják z ukázky však bude zkompilován takto:

  • companion object bude zkompilován coby inner class Companion třídy Example.
  • Třída Example bude obsahovat statickou proměnnou Companion, která obsahuje instanci companion objektu. Při načítání třídy Example tudíž dojde k volání implicitního konstruktoru třídy Companion.
  • Metoda doSomething() je volána jako nestatická metody třídy Companion.
  • Přístup ke konstantě MY_CONST pak bude vypadat takto:
Example.Companion.getMY_CONST()

Na to, že potřebujeme pouze statickou metodu a konstantu je to zbytečný kód navíc. Na druhou stranu má metoda doSomething() plný přístup k privátním prvkům třídy Example, tedy i k privátnímu konstruktoru. Dá se tedy použít jako factory metoda.

Samozřejmě, výsledek kompilace můžeme ovlivnit použitím anotací. Mé doporučení je však následující: statické prvky definovat přednostně jako top-level. Teprve až budeme potřebovat factory metody, použijeme companion object.

Knihovny navíc - důsledek

Aplikaci máme přepsanou, pojďme ji zbuildovat. Výsledný apk soubor nabobtnal z původních 1,68 na 1,86 MB. Nárůst o necelých 11%, což není moc, ale z pohledu uživatele to nepřináší žádnou přidanou hodnotu. Dalo se očekávat, že apk soubor naroste o runtime knihovny Kotlinu.

Pro mě coby programátora však přepis do Kotlinu přinesl zcela jednoznačnou přidanou hodnotu v podobě zjednodušení a zefektivnění dalšího vývoje. Jedním z dalších kroků proto bude minimalizace apk souboru.

Post Scriptum

Blogování o programování mi přineslo nečekaný, ale velmi přínosný vedlejší efekt. Objevil jsem části zdrojáků, které bych mohl naprogramovat ještě efektivněji.

V průběhu psaní tohoto příspěvku jsem se také zúčastnil Android Developer Meetupu pořádaného společností STRV. Hádáte správně, že ukázky použité v přednáškách byly v Kotlinu. Java est mort, vive le Kotlin! (ou, to není moc pozitivní závěr, ale už ho tak nechám...)

Tagy: Android, Java, Programování, Kotlin

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...