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

school-it.pl // android + kotlin // rozdział 6 // lekcja 24

AlertDialog z własnym wyglądem

Wbudowane okna mają tytuł, treść, listę i przyciski. A gdy w oknie ma być formularz — login i hasło, ocena w gwiazdkach z komentarzem, wybór koloru suwakami? Wtedy projektujesz własny układ w XML i wkładasz go do okna metodą setView. W tej lekcji zbudujesz okno logowania, które sprawdza dane i nie zamyka się, dopóki nie będą poprawne.

Kotlin XML setView LayoutInflater getButton dismiss 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zaprojektujesz układ okna dialogowego w osobnym pliku XML.
  • Włożysz go do okna metodą setView.
  • Odczytasz dane z pól znajdujących się w oknie.
  • Sprawisz, że przycisk okna sprawdza dane i zamyka okno tylko wtedy, gdy są poprawne.
  • Zamkniesz okno z kodu metodą dismiss().
TEORIA

Układ okna

Zawartość okna to zwykły plik w res/layout — jak układ własnego Toasta w lekcji 21. Tytuł i przyciski nadal daje AlertDialog.Builder, a twój układ wypełnia środek okna. Dlatego w pliku nie umieszczasz tytułu ani przycisków OK / Anuluj.

12:30
Logowanie
Login
Hasło
Zapamiętaj mnie
AnulujZaloguj
szare ramki — z Buildera,
fioletowa — twój układ (setView)
TEORIA

setView

okno z własnym układem
val widok = layoutInflater.inflate(R.layout.dialog_logowanie, null)

AlertDialog.Builder(this)
    .setTitle("Logowanie")
    .setView(widok)                           // twój układ w środku okna
    .setPositiveButton("Zaloguj") { _, _ -> … }
    .setNegativeButton("Anuluj", null)
    .show()

inflate zamienia plik XML na widok — dokładnie jak w lekcji 21. Każde otwarcie okna to nowe wywołanie inflate, więc pola w oknie są za każdym razem puste.

TEORIA

Kontrolki w oknie

Kontrolki z układu okna nie są częścią ekranu aktywności. Zwykłe findViewById ich nie znajdzie — szukasz ich w widoku okna:

pola w oknie
val etLogin = widok.findViewById<EditText>(R.id.etLogin)
val etHaslo = widok.findViewById<EditText>(R.id.etHaslo)

To zmienne lokalne metody, która otwiera okno — nie pola klasy. Okno żyje krótko, a przy każdym otwarciu powstają nowe kontrolki.

TEORIA

Okno, które się nie zamyka

Przyciski ustawione przez setPositiveButton zawsze zamykają okno po kliknięciu — także wtedy, gdy hasło jest błędne. Użytkownik musiałby otwierać okno od nowa i wpisywać wszystko jeszcze raz. Rozwiązanie w trzech krokach:

przycisk, który sam decyduje o zamknięciu
// 1. przycisk z null — Builder go utworzy, ale bez naszej akcji
val okno = AlertDialog.Builder(this)
    .setTitle("Logowanie")
    .setView(widok)
    .setPositiveButton("Zaloguj", null)
    .setNegativeButton("Anuluj", null)
    .show()                                   // show() zwraca gotowe okno

// 2. po show() przyciski już istnieją — podmieniamy listener przycisku „Zaloguj”
okno.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
    if (daneSaPoprawne()) {
        okno.dismiss()                        // 3. zamykamy SAMI — tylko przy poprawnych danych
    } else {
        etHaslo.error = "Błędne hasło"        // okno zostaje otwarte
    }
}
ElementZnaczenie
val okno = ….show()show() zwraca utworzone okno — zapamiętujemy je w zmiennej.
okno.getButton(AlertDialog.BUTTON_POSITIVE)Przycisk pozytywny okna. Są też BUTTON_NEGATIVE i BUTTON_NEUTRAL.
.setOnClickListener { … }Nasz listener zastępuje domyślny — ten, który zamykał okno.
okno.dismiss()Zamyka okno z kodu.

getButton dopiero po show()

Przed show() okno nie ma jeszcze przycisków i getButton zwróci null — aplikacja się zamknie. Kolejność: show(), potem getButton.

PRZYKŁAD

Logowanie do dziennika

