Synetech

Sbohem Javo, vítej Kotline aneb co máme v SYNETECHu rádi na Kotlinu

Úvod 👋

V tomto článku bych se rád podělil o naše více než roční zkušenosti s Kotlinem na Androidu. V SYNETECHu vyvíjíme všechny Android projekty právě v Kotlinu a jelikož naše společnost tvoří mobilní aplikace pro klienty, máme zodpovědnost je dodat v nejlepší možné kvalitě, stabilní a zároveň v co nejkratším čase.

Pokud se chcete dozvědět více o tom, proč a jak používat Kotlin pro tvorbu mobilních aplikací, čtěte dál.

Co máme rádi na Kotlinu? ❤️

Jelikož si všichni v SYNETECHu uživáme programování, oblíbili jsme si Kotlin hlavně díky jeho výstižnosti, kontrole nullových referencí a 100% interoperabilitě s Javou. Přestože to je pouze špička ledovce výhod, které Kotlin přináší, povězme si o těchto hlavních výhodách něco více.

Interoperabilita s Java

Důvodem, proč se Kotlin tak rychle rozšířil v komunitě Android vývojářů (a vlastně i v Googlu samotném), je jeho 100% interoperabilita s Javou. To znamená, že v Kotlinu můžete použít libovolný Java kód/knihovnu. Díky tomu je možné zároveň používat Kotlin a Javu v jednom projektu a nemusíte přepisovat Java projekt znovu od začátku.

Kotlin a kontrola nullových referencí

“Věděli jste, že 70% produkčních aplikací napsaných v Javě padá na NullPointerException?”

Zdroj:[The Top 10 Exception Types in Production Java Applications — Based on 1B Events | OverOps Blog]

Kotlin obsahuje kontrolu nulových referencí na úrovni syntaxe, která zvyšuje stabilitu aplikace tím, že kontroluje, zda není přistupováno k nullové referenci, a to již při samotném buildu aplikace a ne až při jejím běhu. Proto, pokud správně používáte Kotlin, se nemusíte se obávat _NullPointerException_ výjimek. Kotlin používá stejný způsob definice “nullových” datových typů jako ostatní programovací jazyky (např.Swift) - buď je datový typ optional (volitelný, může tedy být nullový) nebo required (vyžadovaný, nesmí být nullový).

Optional (volitelné) datové typy jsou rozšířeny o znak ? a před přístupem k jejich hodnotě musejí být zkontrolovány, zda nejsou nullové. Pokud je datový typ required (vyžadovaný), pak není nutné kontrolu provádět. Pokud byste se ale pokusili přistoupit k hodnotě optional typu bez kontroly, pak by se na chybu přišlo už během buildu aplikace a proces by selhal dřív než v produkčním kódu u samotných uživatelů.

Výstižnost a přehlednost Kotlinu

Výstižnost syntaxe Kotlinu je hlavní důvod, proč jsme si jej tak oblíbili ❤️. Téměř většina součástí zmíněných níže činí kód výstižnějším a čitelnějším, tudíž výsledné funkcionality jsou stabilnější a odolnější vůči chybám v zápisu kódu.

Datové třídy (data classes)

Pamatujete si, kolik řádek kódu jste museli napsat, abyste definovali jednoduché DTO (Data transfer objects) nebo POJO objekty? Já ano. A právě z tohoto důvodu Kotlin obsahuje datové třídy (data classes). Modifikátor data umístěný před definici třídy určuje, že všechny proměnné definované v primárním konstruktoru budou součástí equals, hashCode , toString a copy metod. Dokonce ani nemusíte ručně psát gettery a settery, protože Kotlin interně přistupuje ke všem proměnným třídy přes automaticky generované gettery/settery, které můžete přetížit (override). Tyto datové třídy jsou vhodné zejména pro použití v jednosměrném datovém toku (uni-directional, stream driven data flows) a společně s dvěma dalšími součástmi Kotlinu (named parameters a extensions), lze snadno vytvořit odlišné modely pro API, business logiku, UI, atd., protože lze jednoduše napsat extension pro převod modelů mezi sebou (např. převod API modelu na doménový), s tím, že s použitím named parameters, buď mapování zůstane funkční i při změně daných modelů nebo build aplikace selže.

