003 ‐ Hod kostkou - zizanek/-github-tz-pma-2026-priprava GitHub Wiki

Hoď kostkou: XML + Kotlin a Jetpack Compose

Na stejné aplikaci si ukážeme dva způsoby tvorby uživatelského rozhraní v Androidu:

  1. XML + Kotlin – rozhraní definujeme v XML a ovládáme ho z Kotlinu.
  2. Jetpack Compose – rozhraní i jeho chování zapisujeme v Kotlinu.

Zadání

Vytvořte aplikaci, která obsahuje:

  • nadpis „Hoď kostkou“,
  • velký symbol kostky,
  • tlačítko „Hodit“.

Po stisknutí tlačítka se symbol kostky desetkrát změní s prodlevou 250 milisekund. Potom se zobrazí výsledný hod.

Během animace bude tlačítko zakázané, aby uživatel nemohl spustit několik hodů současně.

Názvy proměnných, funkcí a ID píšeme anglicky. Texty pro uživatele a komentáře ponecháváme česky.

Společné základy

Symboly kostky

Použijeme textové znaky Unicode:

Hodnota Znak Unicode
1 ⚀ U+2680
2 ⚁ U+2681
3 ⚂ U+2682
4 ⚃ U+2683
5 ⚄ U+2684
6 ⚅ U+2685

Nejde o obrázky. Znaky lze přímo zkopírovat do kódu. Jejich vzhled závisí na použitém písmu.

Seznam v Kotlinu

val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
  • val deklaruje proměnnou, které už nemůžeme přiřadit jinou hodnotu.
  • diceSymbols je název proměnné.
  • listOf(...) vytvoří seznam s rozhraním pro čtení.
  • Jednotlivé položky jsou textové řetězce typu String.

Položky seznamu se indexují od nuly:

diceSymbols[0] // ⚀
diceSymbols[1] // ⚁
diceSymbols[5] // ⚅

Náhodné číslo

val diceValue = (1..6).random()

Výraz 1..6 představuje rozsah od 1 do 6 včetně obou krajních hodnot. Funkce random() z něj vybere náhodnou hodnotu.

Odpovídající symbol získáme takto:

diceSymbols[diceValue - 1]

Odečítáme jedničku, protože čísla na kostce začínají jedničkou, ale indexy seznamu nulou.


1. XML + Kotlin

Vytvoření projektu

V Android Studiu vytvořte projekt pomocí šablony Empty Views Activity a zvolte jazyk Kotlin.

Budeme upravovat dva soubory:

  • app/src/main/res/layout/activity_main.xml
  • MainActivity.kt v balíčku aplikace

Rozhraní: activity_main.xml

Nahraďte obsah souboru tímto XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/llMain"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="24dp"
    android:background="#F5F3FF">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hoď kostkou"
        android:textSize="28sp"
        android:textStyle="bold"
        android:textColor="#352060" />

    <TextView
        android:id="@+id/tvDice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginBottom="24dp"
        android:text="⚀"
        android:textSize="120sp"
        android:textColor="#352060" />

    <Button
        android:id="@+id/btnRoll"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hodit"
        android:textSize="20sp"
        android:textAllCaps="false" />

</LinearLayout>

Vysvětlení XML

LinearLayout

LinearLayout uspořádává své potomky do řady nebo sloupce.

android:orientation="vertical"

Prvky budou pod sebou.

android:gravity="center"

Obsah bude vystředěný vodorovně i svisle.

Velikost prvků

android:layout_width="match_parent"
android:layout_height="match_parent"

Kořenový prvek vyplní dostupný prostor rodiče.

Hodnota wrap_content u textů a tlačítka znamená, že se jejich velikost přizpůsobí obsahu.

Jednotky dp a sp

  • dp používáme pro rozměry a vzdálenosti.
  • sp používáme pro velikost textu. Zohledňuje také uživatelské nastavení velikosti písma.

Padding a margin

  • padding je prostor uvnitř prvku mezi jeho okrajem a obsahem.
  • margin je prostor kolem prvku.

ID prvků

