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

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ź.

Kotlin AlertDialog.Builder setPositiveButton setNegativeButton setNeutralButton setCancelable 2 × 45 min
CEL LEKCJI

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.
TEORIA

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:

okno z pytaniem
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.

12:30
Notatnik
Kupić zeszyt
Usunąć notatkę?
Tej operacji nie można cofnąć.
AnulujUsuń
ikona, tytuł, treść, dwa przyciski

ekran pod oknem jest przyciemniony i nie reaguje na dotyk

TEORIA

Trzy przyciski

MetodaMiejsceZnaczenie
setPositiveButtonprawyGłówna akcja: „Usuń”, „Zapisz”, „Wyloguj”.
setNegativeButtonobok pozytywnegoRezygnacja: „Anuluj”, „Nie zapisuj”.
setNeutralButtonlewyTrzecia 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.

TEORIA

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:

ŹLE — notatka znika, zanim użytkownik zdecyduje
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.

TEORIA

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”.

MetodaEfekt
.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.
TEORIA

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.
PRZYKŁAD

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.

12:30
Notatnik
Jutro: oddać projekt z Androida, zabrać strój na WF
● niezapisane zmiany
Zapisz
Usuń
Zamknij
O aplikacji
tekst zmieniony, użytkownik zamyka
→Zamknij
12:30
Zapisać zmiany?
Notatka ma niezapisane zmiany. Jeśli wyjdziesz bez zapisu, zostaną utracone.
AnulujNie zapisujZapisz
trzy przyciski; neutralny po lewej

przy braku zmian Zamknij od razu zamyka aplikację — bez pytania

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Notatnik, Kotlin, API 24.
  2. Utwórz ikony przez Vector Asset: ic_usun (delete) i ic_info (info).
  3. Podmień activity_main.xml i MainActivity.kt.
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="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>
MainActivity.kt — pełny plik
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

  • zapisanyTekst pamięta ostatnio zapisaną wersję. Metoda saZmiany() 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 i setCancelable(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 \n rozbija tekst na linie.

Sprawdź trzy rzeczy

  1. Wpisz tekst, kliknij Zamknij i spróbuj zamknąć okno przyciskiem Wstecz. Nie da się. W oknie usuwania — da się.
  2. Przenieś etNotatka.setText("") z lambdy „Usuń” za .show(). Notatka zniknie, zanim cokolwiek klikniesz. Przywróć.
  3. Zapisz notatkę i kliknij Zamknij. Aplikacja zamknie się bez pytania — nie ma czego tracić.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
AlertDialog.Builder(this)budowniczyRozpoczyna budowę okna (androidx.appcompat.app).
.setTitle, .setMessage, .setIconmetodyTytuł, treść, ikona.
.setPositiveButton("…") { _, _ -> }metodaGłówna akcja.
.setNegativeButton("…", null)metodaRezygnacja; null — tylko zamknij.
.setNeutralButton(…)metodaTrzecia możliwość, po lewej.
.setCancelable(false)metodaWstecz i dotyk obok nie zamykają.
.setOnCancelListener { }metodaKod po zamknięciu bez wyboru.
.show()metodaPokazuje okno — i nie czeka.
finish()metodaZamyka bieżący ekran.
et.addTextChangedListener { }metodaKod po każdej zmianie tekstu w polu.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
okno się nie pokazujeBrak .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 aplikacjiImport android.app.AlertDialog. Zmień na androidx.appcompat.app.AlertDialog.
Wstecz zamyka okno i nic się nie dziejeTo poprawne zachowanie — jak „Anuluj”. Gdy decyzja jest wymagana: setCancelable(false).
dwa setPositiveButton w jednym oknieDrugi zastępuje pierwszy. Każdy rodzaj przycisku może być tylko raz.
błąd kompilacji w lambdzie: Expected 2 parametersLambda przycisku ma dwa parametry: { _, _ -> … }.
okno „Czy na pewno?” przy każdym kliknięciuUżytkownicy przestają czytać. Pytaj tylko o rzeczy nieodwracalne.
ZADANIA

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.

ZAD 1Wylogowanie★☆☆

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.
ZAD 2Przypomnienie z „Później”★☆☆

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” uruchamia CountDownTimer, który w onFinish wywołuje tę samą metodę.
ZAD 3Reset gry★★☆

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.
ZAD 4Kosz z potwierdzeniem★★☆

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.
ZAD 5Wstecz z pytaniem★★☆

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() } (import androidx.activity.addCallback).
ZAD 6Kreator w oknach★★★

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).
PODSUMOWANIE

Co trzeba zapamiętać

  • AlertDialog.Builder(this).setTitle(…).setMessage(…).setPositiveButton(…).show() — import z androidx.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.

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