Ekran główny pokazuje stan: „Niezalogowany” i przycisk Zaloguj. Przycisk otwiera okno z polami login i hasło oraz polem wyboru „Pokaż hasło”. Błędne dane pokazują komunikat w oknie i okno zostaje otwarte; po trzech błędach okno się zamyka i pojawia się Toast. Poprawne dane (login uczen, hasło android1) zamykają okno, a ekran wita użytkownika.

7:50
Dziennik szkolny
Niezalogowany
Zaloguj
Logowanie
uczen
•••••
Pokaż hasło
Błędny login lub hasło. Pozostało prób: 2
AnulujZaloguj
błędne hasło — okno zostaje otwarte
→poprawne
hasło
7:51
Dziennik szkolny
Witaj, uczen!
zalogowano o 7:51
Wyloguj
okno zamknięte przez dismiss()

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Dziennik, Kotlin, API 24.
  2. W res/layout utwórz plik dialog_logowanie.xml.
  3. Podmień activity_main.xml i MainActivity.kt.
res/layout/dialog_logowanie.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<!-- tylko środek okna: bez tytułu i bez przycisków OK / Anuluj -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingStart="24dp"
    android:paddingTop="8dp"
    android:paddingEnd="24dp">

    <EditText
        android:id="@+id/etLogin"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Login"
        android:inputType="text" />

    <EditText
        android:id="@+id/etHaslo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Hasło"
        android:inputType="textPassword" />

    <CheckBox
        android:id="@+id/cbPokaz"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Pokaż hasło" />

    <TextView
        android:id="@+id/tvBlad"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#B3261E"
        android:textSize="14sp" />

</LinearLayout>
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="Dziennik szkolny"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tvStan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/tvGodzina"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp" />

    <Button
        android:id="@+id/btnLogowanie"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp" />

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

import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import java.util.Calendar

class MainActivity : AppCompatActivity() {

    // ---------- kontrolki ekranu ----------
    private lateinit var tvStan: TextView
    private lateinit var tvGodzina: TextView
    private lateinit var btnLogowanie: Button

    // ---------- stan ----------
    private var zalogowany: String? = null       // null = nikt nie jest zalogowany
    private val poprawnyLogin = "uczen"
    private val poprawneHaslo = "android1"
    private val limitProb = 3

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

    private fun initUI() {
        tvStan = findViewById(R.id.tvStan)
        tvGodzina = findViewById(R.id.tvGodzina)
        btnLogowanie = findViewById(R.id.btnLogowanie)
    }

    private fun ustawZdarzenia() {
        btnLogowanie.setOnClickListener {
            if (zalogowany == null) {
                pokazOknoLogowania()
            } else {
                zalogowany = null
                pokazStan()
            }
        }
    }

    // ---------- okno z własnym układem ----------
    private fun pokazOknoLogowania() {
        // układ okna i jego kontrolki — zmienne LOKALNE, nowe przy każdym otwarciu
        val widok = layoutInflater.inflate(R.layout.dialog_logowanie, null)
        val etLogin = widok.findViewById<EditText>(R.id.etLogin)
        val etHaslo = widok.findViewById<EditText>(R.id.etHaslo)
        val cbPokaz = widok.findViewById<CheckBox>(R.id.cbPokaz)
        val tvBlad = widok.findViewById<TextView>(R.id.tvBlad)

        var bledy = 0

        // pole wyboru w oknie działa jak każde inne
        cbPokaz.setOnCheckedChangeListener { _, pokaz ->
            etHaslo.inputType = if (pokaz) {
                InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
            } else {
                InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
            }
            etHaslo.setSelection(etHaslo.text.length)        // kursor na koniec
        }

        // 1. przycisk „Zaloguj” z null — sami zdecydujemy o zamknięciu
        val okno = AlertDialog.Builder(this)
            .setTitle("Logowanie")
            .setView(widok)
            .setPositiveButton("Zaloguj", null)
            .setNegativeButton("Anuluj", null)
            .setCancelable(false)
            .show()

        // 2. po show() podmieniamy listener przycisku „Zaloguj”
        okno.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
            val login = etLogin.text.toString().trim()
            val haslo = etHaslo.text.toString()

            if (login.isEmpty()) {
                etLogin.error = "Podaj login"
                return@setOnClickListener
            }
            if (haslo.isEmpty()) {
                etHaslo.error = "Podaj hasło"
                return@setOnClickListener
            }

            if (login == poprawnyLogin && haslo == poprawneHaslo) {
                zalogowany = login
                pokazStan()
                okno.dismiss()                               // 3. zamykamy tylko przy sukcesie
                return@setOnClickListener
            }

            bledy++
            if (bledy >= limitProb) {
                okno.dismiss()
                Toast.makeText(this, "Za dużo błędnych prób", Toast.LENGTH_LONG).show()
            } else {
                tvBlad.text = "Błędny login lub hasło. Pozostało prób: ${limitProb - bledy}"
                etHaslo.setText("")
                etHaslo.requestFocus()
            }
        }
    }

    // ---------- stan → ekran ----------
    private fun pokazStan() {
        val uzytkownik = zalogowany
        if (uzytkownik == null) {
            tvStan.text = "Niezalogowany"
            tvStan.setTextColor(Color.GRAY)
            tvGodzina.text = ""
            btnLogowanie.text = "Zaloguj"
        } else {
            val teraz = Calendar.getInstance()
            tvStan.text = "Witaj, $uzytkownik!"
            tvStan.setTextColor(Color.parseColor("#2E7D32"))
            tvGodzina.text = String.format("zalogowano o %d:%02d",
                teraz.get(Calendar.HOUR_OF_DAY), teraz.get(Calendar.MINUTE))
            btnLogowanie.text = "Wyloguj"
        }
    }
}

