Przejdź do treści
School IT / Przestrzeń do nauki

school-it.pl // android + kotlin // rozdział 3 // lekcja 13

Wybór daty i godziny

Datę można by wpisywać w pole tekstowe, ale wtedy użytkownik pomyli format, wpisze 31 kwietnia albo „jutro”. Android ma gotowe okna z kalendarzem i zegarem: DatePickerDialog i TimePickerDialog. W tej lekcji otworzysz je przyciskiem, odbierzesz wybraną datę i godzinę, sprawdzisz, czy mają sens, i zbudujesz rezerwację sali gimnastycznej.

Kotlin DatePickerDialog TimePickerDialog Calendar String.format 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Odczytasz dzisiejszą datę i godzinę klasą Calendar.
  • Otworzysz okno kalendarza i odbierzesz wybraną datę.
  • Otworzysz okno zegara w formacie 24-godzinnym.
  • Wyświetlisz datę i godzinę z zerami wiodącymi: 05.03.2026, 9:05.
  • Zablokujesz daty z przeszłości i sprawdzisz dzień tygodnia.
TEORIA

Dzisiejsza data: Calendar

Okno wyboru daty musi wiedzieć, od której daty zacząć. Dzisiejszą datę daje klasa Calendar z pakietu java.util:

odczyt części daty
val teraz = Calendar.getInstance()          // obecna data i godzina

val rok = teraz.get(Calendar.YEAR)           // 2026
val miesiac = teraz.get(Calendar.MONTH)      // 0–11 !!!
val dzien = teraz.get(Calendar.DAY_OF_MONTH) // 1–31
val godzina = teraz.get(Calendar.HOUR_OF_DAY) // 0–23
val minuta = teraz.get(Calendar.MINUTE)       // 0–59

Miesiące liczone od zera

W Calendar styczeń to 0, a grudzień to 11. Okno wyboru daty też podaje miesiąc od zera. Przy wyświetlaniu zawsze dodaj 1 — to najczęstszy błąd w tej lekcji.

A java.time?

Nowsze programy w Kotlinie używają klas LocalDate i LocalTime z pakietu java.time — tam miesiące liczy się od 1. Okna wyboru Androida podają jednak dane w stylu Calendar, dlatego w tej lekcji zostajemy przy nim.

TEORIA

Okno wyboru daty

DatePickerDialog tworzysz w kodzie przycisku i pokazujesz metodą show(). W konstruktorze podajesz, co zrobić z wybraną datą (lambda), i od jakiej daty kalendarz ma startować:

otwarcie kalendarza
btnData.setOnClickListener {
    val dzis = Calendar.getInstance()

    val okno = DatePickerDialog(
        this,
        { _, r, m, d ->                              // wykona się po kliknięciu OK
            tvData.text = "Wybrano: $d.${m + 1}.$r"
        },
        dzis.get(Calendar.YEAR),                     // data, którą okno pokaże na start
        dzis.get(Calendar.MONTH),
        dzis.get(Calendar.DAY_OF_MONTH)
    )
    okno.datePicker.minDate = dzis.timeInMillis      // bez dat z przeszłości
    okno.show()
}

Lambda dostaje cztery wartości: kontrolkę kalendarza (niepotrzebna — _), rok, miesiąc (od zera!) i dzień. Kod w niej wykona się dopiero po kliknięciu OK — tak jak kod w setOnClickListener wykonuje się dopiero po kliknięciu. Anuluj zamyka okno bez wywołania lambdy.

9:12
Wybierz datę
pon., 16 lis
listopad 2026 ▾‹   ›
PWŚCPSN 1 2345678 9101112131415 16171819202122 23242526272829 30
AnulujOK
DatePickerDialog
9:12
Wybierz godzinę
09:05
00 06 12 18 09
AnulujOK
TimePickerDialog (24 h)

wygląd okien zależy od wersji Androida; dni przed dzisiejszą datą są wyszarzone dzięki minDate

TEORIA

Okno wyboru godziny

otwarcie zegara
btnGodzina.setOnClickListener {
    TimePickerDialog(
        this,
        { _, h, min ->
            tvGodzina.text = "Wybrano: $h:$min"
        },
        9, 0,          // godzina startowa 9:00
        true           // true = zegar 24-godzinny, false = AM/PM
    ).show()
}