android:id="@+id/tvDice"

Vytvoří identifikátor, pomocí kterého prvek najdeme v Kotlinu.

Používáme čtyři ID. Prefix označuje typ prvku: ll = LinearLayout, tv = TextView a btn = Button. Za prefixem následuje anglický název v camelCase:

ID Význam
llMain Kořenové rozložení
tvTitle Nadpis aplikace
tvDice Text se symbolem kostky
btnRoll Tlačítko

Logika: MainActivity.kt

Řádek package musí odpovídat balíčku vašeho projektu.

package com.example.myapp001

import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)

        ViewCompat.setOnApplyWindowInsetsListener(
            findViewById(R.id.llMain)
        ) { view, insets ->
            val systemBars = insets.getInsets(
                WindowInsetsCompat.Type.systemBars()
            )

            view.setPadding(
                systemBars.left,
                systemBars.top,
                systemBars.right,
                systemBars.bottom
            )

            insets
        }

        val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")

        val tvDice = findViewById<TextView>(R.id.tvDice)
        val btnRoll = findViewById<Button>(R.id.btnRoll)

        btnRoll.setOnClickListener {
            lifecycleScope.launch {
                // Během animace zabráníme dalšímu kliknutí.
                btnRoll.isEnabled = false

                // Desetkrát změníme symbol kostky.
                repeat(10) {
                    tvDice.text = diceSymbols.random()
                    delay(250)
                }

                // Výsledný hod.
                val diceValue = (1..6).random()
                tvDice.text = diceSymbols[diceValue - 1]

                btnRoll.isEnabled = true
            }
        }
    }
}

Vysvětlení Kotlin kódu

Aktivita a onCreate

class MainActivity : AppCompatActivity()

MainActivity dědí z AppCompatActivity. Aktivita je komponenta Androidu, která v tomto příkladu hostuje obrazovku aplikace.

override fun onCreate(savedInstanceState: Bundle?)

Metodu onCreate() Android volá při vytváření aktivity. Zde připravíme rozhraní a jeho chování.

Načtení XML

setContentView(R.layout.activity_main)

Načte rozhraní ze souboru activity_main.xml.

R je automaticky generovaná třída s odkazy na prostředky aplikace, například rozložení a ID prvků.

Systémové lišty

enableEdgeToEdge()

Umožní aplikaci vykreslovat obsah také za systémovými lištami.

Následující posluchač nastaví odsazení podle velikosti systémových lišt, aby pod nimi obsah nezůstal schovaný.

Volání setPadding() zde nahrazuje původní padding z XML. V této vystředěné ukázce to není zásadní; pokud chceme zachovat také původních 24 dp, musíme je k odsazení systémových lišt přičíst.

Vyhledání prvků

val tvDice = findViewById<TextView>(R.id.tvDice)
val btnRoll = findViewById<Button>(R.id.btnRoll)

Podle ID získáme odkazy na konkrétní prvky rozhraní.

<TextView> a <Button> určují očekávaný typ prvku.

Pro první aplikaci používáme názorné findViewById. Tento přístup je stále použitelný.

V dalších aplikacích budeme používat View Binding. Ten z XML automaticky vygeneruje třídu s typovanými odkazy na prvky, takže je nebudeme muset ručně vyhledávat. Například místo findViewById<TextView>(R.id.tvDice) budeme přistupovat k prvku pomocí binding.tvDice. Samotné XML a jeho ID mohou zůstat stejné.

View Binding se týká varianty s XML. V Jetpack Compose jej pro toto rozhraní nepotřebujeme.

Reakce na kliknutí

btnRoll.setOnClickListener {
    // Kód provedený po kliknutí.
}

Blok ve složených závorkách se provede při stisknutí tlačítka.

Korutina

lifecycleScope.launch {
    // Průběh hodu.
}

Spustí korutinu navázanou na životnost aktivity. Při zničení aktivity se zruší.

Korutina umožní během animace čekat pomocí delay(), aniž by zablokovala hlavní vlákno a ovládání aplikace.

Animace