// Example of data class (saved more than 10 lines in comparison to Java)
data class User(val name: String, val surname: String, val age: Int)
// Example of custom getters and setters on a property
class MrUser {
var name: String = ""
get() {
return "Mr. $field" // Get name with "Mr." prefix
}
set(newValue) {
println("old: '$field', new: '$newValue'") // Print change
field = newValue // Store new value
}
}
view raw DataClasses.kt hosted with ❤ by GitHub

Více o data classes: Data Classes — Kotlin Programming Language

Zapouzdřené třídy (sealed classes)

Pokud chceme definovat uzavřenou množinu potomků nějaké třídy, užitečným modifikátorem je klíčové slovo sealed, které limituje hierarchii dědění třídy. Ukázkový kód definuje datový typ UIState , který může obsahovat pouze potomka jednoho z následujících datových typů: LoadingState, DataState, ErrorState.

sealed class UIState
object LoadingState : UIState() // Data is being loaded
class DataState(val data: Data) : UIState() // Data loaded successfully
class ErrorState(val error: Throwable) : UIState() // In case of failure
view raw SealedClasses.kt hosted with ❤ by GitHub

Při použití “chytrého přetypování” — smart casts (Type Checks and Casts: ‘is’ and ‘as’ — Kotlin Programming Language) a klíčového slova when (Control Flow: if, when, for, while - Kotlin Programming Language) můžeme reagovat na změnu stavu celkem snadno a přehledně:

fun handleStateChange(newState: UIState) {
when (newState) {
is LoadingState -> println("Loading ...")
is DataState -> println("Got data: ${newState.data}")
is ErrorState -> println("Error: ${newState.error.message}")
}
}
view raw SealedClasses2.kt hosted with ❤ by GitHub

Více o sealed classes: Sealed Classes — Kotlin Programming Language

Extensions

Jak jsem již zmínil výše, extensions mohou být použity pro rozšíření existující třídy o další metody mimo definici dané třídy. Bez extensions by bylo nutné definovat např. Util třídu, která není tak čitelná jako syntaxe při použití extension.

Jako příklad níže je uveden kód, který “přidává” metodu makeInvisible() pro třídu typu View a pro všechny její potomky:

fun View.makeInvisible() {
this.visibility = View.INVISIBLE
}
val button: Button = //...
button.makeInvisible()
view raw Extensions.kt hosted with ❤ by GitHub

Více o extension functions: Extensions — Kotlin Programming Language

Single-expression funkce

Metody v Kotlinu mohou vracet hodnotu přímo — bez nutnosti definice těla metody. To znamená, že toto:

fun answerToEverything() = 42
view raw SingleExpression.kt hosted with ❤ by GitHub

je stejné jako toto:

fun answerToEverything(): Int {
return 42
}
view raw SingleExpression2.kt hosted with ❤ by GitHub

Ale mnohem kratší a přehlednější. A při použití s bloky if,when, let, apply, apod. (které také umožňují vracet hodnotu) ještě více praktické. Například:

fun parseColor(color: String) = when (color.toLowerCase()) {
"red" -> "#f00"
"green" -> "#0f0"
"blue" -> "#00f"
else -> "#fff"
}
view raw SingleExpression3.kt hosted with ❤ by GitHub

Výchozí hodnoty

Kotlin, oproti Javě, umožňuje parametrům metod použít výchozí hodnoty, takže již nemusíte definovat několik metod s různými variacemi parametrů.

Například:

fun logException(e: Throwable, level = Log.ERROR) {
// implementation
}
// Then you can call the method without providing the second parameter
logException(IllegalStateException("Test exception"))
logException(IllegalStateException("Test exception"), Log.WARN)
view raw DefaultValues.kt hosted with ❤ by GitHub

Kotlin také podporuje výchozí implementaci metod definovaných v rozhraních. Například:

interface Greeter {
fun greet() { println("Hi!") }
}
view raw DefaultValues2.kt hosted with ❤ by GitHub

Ale pamatujte, s velkou mocí přichází velká zodpovědnost.

Scope metody

Kotlin obsahuje několik scope metod. Zejména let, apply,run,with,also. Tyto metody snižují potřebu psaní boilerplate kódu tím, že mění scope v bloku těla dané scope metody.

Jeden příklad řekne víc než tisíc slov:

private const val DURATION_IN_MS = 3000L
val fadeInAnimation = AlphaAnimation(0f, 1f).apply {
duration = DURATION_IN_MS
interpolator = AccelerateDecelerateInterpolator()
fillAfter = true
}
view raw ScopeFunctions.kt hosted with ❤ by GitHub