Okno można też utworzyć i od razu pokazać w jednym wyrażeniu — jak tutaj. Godziny w lambdzie są w formacie 0–23, niezależnie od wyglądu zegara.

TEORIA

Wyświetlanie daty

Zapis "$h:$min" o 9:05 wyświetli „9:5”. Daty i godziny zapisujemy z zerami wiodącymi. Robi to String.format ze wzorcem %02d — „liczba całkowita, co najmniej 2 cyfry, brakujące uzupełnij zerem”:

zera wiodące
val tekst = String.format("%02d.%02d.%d", 5, 3, 2026)   // "05.03.2026"
val czas = String.format("%d:%02d", 9, 5)               // "9:05"

Każde %… we wzorcu to miejsce na kolejną wartość z listy po przecinku. %d to liczba całkowita bez dopełnienia.

TEORIA

Sprawdzanie daty

Wybraną datę zapisujemy w nowym obiekcie Calendar. Wtedy łatwo odczytać dzień tygodnia albo porównać daty:

dzień tygodnia wybranej daty
val wybrana = Calendar.getInstance()
wybrana.set(r, m, d)                                  // ustaw rok, miesiąc, dzień

val dzienTygodnia = wybrana.get(Calendar.DAY_OF_WEEK) // 1 = niedziela … 7 = sobota
val weekend = dzienTygodnia == Calendar.SATURDAY || dzienTygodnia == Calendar.SUNDAY
ZapisZnaczenie
kal.set(rok, miesiac, dzien)Ustawia datę w obiekcie (godzina zostaje bieżąca).
kal.get(Calendar.DAY_OF_WEEK)Dzień tygodnia: 1 = niedziela, 2 = poniedziałek, … 7 = sobota.
kal.timeInMillisData jako liczba milisekund od 1.01.1970 — wygodna do porównań i odejmowania.
okno.datePicker.minDate / maxDateNajwcześniejsza / najpóźniejsza data dostępna w kalendarzu.
PRZYKŁAD

Rezerwacja sali gimnastycznej

Nauczyciel rezerwuje salę: wybiera datę (tylko od dziś do 60 dni naprzód) i godzinę rozpoczęcia. Sala jest czynna w dni robocze od 8:00 do 15:00. Aplikacja pokazuje wybraną datę z nazwą dnia, liczbę dni do rezerwacji i komunikat, gdy wybór nie spełnia reguł. Zarezerwuj działa tylko przy poprawnej dacie i godzinie.

9:12
Rezerwacja sali
Wybierz datę
sobota, 14.11.2026 (za 2 dni)
Wybierz godzinę
10:30
W weekend sala jest zamknięta
Zarezerwuj
sobota — rezerwacja niemożliwa
9:13
Rezerwacja sali
Wybierz datę
poniedziałek, 16.11.2026 (za 4 dni)
Wybierz godzinę
10:30
Termin dostępny
Zarezerwuj
Zarezerwowano: 16.11.2026, 10:30–11:30
poniedziałek — termin przyjęty

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa RezerwacjaSali, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt. Okna wyboru nie wymagają żadnego wpisu w XML.
res/layout/activity_main.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_margin="16dp"
    android:fitsSystemWindows="true">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Rezerwacja sali"
        android:textSize="24sp"
        android:textStyle="bold" />

    <Button
        android:id="@+id/btnData"
        style="@style/Widget.Material3.Button.OutlinedButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:text="Wybierz datę" />

    <TextView
        android:id="@+id/tvData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="nie wybrano"
        android:textSize="18sp" />

    <Button
        android:id="@+id/btnGodzina"
        style="@style/Widget.Material3.Button.OutlinedButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:text="Wybierz godzinę" />

    <TextView
        android:id="@+id/tvGodzina"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="nie wybrano"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/tvKomunikat"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="16sp"
        android:textStyle="bold" />

    <Button
        android:id="@+id/btnRezerwuj"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:enabled="false"
        android:text="Zarezerwuj" />

    <TextView
        android:id="@+id/tvPotwierdzenie"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp" />

</LinearLayout>
MainActivity.kt — pełny plik
package pl.schoolit.rezerwacjasali

