school-it.pl // android + kotlin // rozdział 8 // lekcja 32
Intencja z przekazaniem obiektu
W poprzedniej lekcji drugi ekran dostawał pojedyncze wartości: napis, liczbę, true albo false. Gdy jednak przekazujesz książkę, ucznia czy produkt, takich wartości jest kilka, a każda wymaga osobnego putExtra i osobnego odczytu. Prościej wysłać cały obiekt naraz. W tej lekcji spakujesz obiekt własnej klasy do intencji, odbierzesz go na drugim ekranie i poznasz dwa sposoby pakowania: Serializable i Parcelable.
Czego się dziś nauczysz
- Napiszesz klasę danych, którą da się przesłać intencją (
Serializable). - Wyślesz obiekt na drugi ekran jednym
putExtra. - Odbierzesz obiekt i bezpiecznie obsłużysz sytuację, gdy go brakuje.
- Zrozumiesz, dlaczego drugi ekran dostaje kopię, a nie oryginał.
- Zamienisz
Serializablena szybszyParcelablez adnotacją@Parcelize.
Po co przekazywać cały obiekt
Książka ma tytuł, autora, rok wydania i informację, czy jest dostępna. Metodą z lekcji 31 trzeba by wysłać cztery wartości i cztery odczytać — a każda literówka w kluczu to puste pole na drugim ekranie:
intencja.putExtra("tytul", ksiazka.tytul)
intencja.putExtra("autor", ksiazka.autor)
intencja.putExtra("rok", ksiazka.rok)
intencja.putExtra("dostepna", ksiazka.dostepna)
intencja.putExtra("ksiazka", ksiazka)
Gdy klasa dostanie nowe pole, na przykład liczbę stron, pierwszy sposób wymaga zmian w dwóch aktywnościach. Drugi — tylko w klasie i w miejscu, gdzie pole jest wyświetlane.
Serializable — obiekt w paczce
Ekrany aplikacji nie mogą sobie po prostu „podać” obiektu z ręki do ręki. Intencja przechodzi przez system Android, a system przenosi tylko bajty. Obiekt trzeba więc rozłożyć na bajty, przesłać i złożyć z powrotem po drugiej stronie. Działa to jak przeprowadzka szafy: rozkręcasz ją na deski, przewozisz i skręcasz w nowym mieszkaniu.
putExtra(„ksiazka”, …)
na bajty
złożony z bajtów
Rozkładanie na bajty nazywa się serializacją. Klasa zgadza się na nią, gdy po dwukropku dopiszesz interfejs Serializable z Javy. Nic więcej nie trzeba — resztę robi Java:
import java.io.Serializable
data class Ksiazka(
val tytul: String,
val autor: String,
val rok: Int,
val dostepna: Boolean
) : Serializable
data class to klasa przeznaczona do przechowywania danych — Kotlin sam dopisuje jej metody porównywania i kopiowania. Wszystkie jej pola też muszą dać się serializować. String, Int, Double, Boolean i listy takich wartości — tak. Kontrolki (TextView, Button) i obrazki Bitmap — nie.
Odbieranie obiektu
Drugi ekran wyjmuje obiekt metodą getSerializableExtra. Ta metoda nie wie, jakiej klasy obiekt był w paczce — zwraca ogólny typ Serializable. Trzeba więc powiedzieć Kotlinowi „to jest Ksiazka” operatorem as?:
@Suppress("DEPRECATION")
val ksiazka = intent.getSerializableExtra("ksiazka") as? Ksiazka
if (ksiazka == null) { // brak obiektu albo obiekt innej klasy
finish()
return
}
tvTytul.text = ksiazka.tytul // tu Kotlin już wie, że to Ksiazka
| Fragment | Znaczenie |
|---|---|
intent | Intencja, która otworzyła ten ekran. Ma ją każda aktywność. |
getSerializableExtra("ksiazka") | Wyjmuje obiekt zapisany pod kluczem "ksiazka". Gdy go nie ma — null. |
as? Ksiazka | „Potraktuj to jako Ksiazka, a jeśli się nie da — daj null”. Wersja bez znaku zapytania (as) zamknęłaby aplikację. |
finish() + return | Bez danych nie ma czego pokazać — zamykamy ekran i kończymy onCreate. |
Dlaczego @Suppress(„DEPRECATION”)
Od Androida 13 (API 33) jest nowsza wersja tej metody: getSerializableExtra("ksiazka", Ksiazka::class.java). Nie działa jednak na starszych telefonach, a nasz projekt obsługuje je od API 24. Dlatego używamy starej wersji. Android Studio przekreśla ją jako przestarzałą (deprecated) — działa poprawnie, a adnotacja @Suppress("DEPRECATION") tylko wycisza to ostrzeżenie.
Drugi ekran dostaje kopię
Skoro obiekt został rozłożony i złożony od nowa, drugi ekran ma inny obiekt z takimi samymi danymi. Zmiana na drugim ekranie nie zmieni książki na liście w MainActivity. To zabezpieczenie: ekran szczegółów nie zepsuje danych listy przez pomyłkę.
Gdy drugi ekran ma zmienić dane (na przykład oznaczyć książkę jako wypożyczoną), musi odesłać nowy obiekt z powrotem, a pierwszy ekran sam podmieni go na liście. Jak odebrać wynik z drugiego ekranu — w lekcji 33.
Paczka ma limit wielkości
Intencja nie jest magazynem. Wysyłaj obiekty z kilkoma polami albo krótkie listy. Setki elementów lub zdjęcia w obiekcie mogą przekroczyć limit (około 1 MB na wszystkie przesyłane w danej chwili dane) i aplikacja zamknie się z błędem TransactionTooLargeException.
Biblioteka szkolna
Pierwszy ekran pokazuje listę książek z biblioteki — tak jak w lekcji 25, z wbudowanym wierszem simple_list_item_1. W wierszu widać tytuł i autora, bo ArrayAdapter wywołuje na każdym obiekcie toString(). Dotknięcie książki otwiera ekran szczegółów, który dostaje cały obiekt i pokazuje wszystkie jego pola, a status dostępności koloruje na zielono albo czerwono.
+ obiekt
Ksiazka
jeden ekran szczegółów obsługuje każdą książkę — różni się tylko obiekt przesłany w intencji
Przygotowanie projektu
- Nowy projekt: Empty Views Activity, nazwa
Biblioteka, język Kotlin, minimalne SDK API 24. - Klasa danych: prawy przycisk na pakiecie (folder z
MainActivity) → New → Kotlin Class/File → nazwaKsiazka, typ Data class. - Drugi ekran: prawy przycisk na pakiecie → New → Activity → Empty Views Activity → nazwa
SzczegolyActivity. Android Studio utworzy plik Kotlin, plikactivity_szczegoly.xmli sam dopisze aktywność doAndroidManifest.xml. - Podmień zawartość wszystkich plików na kod poniżej (linie
package …zostaw z twojego projektu).
├── AndroidManifest.xmlwpis SzczegolyActivity — dodaje Android Studio
├── java/…/
│ ├── Ksiazka.ktklasa danych — to ją przesyłamy
│ ├── MainActivity.ktlista książek, wysyła obiekt
│ └── SzczegolyActivity.ktodbiera obiekt
└── res/layout/
├── activity_main.xml
└── activity_szczegoly.xml
<!-- drugi ekran: exported="false" — mogą go otwierać tylko ekrany tej aplikacji -->
<activity
android:name=".SzczegolyActivity"
android:exported="false" />
package pl.schoolit.biblioteka
import java.io.Serializable
// Serializable: obiekt można spakować do intencji
data class Ksiazka(
val tytul: String,
val autor: String,
val rok: Int,
val dostepna: Boolean
) : Serializable {
// ten tekst ArrayAdapter pokaże w wierszu listy
override fun toString(): String {
return "$tytul — $autor"
}
}
<?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:layout_marginBottom="8dp"
android:text="Biblioteka szkolna"
android:textSize="24sp"
android:textStyle="bold" />
<ListView
android:id="@+id/lista"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
package pl.schoolit.biblioteka
import android.content.Intent
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var lista: ListView
private val ksiazki = listOf(
Ksiazka("Lalka", "Bolesław Prus", 1890, true),
Ksiazka("Pan Tadeusz", "Adam Mickiewicz", 1834, false),
Ksiazka("Quo vadis", "Henryk Sienkiewicz", 1896, true),
Ksiazka("Ferdydurke", "Witold Gombrowicz", 1937, true),
Ksiazka("Solaris", "Stanisław Lem", 1961, false),
Ksiazka("Zbrodnia i kara", "Fiodor Dostojewski", 1866, true)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
// lista obiektów — w wierszu pojawi się wynik toString()
lista.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, ksiazki)
lista.setOnItemClickListener { _, _, position, _ ->
val ksiazka = ksiazki[position]
val intencja = Intent(this, SzczegolyActivity::class.java)
intencja.putExtra("ksiazka", ksiazka) // cały obiekt pod jednym kluczem
startActivity(intencja)
}
}
private fun initUI() {
lista = findViewById(R.id.lista)
}
}
<?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:id="@+id/tvTytul"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvAutor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textSize="18sp" />
<TextView
android:id="@+id/tvRok"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="18sp" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textSize="18sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnWroc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Wróć do listy" />
</LinearLayout>
package pl.schoolit.biblioteka
import android.graphics.Color
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class SzczegolyActivity : AppCompatActivity() {
private lateinit var tvTytul: TextView
private lateinit var tvAutor: TextView
private lateinit var tvRok: TextView
private lateinit var tvStatus: TextView
private lateinit var btnWroc: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_szczegoly)
initUI()
// wyjmujemy obiekt z intencji, która otworzyła ten ekran
@Suppress("DEPRECATION")
val ksiazka = intent.getSerializableExtra("ksiazka") as? Ksiazka
if (ksiazka == null) {
Toast.makeText(this, "Brak danych książki", Toast.LENGTH_SHORT).show()
finish()
return
}
tvTytul.text = ksiazka.tytul
tvAutor.text = "Autor: ${ksiazka.autor}"
tvRok.text = "Rok wydania: ${ksiazka.rok}"
if (ksiazka.dostepna) {
tvStatus.text = "Dostępna — możesz wypożyczyć"
tvStatus.setTextColor(Color.parseColor("#2E7D32"))
} else {
tvStatus.text = "Wypożyczona"
tvStatus.setTextColor(Color.parseColor("#C62828"))
}
btnWroc.setOnClickListener {
finish() // zamyka ten ekran — wraca lista
}
}
private fun initUI() {
tvTytul = findViewById(R.id.tvTytul)
tvAutor = findViewById(R.id.tvAutor)
tvRok = findViewById(R.id.tvRok)
tvStatus = findViewById(R.id.tvStatus)
btnWroc = findViewById(R.id.btnWroc)
}
}
Co dzieje się po kolei
Ksiazkatodata classz dopiskiem: Serializable— dzięki temuputExtraprzyjmie obiekt. MetodatoString()decyduje, co widać w wierszu listy.MainActivitytrzyma listę obiektów i podaje ją doArrayAdapterz wbudowanym wierszem — zupełnie jak napisy w lekcji 25.- Po kliknięciu
ksiazki[position]daje kliknięty obiekt. Intencja wskazuje klasę drugiego ekranu,putExtra("ksiazka", ksiazka)pakuje obiekt,startActivityotwiera ekran. SzczegolyActivitywonCreatewyjmuje obiekt tym samym kluczem"ksiazka"i zamienia go naKsiazkaprzezas?.- Gdy obiektu nie ma, ekran pokazuje Toast i zamyka się. Gdy jest — wypełnia pola i koloruje status.
finish()zamyka ekran szczegółów. Lista nie była zamknięta — czekała pod spodem, więc po prostu znów ją widać.
Sprawdź trzy rzeczy
- Otwórz kilka książek po kolei i wracaj przyciskiem Wróć do listy albo systemowym przyciskiem Wstecz. Za każdym razem wracasz do tej samej listy — ekrany się nie mnożą.
- Zmień w
MainActivityklucz na"Ksiazka"(wielka litera). Po kliknięciu pojawi się „Brak danych książki” — klucze muszą być identyczne co do znaku. - Usuń z klasy
: Serializable. Linia zputExtrapodświetli się na czerwono — kompilator nie znajdzie wersjiputExtra, która przyjmuje zwykły obiekt.
Parcelable — wersja androidowa
Serializable pochodzi z Javy i działa wszędzie, ale jest wolniejszy, bo w trakcie działania aplikacji sam „bada”, z czego składa się obiekt. Android ma własny, szybszy sposób pakowania: Parcelable (od parcel — paczka). Dawniej wymagał pisania kilkudziesięciu linii kodu, dziś wystarczy adnotacja @Parcelize, a resztę dopisze Kotlin.
Krok 1. W pliku build.gradle.kts (Module :app) dopisz jedną linię w sekcji plugins i kliknij Sync Now. Pozostałe linie zostaw takie, jakie są w twoim projekcie:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("kotlin-parcelize") // nowa linia
}
Krok 2. W klasie zamień Serializable na Parcelable i dodaj adnotację:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Ksiazka(
val tytul: String,
val autor: String,
val rok: Int,
val dostepna: Boolean
) : Parcelable {
override fun toString(): String {
return "$tytul — $autor"
}
}
Krok 3. Wysyłanie się nie zmienia — putExtra("ksiazka", ksiazka) sam wybierze wersję dla Parcelable. Zmienia się odbiór:
@Suppress("DEPRECATION")
val ksiazka = intent.getParcelableExtra<Ksiazka>("ksiazka")
Tu nie trzeba as? — typ podajesz w nawiasach ostrych <Ksiazka>. Wynik i tak może być null, więc sprawdzenie if (ksiazka == null) zostaje.
Serializable | Parcelable + @Parcelize | |
|---|---|---|
| pochodzenie | Java, java.io | Android, android.os |
| przygotowanie | tylko : Serializable | wtyczka w Gradle + adnotacja + : Parcelable |
| odbiór | getSerializableExtra(…) as? Klasa | getParcelableExtra<Klasa>(…) |
| szybkość | wolniejszy | szybszy — zalecany przez Androida |
| kiedy | małe obiekty, nauka, szybki prototyp | docelowe aplikacje, listy obiektów |
Gdy Gradle nie zna wtyczki
Jeżeli po Sync Now pojawi się błąd Plugin [id: 'kotlin-parcelize’] was not found, twój projekt ma inaczej ustawiony Kotlin. Wtedy w tej samej sekcji użyj pełnej nazwy z wersją Kotlina z pliku gradle/libs.versions.toml (pozycja kotlin = "…"): id("org.jetbrains.kotlin.plugin.parcelize") version "WERSJA".
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
data class Nazwa(val …) | klasa | Klasa do przechowywania danych. |
: Serializable | interfejs Javy | Pozwala rozłożyć obiekt na bajty i przesłać go intencją. |
Intent(this, Klasa::class.java) | obiekt | Intencja otwierająca wskazany ekran. |
putExtra("klucz", obiekt) | metoda | Dokłada obiekt do intencji pod kluczem. |
startActivity(intencja) | metoda | Otwiera ekran opisany intencją. |
intent | Intent | W aktywności: intencja, która ją otworzyła. |
getSerializableExtra("klucz") | Serializable? | Wyjmuje obiekt; null, gdy go brak. |
as? Klasa | operator | Bezpieczna zmiana typu — null zamiast awarii. |
@Parcelize + : Parcelable | adnotacja | Kotlin sam pisze kod pakowania obiektu. |
getParcelableExtra<Klasa>("klucz") | Klasa? | Wyjmuje obiekt Parcelable. |
@Suppress("DEPRECATION") | adnotacja | Wycisza ostrzeżenie o przestarzałej metodzie. |
finish() | metoda | Zamyka bieżący ekran i wraca do poprzedniego. |
Color.parseColor("#RRGGBB") | Int | Zamienia zapis koloru na liczbę dla setTextColor. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
klasa bez : Serializable | Błąd kompilacji przy putExtra — None of the following candidates is applicable. |
pole typu Bitmap, View lub innej klasy bez Serializable | Kompiluje się, ale aplikacja zamyka się przy startActivity — w opisie błędu znajdziesz NotSerializableException i nazwę klasy, której nie da się spakować. |
"ksiazka" przy wysyłaniu, "Ksiazka" przy odbiorze | Odbiór daje null. Klucze muszą być identyczne. |
… as Ksiazka (bez ?) | Gdy obiektu brak — awaria NullPointerException: null cannot be cast to non-null type. |
brak wpisu <activity> w manifeście | ActivityNotFoundException po kliknięciu. Dodawaj aktywności przez New → Activity. |
@Parcelize bez wtyczki w Gradle | Unresolved reference: parcelize. Dopisz id("kotlin-parcelize") i zsynchronizuj. |
| zmiana pola obiektu na drugim ekranie | Lista na pierwszym ekranie się nie zmienia — drugi ekran ma kopię. Wynik odsyła się osobno (lekcja 33). |
| przesyłanie długiej listy ze zdjęciami | TransactionTooLargeException. Wysyłaj małe obiekty. |
Zadania
Zadania 1–3 rozbudowują przykład z lekcji. Zadania 4–6 wykonaj w nowych projektach Empty Views Activity. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Dodaj do klasy Ksiazka pola strony: Int i gatunek: String. Uzupełnij dane wszystkich książek i pokaż nowe pola na ekranie szczegółów. Sprawdź, że w MainActivity nie musiałeś zmieniać ani jednej linii poza listą książek.
pokaż wskazówkę
- Dodaj dwa
TextViewdoactivity_szczegoly.xml, dwa pola wSzczegolyActivityi dwie linie winitUI().
Na ekranie szczegółów dodaj przycisk Wypożycz. Jest aktywny tylko dla dostępnych książek. Po kliknięciu pokazuje Toast „Wypożyczono: Quo vadis. Zwrot za 14 dni.” i zamyka ekran.
pokaż wskazówkę
- Aktywność przycisku ustawia
btnWypozycz.isEnabled = ksiazka.dostepna.
Przerób przykład tak, aby Ksiazka była Parcelable z adnotacją @Parcelize. Aplikacja ma działać tak samo. Zapisz w komentarzu na początku pliku Ksiazka.kt, które trzy miejsca w projekcie musiałeś zmienić.
pokaż wskazówkę
- Gradle (wtyczka), klasa (adnotacja i interfejs), odbiór w
SzczegolyActivity.MainActivityzostaje bez zmian.
Klasa Uczen(imie, nazwisko, klasa, oceny: List<Int>). Pierwszy ekran: lista pięciu uczniów w formacie „Nowak Anna (3TP)”. Ekran szczegółów pokazuje imię i nazwisko, klasę, wszystkie oceny w jednej linii („5, 4, 6, 3”) i średnią z dwoma miejscami po przecinku. Średnia poniżej 2,0 jest czerwona, od 4,75 — zielona z dopiskiem „pasek!”.
pokaż wskazówkę
- Lista liczb przechodzi przez
Serializablebez dodatkowej pracy. Oceny w linii:uczen.oceny.joinToString(", "), średnia:uczen.oceny.average().
Klasa Telefon(model, cena, pamiecGB, bateriaMah) i lista sześciu telefonów. Użytkownik klika dwa telefony; nad listą widać „Wybrano: 1/2”, potem „2/2”. Po drugim kliknięciu otwiera się ekran porównania, który dostaje dwa obiekty pod kluczami "pierwszy" i "drugi" i pokazuje je w dwóch kolumnach. Lepsza wartość w każdym wierszu (niższa cena, więcej pamięci, większa bateria) jest pogrubiona.
pokaż wskazówkę
- Pierwszy wybór zapamiętaj w polu
private var pierwszy: Telefon? = null. Po otwarciu ekranu porównania wyzeruj je, żeby można było porównywać od nowa. Pogrubienie:tv.setTypeface(null, Typeface.BOLD).
Klasa Przepis(nazwa, minuty, trudnosc, skladniki: List<String>, kroki: List<String>), wersja Parcelable. Pierwszy ekran: lista przepisów („Naleśniki · 30 min”) i przełącznik Switch „Tylko do 20 minut”, który filtruje listę. Ekran przepisu: nazwa, czas, trudność w postaci gwiazdek (RatingBar tylko do odczytu), lista składników w ListView z wbudowanym wierszem simple_list_item_multiple_choice (odhaczanie kupionych) i przycisk Pokaż kroki, który otwiera trzeci ekran z krokami — ten ekran dostaje tylko listę kroków i nazwę przepisu, nie cały obiekt.
pokaż wskazówkę
- Listę napisów prześlesz bez własnej klasy:
putStringArrayListExtra("kroki", ArrayList(przepis.kroki)), odbiór:getStringArrayListExtra("kroki"). Lista wielokrotnego wyboru wymaga w XMLandroid:choiceMode="multipleChoice".
Co trzeba zapamiętać
- Zamiast kilku wartości osobno wysyłaj jeden obiekt:
putExtra("klucz", obiekt). - Klasa musi się na to zgodzić:
: Serializable(najprościej) albo@Parcelize … : Parcelable(szybciej). - Odbiór:
getSerializableExtra("klucz") as? KlasalubgetParcelableExtra<Klasa>("klucz")— zawsze sprawdźnull. - Klucz przy wysyłaniu i odbiorze musi być identyczny.
- Drugi ekran dostaje kopię obiektu — zmiany trzeba odesłać (lekcja 33).
Dokumentacja: Intent, Generator Parcelable (@Parcelize), Parcelables i Bundle — limity rozmiaru.