repeat(10) {
    tvDice.text = diceSymbols.random()
    delay(250)
}
  • repeat(10) provede blok desetkrát.
  • diceSymbols.random() vybere náhodný symbol ze seznamu.
  • tvDice.text = ... přímo změní text zobrazeného prvku.
  • delay(250) pozastaví korutinu na přibližně 250 ms.

Celkem animace trvá přibližně 2,5 sekundy. Stejný symbol může náhodně padnout několikrát za sebou.

Zakázání tlačítka

btnRoll.isEnabled = false

Během animace tlačítko nereaguje na kliknutí. Po dokončení ho znovu povolíme:

btnRoll.isEnabled = true

2. Jetpack Compose

Vytvoření projektu

Vytvořte druhý projekt pomocí šablony Empty Activity s podporou Compose.

Tato šablona připraví potřebné závislosti a nastavení.

Pouhé vložení následujícího kódu do Views projektu nestačí. Takový projekt by nejprve potřeboval zapnout podporu Compose v Gradle.

V této variantě nepotřebujeme activity_main.xml. Rozhraní vytvoříme přímo v Kotlinu.

Kompletní MainActivity.kt

Řádek package opět upravte podle svého projektu.

package com.example.myapp001jc

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            MaterialTheme {
                DiceApp()
            }
        }
    }
}

@Composable
fun DiceApp() {
    val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")

    // Stav obrazovky.
    var diceValue by remember { mutableStateOf(1) }
    var isRolling by remember { mutableStateOf(false) }

    // Umožní spustit animaci s prodlevami.
    val scope = rememberCoroutineScope()

    val backgroundColor = Color(0xFFF5F3FF)
    val primaryColor = Color(0xFF352060)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(backgroundColor)
            .safeDrawingPadding()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Hoď kostkou",
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold,
            color = primaryColor
        )

        Text(
            text = diceSymbols[diceValue - 1],
            fontSize = 120.sp,
            color = primaryColor,
            modifier = Modifier.padding(vertical = 24.dp)
        )

        Button(
            enabled = !isRolling,
            colors = ButtonDefaults.buttonColors(
                containerColor = primaryColor,
                contentColor = Color.White
            ),
            onClick = {
                isRolling = true

                scope.launch {
                    // Desetkrát změníme symbol kostky.
                    repeat(10) {
                        diceValue = (1..6).random()
                        delay(250)
                    }

                    // Výsledný hod.
                    diceValue = (1..6).random()
                    isRolling = false
                }
            }
        ) {
            Text(
                text = "Hodit",
                fontSize = 20.sp
            )
        }
    }
}

Vysvětlení Compose kódu

setContent

setContent {
    MaterialTheme {
        DiceApp()
    }
}

Místo načtení XML popíšeme obsah obrazovky pomocí Compose funkcí.

MaterialTheme poskytuje téma pro komponenty Material Design, například tlačítka.

@Composable

@Composable
fun DiceApp()

Anotace @Composable označuje funkci, která se účastní sestavování rozhraní v Compose.

V našem případě popisuje celou obrazovku aplikace.

Stav obrazovky

var diceValue by remember { mutableStateOf(1) }
var isRolling by remember { mutableStateOf(false) }

Uchováváme dvě hodnoty:

Proměnná Význam Počáteční hodnota
diceValue Aktuální hodnota kostky 1
isRolling Zda právě probíhá animace false
  • var umožňuje hodnotu měnit.
  • mutableStateOf(...) vytvoří stav, jehož změny může Compose sledovat.
  • remember uchová stav mezi opakovanými vyhodnoceními daného místa rozhraní.
  • by umožní přímo číst a zapisovat hodnotu místo používání .value.

Když se stav změní, Compose může znovu vyhodnotit části rozhraní, které ho čtou. Tento proces se nazývá rekompozice.

remember samo nezachová hodnotu při znovuvytvoření aktivity, například po otočení zařízení. Pro jednoduchý uložitelný stav lze později použít rememberSaveable.

Column

Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
)

Column umisťuje prvky pod sebe, podobně jako svislý LinearLayout.