Výše uvedený kód vytvoří instanci AlphaAnimation , na které modifikuje část proměnných a vrátí finální (již modifikovanou) instanci. V tomto konkrétním případě apply mění referenci this na instanci AlphaAnimation(0f, 1f) a poté je stejná instance vrácena a uložena do fadeInAnimation proměnné.

Pokud se chcete dozvědět více o scope metodách, doporučuji si přečíst článek Mastering Kotlin standard functions: run, with, let, also and apply.

Delegát proměnné (property delegate)

Delegát je třída, která reaguje na přístup k proměnné — na její čtení a/nebo zápis. Tudíž pokaždé, kdy je přistupováno k proměnné, je provolána příslušná metoda delegáta.

Nejznámějším delegátem je lazy, který vyhodnotí tělo funkce poprvé, kdy je daná proměnná čtena, a návratovou hodnotu uchová pro další čtení. Poté při každém čtení proměnné je vrácena původně uložená hodnota. Níže uvedený příklad vyhodnotí metodu computeAnswer() při prvním čtení proměnné answer a poté se již metoda computeAnswer() nevyhodnocuje a vrací se předchozí hodnota.

val answer: Int by lazy {
computeAnswer()
}
fun computeAnswer() = 42
view raw PropertyDelegates.kt hosted with ❤ by GitHub

Více o delegátech: Delegated Properties — Kotlin Programming Language

Funkce a lambdy

Jelikož je Kotlin moderní programovací jazyk, jeho syntaxe podporuje funkce vyššího řádu a lambda výrazy. Pokud tedy chcete psát reaktivní kód, lambdy a callback funkce mohou být užitečné. Jejich deklarace je jednoduchá a přehledná. Následující příklad prezentuje metodu pro “získání všech uživatelů”, která se skládá ze dvou callbacků. Callback onSuccess by měl být provolán v případě úspěšného získání dat. V opačném případě je zavolán callback onError , který obsahuje samotnou výjimku.

data class User(val name)
interface UserDataSource {
fun getAllUsers(
onSuccess: (users: List<User>) -> Unit,
onError: (err: Throwable) -> Unit
)
}
// class UpdateDataSourceImpl: UserDataSource { .... }
UpdateDataSourceImpl().getAllUsers(
onSuccess = { users -> print(users) }, // here you consume success state
onError = { err -> print(err.message) } // here you consume error state
)

Více o lambdách a funkcích: Functions: infix, vararg, tailrec — Kotlin Programming Language

Poznámka: Existuje i další způsob, jak definovat asynchronní operace, kterým jsou coroutines. Více na Coroutines Guide — Kotlin Programming Language.

Multiplatformnost Kotlinu

Dobrou zprávou je, že Kotlin je multiplatformní jazyk! To znamená, že můžete kód používat na více platformách. Rádi bychom psali business logiku pouze jednou a poté bychom ji použili i na ostatních platformách — například jak na Androidu tak na iOS. Více o podpoře více platforem je zmíněno na oficiálním webu Multiplatform Projects — Kotlin Programming Language.

…a mnohem více

Tato sekce pokryla pouze pár vlastností Kotlinu, kterých si opravdu ceníme, ale můžete z něj dostat mnohem více! K tomu vám může pomoci dokumentace syntaxe Kotlinu — Reference — Kotlin Programming Language.

Gotchas — Na co si u Kotlinu dát pozor? 🙀

V této sekci zmíníme pár věcí, na které si dát pozor, a o kterých je dobré vědět. Pár z nich nás překvapilo, ale ne mile…

Nullové reference a Java kód

Jak jsem již zmínil výše, Kotlin kontroluje na úrovni syntaxe, zda proměnné jsou datového typu, který může obsahovat nullovou referenci, či ne. Proto bychom již neměli dostat NullPointerException. To zní hezky. Ale kód napsaný v Javě neobsahuje informaci, zda proměnná/parametr může nabývat nullové reference nebo ne. Ano, některé proměnné nebo parametry mohou být označeny @NotNull či @Nullable anotacemi (Calling Java from Kotlin - Kotlin Programming Language), které Kotlin kompilátor bere v potaz a získá tak informaci o tom, zda reference může být nullová. Bohužel ale většina Java kódu není takto anotována. Proto je lepší předpokládat, že při přístupu k proměnné může být reference nullová (či naopak nesmí být nullová při volání metody s parametry) než čelit NullPointerException výjimce.