import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import java.util.Calendar
import kotlin.math.roundToInt

class MainActivity : AppCompatActivity() {

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

        val btnData = findViewById<Button>(R.id.btnData)
        val btnGodzina = findViewById<Button>(R.id.btnGodzina)
        val btnRezerwuj = findViewById<Button>(R.id.btnRezerwuj)
        val tvData = findViewById<TextView>(R.id.tvData)
        val tvGodzina = findViewById<TextView>(R.id.tvGodzina)
        val tvKomunikat = findViewById<TextView>(R.id.tvKomunikat)
        val tvPotwierdzenie = findViewById<TextView>(R.id.tvPotwierdzenie)

        // dni tygodnia: Calendar podaje 1 = niedziela … 7 = sobota, więc niedziela jest pierwsza
        val nazwyDni = arrayOf("niedziela", "poniedziałek", "wtorek", "środa",
                               "czwartek", "piątek", "sobota")

        // stan wyboru: null = jeszcze nie wybrano
        var wybranaData: Calendar? = null
        var godzina = -1
        var minuta = -1

        fun sprawdz() {
            tvPotwierdzenie.text = ""
            val data = wybranaData
            if (data == null || godzina == -1) {
                tvKomunikat.text = "Wybierz datę i godzinę"
                tvKomunikat.setTextColor(Color.GRAY)
                btnRezerwuj.isEnabled = false
                return
            }

            val dzienTygodnia = data.get(Calendar.DAY_OF_WEEK)
            val weekend = dzienTygodnia == Calendar.SATURDAY || dzienTygodnia == Calendar.SUNDAY
            // sala 8:00–15:00, zajęcia trwają godzinę, więc start najpóźniej o 14:00
            val godzinaPoprawna = godzina in 8..13 || (godzina == 14 && minuta == 0)

            if (weekend) {
                tvKomunikat.text = "W weekend sala jest zamknięta"
                tvKomunikat.setTextColor(Color.parseColor("#C62828"))
                btnRezerwuj.isEnabled = false
            } else if (!godzinaPoprawna) {
                tvKomunikat.text = "Zajęcia mogą zaczynać się od 8:00 do 14:00"
                tvKomunikat.setTextColor(Color.parseColor("#C62828"))
                btnRezerwuj.isEnabled = false
            } else {
                tvKomunikat.text = "Termin dostępny"
                tvKomunikat.setTextColor(Color.parseColor("#2E7D32"))
                btnRezerwuj.isEnabled = true
            }
        }

        sprawdz()

        btnData.setOnClickListener {
            val dzis = Calendar.getInstance()

            val okno = DatePickerDialog(
                this,
                { _, r, m, d ->
                    val data = Calendar.getInstance()
                    data.set(r, m, d)
                    wybranaData = data

                    // ile dni do rezerwacji: różnica w milisekundach / milisekundy w dniu
                    val roznica = data.timeInMillis - Calendar.getInstance().timeInMillis
                    val dni = (roznica / 86_400_000.0).roundToInt()
                    val zaIle = if (dni == 0) "dziś" else "za $dni dni"

                    val nazwaDnia = nazwyDni[data.get(Calendar.DAY_OF_WEEK) - 1]
                    tvData.text = String.format("%s, %02d.%02d.%d (%s)", nazwaDnia, d, m + 1, r, zaIle)
                    sprawdz()
                },
                dzis.get(Calendar.YEAR),
                dzis.get(Calendar.MONTH),
                dzis.get(Calendar.DAY_OF_MONTH)
            )

            // zakres dat: od dziś do 60 dni naprzód
            okno.datePicker.minDate = dzis.timeInMillis
            val koniec = Calendar.getInstance()
            koniec.add(Calendar.DAY_OF_MONTH, 60)
            okno.datePicker.maxDate = koniec.timeInMillis

            okno.show()
        }

        btnGodzina.setOnClickListener {
            // okno startuje od poprzedniego wyboru, a za pierwszym razem od 8:00
            val startH = if (godzina == -1) 8 else godzina
            val startM = if (minuta == -1) 0 else minuta

            TimePickerDialog(
                this,
                { _, h, min ->
                    godzina = h
                    minuta = min
                    tvGodzina.text = String.format("%d:%02d", h, min)
                    sprawdz()
                },
                startH, startM, true
            ).show()
        }

