school-it.pl // android + kotlin // rozdział 9 // lekcja 36
Cykl życia aktywności
Obróć telefon w aplikacji z lekcji 35 — rozmiar czcionki wraca do początkowego. To nie błąd w kodzie, tylko zasada Androida: przy obrocie ekran jest niszczony i tworzony od nowa. Aktywność przechodzi przez etapy życia — powstaje, pokazuje się, chowa, ginie — a Android przy każdym z nich wywołuje metodę, którą możesz nadpisać. Zobaczysz te etapy na żywo w dzienniku i nauczysz się zachowywać dane przy obrocie.
Czego się dziś nauczysz
- Poznasz metody cyklu życia:
onCreate,onStart,onResume,onPause,onStop,onRestart,onDestroy. - Wypiszesz komunikaty do Logcat przez
Log.di przefiltrujesz je. - Zrozumiesz, dlaczego obrót ekranu kasuje zmienne.
- Zapiszesz stan w
onSaveInstanceStatei odczytasz go wonCreate. - Rozróżnisz, co zapamiętują same kontrolki, a co musisz zapisać ty.
Etapy życia ekranu
Aktywność przypomina aktora w teatrze: przychodzi do garderoby (created), wchodzi za kulisy (started — widać go, ale jeszcze nie gra), gra na scenie (resumed), schodzi za kulisy, wraca do garderoby i w końcu wychodzi z teatru. Przy każdym przejściu Android wywołuje jedną metodę:
setContentView, initUI
widoczny
reaguje na dotyk
(np. otwiera się
inny ekran)
znika
po onStop ekran może wrócić: onRestart → onStart → onResume
| Metoda | Kiedy | Do czego się przydaje |
|---|---|---|
onCreate | Raz, gdy powstaje obiekt ekranu. | Układ, kontrolki, zdarzenia, odczyt danych. |
onStart | Ekran za chwilę będzie widoczny. | Odświeżenie danych, które mogły się zmienić. |
onResume | Ekran na wierzchu, użytkownik może go dotykać. | Wznowienie animacji, odliczania, muzyki. |
onPause | Ekran traci pierwszeństwo — zaraz zniknie albo częściowo przykryje go inna aktywność. Okno AlertDialog twojej aplikacji nie wywołuje onPause. | Zatrzymanie odliczania, muzyki. |
onStop | Ekranu już nie widać. | Zapis danych na stałe (lekcja 37). |
onRestart | Zatrzymany ekran wraca na pierwszy plan. | Rzadko potrzebna. |
onDestroy | Obiekt ekranu jest niszczony. | Sprzątanie. Nie licz, że zawsze się wykona. |
Nadpisując metodę cyklu życia, zawsze wywołuj wersję rodzica (super.onStart() itd.) — Android wykonuje tam własną pracę. Bez tego aplikacja zgłosi błąd SuperNotCalledException.
Co się dzieje, gdy…
| Sytuacja | Wywołane metody |
|---|---|
| uruchomienie aplikacji | onCreate → onStart → onResume |
| przycisk / gest Ekran główny | onPause → onStop (+ onSaveInstanceState) |
| powrót do aplikacji | onRestart → onStart → onResume |
| otwarcie drugiego ekranu (lekcja 30) | pierwszy: onPause → onStop; po powrocie: onRestart → onStart → onResume |
Wstecz / finish() | onPause → onStop → onDestroy |
| obrót ekranu | onPause → onStop → onSaveInstanceState → onDestroy, potem nowy obiekt: onCreate → onStart → onResume |
Podglądanie w Logcat
Toast przy każdej metodzie zasłoniłby ekran. Programiści wypisują komunikaty do Logcat — okna na dole Android Studio, gdzie trafiają też błędy aplikacji:
import android.util.Log
override fun onStart() {
super.onStart()
Log.d("CYKL", "onStart") // znacznik (tag), treść
}
W oknie Logcat wpisz w polu filtra tag:CYKL — zobaczysz tylko swoje komunikaty, bez setek systemowych. Log.d to poziom debug; są też Log.i (informacja), Log.w (ostrzeżenie) i Log.e (błąd).
Obrót ekranu
Po obrocie ekran ma inne wymiary i może potrzebować innego układu. Android rozwiązuje to najprościej: niszczy stary obiekt aktywności i tworzy nowy. Nowy obiekt ma nowe pola — z wartościami początkowymi z deklaracji (private var wynik = 0).
| Co | Po obrocie |
|---|---|
pola klasy (var wynik, listy) | znikają — wracają do wartości początkowych |
tekst ustawiony z kodu w TextView | znika |
tekst wpisany w EditText z android:id | zostaje — kontrolka zapamiętuje go sama |
zaznaczenie CheckBox / Switch / RadioButton z id | zostaje |
pozycja przewinięcia ScrollView z id | zostaje |
Ta sama sytuacja zdarza się nie tylko przy obrocie: zmiana języka telefonu, trybu ciemnego czy rozmiaru okna na tablecie też tworzy ekran od nowa. Dlatego nie wystarczy „zablokować obrotu” — trzeba umieć zachować stan.
Zapis stanu: onSaveInstanceState
Zanim stary obiekt zginie, Android daje mu Bundle — „paczkę na drogę”, podobną do extras intencji. Wkładasz do niej wartości, a nowy obiekt dostaje tę paczkę w onCreate jako savedInstanceState:
// stary obiekt — tuż przed zniszczeniem
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("wynik", wynik)
}
// nowy obiekt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
if (savedInstanceState != null) { // null = pierwsze uruchomienie
wynik = savedInstanceState.getInt("wynik", 0)
}
pokazWynik()
}
onSaveInstanceState:
putInt(„wynik”, 3)
onCreate: getInt → 3
Do czego Bundle, a do czego co innego
Bundle jest na chwilowy stan ekranu: wynik meczu, numer pytania, zaznaczenie. Ma mały limit rozmiaru i znika, gdy użytkownik zamknie aplikację Wstecz. Danych, które mają przetrwać zamknięcie aplikacji, nie zapisuje się tutaj — do tego służy SharedPreferences (lekcja 37). A wygodniejszy sposób na przetrwanie obrotu poznasz w rozdziale o MVVM: ViewModel (lekcja 40).
Tablica wyników z dziennikiem
Wynik meczu dwóch drużyn z przyciskami +1 i Zeruj. Każda metoda cyklu życia dopisuje się do dziennika na ekranie i do Logcat. Przełącznik Zapamiętuj wynik przy obrocie decyduje, czy onSaveInstanceState zapisze wynik — możesz porównać oba przypadki.
onStart
onResume
onPause
onStop
onSaveInstanceState
onRestart
onStart
onResume
(zapis włączony)
onStart
onResume
onStart
onResume
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Mecz, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. - Na emulatorze włącz automatyczny obrót: w szybkich ustawieniach (przeciągnij z góry ekranu) kafelek Autoobracanie. Obracasz przyciskami na pasku obok emulatora.
<?xml version="1.0" encoding="utf-8"?>
<!-- ScrollView: w poziomie ekran jest niski, dziennik musi się przewijać -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Mecz"
android:textSize="24sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gospodarze" />
<TextView
android:id="@+id/tvGospodarze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="56sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnGospodarze"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="+1" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Goście" />
<TextView
android:id="@+id/tvGoscie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="56sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnGoscie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="+1" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btnZeruj"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zeruj" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/swZapamietuj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="Zapamiętuj wynik przy obrocie" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Dziennik tego obiektu ekranu"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDziennik"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="13sp" />
</LinearLayout>
</ScrollView>
package pl.schoolit.mecz
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.materialswitch.MaterialSwitch
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var tvGospodarze: TextView
private lateinit var tvGoscie: TextView
private lateinit var btnGospodarze: Button
private lateinit var btnGoscie: Button
private lateinit var btnZeruj: Button
private lateinit var swZapamietuj: MaterialSwitch
private lateinit var tvDziennik: TextView
// ---------- stan (po obrocie wraca do tych wartości!) ----------
private var gospodarze = 0
private var goscie = 0
private val dziennik = mutableListOf<String>()
// ---------- cykl życia ----------
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
if (savedInstanceState == null) {
zapiszWDzienniku("onCreate — pierwsze uruchomienie")
} else {
// nowy obiekt po obrocie: odczyt z paczki (gdy klucza brak → 0)
gospodarze = savedInstanceState.getInt("gospodarze", 0)
goscie = savedInstanceState.getInt("goscie", 0)
zapiszWDzienniku("onCreate — odtworzony z Bundle")
}
ustawZdarzenia()
pokazWynik()
}
override fun onStart() {
super.onStart()
zapiszWDzienniku("onStart")
}
override fun onResume() {
super.onResume()
zapiszWDzienniku("onResume")
}
override fun onPause() {
super.onPause()
zapiszWDzienniku("onPause")
}
override fun onStop() {
super.onStop()
zapiszWDzienniku("onStop")
}
override fun onRestart() {
super.onRestart()
zapiszWDzienniku("onRestart")
}
override fun onDestroy() {
zapiszWDzienniku("onDestroy") // widać już tylko w Logcat
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (swZapamietuj.isChecked) {
outState.putInt("gospodarze", gospodarze)
outState.putInt("goscie", goscie)
}
zapiszWDzienniku("onSaveInstanceState")
}
// ---------- przygotowanie ----------
private fun initUI() {
tvGospodarze = findViewById(R.id.tvGospodarze)
tvGoscie = findViewById(R.id.tvGoscie)
btnGospodarze = findViewById(R.id.btnGospodarze)
btnGoscie = findViewById(R.id.btnGoscie)
btnZeruj = findViewById(R.id.btnZeruj)
swZapamietuj = findViewById(R.id.swZapamietuj)
tvDziennik = findViewById(R.id.tvDziennik)
}
private fun ustawZdarzenia() {
btnGospodarze.setOnClickListener { gospodarze++; pokazWynik() }
btnGoscie.setOnClickListener { goscie++; pokazWynik() }
btnZeruj.setOnClickListener { gospodarze = 0; goscie = 0; pokazWynik() }
}
// ---------- pomocnicze ----------
private fun zapiszWDzienniku(zdarzenie: String) {
Log.d("CYKL", zdarzenie) // Logcat, filtr: tag:CYKL
dziennik.add(zdarzenie)
tvDziennik.text = dziennik.joinToString("\n") // ekran
}
private fun pokazWynik() {
tvGospodarze.text = gospodarze.toString()
tvGoscie.text = goscie.toString()
}
}
Co dzieje się po kolei
- Przy starcie
savedInstanceStatejestnull— dziennik pokazuje „pierwsze uruchomienie”, potemonStartionResume. - Wyjście na ekran główny i powrót: ten sam obiekt, więc dziennik rośnie —
onPause,onStop,onSaveInstanceState,onRestart,onStart,onResume. Wynik się nie zmienia. - Obrót: stary obiekt dostaje
onSaveInstanceState(zapisze wynik, jeśli przełącznik włączony) ionDestroy. Nowy obiekt ma pusty dziennik i odczytuje wynik zBundle. - Przełącznik ma
id, więc sam pamięta zaznaczenie po obrocie. Wyniki wTextViewustawiane z kodu — nie; dlatego trzeba je zapisać. - Pełną historię obu obiektów, razem z
onDestroy, widać w Logcat po filtrzetag:CYKL.
Sprawdź trzy rzeczy
- Ustaw wynik 3 : 1 i obróć ekran z włączonym przełącznikiem — wynik zostaje. Wyłącz przełącznik i obróć znowu — 0 : 0.
- Otwórz Logcat z filtrem
tag:CYKLi obróć ekran. ZnajdźonDestroystarego obiektu i zaraz po nimonCreatenowego. - Wynik 2 : 2, potem systemowe Wstecz i ponowne uruchomienie z ikony. Wynik 0 : 0 i „pierwsze uruchomienie” — Wstecz zamyka ekran na dobre,
Bundlenie jest przekazywany.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
onCreate / onStart / onResume | metody | Ekran powstaje, staje się widoczny, staje się aktywny. |
onPause / onStop / onDestroy | metody | Ekran traci pierwszeństwo, znika z widoku, jest niszczony. |
onRestart | metoda | Zatrzymany ekran wraca. |
super.onX() | wywołanie | Obowiązkowe w każdej nadpisanej metodzie cyklu życia. |
onSaveInstanceState(outState: Bundle) | metoda | Zapis chwilowego stanu przed zniszczeniem. |
savedInstanceState: Bundle? | parametr | Paczka ze stanem w onCreate; null przy pierwszym uruchomieniu. |
bundle.putInt / putString / putBoolean | metody | Zapis wartości pod kluczem. |
bundle.getInt("k", 0), getString("k") | metody | Odczyt wartości. |
Log.d("TAG", "treść") | metoda | Komunikat w Logcat. |
filtr tag:CYKL | Logcat | Pokazuje tylko komunikaty z danym znacznikiem. |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
awaria SuperNotCalledException | W nadpisanej metodzie brak super.onStart() (itd.). |
| po obrocie dane znikają | Brak onSaveInstanceState albo odczytu w onCreate. |
savedInstanceState.getInt(…) → błąd kompilacji | Parametr jest Bundle?. Najpierw if (savedInstanceState != null). |
| odczytano stan, ale ekran pokazuje 0 | Odczyt jest po pokazWynik(). Najpierw odczyt, potem odświeżenie ekranu. |
w onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) nic nie działa | Android Studio podpowiada też wersję z dwoma parametrami — to inna metoda. Użyj tej z jednym. |
dane zapisane w Bundle nie przetrwały zamknięcia aplikacji | Tak ma być — Bundle jest na chwilę. Na stałe: lekcja 37. |
android:screenOrientation="portrait" w manifeście „naprawia” problem | Ukrywa go tylko przy obrocie — tryb ciemny czy zmiana języka nadal tworzą ekran od nowa. Na dużych ekranach Android może tę blokadę pominąć. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Zawsze sprawdź działanie obrotem ekranu. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Wróć do licznika z lekcji 19 i dodaj zapis stanu, tak by liczba osób na sali przetrwała obrót.
pokaż wskazówkę
outState.putInt("liczba", liczba); wonCreate:liczba = savedInstanceState?.getInt("liczba", 0) ?: 0— krótszy zapis bezif.
Popraw notatnik tak, by po obrocie zostawał rozmiar czcionki i tryb nocny. Sprawdź, że sam tekst notatki zostaje już teraz — dlaczego?
pokaż wskazówkę
putFloat("rozmiar", rozmiar)iputBoolean("noc", trybNocny). Tekst pamiętaEditTextzid. Zaznaczenie w menu ustawisz wonCreateOptionsMenu:menu.findItem(R.id.akcja_tryb_nocny).isChecked = trybNocny.
Stoper odliczający sekundy (CountDownTimer z lekcji 15 albo Handler). Gdy użytkownik wyjdzie z aplikacji, odliczanie się zatrzymuje; po powrocie — wznawia od tej samej wartości.
pokaż wskazówkę
- Uruchamiaj odliczanie w
onResume, zatrzymuj (cancel()) wonPause. Liczbę sekund trzymaj w polu.
Quiz z 5 pytaniami (RadioGroup). Po obrocie zostaje numer pytania, liczba punktów i lista udzielonych odpowiedzi.
pokaż wskazówkę
- Listę liczb zapiszesz przez
putIntArray("odp", odpowiedzi.toIntArray()), odczytasz:getIntArray("odp")?.toMutableList().
Przykład z lekcji 26 traci produkty po obrocie. Zapisz listę w Bundle i odtwórz ją w onCreate — przed utworzeniem adaptera.
pokaż wskazówkę
putStringArrayList("zakupy", ArrayList(zakupy)); odczyt:getStringArrayList("zakupy"), potemzakupy.clear(); zakupy.addAll(…)— lista jestval, więc nie podmieniaj jej.
Rozbuduj przykład o drugą aktywność (lekcja 30), która też zapisuje swoje metody cyklu życia z tagiem CYKL i nazwą ekranu („Main: onPause”, „Druga: onCreate”…). Ustal w Logcat dokładną kolejność przy otwieraniu i zamykaniu drugiego ekranu i zapisz ją w komentarzu w kodzie.
pokaż wskazówkę
- Spodziewaj się przeplotu: Main: onPause → Druga: onCreate, onStart, onResume → Main: onStop. Przy powrocie: Druga: onPause → Main: onRestart, onStart, onResume → Druga: onStop, onDestroy.
Co trzeba zapamiętać
- Ekran żyje etapami:
onCreate → onStart → onResume…onPause → onStop → onDestroy. - Obrót (i inne zmiany konfiguracji) niszczy obiekt ekranu i tworzy nowy — pola wracają do wartości początkowych.
- Chwilowy stan: zapis w
onSaveInstanceState, odczyt wonCreatezsavedInstanceState. - Kontrolki z
idsame pamiętają wpisany tekst i zaznaczenia; tekst ustawiony z kodu — nie. - Do podglądania:
Log.d("TAG", …)i filtr w Logcat.
Dokumentacja: Cykl życia aktywności, Zapisywanie stanu interfejsu.