school-it.pl // android + kotlin // rozdział 6 // lekcja 22
AlertDialog — okno wbudowane
„Usunąć notatkę?”, „Zapisać zmiany przed wyjściem?”, „Czy na pewno chcesz się wylogować?” — gdy aplikacja potrzebuje decyzji użytkownika, pokazuje okno dialogowe. Przyciemnia ekran, przerywa pracę i czeka, aż użytkownik wybierze odpowiedź. W tej lekcji zbudujesz okna z jednym, dwoma i trzema przyciskami i poznasz najważniejszą pułapkę: kod po show() wcale nie czeka na odpowiedź.
Czego się dziś nauczysz
- Zbudujesz okno z tytułem, treścią i ikoną.
- Dodasz przycisk pozytywny, negatywny i neutralny i obsłużysz każdy z nich.
- Umieścisz kod zależny od decyzji tam, gdzie trzeba — w lambdzie przycisku.
- Zablokujesz zamknięcie okna przyciskiem Wstecz, gdy wybór jest konieczny.
- Ocenisz, kiedy okno dialogowe jest potrzebne, a kiedy przeszkadza.
Budowanie okna
Okno buduje się „klockami” obiektem AlertDialog.Builder (budowniczy). Każda metoda dokłada jeden element i zwraca budowniczego, więc wywołania łączy się w łańcuch. Na końcu show() tworzy i pokazuje okno:
AlertDialog.Builder(this)
.setTitle("Usunąć notatkę?")
.setMessage("Tej operacji nie można cofnąć.")
.setIcon(R.drawable.ic_usun)
.setPositiveButton("Usuń") { _, _ ->
usunNotatke() // tylko po kliknięciu „Usuń”
}
.setNegativeButton("Anuluj", null) // null = po prostu zamknij okno
.show()
Który AlertDialog?
Klasa AlertDialog jest w dwóch pakietach. Importuj androidx.appcompat.app.AlertDialog — okno dopasuje się do motywu aplikacji. Wersja android.app.AlertDialog wygląda inaczej na różnych telefonach. Biblioteka Material ma też MaterialAlertDialogBuilder — używa się go identycznie, a okno ma wygląd Material 3.
ekran pod oknem jest przyciemniony i nie reaguje na dotyk
Trzy przyciski
| Metoda | Miejsce | Znaczenie |
|---|---|---|
setPositiveButton | prawy | Główna akcja: „Usuń”, „Zapisz”, „Wyloguj”. |
setNegativeButton | obok pozytywnego | Rezygnacja: „Anuluj”, „Nie zapisuj”. |
setNeutralButton | lewy | Trzecia możliwość: „Później”, „Przypomnij jutro”. |
Każdy przycisk może dostać lambdę { _, _ -> … } — dwa parametry to okno i numer przycisku, zwykle niepotrzebne. Zamiast lambdy można podać null — wtedy kliknięcie tylko zamyka okno. Okno zamyka się samo po kliknięciu dowolnego z tych przycisków.
Napis na przycisku to czasownik opisujący skutek: „Usuń”, „Zapisz”, „Wyjdź bez zapisu”. Unikaj „Tak” / „Nie” — przy pytaniu „Anulować zamówienie?” odpowiedź „Nie” jest myląca.
show() nie czeka
To najważniejsza pułapka okien dialogowych. show() tylko wyświetla okno i natychmiast wraca. Kolejne linie po show() wykonają się od razu — zanim użytkownik cokolwiek kliknie:
AlertDialog.Builder(this)
.setTitle("Usunąć notatkę?")
.setPositiveButton("Usuń", null)
.setNegativeButton("Anuluj", null)
.show()
etNotatka.setText("") // wykona się OD RAZU, przed kliknięciem
Wszystko, co zależy od decyzji, musi być w lambdzie przycisku. Tak samo było z setOnClickListener (lekcja 05) i oknem wyboru daty (lekcja 13): kod w klamrach wykonuje się później, gdy zajdzie zdarzenie.
Zamknięcie bez wyboru
Użytkownik może zamknąć okno, nie klikając żadnego przycisku: systemowym przyciskiem Wstecz albo dotykając ekranu poza oknem. Wtedy nie wykona się żadna lambda przycisku — tak, jakby wybrał „Anuluj”.
| Metoda | Efekt |
|---|---|
.setCancelable(false) | Wstecz i dotyk obok nie zamykają okna — trzeba kliknąć przycisk. Używaj tylko, gdy decyzja jest naprawdę konieczna. |
.setOnCancelListener { … } | Kod wykonywany, gdy okno zamknięto bez wyboru. |
Dobre okno dialogowe
- Tylko gdy trzeba. Pytanie „Czy na pewno?” przed każdą drobnostką sprawia, że użytkownik klika „Tak” bez czytania. Pytaj o rzeczy nieodwracalne albo kosztowne.
- Tytuł to pytanie: „Usunąć 3 zdjęcia?”, a nie „Uwaga”.
- Treść mówi o skutkach: „Zdjęcia zostaną usunięte z telefonu.”
- Przyciski to czasowniki: „Usuń” / „Anuluj”.
- Nic do zrobienia — bez okna. Gdy notatka jest pusta, nie pytaj o jej usunięcie.
Notatnik z oknami dialogowymi
Notatnik z jedną notatką. Zapisz zapamiętuje tekst. Usuń pyta o potwierdzenie (dwa przyciski). Zamknij zamyka aplikację — ale gdy są niezapisane zmiany, pyta, co z nimi zrobić (trzy przyciski, bez możliwości zamknięcia Wstecz). O aplikacji pokazuje okno informacyjne z jednym przyciskiem. Pod polem widać, czy są niezapisane zmiany.
przy braku zmian Zamknij od razu zamyka aplikację — bez pytania
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Notatnik, Kotlin, API 24. - Utwórz ikony przez Vector Asset:
ic_usun(delete) iic_info(info). - Podmień
activity_main.xmliMainActivity.kt.
<?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="Notatnik"
android:textSize="24sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etNotatka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:hint="Wpisz notatkę…"
android:inputType="textMultiLine|textCapSentences"
android:minLines="5" />
<TextView
android:id="@+id/tvStan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<Button
android:id="@+id/btnZapisz"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Zapisz" />
<Button
android:id="@+id/btnUsun"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Usuń" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btnZamknij"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Zamknij" />
<Button
android:id="@+id/btnInfo"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="O aplikacji" />
</LinearLayout>
</LinearLayout>
package pl.schoolit.notatnik
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var etNotatka: EditText
private lateinit var tvStan: TextView
private lateinit var btnZapisz: Button
private lateinit var btnUsun: Button
private lateinit var btnZamknij: Button
private lateinit var btnInfo: Button
// ---------- stan ----------
private var zapisanyTekst = "" // ostatnio zapisana wersja notatki
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
ustawZdarzenia()
pokazStan()
}
private fun initUI() {
etNotatka = findViewById(R.id.etNotatka)
tvStan = findViewById(R.id.tvStan)
btnZapisz = findViewById(R.id.btnZapisz)
btnUsun = findViewById(R.id.btnUsun)
btnZamknij = findViewById(R.id.btnZamknij)
btnInfo = findViewById(R.id.btnInfo)
}
private fun ustawZdarzenia() {
etNotatka.addTextChangedListener { pokazStan() } // każda zmiana tekstu
btnZapisz.setOnClickListener { zapisz() }
btnUsun.setOnClickListener { zapytajOUsuniecie() }
btnZamknij.setOnClickListener { zamknij() }
btnInfo.setOnClickListener { pokazInformacje() }
}
private fun saZmiany(): Boolean {
return etNotatka.text.toString() != zapisanyTekst
}
// ---------- czynności ----------
private fun zapisz() {
zapisanyTekst = etNotatka.text.toString()
pokazStan()
Toast.makeText(this, "Zapisano", Toast.LENGTH_SHORT).show()
}
// okno z dwoma przyciskami
private fun zapytajOUsuniecie() {
if (etNotatka.text.isEmpty()) {
return // nie ma czego usuwać — nie pytamy
}
AlertDialog.Builder(this)
.setTitle("Usunąć notatkę?")
.setMessage("Tekst notatki zostanie skasowany. Tej operacji nie można cofnąć.")
.setIcon(R.drawable.ic_usun)
.setPositiveButton("Usuń") { _, _ ->
etNotatka.setText("")
zapisanyTekst = ""
pokazStan()
}
.setNegativeButton("Anuluj", null)
.show()
}
// okno z trzema przyciskami — tylko gdy są zmiany
private fun zamknij() {
if (!saZmiany()) {
finish() // nic do stracenia — zamykamy od razu
return
}
AlertDialog.Builder(this)
.setTitle("Zapisać zmiany?")
.setMessage("Notatka ma niezapisane zmiany. Jeśli wyjdziesz bez zapisu, zostaną utracone.")
.setPositiveButton("Zapisz") { _, _ ->
zapisz()
finish()
}
.setNegativeButton("Nie zapisuj") { _, _ ->
finish()
}
.setNeutralButton("Anuluj", null)
.setCancelable(false) // trzeba wybrać jeden z przycisków
.show()
}
// okno informacyjne z jednym przyciskiem
private fun pokazInformacje() {
AlertDialog.Builder(this)
.setTitle("O aplikacji")
.setIcon(R.drawable.ic_info)
.setMessage("Notatnik 1.0\nPrzykład z lekcji 22 kursu Androida.\nschool-it.pl")
.setPositiveButton("OK", null)
.show()
}
// ---------- stan → ekran ----------
private fun pokazStan() {
if (saZmiany()) {
tvStan.text = "● niezapisane zmiany"
tvStan.setTextColor(Color.parseColor("#C62828"))
} else {
tvStan.text = "✓ zapisano"
tvStan.setTextColor(Color.parseColor("#2E7D32"))
}
btnZapisz.isEnabled = saZmiany()
}
}
Co dzieje się po kolei
zapisanyTekstpamięta ostatnio zapisaną wersję. MetodasaZmiany()porównuje ją z tekstem w polu — to podstawa decyzji, czy pytać przy zamykaniu.addTextChangedListener { … }uruchamia kod po każdej zmianie tekstu w polu — stan „niezapisane zmiany” odświeża się na bieżąco.- Usuwanie: pusta notatka — bez okna. W przeciwnym razie okno z dwoma przyciskami, a kasowanie jest wyłącznie w lambdzie „Usuń”.
- Zamykanie: bez zmian —
finish()od razu. Ze zmianami — trzy przyciski isetCancelable(false), bo przypadkowe dotknięcie obok okna nie powinno decydować o losie notatki. finish()zamyka bieżący ekran — tu jedyny, więc całą aplikację.- Okno informacyjne ma jeden przycisk z
null— tylko zamyka okno. W treści\nrozbija tekst na linie.
Sprawdź trzy rzeczy
- Wpisz tekst, kliknij Zamknij i spróbuj zamknąć okno przyciskiem Wstecz. Nie da się. W oknie usuwania — da się.
- Przenieś
etNotatka.setText("")z lambdy „Usuń” za.show(). Notatka zniknie, zanim cokolwiek klikniesz. Przywróć. - Zapisz notatkę i kliknij Zamknij. Aplikacja zamknie się bez pytania — nie ma czego tracić.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
AlertDialog.Builder(this) | budowniczy | Rozpoczyna budowę okna (androidx.appcompat.app). |
.setTitle, .setMessage, .setIcon | metody | Tytuł, treść, ikona. |
.setPositiveButton("…") { _, _ -> } | metoda | Główna akcja. |
.setNegativeButton("…", null) | metoda | Rezygnacja; null — tylko zamknij. |
.setNeutralButton(…) | metoda | Trzecia możliwość, po lewej. |
.setCancelable(false) | metoda | Wstecz i dotyk obok nie zamykają. |
.setOnCancelListener { } | metoda | Kod po zamknięciu bez wyboru. |
.show() | metoda | Pokazuje okno — i nie czeka. |
finish() | metoda | Zamyka bieżący ekran. |
et.addTextChangedListener { } | metoda | Kod po każdej zmianie tekstu w polu. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| okno się nie pokazuje | Brak .show() na końcu łańcucha. |
| akcja wykonuje się przed decyzją | Kod jest po show() zamiast w lambdzie przycisku. |
| okno wygląda inaczej niż reszta aplikacji | Import android.app.AlertDialog. Zmień na androidx.appcompat.app.AlertDialog. |
| Wstecz zamyka okno i nic się nie dzieje | To poprawne zachowanie — jak „Anuluj”. Gdy decyzja jest wymagana: setCancelable(false). |
dwa setPositiveButton w jednym oknie | Drugi zastępuje pierwszy. Każdy rodzaj przycisku może być tylko raz. |
| błąd kompilacji w lambdzie: Expected 2 parameters | Lambda przycisku ma dwa parametry: { _, _ -> … }. |
| okno „Czy na pewno?” przy każdym kliknięciu | Użytkownicy przestają czytać. Pytaj tylko o rzeczy nieodwracalne. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity, w układzie z lekcji 19. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Ekran „Witaj, Anna!” z przyciskiem Wyloguj. Okno: tytuł „Wylogować się?”, przyciski „Wyloguj” i „Anuluj”. Po wylogowaniu napis zmienia się na „Wylogowano”, a przycisk na „Zaloguj”.
pokaż wskazówkę
- Stan w polu
private var zalogowany = true; ten sam przycisk robi dwie rzeczy zależnie od stanu.
Po starcie aplikacji pojawia się okno „Oceń aplikację” z trzema przyciskami: „Oceń” (napis „Dziękujemy!”), „Nie, dziękuję” (napis „Nie będziemy pytać”), „Później” (okno pokaże się ponownie po 10 sekundach).
pokaż wskazówkę
- Pokazanie okna wydziel do metody i wywołaj ją w
onCreate. „Później” uruchamiaCountDownTimer, który wonFinishwywołuje tę samą metodę.
Przerób licznik z lekcji 19 tak, aby zerowanie (przytrzymanie) pokazywało okno „Wyzerować licznik? Stracisz 7 osób.”. Gdy licznik jest na zero, okno się nie pojawia. Okno nie może się zamknąć przez dotknięcie obok.
pokaż wskazówkę
- Liczba osób w treści:
.setMessage("Stracisz $osoby osób."). Zerowanie przenieś do lambdy przycisku.
Lista zadań z lekcji 21: usuwanie zadania zaznaczonego jako zrobione działa od razu (ze Snackbarem), a usuwanie niezrobionego najpierw pyta: „Usunąć niezrobione zadanie «Trening piłki»?”.
pokaż wskazówkę
- Wydziel metodę
usunNaPewno(nr)i wywołuj ją albo od razu, albo z lambdy okna.
Rozbuduj notatnik tak, aby systemowy przycisk Wstecz (lub gest powrotu) działał jak Zamknij: przy niezapisanych zmianach pokazuje okno z trzema przyciskami.
pokaż wskazówkę
- W
onCreate:onBackPressedDispatcher.addCallback(this) { zamknij() }(importandroidx.activity.addCallback).
Zakup biletu w trzech kolejnych oknach: „Kupić bilet normalny za 4 zł?” → „Kontynuuj” otwiera okno „Dodać ubezpieczenie za 1 zł?” (Dodaj / Pomiń) → okno podsumowania „Do zapłaty: 5 zł” (Zapłać / Anuluj). Anulowanie na którymkolwiek etapie przerywa zakup. Na ekranie zostaje historia zakupów.
pokaż wskazówkę
- Każde okno w osobnej metodzie; lambda przycisku wywołuje metodę następnego okna. Wybór z etapu 2 przekazuj parametrem:
pokazPodsumowanie(zUbezpieczeniem: Boolean).
Co trzeba zapamiętać
AlertDialog.Builder(this).setTitle(…).setMessage(…).setPositiveButton(…).show()— import zandroidx.appcompat.app.- Trzy przyciski: pozytywny (akcja), negatywny (rezygnacja), neutralny (trzecia opcja, po lewej).
show()nie czeka — kod zależny od decyzji umieszczaj w lambdzie przycisku.- Wstecz i dotyk obok zamykają okno jak „Anuluj”;
setCancelable(false)tylko gdy decyzja jest konieczna. - Pytaj tylko o rzeczy nieodwracalne; tytuł to pytanie, przyciski to czasowniki.
Dokumentacja: Okna dialogowe, Material 3 — Dialogs.