        btnRezerwuj.setOnClickListener {
            val data = wybranaData ?: return@setOnClickListener
            tvPotwierdzenie.text = String.format(
                "Zarezerwowano: %02d.%02d.%d, %d:%02d–%d:%02d",
                data.get(Calendar.DAY_OF_MONTH), data.get(Calendar.MONTH) + 1, data.get(Calendar.YEAR),
                godzina, minuta, godzina + 1, minuta
            )
        }
    }
}

Co dzieje się po kolei

  • Stan to wybrana data (Calendar? — na starcie null) oraz godzina i minuta (na starcie -1, czyli „nie wybrano”).
  • W oknie daty minDate i maxDate ograniczają kalendarz: nie da się wybrać przeszłości ani daty dalszej niż 60 dni. koniec.add(Calendar.DAY_OF_MONTH, 60) przesuwa datę o 60 dni — także przez koniec miesiąca i roku.
  • Po wyborze daty powstaje nowy Calendar z tą datą. Z niego odczytujemy dzień tygodnia; odjęcie 1 zamienia numer 1–7 na indeks tablicy 0–6.
  • Liczba dni to różnica milisekund podzielona przez 86 400 000 (tyle milisekund ma doba) i zaokrąglona.
  • sprawdz() łączy obie informacje: weekend i godziny pracy sali. godzina in 8..13 sprawdza zakres.
  • W sprawdz() data jest najpierw kopiowana do val data — po sprawdzeniu null Kotlin pozwala na niej pracować. Przy btnRezerwuj robi to krócej operator ?:: „jeśli null, wyjdź”.

Sprawdź trzy rzeczy

  1. Otwórz kalendarz i spróbuj wybrać wczorajszą datę lub datę za trzy miesiące. Są wyszarzone.
  2. Usuń + 1 przy miesiącu w tvData.text. Listopad pokaże się jako 10 — tak wygląda najczęstszy błąd z datami.
  3. Wybierz godzinę 14:30. Pojawi się komunikat o godzinach pracy, choć 14:00 jest jeszcze poprawne.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
Calendar.getInstance()CalendarObiekt z bieżącą datą i godziną.
kal.get(Calendar.YEAR / MONTH / DAY_OF_MONTH)IntRok, miesiąc (0–11), dzień.
kal.get(Calendar.HOUR_OF_DAY / MINUTE)IntGodzina 0–23, minuta.
kal.get(Calendar.DAY_OF_WEEK)Int1 = niedziela … 7 = sobota.
kal.set(r, m, d), kal.add(pole, ile)metodyUstawienie daty, przesunięcie o dni/miesiące.
kal.timeInMillisLongData jako milisekundy — do porównań.
DatePickerDialog(this, { _, r, m, d -> }, r, m, d)oknoKalendarz; lambda po OK.
okno.datePicker.minDate / maxDateLongZakres dostępnych dat.
TimePickerDialog(this, { _, h, min -> }, h, min, true)oknoZegar; true = 24 h.
okno.show()metodaPokazuje okno.
String.format("%02d", x)StringLiczba z zerem wiodącym.
x in 8..13BooleanCzy liczba jest w zakresie.
wartosc ?: return@…operatorGdy null — wyjdź z klamer.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
wybrano 14 listopada, a wyświetla się 14.10Miesiąc od zera — dodaj 1 przy wyświetlaniu.
„9:5” zamiast „9:05”Brak zer wiodących — String.format("%d:%02d", h, min).
okno się nie pokazujeBrak .show().
kod po okno.show() nie widzi wybranej datyshow() nie czeka na użytkownika — kod po nim wykona się od razu. Wszystko, co zależy od wyboru, pisz w lambdzie.
nazwa dnia przesunięta o jedenTablica zaczyna się od poniedziałku, a Calendar numeruje od niedzieli. Ułóż tablicę od niedzieli i odejmij 1.
import java.sql.Date / android.icu.util.CalendarAndroid Studio podsuwa kilka klas o podobnych nazwach. W tej lekcji: java.util.Calendar.
minDate ustawione po show()Może nie zadziałać w otwartym oknie. Ustawiaj zakres przed show().
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Data urodzenia★☆☆

