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.
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.
Dzisiejsza data: Calendar
Okno wyboru daty musi wiedzieć, od której daty zacząć. Dzisiejszą datę daje klasa Calendar z pakietu java.util:
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.
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ć:
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.
wygląd okien zależy od wersji Androida; dni przed dzisiejszą datą są wyszarzone dzięki minDate
Okno wyboru godziny
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.
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”:
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.
Sprawdzanie daty
Wybraną datę zapisujemy w nowym obiekcie Calendar. Wtedy łatwo odczytać dzień tygodnia albo porównać 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
| Zapis | Znaczenie |
|---|---|
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.timeInMillis | Data jako liczba milisekund od 1.01.1970 — wygodna do porównań i odejmowania. |
okno.datePicker.minDate / maxDate | Najwcześniejsza / najpóźniejsza data dostępna w kalendarzu. |
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.
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
RezerwacjaSali, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Okna wyboru nie wymagają żadnego wpisu w XML.
<?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>
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 starcienull) oraz godzina i minuta (na starcie-1, czyli „nie wybrano”). - W oknie daty
minDateimaxDateograniczają 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
Calendarz 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..13sprawdza zakres.- W
sprawdz()data jest najpierw kopiowana doval data— po sprawdzeniunullKotlin pozwala na niej pracować. PrzybtnRezerwujrobi to krócej operator?:: „jeślinull, wyjdź”.
Sprawdź trzy rzeczy
- Otwórz kalendarz i spróbuj wybrać wczorajszą datę lub datę za trzy miesiące. Są wyszarzone.
- Usuń
+ 1przy miesiącu wtvData.text. Listopad pokaże się jako 10 — tak wygląda najczęstszy błąd z datami. - Wybierz godzinę 14:30. Pojawi się komunikat o godzinach pracy, choć 14:00 jest jeszcze poprawne.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
Calendar.getInstance() | Calendar | Obiekt z bieżącą datą i godziną. |
kal.get(Calendar.YEAR / MONTH / DAY_OF_MONTH) | Int | Rok, miesiąc (0–11), dzień. |
kal.get(Calendar.HOUR_OF_DAY / MINUTE) | Int | Godzina 0–23, minuta. |
kal.get(Calendar.DAY_OF_WEEK) | Int | 1 = niedziela … 7 = sobota. |
kal.set(r, m, d), kal.add(pole, ile) | metody | Ustawienie daty, przesunięcie o dni/miesiące. |
kal.timeInMillis | Long | Data jako milisekundy — do porównań. |
DatePickerDialog(this, { _, r, m, d -> }, r, m, d) | okno | Kalendarz; lambda po OK. |
okno.datePicker.minDate / maxDate | Long | Zakres dostępnych dat. |
TimePickerDialog(this, { _, h, min -> }, h, min, true) | okno | Zegar; true = 24 h. |
okno.show() | metoda | Pokazuje okno. |
String.format("%02d", x) | String | Liczba z zerem wiodącym. |
x in 8..13 | Boolean | Czy liczba jest w zakresie. |
wartosc ?: return@… | operator | Gdy null — wyjdź z klamer. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| wybrano 14 listopada, a wyświetla się 14.10 | Miesią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 pokazuje | Brak .show(). |
kod po okno.show() nie widzi wybranej daty | show() 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 jeden | Tablica zaczyna się od poniedziałku, a Calendar numeruje od niedzieli. Ułóż tablicę od niedzieli i odejmij 1. |
import java.sql.Date / android.icu.util.Calendar | Android 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
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
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.
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.
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ą przeztimeInMillis.
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 potemset(Calendar.DAY_OF_MONTH, 1)iadd(Calendar.DAY_OF_MONTH, -1).
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 jakominDate. Zmiana daty odbioru kasuje datę zwrotu.
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 jakotimeInMillis) imutableListOf<String>(). Numer tygodnia:get(Calendar.WEEK_OF_YEAR)(uważaj na przełom roku). Sortowanie: buduj listę par i użyjsortedBy { it.first }.
Co trzeba zapamiętać
- Bieżąca data:
Calendar.getInstance(); miesiące są liczone od zera. DatePickerDialogiTimePickerDialogtworzysz w kodzie przycisku i pokazujesz przezshow().- Wybór odbierasz w lambdzie — kod po
show()nie czeka na użytkownika. - Zakres dat ustawiasz przez
datePicker.minDateimaxDateprzedshow(). - Wyświetlaj z zerami wiodącymi:
String.format("%02d.%02d.%d", d, m + 1, r).
Dokumentacja: Okna wyboru daty i godziny, Calendar.