school-it.pl // android + kotlin // rozdział 8 // lekcja 33
Zwracanie wyniku z ekranu
Do tej pory dane płynęły w jedną stronę: z pierwszego ekranu na drugi. Często jednak drugi ekran służy do wybrania lub zmiany czegoś — i pierwszy ekran musi się dowiedzieć, co wybrano. Zbudujesz profil gracza: ekran edycji zmienia nick i klasę postaci, a po zapisaniu odsyła je na ekran główny. Gdy użytkownik zrezygnuje, ekran główny też się o tym dowie.
Czego się dziś nauczysz
- Zarejestrujesz odbiorcę wyniku:
registerForActivityResult. - Otworzysz drugi ekran przez
launch(intencja)zamiaststartActivity. - Odeślesz dane z drugiego ekranu:
setResult(RESULT_OK, intencja)ifinish(). - Rozróżnisz zapis (
RESULT_OK) od rezygnacji (RESULT_CANCELED). - Połączysz w jednym przykładzie wysyłanie danych (lekcja 31) i odbiór wyniku.
Ekran, który odpowiada
Pomyśl o wysłaniu kolegi do sklepiku: dajesz mu kartkę (intencja z danymi), on idzie, a po powrocie mówi „kupiłem, oto reszta” albo „nie było, nic nie kupiłem”. Pierwszy ekran czeka pod spodem, a gdy drugi się zamknie — dostaje odpowiedź.
„idź i wróć z odpowiedzią”
setResult(RESULT_OK, wynik)
finish()
resultCode i dane
Stary sposób w starych poradnikach
W starszych materiałach znajdziesz startActivityForResult(intencja, 1) i metodę onActivityResult. Ten sposób jest przestarzały (deprecated). W nowych projektach używa się registerForActivityResult — jak w tej lekcji.
Rejestracja odbiorcy wyniku
Najpierw pierwszy ekran mówi: „gdy wrócisz z edycji, wykonaj ten kod”. Taki odbiorca jest polem klasy — musi powstać, zanim ekran wystartuje:
private val edycjaProfilu = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() // „uruchom ekran, oczekuj wyniku”
) { wynik ->
// ten kod wykona się PÓŹNIEJ — gdy ekran edycji się zamknie
if (wynik.resultCode == RESULT_OK) {
val nowyNick = wynik.data?.getStringExtra("nick")
…
}
}
A tak otwierasz ekran edycji — zamiast startActivity wywołujesz launch na polu odbiorcy:
val intencja = Intent(this, EdycjaActivity::class.java)
intencja.putExtra("nick", nick) // obecne dane — do wypełnienia formularza
edycjaProfilu.launch(intencja)
| Fragment | Znaczenie |
|---|---|
registerForActivityResult(…) { wynik -> … } | Rejestruje kod, który odbierze wynik. Zwraca obiekt z metodą launch. |
ActivityResultContracts.StartActivityForResult() | Rodzaj umowy: zwykły ekran z wynikiem w intencji. |
wynik.resultCode | RESULT_OK albo RESULT_CANCELED. |
wynik.data | Intencja z danymi odesłanymi przez drugi ekran; może być null, stąd ?.. |
Odesłanie wyniku
Drugi ekran pakuje odpowiedź do nowej, pustej intencji — nie wskazuje w niej żadnego ekranu, bo wiadomo, dokąd wróci. Potem ustawia wynik i się zamyka:
val wynik = Intent() // pusta intencja — tylko „koperta” na dane
wynik.putExtra("nick", nowyNick)
setResult(RESULT_OK, wynik) // „udało się, oto dane”
finish() // zamknij — wynik trafi do MainActivity
setResult(RESULT_CANCELED) // „nic nie zmieniam”
finish()
Zapis albo rezygnacja
| Co zrobił użytkownik | resultCode | wynik.data |
|---|---|---|
| kliknął Zapisz | RESULT_OK | intencja z nickiem i klasą |
| kliknął Anuluj | RESULT_CANCELED | null |
| użył systemowego Wstecz | RESULT_CANCELED | null |
Wstecz nie wywołuje żadnego naszego kodu, a mimo to pierwszy ekran dostaje odpowiedź: RESULT_CANCELED to wynik domyślny. Dlatego w odbiorcy zawsze sprawdzasz resultCode — nie zakładaj, że użytkownik coś zapisał.
Profil gracza
Ekran główny pokazuje ikonę klasy postaci, nick i klasę. Edytuj profil otwiera edycję z polami wypełnionymi obecnymi danymi. Zapisz sprawdza długość nicku (3–12 znaków) i odsyła nowe dane — ekran główny odświeża się i pokazuje Snackbar. Anuluj lub Wstecz — ekran główny pokazuje Toast „Bez zmian”.
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
ProfilGracza, Kotlin, API 24. - Kreatorem dodaj aktywność
EdycjaActivity. - Podmień zawartość obu układów i obu klas.
registerForActivityResultjest w biblioteceandroidx.activity, którą projekt już ma — nic nie trzeba dopisywać w Gradle.
<?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:gravity="center_horizontal"
android:orientation="vertical"
android:layout_margin="16dp"
android:fitsSystemWindows="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Profil gracza"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvIkona"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textSize="72sp" />
<TextView
android:id="@+id/tvNick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvKlasa"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp" />
<Button
android:id="@+id/btnEdytuj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Edytuj profil" />
</LinearLayout>
package pl.schoolit.profilgracza
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var glownyUklad: View
private lateinit var tvIkona: TextView
private lateinit var tvNick: TextView
private lateinit var tvKlasa: TextView
private lateinit var btnEdytuj: Button
// ---------- dane ----------
private var nick = "Gracz1"
private var klasa = "Wojownik"
// ---------- odbiorca wyniku z EdycjaActivity (MUSI być polem) ----------
private val edycjaProfilu = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { wynik ->
if (wynik.resultCode == RESULT_OK) {
val dane = wynik.data
nick = dane?.getStringExtra("nick") ?: nick // brak danych → zostaw stary
klasa = dane?.getStringExtra("klasa") ?: klasa
pokazProfil()
Snackbar.make(glownyUklad, "Profil zapisany", Snackbar.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Bez zmian", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
btnEdytuj.setOnClickListener { otworzEdycje() }
pokazProfil()
}
private fun initUI() {
glownyUklad = findViewById(R.id.main)
tvIkona = findViewById(R.id.tvIkona)
tvNick = findViewById(R.id.tvNick)
tvKlasa = findViewById(R.id.tvKlasa)
btnEdytuj = findViewById(R.id.btnEdytuj)
}
// ---------- akcje ----------
private fun otworzEdycje() {
val intencja = Intent(this, EdycjaActivity::class.java)
intencja.putExtra("nick", nick) // obecne dane — formularz ma je pokazać
intencja.putExtra("klasa", klasa)
edycjaProfilu.launch(intencja) // zamiast startActivity
}
// ---------- dane → ekran ----------
private fun ikona(): String = when (klasa) {
"Mag" -> "🔮"
"Łucznik" -> "🏹"
else -> "⚔️"
}
private fun pokazProfil() {
tvIkona.text = ikona()
tvNick.text = nick
tvKlasa.text = klasa
}
}
<?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="Edycja profilu"
android:textSize="24sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etNick"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="Nick (3–12 znaków)"
android:inputType="text"
android:maxLength="12" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Klasa postaci"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgKlasa"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/rbWojownik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wojownik" />
<RadioButton
android:id="@+id/rbMag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mag" />
<RadioButton
android:id="@+id/rbLucznik"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Łucznik" />
</RadioGroup>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<Button
android:id="@+id/btnAnuluj"
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="Anuluj" />
<Button
android:id="@+id/btnZapisz"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Zapisz" />
</LinearLayout>
</LinearLayout>
package pl.schoolit.profilgracza
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AppCompatActivity
class EdycjaActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var etNick: EditText
private lateinit var rgKlasa: RadioGroup
private lateinit var btnAnuluj: Button
private lateinit var btnZapisz: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edycja)
initUI()
wypelnijFormularz()
ustawZdarzenia()
}
private fun initUI() {
etNick = findViewById(R.id.etNick)
rgKlasa = findViewById(R.id.rgKlasa)
btnAnuluj = findViewById(R.id.btnAnuluj)
btnZapisz = findViewById(R.id.btnZapisz)
}
// obecne dane z intencji → formularz (lekcja 31)
private fun wypelnijFormularz() {
etNick.setText(intent.getStringExtra("nick") ?: "")
when (intent.getStringExtra("klasa")) {
"Mag" -> rgKlasa.check(R.id.rbMag)
"Łucznik" -> rgKlasa.check(R.id.rbLucznik)
else -> rgKlasa.check(R.id.rbWojownik)
}
}
private fun ustawZdarzenia() {
btnAnuluj.setOnClickListener { anuluj() }
btnZapisz.setOnClickListener { zapisz() }
}
// ---------- akcje ----------
private fun zapisz() {
val nick = etNick.text.toString().trim()
if (nick.length !in 3..12) {
etNick.error = "Nick musi mieć od 3 do 12 znaków"
return // ekran zostaje otwarty
}
val klasa = findViewById<RadioButton>(rgKlasa.checkedRadioButtonId).text.toString()
val wynik = Intent() // pusta „koperta” na odpowiedź
wynik.putExtra("nick", nick)
wynik.putExtra("klasa", klasa)
setResult(RESULT_OK, wynik)
finish()
}
private fun anuluj() {
setResult(RESULT_CANCELED)
finish()
}
}
Co dzieje się po kolei
- Pole
edycjaProfilupowstaje razem z obiektemMainActivity— jeszcze przedonCreate. Jego kod w klamrach czeka na wynik. - Edytuj profil: intencja z obecnym nickiem i klasą, wysłana przez
launch. EdycjaActivitywypełnia formularz danymi z intencji:setTextdla pola icheck(id)dla grupy przycisków.- Zapisz: przy złym nicku — komunikat i
return; ekran nie znika. Przy dobrym — dane w nowej intencji,setResult(RESULT_OK, …),finish(). - Po zamknięciu edycji Android wywołuje kod odbiorcy w
MainActivity: przyRESULT_OKpodmienia pola i odświeża ekran, w przeciwnym razie pokazuje „Bez zmian”.
Sprawdź trzy rzeczy
- Otwórz edycję i wyjdź systemowym Wstecz. Pojawi się „Bez zmian” — mimo że nie napisałeś dla Wstecz ani linii kodu.
- W
zapisz()zakomentujsetResult(RESULT_OK, wynik). Po zapisie też zobaczysz „Bez zmian”: bez ustawienia wyniku zostaje domyślnyRESULT_CANCELED. - Wpisz nick „AB” i kliknij Zapisz. Pole pokaże błąd, a ekran edycji zostanie otwarty —
finish()się nie wykonało.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
registerForActivityResult(kontrakt) { wynik -> … } | metoda | Rejestruje odbiorcę wyniku; wywołuj jako pole klasy. |
ActivityResultContracts.StartActivityForResult() | kontrakt | Otwarcie ekranu z wynikiem w intencji. |
odbiorca.launch(intencja) | metoda | Otwiera ekran i czeka na wynik. |
wynik.resultCode | Int | RESULT_OK lub RESULT_CANCELED. |
wynik.data | Intent? | Dane odesłane przez drugi ekran. |
setResult(RESULT_OK, intencja) | metoda | Ustawia odpowiedź „udało się” z danymi. |
setResult(RESULT_CANCELED) | metoda | Ustawia odpowiedź „rezygnacja” (to też wynik domyślny). |
Intent() | Intent | Pusta intencja — tylko na dane wyniku. |
radioGroup.check(R.id.x) | metoda | Zaznacza przycisk w grupie z kodu. |
editText.setText("…") | metoda | Wpisuje tekst do pola. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
registerForActivityResult wewnątrz setOnClickListener → awaria IllegalStateException | Rejestrować można tylko przed startem ekranu. Zrób z tego pole klasy. |
drugi ekran otwarty przez startActivity — wynik nie przychodzi | Otwieraj przez odbiorca.launch(intencja). |
| zawsze „Bez zmian” | Brak setResult(RESULT_OK, wynik) przed finish(). |
setResult po finish() | Zbyt późno — najpierw setResult, potem finish(). |
wynik.data.getStringExtra(…) → błąd kompilacji | data może być null: wynik.data?.getStringExtra(…). |
dane z edycji czytane z intent w MainActivity | intent to intencja, która otworzyła ekran główny. Wynik jest w wynik.data. |
odpowiedź wysłana przez startActivity(Intent(this, MainActivity::class.java)) | Powstaje nowy ekran główny zamiast powrotu do starego. Odpowiada się przez setResult + finish(). |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Odbiorca wyniku zawsze jako pole klasy. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Ekran główny z przyciskiem Wybierz kolor. Drugi ekran ma cztery przyciski kolorów. Kliknięcie koloru odsyła jego kod (np. "#1565C0"), a ekran główny zmienia nim swoje tło.
pokaż wskazówkę
- Napisz w drugim ekranie metodę
wybierz(kod: String)zsetResultifinish(); w głównym:glownyUklad.setBackgroundColor(Color.parseColor(kod)).
Ekran główny: „Twoje miasto: (nie wybrano)” i przycisk Zmień. Drugi ekran: ListView z 10 miastami. Kliknięcie wiersza odsyła nazwę miasta i zamyka listę.
pokaż wskazówkę
- W
setOnItemClickListener:Intent().putExtra("miasto", miasta[position]),setResult,finish().
Lista notatek (lekcja 26) i przycisk Nowa notatka. Drugi ekran ma pole tytułu i wieloliniowe pole treści. Po zapisie notatka „tytuł — początek treści” trafia na górę listy.
pokaż wskazówkę
- W odbiorcy:
notatki.add(0, …)iadapter.notifyDataSetChanged(). Pole treści:inputType="textMultiLine".
Ekran zamówienia kawy z dwoma przyciskami: Rozmiar (ekran z S/M/L) i Dodatki (ekran z CheckBoxami). Każdy ekran odsyła swój wynik, a podsumowanie pokazuje wszystko razem.
pokaż wskazówkę
- Dwa pola:
wyborRozmiaruiwyborDodatkow— każde z własnymregisterForActivityResulti własnym kodem odbioru.
Rozbuduj przykład: Anuluj po zmianie nicku pyta AlertDialogiem „Porzucić zmiany?”. Bez zmian zamyka od razu.
pokaż wskazówkę
- Zapamiętaj w polu nick otrzymany z intencji i porównaj z obecną treścią pola. W oknie:
setPositiveButton("Porzuć") { _, _ -> anuluj() }.
Rozbuduj przykład z lekcji 32: ekran szczegółów ma przycisk Wypożycz / Zwróć. Po kliknięciu odsyła numer książki i jej nowy stan, a lista na ekranie głównym się aktualizuje (np. „✓ ” przed dostępnymi tytułami).
pokaż wskazówkę
- Wysyłaj numer pozycji razem z obiektem; w odpowiedzi odeślij numer i obiekt ze zmienionym polem (
ksiazka.copy(dostepna = …)). W odbiorcy:ksiazki[nr] = nowa,notifyDataSetChanged().
Co trzeba zapamiętać
- Odbiorca wyniku: pole
registerForActivityResult(StartActivityForResult()) { wynik -> … }. - Otwarcie:
odbiorca.launch(intencja). - Odpowiedź:
setResult(RESULT_OK, Intent().putExtra(…)), potemfinish(). - Rezygnacja i Wstecz dają
RESULT_CANCELED— zawsze sprawdzajresultCode. - Dane odpowiedzi czytasz z
wynik.data, nie zintent.
Dokumentacja: Pobieranie wyniku z aktywności.