Co dzieje się po kolei

  • dialog_logowanie.xml zawiera tylko środek okna — tytuł i przyciski daje Builder. Boczny padding 24dp wyrównuje pola z tytułem okna.
  • pokazOknoLogowania() przy każdym wywołaniu tworzy nowy widok, więc pola zawsze startują puste. Kontrolki okna są zmiennymi lokalnymi tej metody.
  • Pole wyboru „Pokaż hasło” ma własny listener — kontrolki w oknie działają tak samo jak na ekranie.
  • „Zaloguj” jest utworzony z null, a po show() dostaje nasz listener. Ten listener sam decyduje: przy błędzie pokazuje komunikat i zostawia okno, przy sukcesie wywołuje okno.dismiss().
  • Licznik bledy jest lokalny dla okna — każde nowe otwarcie daje trzy nowe próby.
  • setCancelable(false): dotknięcie obok nie zgubi wpisanego loginu. „Anuluj” nadal działa — ma domyślne zachowanie.

To tylko ćwiczenie

Hasło wpisane w kod aplikacji każdy może odczytać z pliku APK. Prawdziwe logowanie sprawdza dane na serwerze. Tu ćwiczymy wyłącznie budowę okna.

Sprawdź trzy rzeczy

  1. Wpisz złe hasło dwa razy — okno zostaje, licznik prób maleje. Za trzecim razem okno się zamyka.
  2. Zakomentuj blok okno.getButton(…). Teraz „Zaloguj” zawsze zamyka okno i nic się nie sprawdza.
  3. Przenieś blok getButton przed .show() (zmieniając show() na create()). Aplikacja zamknie się przy otwieraniu okna — przycisków jeszcze nie ma.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
layoutInflater.inflate(R.layout.dialog_…, null)ViewWidok zawartości okna.
.setView(widok)metodaWłasny układ w środku okna.
widok.findViewById<T>(…)metodaKontrolka z układu okna.
val okno = builder.show()AlertDialogPokazuje okno i zwraca je.
builder.create()AlertDialogTworzy okno bez pokazywania.
okno.getButton(AlertDialog.BUTTON_POSITIVE)ButtonPrzycisk okna (po show()).
okno.dismiss()metodaZamyka okno z kodu.
InputType.TYPE_TEXT_VARIATION_PASSWORD / …VISIBLE_PASSWORDstałeHasło ukryte / widoczne.
et.setSelection(pozycja)metodaUstawia kursor w polu.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
findViewById<EditText>(R.id.etLogin) bez widok.Zwraca null — pole jest w oknie, nie na ekranie. Awaria NullPointerException.
okno zamyka się mimo złego hasłaLogika jest w lambdzie setPositiveButton. Użyj null i getButton po show().
getButton przed show()Zwraca null — awaria. Najpierw show().
okno nie zamyka się po poprawnych danychBrak okno.dismiss() w naszym listenerze.
przy drugim otwarciu w polach jest stary tekstWidok utworzony raz i zapamiętany w polu klasy. Twórz go w metodzie otwierającej okno.
The specified child already has a parentTen sam widok podany do drugiego okna. Każde okno potrzebuje nowego inflate.
w układzie okna są przyciski OK/Anuluj i dodatkowo Builder dodaje swojePodwójne przyciski. Przyciski zostaw Builderowi albo usuń setPositiveButton i zamykaj okno własnymi przyciskami z układu.
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 1Oceń lekcję★☆☆