Parametry zde vystředí obsah vodorovně i svisle.

Modifier

Modifier
    .fillMaxSize()
    .background(backgroundColor)
    .safeDrawingPadding()
    .padding(24.dp)

Modifier upravuje rozložení a vzhled prvku. Pořadí jednotlivých úprav je důležité.

V této ukázce:

  1. fillMaxSize() vyplní dostupný prostor.
  2. background(...) nastaví pozadí.
  3. safeDrawingPadding() odsune obsah od systémových lišt a dalších oblastí, kde by mohl být překrytý.
  4. padding(24.dp) přidá vnitřní odsazení.

Barvy

Color(0xFFF5F3FF)

Barva je zapsaná ve formátu AARRGGBB:

  • AA – krytí,
  • RR – červená,
  • GG – zelená,
  • BB – modrá.

Úvodní FF znamená plné krytí.

Zobrazení kostky

Text(
    text = diceSymbols[diceValue - 1],
    fontSize = 120.sp,
    color = primaryColor
)

Text závisí na aktuální hodnotě diceValue.

Při změně diceValue nemusíme vyhledat textový prvek ani mu ručně přiřadit nový text. Compose aktualizuje zobrazení podle stavu.

Povolení tlačítka

enabled = !isRolling

Znak ! znamená logickou negaci:

  • když isRolling je true, tlačítko je zakázané,
  • když isRolling je false, tlačítko je povolené.

Korutina a animace

val scope = rememberCoroutineScope()

Vytvoří scope navázaný na životnost tohoto místa v Compose rozhraní. Můžeme z něj spustit korutinu při kliknutí. Když dané místo opustí kompozici, scope se zruší.

onClick = {
    isRolling = true

    scope.launch {
        repeat(10) {
            diceValue = (1..6).random()
            delay(250)
        }

        diceValue = (1..6).random()
        isRolling = false
    }
}

Průběh je stejný jako v XML variantě:

  1. Zakážeme tlačítko změnou stavu isRolling.
  2. Desetkrát změníme číslo kostky.
  3. Mezi změnami počkáme 250 ms.
  4. Vybereme výsledný hod.
  5. Znovu povolíme tlačítko.

Porovnání přístupů

Oblast XML + Kotlin Jetpack Compose
Popis rozhraní XML Kotlin
Zobrazení rozhraní setContentView(...) setContent { ... }
Svislé rozložení LinearLayout Column
Text TextView Text
Vyhledání prvku findViewById(...) V ukázce není potřeba
Změna kostky Přiřadíme tvDice.text Změníme stav diceValue
Zakázání tlačítka Změníme isEnabled enabled závisí na stavu
Korutina pro animaci lifecycleScope rememberCoroutineScope()

Hlavní rozdíl v této ukázce

V XML variantě přímo přikazujeme konkrétnímu prvku, co má zobrazit:

tvDice.text = diceSymbols[diceValue - 1]

V Compose popíšeme, co se má zobrazovat podle stavu:

Text(text = diceSymbols[diceValue - 1])

Potom měníme stav:

diceValue = (1..6).random()

První ukázka předvádí imperativní aktualizaci rozhraní, druhá deklarativní popis rozhraní.

Ověření funkčnosti

V obou aplikacích zkontrolujte:

  • Po spuštění se zobrazuje tvDice s jedním bodem.
  • Po kliknutí se diceSymbols střídají přibližně 2,5 sekundy.
  • Během animace nelze znovu stisknout tlačítko.
  • Po dokončení zůstane zobrazený výsledný hod.
  • Tlačítko lze znovu použít.

Vzhled tlačítka a vykreslení znaků se mohou mezi variantami mírně lišit podle tématu a písma.

Úkoly k procvičení

  1. Změňte barvu pozadí a velikost kostky.
  2. Zkraťte animaci na jednu sekundu.
  3. Během animace změňte text tlačítka na „Házím…“.
  4. Přidejte druhou kostku.
  5. Zobrazte součet hodnot obou kostek.

Dokumentace

⚠️ **GitHub.com Fallback** ⚠️