Przycisk otwiera kalendarz ustawiony na 1 stycznia 2009, bez dat z przyszłości. Po wyborze: „Urodziłeś się 07.05.2009, w czwartek”.

pokaż wskazówkę
  • Startowa data to po prostu trzy liczby w konstruktorze: 2009, 0, 1. Bez przyszłości: maxDate = Calendar.getInstance().timeInMillis.
ZAD 2Budzik★☆☆

Przycisk Ustaw budzik otwiera zegar. Po wyborze: „Budzik ustawiony na 6:45 — za 8 h 20 min” (względem bieżącej godziny; jeśli godzina już minęła — liczy do jutra).

pokaż wskazówkę
  • Zamień obie godziny na minuty od północy. Różnica ujemna? Dodaj 24 × 60.
ZAD 3Ile dni do wakacji★★☆

Ekran od razu pokazuje liczbę dni do najbliższego 27 czerwca (jeśli w tym roku już minął — do przyszłorocznego). Przycisk pozwala wybrać dowolną inną datę i policzyć dni do niej. Wynik w tygodniach i dniach: „52 dni (7 tyg. i 3 dni)”.

pokaż wskazówkę
  • Tygodnie: dni / 7, reszta dni: dni % 7. Porównaj z dzisiejszą datą przez timeInMillis.
ZAD 4Wizyta u lekarza★★☆

Wybór daty (dni robocze, od jutra do końca przyszłego miesiąca) i godziny (co 15 minut, 8:00–17:45). Godzina inna niż :00, :15, :30, :45 jest zaokrąglana w dół do najbliższego kwadransa z komunikatem „Zaokrąglono do 10:15”.

pokaż wskazówkę
  • Zaokrąglenie: min / 15 * 15 — dzielenie liczb całkowitych obcina resztę. Koniec przyszłego miesiąca: add(Calendar.MONTH, 2), a potem set(Calendar.DAY_OF_MONTH, 1) i add(Calendar.DAY_OF_MONTH, -1).
ZAD 5Wypożyczalnia rowerów★★☆

Dwie daty: odbioru i zwrotu. Data zwrotu musi być co najmniej dzień po odbiorze — kalendarz zwrotu ma minDate ustawione na dzień po wybranej dacie odbioru, a przycisk zwrotu jest nieaktywny, dopóki nie wybrano odbioru. Cena: 35 zł za dzień, od 7 dni — 25 zł za dzień.

pokaż wskazówkę
  • Datę odbioru trzymaj w var odbior: Calendar?. Przy otwieraniu kalendarza zwrotu skopiuj ją: val min = odbior.clone() as Calendar, dodaj 1 dzień i ustaw jako minDate. Zmiana daty odbioru kasuje datę zwrotu.
ZAD 6Planer sprawdzianów★★★

Pole z nazwą przedmiotu, wybór daty i przycisk Dodaj. Sprawdziany dopisują się do listy w jednym TextView w kolejności dat (najbliższy na górze), np. „wt. 17.11 — matematyka (za 5 dni)”. Reguła szkolna: najwyżej 3 sprawdziany w tygodniu i najwyżej 1 dziennie — przy naruszeniu reguły sprawdzian się nie dodaje, a pojawia się komunikat.

pokaż wskazówkę
  • Trzymaj sprawdziany w dwóch listach mutableListOf<Long>() (daty jako timeInMillis) i mutableListOf<String>(). Numer tygodnia: get(Calendar.WEEK_OF_YEAR) (uważaj na przełom roku). Sortowanie: buduj listę par i użyj sortedBy { it.first }.
PODSUMOWANIE

Co trzeba zapamiętać

  • Bieżąca data: Calendar.getInstance(); miesiące są liczone od zera.
  • DatePickerDialog i TimePickerDialog tworzysz w kodzie przycisku i pokazujesz przez show().
  • Wybór odbierasz w lambdzie — kod po show() nie czeka na użytkownika.
  • Zakres dat ustawiasz przez datePicker.minDate i maxDate przed show().
  • Wyświetlaj z zerami wiodącymi: String.format("%02d.%02d.%d", d, m + 1, r).

Dokumentacja: Okna wyboru daty i godziny, Calendar.

Postęp zapisuje się w tej przeglądarce.