Okno z RatingBar (5 gwiazdek) i polem „Komentarz (opcjonalnie)”. Po kliknięciu Wyślij ekran pokazuje ocenę i komentarz. Okno może się zamknąć zawsze (bez sprawdzania).

pokaż wskazówkę
  • Tu wystarczy zwykła lambda setPositiveButton — odczytaj w niej rb.rating i tekst pola z widoku okna.
ZAD 2Dodaj ucznia★☆☆

Przycisk Dodaj ucznia otwiera okno z polami imię, nazwisko i spinnerem klasy (1TP–5TP). Po zatwierdzeniu uczeń dopisuje się do listy w TextView: „Nowak Anna (3TP)”. Puste imię lub nazwisko — błąd przy polu i okno zostaje.

pokaż wskazówkę
  • Spinner w oknie wypełnij przez android:entries w XML okna. Wzór getButton + dismiss() jak w przykładzie.
ZAD 3Wybór koloru w oknie★★☆

Okno z trzema suwakami RGB (lekcja 14) i prostokątem podglądu, który zmienia się w trakcie przesuwania. Po OK wybrany kolor staje się tłem ekranu. Okno startuje z obecnym kolorem tła.

pokaż wskazówkę
  • Obecny kolor trzymaj w polu jako trzy liczby. Ustaw nimi progress suwaków przed show().
ZAD 4Zmiana hasła★★☆

Okno z polami: obecne hasło, nowe hasło, powtórz nowe. Reguły: obecne musi się zgadzać, nowe ma co najmniej 8 znaków i cyfrę, oba nowe są identyczne, nowe różni się od obecnego. Każdy błąd przy swoim polu, okno zostaje otwarte. Po sukcesie Snackbar „Hasło zmienione”.

pokaż wskazówkę
  • Sprawdzaj po kolei i przerywaj przez return@setOnClickListener przy pierwszym błędzie — jak w lekcji 06.
ZAD 5Okno bez przycisków Buildera★★☆

Okno „Twój wynik” z dużą liczbą punktów, gwiazdkami tylko do odczytu i dwoma własnymi przyciskami w układzie: Zagraj ponownie i Menu. Builder nie dodaje żadnych przycisków. Okno ma zaokrąglone tło z kształtu (lekcja 17).

pokaż wskazówkę
  • Przyciski z układu obsłuż przez widok.findViewById<Button>(…).setOnClickListener { okno.dismiss(); … }. Własne tło okna: okno.window?.setBackgroundDrawableResource(R.drawable.tlo_okno) po show().
ZAD 6Edycja wpisu★★★

Lista trzech wydarzeń (nazwa, data, sala) w trzech kartach. Dotknięcie karty otwiera okno edycji z wypełnionymi polami tego wydarzenia; data wybierana przyciskiem otwierającym DatePickerDialog (okno w oknie). Po OK karta się aktualizuje. Okno ma też przycisk neutralny Usuń, który pyta o potwierdzenie drugim, zwykłym oknem.

pokaż wskazówkę
  • Metoda pokazEdycje(nr: Int) wypełnia pola wartościami z tablic przed show(). Datę wybraną w kalendarzu trzymaj w zmiennej lokalnej okna i zapisz do tablicy dopiero po OK.
PODSUMOWANIE

Co trzeba zapamiętać

  • Środek okna projektujesz w res/layout — bez tytułu i bez przycisków Buildera.
  • inflate przy każdym otwarciu, potem .setView(widok).
  • Kontrolki okna szukasz przez widok.findViewById — to zmienne lokalne metody.
  • Okno sprawdzające dane: setPositiveButton("…", null), val okno = ….show(), potem okno.getButton(…).setOnClickListener i okno.dismiss() przy sukcesie.

Dokumentacja: Okno z własnym układem, AlertDialog (androidx).

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