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

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.

Kotlin XML registerForActivityResult setResult RESULT_OK RESULT_CANCELED 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zarejestrujesz odbiorcę wyniku: registerForActivityResult.
  • Otworzysz drugi ekran przez launch(intencja) zamiast startActivity.
  • Odeślesz dane z drugiego ekranu: setResult(RESULT_OK, intencja) i finish().
  • Rozróżnisz zapis (RESULT_OK) od rezygnacji (RESULT_CANCELED).
  • Połączysz w jednym przykładzie wysyłanie danych (lekcja 31) i odbiór wyniku.
TEORIA

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

MainActivity
edycjaProfilu.launch(intencja)
„idź i wróć z odpowiedzią”
→
EdycjaActivity
użytkownik zmienia dane
setResult(RESULT_OK, wynik)
finish()
→
MainActivity
kod odbiorcy dostaje
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.

TEORIA

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:

MainActivity — pole z odbiorcą wyniku
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:

otwarcie ekranu z oczekiwaniem na wynik
val intencja = Intent(this, EdycjaActivity::class.java)
intencja.putExtra("nick", nick)              // obecne dane — do wypełnienia formularza
edycjaProfilu.launch(intencja)
FragmentZnaczenie
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.resultCodeRESULT_OK albo RESULT_CANCELED.
wynik.dataIntencja z danymi odesłanymi przez drugi ekran; może być null, stąd ?..
TEORIA

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:

EdycjaActivity — zapis
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
EdycjaActivity — rezygnacja
setResult(RESULT_CANCELED)              // „nic nie zmieniam”
finish()
TEORIA

Zapis albo rezygnacja

Co zrobił użytkownikresultCodewynik.data
kliknął ZapiszRESULT_OKintencja z nickiem i klasą
kliknął AnulujRESULT_CANCELEDnull
użył systemowego WsteczRESULT_CANCELEDnull

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

PRZYKŁAD

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

16:20
Profil gracza
⚔️
Gracz1
Wojownik
Edytuj profil
start — dane domyślne
→launch
16:21
Edycja profilu
Nick
Czarodziej_Ola
Klasa postaci
Wojownik
Mag
Łucznik
Anuluj
Zapisz
EdycjaActivity — nowe dane
→RESULT_OK
16:21
Profil gracza
🔮
Czarodziej_Ola
Mag
Edytuj profil
Profil zapisany
po powrocie — wynik odebrany

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa ProfilGracza, Kotlin, API 24.
  2. Kreatorem dodaj aktywność EdycjaActivity.
  3. Podmień zawartość obu układów i obu klas. registerForActivityResult jest w bibliotece androidx.activity, którą projekt już ma — nic nie trzeba dopisywać w Gradle.
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: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>
MainActivity.kt — pełny plik
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
    }
}
res/layout/activity_edycja.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="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>
EdycjaActivity.kt — pełny plik
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 edycjaProfilu powstaje razem z obiektem MainActivity — jeszcze przed onCreate. Jego kod w klamrach czeka na wynik.
  • Edytuj profil: intencja z obecnym nickiem i klasą, wysłana przez launch.
  • EdycjaActivity wypełnia formularz danymi z intencji: setText dla pola i check(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: przy RESULT_OK podmienia pola i odświeża ekran, w przeciwnym razie pokazuje „Bez zmian”.

Sprawdź trzy rzeczy

  1. Otwórz edycję i wyjdź systemowym Wstecz. Pojawi się „Bez zmian” — mimo że nie napisałeś dla Wstecz ani linii kodu.
  2. W zapisz() zakomentuj setResult(RESULT_OK, wynik). Po zapisie też zobaczysz „Bez zmian”: bez ustawienia wyniku zostaje domyślny RESULT_CANCELED.
  3. Wpisz nick „AB” i kliknij Zapisz. Pole pokaże błąd, a ekran edycji zostanie otwarty — finish() się nie wykonało.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
registerForActivityResult(kontrakt) { wynik -> … }metodaRejestruje odbiorcę wyniku; wywołuj jako pole klasy.
ActivityResultContracts.StartActivityForResult()kontraktOtwarcie ekranu z wynikiem w intencji.
odbiorca.launch(intencja)metodaOtwiera ekran i czeka na wynik.
wynik.resultCodeIntRESULT_OK lub RESULT_CANCELED.
wynik.dataIntent?Dane odesłane przez drugi ekran.
setResult(RESULT_OK, intencja)metodaUstawia odpowiedź „udało się” z danymi.
setResult(RESULT_CANCELED)metodaUstawia odpowiedź „rezygnacja” (to też wynik domyślny).
Intent()IntentPusta intencja — tylko na dane wyniku.
radioGroup.check(R.id.x)metodaZaznacza przycisk w grupie z kodu.
editText.setText("…")metodaWpisuje tekst do pola.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
registerForActivityResult wewnątrz setOnClickListener → awaria IllegalStateExceptionRejestrować można tylko przed startem ekranu. Zrób z tego pole klasy.
drugi ekran otwarty przez startActivity — wynik nie przychodziOtwieraj 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 kompilacjidata może być null: wynik.data?.getStringExtra(…).
dane z edycji czytane z intent w MainActivityintent 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

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.

ZAD 1Wybór koloru tła★☆☆

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) z setResult i finish(); w głównym: glownyUklad.setBackgroundColor(Color.parseColor(kod)).
ZAD 2Wybór z listy★☆☆

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().
ZAD 3Dodawanie do listy przez osobny ekran★★☆

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, …) i adapter.notifyDataSetChanged(). Pole treści: inputType="textMultiLine".
ZAD 4Dwa różne ekrany wyniku★★☆

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: wyborRozmiaru i wyborDodatkow — każde z własnym registerForActivityResult i własnym kodem odbioru.
ZAD 5Edycja z pytaniem o porzucenie★★☆

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() }.
ZAD 6Biblioteka z wypożyczaniem★★★

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

Co trzeba zapamiętać

  • Odbiorca wyniku: pole registerForActivityResult(StartActivityForResult()) { wynik -> … }.
  • Otwarcie: odbiorca.launch(intencja).
  • Odpowiedź: setResult(RESULT_OK, Intent().putExtra(…)), potem finish().
  • Rezygnacja i Wstecz dają RESULT_CANCELED — zawsze sprawdzaj resultCode.
  • Dane odpowiedzi czytasz z wynik.data, nie z intent.

Dokumentacja: Pobieranie wyniku z aktywności.