Řízení výjimek Java vs. Kotlin

V Javě existuje klíčové slovo throws, které indikuje, že metoda může vyhodit specifickou výjimku a tudíž přistupující část kódu musí tuto výjimku odchytit či propagovat dále, aby nedošlo k pádu aplikace. Kotlin nepodporuje kontrolu výjimek Calling Java from Kotlin - Kotlin Programming Language, a tudíž nevyžaduje, aby se dané vyjímky musely odchytit/propagovat dále. Pokud tedy přistupujete k logice psané v Javě, je dobré si zkontrolovat, zda daná metoda záměrně nevyhazuje konkrétní výjimku. V případě že ano, musíme ošetřit volání dané metody try-catch blokem jako bychom to udělali v Javě.

Rozsahy ve smyčkách

Kotlin zavedl novou syntaxi pro definici for-smyčky — tzv. “rozsahy” (ranges). Nyní lze tedy for-smyčku definovat následovně:

var max = 10
for (i in 1..max) {
// ...
}
view raw RangesAndLoops1.kt hosted with ❤ by GitHub

Ale to není stejné jako toto:

var max = 10;
for (int i = 1 ; i <= max ; i++) {
// ...
}
view raw RangesInLoops2.kt hosted with ❤ by GitHub

Protože výraz 1..max je v Kotlinu převeden na instanci IntRangea tudíž je podmínka vyhodnocena pouze jednou. Výše uvedený kód odpovídá tomuto kódu:

var max = 10
val range = IntRange(1, max)
for (i in range) {
// ...
}
view raw RangesInLoops3.kt hosted with ❤ by GitHub

To znamená, že pokud byste změnili hodnotu proměnné max v těle smyčky, nemělo by to žádný vliv na její rozsah. Není špatně, že kód je de-facto optimalizovaný a podmínka je vyhodnocena pouze jednou, ale pokud takovéto chování neočekáváte, může vás nemile překvapit a mohli byste strávit hodně času debuggováním, protože byste očekávali, že se podmínka vyhodnocuje v každém kroku. Pro takové situace se hodí spíše použít smyčku while.

Priorita Elvis operatoru

Představte si, že máte následující kus kódu:

val a: Int? = 10
val b: Int = 10
val sum = a ?: 0 + b
view raw Elvis.kt hosted with ❤ by GitHub

Čekali byste, že hodnota sum bude 20, že? Ale ve skutečnosti je 10! Proč? Protože dle priorit operátorů (dle:Grammar) má operátor sčítání vyšší prioritu než Elvis operátor. Tudíž Kotlin kompilátor vyhodnotí výraz následovně:

val sum = a ?: (0 + b)
view raw Elvis2.kt hosted with ❤ by GitHub

Není to úplně patrné na první pohled, proto je dobré být opatrný při používání Elvis operátoru.

Konstruktory and inicializéry

Dle Kotlin Reference Classes and Inheritance — Kotlin Programming Language, jsou init bloky (známé též jako inicializéry) součástí konstruktoru a jsou vyhodnocovány v pořadí, v jakém jsou definovány v kódu. Výstup z následujícího kódu (převzato z Kotlin Reference):

class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
InitOrderDemo("hello")
view raw Constructors.kt hosted with ❤ by GitHub

je následující:

First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5

Dejte si tedy pozor na inicializační bloky, protože záleží na jejich pořadí.

Shrnutí aneb proč tedy Kotlin?

Pamatuji si radost, jakou jsme měli, když Apple představil Swift. Bylo super psát bezpečný a výstižný kód a ušetřilo to celkem dost času a výsledné aplikace byly mnohem stabilnější. Myslím si, že adaptace Kotlinu pro Android platformu je ta nejlepší věc, co se mohla Android komunitě stát. A při použití Android Kotlin knihovny GitHub — Kotlin/anko: Pleasant Android application development , je vývoj mnohem rychlejší, jednodušší a hlavně méně “bolestivý”.

Nebojte se ve vašem projektu dát Kotlinu šanci, je opravdu snadné se Kotlin naučit. A pokud se obáváte nasadit Kotlin na váš projekt, můžete jej vyzkoušet na stránce Try Kotlin. Komunita okolo Kotlinu společně s JetBrains odvedla opravdu kus dobré práce! 👏

Díky za přečtení a dejte nám vědět vaše zkušenosti s Kotlinem formou komentářů níže 🙂👇.