You are on page 1of 713

y˜– Š ’š8·

h‘‡•’Œ‡
w•’Š•„’š„‘Œˆ „“ŒŽ„†Œ

|‘ŒŽ„
„š–—œ‡„8†œ†‹
„Ž—œš‘’N†Œ
w•’ˆŽ—˜ „“ŒŽ„†ˆ±
Ž—I•ˆ …A‡8 ‹Œ—„Œ
–“•ˆ‡„Xœ

w•ˆŽ’‘„ –ŒA± „Ž


t„—ˆ•Œ„ kˆ–ŒŠ‘
’Xˆ Œˆ‘Œ9 v“„‘˜ “’A†Œ„
{š’ˆ Xœ†Œˆ ‘Œˆ  —ˆ
ŒˆŒ

w’‡ 8† „…„š –ŒA


–ŒA ‡’ ˜– ˜Š  …Œ…Œ’—ˆŽ„Œ
’Ž„Œ„†œ‘œ†‹ z˜““’•—
h‘‡•’Œ‡„ sŒ…•„•Œˆ–

k„š‘
š‘ n•Œ‰‰Œ—‹–
•Œ‰‰Œ—‹– Æ k
k„™Œ‡
„™Œ‡ n•Œ‰‰
n•Œ‰‰Œ—‹–
Naszym Przyjaciołom i naszej Rodzinie
— dziękujemy Wam bardzo za miłość i wsparcie
O autorach

Autorzy książki Android. Programowanie aplikacji. Rusz głową!

iths
David Griff

Dawn Griffiths

Dawn Griffiths zaczynała jako matematyk David Griffiths zaczął programować w wieku 12 lat,
na jednym z czołowych brytyjskich uniwersytetów, kiedy obejrzał film dokumentalny poświęcony
gdzie ukończyła studia matematyczne z wyróżnieniem. pracom Seymoura Paperta. W wieku 15 lat napisał
Później rozpoczęła karierę w branży produkcji implementację języka LOGO opracowanego przez
oprogramowania i dysponuje już dwudziestoletnim Paperta. Po zakończeniu studiów matematycznych
doświadczeniem w branży IT. na uniwersytecie zaczął pisać kod przeznaczony
dla komputerów oraz artykuły w czasopismach dla
Przed napisaniem książki Android. Programowanie ludzi. Pracował jako instruktor zwinnych metod
aplikacji. Rusz głową! Dawn napisała trzy inne książki programowania, programista, parkingowy, ale nie
z serii Head First (Head First Statistics — polskie w takiej kolejności. Potrafi programować w ponad
wydanie: Head First. Statystyka. Edycja polska, Head dziesięciu językach i pisać prozę tylko w jednym,
First 2D Geometry oraz Head First C — polskie a kiedy nie pisze, ani nie zajmuje się doradztwem,
wydanie: C. Rusz głową!) i brała udział w pracach spędza większość czasu ze swoją uroczą żoną
nad wieloma innymi z tej serii. — i współautorką tej książki — Dawn.
Kiedy nie pracuje nad żadną z książek z serii Rusz Przed napisaniem książki Android. Programowanie
głową!, doskonali się w tai-chi, biega, robi koronki aplikacji. Rusz głową! David napisał trzy inne książki
i gotuje. Uwielbia także podróżować i spędzać czas z tej serii: Head First Rails (polskie wydanie: Head First
ze swoim mężem Davidem. Ruby on Rails. Edycja polska), Head First Programming
oraz Head First C (polskie wydanie: C. Rusz głową!).

Można go śledzić na Twitterze:


http://twitter.com/HeadFirstDroid.

iv
Spis treści

Spis treści (skrócony)


Wprowadzenie xxiii
1 Zaczynamy. Skok na głęboką wodę 1
2 Tworzenie interaktywnych aplikacji. Aplikacje, które coś robią 39
3 Wiele aktywności i intencji. Jakie są Twoje intencje? 73
4 Cykl życia aktywności. Była sobie aktywność 115
5 Interfejs użytkownika. Podziwiaj widoki 163
6 Widoki list i adaptery. Zorganizuj się 227
7 Fragmenty. Zadbaj o modularyzację 269
8 Fragmenty zagnieżdżone. Zadbaj o potomstwo 325
9 Paski akcji. Na skróty 365
10 Szuflady nawigacyjne. Z miejsca na miejsce 397
11 Bazy danych SQLite. Odpal bazę danych 437
12 Kursory i zadania asynchroniczne. Nawiązywanie połączenia z bazą danych 471
13 Usługi. Do usług 541
14 Material Design. W materialistycznym świecie 597
A ART. Środowisko uruchomieniowe Androida 649
B ADB. Android Debug Bridge 653
C Emulator. Emulator Androida 659
D Pozostałości. Dziesięć najważniejszych zagadnień (których nie opisaliśmy) 663

Spis treści (z prawdziwego zdarzenia)

W
Wprowadzenie
Twój mózg jest nastawiony na Androida. Jesteś tu po to, by się czegoś nauczyć, natomiast
Twój mózg robi Ci przysługę, upewniając się, że to, czego się nauczyłeś, szybko wyleci
z pamięci. Twój mózg myśli sobie: „Lepiej zostawić miejsce na coś ważnego, na przykład: których
dzikich zwierząt lepiej unikać albo czy jeżdżenie nago na snowboardzie to dobry pomysł”.
A zatem, w jaki sposób możesz skłonić swój mózg, by myślał, że Twoje życie zależy
od umiejętności pisania aplikacji na Androida?

Dla kogo jest przeznaczona ta książka? xxiv


Wiemy, co sobie myślisz xxv
Wiemy, co sobie myśli Twój mózg xxv
Metapoznanie — myślenie o myśleniu xxvii
Oto co MY zrobiliśmy xxviii
Przeczytaj to xxx
Zespół recenzentów technicznych xxxii
Podziękowania xxxiii

v
Spis treści

Zaczynamy

1
Skok na głęboką wodę
Android błyskawicznie podbił świat.
Każdy chce mieć smartfon lub tablet, a urządzenia z Androidem są niezwykle popularne.
W tej książce nauczymy Cię, jak pisać własne aplikacje, a zaczniemy od pokazania procesu
przygotowania bardzo prostej aplikacji i uruchomienia jej na wirtualnym urządzeniu z Androidem.
W trakcie tych prac poznasz także kilka podstawowych komponentów wszystkich aplikacji
na Androida, takich jak aktywności i układy. Jedyną rzeczą, której będziesz do tego
potrzebować, jest znajomość Javy, choć wcale nie musisz być w niej mistrzem…

2
3
5
6
8
12
SDK
oid
13
dr
An 14
15
Android Studio utworzy pełną strukturę katalogów aplikacji 16
Przydatne pliki projektu 17
Edycja kodu z użyciem edytorów Android Studio 18
Uruchamianie aplikacji w emulatorze Androida 23
Tworzenie wirtualnego urządzenia z Androidem 24
Uruchomienie aplikacji w emulatorze 27
Postępy możesz obserwować w konsoli 28
Jazda próbna 29
Ale co się właściwie stało? 30
Usprawnianie aplikacji 31
Czym jest układ? 32
Plik activity_main.xml zawiera dwa elementy 33
Plik układu zawiera odwołanie do łańcucha, a nie sam łańcuch znaków 34
Zajrzyjmy do pliku strings.xml 35
Weź swoją aplikację na jazdę próbną 37
Twój przybornik do Androida 38

<Layout>

</Layout>
<Układ>
Aktywność
vi Układ
Urządzenie </Układ>
Spis treści

Tworzenie interaktywnych aplikacji

2
Aplikacje, które coś robią
Większość aplikacji musi w jakiś sposób reagować na poczynania
użytkowników.
Z tego rozdziału dowiesz się, co zrobić, aby Twoje aplikacje były nieco bardziej interaktywne.
Przekonasz się, jak zmusić aplikację, by coś zrobiła w odpowiedzi na działania użytkownika,
oraz jak sprawić, by aktywności i układy porozumiewały się ze sobą jak starzy kumple.
Przy okazji pokażemy Ci nieco dokładniej, jak naprawdę działa Android poznasz plik R,
czyli ukryty klejnot, który spaja pozostałe elementy aplikacji.

<Layout> 40
42
</Layout> <resources>
43
Układ </resources> 44
45
strings.xml
48
49
Stosuj zasoby łańcuchowe, a nie łańcuchy podawane w kodzie 50
Zmiana układu i zastosowanie w nim zasobów łańcuchowych 51
Aktywność Weź swoją aplikację na jazdę próbną 52
Dodanie wartości do komponentu Spinner 53
Dodanie do komponentu Spinner odwołania do string-array 54
Jazda próbna komponentu Spinner 54
Musimy zadbać o to, by przycisk coś robił 55
Niech przycisk wywołuje metodę 56
BeerExpert
Jak wygląda kod aktywności? 57
Dodaj do aktywności metodę onClickFindBeer() 58
Metoda onClickFindBeer() musi coś robić 59
Dysponując obiektem View, można odwoływać się do jego metod 60
Aktualizacja kodu aktywności 61
Pierwsza wersja aktywności 63
Jazda próbna — test modyfikacji 65
Tworzenie własnej klasy Javy 66
Dodaj do aktywności wywołanie metody naszej klasy,
aby była wyświetlana FAKTYCZNA porada 67
Kod aktywności, wersja 2 69
Co się dzieje podczas wykonywania tego kodu? 70
Jazda próbna — test aplikacji 71
Twój przybornik do Androida 72

vii
Spis treści

Wiele aktywności i intencji

3
Jakie są Twoje intencje?
Większość aplikacji potrzebuje więcej niż jednej aktywności.
Dotychczas mieliśmy do czynienia z aplikacjami składającymi się tylko z jednej aktywności. Kiedy
jednak sprawy się komplikują, jedna aktywność zwyczajnie nie wystarczy. Dlatego w tym rozdziale
pokażemy Ci, jak tworzyć aplikacje składające się z wielu aktywności i jak nasze aplikacje
mogą porozumiewać się z innymi, wykorzystując w tym celu intencje. Pokażemy także, jak można
używać intencji, by wykraczać poza granice naszych aplikacji, i jak wykorzystywać aktywności
należące do innych aplikacji dostępnych w urządzeniu do wykonywania akcji. To wszystko
zapewni nam znacznie większe możliwości.

74
75
75
Intencja
78
80
83
Do: InnaAktywnosc
84
85
86
87
Metoda putExtra() zapisuje w intencji dodatkowe informacje 88
Aktualizacja kodu aktywności CreateMessageActivity 91
Zastosowanie informacji przekazanych w intencji w klasie
ReceiveMessageActivity 92
Co się dzieje, gdy użytkownik kliknie przycisk Wyślij wiadomość? 93
Jazda próbna aplikacji 94
Jak działają aplikacje na Androida? 95
Co się dzieje podczas działania kodu? 99
Jak Android korzysta z filtrów intencji? 102
Hej, Musisz uruchomić aplikację na PRAWDZIWYM urządzeniu 105
użytkowniku! Każda Jazda próbna aplikacji 107
z tych aktywności może
Zmień kod, aby wyświetlać okno dialogowe 111
wysyłać wiadomości. Której
z nich chcesz użyć? Jazda próbna aplikacji 112
Twój przybornik do Androida 114

CreateMessageActivity

viii Android
Użytkownik
Spis treści

Cykl życia aktywności

4
Była sobie aktywność
Aktywności stanowią podstawę wszystkich aplikacji na Androida.

Wiesz już, jak tworzyć aktywności i jak sprawić, by jedna aktywność uruchomiła drugą, używając
do tego celu intencji. Ale co tak naprawdę dzieje się za kulisami? W tym rozdziale nieco
dokładniej poznamy cykl życia aktywności. Co się dzieje, kiedy aktywność jest tworzona
i usuwana? Jakie metody są wywoływane, gdy aktywność jest wyświetlana i pojawia się na
Aktywność ekranie, a jakie gdy aktywność traci miejsce wprowadzania i jest ukrywana? W jaki sposób
uruchomiona można zapisywać i odtwarzać stan aktywności?

116
118
onCreate()
119
122
123
onStart() Obiekty Handler umożliwiają planowanie wykonania kodu 124
Pełny kod metody runTimer() 125
Kompletny kod aktywności StopwatchActivity 126
onResume() Obrót ekranu zmienia konfigurację urządzenia 132
Od narodzin do śmierci: stany aktywności 133
Cykl życia aktywności: od utworzenia do usunięcia 134
W jaki sposób radzić sobie ze zmianami konfiguracji? 136
Aktywność onRestart()
Co się stanie po uruchomieniu aplikacji? 139
działająca Tworzenie i usuwanie to nie cały cykl życia aktywności 142
Cykl życia aktywności: widzialny czas życia 143
Zaktualizowany kod aktywności StopwatchActivity 147
Co się dzieje podczas działania aplikacji? 148
onPause()
Jazda próbna aplikacji 149
A co się dzieje, jeśli aplikacja jest tylko częściowo widoczna? 150
Cykl życia aktywności: życie na pierwszym planie 151
onStop()
Zatrzymanie stopera w razie wstrzymania aktywności 154
Kompletny kod aktywności 157
Wygodny przewodnik po metodach cyklu życia aktywności 161
onDestroy() Twój przybornik do Androida 162

Aktywność
usunięta
ix
Spis treści

Interfejs użytkownika

5
Podziwiaj widoki
Nie masz innego wyjścia, musisz tworzyć szałowe układy.
Jeśli chcesz pisać aplikacje, których inni będą używać, musisz zadbać o to, by wyglądały one
dokładnie tak, jak sobie tego życzysz. Zagadnienie tworzenia układów potraktowaliśmy
dotychczas bardzo powierzchownie, najwyższy zatem czas, by przyjrzeć mu się dokładniej.
W tym rozdziale pokażemy Ci różne typy układów, które można tworzyć, i zabierzemy Cię na
wycieczkę po najważniejszych komponentach GUI i sposobach ich stosowania. Pod koniec tego
rozdziału przekonasz się, że choć wszystkie te układy i komponenty wyglądają nieco inaczej,
to jednak mają ze sobą więcej wspólnego, niż można by przypuszczać.

Widoki mogą być 165


rozmieszczane
względem układu 168
nadrzędnego… Rozmieszczanie widoków względem innych widoków 170
Atrybuty do rozmieszczania widoków względem innych widoków 171
RelativeLayout — podsumowanie 173
…bądź względem
innych widoków. Układ LinearLayout wyświetla widoki w jednym wierszu lub kolumnie 174
Zmieńmy nieco prosty układ liniowy 176
Dodawanie wagi do widoków 179
Dodawanie wagi do większej liczby widoków 180
Stosowanie atrybutu android:gravity — lista wartości 182
Inne wartości, których można używać w atrybucie
android:layout_gravity 184
Kompletny układ liniowy 185
LinearLayout — podsumowanie 186
Układ GridLayout wyświetla widoki w siatce 189
Dodawanie widoków do układu siatki 190
Utwórzmy nowy układ siatki 191
Wiersz 0: dodajemy widoki do określonych wierszy i kolumn 193
Wiersz 1: tworzymy widok zajmujący komórki kilku kolumn 194
Wiersz 2: tworzymy widok zajmujący komórki kilku kolumn 195
Układ względny
Pełny kod układu siatki 196
<Layout> GridLayout — podsumowanie 197
Układy i komponenty GUI mają wiele wspólnego 201
</Layout>
ViewGroup Zabawy z widokami 205
layout.xml Twój przybornik do Androida 225

Pole
Przycisk tekstowe

x View View
Spis treści

Widoki list i adaptery

6
Zorganizuj się
Chcesz wiedzieć, jaki jest najlepszy sposób na określenie struktury aplikacji?
Znasz już podstawowe elementy konstrukcyjne używane do tworzenia aplikacji, więc teraz nadszedł
czas, żebyś się lepiej zorganizował. W tym rozdziale pokażemy Ci, jak możesz przekształcić
zbiór pomysłów w niesamowitą aplikację. Zobaczysz, że listy danych mogą stać się kluczowym
elementem projektu aplikacji i że łączenie ich może prowadzić do powstania aplikacji łatwej
w użyciu i zapewniającej ogromne możliwości. Przy okazji zapoznasz się z obiektami
nasłuchującymi i adapterami, dzięki którym Twoja aplikacja stanie się bardziej dynamiczna.

228
Ekran

początkowy
229
z listą opcji
230
231
Menu zawierające 232
wszystko, co 233
można u nas Struktura aplikacji dla kafeterii Coffeina 234
zjeść Układ aktywności głównego poziomu składa się z obrazka i listy 238
Kompletny kod układu aktywności głównego poziomu 240
Szczegółowe Zapewnianie reakcji ListView na kliknięcia za pomocą
informacje obiektu nasłuchującego 241
o każdym Kompletny kod aktywności TopLevelActivity 243
napoju Jak utworzyć aktywność listy? 249
Łączenie widoków list z tablicami za pomocą adaptera ArrayAdapter 251
Dodanie adaptera ArrayAdapter do aktywności DrinkCategoryActivity 252
Co się stanie po wykonaniu kodu? 253
Jak obsługiwaliśmy kliknięcia w aktywności TopLevelActivity? 256
Kompletny kod aktywności DrinkCategoryActivity 258
Aktywność szczegółów wyświetla informacje o jednym rekordzie 259
Wypełnienie widoków danymi 261
Kod aktywności DrinkActivity 263
Jazda próbna aplikacji 266
Twój przybornik do Androida 268
Utworzymy adapter ArrayAdapter, To jest
To jest nasz widok listy. nasza tablica.
aby powiązać widok listy z naszą tablicą.

ListView Array Drink.


Adapter drinks

xi
Spis treści

Fragmenty

7
Zadbaj o modularyzację
Wiesz już, jak tworzyć aplikacje, które działają tak samo niezależnie od tego,
na jakim urządzeniu zostały uruchomione…
…ale co zrobić w przypadku, kiedy akurat chcesz, by aplikacja wyglądała i działała inaczej
w zależności od tego, czy zostanie uruchomiona na telefonie, czy na tablecie? W tym rozdziale
pokażemy Ci, co zrobić, aby aplikacja wybierała układ, który najlepiej pasuje do wielkości
ekranu urządzenia. Oprócz tego przedstawimy fragmenty, czyli modularne komponenty
kodu, które mogą być wielokrotnie używane przez różne aktywności.

273
275
276
278
282
283
284
286
290
292
294
295
A zatem fragment 301
będzie zawierał jedną listę. 302
Kiedy chcieliśmy użyć aktywności 303
zawierającej pojedynczą listę,
305
zastosowaliśmy aktywność typu
ListActivity. Zastanawiam się, czy 307
przypadkiem nie istnieje jakiś typ 309
fragmentu stanowiący odpowiednik 315
tej klasy.
319
321
322

xii
Spis treści

Fragmenty zagnieżdżone

8
Zadbaj o potomstwo
Wiesz już, że stosowanie fragmentów w aktywnościach pozwala na
wielokrotne wykorzystywanie kodu i zwiększa elastyczność aplikacji.
W tym rozdziale mamy zamiar pokazać Ci, jak zagnieżdżać fragmenty w innych
fragmentach. Dowiesz się, jak używać menedżera fragmentów podrzędnych,
by poskromić niesforne transakcje. Ponadto dowiesz się, dlaczego tak ważna jest
znajomość różnic między aktywnościami i fragmentami.

326
332
335
Metoda getFragmentManager() tworzy transakcje
na poziomie aktywności 340
Zagnieżdżone fragmenty wymagają zagnieżdżonych transakcji 341
Kompletny kod fragmentu WorkoutDetailFragment 343
Jazda próbna aplikacji 344
Dlaczego kliknięcie przycisku powoduje awarię aplikacji? 345
Przyjrzyjmy się kodowi układu StopwatchFragment 346
Zaimplementuj we fragmencie interfejs OnClickListener 349
Powiązanie obiektu nasłuchującego OnClickListener z przyciskami 351
Kod fragmentu StopwatchFragment 352
Jazda próbna aplikacji 354
Kod fragmentu WorkoutDetailFragment 358
Jazda próbna aplikacji 359
Twój przybornik do Androida 364

Jak widzę, pojawiły się


jakieś transakcje fragmentu.
Muszę je natychmiast
zastosować.
Transakcje
fragmentu

Aktywność Fragment

xiii
Spis treści

Paski akcji

9
Na skróty
Każdy lubi chodzić na skróty.
Z tego rozdziału dowiesz się, jak korzystając z pasków akcji, wzbogacić aplikację o możliwość chodzenia
na skróty. Pokażemy Ci, jak uruchamiać inne aplikacje za pomocą elementów akcji dodawanych do
pasków akcji, jak udostępniać treści innym aplikacjom, używając dostawcy akcji współdzielenia, oraz
jak poruszać się w górę hierarchii aplikacji za pomocą przycisku W górę umieszczonego na pasku
akcji. Dowiesz się też, jak można nadawać tworzonym aplikacjom spójny wygląd i sposób działania,
korzystając z motywów, i poznasz pakiet biblioteki wsparcia systemu Android.

366
tępniania
Właśnie tak wygląda akcja udos 367
jej
wyświetlona na pasku akcji. Po aplikacji,
kliknięciu wyświetlana jest lista 368
ci.
którym możemy udostępnić treś 369
370
371
372
373
374
375
376
377
378
379
382

383
384
386
388
389
391
API 21? 392
Idealnie pasuje. 393
394
395

values-v21
<xml> Name: AppTheme
</xml>
Parent: Theme.Material.Light
Android styles.xml
xiv
Spis treści

Szuflady nawigacyjne

10
Z miejsca na miejsce
Aplikacje są nieporównanie lepsze, gdy można się po nich łatwo poruszać.
W tym rozdziale przedstawimy Ci szufladę nawigacyjną wysuwany panel, który jest
wyświetlany na ekranie po przesunięciu palcem lub kliknięciu ikony umieszczonej na pasku akcji.
Pokażemy Ci, jak można wyświetlać w niej listę odnośników umożliwiających przechodzenie
do kluczowych węzłów aplikacji. Oprócz tego przekonasz się, że przełączanie fragmentów
pozwala łatwo docierać do tych węzłów i je szybko wyświetlać.

398
399
400
401
402
403
404
405
406
407
412
413
414
Stosowanie ActionBarDrawerToggle 417
Modyfikowanie elementów paska akcji w trakcie działania aplikacji 418
Zaktualizowany kod aktywności MainActivity 419
Włączenie możliwości otwierania i zamykania szuflady nawigacyjnej 420
Synchronizacja stanu przycisku ActionBarDrawerToggle 421
Zaktualizowany kod aktywności MainActivity 422
Obsługa zmian konfiguracji 425
Reagowanie na zmiany stosu cofnięć 426
Dodawanie znaczników do fragmentów 427
Kompletny kod aktywności MainActivity 429
Zawartość ekranu jest
wyświetlana w układzie Jazda testowa aplikacji 435
FrameLayout. Chcemy, by Twój przybornik do Androida 436
wypełniała ona cały dostępny
obszar ekranu. W tym przykładzie
jest ona częściowo przesłonięta
przez szufladę nawigacyjną.

xv
Spis treści

Bazy danych SQLite

11
Odpal bazę danych
Jeśli rejestrujesz najlepsze wyniki lub przesyłane komunikaty,
to Twoja aplikacja będzie musiała przechowywać dane.
A w Androidzie dane są zazwyczaj bezpiecznie i trwale przechowywane w bazach danych
SQLite. W tym rozdziale pokażemy Ci, jak utworzyć bazę danych, dodawać do niej tabele,
wypełnić ją wstępnie danymi, a wszystko to za pomocą pomocnika SQLite. Dowiesz się też,
w jaki sposób można bezproblemowo przeprowadzać aktualizacje struktury bazy danych
i jak w razie konieczności wycofania zmian wrócić do jej wcześniejszych wersji.

Znowu w kafeterii Coffeina 438


Jaśnie panie,
Android trwale przechowuje dane, używając baz danych SQLite 439
oto pańska baza
danych. Czy mogę Android udostępnia kilka klas związanych z SQLite 440
jeszcze czymś służyć? Obecna struktura aplikacji kafeterii Coffeina 441
Pomocnik SQLite zarządza Twoją bazą danych 443
Pomocnik SQLite 443
Tworzenie pomocnika SQLite 444
Wnętrze bazy danych SQLite 446
Tabele tworzymy w języku SQL 447
Wstawianie danych za pomocą metody insert() 448
Aktualizacja rekordów za pomocą metody update() 449
onCreate() Określanie wielu warunków 450
Kod klasy CoffeinaDatabaseHelper 451
Co robi kod pomocnika SQLite? 452
Pomocnik SQLite Co zrobić, gdy trzeba będzie zmienić bazę? 455
Bazy danych SQLite mają numer wersji 456
Aktualizacja bazy danych — omówienie 457
DRINK Jak pomocnik SQLite podejmuje decyzje? 459
Aktualizacja bazy w metodzie onUpgrade() 460
Nazwa: Coffeina
Przywracanie starszej wersji bazy za pomocą metody onDowngrade() 461
Wersja: 1
Zaktualizujmy bazę danych 462
Aktualizacja istniejącej bazy danych 465
Zmiana nazwy tabeli 466
Baza danych SQLite
Pełny kod pomocnika SQLite 467
Kod pomocnika SQLite (ciąg dalszy) 468
Co się dzieje podczas działania kodu? 469
Twój przybornik do Androida 470

xvi
Spis treści

Kursory i zadania asynchroniczne

12
Nawiązywanie połączenia z bazą danych
Jak łączysz swoje aplikacje z bazami danych SQLite?
Dotychczas dowiedziałeś się, jak tworzyć bazy danych, używając pomocnika SQLite. Kolejnym
krokiem będzie uzyskanie dostępu do tych baz danych w aktywnościach. Z tego rozdziału dowiesz
się, jak tworzyć kursory, by pobierać dane z bazy danych, jak poruszać się po kursorach oraz
jak pobierać z nich dane. Oprócz tego dowiesz się, jak używać adapterów kursorów, by łączyć
kursory z widokami list. A na koniec przekonasz się, że pisanie wydajnego kodu wielowątkowego
korzystającego z klasy AsyncTask może zagwarantować wysoką wydajność działania aplikacji.

474
onPreExecute 478
479
481
488
doInBackground 489
490
Dodanie ulubionych napojów do aktywności DrinkActivity 508
Kod aktywności DrinkActivity 513
Nowy kod aktywności głównego poziomu 518
onProgressUpdate Zmodyfikowany kod aktywności TopLevelActivity 524
Metoda onPreExecute() 531
Metoda doInBackground() 532
Metoda onProgressUpdate() 533
onPostExecute Metoda onPostExecute() 534
Klasa AsyncTask 535
Kod aktywności DrinkActivity 537
Twój przybornik do Androida 540

Hej, adapterze, Hej, kursorze, potrzebuję


potrzebuję więcej więcej… Kursorze? Stary,
danych. jesteś tam?

Kursor
Widok ListView Adapter CursorAdapter
xvii
Spis treści

Usługi

13
Do usług
Są operacje, które będziemy chcieli realizować niezależnie od tego,
która aplikacja jest widoczna na ekranie.
Na przykład kiedy rozpoczniesz odtwarzanie pliku w aplikacji muzycznej, to najprawdopodobniej
będziesz oczekiwać, że będzie ona kontynuowała odtwarzanie nawet wówczas, gdy przejdziesz
do innej aplikacji. Z tego rozdziału dowiesz się, jak używać usług, by radzić sobie w sytuacjach
takich jak ta. Przy okazji nauczysz się korzystać z kilku wbudowanych usług systemu Android.
Dowiesz się, jak za pomocą usługi powiadomień sprawić, by użytkownik zawsze był dobrze
poinformowany, i jak poznać aktualne położenie geograficzne, korzystając z usługi lokalizacji.
543
545
546
547
554
557
559
561
562
570
573
575
577
578
580
582
586
Tworzenie obiektu ServiceConnection 587
Powiązanie z usługą należy utworzyć podczas uruchamiania aktywności 588

<Layout>
Wyświetlanie przebytego dystansu 589
Kompletny kod aktywności MainActivity 590
</Layout>
Twój przybornik do Androida 595
activity_main.xml
getDistance() Systemowa
usługa
lokalizacyjna

1.11
MainActivity.java OdometerService.java
xviii
Liczba przebytych kilometrów.
Spis treści

Material Design

14
W materialistycznym świecie
W API poziomu 21 Google wprowadził Material Design.
Z tego rozdziału dowiesz się, czym jest Material Design i jak sprawić, by nasze aplikacje do
niego pasowały. Zaczniemy od przedstawienia widoków kart (ang. card views), których możemy
wielokrotnie używać w całej aplikacji, zapewniając jej spójny wygląd i sposób obsługi. Następnie
przedstawimy widok RecyclerView elastycznego przyjaciela widoków list. Przy okazji dowiesz
się także, jak tworzyć własne adaptery i jak całkowicie zmienić wygląd widoku RecyclerView,
używając zaledwie dwóch wierszy kodu.

598
600
603
604
606
607
608
609
610
611
612
613
Kod klasy PizzaMaterialFragment 614
Do rozmieszczania zawartości RecyclerView używa menedżera układu 615
Określanie menedżera układu 616
Kompletny kod klasy PizzaMaterialFragment 617
Zastosowanie fragmentu PizzaMaterialFragment
w aktywności MainActivity 618
Co się stanie po uruchomieniu kodu? 619
Utworzenie aktywności PizzaDetailActivity 627
Co musi robić aktywność PizzaDetailActivity? 628
Aktualizacja pliku AndroidManifest.xml 628
Kod pliku PizzaDetailActivity.java 629
Obsługa kliknięć w widoku RecyclerView 631
Dodanie interfejsu do adaptera 634
Implementacja interfejsu Listener
we fragmencie PizzaMaterialFragment 636
Umieszczenie treści na samym początku 639
Kompletny kod pliku układu fragment_top.xml 644
Kompletny kod klasy TopFragment 645
Twój przybornik do Androida 647

xix
Spis treści

ART

A
Środowisko uruchomieniowe Androida
Aplikacje na Androida muszą działać na urządzeniach wyposażonych
w słabe procesory i bardzo małą pamięć.
Aplikacje pisane w Javie mogą potrzebować sporo pamięci, a ponieważ działają wewnątrz własnej
wirtualnej maszyny Javy (JVM), ich uruchomienie na komputerze o niewielkiej mocy obliczeniowej
może trwać dosyć długo. Android rozwiązuje ten problem, nie używając JVM do uruchamiania
aplikacji. Zamiast JVM używa całkowicie odmiennej wirtualnej maszyny, nazywanej środowiskiem
uruchomieniowym Androida (ang. Android Runtime, w skrócie ART). W tym dodatku zobaczysz,
jak to się dzieje, że ART umożliwia dobre działanie aplikacji napisanych w Javie nawet na małych
komputerach o niewielkiej mocy obliczeniowej.

.java .class classes.dex .apk

ADB

B
Android Debug Bridge
W tej książce skoncentrowaliśmy się na zaspokajaniu wszystkich potrzeb
związanych z pisaniem aplikacji na Androida z wykorzystaniem IDE.
Zdarzają się jednak sytuacje, w których zastosowanie narzędzi obsługiwanych z poziomu wiersza
poleceń jest po prostu przydatne. Mamy tu na myśli na przykład przypadki, gdy Android Studio nie
jest w stanie zauważyć urządzenia z Androidem, choć my wiemy, że ono istnieje. W tym rozdziale
przedstawimy Android Debug Bridge (w skrócie ADB) obsługiwany z poziomu wiersza
poleceń program narzędziowy, którego można używać do komunikacji z emulatorem
lub z rzeczywistymi urządzeniami zaopatrzonymi w Androida.

adb adbd Urządzenie

polecenie adb proces


demona adb

xx Urządzenie
Spis treści

Emulator

C
Emulator Androida
Czy miałeś kiedyś wrażenie, że cały swój czas spędzasz, czekając na emulator?
Nie ma najmniejszych wątpliwości co do tego, że emulator Androida jest bardzo przydatny. Dzięki
niemu możemy się przekonać, jak nasza aplikacja będzie działała na urządzeniach innych niż te,
do których mamy fizyczny dostęp. Niekiedy jednak można odnieść wrażenie, że emulator działa…
wolno. W tym dodatku wyjaśnimy, dlaczego tak się dzieje. Ale to nie wszystko, damy Ci bowiem
także kilka wskazówek, jak przyspieszyć jego działanie.

AVD AVD AVD AVD AVD

Wszystkie wirtualne urządzenia


z Androidem (AVD) są wykonywane Emulator QEMU
przez emulator QEMU.

Pozostałości

D
Dziesięć najważniejszych zagadnień (których nie opisaliśmy)
Nawet po tym wszystkim, co opisaliśmy w tej książce, wciąż pozostaje
wiele innych interesujących zagadnień.
Jest jeszcze kilka dodatkowych spraw, o których musisz się dowiedzieć. Czulibyśmy się nie
w porządku, gdybyśmy je pominęli, a jednocześnie chcieliśmy oddać w Twoje ręce książkę,
którą dasz radę podnieść bez intensywnego treningu na siłowni. Dlatego zanim odłożysz
tę książkę, przeczytaj kilka dodatkowych zagadnień opisanych w tym dodatku.

Bateria już 664


jest prawie 665
rozładowana… jeśli 666
to kogoś interesuje.
667
668
669
669
670
671
672
Android
673

S
674

xxi
Jak korzystać z tej książki

Wprowadzenie
Nie mogę uwierzyć,
że zamieścili to w książce
o programowaniu aplikacji
na Androida!

odpowiemy na palące
W tej części książki orzy umieścili
pytanie: „Dl acz ego aut
e niezwykłe rzeczy?”.
w książce te wszystki

jesteś tutaj  xxiii


Jak korzystać z tej książki

Dla kogo jest przeznaczona ta książka?


Jeśli możesz odpowiedzieć twierdząco na wszystkie poniższe pytania…

1 Czy już umiesz programować w Javie?

2 Czy chcesz być mistrzem w pisaniu aplikacji na Androida, stworzyć


wspaniały program, zarobić fortunę, a na emeryturę przenieść się
na swoją własną wyspę?
No dobrze, być może te plany
są nieco zbyt dalekosiężne.
3 Czy wolisz coś robić i wykorzystywać zdobytą wiedzę czy słuchać Ale przecież od czegoś trzeba
kogoś na niekończących się wykładach? zacząć, prawda?

…to jest to książka dla Ciebie.

Kto raczej nie powinien sięgać po tę książkę?


Jeśli możesz odpowiedzieć twierdząco na któreś z poniższych pytań…

1 Czy szukasz szybkiego wprowadzenia lub podręcznika do pisania


aplikacji na Androida?

2 Czy wolisz, by piętnaście wrzeszczących małp wyrwało Ci paznokcie


z palców u stóp niż nauczyć się czegoś nowego? Czy uważasz, że
książka o programowaniu aplikacji na Androida powinna opisywać
każde możliwe zagadnienie, a przy okazji jej lektura powinna być
śmiertelnie nużąca, i to im bardziej, tym lepiej?

…to ta książka nie jest dla Ciebie.

[Notatka z działu marketingu:


Ta książka jest dla każdego, kto
ma kartę kredytową… Chociaż
w sumie czeki też przyjmujemy
.]

xxiv Wprowadzenie
Wprowadzenie

Wiemy, co sobie myślisz


„Jakim cudem to może być poważną książką o tworzeniu aplikacji na Androida?”

„Po co te wszystkie obrazki?”

„Czy w taki sposób można się czegokolwiek nauczyć?”


Twój mózg myśli, że
właśnie TO jest istotne.
Wiemy, co sobie myśli Twój mózg
Twój mózg pragnie nowości. Zawsze szuka, przegląda i wyczekuje czegoś
niezwykłego. Tak został stworzony i to pomaga Ci przetrwać.

Zatem co Twój mózg robi z tymi wszystkimi rutynowymi, zwyczajnymi, normalnymi


informacjami, jakie do niego docierają? Otóż robi wszystko, co tylko może,
aby nie przeszkadzały mu w jego naprawdę ważnym zadaniu — zapamiętywaniu
rzeczy, które są naprawdę ważne. Twój mózg nie traci czasu i energii na
zapamiętywanie nudnych informacji; one nigdy nie przechodzą przez filtr
„to jest ewidentnie nieważne”.

Skąd Twój mózg wie, co jest istotne? Załóżmy, że jesteś na codziennej


przechadzce i nagle przed Tobą staje tygrys. Co się dzieje w Twej głowie
i w Twoim ciele?
Wspaniale. Zostało
Neurony płoną. Emocje szaleją. Adrenalina napływa falami. jeszcze tylko 680
głupich, nudnych
I właśnie stąd Twój mózg wie, że… i drętwych stron
To musi być ważne! Nie zapominaj o tym!! ózg
Twój m, że
u waża ie warto
Ale wyobraź sobie, że jesteś w domu albo w bibliotece. Jesteś w bezpiecznym n
TEGO tywać.
miejscu — przytulnym i pozbawionym tygrysów. Uczysz się. Przygotowujesz się zapamię
do egzaminu. Albo rozgryzasz jakiś trudny problem techniczny, którego
rozwiązanie, według szefa, powinno zająć Ci tydzień, a najdalej dziesięć dni.

Jest tylko jeden, drobny problem. Twój mózg stara się Ci pomóc.
Próbuje zapewnić, aby te ewidentncie nieważne informacje nie zajęły
cennych zasobów w Twojej głowie. Zasobów, które powinny zostać
wykorzystane na zapamiętanie naprawdę ważnych rzeczy. Takich jak
tygrysy. Takich jak zagrożenie, jakie niesie ze sobą pożar. Takich jak to,
że nie powinieneś był publikować na Facebooku tych zdjęć z imprezy.
Co gorsze, nie ma żadnego sposobu, aby powiedzieć mózgowi:
„Hej, mózgu mój, dziękuję ci bardzo, ale niezależnie od tego, jak nudna
jest tak książka i jak nieznaczne są emocje, których aktualnie doznaję,
to jednak naprawdę chcę zapamiętać wszystkie te informacje”.

jesteś tutaj  xxv


Jak korzystać z tej książki

Wyobrażamy sobie, że Czytelnik tej książki jest uczniem


trzeba to coś poznać,
A zatem czego trzeba, żeby się czegoś nauczyć? W pierwszej kolejności
o wtłoczenie do
a następnie postarać się tego czegoś nie zapomnieć. I nie chodzi tu jedynie
w dziedzin ie przyswa jania informacji,
głowy suchych faktów. Najnowsze badania prowadzone
ą, że uczenie się wymaga czegoś więcej niż tylko
neurobiologii i psychologii nauczania pokazuj
co potrafi pobudz ić nasze mózgi do działani a.
czytania tekstu. My wiemy,

Oto niektóre z głównych założeń niniejszej książki:


i sprawiają, że nauka
Zobrazuj to. Rysunki są znacznie łatwiejsze do zapamiętania niż same słowa
przypom inaniem sobie i przekazywaniem
staje się zdecydowanie bardziej efektywna (badania nad
rysunków poprawi a efektyw ność zapamię tywania o 89%).
informacji dowodzą, że wykorzystanie
ą, że informac je stają się znacznie bardziej zrozumia łe. Wystarc zy umieścić
Poza tym rysunki sprawiaj
, a nie na następne j
słowa bezpośrednio na lub w okolicach rysunku, do którego się odnoszą
w stanie rozwiąza ć problem , którego te słowa
stronie, a prawdopodobieństwo, że osoby uczące się będą
dotyczą, wzrośnie niemal dwukrotnie.
Stosuj konwersacyjny i spersonalizowany styl. Jak wynika z najnows
zych badań, w testach
końcowych studenci uzyskiwali wyniki o 40% lepsze, jeśli treść była przekazy wana w sposób
j osobie i w konwenc ji rozmow y, a nie w sposób formalny . Zamiast robić wykład
bezpośredni, w pierwsze
osoby zbyt poważni e. Kiedy byłbyś
opowiadaj historyjki. Używaj zwyczajnego języka. Nie traktuj swojej
bardziej uważny — podczas rozmowy przy obiedzie czy podczas wykładu ?
neuronów do aktywnego
Zmuś ucznia do głębszego zastanowienia. Innymi słowy: jeśli nie zmusisz
. Czytelnik musi być zmotyw owany, zaangażowany,
wysiłku, w Twojej głowie nie zdarzy się nic wielkiego
rozwiązy waniem problem ów, wyciągan iem wnioskó w i zdobywaniem
zaciekawiony i podekscytowany
wyzwań , zapraszanie do
nowej wiedzy. A osiągnięcie tego wszystkiego jest możliwe poprzez stawianie
zastanow ienia oraz poprzez nakłanianie
rozwiązywania ćwiczeń i zadawanie pytań zmuszających do
obu półkul mózgow ych i kilku zmysłów .
do działań, które wymagają zaangażowania
Zdobądź — i zatrzymaj na dłużej — uwagę Czytelnika. Każdy znalazł
się kiedyś w sytuacji, gdy
zasypiał po przeczyt aniu pierwsze j strony. Mózg zwraca uwagę na
bardzo chciał się czegoś nauczyć, lecz
ące, dziwne, przykuw ające wzrok, nieoczek iwane. Jednak poznawanie nowego
rzeczy niezwykłe, interesuj
nie interesuj ące, Twój mózg
zagadnienia technicznego wcale nie musi być nudne. Jeśli będzie to zagadnie
przyswoi je znacznie szybciej.
Wyzwól emocje. Teraz już wiemy, że zdolność zapamiętywania informac
ji jest w znacznej mierze
tujemy to, na czym nam zależy. Zapamiętujemy
zależna od ich zawartości emocjonalnej. Zapamię
my. Oczywiś cie nie mamy tu na myśli wzrusza jących historii
w sytuacjach, w których coś odczuwa
psie. Chodzi nam o emocje takie jak zaskocze nie, ciekawo ść, radosne podekscytowanie,
o chłopcu i jego
my, gdy znajdziem y rozwiązanie
„o rany…” i uczucie satysfakcji — „jestem wielki!” — jakie odczuwa
za trudne, lub zdamy sobie sprawę, że znamy
zagadki, nauczymy się czegoś, co powszechnie uchodzi
więcej szczegółów technicznych niż Zenek z działu inżynieri i.

xxvi Wprowadzenie
Wprowadzenie

Metapoznanie — myślenie o myśleniu


Jeśli naprawdę chcesz się czegoś nauczyć i jeśli chcesz się tego nauczyć szybciej i dokładniej,
to zwracaj uwagę na to, jak koncentrujesz uwagę. Myśl o tym, jak myślisz. Dowiedz się, Zastanawiam się,
jak przyswajasz wiedzę. jak zmusić mózg
do zapamiętania
Większość z nas w okresie dorastania nie uczestniczyła w zajęciach z metapoznania albo teorii tych informacji…
nauczania. Oczekiwano od nas, że będziemy się uczyć, ale nie uczono nas, jak mamy to robić.
Zakładamy jednak, że jeśli trzymasz w ręku tę książkę, to chcesz nauczyć się programować
aplikacje na Androida. I prawdopodobnie nie chcesz na to tracić zbyt wiele czasu. Jeśli chcesz
wykorzystać to, co przeczytałeś w tej książce, musisz to zapamiętać. A oprócz tego musisz to
zrozumieć. Aby w możliwie jak największym stopniu wykorzystać zarówno tę, jak i dowolną inną
książkę lub jakikolwiek inny sposób uczenia się, musisz wziąć odpowiedzialność za swój mózg.
Myśl o tym, czego się uczysz.
Sztuczka polega na tym, aby przekonać mózg, że poznawany materiał jest
Naprawdę Ważny. Kluczowy dla Twojego dobrego samopoczucia. Tak ważny
jak tygrys stojący naprzeciw Ciebie. W przeciwnym razie będziesz prowadzić
nieustającą wojnę z własnym mózgiem, który ze wszystkich sił będzie się starał,
aby nowe informacje nie zostały utrwalone.

A zatem jak ZMUSIĆ mózg, aby potraktował programowanie


jak głodnego tygrysa?
Można to zrobić w sposób powolny i męczący lub szybki i bardziej efektywny.
Powolny sposób polega na wielokrotnym powtarzaniu. Oczywiście wiesz, że jesteś w stanie
nauczyć się i zapamiętać nawet najnudniejsze zagadnienie, mozolnie je wkuwając. Po odpowiedniej
ilości powtórzeń Twój mózg stwierdzi: „Zdaje się, że to nie jest dla niego szczególnie ważne, lecz
w kółko to czyta i powtarza, więc przypuszczam, że jakąś wartość to jednak musi mieć”.
Szybszy sposób polega na zrobieniu czegoś, co zwiększy aktywność mózgu, a zwłaszcza jeśli
czynność ta wyzwoli kilka różnych typów aktywności. Wszystkie zagadnienia, o jakich pisaliśmy na
poprzedniej stronie, są kluczowymi elementami rozwiązania i udowodniono, że wszystkie potrafią
pomóc w zmuszeniu mózgu do tego, aby pracował na Twoją korzyść. Na przykład badania
dowodzą, że umieszczenie słów na opisywanych rysunkach (a nie w innych miejscach tekstu na
stronie, na przykład w nagłówku lub wewnątrz akapitu) sprawia, iż mózg stara się zrozumieć
relację pomiędzy słowami a rysunkiem, a to z kolei zwiększa aktywność neuronów. Większa
aktywność neuronów to większa szansa, że mózg uzna informacje za warte zainteresowania i,
ewentualnie, zapamiętania.
Prezentowanie informacji w formie konwersacji pomaga, ponieważ ludzie zdają się wykazywać
większe zainteresowanie w sytuacjach, gdy uważają, że biorą udział w rozmowie, bo oczekuje
się od nich, iż będą śledzić jej przebieg i brać w niej czynny udział. Zadziwiające jest to, iż
mózg zdaje się nie zważać na to, że rozmowa jest prowadzona z książką! Natomiast jeśli sposób
przedstawiania informacji jest formalny i suchy, mózg postrzega to tak samo jak w sytuacji,
gdy uczestniczysz w wykładzie na sali pełnej sennych studentów. Nie ma potrzeby wykazywania
jakiejkolwiek aktywności.
Ale rysunki i rozmowa to dopiero początek…

jesteś tutaj  xxvii


Jak korzystać z tej książki

Oto co MY zrobiliśmy
Użyliśmy rysunków, ponieważ Twój mózg zwraca większą uwagę na obrazy niż na tekst. Jeśli chodzi
o mózg, to faktycznie jeden obraz jest wart tysiąc słów. W sytuacjach, gdy mieliśmy do czynienia zarówno
z tekstem, jak i z rysunekiem, umieściliśmy tekst na rysunku, mózg bowiem działa bardziej efektywnie,
kiedy tekst jest na czymś, co opisuje, niż kiedy jest umieszczony w innym miejscu i stanowi część
większego fragmentu tekstu.

Zastosowaliśmy powtórzenia, wielokrotnie podając tę samą informację na różne sposoby i przy


wykorzystaniu różnych środków przekazu oraz odwołując się do różnych zmysłów. Wszystko to po to,
aby zwiększyć szansę, że informacja zostanie zakodowana w większej ilości obszarów Twojego mózgu.

Użyliśmy pomysłów i rysunków w nieoczekiwany sposób, ponieważ Twój mózg pragnie nowości, a poza
tym staraliśmy się zawrzeć w nich chociaż trochę emocji, gdyż mózg jest skonstruowany tak, że zwraca
uwagę na biochemię związaną z emocjami. Prawdopodobieństwo zapamiętania informacji jest większe,
jeśli sprawia ona, że coś czujemy, nawet jeśli to uczucie nie jest niczym więcej jak lekkim rozbawieniem,
zaskoczeniem lub zainteresowaniem.

Użyliśmy bezpośrednich zwrotów i przekazaliśmy treści w stylu konwersacyjnym, gdyż mózg zwraca
większą uwagę, jeśli sądzi, że prowadzisz rozmowę, niż wtedy, kiedy jesteś jedynie biernym słuchaczem
prezentacji. Mózg działa w ten sposób nawet wówczas, gdy czytasz zapis rozmowy.

Zamieściliśmy w książce wiele ćwiczeń, ponieważ mózg uczy się i pamięta więcej, gdy coś robi, niż gdy
o czymś czyta. Poza tym podane ćwiczenia stanowią wyzwania, choć nie są przesadnie trudne, bo właśnie
takie preferuje większość osób.

Zastosowaliśmy wiele stylów nauczania, gdyż Ty możesz preferować instrukcje opisujące krok po kroku
sposób postępowania, ktoś inny może woleć analizowanie zagadnienia opisanego ogólnie, a jeszcze inna
osoba może chcieć przejrzeć przykładowy fragment kodu. Niezależnie jednak od ulubionego sposobu
nauki każdy skorzysta na tym, że te same informacje będą przedstawiane kilkukrotnie na różne sposoby.

Podaliśmy informacje przeznaczone dla obu półkul Twojego mózgu, gdyż im bardziej mózg będzie
zaangażowany, tym większe będzie prawdopodobieństwo nauczenia się i zapamiętania podawanych
informacji i tym dłużej możesz koncentrować się na nauce. Ponieważ angażowanie jednej półkuli mózgu
często oznacza, że druga może odpocząć, zatem będziesz mógł uczyć się bardziej produktywnie przez
dłuższy okres czasu.

Dodatkowo zamieściliśmy opowiadania i ćwiczenia prezentujące więcej niż jeden punkt widzenia,
ponieważ mózg uczy się łatwiej, gdy jest zmuszony do przetwarzania i podawania własnej opinii.

Postawiliśmy przed Tobą wyzwania, zarówno poprzez podawanie ćwiczeń, jak i stawianie pytań, na które
nie zawsze można odpowiedzieć w prosty sposób, a to dlatego, że mózg uczy się i pamięta, gdy musi
popracować nad czymś. Pomyśl sam: nie można zdobyć dobrej kondycji, obserwując ćwiczenia w telewizji.
Ale dołożyliśmy wszelkich starań, aby zapewnić, że gdy pracujesz, to robisz dokładnie to, co trzeba.
Aby ani jeden dendryt nie musiał przetwarzać trudnego przykładu ani analizować tekstu zbyt lapidarnego
lub napisanego niezrozumiałym żargonem.

Przedstawiliśmy ludzi — w opowiadaniach, w przykładach, na rysunkach itd. — bo Ty też jesteś


człowiekiem. Dlatego Twój mózg zwraca bardziej uwagę na ludzi niż na rzeczy.
xxviii Wprowadzenie
Wprowadzenie

Oto, co możesz zrobić TY,


aby zmusić swój mózg do posłuszeństwa
Wytnij
te porady A zatem zrobiliśmy, co było w naszej mocy. Reszta zależy od Ciebie. Możesz zacząć
i przyklej
na lodówce. od poniższych porad. Posłuchaj swojego mózgu i określ, które sprawdzają się w Twoim
przypadku, a które nie dają pozytywnych rezultatów. Próbuj nowych rzeczy.

1 Zwolnij — im więcej rozumiesz, tym mniej musisz 6 Pij wodę, dużo wody
zapamiętać Mózg pracuje najlepiej, gdy dostarcza mu się dużo płynów.
Nie ograniczaj się jedynie do czytania. Przerwij na chwilę Odwodnienie (do którego może dojść nawet, zanim
lekturę i pomyśl. Kiedy znajdziesz w tekście pytanie, nie poczujesz pragnienie) obniża zdolność percepcji.
zaglądaj od razu na stronę z odpowiedzią. Wyobraź sobie,
że ktoś faktycznie zadaje Ci pytanie. Im bardziej zmusisz 7 Posłuchaj swojego mózgu
swój mózg do myślenia, tym większa będzie szansa, że się Uważaj, kiedy Twój mózg staje się przeciążony. Jeśli
czegoś nauczysz i coś zapamiętasz. spostrzeżesz, że zaczynasz czytać pobieżnie i zapominać,
o czym przeczytałeś przed chwilą, to najwyższy czas, żeby
2 Rób ćwiczenia, notuj sobie zrobić przerwę. Po przekroczeniu pewnego punktu
Zamieściliśmy je w książce, ale gdybyśmy zrobili je nie będziesz się uczył szybciej, „wciskając” do głowy
za Ciebie, to niczym nie różniłoby się to od sytuacji, więcej informacji, co gorsze, może to zaszkodzić całemu
w której ktoś za Ciebie wykonywałby ćwiczenia fizyczne. procesowi nauki.
I nie ograniczaj się jedynie do czytania ćwiczeń. Używaj
ołówka. Można znaleźć wiele dowodów na to, że fizyczna 8 Poczuj coś!
aktywność podczas nauki może poprawić jej wyniki. Twój mózg musi wiedzieć, że to, czego się uczysz, jest
ważne. Z zaangażowaniem śledź zamieszczane w tekście
3 Czytaj fragmenty oznaczone jako „Nie istnieją opowiadania. Nadawaj własne tytuły zdjęciom. Zalewanie
głupie pytania” się łzami ze śmiechu po przeczytaniu głupiego dowcipu
Chodzi tu o wszystkie fragmenty umieszczone z boku i tak jest lepsze od braku jakiejkolwiek reakcji.
tekstu. Nie są to fragmenty opcjonalne — stanowią one
część podstawowego tekstu książki! Nie pomijaj ich. 9 Pisz jak najwięcej kodu
Istnieje tylko jeden sposób, by nauczyć się programowania
4 Niech lektura tej książki będzie ostatnią rzeczą, jaką aplikacji na Androida: pisanie kodu, a im będzie go
robisz przed pójściem spać — a przynajmniej ostatnią więcej, tym lepiej. I właśnie to będziesz robić podczas
rzeczą stanowiącą wyzwanie intelektualne lektury tej książki. Pisanie programów jest umiejętnością,
Pewne elementy procesu uczenia się (a w szczególności a jedynym sposobem jej nabycia jest ciągła praktyka.
przenoszenie informacji do pamięci długoterminowej) Mamy zamiar dać Ci wiele okazji do tego: w każdym
następują po odłożeniu książki. Mózg potrzebuje trochę rozdziale zamieściliśmy ćwiczenia stawiające przed Tobą
czasu dla siebie i musi dodatkowo przetworzyć dostarczone problemy, które możesz rozwiązać. Nie pomijaj ich —
informacje. Jeśli podczas tego koniecznego na wykonanie podczas rozwiązywania ćwiczeń możesz się bardzo wiele
dodatkowego przetwarzania czasu zmusisz go do innej nauczyć. Zamieściliśmy także rozwiązania wszystkich
działalności, to część z przyswojonych informacji może ćwiczeń — śmiało zerknij na rozwiązanie, jeśli utkniesz!
zostać utracona. (Łatwo jest utknąć na jakiejś drobnostce). Staraj się
jednak rozwiązać problem, zanim zajrzysz do rozwiązania.
5 Mów o zdobywanych informacjach — głośno I koniecznie, zanim przejdziesz do dalszej części książki,
Mówienie aktywuje odmienne obszary mózgu. Jeśli postaraj się uruchomić programy, nad którymi pracujesz.
próbujesz coś zrozumieć lub zwiększyć szansę na
zapamiętanie informacji na dłużej, powtarzaj to na głos.
Jeszcze lepiej — staraj się to na głos komuś wytłumaczyć.
W ten sposób nauczysz się szybciej, a oprócz tego będziesz
mógł odkryć kwestie, o których nie wiedziałeś podczas
czytania tekstu książki. jesteś tutaj  xxix
Jak korzystać z tej książki

Przeczytaj to
Ta książka jest doznaniem poznawczym, a nie podręcznikiem. Celowo usunęliśmy z niej
wszystko, co mogłoby Ci przeszkadzać w uczeniu się i poznawaniu materiału zamieszczonego
w danym miejscu książki. W przypadku pierwszej lektury tej książki należy zacząć od samego
początku, gdyż jej dalsze fragmenty bazują na wiedzy, którą musisz zdobyć wcześniej.

Zakładamy, że nowością jest dla Ciebie Android, ale nie Java

Aplikacje na Androida będziemy pisać, używając kombinacji kodu pisanego w Javie


i kodu XML. Zakładamy, że miałeś już wcześniej kontakt z językiem Java. Jeśli jeszcze
nie napisałeś w ogóle żadnego programu w Javie, to prawdopodobnie powinieneś sięgnąć
po książkę Java. Rusz głową! Wydanie II.

Zaczynamy od napisania aplikacji w rozdziale 1.

Możesz nam wierzyć lub nie, ale nawet jeśli nigdy wcześniej nie zdarzyło Ci się napisać
żadnej aplikacji na Androida, to możesz wskoczyć na głęboką wodę i zacząć pisać własne
aplikacje. Przy okazji poznasz trochę Android Studio — oficjalne IDE do pisania aplikacji
na Androida.

Przykłady zostały zaprojektowane pod kątem nauki

Podczas lektury niniejszej książki napiszesz kilka różnych aplikacji. Niektóre z nich są
bardzo małe, co pozwoli Ci skoncentrować się na konkretnym aspekcie programowania na
Androida. Inne z kolei są większe, dzięki czemu przekonasz się, jak ich różne komponenty
pasują do siebie i ze sobą współpracują. W książce nie dokończymy wszystkich fragmentów
każdej z tych aplikacji, możesz to jednak zrobić samodzielnie. To wszystko zalicza się do
doznania poznawczego. Kody źródłowe aplikacji przedstawionych w tej książce można
pobrać z serwera FTP wydawnictwa Helion: ftp://ftp.helion.pl/przyklady/andrrg.zip

Aktywności NIE są opcjonalne

Ćwiczenia i aktywności nie są jedynie dodatkami, są one elementem treści tej książki.
Niektóre z nich mają wspomóc Twoją pamięć, inne — ułatwić Ci zrozumienie, a jeszcze inne
— pomóc w wykorzystaniu nabytej wiedzy i umiejętności. Nie pomijaj tych ćwiczeń!

xxx Wprowadzenie
Wprowadzenie

Powtórzenia są celowe i ważne

Jedną z cech, która wyróżnia książki z serii Rusz głową!, jest to, że nam naprawdę zależy, żebyś
wszystko zrozumiał. Chcemy także, byś kończąc lekturę tej książki, pamiętał wszystko, co w niej
przeczytałeś. W przypadku większości książek informacyjnych i encyklopedycznych przyswojenie
i zapamiętanie informacji nie jest celem, ale w tej książce chodzi o naukę, dlatego znajdziesz
w niej wiele pojęć, które pojawiają się kilka razy.

Do ćwiczeń z cyklu „Wysil szare komórki” nie podaliśmy odpowiedzi

Do niektórych ćwiczeń w ogóle nie można podać jednej dobrej odpowiedzi, w innych przypadkach
to doświadczenie, które zdobywasz, rozwiązując te ćwiczenia, ma dać Ci możliwość określenia, czy
i kiedy podana odpowiedź będzie poprawna. W niektórych ćwiczeniach z tej serii znajdziesz także
podpowiedzi, które ułatwią Ci znalezienie rozwiązania.

jesteś tutaj  xxxi


Zespół recenzentów

Zespół recenzentów technicznych

Edward

Recenzenci techniczni książki

Edward Yue Shung Wong pasjonował się programowaniem od czasu, gdy w 2006
roku napisał pierwszy wiersz kodu w języku Haskell. Obecnie pracuje w sercu
londyńskiego City nad sterowanym zdarzeniami przetwarzaniem danych handlowych.
Edward z radością dzieli się swoją pasją programistyczną z londyńską społecznością
programistów Javy w ramach London Java Community i Software Craftsmanship
Community. Jeśli akurat przebywa z dala od klawiatury, to można go znaleźć na
boisku do piłki nożnej, gdzie czuje się w swoim żywiole, lub obejrzeć na YouTubie
(@arkangelofkaos), jak gra w gry komputerowe.

Tony Williams jest programistą używającym Javy i piszącym aplikacje na Androida.

xxxii Wprowadzenie
Wprowadzenie

Podziękowania
Dla naszej redaktorki

Chcieliśmy bardzo podziękować naszej redaktorce, Meghan Blanchette,


za pracę nad serią Head First. Jej opinie i spostrzeżenia były bezcenne.
Bardzo doceniamy te wszystkie sytuacje, w których zwracała nam uwagę,
że choć pisane przez nas słowa mają odpowiednie litery, to jednak są Meghan Blanchette
zapisane w niewłaściwej kolejności.

Bardzo dziękujemy także Bertowi Batesowi za nauczenie nas, jak odrzucić


stary zestaw reguł, oraz za wpuszczenie nas do swojego mózgu. Ta książka
stała się nieporównanie lepsza dzięki jego reakcjom i opiniom.

Dla zespołu O’Reilly

Wielkie podziękowania składamy Mike’owi Hendricksonowi za wiarę, jaką


w nas pokładał, i za wybranie nas do napisania tej książki; Courtney Nash
za pomoc na początkowych etapach pracy nad tą książką oraz zespołowi
wstępnego wydania za pracę nad udostępnieniem wstępnej wersji niniejszej
książki. I w końcu chcieliśmy także podziękować Melanie Yarbrough
i Jasmine Kwityn oraz całej reszcie zespołu produkcyjnego za doskonałe
kierowanie pracami nad książką w tracie całego procesu wydawniczego
oraz za wspaniałą i ciężką pracę za kulisami.

Dla rodziny, przyjaciół i kolegów

Pisanie książki z serii Head First jest jak jazda kolejką górską, a prace nad
tą akurat książką nie były pod tym względem wyjątkiem. Ta książka w ogóle
nie ujrzałaby światła dziennego, gdyby nie uprzejmość i wsparcie naszych
przyjaciół i rodziny. Specjalne podziękowania przesyłamy: Andy’emu P,
Steve’owi, Jacqui, Angeli, Paulowi B, Mamie, Tacie, Carlowi, Robowi
i Lorraine.

Dla pozostałych osób

Nasz zespół recenzentów technicznych wykonał doskonałą robotę, utrzymując


nas w ryzach i zapewniając, żeby zagadnienia będące przedmiotem tej książki
były opisane dokładnie i prawidłowo. Jesteśmy także niezmiernie wdzięczni
wszystkim osobom, które przekazywały nam swoje uwagi po ukazaniu się
pierwszych wydań niniejszej książki. Uważamy, że dzięki Wam ta książka
stała się dużo, dużo lepsza.

I w końcu chcieliśmy podziękować Kathy Sierze i Bertowi Batesowi


za stworzenie tej niesamowitej serii książek.

jesteś tutaj  xxxiii


xxxiv Wprowadzenie
1. Zaczynamy

Skok na głęboką wodę

Android błyskawicznie podbił świat.


Każdy chce mieć smartfon lub tablet, a urządzenia z Androidem są niezwykle popularne.
W tej książce nauczymy Cię, jak pisać własne aplikacje, a zaczniemy od pokazania procesu
przygotowania bardzo prostej aplikacji i uruchomienia jej na wirtualnym urządzeniu z Androidem.
W trakcie tych prac poznasz także kilka podstawowych komponentów wszystkich aplikacji
na Androida, takich jak aktywności i układy. Jedyną rzeczą, której będziesz do tego
potrzebować, jest znajomość Javy, choć wcale nie musisz być w niej mistrzem…

to jest nowy rozdział  1


Android — przegląd

Witamy w Androidowie Nasze aplikacje na Androida


będziemy tworzyć, używając
Android jest najpopularniejszą na świecie mobilną platformą
połączenia Javy i XML-a.
systemową. Według ostatnich szacunków na świecie jest obecnie
używanych ponad miliard urządzeń z tym systemem, przy czym ich W trakcie prac wszystko
liczba błyskawicznie rośnie. dokładnie wyjaśnimy, jednak
Android jest kompleksową platformą o otwartym kodzie źródłowym, aby w pełni skorzystać z tej
bazującą na Linuksie i wspieraną przez Google’a. Stanowi on potężną
książki, musisz dysponować
platformę programistyczną, zawierającą absolutnie wszystko, czego
potrzeba do tworzenia wspaniałych aplikacji z użyciem języków Java w miarę dobrą znajomością
i XML. Co więcej, Android pozwala na rozpowszechnianie tych języka Java.
aplikacji i uruchamianie ich na bardzo wielu różnych urządzeniach
— telefonach, tabletach itd. jak mają
Układy informują,
ególne
wyglądać poszcz
A zatem z czego składa się typowa aplikacja na Androida? ekra ny ap lik ac ji.

Układy określają postać poszczególnych ekranów


Typowa aplikacja na Androida składa się z jednego lub kilku ekranów.
Postać każdego z tych ekranów definiowana jest przy użyciu układu.
Te układy zazwyczaj są tworzone w języku XML i mogą zawierać
komponenty graficznego interfejsu użytkownika (w skrócie GUI,
od angielskich słów graphical user interface), takie jak przyciski,
pola tekstowe czy etykiety.

Kod napisany w Javie definiuje, co robi aplikacja


Jednak układy określają jedynie wygląd aplikacji. To, co aplikacja
będzie robić, określamy natomiast, pisząc kod w języku Java. Specjalne
klasy Javy, nazywane aktywnościami, określają, które układy należy
wybierać oraz jak aplikacja ma reagować na czynności wykonywane
przez użytkownika. Na przykład jeśli układ zawiera przycisk,
Aktywności
to w aktywności musimy napisać kod, który określi, co aplikacja definiują,
ma zrobić po naciśnięciu tego przycisku. co aplikacja
ma robić.

Czasami konieczne są także dodatkowe zasoby


Oprócz kodu Javy oraz układów aplikacje na Androida często będą
potrzebowały także zasobów dodatkowych, takich jak obrazki lub dane.
A zatem do tworzonych aplikacji można dołączać takie dodatkowe pliki. Zasoby mogą
zawierać na
Innymi słowy: aplikacje na Androida są w zasadzie grupami plików przykład pliki
umieszczonych w odpowiednich katalogach. Podczas budowania dźwiękowe
i graficzne.
aplikacji wszystkie te pliki zostają ze sobą połączone i wspólnie
tworzą aplikację, którą można uruchomić na urządzeniu.

2 Rozdział 1.
Zaczynamy

Platforma Android w szczegółach Nie przejmuj się, jeśli masz


wrażenie, że to bardzo dużo
Spokojnie jak na początek.
Platforma Android składa się z kilku różnych komponentów.
Tworzą ją standardowe aplikacje, takie jak Kontakty, interfejsy Pokazujemy Ci tutaj poglądowy
programistyczne, API, pomagające nam w kontrolowaniu wyglądu schemat wszystkiego, co składa się na platformę
i działania aplikacji, jak również bardzo wiele dodatkowych plików systemu Android. Jego poszczególne komponenty
i bibliotek. Poniższy schemat pokazuje, w jaki sposób tworzą one będziemy opisywać bardziej szczegółowo w dalszej
jedną spójną platformę systemową. części książki, kiedy pojawi się taka konieczność.

Android jest wyposażony


w zestaw podstawowych Aplikacje
aplikacji, takich jak
Kontakty, Kalendarz i Mapy, Aparat Kontakty Telefon Internet ...
oraz w przeglądarkę WWW.
Pisząc własne aplikacje, Framework aplikacji
mamy dostęp do tych
samych podstawowych Menedżer Menedżer Dostawcy System
API, które są używane aktywności okien treści widoków
przez podstawowe Środowisko
aplikacje. Używając tych Menedżer Menedżer Menedżer Menedżer Menedżer uruchomieniowe
API, kontrolujemy wygląd pakietów telefonii zasobów lokalizacji powiadomień Androida jest
aplikacji i jej działanie. wyposażone
Biblioteki w zestaw
Środowisko wykonawcze podstawowych
Poniżej frameworku
Surface Media Android bibliotek,
SQLite
Manager Framework Podstawowe implementujących
aplikacji ukryty jest
biblioteki przeważającą
zestaw bibliotek C i C++.
OpenGL | ES FreeType WebKit część języka
Dostęp do nich zapewniają
programowania Java.
API należące do
Każda aplikacja
frameworku aplikacji.
SGL SSL libc na Androida jest
wykonywana we
Poniżej wszystkich
własnym procesie.
pozostałych warstw
platformy Android Jądro systemu Linux
umieszczone jest jądro
Sterownik
systemu Linux. To ono Sterownik ekranu Sterownik aparatu Binder (IPC)
pamięci flash
odpowiada za obsługę
sterowników oraz
Sterownik Sterownik Sterownik Zarządzanie
podstawowych usług, klawiszy WiFi audio energią
takich jak zabezpieczenia
i zarządzanie pamięcią.

Na szczęście okazuje się, że te wszystkie potężne biblioteki Androida są udostępniane


przez interfejsy programowania aplikacji — API — wchodzące w skład frameworku
do tworzenia aplikacji i to właśnie z tych API będziemy korzystać, pisząc własne
programy. Zatem wszystkim, czego potrzebujesz, by zacząć, jest znajomość języka
programowania Java i pomysł na wspaniałą aplikację.

jesteś tutaj  3
Czynności

Oto, co mamy zamiar zrobić


A zatem, bez niepotrzebnego przeciągania, spróbujmy utworzyć
pierwszą, prostą aplikację na Androida. W tym celu musimy
wykonać kilka czynności:

1 Przygotować środowisko programistyczne.


W pierwszej kolejności musimy zainstalować Android
Studio — środowisko programistyczne zawierające
wszystkie narzędzia niezbędne do pisania aplikacji
na Androida.

2 Napisać i zbudować prostą aplikację.


Używając Android Studio, przygotujemy prostą
aplikację, która wyświetla na ekranie jakiś tekst.

3 Uruchomić aplikację w emulatorze.


Skorzystamy z wbudowanego emulatora,
by uruchomić aplikację i przekonać się, jak działa.

4 Zmodyfikować aplikację.
I na koniec wprowadzimy kilka zmian w aplikacji
stworzonej w kroku 2.

Nie istnieją
głupie pytania
P: Czy wszystkie aplikacja na P: W jakim stopniu muszę znać P: Czy muszę znać biblioteki Swing
Androida są pisane w Javie? Javę, by pisać aplikacje na Androida? i AWT?

O: Aplikacje na Androida można pisać O: Powinieneś dobrze znać Javę SE. Jeśli O: Android nie używa ani biblioteki
także w innych językach, lecz prawda nie czujesz się pewnie, to radzimy sięgnąć Swing, ani biblioteki AWT, dlatego nie
jest taka, że przeważająca większość najpierw po książkę Java. Rusz głową! przejmuj się, jeśli nie masz doświadczeń
programistów używa Javy. napisaną przez Kathy Sierrę i Berta Batesa. w pisaniu w Javie aplikacji o graficznym
interfejsie użytkownika.

4 Rozdział 1.
Zaczynamy
Jesteś tutaj.

¨
Środowisko programistyczne Przygotowanie środowiska
¨ Stworzenie aplikacji
¨ Uruchomienie aplikacji
Java jest najpopularniejszym językiem używanym do pisania aplikacji na Androida. ¨ Modyfikacja aplikacji
Jednak urządzenia działające pod tym systemem nie wykonują plików .class ani .jar.
Zamiast tego, aby poprawić szybkość i wydajność działania, urządzenia te korzystają
z własnego, zoptymalizowanego formatu zapisu skompilowanego kodu. A to oznacza,
że do pisania aplikacji na Androida nie można używać zwyczajnych środowisk do
pisania kodu w Javie — konieczne są bowiem specjalne narzędzia do konwersji
skompilowanego kodu do formatu używanego przez platformę Android, do
zainstalowania ich na urządzeniu oraz debugowania kodu po uruchomieniu aplikacji.

Wszystkie te narzędzia wchodzą w skład Android SDK. Spójrzmy zatem, co on zawiera.

Android SDK
Android SDK (Software Development Kit) to zestaw narzędzi i bibliotek niezbędnych
do pisania aplikacji na Androida. W jego skład wchodzą:

Platforma SDK
Istnieje jedna taka platforma Dokumentacja
dla każdej wersji Androida. By móc korzystać
z najnowszej dokumentacji
bez połączenia z internetem.
Narzędzia SDK
Narzędzia do debugowania Wsparcie dla Androida
i testowania oraz inne przydatne Dodatkowe API, które nie
programy.
SDK wchodzą w skład standardowej
id
ro
platformy.
d
An
Aplikacje przykładowe Płatności Google Play
Jeśli do zrozumienia niektórych API Pozwalają na zintegrowanie
potrzebujesz kodu praktycznych z aplikacją mechanizmów
To tylko kilka do rozliczeń finansowych.
przykładów, to mogą Ci w tym najważniejszych
pomóc aplikacje przykładowe. elementów.

Android Studio to specjalna wersja IntelliJ IDEA


IntelliJ IDEA jest jednym z najpopularniejszych zintegrowanych środowisk
programistycznych (w skrócie: IDE) do pisania programów w Javie. Android Studio
jest wersją tego IDE, która zawiera Android SDK i dodatkowe narzędzia graficzne
wspomagające tworzenie aplikacji na Androida.

Oprócz edytora oraz dostępu do narzędzi i bibliotek wchodzących w skład Android


SDK Android Studio udostępnia także szablony, które mogą bardzo pomóc w tworzeniu
nowych aplikacji i klas i znacząco ułatwiają wykonywanie takich czynności jak pakowanie
i uruchamianie aplikacji.
jesteś tutaj  5
Instalacja

¨
Zainstaluj Javę Przygotowanie środowiska
¨ Stworzenie aplikacji
¨ Uruchomienie aplikacji
Android Studio jest środowiskiem programistycznym służącym do pisania ¨ Modyfikacja aplikacji
aplikacji w języku Java, musisz się zatem upewnić, czy na swoim komputerze masz
zainstalowaną odpowiednią wersję Javy.

Zacznij od sprawdzenia wymagań systemowych Android Studio, aby dowiedzieć


się, której wersji Java Development Kit (JDK) i Java Runtime Environment (JRE)
będziesz potrzebować. Informacje o tych wymaganiach można znaleźć na stronie:
witrynach firm u
Adresy stron w
og le od czasu do czas
http://developer.android.com/sdk/index.html#Requirements Oracle i Go śli zatem te
się zmieniają. Je
prawidłowo, to
Kiedy już się dowiesz, jakich wersji JDK i JRE będziesz potrzebować, nie będą działać trochę poszukać.
będziesz musiał
pobierz je ze strony:

http://www.oracle.com/technetwork/java/javase/downloads/index.html

Jeśli ten adres URL ulegnie


Następnie zainstaluj Android Studio zmianie, to poszukaj hasła
„Android Studio” w witrynie
Po pobraniu i zainstalowaniu Javy musisz pobrać Android Studio. developer.android.com.
Znajdziesz je na tej stronie:

https://developer.android.com/sdk/installing/index.html?pkg=studio
W tej książce nie zamieszczamy instrukcji
Na tej samej stronie zostały także opublikowane informacje dotyczące instalacji instalacji Android Studio, gdyż mogłoby
Android Studio. Postępuj zgodnie z nimi, aby zainstalować IDE na swoim się okazać, że bardzo szybko staną się one
nieaktualne. Postępuj zgodnie z opisem
komputerze. Kiedy to już zrobisz, uruchom Android Studio i postępuj zgodnie instalacji zamieszczonym na stronie,
z instrukcjami, by zainstalować najnowsze narzędzia SDK i biblioteki. a na pewno bez problemu się uda.

Kiedy wszystko będzie już gotowe, zobaczysz ekran powitalny Android Studio.
Teraz będziesz już gotów, by rozpocząć tworzenie swojej pierwszej aplikacji
na Androida.

To jest ekran
powitalny
Android Studio.
Prezentuje on
zestaw opcji
odpowiadający
czynnościom,
jakie można
wykonać.

6 Rozdział 1.
Zaczynamy

Nie istnieją
głupie pytania

P: Napisaliście, że do pisania aplikacji P: A czy można pisać aplikacje na P: Większość aplikacji jest
na Androida będziemy używać Android Android bez używania żadnego IDE? budowanych przy użyciu gradle?
Studio. Czy to konieczne? Napisaliście chyba wcześniej,
O: Tak, można, lecz wymaga to znacznie że bardzo wielu programistów
O: Precyzyjnie rzecz ujmując, Android więcej pracy. Większość aplikacji na używa Android Studio?
Studio nie jest niezbędne do pisania aplikacji Androida jest obecnie przygotowywanych
na Androida. Będziesz potrzebować przy użyciu programu do budowania O: Android Studio udostępnia graficzny
dowolnego narzędzia umożliwiającego o nazwie gradle. Projekty gradle można interfejs do obsługi gradle, jak również wiele
pisanie i kompilację kodu Javy oraz paru tworzyć i budować, używając dowolnego innych narzędzi służących do tworzenia
innych specjalistycznych narzędzi służących edytora tekstów i wiersza poleceń. układów, odczytu dzienników oraz
do konwersji skompilowanego kodu do debugowania.
postaci, w której urządzenia z Androidem P: Narzędzie do budowania?
będą go w stanie wykonywać. Czy gradle to coś takiego jak ANT?

P: Czy zatem mogę używać mojego O: Owszem, lecz gradle ma nieporównanie


ulubionego IDE? większe możliwości. Gradle, podobnie jak
ANT, potrafi kompilować i wdrażać kod,
O: Android Studio jest oficjalnym IDE ale oprócz tego korzysta także z Maven do
służącym do pisania aplikacji na Androida, pobierania wszystkich dodatkowych bibliotek
jednak popularne jest także Eclipse. Więcej używanych w kodzie. Gradle używa Groovy
informacji na ten temat można znaleźć jako języka skryptowego, co oznacza,
na stronie https://developer.android.com/ że całkiem łatwo można tworzyć bardzo
tools/sdk/eclipse-adt.html. złożone skrypty do budowy projektów.

Stwórzmy prostą aplikację


Skoro już przygotowałeś własne środowisko programistyczne,
jesteś gotowy do stworzenia pierwszej aplikacji na Androida.
Oto, jak ona będzie wyglądać:

Ta aplikacja jest
To jest nazwa bardzo prosta, ale jak
aplikacji. na Twoją pierwszą
aplikację na Androida
w zupełności
wystarczy.

To jest przykładowy
napis, który Android
Studio samo dodało
do aplikacji.

jesteś tutaj  7
Utworzenie projektu Ten etap prac już zakończyłeś,
więc oznaczyliśmy go ptaszkiem.

Stwórzmy prostą aplikację ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Tworząc nową aplikację, zawsze trzeba utworzyć dla niej nowy projekt.
¨ Modyfikacja aplikacji
Upewnij się, czy Android Studio jest uruchomione, a następnie
wykonaj następujące czynności:

1. Utworzenie nowego projektu


Ekran powitalny Android Studio udostępnia kilka opcji pozwalających
na wykonywanie różnych czynności. Ponieważ chcemy utworzyć nowy
projekt, kliknij opcję Start a new Android Studio project.

Kliknij tę opcję, aby


rozpocząć tworzenie
nowego projektu
Android Studio.

Tu będą wyświetlane wszystkie


projekty, które stworzyłeś
już wcześniej. Ponieważ ten
projekt jest pierwszy, lista
jest jeszcze pusta.

8 Rozdział 1.
Zaczynamy

Tworzenie aplikacji (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
2. Konfiguracja projektu
Teraz musisz skonfigurować aplikację i podać jej nazwę, nazwę
firmowej domeny oraz lokalizację plików aplikacji na dysku.
Nazwa pakietu już
Android Studio używa nazwy firmowej domeny i nazwy aplikacji do na zawsze zostanie
wygenerowania nazwy pakietu aplikacji. Na przykład jeśli nadasz Obejrzyj to! taka sama.
aplikacji nazwę Moja pierwsza apka, a nazwą firmowej domeny będzie
hfad.com, to nazwą pakietu użytą przez Android Studio będzie Stanowi ona unikalny identyfikator
com.hfad.mojapierwszaapka. aplikacji i jest używana do
zarządzania jej wieloma wersjami.
Wpisz zatem Moja pierwsza Apka jako nazwę aplikacji (w polu
Application name) i hfad.com jako nazwę domeny (w polu Company
Domain), a potem zaakceptuj sugerowaną lokalizację projektu.
Następnie kliknij przycisk Next.

Nazwa aplikacji jest wyświetlana


w sklepie Google Play i w wielu
innych miejscach.

Kreator utworzył
nazwę pakietu na
podstawie nazwy
aplikacji i nazwy
firmowej domeny.

przechowywane w tym katalogu.


Wszystkie pliki tego projektu będą

jesteś tutaj  9
Poziom API

Tworzenie aplikacji (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
3. Określenie poziomu API ¨ Modyfikacja aplikacji

Teraz musisz określić, którego poziomu API platformy Android będzie używać
tworzona aplikacja. Numer poziomu API rośnie wraz z każdą nową wersją
Androida. Jeśli nie chcesz, by aplikacja działała wyłącznie na najnowszych
urządzeniach, to wybierz jeden z wcześniejszych API.
Na następnej stronie znajdziesz .
W tym przykładzie wybierzemy API poziomu 15, co oznacza, że aplikacja będzie więcej informacji o poziomach API
w stanie działać na przeważającej większości urządzeń. Co więcej, utworzymy
wyłącznie aplikację w wersji przeznaczonej na telefony i tablety, dlatego nie
zaznaczaj żadnych innych pól wyboru widocznych w tym oknie kreatora.

Kiedy już wybierzesz odpowiednie opcje, kliknij przycisk Next.

Minimalna wymagana
wersja SDK to najniższa
wersja obsługiwana
przez aplikację. Twoja
aplikacja będzie działać
na urządzeniach z API
wybranego lub wyższego
poziomu, natomiast na
urządzeniach z API
niższego poziomu nie uda
się uruchomić aplikacji.

10 Rozdział 1.
Zaczynamy

Wersje Androida pod lupą


Prawdopodobnie słyszałeś sporo słodkich rzeczy w kontekście Androida. Chodzi o takie terminy
jak Ice Cream Sandwich, Jelly Bean, KitKat czy też Lollipop1. O co chodzi z tymi słodyczami?

Wszystkie wersje Androida mają swój numer i nazwę kodową. Numer wersji dokładnie określa numer
platformy Android (na przykład 5.0), natomiast nazwa kodowa to nieco bardziej „przyjazna” nazwa, która może
obejmować kilka kolejnych wersji Androida (tak jest na przykład w przypadku wersji Jelly Bean). Poziom API
odnosi się do wersji API używanych przez aplikacje. Na przykład Androidowi 5.0 odpowiada poziom API 21.

Version Codename API level


1.0 1
1.1 2
1.5 Cupcake 3
1.6 Donut 4 Niemal nikt już nie
używa tych wersji
2.0 Eclair 5 API.
2.01 Eclair 6
2.1 Eclair 7
2.2.x Froyo 8
2.3 - 2.3.2 Gingerbread 9
2.3.2 - 2.3.7 Gingerbread 10
3.0 Honeycomb 11
3.1 Honeycomb 12
3.2 Honeycomb 13
4.0 - 4.0.2 Ice Cream Sandwich 14
4.0.3-4.0.4 Ice Cream Sandwich 15
4.1 Jelly Bean 16
4.2 Jelly Bean 17
4.3 Jelly Bean 18 Większość urządzeń
używa obecnie tych
4.4 KitKat 19 wersji API.
4.4 KitKat (z rozszerzeniem dla 20
urządzeń ubieralnych)
5.0 Lollipop 21

Pisząc aplikacje na Andorida, naprawdę trzeba poważnie zastanowić się nad tym, z jakimi wersjami systemu
nasza aplikacja ma być zgodna. Jeśli określimy, że będzie ona zgodna tylko z najnowszą wersją SDK, to może się
okazać, że nie będzie w stanie działać na wielu urządzeniach. Udziały procentowe urządzeń z poszczególnymi
wersjami Androida można znaleźć na stronie https://developer.android.com/about/dashboards/index.html.

1
Wszystkie te nazwy odpowiadają nazwom słodyczy: ice cream sandwich to lodowa kanapka, jelly bean to żelki w kształcie
fasolek, a lollipop to lizak — przyp. tłum.

jesteś tutaj  11
15 000 metrów

Aktywności z wysokości 15 tysięcy metrów ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Kolejną rzeczą, którą musisz zrobić, jest dodanie do projektu aktywności.
¨ Modyfikacja aplikacji
Każda aplikacja na Androida jest zestawem ekranów, a każdy z ekranów składa się
z aktywności i układu.

Aktywność (ang. activity) jest pojedynczą, precyzyjnie zdefiniowaną czynnością, Układy definiują,
którą użytkownik może wykonywać. Można zatem utworzyć aktywność pozwalającą
na redagowanie e-maila, robienie zdjęcia czy też odnajdywanie kontaktu. Aktywności
jak będzie
są zazwyczaj skojarzone z jednym ekranem, a ich kod jest pisany w Javie. wyglądał interfejs
Układ (ang. layout) opisuje wygląd ekranu. Układy są tworzone w formie zwyczajnych
plików XML i informują system o tym, w jaki sposób na ekranie są rozmieszane
użytkownika.
poszczególne elementy interfejsu użytkownika.

Przyjrzyjmy się nieco dokładniej, jak aktywności i układy współpracują ze sobą, Aktywności
by utworzyć interfejs użytkownika aplikacji:
definiują akcje.
1 Urządzenie uruchamia
1
aplikację i tworzy obiekt
aktywności.
<Układ>
2
2 Obiekt aktywności
określa układ. </Układ>
<Układ>
Aktywność
3 Aktywność nakazuje
Urządzenie </Układ> Układ
urządzeniu wyświetlenie 3
układu na ekranie.

4 Użytkownik prowadzi
interakcje z układem
wyświetlonym na ekranie.

5 Aktywność odpowiada na
te interakcje, wykonując 4
odpowiedni kod aplikacji. 5

6 Aktywność aktualizuje
treści w układzie…

7 …które użytkownik widzi 6 Aktywność


na ekranie. 7 Urządzenie
Użytkownik

Skoro już wiesz nieco więcej o aktywnościach i układach,


możemy wykonać kilka ostatnich kroków kreatora
i wygenerować prostą aktywność i układ.

12 Rozdział 1.
Zaczynamy

Tworzenie aplikacji (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
4. Utworzenie aktywności ¨ Modyfikacja aplikacji

Kolejne okno kreatora prezentuje grupę szablonów, których można używać


do tworzenia aktywności i układów. Musisz wybrać jeden z nich. Ponieważ
mamy zamiar utworzyć aplikację składającą się z prostej aktywności i układu,
wybierz szablon Blank Activity (pusta aktywność) i kliknij przycisk Next.

Dostępnych jest
kilkanaście innyc także
rodzajów aktywn h

które można wybr ci,
tym razem upew ać, ale
czy został zaznacnij się,
szablon Blank Ac zony
tivity.

jesteś tutaj  13
Konfiguracja aktywności

Tworzenie aplikacji (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
5. Skonfigurowanie aktywności ¨ Modyfikacja aplikacji

Teraz zostaniesz poproszony o podanie nazwy ekranu aktywności i układu.


Dodatkowo musisz także określić tytuł ekranu i podać nazwę zasobu menu.
Jako nazwę aktywności (w polu Activity Name) wpisz MainActivity,
natomiast układowi nadaj (w polu Layout Name) nazwę activity_main.
Aktywność jest klasą Javy, a układ — plikiem XML. Oznacza to, że
podane informacje spowodują utworzenie pliku źródłowego Javy o nazwie
MainActivity.java i pliku XML o nazwie activity_main.xml.

Kiedy klikniesz przycisk Finish, Android Studio wygeneruje aplikację.

Jako nazwę aktywności


podaj „MainActivity”,
a jako nazwę układu
„activity_main”,
w pozostałych polach
zaś zostaw domyślne
wartości.

14 Rozdział 1.
Zaczynamy

Właśnie utworzyłeś swoją pierwszą aplikację ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
na Androida ¨
¨
Uruchomienie aplikacji
Modyfikacja aplikacji

A co się właściwie stało?

 Kreator Android Studio utworzył projekt aplikacji i skonfigurował ją zgodnie


z wybranymi opcjami.
Określiłeś, z którą wersją Androida ma być zgodna tworzona aplikacja, a kreator utworzył
wszystkie pliki i katalogi, których będzie potrzebowała prosta, działająca aplikacja.

 Kreator wygenerował także prostą aktywność i układ, zawierające kody


z wybranego szablonu.
Kod wchodzący w skład szablonu obejmuje kod XML układu i kod aktywności napisany
w Javie. Ich działanie ogranicza się do wyświetlenia na ekranie prostego tekstu „Hello
world!”. Oczywiście ten kod możesz teraz zmienić.

Po zakończeniu tworzenia projektu — podaniu wymaganych informacji we wszystkich oknach


dialogowych kreatora — Android Studio automatycznie wyświetli projekt.
To jest projekt
wyświetlony
Oto, jak będzie wyglądał nasz nowo utworzony projekt (nie przejmuj się, jeśli na razie wygląda na w Android Studio.
bardzo skomplikowany — jego poszczególne elementy wyjaśnimy na kilku kolejnych stronach):

jesteś tutaj  15
Struktura katalogów

Android Studio utworzy pełną strukturę ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
katalogów aplikacji ¨
¨
Uruchomienie aplikacji
Modyfikacja aplikacji

Aplikacja na Androida jest w rzeczywistości grupą prawidłowych plików rozmieszczonych


w określonej strukturze katalogów. Android Studio przygotowuje dla nas te wszystkie
pliki i katalogi podczas tworzenia nowej aplikacji. Najprostszym sposobem przejrzenia
tej struktury katalogów jest skorzystanie z eksploratora plików umieszczonego w lewej
kolumnie Android Studio.

Eksplorator plików prezentuje wszystkie aktualnie otwarte projekty. Aby rozwinąć lub
zwinąć katalog, wystarczy kliknąć strzałkę wyświetloną z lewej strony ikony katalogu.

Tutaj wybierz
Struktura katalogów projektu
opcję Project,
aby wyświetlić zawiera pliki różnych typów
pliki i katalogi
wchodzące Jeśli przejrzysz zawartość projektu, to zauważysz,
w skład projektu. że kreator utworzył wiele katalogów, a w nich pliki
wielu różnych typów:
To jest
nazwa  Pliki źródłowe Java i XML
projektu. To wygenerowane przez kreator pliki aktywności
i układów.

 Pliki Javy wygenerowane przez Androida


To dodatkowe pliki źródłowe Javy wygenerowane
Klikając
te strzałki, automatycznie przez Android Studio; nie musisz
możesz się przejmować ich zawartością ani zmieniać jej
rozwijać ręcznie.
i zwijać
katalogi.
 Pliki zasobów
Zaliczają się do nich domyślne pliki graficzne ikon,
Wszystkie
te pliki pliki stylów oraz pliki z wartościami łańcuchowymi
i katalogi używanymi przez aplikację.
wchodzą
w skład
projektu.  Biblioteki Androida
W kreatorze określana jest minimalna wersja
SDK, z którą będzie zgodna aplikacja. Android
Studio zadba, by w projekcie znalazły się biblioteki
odpowiednie dla wybranej wersji SDK.

 Pliki konfiguracyjne
Pliki konfiguracyjne informują Androida
o zawartości aplikacji oraz o to tym, jak powinna
ona działać.

Przyjrzymy się teraz nieco dokładniej wybranym,


16 Rozdział 1. kluczowym plikom i katalogom Androidowa.
Zaczynamy

Przydatne pliki projektu ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Projekty Android Studio używają narzędzia do budowy projektów gradle do kompilacji
¨ Modyfikacja aplikacji
i wdrażania aplikacji. Projekty gradle mają określoną, standardową strukturę. Poniżej
przedstawiliśmy kilka kluczowych plików i katalogów, których będziemy używać:

Katalog build/ zawiera pliki


Mojapierwszaapka utworzone dla nas przez Android
Studio. Zazwyczaj nie musimy
Nazwa katalogu samodzielnie modyfikować
głównego odpowiada app
zawartości tych plików i katalogów.
nazwie projektu.
build
Każdy projekt aplikacji na
Androida musi zawierać plik
generated
o nazwie R.java, który jest
tworzony automatycznie
source i umieszczany w automatycznie
wygenerowanych katalogach.
Android używa go do
r
Katalog src/ zawiera kody zarządzania zasobami aplikacji.
src
źródłowe, które tworzymy
i edytujemy. debug
main
com.hfad.mojapierwszaapka
Katalog java/ zawiera tworzone
przez nas kody źródłowe aplikacji. java
To właśnie tu są umieszczone R.java
wszystkie aktywności aplikacji. com.hfad.mojapierwszaapka
Plik MainActivity.java definiuje
Zasoby aplikacji można znaleźć aktywność. Aktywność informuje
w katalogu res/. Katalog MainActivity.java system, w jaki sposób aplikacja
layout/ zawiera pliki układów, ma prowadzić interakcję
a katalog values/ pliki zasobów res z użytkownikiem.
z wartościami, takimi jak łańcuchy
znaków. Istnieją także inne typy Plik activity_main.xml definiuje
layout układ. Układ informuje system,
zasobów. <xml>
</xml> jak ma wyglądać aplikacja.
activity_main.xml
Każda aplikacja na Androida w swoim
katalogu głównym musi zawierać plik Plik strings.xml zawiera pary
o nazwie AndroidManifest.xml. To tak values identyfikator – łańcuch. W tym pliku
<xml>
zwany plik manifestu, zawierający </xml>
przechowywane są takie łańcuchy
kluczowe informacje dotyczące znaków jak nazwa aplikacji i wszelkie
strings.xml inne domyślne wartości tekstowe.
aplikacji, takie jak komponenty,
z których się ona składa, wymagane <xml> Inne pliki, takie jak pliki aktywności
biblioteki i inne deklaracje.
</xml>
lub układów, mogą odczytywać
AndroidManifest.xml zapisane w nim łańcuchy znaków.

jesteś tutaj  17
Edytory

Edycja kodu z użyciem edytorów Android Studio ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Do przeglądania i edytowania plików w Android Studio służą edytory. Wystarczy
¨ Modyfikacja aplikacji
dwukrotnie kliknąć plik, z którym chcemy pracować, a Android Studio natychmiast
wyświetli jego zawartość w środkowej, największej części okna.

Edytor kodu
Większość plików jest
wyświetlana w edytorze kodu.
Edytor kodu przypomina
zwyczajny edytor tekstów,
lecz dysponuje kilkoma
dodatkowymi możliwościami,
takimi jak kolorowanie
kodu i sprawdzanie jego
poprawności syntaktycznej.

Dwukrotnie kliknij plik, …a jego zawartość


który Cię interesuje… zostanie wyświetlona
w panelu edytora.

Edytor projektu
W przypadku edycji
układu mamy do
dyspozycji dodatkową
opcję. Zamiast
edytować bezpośrednio
kod XML możemy
skorzystać z edytora
projektu. Pozwala on To różne sposoby
przeciągać do układu prezentacji
zawartości tego
graficzne komponenty samego pliku:
interfejsu użytkownika pierwszy widok
Te przyciski przedstawia jego
i rozmieszczać je pozwalają kod, a drugi
w wybrany sposób. Edytor wybrać edytor, pokazuje wygląd
którego chcemy układu.
kodu i edytor projektu używać.
zapewniają dwa różne
sposoby prezentacji tego
samego pliku, zatem
w każdej chwili można się
pomiędzy nimi przełączać.

18 Rozdział 1.
?
  Zaczynamy

DO CZEGO SŁUŻĘ?

Poniżej przedstawiamy kod źródłowy pliku układu wygenerowanego przez


Android Studio. Wiemy, że jeszcze nigdy nie widziałeś takiego pliku na oczy,
ale przekonajmy się, czy potrafisz dopasować opisy umieszczone u dołu strony
z odpowiednimi wierszami kodu. Aby Ci ułatwić, podaliśmy jedną odpowiedź.

activity_main.xml

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:paddingBottom=”16dp”
tools:context=”.MainActivity”>

<TextView
android:text=”@string/hello_world”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

</RelativeLayout>

Dodaje wypełnienie do Wyświetla zawartość zasobu


marginesów ekranu. łańcuchowego o nazwie
hello_world.

Dodaje do układu komponent


GUI o nazwie TextView, który
służy do wyświetlania tekstów. Sprawia, że szerokość i wysokość
układu będą odpowiadały
szerokości i wysokości ekranu
Sprawia, że tekst będzie urządzenia.
dostosowywany do wielkości
komponentu zarówno w poziomie,
jak i w pionie.

jesteś tutaj  19
?
Rozwiązanie  


DO CZEGO SŁUŻĘ?
ROZWIĄZANIE
Poniżej przedstawiamy kod źródłowy pliku układu wygenerowanego przez
Android Studio. Wiemy, że jeszcze nigdy nie widziałeś takiego pliku na oczy,
ale przekonajmy się, czy potrafisz dopasować opisy umieszczone u dołu strony
z odpowiednimi wierszami kodu. Aby Ci ułatwić, podaliśmy jedną odpowiedź.

activity_main.xml

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:paddingBottom=”16dp”
tools:context=”.MainActivity”>

<TextView
android:text=”@string/hello_world”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

</RelativeLayout>

Dodaje wypełnienie Wyświetla zawartość zasobu


do marginesów ekranu. łańcuchowego o nazwie
hello_world.

Dodaje do układu komponent


GUI o nazwie TextView, który
służy do wyświetlania tekstów. Sprawia, że szerokość i wysokość
układu będą odpowiadały
szerokości i wysokości ekranu
Sprawia, że tekst będzie urządzenia.
dostosowywany do wielkości
komponentu zarówno w poziomie,
jak i w pionie.

20 Rozdział 1.
?
  Zaczynamy

DO CZEGO SŁUŻĘ?

A teraz przekonajmy się, czy będziesz w stanie podobnie dopasować opisy do


kodu aktywności. To jest kod przykładowy, a nie kod wygenerowany przez
Android Studio podczas tworzenia projektu. A zatem dopasuj opisy do
odpowiednich wierszy kodu.

MainActivity.java

package com.hfad.mojapierwszaapka;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

To jest nazwa pakietu. Implementacja metody onCreate()


zdefiniowanej w klasie Activity.
Ta metoda jest wywoływana
w momencie pierwszego tworzenia
To są klasy Androida aktywności.
używane przez naszą klasę
MainActivity.

MainActivity rozszerza klasę


android.app.Activity.
Określa, którego układu
należy użyć.

jesteś tutaj  21
?
Kolejne rozwiązanie  


DO CZEGO SŁUŻĘ?
ROZWIĄZANIE
A teraz przekonajmy się, czy będziesz w stanie podobnie dopasować opisy do
kodu aktywności. To jest kod przykładowy, a nie kod wygenerowany przez
Android Studio podczas tworzenia projektu. A zatem dopasuj opisy do
odpowiednich wierszy kodu.

MainActivity.java

package com.hfad.mojapierwszaapka;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

To jest nazwa pakietu. Implementacja metody onCreate()


zdefiniowanej w klasie Activity.
Ta metoda jest wywoływana
w momencie pierwszego tworzenia
To są klasy Androida aktywności.
używane przez naszą klasę
MainActivity.

MainActivity rozszerza klasę


android.app.Activity.
Określa, którego układu
należy użyć.

22 Rozdział 1.
Zaczynamy

Uruchamianie aplikacji w emulatorze Androida ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Dotychczas przekonałeś się jedynie, jak wygenerowana aplikacja będzie ¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
wyglądała w Android Studio, i dowiedziałeś nieco, z czego się ona składa.
Ale zapewne tym, co naprawdę chciałbyś zobaczyć, jest ta aplikacja
w działaniu, prawda?

Jeśli chodzi o uruchamianie aplikacji, to mamy do wyboru kilka opcji.


Pierwszą z nich jest uruchomienie aplikacji na fizycznym urządzeniu.
Ale co zrobić, jeśli nie będziemy mieli takiego pod ręką albo jeśli będziemy
Emulator Androida pozwala
chcieli sprawdzić działanie aplikacji na urządzeniu konkretnego typu, wykonywać aplikacje na
którego nie posiadamy? wirtualnych urządzeniach
W takich przypadkach można skorzystać z alternatywnego rozwiązania, z Androidem (AVD). Takie
jakim jest uruchomienie aplikacji na emulatorze Androida wchodzącym
w skład Android SDK. Emulator umożliwia przygotowanie jednego lub kilku
wirtualne urządzenie działa
wirtualnych urządzeń z Androidem (określanych skrótowo jako AVD), tak samo jak urządzenie
a następnie uruchamianie aplikacji w emulatorze w taki sposób, jak gdyby fizyczne. Można utworzyć
działały na fizycznym urządzeniu.
wiele urządzeń wirtualnych,
Jak zatem wygląda emulator? emulujących różne typy
Rysunek zamieszczony obok przedstawia AVD uruchomione w emulatorze urządzeń.
Androida. Jak widać, emulator wygląda jak telefon działający na komputerze.

Emulator jest aplikacją, która odtwarza Kiedy już skonfigurujesz


AVD, możesz uruchomić
konkretne środowisko sprzętowe urządzenia na nim aplikację
z Androidem: zaczynając od jego procesora i zobaczyć, jak działa.
i pamięci, a kończąc na układzie dźwiękowym Android Studio samo
uruchamia emulator.
i ekranie. Emulator Androida bazuje
na istniejącym już emulatorze o nazwie
QEMU, podobnym do wielu innych aplikacji
obsługujących maszyny wirtualne, z którymi
być może już się zetknąłeś, takimi jak
VirtualBox lub VMWare.

Dokładny wygląd i zachowanie urządzenia


wirtualnego zależy od jego konfiguracji.
To przedstawione obok ma symulować
telefon Nexus 4, dlatego będzie wyglądało
i działało tak jak Nexus 4 uruchomiony Podobnie jak
normalny telefon,
na komputerze. także wirtualne
urządzenie trzeba
A zatem przygotuj teraz własne urządzenie odblokować, zanim
wirtualne, żeby zobaczyć swoją aplikację zaczniemy go
używać — wystarczy
działającą w emulatorze. kliknąć ikonę kłódki
i przeciągnąć ją
w górę.

jesteś tutaj  23
Tworzenie AVD

Tworzenie wirtualnego urządzenia z Androidem ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Utworzenie wirtualnego urządzenia z Androidem w Android Studio wymaga


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
wykonania kilku czynności. Spróbujemy teraz utworzyć AVD symulujące
telefon Nexus 4 obsługujący API poziomu 21. Dzięki temu przekonasz się, jak
Twoja nowa aplikacja wygląda i działa na urządzeniach tego typu. Czynności
wykonywane w kreatorze są niemal takie same, niezależnie od typu tworzonego
urządzenia.

Otwórz Android Virtual Device Manager


Program AVD Manager umożliwia tworzenie nowych
urządzeń wirtualnych oraz przeglądanie i edytowanie już
istniejących. Aby go otworzyć, należy wybrać z menu Android
Studio opcję Tools/AVD Manager.

Jeśli wcześniej nie utworzyłeś żadnego AVD, to na ekranie


zostanie wyświetlone okno z sugestią, abyś to zrobił.

Kliknij przycisk Create a virtual


device, aby utworzyć nowe AVD.

Wybierz komponenty sprzętowe


W następnym oknie dialogowym kreatora znajdzie się prośba
o wybranie definicji urządzenia. Ta definicja określa typ
urządzenia, które
tworzone AVD będzie
emulować. Możesz
wybierać spośród wielu
rodzajów telefonów,
tabletów, urządzeń
ubieralnych oraz
przystawek telewizyjnych.

My chcielibyśmy się
przekonać, jak nasza
nowa aplikacja wygląda
na telefonie Nexus 4.
A zatem najpierw na
liście Category zaznacz Kiedy wybierzesz urządzenie,
tutaj zostaną wyświetlone
opcję Phone, a potem szczegółowe informacje o nim.
z listy obok wybierz
opcję Nexus 4. Następnie
kliknij przycisk Next.

24 Rozdział 1.
Zaczynamy

Tworzenie AVD (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Wybierz obraz systemu ¨ Modyfikacja aplikacji

Kolejną czynnością, jaką musisz wykonać, jest wybór obrazu systemu. Obraz systemu
udostępnia zainstalowaną wersję systemu operacyjnego Android. W ten sposób możesz
określić, która wersja systemu ma działać na tworzonym urządzeniu wirtualnym i jaki
ma być typ jego procesora (ARM lub x86).

Musisz wybrać obraz systemu odpowiadający poziomowi API, który będzie zgodny
z tworzoną aplikacją. Na przykład jeśli tworząc aplikację, określiłeś, że minimalnym
poziomem API jest poziom 15, to tworząc AVD, wybierz obraz systemu obsługujący
co najmniej API poziomu 15. W tym przykładzie wybierzemy obraz systemu dla API
poziomu 21. A zatem wybierz opcję Lollipop 21 armeabi-v7a z wartością Android 5.0.1
lub Default w kolumnie Target. Kiedy to zrobisz, kliknij przycisk Next.

Jeśli te obrazy
systemów
nie będą
zainstalowane,
to AVD Manager
umożliwi ich
pobranie.

Na następnej stronie będziemy kontynuować konfigurowanie AVD.

jesteś tutaj  25
Sprawdzenie konfiguracji

Tworzenie AVD (ciąg dalszy) ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji
¨ Uruchomienie aplikacji
Sprawdź konfigurację AVD ¨ Modyfikacja aplikacji

W następnym oknie dialogowym kreatora zostaniesz poproszony o zweryfikowanie


konfiguracji AVD. To okno zawiera podsumowanie wszystkich wybranych wcześniej
opcji oraz daje możliwość ich zmiany. Zaakceptuj opcje i kliknij przycisk Finish.

To są wszystkie
opcje, które wybrałeś
na kilku poprzednich
stronach.

Teraz AVD Manager utworzy urządzenie wirtualne, a kiedy skończy, wyświetli je na liście.
Możesz już zamknąć okno AVD Managera.

Twój wirtualny Nexus 4 został utworzony.

26 Rozdział 1.
Zaczynamy

Uruchomienie aplikacji w emulatorze ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Skoro już przygotowałeś swoje urządzenie wirtualne, możesz spróbować


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
uruchomić na nim aplikację. W tym celu wystarczy wybrać opcję Run ‘app’
z menu Run. Kiedy zostaniesz poproszony o wybranie urządzenia, upewnij
się, czy przycisk opcji Launch emulator jest zaznaczony i czy z listy poniżej
został wybrany utworzony wcześniej wirtualny Nexus 4.

A teraz, cierpliwie czekając na uruchomienie urządzenia, przyjrzymy się


nieco dokładniej, co dzieje się po wybraniu opcji Run.

Skompilowanie, spakowanie, wdrożenie i wykonanie


Wybranie opcji Run nie powoduje jedynie uruchomienia aplikacji — oprócz
tego wykonywane są wszystkie inne niezbędne czynności przygotowawcze:

To jest utworzone przed chwilą


urządzenie wirtualne.

Biblioteki Zasoby

Plik APK to pakiet


2
1
aplikacji na Androida.
APK
W zasadzie jest to
Plik Javy Kod bajtowy Plik APK zwyczajny plik JAR
4
3 5 lub ZIP zawierający
Run
aplikację.

Emulator

Emulator

1 Pliki źródłowe Javy zostają skompilowane 4 Po uruchomieniu emulatora i wybranego


do postaci kodów bajtowych. urządzenia wirtualnego zostanie na nie
wgrany plik APK aplikacji, po czym aplikacja
2 Zostaje utworzony pakiet aplikacji, zostanie zainstalowana w systemie.
czyli plik APK.
Plik APK zawiera skompilowane pliki Javy 5 AVD uruchomi główną aktywność aplikacji.
oraz wszystkie biblioteki i zasoby niezbędne W tym momencie aplikacja zostanie wyświetlona
do uruchomienia aplikacji. na ekranie AVD i będzie gotowa do testowania.

3 Jeśli emulator jeszcze nie działa, to zostanie


uruchomiony, a na nim zostanie uruchomione
wybrane urządzenie wirtualne.
jesteś tutaj  27
Prosimy o cierpliwość

Postępy możesz obserwować w konsoli ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Uruchomienie emulatora i urządzenia wirtualnego czasami może zająć całkiem


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
sporo czasu — często jest to nawet kilka minut. Na szczęście okazuje się, że postępy
wykonywanych czynności można obserwować w konsoli Android Studio. Konsola
udostępnia szczegółowy dziennik wszystkich czynności wykonywanych przez narzędzie Sugerujemy, żebyś w trakcie
oczekiwania na uruchomienie emulatora
gradle, a jeśli podczas tych operacji wystąpią jakieś błędy, to zostaną one wyraźnie znalazł sobie jakieś inne zajęcie,
wyróżnione. na przykład możesz szydełkować
lub ugotować sobie szybki obiad.
Panel konsoli jest wyświetlany u dołu okna Android Studio.

Poniżej zamieściliśmy zawartość konsoli wyświetloną podczas wykonanej Android Studio uruchamia emulator,
a w nim AVD symulujące Nexusa 4
przez nas próby wykonania aplikacji: — to, które przed chwilą utworzyłeś.

Waiting for device.


/Applications/adt-bundle-mac/sdk/tools/emulator -avd Nexus_4_API_21 -netspeed full -netdelay none
Device connected: emulator-5554
Device Nexus_4_API_21 [emulator-5554] is online, waiting for processes to start up..
Device is ready: Nexus_4_API_21 [emulator-5554]
Urządzenie wirtualne zostało
Target device: Nexus_4_API_21 [emulator-5554] uruchomione i działa.
Uploading file
local path: /Users/Piotrek/Documents/Helion/ksiazki/Head First Android Development/kody_robocze/MojaPierwszaApka/app/build/
outputs/apk/app-debug.apk
remote path: /data/local/tmp/com.hfad.mojapierwszaapka
Installing com.hfad.mojapierwszaapka
DEVICE SHELL COMMAND: pm install -r ”/data/local/tmp/com.hfad.mojapierwszaapka”
pkg: /data/local/tmp/com.hfad.mojapierwszaapka APK.
Trwa wgrywanie i instalacja pliku
Success
Launching application: com.hfad.myfirstapp/com.hfad.mojapierwszaapka.MainActivity.
DEVICE SHELL COMMAND: am start -n ”com.hfad.mojapierwszaapka /com.hfad.mojapierwszaapka.MainActivity” -a
android.intent.action.MAIN -c android.intent.category.LAUNCHER
I w końcu zostaje uruchomiona
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] aplikacja i wykonana jej aktywność
cmp=com.hfad.mojapierwszaapka/.MainActivity }
główna; ta aktywność została
wygenerowana przez kreator.

28 Rozdział 1.
Zaczynamy

¨ Przygotowanie środowiska
¨ Stworzenie aplikacji
Jazda próbna ¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
Sprawdźmy zatem, co widać na ekranie podczas uruchamiania aplikacji.

W pierwszej kolejności w osobnym oknie jest wyświetlany emulator.


Następnie, przez dłuższą chwilę, emulator wczytuje AVD, a chwilkę
po jego uruchomieniu wyświetlany jest ekran blokady urządzenia. …a to jest zablokowane
urządzenie wirtualne

wygląda ono i działa
tak samo
Najpierw zostaje jak prawdziwy telefon
Nexus 4.
uruchomiony
emulator…

Kiedy odblokujesz ekran AVD, przesuwając


ikonę kłódki ku górze, zostanie na nim
wyświetlona utworzona aplikacja. Jej nazwa
będzie widoczna na pasku w górnej części To jest tytuł
aplikacji.
ekranu, a poniżej, w głównej części ekranu,
będzie widoczny domyślny napis
„Hello world!”. To jest
przykładowy
tekst
zastosowany
przez kreator.

Android Studio, bez zawracania


nam tym głowy, wygenerowało
tekst „Hello world!”.
A to jest
aplikacja
działająca
w AVD.

jesteś tutaj  29
Co się stało?

Ale co się właściwie stało? ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Przyjrzyjmy się dokładnie wszystkiemu,


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
co się dzieje podczas uruchamiania aplikacji:

1 Android Studio uruchamia emulator,


wczytuje AVD i instaluje aplikację.
1
2
2 Po uruchomieniu aplikacji zostaje
utworzony obiekt aktywności, której <Layout>
kod pochodzi z pliku MainActivity.java. 3

3 Aktywność informuje, że używa </Layout>


układu activity_main.xml. <Layout>
Aktywność
Urządzenie
</Layout>
Układ
4
4 Aktywność nakazuje systemowi
wyświetlenie układu na ekranie.
W efekcie zostaje wyświetlony tekst W naszym przypadku
jest to urządzenie
„Hello world!”. wirtualne.

Nie istnieją
głupie pytania

P: Wspominaliście, że podczas tworzenia pliku APK kody P: To wszystko wygląda na bardzo skomplikowane. Nie
źródłowe Javy zostają skompilowane do postaci kodów można po prostu użyć zwyczajnej wirtualnej maszyny Javy?
bajtowych i dodane do pliku APK. Przypuszczam,
że chodziło Wam o to, że zostają one skompilowane O: Środowisko ART jest w stanie konwertować kody DEX na kod
do kodów bajtowych Javy, prawda? maszynowy, wykonywany bezpośrednio przez procesor urządzenia
z Androidem. To sprawia, że aplikacja działa znacznie szybciej
O: Owszem, zostają, ale to nie wszystko. Na Androidzie sytuacja i zużywa znacznie mniej baterii.
wygląda nieco inaczej.
Kluczowa różnica polega na tym, że na Androidzie kod nie jest P : Czy wirtualna maszyna Javy naprawdę powoduje
wykonywany w standardowej wirtualnej maszynie Javy Java aż tak duże narzuty?
VM. Zamiast tego działa on w środowisku uruchomieniowym
Androida (ART), a na starszych urządzeniach w poprzedniku
O: Tak. Ponieważ na Androidzie każda aplikacja działa
w odrębnym procesie. Oznacza to, że gdyby była używana
ART, środowisku uruchomieniowym noszącym nazwę Dalvik.
standardowa wirtualna maszyna Javy, to urządzenia z Androidem
Oznacza to, że piszemy kod w Javie, kompilujemy go do postaci
musiałyby mieć znacznie więcej pamięci.
plików .class, używając kompilatora Javy, a następnie pliki klasowe
zostają zapisane w pliku w formacie DEX, który zajmuje mniej
miejsca i jest bardziej wydajny od zwyczajnych kodów bajtowych.
P: Czy za każdym razem, gdy tworzę nową aplikację,
muszę także tworzyć nowe AVD?
Środowisko uruchomieniowe wykonuje te kody DEX. Więcej
informacji na ten temat można znaleźć w Dodatku A. O: Nie. Po utworzeniu AVD możesz go używać do uruchamiania
wszystkich tworzonych aplikacji. Czasami można jednak utworzyć
wiele urządzeń wirtualnych, aby testować aplikacje w różnych
środowiskach. Na przykład można utworzyć wirtualny tablet, aby
przekonać się, jak aplikacja będzie wyglądać i działać na większych
urządzeniach.

30 Rozdział 1.
Zaczynamy

Usprawnianie aplikacji ¨
¨
Przygotowanie środowiska
Stworzenie aplikacji

Podczas lektury kilku ostatnich stron stworzyłeś prostą aplikację na


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
Androida i przekonałeś się, jak ona wygląda i działa w emulatorze.
Teraz zajmiemy się nieznacznym usprawnieniem tej aplikacji.

Obecnie aplikacja wyświetla tekst „Hello world!” umieszczony


w niej przez kreator. Mamy zamiar zmienić go na coś nieco bardziej
interesującego. A co należy w tym celu zrobić? Aby odpowiedzieć
na to pytanie, musimy się trochę cofnąć i przyjrzeć się konstrukcji
aplikacji.
Chcesz zmienić tekst
wyświetlany przez aplikację
na coś innego niż „Hello world!”.
Aplikacja składa się z jednej aktywności
i jednego układu
Podczas tworzenia aplikacji określiliśmy, w jaki sposób ma być
skonfigurowana, a kreator Android Studio zrobił całą resztę
— przede wszystkim utworzył prostą aktywność i domyślny układ.

Nasza aktywność
określa, co
aplikacja robi
Aktywność kontroluje działanie aplikacji i w jaki sposób
prowadzi interakcję
Android Studio utworzyło aktywność o nazwie z użytkownikiem.
MainActivity.java. Ta aktywność określa, co aplikacja
robi i jak ma reagować na poczynania użytkownika. MainActivity.java

Układ określa wygląd aplikacji


Aktywność MainActivity.java określa, że używa układu <Layout> Nasz układ
activity_main.xml, który także został wygenerowany przez określa, jak
aplikacja
Android Studio. Ten układ określa, jak wygląda aplikacja. wygląda.
</Layout>

activity_main.xml
Teraz chcielibyśmy zmienić wygląd aplikacji, a konkretnie — treść
tekstu wyświetlanego na ekranie. Oznacza to, że będziemy musieli
zmodyfikować komponent odpowiadający za wygląd aplikacji.
Musimy zatem nieco dokładniej przyjrzeć się układowi.

jesteś tutaj  31
Układ

Czym jest układ? ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Naszym celem jest zmiana przykładowego tekstu „Hello world!”


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
zastosowanego przez Android Studio. Zacznijmy zatem od pliku
activity_main.xml. Jeśli ten plik jeszcze nie jest otwarty w edytorze,
to otwórz go teraz — odszukaj plik w katalogu app/src/main/res/
Edytor projektu.
layout i dwukrotnie go kliknij.

Edytor projektu
W Android Studio pliki układów można
przeglądać i edytować na dwa sposoby:
używając edytora kodu lub edytora projektu.

Jeśli skorzystamy z tej drugiej opcji,


przekonamy się, że zgodnie z oczekiwaniami
A to jest
w układzie zostanie wyświetlony tekst „Hello przykładowy
world!”. A jak wygląda źródłowy kod XML tekst.
tego pliku układu?

Aby się przekonać, wystarczy przełączyć się


do edytora kodu.

Aby przełączyć się do


edytora projektu, wystarczy
kliknąć kartę „Design”.

Edytor kodu
W przypadku wybrania edytora kodu
Edytor kodu
jest w nim wyświetlana zawartość pliku
activity_main.xml. Przyjrzymy się jej
teraz nieco dokładniej.

Aby wyświetlić edytor kodu,


wystarczy kliknąć kartę „Text”
wyświetloną u dołu okna IDE.
32 Rozdział 1.
Zaczynamy

Plik activity_main.xml zawiera dwa elementy ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Poniżej przedstawiamy kod umieszczony w pliku activity_main.xml


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
wygenerowanym dla nas przez Android Studio.

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools” To jest pełna ścieżka
android:layout_width=”match_parent” dostępu do pliku
activity_main.xml.
android:layout_height=”match_parent” Mojapierwszaapka
... Tutaj Android Studio
ay nt
t>.

umieściło nieco więcej


eL me
ou

tools:context=”.MainActivity” > kodu XML, ale na razie app/src/main


tiv ele

nie musisz zaprzątać


ela t

sobie nim głowy.


<R jes

res
To

<TextView
android:text=”@string/hello_world” To jest element <TextView>
zagnieżdżony wewnątrz layout
android:layout_width=”wrap_content” elementu <RelativeLayout>. <xml>
</xml>
android:layout_height=”wrap_content” /> activity_main.xml
</RelativeLayout>

Powyższy kod składa się z dwóch elementów. Android Studio czasami wyświetla
Pierwszy to <RelativeLayout>. Informuje on system, wartość odwołania zamiast
że położenie elementów układu ma być określane w sposób Obejrzyj to! rzeczywistego kodu.
względny. Elementu <RelativeLayout> można użyć
na przykład do wyśrodkowania elementów w układzie, Na przykład zamiast faktycznego kodu ”@string/
wyświetlenia ich przy dolnej krawędzi ekranu bądź określenia hello_world” Adroid Studio może wyświetlić
położenia jednego elementu względem innego. tekst ”Hello world!”. Każde takie podstawienie
powinno zostać wyróżnione w edytorze kodu
Drugim elementem naszego układu jest <TextView>. podświetleniem, a jego kliknięcie lub umieszczenie
Służy on do wyświetlania tekstu na ekranie. Jak widać, na nim wskaźnika myszy powinno spowodować
element <TextView> został umieszczony wewnątrz elementu pokazanie faktycznego kodu.
<RelativeLayout>, a w naszym przypadku służy on do
wyświetlania przykładowego tekstu „Hello world!”.

Kluczowym elementem kodu w elemencie <TextView> jest


jego pierwszy wiersz. Co można o nim powiedzieć?

<TextView
Element android:text=”@string/hello_world” Co możesz
TextView powiedzieć o tym
opisuje tekst
android:layout_width=”wrap_content” wierszu kodu?
wyświetlany android:layout_height=”wrap_content” />
w układzie.

jesteś tutaj  33
Łańcuchy znaków

Plik układu zawiera odwołanie do łańcucha,


a nie sam łańcuch znaków Wartości łańcuchowe
należy umieszczać
Kluczowym fragmentem elementu <TextView> jest jego pierwszy wiersz:
w pliku strings.xml, a nie
android:text=”@string/hello_world”
podawać na stałe w kodzie
Fragment android:text oznacza, że jest to właściwość text elementu aplikacji. strings.xml to
<TextView>; innymi słowy określa on tekst, który powinien zostać wyświetlony
w układzie. Ale dlaczego ma on postać ”@string/hello_world”, a nie ”Hello plik zasobów służący do
world!”? Jakie jest faktyczne znaczenie atrybutu android:text i jego wartości przechowywania par nazwa
zastosowanej w powyższym przykładzie?
– wartość łańcuchowa.
Zacznijmy od jego początku: @string. To sposób pozwalający poinformować
system, że ma odszukać tekst w pliku zasobów wartości łańcuchowych. W naszym
Układy i aktywności mogą
przypadku Android Studio automatycznie utworzyło taki plik zasobów, a nosi on pobierać i używać tych
nazwę strings.xml i jest umieszczony w katalogu app/src/main/res/values.
wartości łańcuchowych,
Druga część, hello_world, informuje, że należy odszukać wartość zasobu posługując się ich nazwami.
o nazwie hello_world. A zatem cały fragment @string/hello_world oznacza:
„odszukaj zasób łańcuchowy o nazwie hello_world i użyj skojarzonej z nim
wartości”.
…z zasobu łańcuchowego
o nazwie hello_world.
Wyświetl tekst…
android:text="@string/hello_world" />

To trochę skomplikowane.
Dlaczego w pliku activity_main.xml nie
można po prostu podać wyświetlanego
tekstu? Czy tak nie byłoby prościej?

Jest jeden kluczowy powód takiego rozwiązania: lokalizacja.

Załóżmy, że napisałeś aplikację i stała się ona wielkim hitem w lokalnym, krajowym sklepie
Google Play. Ale przecież nie chcesz się ograniczać tylko do jednego kraju lub języka
— chcesz zapewne, by Twoja aplikacja była dostępna globalnie, w wielu językach.

Umieszczenie używanych łańcuchów znaków w pliku strings.xml sprawia, że radzenie sobie z takimi
zagadnieniami staje się znacznie prostsze. Zamiast zmieniać podane na stałe wartości w wielu
miejscach kodu i w plikach układu wystarczy zastąpić plik strings.xml jego wielojęzyczną wersją.

Oprócz tego stosowanie pliku strings.xml jako centralnego źródła wartości tekstowych znacząco
ułatwia wprowadzanie globalnych zmian w tekstach używanych w aplikacji. Jeśli szef zażąda
skorygowania tekstów wyświetlanych w aplikacji ze względu na zmianę nazwy firmy, wystarczy
wprowadzić poprawki w pliku strings.xml.

34 Rozdział 1.
Zaczynamy

Zajrzyjmy do pliku strings.xml ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Android Studio automatycznie utworzyło dla nas plik z zasobami łańcuchowymi,


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
o nazwie strings.xml, sprawdźmy zatem, czy faktycznie zawiera on zasób o nazwie
hello_world. Skorzystaj z eksploratora plików, by odnaleźć katalog app/src/main/
res/values, i kliknij go dwukrotnie, aby go otworzyć.
To jest pełna ścieżka
dostępu do pliku
Oto, jak wygląda zawartość pliku strings.xml: Mojapierwszaapka strings.xml.

<?xml version=”1.0” encoding=”utf-8”?> app/src/main


<resources>
res
<string name=”app_name”>Moja pierwsza apka</string>
<string name=”hello_world”>Hello world!</string> values
Plik strings.xml zawiera
<xml>
<string name=”action_settings”>Settings</string>
łańcuch znaków o nazwie
</xml>

</resources> hello_world i wartości strings.xml


„Hello world!”.

Jak widać, w pliku znajduje się wiersz kodu wyglądający dokładnie jak to, czego szukamy.
Opisuje on zasób łańcuchowy o nazwie hello_world i wartości ”Hello world!”:

<string name=”hello_world”>Hello world!</string>

Zmodyfikuj plik strings.xml, aby zmienić tekst


Zmieńmy zatem przykładowy tekst wyświetlany w aplikacji. Jeśli jeszcze tego
nie zrobiłeś, to odszukaj teraz w Android Studio plik strings.xml i go otwórz.

Poniżej przedstawiliśmy kod stanowiący zawartość tego pliku. Musisz odszukać


w nim łańcuch o nazwie ”hello_world” i zmienić skojarzoną z nim wartość
z ”Hello world!” na ”Siemka stary!”:

<?xml version=”1.0” encoding=”utf-8”?>


<resources> Zmień podaną tu wartość
y!”.
z „Hello world!” na „Siemka star
<string name=”app_name”>Moja pierwsza apka</string>
<string name=”hello_world”>Hello world!Siemka stary!</string>
<string name=”action_settings”>Settings</string>
</resources>

Po wprowadzeniu zmian w pliku zapisz je, wybierając z menu głównego


opcję File/Save All.

jesteś tutaj  35
Pod lupą

Pliki zasobów łańcuchowych pod lupą


Plik strings.xml jest domyślnym plikiem zasobów używanym do przechowywania
par nazwa – wartość, do których następnie można się odwoływać w innych
miejscach aplikacji. Oto jego format:
Element <string> określa, że
para nazwa wartość jest
Element <?xml version=”1.0” encoding=”utf-8”?> łańcuchem znaków.
<resources> <resources>
informuje, że
zawartością <string name=”app_name”>Moja pierwsza apka</string>
pliku są zasoby. <string name=”hello_world”>Hello world!</string>
<string name=”action_settings”>Settings</string>
</resources>

Istnieją dwa czynniki, dzięki którym Android jest w stanie rozpoznać,


że plik strings.xml jest plikiem zasobów łańcuchowych:

 Został on zapisany w katalogu app/src/main/res/values.


Pliki XML umieszczone w tym katalogu zawierają proste wartości,
takie jak łańcuchy znaków lub kolory.

 Plik zawiera element <resources>, który z kolei zawiera jeden


lub więcej elementów <string>.
Sam format, w jakim została zapisana zawartość pliku, wskazuje, że zostały
w nim podane łańcuchy znaków. Element <resources> informuje system, że plik
zawiera zasoby, a element <string> — że tymi zasobami są łańcuchy znaków.

To wszystko oznacza, że zasobów łańcuchowych wcale nie musimy przechowywać w pliku


o nazwie strings.xml — ten plik może nosić dowolną inną nazwę, można także używane
łańcuchy znaków zapisać w kilku różnych plikach.

Każda para nazwa – wartość ma następującą postać:

<string name=”nazwa_łańcucha”>wartość_łańcucha</string>

gdzie nazwa_łańcucha jest identyfikatorem danego łańcucha znaków, a wartość_łańcucha


określa sam łańcuch.

W pliku układu można odwołać się do konkretnego łańcucha znaków, używając zapisu
o postaci:

”@string/nazwa_łańcucha” To jest nazwa łańcucha, którego


wartość chcemy zwrócić.
Fragment „string” informuje system,
że ma odszukać zasób łańcuchowy
o podanej nazwie.

36 Rozdział 1.
Zaczynamy

Weź swoją aplikację na jazdę próbną ¨


¨
Przygotowanie środowiska
Stworzenie aplikacji

Po wprowadzeniu zmian opisanych na poprzednich stronach spróbuj


¨ Uruchomienie aplikacji
¨ Modyfikacja aplikacji
ponownie uruchomić aplikację w emulatorze, wybierając w tym celu
opcję Run/Run ‘app’ z menu głównego. Powinieneś zauważyć, że aplikacja
wyświetla teraz tekst „Siemka stary!”, a nie „Hello world!”.

To jest
zaktualizowana
wersja aplikacji
uruchomiona
w emulatorze.

Przykładowy tekst ma teraz


postać „Siemka stary!”,
a nie „Hello world!”.

Nie istnieją
głupie pytania
P: Czy umieszczanie wartości łańcuchowych w plikach P: A skąd aplikacja będzie wiedziała, którego z nich użyć?
zasobów, takich jak strings.xml, jest absolutnie
konieczne? O: Umieść swój domyślny plik z polskimi zasobami łańcuchowymi
w standardowym miejscu katalogu app/src/main/res/values.
O: Nie ma takiego obowiązku, jednak Android wyświetla A plik z tekstami angielskimi w katalogu app/src/main/res/values-
komunikat ostrzegawczy, jeśli wartości tekstowe zostaną podane en. Jeśli w urządzeniu został wybrany język angielski, to Android
na stałe w kodzie. Początkowo można sądzić, że to sporo zastosuje pliki zasobów z katalogu app/src/main/res/values-en.
zachodu, lecz takie rozwiązanie znacznie ułatwia inne prace, takie Jeśli w urządzeniu został wybrany jakikolwiek inny język, system
jak lokalizowanie aplikacji. Poza tym łatwiej jest już od samego użyje plików zasobów z katalogu app/src/main/res/values.
początku używać zasobów łańcuchowych niż dodawać je później.
P: Kod układu, który wygenerowało moje Android
P: W jaki sposób umieszczenie łańcuchów znaków Studio, wygląda nieco inaczej niż ten przedstawiony
w osobnym pliku może pomóc w lokalizacji aplikacji? w książce. Czy mam się tym martwić?

O: Załóżmy, że chcesz, by Twoja aplikacja domyślnie wyświetlała O: Kod XML generowanego układu może się nieco różnić
tekstu w języku polskim i w języku angielskim, jeśli w telefonie w zależności od używanej wersji Android Studio. Nie musisz się
został wybrany inny język niż polski. W takim przypadku zamiast jednak tym przejmować, gdyż zaczynając od zaraz i tak będziesz
na stałe umieszczać teksty w różnych językach w aplikacji, mógłbyś się uczył tworzyć własne układy, dlatego i tak zastąpisz układ
utworzyć jeden plik z tekstami polskimi i drugi z angielskimi. wygenerowany przez Android Studio.

jesteś tutaj  37
Przybornik

Twój przybornik do Androida


Rozdział 1.

Opanowałeś już rozdział 1. i dodałeś j


do swojego przybornika z narzędziami Pełny kod przykładowe
lika cji pre zen tow ane j
ap
podstawowe pojęcia i informacje mo żes z
w tym rozdziale
związane z tworzeniem aplikacji na Androida. pobrać z ser we ra FT P
wydawnictwa Helion:
ftp://ftp.helion.pl/
przyklady/andrrg.zip

CELNE SPOSTRZEŻENIA
 Kolejne wersje Androida są identyfikowane na  Plik AndroidManifest.xml zawiera informacje
podstawie numeru wersji, poziomu API oraz o samej aplikacji. Jest on przechowywany
nazwy kodowej. w katalogu app/src/main.
 Android Studio jest specjalną wersją InelliJ IDEA,  AVD to skrót od angielskich słów Android
dostosowaną do współpracy z Android Software Virtual Device, oznaczający wirtualne urządzenie
Development Kit (Android SDK) i z systemem z Androidem. Takie wirtualne urządzenia są
budowy gradle. wykonywane w emulatorze Androida i udają
urządzenia fizyczne.
 Typowa aplikacja na Androida składa się
z aktywności, układów oraz plików zasobów.  Plik APK to plik pakietu aplikacji. Przypomina on
plik JAR, przy czym zawiera aplikację na Androida
 Układy opisują, jak aplikacja ma wyglądać. Są one
jej kody bajtowe, biblioteki i zasoby. Instalacja
przechowywane w katalogu app/src/main/res/
aplikacji na urządzeniu z Androidem polega na
layout.
zainstalowaniu pliku APK.
 Aktywności opisują, co aplikacja robi i w jaki  Aplikacje na Androida działają w niezależnych
sposób prowadzi interakcję z użytkownikiem.
procesach wykonywanych przez środowisko
Pisane przez nas aktywności są przechowywane
uruchomieniowe Androida (ART).
w katalogu app/src/main/java.
 RelativeLayout określa rozmieszczenie
 Plik strings.xml zawiera pary nazwa – wartość.
elementów w układzie w sposób względny.
Pozwala on zapewnić separację pomiędzy
wartościami tekstowymi oraz układami  Element TextView służy do wyświetlania
i aktywnościami, jak również ułatwia tekstów.
lokalizowanie aplikacji.

38 Rozdział 1.
2. Tworzenie interaktywnych aplikacji

Aplikacje, które coś robią


Zastanawiam się,
co się stanie, jeśli
nacisnę przycisk
z napisem „katapulta”…

Większość aplikacji musi w jakiś sposób reagować na poczynania użytkowników.


Z tego rozdziału dowiesz się, co zrobić, aby Twoje aplikacje były nieco bardziej interaktywne.
Przekonasz się, jak zmusić aplikację, by coś zrobiła w odpowiedzi na działania użytkownika, oraz
jak sprawić, by aktywności i układy porozumiewały się ze sobą jak starzy kumple. Przy okazji
pokażemy Ci nieco dokładniej, jak naprawdę działa Android poznasz plik R, czyli ukryty
klejnot, który spaja pozostałe elementy aplikacji.

to jest nowy rozdział  39


Doradca piwny

W tym rozdziale napiszemy aplikację


Doradca piwny Wybierz ulubiony
rodzaj piwa, kliknij
przycisk…
Z poprzedniego rozdziału wiesz, jak można utworzyć prostą
aplikację, używając do tego celu kreatora New Project
udostępnianego przez Android Studio, oraz jak zmienić tekst
wyświetlany w układzie. Zazwyczaj jednak, tworząc aplikacje
na Androida, będziemy chcieli, aby coś robiły.
…a aplikacja
W tym rozdziale pokażemy Ci, jak napisać aplikację, z którą wyświetli listę
sugerowanych
użytkownik będzie mógł prowadzić interakcję, a konkretnie rzecz gatunków piw.
biorąc, będzie to aplikacja o nazwie Doradca Piwny. Pozwoli ona
użytkownikowi wybrać ulubiony rodzaj piwa, kliknąć przycisk
i otrzymać listę smacznych piw, które warto spróbować.

Oto struktura aplikacji:

1 Wygląd aplikacji zostanie określony przez układ.


Tak będzie wyglądał
Układ będą tworzyć trzy komponenty graficznego interfejsu układ naszej aplikacji.
użytkownika:
• komponent typu Spinner, czyli rozwijana lista
pozwalająca użytkownikowi wybrać ulubiony rodzaj piwa; <Layout>
• przycisk, którego kliknięcie spowoduje zwrócenie listy 1
2
piw wybranego rodzaju; </Layout> <resources>

• pole tekstowe wyświetlające dobrane gatunki piwa.


Układ </resources>

2 Plik strings.xml zawiera wszystkie zasoby łańcuchowe strings.xml


używane przez układ, na przykład etykietę przycisku
wyświetlanego na ekranie.

3 Aktywność określa, w jaki sposób aplikacja powinna 3


prowadzić interakcję z użytkownikiem.
Aktywność pobierze rodzaj piwa wybrany przez użytkownika Aktywność
i użyje go do wyświetlenia listy piw, którymi użytkownik
mógłby być zainteresowany. Czynności te będą wykonywane
przy użyciu niestandardowej, napisanej przez nas klasy Javy.

4 Napisana przez nas klasa Javy zawiera logikę


działania aplikacji. 4
Ta klasa będzie zawierała metodę pobierającą rodzaj
piwa (przekazywany jako parametr) i zwracającą listę piw
Nasza klasa Javy
tego rodzaju. Aktywność będzie wywoływać tę metodę,
przekazywać do niej wybrany rodzaj piwa i wyświetlać
zwrócone wyniki.

40 Rozdział 2.
Tworzenie interaktywnych aplikacji

A oto, co musisz zrobić


A zatem bierzmy się do pracy i stwórzmy naszego doradcę piwnego.
W tym celu musisz wykonać kilka czynności (w dalszej części rozdziału
będziemy je odznaczać na liście rzeczy to zrobienia):

1 Utworzyć projekt.
Tworzymy zupełnie nową aplikację, musisz więc utworzyć
nowy projekt. Podobnie jak w poprzednim rozdziale, będziesz
potrzebować prostego układu i aktywności.

2 Zaktualizować układ.
Po wygenerowaniu aplikacji musisz zaktualizować jej układ
i dodać do niego wszystkie niezbędne komponenty GUI.

3 Powiązać układ z aktywnością.


Układ określa jedynie wizualne elementy aplikacji. Aby aplikacja <Layout>

była nieco bardziej inteligentna, trzeba powiązać układ z kodem


</Layout>
Javy w aktywności.

4 Napisać logikę działania aplikacji.


Dodasz do aplikacji własną klasę Javy i posłużysz się nią,
by mieć pewność, że będzie wyświetlać odpowiednie gatunki piwa,
dostosowane do opcji wybranej przez użytkownika.

jesteś tutaj  41
Utworzenie projektu

¨  Utworzenie projektu
Utworzenie projektu ¨  Aktualizacja układu
¨  Połączenie aktywności
Zabierzmy się zatem za pisanie nowej aplikacji (czynności, jakie należy w tym celu ¨  Implementacja logiki
wykonać, są bardzo podobne do tych, które wykonaliśmy w poprzednim rozdziale):
 Otwórz Android Studio i na ekranie powitalnym kliknij opcję Start a new Android Studio project.
Spowoduje to uruchomienie kreatora przedstawionego w rozdziale 1.

 Kiedy zostaniesz poproszony o podanie nazwy aplikacji, wpisz Doradca piwny, co automatycznie
spowoduje zmianę nazwy pakietu na com.hfad.doradcapiwny.

 Chcemy, by nasza aplikacja działała na większości telefonów i tabletów, dlatego na liście Minimum
SDK wybierz API poziomu 15 i upewnij się, że będzie zaznaczone pole wyboru Phone and Tablet.
Oznacza to, że każdy telefon lub tablet, na którym będziemy chcieli uruchomić aplikację, będzie
musiał mieć zainstalowane API przynajmniej poziomu 15. Kryterium to spełnia przeważająca
większość aktualnie używanych urządzeń z Androidem.

 Jako domyślną aktywność wybierz szablon Blank Activity (pusta aktywność). Nadaj jej nazwę
FindBeerActivity, a plik układu nazwij activity_find_beer. Zaakceptuj domyślne wartości w polach
Title i Menu Resource Name, gdyż nie będziemy ich na razie potrzebować.

wszystkie te
Kreator przeprowadzi Cię przez im rozdziale.
czynności, podo bnie jak w popr zedn
j nazw ę Dora dca piwny,
Tworzonej aplikacji nada m API
określ, że jej minimalnym poziome y aktywności,
będzie 15, a następnie podaj nazw ity_find_beer.
activ
FindBeerActivity, i pliku układu,

42 Rozdział 2.
Tworzenie interaktywnych aplikacji

Utworzyliśmy domyślną aktywność i układ


DoradcaPiwny
Kiedy klikniesz przycisk Finish, Android Studio utworzy nowy projekt zawierający
aktywność zdefiniowaną w pliku FindBeerActivity.java oraz plik układu o nazwie
app/src/main
activity_find_beer.xml. Zacznijmy od wprowadzenia zmian w pliku układu.
W tym celu wyświetl zawartość katalogu aap/src/main/res/layout i otwórz plik
res
activity_find_beer.xml.

Podobnie jak w poprzednim rozdziale, także teraz kreator utworzył domyślny layout
układ, który zawiera element <TextView> prezentujący tekst „Hello world!”. <xml>
</xml>
Poniżej przedstawiliśmy kod tego układu:
activity_find_beer.xml

Kod XML układu


<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingLeft=”16dp” Te wszystkie elementy
odnoszą się do układu
android:paddingRight=”16dp” jako całości. Określają
jego szerokość i wysokość
android:paddingTop=”16dp” oraz wypełnienia jego
android:paddingBottom=”16dp” marginesów.
tools:context=”.FindBeerActivity”>

<TextView
android:text=”@string/hello_world”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

</RelativeLayout> Element <TextView> widoczny


w kodzie XML został wyświetlony
Edytor projektu w edytorze projektu.

jesteś tutaj  43
Edytor projektu

¨  Utworzenie projektu
Dodawanie komponentów w edytorze projektu ¨  Aktualizacja układu
¨  Połączenie aktywności
Komponenty graficznego interfejsu użytkownika można dodawać do układów na dwa ¨  Implementacja logiki
sposoby: bezpośrednio w kodzie XML bądź w edytorze projektu. Zacznijmy od dodania
przycisku w edytorze projektu.

Z lewej strony edytora projektu jest wyświetlona paleta zawierająca komponenty GUI, które
można przeciągać do układu. Jeśli przyjrzysz się sekcji Widgets, zauważysz w niej na pewno
komponent przycisku — Button. Kliknij go, a następnie przeciągnij do edytora projektu.

To jest paleta
komponentów.

W naszej
wersji aplikacji
A to jest przycisk umieściliśmy
— komponent Button. przycisk
Przeciągnij go do układu. pod tekstem
„Hello world!”.

Zmiany wprowadzane w edytorze projektu są odzwierciedlane w kodzie XML


Przeciąganie komponentów w opisany powyżej sposób jest bardzo wygodną metodą aktualizowania
układów. Jeśli teraz przełączysz się do edytora projektu, to zauważysz, że dodanie przycisku
spowodowało wprowadzenie zmian w kodzie pliku układu: Postać kodu, który edytor projektu
... doda do pliku układu, będzie zależeć
od miejsca, w którym umieścimy
<TextView przycisk. Jest całkiem prawdopodobne,
android:text=”@string/hello_world” że kod XML Twojego układu będzie
wyglądał nieco inaczej niż ten
android:layout_width=”wrap_content” przedstawiony tutaj, ale nie przejmuj
android:layout_height=”wrap_content” się tym, bo niebawem to zmienimy.
android:id=”@+id/textView” />
Teraz do elementu Text
View
W pliku pojawił się <Button został dodany identyfikator.
nowy element Button; android:layout_width=”wrap_content”
opisuje on przycisk, android:layout_height=”wrap_content”
który dodałeś do układu.
Przyjrzymy się mu nieco android:text=”New Button”
dokładniej na kilku android:id=”@+id/button”
następnych stronach
książki. android:layout_below=”@+id/textView”
android:layout_alignLeft=”@+id/textView” />
...
44 Rozdział 2.
Tworzenie interaktywnych aplikacji

Plik activity_find_beer.xml zawiera nowy przycisk


Edytor dodał do pliku activity_find_beer.xml nowy element Button:

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”New Button”
android:id=”@+id/button”
android:layout_below=”@+id/textView”
android:layout_alignLeft=”@+id/textView” />

W Androidowie termin „przycisk” oznacza standardowe przyciski, które użytkownik


może kliknąć, aby wykonać pewną akcję. Każdy przycisk zawiera właściwości służące
do kontrolowania jego położenia, wielkości i wyglądu oraz określa metodę aktywności,
którą należy wykonać. Te wszystkie właściwości nie są unikalne wyłącznie dla przycisków
— dysponują nimi także inne komponenty GUI, choćby takie jak TextView.

Przyciski i widoki tekstowe są klasami pochodnymi klasy View


Istnieje pewien ważny powód, który sprawia, że przyciski i widoki tekstowe,
czyli komponenty TextView, mają wiele wspólnych właściwości — oba te Klasa View zawiera wiele różnych
rodzaje komponentów dziedziczą po tej samej klasie: View. Więcej informacji metod. Przyjrzymy się im dokładniej
w dalszej części książki.
na jej temat znajdziesz w dalszej części książki, a na razie przedstawiliśmy
poniżej kilka jej najczęściej używanych właściwości.
android.view.View
android:id setId(int)
Ta właściwość określa nazwę identyfikującą dany komponent. Właściwość ID ...
umożliwia kontrolę działania komponentu z poziomu kodu, a także określanie
jego położenia w układzie: TextView jest
typu View…
android:id=”@+id/button”

android:text android.widget.TextView
setText(CharSequence,
Ta właściwość określa, jaki tekst powinien zostać wyświetlony w komponencie. TextView.BufferType)
W przypadku komponentu Button będzie to tekst wyświetlany na przycisku:
...
android:text=”New button”

android:layout_width, android:layout_height …a Button jest typu


TextView, co oznacza,
Te właściwości określają podstawową szerokość i wysokość komponentu. że jest także typu View.
Wartość ”wrap_content” oznacza, że wielkość komponentu powinna być
na tyle duża, by można było wyświetlić w nim jego zawartość. android.widget.Button
android:layout_width=”wrap_content” ...
android:layout_height=”wrap_content”
jesteś tutaj  45
Kod układu

¨  Utworzenie projektu
Dokładniejszy przegląd kodu układu ¨  Aktualizacja układu
¨  Połączenie aktywności
Przyjrzyjmy się nieco dokładniej kodowi układu i podzielmy go na poszczególne elementy, ¨  Implementacja logiki
abyś mógł lepiej zrozumieć, jak on działa (nie przejmuj się, jeśli Twój kod wygląda nieco
inaczej, po prostu przeanalizuj wraz z nami kod podany poniżej):

To jest element <RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”


RelativeLayout.
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:paddingBottom=”16dp”
tools:context=”.FindBeerActivity”>

To jest <TextView
widok
tekstowy. android:text=”@string/hello_world” DoradcaPiwny
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” app/src/main
android:id=”@+id/textView” />
res
To jest <Button
przycisk.
android:layout_width=”wrap_content” layout
android:layout_height=”wrap_content” <xml>
</xml>
android:text=”New Button”
activity_find_beer.xml
android:id=”@+id/button”
android:layout_below=”@+id/textView”
android:layout_alignLeft=”@+id/textView” />

</RelativeLayout> Ten znacznik zamyka element Rela


tiveLayout.

Element RelativeLayout
ślania
Pierwszym elementem w kodzie układu jest <RelativeLayout>. Element Istnieją także inne sposoby okre
a kom pone ntów graf iczn ego
położeni
ten informuje system, że położenie poszczególnych komponentów GUI interfejsu użytkownika. Poznasz
je
ma być wyznaczane względem innych komponentów. Można go zatem w dalszej części książki.
użyć na przykład, aby wyświetlić jeden komponent z lewej strony innego
bądź aby wyrównać komponenty w określony sposób.

W tym przykładzie przycisk jest umieszczony bezpośrednio poniżej


widoku tekstowego, a zatem położenie przycisku jest określane względem
tego widoku.

46 Rozdział 2.
Tworzenie interaktywnych aplikacji

Element TextView
Pierwszym elementem umieszczonym wewnątrz <RelativeLayout> Zastosowanie układu
jest <TextView>:
... względnego oznacza,
<TextView że położenie
android:text=”@string/hello_world”
android:layout_width=”wrap_content” komponentów GUI
android:layout_height=”wrap_content”
będzie określane
android:id=”@+id/textView” />
... względem innych
Jak widać, w kodzie tego elementu nie zostały podane żadne właściwości elementów układu.
określające, w którym miejscu układu powinien on zostać wyświetlony, dlatego
domyślnie Android wyświetli go w lewym górnym rogu ekranu. Zwróć też
uwagę, że komponent ten ma identyfikator o wartości textView. Przekonasz się,
dlaczego to takie ważne, gdy przyjrzymy się następnemu elementowi.

Element Button
Ostatnim elementem umieszczonym w układzie <RelativeLayout>
jest przycisk — element <Button>: Widok tekstowy jest domyślnie
wyświetlany w lewym górnym
... rogu ekranu.

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”New Button”
android:id=”@+id/button”
android:layout_below=”@+id/textView”
android:layout_alignLeft=”@+id/textView” />
...

Dodając przycisk do układu, umieściliśmy go w taki sposób, by był widoczny Przycisk ma zostać wyświetlony
poniżej widoku tekstowego,
poniżej tekstu, a jego lewa krawędź znajdowała się bezpośrednio poniżej lewej a ich lewe krawędzie mają być
krawędzi widoku tekstowego. Oznacza to, że położenie przycisku zostało wyrównane.
określone względem położenia widoku tekstowego i ten fakt jest wyraźnie
odzwierciedlony w kodzie XML układu:

android:layout_below=”@+id/textView”
android:layout_alignLeft=”@+id/textView”

Istnieje kilka możliwych sposobów zapisu kodu XML układu, pozwalających


uzyskać ten sam efekt wizualny. Na przykład powyższy kod XML określa, że
przycisk jest umieszczony poniżej widoku tekstowego. Ale identyczny efekt można
uzyskać, stwierdzając, że widok tekstowy ma być umieszczony powyżej przycisku.
jesteś tutaj  47
Aktualizacja układu

DoradcaPiwny
Zmiany w kodzie XML układu…
app/src/main
Przekonałeś się już, że zmiany wprowadzane w widoku projektu są
natychmiast uwzględniane w kodzie XML układu. To samo dotyczy
res
odwrotnej sytuacji: wszelkie zmiany wprowadzane w kodzie XML
układu są pokazywane w widoku projektu.
layout
Sprawdźmy, czy faktycznie tak się dzieje. Zastąp zawartość swojego <xml>
</xml>
pliku activity_find_beer.xml kodem przedstawionym poniżej:
activity_find_beer.xml

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
tools:context=”.FindBeerActivity” >

Spinner to
stosowana <Spinner
w Androidzie android:id=”@+id/color” Ten element wyświetla
nazwa na rozwijaną w układzie komponent
android:layout_width=”wrap_content” Spinner, czyli rozwijaną
listę konkretnych
wartości. Pozwala android:layout_height=”wrap_content” listę wartości.
on wybrać jedną android:layout_alignParentTop=”true”
z dostępnych opcji.
android:layout_centerHorizontal=”true”
android:layout_marginTop=”37dp” />

<Button
android:id=”@+id/find_beer”
Umieść przycisk android:layout_width=”wrap_content”
poniżej rozwijanej android:layout_height=”wrap_content”
listy i wyrównaj go android:layout_alignLeft=”@+id/color”
do lewej krawędzi
listy. android:layout_below=”@+id/color”
android:text=”Button” />

<TextView
android:id=”@+id/brands”
android:layout_width=”wrap_content”
Umieść widok android:layout_height=”wrap_content”
Zrób to sam!
tekstowy poniżej
przycisku android:layout_alignLeft=”@+id/find_beer”
i wyrównaj go android:layout_below=”@+id/find_beer”
do lewej krawędzi
przycisku. android:layout_marginTop=”18dp”
android:text=”TextView” /> Zastąp zawartość swojego 
pliku activity_find_beer.xml 
</RelativeLayout>
przedstawionym tu kodem XML.

48 Rozdział 2.
Tworzenie interaktywnych aplikacji

…są uwzględniane w edytorze projektu ¨  Utworzenie projektu


¨  Aktualizacja układu
¨  Połączenie aktywności
Kiedy już wprowadzisz modyfikacje w kodzie XML układu, przełącz się do
¨  Implementacja logiki
widoku projektu. Zamiast układu zawierającego tekst i przycisk zobaczysz
tekst wyświetlony poniżej przycisku.

Natomiast powyżej przycisku zostanie wyświetlony spinner. Spinner to


stosowana w terminologii Androida nazwa określająca rozwijaną listę
wartości. Dotknięcie tego komponentu powoduje rozwinięcie listy,
tak by można było wybrać jeden z jej elementów. Komponent Spinner
tworzy rozwijaną listę
wartości. Pozwala on
wybrać jedną wartość
To jest rozwijana lista,
z której użytkownik będzie
mógł wybrać ulubiony
ze zbioru określonych
rodzaj piwa.
wartości.
Użytkownik kliknie
przycisk… Komponenty GUI, takie
…a tu zostanie wyświetlona jak przyciski, widoki
lista dobranych dla niego
gatunków piwa. tekstowe oraz listy
rozwijane, mają bardzo
podobne atrybuty,
gdyż wszystkie są typu
View. Wszystkie te
komponenty dziedziczą
bowiem po wspólnej
klasie View.

Pokazaliśmy Ci, jak można dodawać komponenty GUI do układu, i to


zarówno przy użyciu graficznego edytora projektu, jak i bezpośrednio
w kodzie XML. Ogólnie rzecz biorąc, zamierzone efekty łatwiej można
uzyskać, modyfikując kod XML układu niż wybierając opcje w edytorze
projektu. Dzieje się tak dlatego, że wprowadzając zmiany w kodzie XML,
mamy bardziej bezpośrednią kontrolę nad układem — innymi słowy nie
jesteśmy uzależnieni od Android Studio.

jesteś tutaj  49
Dodanie łańcuchów znaków

¨  Utworzenie projektu
Stosuj zasoby łańcuchowe, ¨  Aktualizacja układu
a nie łańcuchy podawane w kodzie ¨  Połączenie aktywności
¨  Implementacja logiki

Jest jeszcze jedna rzecz, którą musimy zmienić przed próbą uruchomienia
tej aplikacji. Aktualnie zarówno tekst umieszczony na przycisku, jak i tekst
prezentowany w widoku tekstowym są podane na stałe, bezpośrednio
we właściwości text. Jak już wspominaliśmy w rozdziale 1., takie teksty
warto zamienić na zasoby łańcuchowe, definiowane w pliku strings.xml.
Choć nie jest to właściwie obowiązkowe, to jednak takie postępowanie jest
dobrym nawykiem, który warto sobie wyrobić. Stosowanie plików zasobów
łańcuchowych do gromadzenia statycznych tekstów ułatwia późniejsze
tworzenie innych wersji językowych aplikacji, a jeśli kiedyś pojawi się
konieczność wprowadzenia zmian w wyświetlanych tekstach, będzie to
można zrobić w jednym miejscu.

Otwórz zatem plik app/src/main/res/values/strings.xml. Po przełączeniu się


do widoku XML jego zawartość powinna wyglądać podobnie do kodu
zamieszczonego poniżej:

<?xml version=”1.0” encoding=”utf-8”?>


To są łańcuchy znaków utworzon
<resources> dla nas przez Android Studio.
e

<string name=”app_name”>Doradca Piwny</string>


<string name=”hello_world”>Hello world!</string>
<string name=”action_settings”>Settings</string>

</resources>
DoradcaPiwny

W pierwszej kolejności usuń zasób ”hello_world”, gdyż nie będzie


app/src/main
nam już potrzebny. Następnie dodaj nowy zasób łańcuchowy o nazwie
”find_beer” i wartości ”Odszukaj piwo!”. Potem dodaj nowy zasób
o nazwie ”brands”, ale na razie nie określaj jeszcze jego wartości. res

Nowy kod pliku powinien wyglądać tak: values


<xml>
... </xml>

<string name=”app_name”>Doradca piwny</string> strings.xml

<string name=”hello_world”>Hello world!</string>


<string name=”action_settings”>Settings</string> Musisz usunąć zasób
<string name=”find_beer”>Odszukaj piwo!</string> hello_world oraz dodać
dwa nowe zasoby:
<string name=”brands”></string> find_beer i brands.
...

50 Rozdział 2.
Tworzenie interaktywnych aplikacji

Zmiana układu i zastosowanie w nim zasobów łańcuchowych


Teraz zajmiemy się zmianą przycisku i widoku tekstowego oraz zastosowaniem
w nich zasobów łańcuchowych utworzonych na poprzedniej stronie.

Otwórz zatem plik activity_find_beer.xml i wprowadź w nim następujące zmiany:

 Zmień wiersz android:text=”Button” na android:text=”@string/find_beer”.

 Zmień wiersz android:text=”TextView” na android:text=”@string/brands”.

...
<Spinner
android:id=”@+id/color” DoradcaPiwny

android:layout_width=”wrap_content”
app/src/main
android:layout_height=”wrap_content”
android:layout_alignParentTop=”true”
res
android:layout_centerHorizontal=”true”
android:layout_marginTop=”37dp” /> layout
<xml>
</xml>

<Button activity_find_beer.xml
android:id=”@+id/find_beer”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/color”
android:layout_below=”@+id/color” Dzięki temu na przycisku
zostanie wyświetlona
android:text=”@string/find_beer” /> zawartość zasobu
łańcuchowego find_beer.

<TextView
android:id=”@+id/brands”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/find_beer”
android:layout_below=”@+id/find_beer”
Dzięki temu w widoku tekstowym zostanie
android:layout_marginTop=”18dp” wyświetlona zawartość zasobu brands.
Choć obecnie jest on pusty, to jednak
android:text=”@string/brands” /> jeśli w przyszłości podamy w nim jakiś inny
łańcuch znaków, zostanie on wyświetlony.
...

jesteś tutaj  51
Jazda próbna

¨  Utworzenie projektu
Weź swoją aplikację na jazdę próbną ¨  Aktualizacja układu
¨  Połączenie aktywności
Wciąż mamy jeszcze sporo do zrobienia w naszej aplikacji, sprawdźmy ¨  Implementacja logiki
jednak, jak ona wygląda w swojej aktualnej postaci. Zapisz wszystkie
wprowadzone zmiany, a następnie wybierz opcję Run ‘app’ z menu Run.
Kiedy zostaniesz o to poproszony, wybierz opcję uruchomienia emulatora.

Poczekaj cierpliwie na uruchomienie aplikacji, w końcu pojawi się


w emulatorze. To jest rozwijana
lista, choć
Spróbuj dotknąć rozwijaną listę. Choć obecnie nie będzie to takie na razie jest
oczywiste, dotknięcie tego komponentu powoduje wyświetlenie jeszcze pusta.
rozwijanej listy wartości — na razie jednak jest ona pusta.
Przycisk znajduje się
Oto, co udało się nam już zrobić poniżej rozwijanej listy
i jest wyrównany do jej
Poniżej zamieściliśmy krótkie podsumowanie wszystkiego, co udało się lewej krawędzi.
nam do tej pory zrobić:

1 Stworzyliśmy układ określający wygląd aplikacji.


Nasz układ zawiera rozwijaną listę, przycisk oraz widok Nie istnieją
głupie pytania
tekstowy.
P: Po uruchomieniu aplikacji układ 
2 Plik strings.xml zawiera używane przez aplikację wygląda nieco inaczej niż w edytorze 
zasoby łańcuchowe. projektu. Dlaczego tak się dzieje?
Dodaliśmy do niego etykietę przycisku i pusty łańcuch
znaków określający tymczasowo rodzaje piwa. O: Edytor projektu robi, co tylko może, by
w jak najlepszy sposób przedstawić postać
układu, niemniej jednak ma kilka ograniczeń.
3 Aktywność określa, jak aplikacja powinna prowadzić Na przykład kod XML naszego układu określa,
interakcję z użytkownikiem.
że rozwijana lista powinna być wyśrodkowana
Android Studio utworzyło dla nas prostą aktywność, w poziomie, co wcale nie musi być widoczne
na razie jednak jeszcze nic z nią nie zrobiliśmy. w edytorze projektu.
W praktyce zawsze najlepiej jest pracować
bezpośrednio na kodzie XML. W ten sposób
<Layout> znacznie łatwiej można się zorientować, co się
1
</Layout> 2 <resources>
dzieje, a oprócz tego mamy znacznie większą
kontrolę nad układem.
Układ
P:  A czy w układzie nie było jeszcze 
</resources>

strings.xml widoku tekstowego?

O: Cały czas jest, tylko aktualnie nie zawiera


żadnego tekstu, więc nie można go zauważyć.
Pojawi się w dalszej części rozdziału, kiedy
3
wyświetlimy w nim jakiś tekst.

Aktywność

52 Rozdział 2.
Tworzenie interaktywnych aplikacji

¨  Utworzenie projektu
Dodanie wartości do komponentu Spinner ¨  Aktualizacja układu
¨  Połączenie aktywności
Obecnie nasz układ zawiera rozwijaną listę, lecz jeszcze jest ona pusta. Za każdym ¨  Implementacja logiki
razem, gdy używamy komponentu tego typu, musimy zadbać o to, by zawierał on
listę wartości, gdyż tylko w takim przypadku użytkownik będzie mógł coś wybrać.
Zasoby to
Listę wartości do komponentu Spinner można przekazać mniej więcej w taki różnego rodzaju
sam sposób, w jaki określa się tekst wyświetlany w widoku tekstowym: przy użyciu
zasobów. Dotychczas używaliśmy pliku strings.xml wyłącznie do definiowania używane przez
pojedynczych wartości łańcuchowych. Tym razem musimy natomiast określić aplikację dane
tablicę takich łańcuchów i odwołać się do niej w komponencie.
i pliki, które
Tworzenie zasobu tablicowego nie są kodem.
przypomina dodawanie łańcuchów znaków
Jak już wiemy, zasób łańcuchowy można dodać do pliku strings.xml,
używając następującego kodu XML:

<string name=”nazwa_łańcucha”>wartość_łańcucha</string>

W kodzie tym nazwa_łańcucha jest identyfikatorem, a wartość_łańcucha jest


wartością zasobu.

Aby dodać tablicę łańcuchów, należy użyć kodu o następującej postaci:


<string-array name=”nazwa_tablicy_łańcuchów”> To jest nazwa tablicy.
<item>łańcuch_wartość1</item>
<item>łańcuch_wartość2</item> To są wartości umieszczone w tabli
Można dodawać dowolną ich liczb cy.
<item>łańcuch_wartość3</item> ę.
...
</string-array>
W powyższym kodzie nazwa_tablicy_łańcuchów jest nazwą tablicy, a łańcuch_
wartość1, łańcuch_wartość2 oraz łańcuch_wartość3 to poszczególne
łańcuchy znaków zapisane w tej tablicy.

Dodajmy zatem zasób string-array do naszej aplikacji. Otwórz plik strings.xml


i dodaj do niego poniższy, wyróżniony fragment kodu:
DoradcaPiwny
...
<string name=”brands”></string> app/src/main
<string-array name=”beer_colors”>
<item>jasne</item> res
<item>bursztynowe</item>
<item>brązowe</item> Dodaj ten element string-array do values
<item>ciemne</item> pliku strings.xml. Definiuje on tablicę <xml>
łańcuchów znaków o nazwie beer_colors, </xml>
</string-array> która zawiera następujące wartości: strings.xml
... jasne, bursztynowe, brązowe i ciemne.

jesteś tutaj  53
Jazda próbna

Dodanie do komponentu Spinner odwołania do string-array


W kodzie układu można odwołać się do zasobu string-array w bardzo podobny
sposób jak do zasobów łańcuchowych. W tym przypadku zamiast zapisu:

”@string/nazwa_łańcucha”

musimy użyć zapisu: Użyj zapisu @string, aby odwołać się do


zasobu łańcuchowego, i zapisu @array,
by odwołać się do tablicy.
”@array/nazwa_tablicy”

gdzie nazwa_tablicy jest nazwą zasobu tablicowego.

Użyjmy zatem naszego nowego zasobu w układzie. Przejdź do pliku DoradcaPiwny


activity_layout_beer.xml i dodaj do elementu Spinner atrybut entries:
... app/src/main
<Spinner
... res
android:layout_marginTop=”37dp”
android:entries=”@array/beer_colors” /> layout
<xml>
... </xml>
Ten zapis oznacza: „elementy
listy w komponencie Spinner activity_find_beer.xml
pochodzą z tablicy beer_colors”.

Jazda próbna komponentu Spinner


¨  Utworzenie projektu
Przekonajmy się zatem, jakie efekty wywołały te wszystkie zmiany w wyglądzie ¨  Aktualizacja układu
naszej aplikacji. Zapisz wszystkie zmodyfikowane pliki i uruchom aplikację. ¨  Połączenie aktywności
Uzyskane wyniki powinny przypominać te pokazane na poniższych rysunkach. ¨  Implementacja logiki

Domyślnie
w komponencie
wyświetlana Kliknij Kiedy
jest pierwsza komponent, klikniesz
wartość. aby rozwinąć wartość,
listę wartości. zostanie
ona
wybrana.

54 Rozdział 2.
Tworzenie interaktywnych aplikacji

¨  Utworzenie projektu
Musimy zadbać o to, by przycisk coś robił ¨  Aktualizacja układu
¨  Połączenie aktywności
Jak na razie dodaliśmy do układu komponenty GUI i wypełniliśmy rozwijaną listę, używając ¨  Implementacja logiki
tablicy łańcuchów znaków. Kolejnym zadaniem, którym musimy się zająć, jest zapewnienie,
by po kliknięciu przycisku aplikacja odpowiednio zareagowała na wartość wybraną z listy.
Chcemy, by nasza aplikacja działała mniej więcej w sposób opisany poniżej:

1 Użytkownik wybiera z rozwijanej listy 4 Metoda getBrands() odnajduje


rodzaj piwa. pasujące gatunki piwa
Użytkownik klika przycisk, by odszukać odpowiadające przekazanemu
pasujące gatunki piwa. rodzajowi, po czym zwraca je do
aktywności jako listę ArrayList
zawierającą łańcuchy znaków.
2 Układ określa, którą metodę aktywności
należy wywołać po kliknięciu przycisku.
5 Aktywność pobiera odwołanie do
widoku tekstowego umieszczonego
3 Metoda zdefiniowana w aktywności w układzie i wyświetla w nim
pobiera wartość wybraną z listy zwróconą listę gatunków piwa.
określającą ulubiony rodzaj piwa Ta lista jest wyświetlana na ekranie
i przekazuje ją do metody getBrands() urządzenia.
napisanej przez nas klasy o nazwie
BeerExpert.

2
1
<Layout>

</Layout> 3
5
Układ Aktywność
Urządzenie
getBrands("bursztynowe")
"Jack Amber"
"Red Moose"
4

BeerExpert

W pierwszej kolejności zadbajmy o to, by kliknięcie przycisku


powodowało wywołanie jakiejś metody aktywności.

jesteś tutaj  55
Atrybut onClick

Niech przycisk wywołuje metodę ¨  Utworzenie projektu


¨  Aktualizacja układu
Można przypuszczać, że zawsze dodając do układu jakiś przycisk, będziesz chcieć,
¨  Połączenie aktywności
¨  Implementacja logiki
by jego kliknięcie powodowało wykonanie jakiejś operacji. W tym celu przycisk
musi wywoływać metodę zdefiniowaną w aktywności.

Aby kliknięcie przycisku powodowało wywołanie metody aktywności, konieczne


jest wprowadzenie zmian w dwóch plikach:

 Musimy zmienić plik układu activity_find_beer.xml.


Określimy w nim, która metoda klasy aktywności ma zostać wywołana po kliknięciu przycisku.

 Musimy zmienić plik aktywności FindBeerActivity.java.


W tym pliku zdefiniujemy wywoływaną metodę.

Zacznijmy od modyfikacji układu.

Użyj onClick, by określić metodę wywoływaną przez przycisk


Poinstruowanie systemu, którą metodę należy wywołać po kliknięciu przycisku, wymaga
tylko jednego wiersza kodu XML. Musimy w tym celu dodać do elementu <Button>
atrybut android:onClick i podać w nim nazwę wywoływanej metody:
To oznacza: „kiedy komponent zostanie kliknięty, wywołaj
android:onClick=”nazwa_metody” zdefiniowaną w aktywności metodę nazwa_metody”.

Spróbujmy to zrobić. Przejdź do pliku układu, activity_find_beer.xml, i dodaj do elementu


<Button> nowy wiersz kodu XML, który określi, że kliknięcie przycisku ma spowodować
wywołanie metody onClickFindBeer():

...
<Button
DoradcaPiwny
android:id=”@+id/find_beer”
android:layout_width=”wrap_content” app/src/main
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/color” res
android:layout_below=”@+id/color”
android:text=”@string/find_beer” layout
<xml>
android:onClick=”onClickFindBeer” /> </xml>

... Kiedy przycisk


wywołaj zdef zostanie kliknięty, activity_find_beer.xml
in
metodę onClic iowaną w aktywności
Implementacj kFindBeer().
Kiedy już wprowadzisz te zmiany, zapisz plik. się na kilku
ą tej metody
kolejnych stro zajmiemy
nach.
Skoro układ już wie, którą metodę aktywności ma wywołać,
nadszedł czas, by ją zaimplementować. Przyjrzyjmy się zatem
aktywności.
56 Rozdział 2.
Tworzenie interaktywnych aplikacji

Jak wygląda kod aktywności?


Kiedy po raz pierwszy tworzyliśmy projekt aplikacji, kazaliśmy kreatorowi
wygenerować prostą aktywność o nazwie FindBeerActivity. Jej kod został
DoradcaPiwny
zapisany w pliku FindBeerActivity.java. Otwórz go teraz — wyświetl zawartość
katalogu app/src/main/java i dwukrotnie kliknij ikonę pliku.
app/src/main
Po wyświetleniu pliku przekonasz się, że Android Studio wygenerowało całkiem sporo
kodu napisanego w Javie. Zamiast analizować go teraz krok po kroku, zastąpimy java
go w całości kodem przedstawionym poniżej. Takie rozwiązanie wynika z faktu, że
przeważająca część kodu aktywności wygenerowanego przez Android Studio jest nam com.hfad.doradcapiwny
niepotrzebna, a chcemy skupić się na podstawach samego programowania aplikacji na
Androida, a nie na rozwiązaniach specyficznych dla konkretnego IDE. A zatem usuń cały
kod zapisany w pliku FindBeerActivity.java i zastąp go kodem przedstawionym poniżej: FIndBeerActivity.java

package com.hfad.doradcapiwny;

import android.os.Bundle; Klasa rozszerza klasę Activity,


czyli jedną z klas systemu Android.
import android.app.Activity;

jest
public class FindBeerActivity extends Activity { To jest metoda onCreate(), która go
wywoływana w momencie pierwsze
tworzenia aktywności.
@Override
protected void onCreate(Bundle savedInstanceState) { Metoda setContentView() informuje
super.onCreate(savedInstanceState); system o tym, którego układu używa
dana aktywność. W tym przypadku
setContentView(R.layout.activity_find_beer); jest to układ activity_find_beer.
}
}

Powyższy kod to wszystko, czego potrzeba do utworzenia prostej aktywności. Jak widać,
aktywność jest klasą, która dziedziczy po klasie android.app.Activity i implementuje
metodę onCreate().

Wszystkie aktywności muszą dziedziczyć po klasie Activity. Klasa ta definiuje grupę


metod, które przekształcają zwyczajne klasy Javy w pełnowartościowe aktywności Androida.

Oprócz tego wszystkie aktywności muszą implementować metodę onCreate(). Metoda ta


jest wywoływana w momencie tworzenia obiektu aktywności i służy do wykonania prostych Zrób to sam!
operacji konfiguracyjnych, takich jak określenie układu skojarzonego z daną aktywnością.
Ta konkretna czynność jest wykonywana poprzez wywołanie metody setContentView().
Jej wywołanie użyte w powyższym kodzie, setContentView(R.layout.activity_find_
beer), informuje system, że aktywność używa układu activity_find_beer.
Zastąp kod w swoim pliku 
Na poprzedniej stronie dodaliśmy do przycisku atrybut onClick i przypisali mu wartość FindBeerActivity.java 
onClickFindBeer. Musimy zatem dodać tę metodę do aktywności, aby można ją było kodem przedstawionym 
wywołać w momencie kliknięcia przycisku. Dzięki temu aktywność będzie w stanie
na tej stronie.
zareagować, gdy użytkownik dotknie przycisku wyświetlonego w interfejsie aplikacji.
jesteś tutaj  57
onClickFindBeer()

Dodaj do aktywności metodę onClickFindBeer() ¨  Utworzenie projektu


¨  Aktualizacja układu
Metoda onClickFindBeer() musi mieć ściśle określoną sygnaturę, gdyż
¨  Połączenie aktywności
¨  Implementacja logiki
w przeciwnym razie nie zostanie wywołana po kliknięciu przycisku wyświetlonego
w układzie. Metoda musi wyglądać dokładnie tak jak pokazaliśmy poniżej:

public void onClickFindBeer(View view) {


}
Metoda musi być Metoda musi zwracać Metoda musi mieć jeden
publiczna. wartość typu void. parametr typu View.

Jeśli metoda nie będzie miała powyższej postaci, to nie zostanie wywołana, Jeśli chcesz, aby
gdy użytkownik dotknie przycisku. Stanie się tak dlatego, że za kulisami
Android będzie poszukiwał publicznej metody zwracającej wartość typu metoda odpowiadała
void, której nazwa odpowiada wartości podanej w kodzie XML układu.
na kliknięcia przycisku,
Z pozoru można przypuszczać, że parametr View tej metody jest musi być metodą
niepotrzebny, niemniej jednak jego istnienie jest uzasadnione. Otóż
parametr ten odwołuje się do komponentu GUI, który doprowadził do publiczną, zwracać
wywołania metody (w naszym przypadku komponentem tym jest przycisk).
Jak już wspominaliśmy, elementy graficznego interfejsu użytkownika, takie
wartość typu void
jak przyciski lub widoki tekstowe, są obiektami klasy View. i mieć jeden parametr
Zmodyfikujmy zatem kod naszej aktywności. Dodaj do niej metodę typu View.
onClickFindBeer():

Używamy tej klasy,


więc musimy ją ...
zaimportować. import android.view.View;
DoradcaPiwny

public class FindBeerActivity extends Activity {


app/src/main
...
Dodaj metodę // Metoda wywoływana, gdy użytkownik kliknie przycisk
onClickFindBeer java
do klasy public void onClickFindBeer(View view) {
FindBeerActivity.java.
} com.hfad.doradcapiwny
}

FIndBeerActivity.java
<Layout>
onClickFindBeer()
</Layout>

FindBeerActivity.java
activity_find_beer.xml

58 Rozdział 2.
Tworzenie interaktywnych aplikacji

Metoda onClickFindBeer() musi coś robić ¨  Utworzenie projektu


¨  Aktualizacja układu
Skoro dodaliśmy do aktywności metodę onClickFindBerr(), kolejnym zadaniem, ¨  Połączenie aktywności
które przed nami stoi, jest sprawienie, by metoda ta coś robiła. Naszym celem jest ¨  Implementacja logiki
wyświetlenie w aplikacji listy różnych gatunków piwa, które odpowiadają rodzajowi
wybranemu przez użytkownika.

Aby to zrobić, w pierwszej kolejności musimy pobrać referencje do dwóch


komponentów GUI: rozwijanej listy i widoku tekstowego. Dzięki temu będziemy
mogli zarówno pobrać wartość wybraną przez użytkownika na rozwijanej liście,
jak i wyświetlić tekst w widoku tekstowym.

Zastosuj metodę findViewById(),


by pobrać referencję do widoku
Referencje do dwóch interesujących nas komponentów GUI można pobrać przy
użyciu metody findViewById(). Metoda ta pobiera jako parametr identyfikator
komponentu GUI i zwraca obiekt klasy View. Ten zwrócony obiekt należy następnie
rzutować do odpowiedniego typu komponentu (na przykład TextView lub Button).

Poniżej pokazaliśmy, w jaki sposób możemy użyć metody findViewById(), żeby


pobrać referencję widoku tekstowego o identyfikatorze brands: Interesuje nas widok
o identyfikatorze brands.
Przyjrzyj się dokładnie, w jaki sposób określiliśmy identyfikator widoku tekstowego.

TextView brands = (TextView) findViewById(R.id.brands);

brands to widok tekstowy, więc musisz


rzutować zwrócony wynik do tego typu.
R jest specjalną klasą
Zamiast przekazywać jego nazwę przekazaliśmy identyfikator o postaci Javy umożliwiającą
R.id.brands. Ale co to oznacza? I czym jest R?
R.java to specjalny plik Javy generowany przez narzędzia Android SDK za każdym
pobieranie odwołań
razem, gdy jest budowana aplikacja. Plik ten jest zapisywany w katalogu app/build/ do różnych zasobów
generated/source/r/debug projektu i należy do tego samego pakietu co aplikacja.
Android używa go do zarządzania wszystkimi zasobami używanymi w aplikacji, aplikacji.
a jego zawartość pozwala nam między innymi na odwoływanie się do komponentów
GUI z poziomu kodu aktywności.
Jeśli wyświetlisz kod pliku R.java, to przekonasz się, że zawiera on grupę klas Plik R.java jest
generowany
Spokojnie
wewnętrznych — po jednej klasie dla każdego typu zasobów. W każdej z tych klas
znajdują się pola odpowiadające wszystkim zasobom danego typu. Na przykład automatycznie.
klasa R zawiera klasę wewnętrzną o nazwie id, która z kolei definiuje wartość
Sami nie musimy
static final brands. Poniższe wywołanie:
niczego zmieniać w pliku R.java,
(TextView) findViewById(R.id.brands); warto jednak wiedzieć o jego
istnieniu.
używa tej wartości, by odwołać się do komponentu brands.

jesteś tutaj  59
Metody klasy View

Dysponując obiektem View, ¨  Utworzenie projektu


¨  Aktualizacja układu
można odwoływać się do jego metod ¨  Połączenie aktywności
¨  Implementacja logiki
Metoda findViewById() zwraca obiekt reprezentujący komponent GUI. Oznacza
to, że można używać metod udostępnianych przez daną klasę Javy do pobierania
i ustawiania właściwości komponentu GUI. Przyjrzyjmy się temu nieco dokładniej.

Określanie tekstu wyświetlanego w komponencie TextView


Jak wiadomo, referencję do widoku tekstowego można pobrać, używając
następującego kodu:

TextView brands = (TextView) findViewById(R.id.brands);

Wykonanie tego kodu powoduje utworzenie obiektu TextView o nazwie brands;


jednocześnie zyskujemy możliwość wywoływania metod tego obiektu.

Załóżmy, że chcesz wyświetlić w widoku tekstowym tekst „Utelka iwa”.


Moglibyśmy to zrobić w następujący sposób:

brands.setText(”Utelka iwa”); Ustawiamy tekst w komponencie


TextView na „Utelka iwa”.

Pobranie wartości wybranej w komponencie Spinner


Referencję do listy rozwijanej można pobrać w taki sam sposób jak do widoku
tekstowego. Także w tym przypadku posłużymy się metodą findViewById(),
ale zwrócony wynik rzutujemy na typ Spinner:

Spinner color = (Spinner) findViewById(R.id.color);

W ten sposób uzyskamy obiekt typu Spinner i będziemy mogli korzystać z jego metod.
Poniższy przykład pokazuje, jak można pobrać aktualnie wybraną wartość komponentu
i skonwertować ją na łańcuch znaków:

String.valueOf(color.getSelectedItem()) To wywołanie pobiera z rozwijanej listy


aktualnie wybraną wartość i konwertuje
ją na łańcuch znaków.
Wywołanie o postaci:

color.getSelectedItem()

zwraca w rzeczywistości ogólny obiekt Javy. Wynika to z faktu, że zawartością


rozwijanych list mogą być nie tylko łańcuchy znaków, lecz także na przykład obrazki.
W naszym przypadku wiemy, że są to łańcuchy znaków, więc możemy wywołać metodę
String.valueOf(), aby skonwertować Object na String.

60 Rozdział 2.
Tworzenie interaktywnych aplikacji

Aktualizacja kodu aktywności


Teraz już wiesz dostatecznie dużo, by napisać kod metody onClickFindBeer().
Zamiast pisać ten kod w jednym wierszu zacznijmy od odczytania wartości wybranej
na liście rozwijanej, a następnie wyświetlmy ją w widoku tekstowym.

Magnesiki aktywności
Ktoś napisał nową wersję metody onClickFindBeer(), którą możesz dodać
do swojej aktywności, używając do tego celu magnesików przyczepionych
na lodówce. Niestety tajemnicza kuchenna trąba powietrzna oderwała kilka
magnesików. Czy potrafisz przyczepić je z powrotem we właściwych miejscach?

Kod musi pobierać rodzaj piwa wybrany na liście rozwijanej, a następnie


wyświetlić go w widoku tekstowym.

// Metoda wywoływana, gdy użytkownik kliknie przycisk


public void onClickFindBeer(. .......... view) {

// Pobiera referencję komponentu TextView


........ brands = .......... .............. (. ...............);

// Pobiera referencję komponentu Spinner


Spinner ............. = ............ .................(................);

// Pobiera wartość wybraną w komponencie Spinner


String ............ = String.valueOf(color. ..................);

// Wyświetla wybraną wartość


brands. ................(beerType);
}

setText
findViewById R.id.color
TextView color Nie musisz
używać
R.view.brands wszystkich
R.id.brands magnesików.
(TextView) findView

Button R.view.color
findView View findViewById
(Spinner)
getSelectedItem()
beerType
jesteś tutaj  61
Magnesiki — rozwiązanie

Magnesiki aktywności. Rozwiązanie


Ktoś napisał nową wersję metody onClickFindBeer(), którą możesz dodać
do swojej aktywności, używając do tego celu magnesików przyczepionych
na lodówce. Niestety tajemnicza kuchenna trąba powietrzna oderwała kilka
magnesików. Czy potrafisz przyczepić je z powrotem we właściwych miejscach?

Kod musi pobierać rodzaj piwa wybrany na liście rozwijanej, a następnie


wyświetlić go w widoku tekstowym.

// Metoda wywoływana, gdy użytkownik kliknie przycisk


public void onClickFindBeer(. View .. view) {

// Pobiera referencję komponentu TextView


TextView . brands = . (TextView) findViewById ( R.id.brands .);

// Pobiera referencję komponentu Spinner


Spinner . color .. = (Spinner) findViewById ( R.id.color .);

// Pobiera wartość wybraną w komponencie Spinner


String beerType = String.valueOf(color. getSelectedItem() );

// Wyświetla wybraną wartość


brands. . setText ...(beerType);
}

R.view.brands Tych magnesików


nie musiałeś używać.

findView
Button
R.view.color

findView

62 Rozdział 2.
Tworzenie interaktywnych aplikacji

Pierwsza wersja aktywności ¨  Utworzenie projektu


¨  Aktualizacja układu
Nasz chytry plan zakłada, by kod aktywności pisać etapami i po każdym z nich go
¨  Połączenie aktywności
¨  Implementacja logiki
testować. Na samym końcu aktywność będzie pobierać wartość wybraną z rozwijanej
listy, wywoływać metodę napisanej przez nas klasy, a następnie wyświetlać dobrane
gatunki piwa. W przypadku pierwszej wersji aktywności chcemy jedynie upewnić się,
że prawidłowo pobraliśmy wartość wybraną z rozwijanej listy.

Poniżej przedstawiliśmy kod aktywności, włącznie z metodą, którą uzupełniłeś na


poprzedniej stronie. Wprowadź te zmiany w kodzie pliku FindBeerActivity.java,
a następnie go zapisz:

package com.hfad.doradcapiwny;
DoradcaPiwny

import android.os.Bundle;
import android.app.Activity; app/src/main

import android.view.View;
java
import android.widget.Spinner; Używamy tych
dodatkowych klas.
import android.widget.TextView;
com.hfad.doradcapiwny

public class FindBeerActivity extends Activity {


FIndBeerActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) { Tej metody nie musimy zmieniać.
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_find_beer);
}

// Metoda wywoływana, gdy użytkownik kliknie przycisk


public void onClickFindBeer( View view) {
// Pobiera referencję komponentu TextView
TextView brands = (TextView) findViewById (R.id.brands); Metoda findViewById()
zwraca obiekt View,
// Pobiera referencję komponentu Spinner musimy go zatem rzutować
Spinner color = (Spinner) findViewById(R.id.color); na odpowiedni typ.
// Pobiera wartość wybraną w komponencie Spinner
String beerType = String.valueOf(color.getSelectedItem() );
// Wyświetla wybraną wartość
brands. setText(beerType); Metoda getSelectedItem()
zwraca wynik typu Object,
} musimy zamienić go na String.
}

jesteś tutaj  63
Co się dzieje?

Co ten kod robi? ¨  Utworzenie projektu


¨  Aktualizacja układu
Zanim weźmiemy nasz kod na jazdę próbną, spróbujmy zrozumieć,
¨  Połączenie aktywności
¨  Implementacja logiki
co on tak naprawdę robi.

1 Użytkownik wybiera rodzaj piwa z rozwijanej listy i klika przycisk Odszukaj piwo!.
Powoduje to wywołanie metody public void onClickFindBeer(View) zdefiniowanej w aktywności.
Układ określa, którą metodę aktywności należy wywołać w reakcji na kliknięcie przycisku przy użyciu
właściwości android:onClick elementu <Button>.

<Layout>

</Layout>

Układ FindBeerActivity

2 Aktywność pobiera referencje do komponentów TextView i Spinner, wywołując w tym celu


metodę findViewById().

komponent Spinner

FindBeerActivity

komponent TextView

3 Aktywność odczytuje wartość wybraną w komponencie Spinner i konwertuje ją na łańcuch znaków.

bursztynowe
FindBeerActivity komponent
Spinner

4 Aktywność określa wartość właściwości text komponentu TextView tak, by odpowiadała ona
aktualnie wybranemu rodzajowi piwa.

"bursztynowe"

FindBeerActivity komponent TextView

64 Rozdział 2.
Tworzenie interaktywnych aplikacji

Jazda próbna — test modyfikacji


Wprowadź wszystkie zmiany w kodzie aktywności, następnie
zapisz plik i uruchom aplikację. Tym razem kiedy klikniesz
przycisk Odszukaj piwo!, aplikacja wyświetli wartość,
która w danej chwili będzie wybrana z rozwijanej listy.

piwa jest
Wybrany rodzaj doku
wyświetlany w wi
tekstowym.

Nie istnieją
głupie pytania

P : Dodałem nowy łańcuch znaków do pliku strings.xml,  P: W tym przypadku? A nie zawsze?
ale nie mogę go znaleźć w pliku R.java. Dlaczego go tam 
nie ma? O: Rozwijanych list można używać do bardziej złożonych rzeczy
niż wyświetlanie tekstów. Na przykład można na nich wyświetlać
O: Android Studio generuje plik R.java w momencie zapisywania ikonę obok tekstu. Fakt, że metoda getSelectedItem() zwraca
zmian wprowadzonych w projekcie. Jeśli dodałeś zasoby, lecz nie wynik typu Object, zapewnia większą elastyczność.
widzisz ich w pliku R.java, to sprawdź, czy zmiany zostały zapisane.
Plik R.java jest także aktualizowany w momencie budowania P: Czy nazwa metody onClickFindBeer ma znaczenie?
aplikacji. Aplikacja jest budowana przed jej uruchomieniem,
a zatem także uruchomienie aplikacji powoduje zaktualizowanie
O: Znaczenie ma tylko to, by nazwa metody w kodzie aktywności
odpowiadała nazwie podanej w atrybucie onClick przycisku
pliku R.java.
w kodzie układu.
P: Wygląda na to, że wartości wyświetlane w rozwijanej  P: Dlaczego zastąpiliśmy kod aktywności wygenerowany 
liście są statyczne, gdyż są to łańcuchy podane w zasobie 
przez Android Studio?
string-array. Czy można te wartości modyfikować 
programowo? O: Zintegrowane środowiska programistyczne, takie jak Android
O: Owszem, można, choć jest to bardziej złożone niż użycie Studio, zawierają wiele funkcji i narzędzi umożliwiających
programistom oszczędzanie czasu. Generują za nas dużo kodu, co
wartości statycznych. W dalszej części książki pokażemy,
w jaki sposób można uzyskać pełną kontrolę nad wartościami czasami jest bardzo użyteczne. Uważamy jednak, że podczas nauki
wyświetlanymi w komponentach, w tym także na rozwijanych nowego języka lub nowego obszaru zagadnień programistycznych,
listach. takich jak tworzenie aplikacji na Androida, lepiej jest poznawać
podstawy języka, a nie kod generowany przez IDE. Dzięki temu
P: Jaki jest typ obiektu zwracanego przez metodę  można lepiej zrozumieć kod, którego następnie będzie można
używać niezależnie od stosowanego IDE.
getSelectedItem()?

O: Jest on zadeklarowany jako Object. Ponieważ wartości


zostały określone przy użyciu zasobu string-array, w tym
przypadku faktycznym typem zwracanej wartości jest String.

jesteś tutaj  65
Klasa BeerExpert

Tworzenie własnej klasy Javy <Layout>

Zgodnie z tym, co sygnalizowaliśmy już na samym początku rozdziału, nasza </Layout> <resources>

aplikacja Doradca piwny wyświetla rekomendowane gatunki piwa, używając


Układ </resources>
napisanej przez nas niestandardowej klasy języka Java. Jest to najzwyklejsza
klasa Javy, która nic nie wie o tym, że jest używana w aplikacji działającej strings.xml
w systemie Android.

Specyfikacja własnej klasy Javy


Nasza klasa musi spełniać następujące wymagania: Aktywność

 Musi należeć do pakietu o nazwie com.hfad.doradcapiwny. Musimy napisać


klasę, której
 Musi mieć nazwę BeerExpert. aktywność będzie
mogła używać
do odnajdywania
 Ma udostępniać jedną metodę, getBrands(), pobierającą preferowany gatunków piwa
odpowiadających
kolor piwa (określony jako łańcuch znaków), i zwracać obiekt typu BeerExpert rodzajowi
List<String>, listę zawierającą sugerowane gatunki piwa. wybranemu przez
użytkownika.

Implementacja i testowanie klasy


DoradcaPiwny
Klasy Javy mogą być niesłychanie złożone i korzystać z wywołań metod
implementujących skomplikowaną logikę aplikacji. Możesz bądź to napisać
app/src/main
swoją własną wersję tej klasy, bądź też skorzystać z naszej, bardzo wyszukanej
wersji, przedstawionej poniżej:
java
package com.hfad.doradcapiwny;
import java.util.ArrayList; com.hfad.doradcapiwny
import java.util.List; To zwyczajny kod napisany
w Javie, nie ma nic
wspólnego z Androidem. BeerExpert.java
public class BeerExpert {
List<String> getBrands(String color) {
List<String> brands = new ArrayList<String>();
if (color.equals(”bursztynowe”)) { Zrób to sam!
brands.add(”Jack Amber”);
brands.add(”Red Moose”);
} else {
brands.add(”Jail Pale Ale”); Dodaj do projektu klasę BeerExpert. Zaznacz 
brands.add(”Gout Stout”); pakiet com.hfad.doradcapiwny w katalogu app/
} src/main/java i wybierz opcję File/New.../Java
return brands; Class. W efekcie zostanie utworzona nowa 
} klasa należąca do wybranego pakietu.
}

66 Rozdział 2.
Tworzenie interaktywnych aplikacji

Dodaj do aktywności wywołanie metody naszej klasy, ¨  Utworzenie projektu


¨  Aktualizacja układu
aby była wyświetlana FAKTYCZNA porada ¨  Połączenie aktywności
¨  Implementacja logiki
W drugiej wersji kodu aktywności musimy rozszerzyć kod metody onClickFindBeer()
o wywołanie metody klasy BeerExpert i zastosowanie zwróconych przez nią rekomendacji.
Te zmiany wiążą się z zastosowaniem zwyczajnego kodu napisanego w Javie. Możesz
spróbować samodzielnie napisać ten kod i wypróbować jego działanie, uruchamiając
aplikację, bądź też możesz odwrócić kartkę i przeanalizować nasze rozwiązanie.

Zaostrz ołówek
Rozszerz kod aktywności tak, by wywoływał metodę getBrands()
klasy BeerExpert i wyświetlał wyniki w widoku tekstowym.

package com.hfad.doradcapiwny;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.List; Ten wiersz kodu dodaliśmy za Ciebie.

public class FindBeerActivity extends Activity {


private BeerExpert expert = new BeerExpert(); Musisz użyć klasy
... BeerExpert, by pobrać
rekomendowane gatunki piwa,
// Metoda wywoływana, gdy użytkownik kliknie przycisk zatem dodaliśmy także ten
public void onClickFindBeer( View view) { wiersz kodu.
// Pobiera referencję komponentu TextView
TextView brands = (TextView) findViewById (R.id.brands);
// Pobiera referencję komponentu Spinner
Spinner color = (Spinner) findViewById(R.id.color);
// Pobiera wartość wybraną w komponencie Spinner
String beerType = String.valueOf(color. getSelectedItem() );

} Twoim zadaniem jest zaktualizowa


} metody onClickFindBeer(). nie kodu

jesteś tutaj  67
Rozwiązanie zadania

Zaostrz ołówek
Rozwiązanie Rozszerz kod aktywności tak, by wywoływał metodę getBrands()
klasy BeerExpert i wyświetlał wyniki w widoku tekstowym.

package com.hfad.doradcapiwny;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.List;

public class FindBeerActivity extends Activity {


private BeerExpert expert = new BeerExpert();
...

// Metoda wywoływana, gdy użytkownik kliknie przycisk


public void onClickFindBeer( View view) {
// Pobiera referencję komponentu TextView
TextView brands = (TextView) findViewById (R.id.brands);
// Pobiera referencję komponentu Spinner
Spinner color = (Spinner) findViewById(R.id.color);
// Pobiera wartość wybraną w komponencie Spinner
String beerType = String.valueOf(color. getSelectedItem() );

// pobranie rekomendacji z klasy BeerExpert

List<String> brandsList = expert.getBrands(beerType); Pobiera listę


sugerowanych gatunków.
StringBuilder brandsFormatted = new StringBuilder(); Konstruuje łańcuch znaków,
w którym zostaną zapisane
wartości z listy.
for (String brand : brandsList) {

brandsFormatted.append(brand).append( ‘\n’); Wyświetla każdy


gatunek piwa
w osobnym wierszu.
}

// wyświetlenie wyników
Wyświetla wyniki
brands.setText(brandsFormatted); w widoku tekstowym.

}
} wymaga jedynie zwyczajnego
Zastosowanie klasy BeerExpert nie musisz się przejmować,
sane go w Javi e, dlate go
kodu napi lądać nieco inaczej niż nasze.
jeśli Twoje rozw iąza nie będz ie wyg

68 Rozdział 2.
Tworzenie interaktywnych aplikacji

Kod aktywności, wersja 2 ¨  Utworzenie projektu


¨  Aktualizacja układu
Poniżej zamieściliśmy pełną wersję kodu aktywności. Wprowadź zmiany w swojej
¨  Połączenie aktywności
¨  Implementacja logiki
wersji pliku FindBeerActivity.java. Upewnij się, że dodałeś do projektu klasę
BeerExpert, a następnie zapisz wszystkie zmiany:

package com.hfad.doradcapiwny;
DoradcaPiwny
import android.os.Bundle;
import android.app.Activity; app/src/main
import android.view.Menu;
import android.view.View; java
import android.widget.Spinner;
import android.widget.TextView; com.hfad.doradcapiwny
import java.util.List; Używamy tej dodatkowej klasy.

FIndBeerActivity.java
public class FindBeerActivity extends Activity {
private BeerExpert expert = new BeerExpert();

Dodaj instancję klasy BeerExpert jako prywatne pole klasy.


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_find_beer);
}

// Metoda wywoływana, gdy użytkownik kliknie przycisk


public void onClickFindBeer( View view) {
// Pobiera referencję komponentu TextView
TextView brands = (TextView) findViewById (R.id.brands);
// Pobiera referencję komponentu Spinner
Spinner color = (Spinner) findViewById(R.id.color);
// Pobiera wartość wybraną w komponencie Spinner
String beerType = String.valueOf(color. getSelectedItem() );
// Pobranie rekomendacji z klasy BeerExpert Użyj klasy BeerExpert do
List<String> brandsList = expert.getBrands(beerType); pobrania listy sugerowanych
gatunków piwa.
StringBuilder brandsFormatted = new StringBuilder();
for (String brand : brandsList) {
Skonstruuj łańcuch znaków,
brandsFormatted.append(brand).append(‘\n’); który wyświetli każdy
} gatunek w nowym wierszu.
// Wyświetlenie wyników
brands.setText(brandsFormatted); Wyświetl skonstruowany łańcuch znaków
} w widoku tekstowym.
}

jesteś tutaj  69
Co się dzieje?

Co się dzieje podczas wykonywania tego kodu?


1 Kiedy użytkownik klika przycisk Odszukaj piwo!, zostaje wywołana
metoda onClickFindBeer() aktywności.
Metoda ta tworzy referencje do listy rozwijanej i widoku tekstowego,
a następnie pobiera wartość wybraną w liście.

<Layout>

</Layout> bursztynowe komponent


FindBeerActivity Spinner
Układ
komponent
TextView

2 Metoda onClickFindBeer() wywołuje metodę getBrands() klasy BeerExpert,


przekazując do niej wartość wybraną w komponencie Spinner.
Metoda getBrands() zwraca listę sugerowanych gatunków piwa.

getBrands("bursztynowe")

"Jack Amber"
FindBeerActivity "Red Moose" BeerExpert

3 Metoda onClickFindBeer() formatuje listę gatunków piwa i zapisuje ją


we właściwości text komponentu TextView.

"Jack Amber
Red Moose"

FindBeerActivity komponent
TextView

70 Rozdział 2.
Tworzenie interaktywnych aplikacji

Jazda próbna — test aplikacji ¨  Utworzenie projektu


¨  Aktualizacja układu
Kiedy już wprowadzisz w aplikacji zmiany przedstawione na poprzedniej stronie,
¨  Połączenie aktywności
¨  Implementacja logiki
uruchom ją i przetestuj. Spróbuj wybierać różne rodzaje piwa i za każdym razem
klikaj przycisk Odszukaj piwo!.

Takie wyniki uzyskasz


po wybraniu opcji „jasne”.

Takie wyniki uzyskasz


po wybraniu opcji
„bursztynowe”.

Po wybraniu rodzaju piwa i kliknięciu przycisku


Odszukaj piwo!, aplikacja użyje klasy BeerExpert do
określenia i wyświetlenia sugerowanych gatunków piwa.

jesteś tutaj  71
Przybornik

Twój przybornik do Androida


Rozdział 2.

Opanowałeś już rozdział 2. i dodałeś  j
Pełny kod przykładowe
do swojego przybornika z narzędziami  aplikacji prezen tow ane j w  tym
umiejętności tworzenia interaktywnych  rozdziale mo żes z pob rać
aplikacji na Androida. nictwa
z serwera FTP wydaw
.helion .pl/
Helion: ftp://ftp
przyklady/an drrg.z ip

    CELNE SPOSTRZEŻENIA
 Element Button pozwala dodawać przyciski.
 Element Spinner umożliwia dodawanie pola, którego kliknięcie powoduje wyświetlenie listy wartości.
 Wszystkie komponenty GUI (graficznego interfejsu użytkownika) są rodzajami widoków.
Wszystkie one dziedziczą po klasie View.
 Tablicę łańcuchów znaków można dodawać, używając następującego zapisu:
<string-array name=”tablica”>
<item>łańcuch1</item>
...
</string-array>

 Do zasobu string-array można odwołać się w układzie, używając zapisu:


”@array/nazwa_tablicy”

 Aby kliknięcie przycisku wywołało metodę, należy dodać do jego kodu w układzie następujący atrybut:
android:onClick=”metodaClick”

Oprócz tego w aktywności należy zaimplementować metodę o podanej nazwie:


public void metodaClick(View view) {
}
 Plik R.java jest generowany automatycznie. Pozwala on pobierać w kodzie Javy referencje do układów,
komponentów GUI, łańcuchów znaków oraz wszelkich innych zasobów.
 Metoda findViewById() zwraca referencję do widoku.
 Metoda setText() pozwala określić tekst wyświetlany w widoku.
 Metoda getSelectedItem() zwraca wartość, która jest aktualnie wybrana z rozwijanej listy.
 Nowe klasy można dodawać do projektu, wybierając z menu opcję File/New.../Java Class.

72 Rozdział 2.
3. Wiele aktywności i intencji

Jakie są Twoje intencje?


Wysłałam intencję w sprawie obsługi
mojej ACTION_CALL i dostałam
do wyboru oferty przeróżnych
aktywności.

Większość aplikacji potrzebuje więcej niż jednej aktywności.


Dotychczas mieliśmy do czynienia z aplikacjami składającymi się tylko z jednej aktywności. Kiedy
jednak sprawy się komplikują, jedna aktywność zwyczajnie nie wystarczy. Dlatego w tym rozdziale
pokażemy Ci, jak tworzyć aplikacje składające się z wielu aktywności i jak nasze aplikacje
mogą porozumiewać się z innymi, wykorzystując w tym celu intencje. Pokażemy także, jak można
używać intencji, by wykraczać poza granice naszych aplikacji, i jak wykorzystywać aktywności
należące do innych aplikacji dostępnych w urządzeniu do wykonywania akcji. To wszystko
zapewni nam znacznie większe możliwości.

to jest nowy rozdział  73


Zadania

Aplikacja może zawierać więcej niż jedną aktywność


Wcześniej w tej książce napisaliśmy, że aktywność jest jedną, dobrze zdefiniowaną Aktywność jest
operacją, którą użytkownik może wykonywać w aplikacji, taką jak na przykład
wyświetlanie listy receptur. Jeśli aplikacja jest w miarę prosta, to taka jedna aktywność
jedną, konkretnie
może w zupełności wystarczyć. określoną operacją,
którą użytkownik
Jednak w bardzo wielu przypadkach użytkownik będzie chciał robić więcej niż jedną
rzecz — na przykład oprócz wyświetlania listy receptur będzie także chciał je dodawać. może wykonywać.
W takich przypadkach konieczne będzie zastosowanie więcej niż jednej aktywności: Połączenie większej
jednej do wyświetlania listy receptur i drugiej do dodawania nowych receptur.
liczby aktywności
Najlepszym sposobem, by zrozumieć, o co tu chodzi, jest przeanalizowanie w celu wykonania
odpowiedniego przykładu w działaniu. W tym rozdziale napiszemy aplikację składającą
czegoś bardziej
się z dwóch aktywności. Pierwsza z nich będzie umożliwiała wpisanie wiadomości.
Kliknięcie przycisku wyświetlanego w pierwszej aktywności spowoduje uruchomienie złożonego
drugiej aktywności i przekazanie do niej treści wiadomości. Ta druga aktywność nazywamy zadaniem.
wyświetli wiadomość.

Pierwsza aktywność Kiedy klikniesz przycisk


umożliwia wpisanie Wyślij wiadomość
wiadomości. wyświetlony w pierwszej
aktywności, treść
wiadomości zostanie
przekazana do drugiej
aktywności. Następnie
druga aktywność
zostanie wyświetlona na
pierwszej i wyświetli na
ekranie przekazaną treść
wiadomości.

Oto czynności, które wykonasz:


1 Utworzysz prostą aplikację z jedną aktywnością i układem.

2 Dodasz do aplikacji drugą aktywność i układ.

3 Sprawisz, że pierwsza aktywność wywoła drugą.

4 Przekażesz dane z pierwszej aktywności do drugiej.

74 Rozdział 3.
Wiele aktywności i intencji

Oto struktura naszej aplikacji


Aplikacja składa się z dwóch aktywności i dwóch układów:

1 Podczas uruchamiania aplikacji zostanie wykonana aktywność


CreateMessageActivity.
Ta aktywność używa układu o nazwie activity_create_message.xml.

2 Użytkownik klika przycisk wyświetlony w aktywności CreateMessageActivity.


To kliknięcie powoduje uruchomienie aktywności ReceiveMessageActivity,
która z kolei używa układu activity_receive_message.xml.

<Layout> <Layout>

</Layout> </Layout>

activity_create_message.xml activity_receive_message.xml
Teskt wpisany w aktywności
CreateMessageActivity
zostaje przekazany
do aktywności
1 ReceiveMessageActivity.

Urządzenie CreateMessageActivity.java ReceiveMessageActivity.java

Utworzenie projektu ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
¨  Wywołanie drugiej aktywności
Projekt tej aplikacji możesz utworzyć w dokładnie taki sam sposób, ¨  Przekazanie danych
w jaki tworzyłeś projekty w dwóch poprzednich rozdziałach. Utwórz
w Android Studio nowy projekt aplikacji o nazwie Komunikator
i użyj przy tym pakietu o nazwie com.hfad.komunikator. Jako
minimalny poziom API wybierz API poziomu 15, tak by aplikacja
mogła działać na większości urządzeń. Aby Twoja aplikacja
odpowiadała naszej, przedstawionej w tej książce, będziesz także
potrzebować pustej aktywności o nazwie CreateMessageActivity
i układu o nazwie activity_create_message.

Na następnej stronie zajmiemy się aktualizacją układu aplikacji.

jesteś tutaj  75
Aktualizacja układu

Aktualizacja układu Komunikator

Poniżej przedstawiliśmy kod XML umieszczony w pliku activity_create_message.xml. app/src/main


Usunęliśmy z niego element <TextView> umieszczony tam przez Android Studio
i zastąpiliśmy go dwoma nowymi elementami: <Button> i <EditText>. Element res
<EditText> tworzy pole tekstowe, którego można używać do wpisywania danych.
layout
Zmień swój plik activity_create_message.xml, tak by zawierał poniższy kod XML: <xml>
</xml>

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android” activity_create_


message.xml
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
tools:context=”.CreateMessageActivity” >

<Button android:id=”@+id/send”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
Zastąp android:layout_alignParentLeft=”true”
element
<TextView> android:layout_alignParentTop=”true”
wygenerowany
przez Android android:layout_marginLeft=”36dp”
Studio android:layout_marginTop=”21dp”
elementami Kliknięcie przycisku spowoduje wywołanie
<Button> android:onClick=”onSendMessage” metody onSendMessage() zdefiniowanej
i <EditText>. w aktywności.
android:text=”@string/send” />
To jest zasób łańcuchowy.
<EditText android:id=”@+id/message”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” Element <EditText> definiuje
android:layout_alignLeft=”@+id/send” pole tekstowe służące
android:layout_below=”@+id/send” do wpisywania danych.
android:layout_marginTop=”18dp”
Dziedziczy on po tej samej
android:ems=”10” >
</EditText> klasie View co wszystkie inne
Ten atrybut określa, jak szerokie powinno
być pole <EditText>. W tym przypadku
komponenty GUI, które
</RelativeLayout> jego wielkość powinna wystarczyć do
zapisania 10 wielkich liter „M”.
poznaliśmy do tej pory.

76 Rozdział 3.
Wiele aktywności i intencji

Aktualizacja pliku strings.xml… ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
¨  Wywołanie drugiej aktywności
Tekst prezentowany na przycisku dodanym do układu jest pobierany z zasobu ¨  Przekazanie danych
@string/send. Oznacza to, że musisz dodać zasób o nazwie send do pliku
strings.xml i określić jego wartość. To właśnie ta wartość będzie tekstem,
który zostanie wyświetlony na przycisku. Zrób to teraz:
Komunikator
...
<string name=”send”>Wyślij wiadomość</string> app/src/main
Dodaj nowy zasób
... łańcuchowy o nazwie
send. W naszej aplikacji res
zapisaliśmy w nim
łańcuch znaków „Wyślij
…i dodanie metody do kodu aktywności wiadomość”, dlatego
właśnie ten tekst
values
<xml>
Poniższy atrybut umieszczony w elemencie <Button>: zostanie wyświetlony </xml>
na przycisku. strings.xml
android:onClick=”onSendMessage”
oznacza, że kliknięcie przycisku spowoduje wywołanie metody
onSendMessage() zdefiniowanej w aktywności. Musimy ją zatem dodać.

Otwórz plik CreateMessageActivity.java i zastąp kod wygenerowany przez


Android Studio poniższym kodem:
package com.hfad.komunikator;
Zastąpiliśmy kod wygenerowany
przez Android Studio, gdyż jego
import android.app.Activity; przeważająca część nie jest
import android.os.Bundle; nam do niczego potrzebna.

import android.view.View;

public class CreateMessageActivity extends Activity { Komunikator


ana
Metoda onCreate() jest wywoływ
za pierwszym razem, gdy jest app/src/main
@Override tworzona aktywność.
protected void onCreate(Bundle savedInstanceState) { java
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message); com.hfad.komunikator
}
CreateMessage
// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku Activity.java

public void onSendMessage(View view) { Ta metoda zostanie wywołana po kliknięciu


} przycisku. Jej kod uzupełnimy w trakcie
dalszych prac nad aplikacją.
}

Skoro załatwiliśmy sprawę pierwszej aktywności, przejdźmy teraz


do drugiej.
jesteś tutaj  77
Utworzenie aktywności

¨  Utworzenie pierwszej aktywności
Utworzenie drugiej aktywności i układu ¨  Utworzenie drugiej aktywności
¨  Wywołanie drugiej aktywności
Android Studio udostępnia kreator umożliwiający dodawanie do aplikacji ¨  Przekazanie danych
kolejnych aktywności i układów. Stanowi on uproszczoną wersję kreatora nowych
aplikacji i możemy go używać za każdym razem, gdy chcemy dodać do aplikacji
nową aktywność.

Aby utworzyć nową aktywność, wybierz z menu głównego Android Studio opcję
File/New/Activity, a następnie wybierz opcję Blank Activity. W efekcie na ekranie
zostanie wyświetlone okno, w którym będziesz mógł podać dane dotyczące tworzonej
aktywności.

Tworząc nowe aktywności i układy, należy określić ich nazwy. W tym przypadku
aktywności nadaj nazwę ReceiveMessageActivity, a układowi — activity_receive_
message. Upewnij się, czy oba pliki znajdą się w pakiecie com.hfad.komunikator.
W pozostałych polach zaakceptuj wartości domyślne, a następnie kliknij przycisk Finish.

Aktywności nadaj nazwę „ReceiveMessage


Activity”,
a układowi „activity_receive_message”.

W pozostałych
polach zaakceptuj
wartości
domyślne, gdyż
interesuje nas
utworzenie nowej
aktywności
i nowego układu.
Większość kodu
wygenerowanego
przez Android
Studio usuniemy
i zastąpimy
własnym.

78 Rozdział 3.
Wiele aktywności i intencji

Co się właściwie stało?


Kiedy kliknąłeś przycisk Finish, Andorid Studio wygenerowało
dla Ciebie lśniący nowością plik aktywności i równie nowiutki
plik układu. Jeśli spojrzysz do eksploratora, przekonasz
się, że w katalogu app/src/main/java pojawił się plik
ReceiveMessageActivity.java, a w katalogu app/src/main/res/layout
— plik activity_receive_message.xml.

To są dwa nowe, utworzone przed chwilą pliki: nowa


aktywność i jej układ. Teraz w aplikacji znajdują się
już dwie aktywności i dwa układy.

Każda aktywność używa innego układu. Pierwsza aktywność,


CreateMessageActivity, używa układu activity_create_
message.xml, natomiast druga, ReceiveMessageActivity
— układu activity_receive_message.xml.

<Layout> <Layout>

</Layout> </Layout>

activity_create_message.xml activity_receive_message.xml

CreateMessageActivity.java RecieveMessageActivity.java

Za kulisami Android Studio wprowadziło także niezbędne


zmiany w pliku konfiguracyjnym aplikacji — AndroidMainfest.xml.
Przyjrzyjmy się mu zatem nieco dokładniej.

jesteś tutaj  79
AndroidManifest.xml

¨  Utworzenie pierwszej aktywności
Przedstawiamy plik manifestu aplikacji ¨  Utworzenie drugiej aktywności
na Androida ¨  Wywołanie drugiej aktywności
¨  Przekazanie danych
Każda aplikacja na Androida musi zawierać plik o nazwie AndroidManifest.xml. Można
go znaleźć w katalogu projektu, w podkatalogu app/src/main. Plik ten zawiera kluczowe
informacje dotyczące aplikacji, takie jak aktywności, z których się składa, używane Komunikator
biblioteki oraz wiele innych deklaracji. Android generuje dla nas ten plik w momencie
tworzenia aplikacji. Jeśli przypomnisz sobie ustawienia wybierane podczas tworzenia app/src/main
aplikacji, to niektóre fragmenty kodu pliku manifestu będą wyglądać podobnie. <xml>
Plik AndoridManifest.xml </xml>

Poniżej przedstawiliśmy naszą wersję pliku AndroidManiest.xml: można znaleźć w tym AndroidManifest.xml
katalogu.
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.hfad.komunikator” > To jest podana przez
nas nazwa pakietu.
Jeśli 
piszesz 
<application Android Studio dodało do
naszej aplikacji domyślną ikonę.
Obejrzyj to!aplikacje 
android:allowBackup=”true” Przyjrzymy się jej dokładniej na Androida, nie 
android:icon=”@mipmap/ic_launcher” w dalszej części książki. używając żadnego 
zintegrowanego 
android:label=”@string/app_name” Motyw graficzny określa środowiska 
android:theme=”@style/AppTheme” > wygląd aplikacji. Także o tych
zagadnieniach dowiesz się programistycznego, 
więcej w dalszej części książki. to musisz utworzyć 
ten plik ręcznie.
<activity
android:name=”.CreateMessageActivity”
To jest nasza android:label=”@string/app_name” > Ten wiersz kodu
pierwsza określa, że to jest
aktywność, <intent-filter> główna aktywność
Create aplikacji.
<action android:name=”android.intent.action.MAIN” />
Message
Activity. <category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity> Z kolei ten wiersz informuje,
że aktywności można użyć
do uruchomienia aplikacji.
<activity
To jest
nasza druga android:name=”.ReceiveMessageActivity”
aktywność,
Receive android:label=”@string/title_activity_receive_message” >
Message
Activity. </activity>

Android Studio dodało ten fragment


</application> kodu, kiedy utworzyliśmy drugą
aktywność.
</manifest>

80 Rozdział 3.
Wiele aktywności i intencji

Każdą aktywność należy zadeklarować


Wszystkie aktywności należy zadeklarować w pliku AndrodiManifest.xml.
Jeśli nie zostanę
Jeśli aktywność nie zostanie zadeklarowana w tym pliku, to system nie wymieniona w pliku
będzie o niej wiedział. A jeśli system nie będzie nic widział o aktywności, AndroidManifest.xml, to
to nigdy nie zostanie ona wykonana. z punktu widzenia systemu
nie będę istnieć i nigdy
W pliku manifestu aktywności deklaruje się przy użyciu elementów nie zostanę wywołana.
<activity> umieszczanych wewnątrz elementu <application>. Okazuje
się, że każdej aktywności w aplikacji musi odpowiadać element <activity>.
Poniżej przedstawiamy jego ogólną postać:
<application
Każda aktywność musi zostać
... zadeklarowana wewnątrz elementu
<application>.
...> Ten wiersz jest
obowiązkowy.
<activity
android:name=”nazwa_klasy_aktywności” Ten wiersz jest opcjonalny, Aktywność
ale Android Studio
android:label=”@string/etykieta_aktywności” generuje go za nas.
... Aktywność może zawierać także inne właściwości.
...>
...
</activity>
...
</application>
Nasza druga 

Obejrzyj to!
Poniższy wiersz kodu jest obowiązkowy i służy do określenia nazwy klasy aktywności: aktywność 
została 
android:name=”nazwa_klasy_aktywności” automatycznie 
zadeklarowana, gdyż 
Przy czym nazwa_klasy_aktywności to, jak łatwo się domyślić, nazwa klasy dodaliśmy ją, używając 
aktywności poprzedzona znakiem kropki (.). W naszym przypadku będzie to zatem kreatora Android Studio.
.ReceiveMessageActivity. Nazwa klasy jest poprzedzona znakiem kropki, gdyż
Android łączy nazwę klasy z nazwą pakietu, uzyskując w ten sposób pełną nazwę klasy. W przypadku samodzielnego
dodawania dodatkowych
Ten wiersz jest opcjonalny i służy do określenia przyjaznej dla użytkownika nazwy aktywności będziemy
aktywności: musieli ręcznie wprowadzać
odpowiednie zmiany
android:label=”@string/etykieta_aktywności”
w pliku manifestu —
Ta nazwa jest wyświetlana na samej górze ekranu w czasie, gdy dana aktywność jest AndroidManifest.xml. Także
wykonywana. Jeśli atrybut nie zostanie określony, Android będzie w tym miejscu w przypadku stosowania innego
wyświetlał nazwę aplikacji. IDE informacje o dodatkowych
aktywnościach mogą nie być
Deklaracja aktywności może zawierać także inne właściwości, takie jak uprawnienia automatycznie dodawane do
niezbędne do wykonania aktywności, oraz informację, czy dana aktywność może być pliku manifestu.
używana przez aktywności należące do innych aplikacji.

jesteś tutaj  81
intencje

¨  Utworzenie pierwszej aktywności
Intencja jest rodzajem komunikatu ¨  Utworzenie drugiej aktywności
¨  Wywołanie drugiej aktywności
Dotychczas stworzyliśmy aplikację z dwiema aktywnościami, z których każda używa ¨  Przekazanie danych
własnego układu. W momencie uruchamiania aplikacji zostanie wykonana pierwsza
aktywność — CreateMessageActivity. Naszym kolejnym zadaniem jest zadbanie
o to, by po kliknięciu przycisku Wyślij wiadomość aktywność CreateMessageActivity Aktywność uruchamia się,
wywołała drugą aktywność — ReceiveMessageActivity.
tworząc intencję i używając
Zawsze, gdy chcemy uruchomić jedną aktywność z poziomu innej aktywności, musimy
użyć w tym celu intencji. Można ją sobie wyobrażać jako „intencję zrobienia czegoś”. jej w wywołaniu metody
Intencja to rodzaj komunikatu pozwalającego powiązać ze sobą niezależne obiekty
(takie jak aktywności) w trakcie działania aplikacji. Jeśli jedna aktywność chce startActivity().
uruchomić drugą, robi to, przesyłając do systemu Android odpowiednią intencję.
W efekcie system uruchomi aktywność i przekaże do niej tę intencję.
Intencja określa aktywność,
Do utworzenia i przesłania intencji wystarczy jeden lub dwa wiersze kodu. W pierwszej do której chcemy ją przesłać.
Informację tę można by porównać
kolejności należy utworzyć intencję, używając poniższego wiersza kodu: z adresem umieszczonym na
Intent intent = new Intent(this, KlasaDocelowa.class); kopercie.

Pierwszy parametr tego wywołania informuje system, z jakiego obiektu pochodzi Intencja
intencja, i jak widać, aby odwołać się do bieżącej aktywności, można posłużyć
się referencją this. Drugim parametrem jest nazwa klasy aktywności, do której
intencja jest skierowana.
Do: InnaAktywnosc
Po utworzeniu intencji można ją przekazać do systemu Android w następujący
sposób:
Wywołanie metody startActivity()
startActivity(intent); uruchamia aktywność określoną w intencji.

To wywołanie informuje system, że ma uruchomić aktywność określoną przez


przekazaną intencję.

Kiedy Android odbierze intencję, sprawdza, czy wszystko jest w porządku, po czym
uruchamia aktywność. Jeśli aktywności nie uda się odnaleźć, zgłaszany jest wyjątek
ActivityNotFoundException.

Niech no sprawdzę.
„Mój drogi Tak, wydaje się, że wszystko jest
Androidzie, czy mogłabym w porządku. Powiem Aktywności 2,
Cię prosić, żebyś kazał Aktywności 2 żeby się brała do pracy.
Oo… komunikat.
wziąć się do roboty? Szczerze oddana, Chyba zacznę działać.
Twoja stara kumpela, Aktywność 1”.

Intencja Intencja

Do: Aktywność2 Do: Aktywność2

Aktywność1 Android Aktywność2

82 Rozdział 3.
Wiele aktywności i intencji

Użycie intencji do uruchomienia drugiej aktywności


Zastosujmy to rozwiązanie w praktyce i użyjmy intencji do uruchomienia
aktywności ReceiveMessageActivity. Ponieważ aktywność chcemy uruchomić
w odpowiedzi na kliknięcie przycisku Wyślij wiadomość, do kodu metody
onSendMessage() dodamy dwa wiersze komend.

Wprowadź zmiany wyróżnione w poniższym kodzie:

package com.hfad.komunikator;
Musimy zaimportować
klasę Intent, android.
import android.app.Activity; content.Intent,
gdyż będziemy jej
import android.content.Intent; używać w metodzie
import android.os.Bundle; onSendMessage().

import android.view.View;
Komunikator

public class CreateMessageActivity extends Activity { app/src/main

Kodu tej metody nie trzeba zmieniać.


@Override java
protected void onCreate(Bundle savedInstanceState) {
com.hfad.komunikator
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message);
CreateMessage
} Activity.java

// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku


public void onSendMessage(View view) {
Intent intent = new Intent(this, ReceiveMessageActivity.class);
startActivity(intent);
} Te dwa wiersze uruchamiają aktywność
ReceiveMessageActivity.
}

Co się wydarzy, kiedy uruchomimy aplikację?

jesteś tutaj  83
Co się dzieje?

¨  Utworzenie pierwszej aktywności
Co się dzieje po uruchomieniu aplikacji? ¨  Utworzenie drugiej aktywności
¨  Wywołanie drugiej aktywności
Zanim weźmiemy naszą nową aplikację na jazdę próbną, ¨  Przekazanie danych
jeszcze raz przeanalizujmy, jak ona będzie działać:

<Layout>

1 Po uruchomieniu aplikacji </Layout>

zaczyna działać aktywność


CreateMessageActivity. activity_create_message.xml
Po uruchomieniu tej aktywności
informuje ona system, że ma być
używany układ zapisany w pliku activity_
create_message.xml. Ten właśnie układ CreateMessageActivity.java
Urządzenie
zostaje wyświetlony w nowym oknie.

onSendMessage()
2 Użytkownik klika przycisk.
W odpowiedzi na kliknięcie zostaje
wywołana metoda onSendMessage()
aktywności CreateMessageActivity.
CreateMessageActivity
Urządzenie

onSendMessage()

Intencja
3 Metoda onSendMessage() prosi
system o uruchomienie aktywności CreateMessageActivity
ReceiveMessageActivity, Do: Receive
używając do tego intencji. Message
Android upewnia się, że intencja Activity
jest prawidłowa, po czym nakazuje
uruchomienie aktywności Intencja
ReceiveMessageActivity. Android

Do: ReceiveMessageActivity

ReceiveMessageActivity

84 Rozdział 3.
Wiele aktywności i intencji

Historii ciąg dalszy


4 Po uruchomieniu aktywności
ReceiveMessageActivity informuje
ona system, że używa układu CreateMessageActivity
z pliku activity_receive_message.xml,
zatem ten układ jest wyświetlany.

Android
Urządzenie

ReceiveMessageActivity
<Layout>

</Layout>

activity_receive_message

Jazda próbna aplikacji


¨  Utworzenie pierwszej aktywności
Zapisz wszystkie zmiany, a następnie uruchom aplikację. Początkowo ¨  Utworzenie drugiej aktywności
zostanie uruchomiona aktywność CreateMessageActivity, kiedy jednak ¨  Wywołanie drugiej aktywności
klikniesz przycisk Wyślij wiadomość, aplikacja uruchomi drugą aktywność ¨  Przekazanie danych
— ReceiveMessageActivity.

Wpisz komunikat
i kliknij
przycisk Wyślij
wiadomość.

Kiedy klikniesz przycisk Wyślij wiadomość,


zostanie uruchomiona aktywność
ReceiveMessageActivity, a używany przez
nią układ pojawi się na ekranie. Widoczny
na nim będzie napis „Hello world!”, gdyż
jest to domyślny tekst umieszczany
w układzie przez Android Studio.

jesteś tutaj  85
Przekazanie tekstu

Przekazanie tekstu do drugiej aktywności ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
Jak na razie skoncentrowaliśmy się na tym, by aktywność CreateMessageActivity
¨  Wywołanie drugiej aktywności
¨  Przekazanie danych
uruchamiała drugą aktywność, ReceiveMessageActivity, po kliknięciu
przycisku Wyślij wiadomość. Kolejnym krokiem będzie zapewnienie
<Layout> <Layout>
możliwości przekazywania tekstu z aktywności CreateMessageActivity
do ReceiveMessageActivity, tak by ta druga mogła go wyświetlić. </Layout>
1 </Layout>

W tym celu konieczne będzie wykonanie następujących czynności:


activity_create_ activity_receive_
1 Zmienić układ activity_receive_message.xml, tak by można w nim message.xml message.xml
było wyświetlać tekst. Aktualnie ten układ ma domyślną postać
wygenerowaną przez kreator. 2 Intencja 3
2 Zaktualizować kod w pliku CreateMessageActivity.java, tak by aktywność
pobierała tekst wpisany przez użytkownika w polu tekstowym.
Ten tekst należy następnie dodać do intencji przed jej przesłaniem. CreateMessage RecieveMessage
Activity.java Activity.java
3 Zaktualizować kod w pliku ReceiveMessageAction.java,
tak by przesłany w intencji tekst był wyświetlany na ekranie.

Zacznijmy od aktualizacji układu


Poniżej przedstawiliśmy kod układu activity_receive_message.xml wygenerowany
dla nas przez Android Studio:
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
Komunikator
android:layout_height=”match_parent”
android:paddingLeft=”16dp”
android:paddingRight=”16dp” app/src/main
android:paddingTop=”16dp”
android:paddingBottom=”16dp” res
tools:context=”com.hfad.komunikator.ReceiveMessageActivity”>
layout
<xml>
<TextView To jest komponent </xml>

android:text=”@string/hello_world” TextView aktualnie activity_receive_


wyświetlany w układzie. message.xml
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</RelativeLayout>

Trzeba wprowadzić kilka zmian w układzie. Musimy dodać do elementu <TextView> identyfikator
”message”, żebyśmy mogli odwoływać się do niego w kodzie aktywności; oprócz tego musimy zapobiec
wyświetlaniu tekstu „Hello world!”. Jak powinien wyglądać zmodyfikowany układ? Spróbuj go przygotować
Ćwiczenie samodzielnie, zanim spojrzysz na następną stronę.

86 Rozdział 3.
Wiele aktywności i intencji

Aktualizacja właściwości widoku tekstowego


Komunikator
W układzie drugiej aktywności trzeba zmienić kilka rzeczy.
app/src/main
W pierwszej kolejności musimy dodać identyfikator do elementu <TextView>. Takie
identyfikatory trzeba dodawać do wszystkich komponentów GUI, do których chcemy
się odwoływać w kodzie aplikacji, gdyż dzięki nim będzie można pobierać referencje res
do komponentów. Oprócz tego musimy zadbać o to, by nie był wyświetlany tekst
„Hello world!”. layout
<xml>
</xml>
Obie te sprawy możemy załatwić, wprowadzając zmiany pokazane w poniższym
activity_receive_
przykładzie:
message.xml
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:paddingBottom=”16dp”
tools:context=”com.hfad.komunikator.ReceiveMessageActivity”>

Ten wiersz dodaje do elementu <TextView>


<TextView identyfikator „message”.
android:id=”@+id/message”
Usuwamy wiersz wyświetlający w komponenci
android:text=”@string/hello_world” tekst z zasobu @string/hello_world. e

android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</RelativeLayout>
Nie istnieją
głupie pytania
Zamiast usuwać poniższy wiersz kodu:
P: Czy muszę używać intencji? Czy nie mogę 
android:text=”@string/hello_world” utworzyć instancji drugiej aktywności w kodzie 
pierwszej?
można także zmodyfikować plik strings.xml tak, by zasób
hello_world zawierał pusty łańcuch znaków. W tym O: To jest dobre pytanie, jednak nie to nie jest
przykładzie zdecydowaliśmy się tego nie robić, gdyż jedynym androidowy sposób uruchamiania aktywności. Jednym
tekstem, który kiedykolwiek będziemy chcieli wyświetlać z powodów jest to, że przekazując do systemu intencje,
w widoku tekstowym, jest komunikat przesłany z aktywności Android będzie wiedział, w jakiej kolejności były
CreateMessageActivity. wykonywane poszczególne aktywności. A to z kolei
oznacza, że w razie kliknięcia przycisku Wstecz na
Skoro poradziliśmy sobie z układami, możemy zająć się urządzeniu Android będzie dokładnie wiedział,
kodem aktywności. gdzie ma wrócić.

jesteś tutaj  87
Ekstra, ekstra

Metoda putExtra() zapisuje w intencji ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
dodatkowe informacje ¨  Wywołanie drugiej aktywności
¨  Przekazanie danych
Wiesz już, że nową intencję można utworzyć w następujący sposób:
Metoda putExtra()
Intent intent = new Intent(this, KlasaDocelowa.class); umożliwia
dodawanie
do wysyłanych
Jednak do takiej intencji można dodać dodatkowe informacje, które komunikatów
następnie będzie można odczytać w aktywności docelowej i odpowiednio dodatkowych Intencja
na nie zareagować. Do zapisywania takich dodatkowych informacji służy informacji.
metoda putExtra():
intent.putExtra(”message”, value); Do: ReceiveMessageActivity
message: “Witam!”
gdzie message jest łańcuchem znaków określającym nazwę przekazywanej
wartości, a value jest samą wartością. Metoda putExtra() jest przeciążona,
dzięki czemu można jej używać do przekazywania wartości wielu różnych typów.
Na przykład mogą to być wartości typów prostych, takich jak boolean lub int, Do intencji można dodawać
tablice typów prostych bądź łańcuchy znaków — String. Metodę putExtra() wartości wielu różnych typów.
można wywoływać wielokrotnie, aby zapisać w intencji więcej danych. W takich Wszystkie dostępne typy można
poznać, przeglądając dokumentację
sytuacjach należy jednak pamiętać, by każda wartość miała unikalną nazwę. Androida. Oprócz tego informacje
te będą także wyświetlane
przez Android Studio podczas
Jak pobrać dodatkowe informacje z intencji? wpisywania kodu.

To jednak jeszcze nie koniec historii. Kiedy system nakaże uruchomienie


aktywności ReceiveMessageActivity, musi ona dysponować jakąś
możliwością pobrania dodatkowych informacji, które aktywność
CreateMessageActivity przesłała do niej w intencji.

Istnieje kilka przydatnych metod, których można użyć do tego celu.


Pierwszą z nich jest:
Intencja
getIntent();

Metoda getIntent() zwraca intencję, która została użyta do uruchomienia


aktywności. Z kolei tej intencji można użyć do pobrania przekazanych w niej Do: ReceiveMessageActivity
informacji. Konkretny sposób pobierania informacji z intencji zależy od ich message: “Witam!”
typu. Na przykład jeśli wiemy, że intencja zawiera łańcuch znaków o nazwie
”message”, to możemy go pobrać, używając następującego fragmentu kodu:
Pobiera intencję.
Intent intent = getIntent();
Pobiera łańcuch znaków
String string = intent.getStringExtra(“message”); o nazwie „message”
przekazany w intencji.
Nasze możliwości nie ograniczają się jednak do pobierania wyłącznie
łańcuchów znaków. Na przykład poniższe wywołanie:

int intNum = intent.getIntExtra(”name”, default_value);


pozwala pobrać wartość typu int o nazwie name. Argument default_value
określa wartość domyślną.
88 Rozdział 3.
Wiele aktywności i intencji

package com.hfad.komunikator; Zagadkowy basen


Twoim zadaniem jest powyciąganie z basenu
import android.os.Bundle; fragmentów kodu i wstawienie ich w odpowiednie
import android.app.Activity; puste miejsca pliku CreateMessageActivity.java.
import android.content.Intent; Żadnego fragmentu kodu nie można użyć więcej
niż raz, lecz nie wszystkie fragmenty będą
import android.view.View;
potrzebne. Twoim celem jest skompletowanie
............................................ aktywności, która będzie pobierać tekst z widoku
tekstowego i zapisywać go w intencji.

public class CreateMessageActivity extends Activity {


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message);
}

// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku


public void onSendMessage(View view) {
.......................................................
.......................................................
Intent intent = new Intent(this, ReceiveMessageActivity.class);
.......................................................
startActivity(intent);
}
}

EditText
EditText
import putExtra

messageView putExtraString “message” =


String ;
findViewById (
getText() ( ; .
messageView =
R.id.message messageText ( .
) ;
messageText intent ,
android.widget.EditText )
) ;
toString() .

jesteś tutaj  89
Rozwiązanie zagadkowego basenu

package com.hfad.komunikator;
Zagadkowy basen. Rozwiązanie
import android.os.Bundle;
Twoim zadaniem jest powyciąganie
import android.app.Activity;
Musisz z basenu fragmentów kodu
import android.content.Intent; zaimportować i wstawienie ich w odpowiednie
import android.view.View; klasę EditText. puste miejsca pliku
import android.widget.EditText
............................................. CreateMessageActivity.java.
Żadnego fragmentu kodu
nie można użyć więcej niż
raz, lecz nie wszystkie fragmenty
public class CreateMessageActivity extends Activity { będą potrzebne. Twoim celem jest
@Override skompletowanie aktywności, która
będzie pobierać tekst z widoku
protected void onCreate(Bundle savedInstanceState) {
tekstowego i zapisywać go w intencji.
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message);
}

// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku


public void onSendMessage(View view) { Te dwa wiersze
EditText messageView = (EditText) findViewById(R.id.message); pobierają tekst
........................................................... z pola tekstowego
String messageText = messageView.getText().toString(); o identyfikatorze
........................................................... „message”.
Intent intent = new Intent(this, ReceiveMessageActivity.class);
intent.putExtra("message", messageText);
.......................................................
startActivity(intent);
Ten wiersz dodaje tekst do
} intencji, nadając mu nazwę
„message”.
}

Te fragmenty kodu
nie były potrzebne.

putExtraString

90 Rozdział 3.
Wiele aktywności i intencji

Aktualizacja kodu aktywności CreateMessageActivity ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
Zaktualizowaliśmy kod pliku CreateMessageActivity.java w taki sposób, że aktywność
¨  Wywołanie drugiej aktywności
¨  Przekazanie danych
pobiera tekst wpisany przez użytkownika w polu tekstowym i dodaje go do intencji.
Poniżej przedstawiliśmy kompletną wersję kodu (koniecznie wprowadź te same
modyfikacje, wyróżnione pogrubioną czcionką, w swoim pliku):

package com.hfad.komunikator;

Komunikator
import android.os.Bundle;
import android.app.Activity; app/src/main
import android.content.Intent;
Musisz zaimportować klasę EditText,
import android.view.View; android.widget.EditText, gdyż jest java
import android.widget.EditText; ona używana w kodzie aktywności.
com.hfad.komunikator

public class CreateMessageActivity extends Activity { CreateMessage


@Override Activity.java

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message);
}

Te wiersze pobierają
// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku tekst zapisany
w komponencie EditText.
public void onSendMessage(View view) {
EditText messageView = (EditText)findViewById(R.id.message);
String messageText = messageView.getText().toString();
Intent intent = new Intent(this, ReceiveMessageActivity.class);
intent.putExtra(ReceiveMessageActivity.EXTRA_MESSAGE, messageText);
startActivity(intent);
} Ten fragment kodu tworzy intencję,
następnie dodaje do niej tekst. Nazwę
} To wywołanie informacji zapisywanej w intencji
uruchamia aktywność określamy przy użyciu stałej, dzięki
ReceiveMessageActivity, czemu możemy mieć pewność, że w obu
używając w tym celu intencji. aktywnościach, CreateMessageActivity
i ReceiveMessageActivity, będzie używany
ten sam łańcuch znaków. Tę stałą dodamy
do klasy ReceiveMessageActivity na
następnej stronie.
Teraz, kiedy aktywność CreateMessageActivity zapisuje już
dodatkowe informacje w intencji, musimy zająć się ich pobraniem
i wyświetleniem.

jesteś tutaj  91
Metoda getStringExtra()

Zastosowanie informacji przekazanych w intencji ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
w klasie ReceiveMessageActivity ¨  Wywołanie drugiej aktywności
¨  Przekazanie danych
Skoro zmodyfikowaliśmy już kod aktywności CreateMessageActivity Intencja
i zaimplementowaliśmy zapisywanie tekstu w intencji, nadszedł czas
na wprowadzenie zmian w kodzie klasy ReceiveMessageActivity
i zastosowanie przekazanego tekstu.

Chcemy sprawić, by aktywność ReceiveMessageActivity, CreateMessage RecieveMessage


bezpośrednio po je utworzeniu, wyświetlała przekazany tekst w widoku Activity.java Activity.java
tekstowym. Ponieważ metoda onCreate()jest wywoływana zaraz po
utworzeniu aktywności, to właśnie do niej dodamy niezbędny kod. Musimy zadbać
o to, by aktywność
Aby pobrać tekst z intencji, w pierwszej kolejności musimy pobrać ReceiveMessageActivity
skorzystała z intencji,
intencję, używając metody getIntent(), a następnie pobrać sam która ją uruchomiła.
łańcuch znaków, używając metody getStringExtra().

Poniżej przedstawiliśmy pełny kod aktywności zapisany w pliku


ReceiveMessageActivity.java (zastąp nim początkowy kod wygenerowany
przez Android Studio, a następnie zapisz zmodyfikowany plik):
package com.hfad.komunikator;
Komunikator

import android.app.Activity;
app/src/main
import android.content.Intent;
Musimy
import android.os.Bundle; zaimportować klasy
Intent i TextView. java
import android.widget.TextView;
com.hfad.komunikator
public class ReceiveMessageActivity extends Activity {
public static final String EXTRA_MESSAGE = ”message”;
ReceiveMessage
Activity.java
@Override To nazwa wartości, którą będziemy przekazywali w intencji.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); Te dwa wiersze pobierają
intencję, a następnie używają
setContentView(R.layout.activity_receive_message); metody getStringExtra(),
by odczytać przekazany
Intent intent = getIntent(); w niej łańcuch znaków.
String messageText = intent.getStringExtra(EXTRA_MESSAGE);
TextView messageView = (TextView)findViewById(R.id.message);
messageView.setText(messageText);
}
} Ten wiersz wyświetla łańcuch znaków w widoku tekstowym.

Zanim weźmiemy aplikację na jazdę próbną, przeanalizujmy


dokładniej, jak działa ten kod.

92 Rozdział 3.
Wiele aktywności i intencji

Co się dzieje, gdy użytkownik kliknie przycisk Wyślij wiadomość?


1 Kiedy użytkownik klika przycisk, Intencja
zostaje wywołana metoda onSendMessage()
onSendMessage().
Kod metody onSendMessage() tworzy nową Do: ReceiveMessage
intencję, żądającą uruchomienia aktywności Activity
ReceiveMessageActivity, dodaje do niej CreateMessageActivity message:”Cześć!” Android
wiadomość wpisaną przez użytkownika, po
czym przekazuje intencję do systemu wraz
z prośbą o uruchomienie aktywności.

2 Android sprawdza, czy z intencją


jest wszystko w porządku,
a następnie uruchamia aktywność CreateMessageActivity
ReceiveMessageActivity.
Intencja

Do: ReceiveMessage Android


Activity
message:”Cześć!”
ReceiveMessageActivity

3 Po uruchomieniu aktywność
ReceiveMessageActivity określa,
że używa układu activity_receive_ CreateMessageActivity
message.xml, i to właśnie on jest
wyświetlany na ekranie urządzenia. Cześć!
Aktywność aktualizuje także układ,
wyświetlając w nim tekst przekazany
w intencji. Urządzenie

ReceiveMessageActivity

<Layout>

</Layout>

activity_receive_message

jesteś tutaj  93
Jazda próbna

Jazda próbna aplikacji ¨  Utworzenie pierwszej aktywności


¨  Utworzenie drugiej aktywności
Upewnij się, czy wprowadziłeś zmiany w obu aktywnościach, następnie zapisz ¨  Wywołanie drugiej aktywności
wszystkie zmienione pliki i uruchom aplikację. Początkowo zostanie uruchomiona ¨  Przekazanie danych
aktywność CreateMessageActivity, lecz po wpisaniu tekstu i kliknięciu przycisku
Wyślij wiadomość aplikacja uruchomi aktywność ReceiveMessageActivity.
Wpisany wcześniej tekst zostanie wyświetlony w widoku tekstowym.

Oto wpisany wcześniej tekst,


który pomyślnie udało się
przekazać do drugiej aktywności Obie aktywności zajmują
przy użyciu intencji. cały obszar ekranu
urządzenia, lecz tu
pominęliśmy część pustych
fragmentów ekranu.

Możemy zmienić aplikację tak, by wiadomości były wysyłane do innych osób


Teraz, kiedy nasza aplikacja wysyła już wiadomości do innej aktywności, możemy ją zmienić
w taki sposób, by wiadomości były wysyłane do innych osób. Możemy to zrobić, integrując ją
z innymi aplikacjami wysyłającymi widomości, już zainstalowanymi na naszym urządzeniu.
W zależności od zainstalowanych aplikacji możemy zapewnić możliwość wysyłania
wiadomości przy użyciu Messaging, GMaila, Google+, Facebooka, Twittera itd.

Hej, zaczekajcie no chwilkę!


Zapewnienie współpracy naszej aplikacji
z innymi wymaga pewnie strasznie dużych
nakładów pracy. A poza tym skąd możemy
wiedzieć, jakie aplikacje będą zainstalowane na
urządzeniach użytkowników? Skończcie, proszę,
z tymi fantazjami.

Okazuje się, że ze względu na sposób, w jaki zaprojektowano 
Androida, nie jest to aż tak trudne, jak się wydaje.
Czy pamiętasz, jak na początku rozdziału napisaliśmy, że zadania to
sekwencje połączonych aktywności? No więc okazuje się, że nasze
możliwości nie ograniczają się do stosowania aktywności należących do
naszej aplikacji. Można wykraczać poza granice własnej i używać aktywności
należących do innych aplikacji.
94 Rozdział 3.
Wiele aktywności i intencji

Jak działają aplikacje na Androida?


Jak już się przekonaliśmy, wszystkie aplikacje na Androida składają się
z jednej lub kilku aktywności oraz z komponentów dodatkowych, takich jak
układy. Każda aktywność reprezentuje jedną, dobrze zdefiniowaną operację,
którą może wykonywać użytkownik. Na przykład takie aplikacje jak Gmail,
Google+, Messaging, Facebook oraz Twitter udostępniają aktywności
umożliwiające wysyłanie wiadomości, choć każda z nich może to robić
w zupełnie odmienny sposób.
Urządzenie

Gmail Google+ SMS/MMS


Każda aplikacja składa się
z grupy aktywności. Oprócz
tego aplikacje zawierają
także inne komponenty, lecz
na razie koncentrujemy się
wyłącznie na aktywnościach.

Intencje mogą uruchamiać aktywności w innych aplikacjach


Wiesz już, w jaki sposób można używać intencji do uruchomienia drugiej
aktywności w tej samej aplikacji. Pierwsza aktywność przekazuje systemowi
intencję, a Android nakazuje uruchomienie drugiej aktywności.

Dokładnie to samo dotyczy uruchamiania aktywności należących do innych


aplikacji. Aktywność należąca do naszej aplikacji przekazuje systemowi
intencję, ten ją sprawdza, a następnie nakazuje uruchomienie drugiej
aktywności, nawet jeśli będzie ona należeć do innej aplikacji. Na przykład
możemy użyć intencji w celu uruchomienia aktywności służącej do wysyłania
wiadomości należącej do aplikacji Gmail i przekazać jej tekst, który chcemy Możemy stworzyć intencję, która
wysłać. Zamiast pisać własne aktywności do wysyłania poczty elektronicznej, uruchomi wybraną aktywność,
nawet jeśli ta aktywność będzie
możemy skorzystać z już istniejącej aplikacji Gmail. należała do innej aplikacji.

Komunikator Intencja Gmail


Intencja
To jest
aplikacja,
którą
piszemy
w tym
rozdziale.
Android

Oznacza to, że tworząc sekwencję aktywności dostępnych na urządzeniu,


możemy tworzyć aplikacje o znacznie większych możliwościach.

jesteś tutaj  95
Stosowanie akcji

Ale przecież nie wiemy, jakie aplikacje są dostępne na urządzeniu


Zanim będziemy mogli korzystać z aplikacji należących do innych aplikacji, musimy
znaleźć odpowiedzi na trzy pytania:

 Skąd mamy wiedzieć, jakie aktywności są dostępne na urządzeniu użytkownika?


 Skąd mamy wiedzieć, które z tych aktywności nadają się do wykonania interesującej nas operacji?
 Skąd mamy wiedzieć, jak używać tych aktywności?
Bardzo dobrą wiadomością jest to, że wszystkie te problemy możemy rozwiązać, używając akcji
(ang. actions). Akcje to sposób pozwalający na poinformowanie systemu Android o tym, jakie
standardowe operacje może wykonywać dana aktywność. Na przykład Android wie, że wszystkie
aktywności zarejestrowane do wykonywania akcji SEND (wysyłania) potrafią wysyłać wiadomości.

Kolejną rzeczą, której się nauczymy, będzie tworzenie intencji zwracających zbiór aktywności,
których można używać w standardowy sposób — na przykład do wysyłania wiadomości.

Oto, co mamy zamiar zrobić


1 Utworzyć intencję określającą akcję.
Intencja ta przekaże Androidowi informację, że chcemy użyć aktywności, która potrafi wysyłać
wiadomości. Intencja ta będzie także zawierała tekst tej wiadomości.

2 Zapewnić użytkownikowi możliwość wyboru, której aplikacji chce użyć.


Istnieje spora szansa, że na urządzeniu będzie dostępnych więcej aplikacji pozwalających na wysyłanie
wiadomości, dlatego użytkownik będzie musiał wybrać jedną z nich. Chcemy, aby użytkownik mógł
wybrać używaną aplikację po każdym kliknięciu przycisku Wyślij wiadomość.

96 Rozdział 3.
Wiele aktywności i intencji

Utworzenie intencji określającej akcję ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
Dotychczas dowiedziałeś się, w jaki sposób można tworzyć intencje uruchamiające
konkretną aktywność:
Przekazaliśmy intencji informację
Intent intent = new Intent(this, ReceiveMessageActivity.class); o tym, która klasa nas interesuje.
Ale co zrobić w przypadku, gdy nie
Są to tak zwane intencje jawne (ang. explicit intent), gdyż jawnie informują będziemy znali tej klasy?
system, którą klasę ma uruchomić.
Jeśli jednak chcemy wykonać określoną czynność, lecz nie interesuje nas, która
aktywność to zrobi, to możemy utworzyć intencję niejawną (ang. implicit intent).
W takim przypadku określamy akcję, którą chcemy wykonać, pozostawiając
Androidowi możliwość doboru odpowiednich aktywności.

Sposób tworzenia intencji Informacje o wszelkich


Intencję określającą akcję do wykonania można utworzyć w następujący sposób: dostępnych akcjach
Intent intent = new Intent(action); oraz dodatkowych
gdzie action określa typ akcji, którą aktywność ma wykonać. Android udostępnia informacjach, które
obszerną grupę standardowych akcji. Na przykład akcja Intent.ACTION_DIAL
pozwala wybrać numer telefonu, akcja Intent.ACTION_WEB_SEARCH umożliwia można do nich
wyszukanie frazy w internecie, a za pomocą akcji Intent.ACTION_SEND można przekazywać, znajdziesz
wysłać wiadomość. A zatem gdybyśmy chcieli utworzyć intencję określającą, że
zależy nam na wysłaniu wiadomości, moglibyśmy to zrobić w następujący sposób: w dokumentacji
Intent intent = new Intent(Intent.ACTION_SEND);
Androida na stronie
http://tinyurl.com/
Dodawanie informacji n57qb5.
Po określeniu akcji, której chcemy użyć, możemy dodać do intencji dodatkowe
informacje. W naszym przypadku chcemy dodać tekst, który stanie się treścią
wysyłanej wiadomości. Do tego celu musimy użyć dwóch poniższych wierszy kodu:

intent.setType(”text/plain”); Wszystkie te atrybuty są powiązane


intent.putExtra(Intent.EXTRA_TEXT, messageText); z akcją Intent.ACTION_SEND.
Nie będą one miały zastosowania
wszystkich dostępnych akcjach.
gdzie messageText jest tekstem, który chcemy wysłać. Powyższy fragment kodu
informuje system, że poszukujemy aktywności potrafiącej obsługiwać dane typu
MIME ”text/plain”, a dodatkowo określa sam wysyłany tekst.

Gdyby istniały jeszcze jakieś inne informacje, które chcielibyśmy dodać do intencji,
to moglibyśmy to zrobić, używając kolejnych wywołań metody putExtra().
Na przykład poniższy kod pozwala określić temat wysyłanej wiadomości: Jeśli dana aplikacja nie
umożliwia określania tematu
intent.putExtra(Intent.EXTRA_SUBJECT, subject); wiadomości, to zostanie on
zignorowany. Jednak wszystkie
gdzie subject jest tekstem określającym temat wiadomości. aplikacje, które wiedzą, jak
używać tematu, zastosują go.

jesteś tutaj  97
Zastosowanie akcji

Zmiana intencji w celu użycia akcji ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
Zmienimy teraz kod w pliku CreateMessageActivity.java i utworzymy w nim intencję
niejawną z akcją do wysyłania wiadomości. Wprowadź zatem zmiany przedstawione
w poniższym kodzie i zapisz plik:

package com.hfad.komunikator;

Komunikator
import android.os.Bundle;
import android.app.Activity; app/src/main

import android.content.Intent;
java
import android.view.View;
import android.widget.EditText; com.hfad.komunikator

public class CreateMessageActivity extends Activity { CreateMessage


Activity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_message);
}

// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku


public void onSendMessage(View view) {
EditText messageView = (EditText)findViewById(R.id.message);
String messageText = messageView.getText().toString();
Usuń te dwa
wiersze kodu. Intent intent = new Intent(this, ReceiveMessageActivity.class);
intent.putExtra(ReceiveMessageActivity.EXTRA_MESSAGE, messageText);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(“text/plain”);
intent.putExtra(Intent.EXTRA_TEXT, messageText);
startActivity(intent);
Zamiast tworzyć intencję, która
} jawnie żąda uruchomienia aktywności
ReceiveMessageActivity, tworzymy intencję,
} używając akcji wysyłania wiadomości.

Przeanalizujmy teraz krok po kroku, co się dzieje, kiedy użytkownik


kliknie przycisk Wyślij wiadomość.

98 Rozdział 3.
Wiele aktywności i intencji

Co się dzieje podczas działania kodu?

1 Po wywołaniu metody onSendMessage()


zostaje utworzona intencja. Metoda onSendMessage() Intencja
startActivity() przekazuje tę intencję
do systemu Android.
Intencja zawiera określenie akcji ACTION_SEND ACTION_SEND
type: “text/plain”
i określenie typu MIME text/plain. CreateMessageActivity messageText:”Cześć!” Android

A… Intencja niejawna.
Muszę zatem znaleźć wszystkie
2 Android dowiaduje się, że intencję może
aktywności, które są w stanie
przekazać wyłącznie do aktywności, które
obsługiwać akcję ACTION_SEND,
potrafią wykonywać akcję ACTION_SEND
i operują na danych typu text/plain.
operują na danych typu text/plain
Sprawdza zatem wszystkie aktywności,
i należą do kategorii DEFAULT.
wyszukując spośród nich te, do których
może wysłać intencję.
Jeśli system nie znajdzie aktywności, które mogą
obsłużyć intencję, zostanie zgłoszony wyjątek
ActivityNotFoundException.

CreateMessageActivity Android

CreateMessageActivity
3 Jeśli tylko jedna aktywność jest
w stanie obsługiwać przekazaną intencję, Intencja
to Android uruchomi ją i przekaże
do niej intencję.
Do: Aktywność Android
messageText:”Cześć!”

Aktywność

jesteś tutaj  99
Co się dzieje?

Historii ciąg dalszy ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru

Hej, użytkowniku!
Każda z tych aktywności
4 Jeśli więcej niż jedna może wysyłać wiadomości.
aktywność może odebrać Której z nich chcesz
intencję, Android użyć?
wyświetli okno dialogowe
pozwalające wybrać
jedną z nich i poprosi CreateMessageActivity
użytkownika o dokonanie
wyboru.

Android
Użytkownik

5 Kiedy użytkownik
wybierze aktywność,
której chce użyć,
Android uruchamia ją CreateMessageActivity
i przekazuje do niej
intencję. Intencja
Aktywność wyświetli
Android
dodatkowy tekst
przekazany w intencji Do: Wybrana aktywność
Użytkownik
jako treść wysyłanej messageText: „Cześć!”
wiadomości. Wybrana
aktywność

Aby wyświetlić okno dialogowe do wyboru aktywności, Android


musi wiedzieć, które z nich są w stanie odebrać i obsłużyć intencję.
Na kilku następnych stronach dowiesz się, jak to zrobić.

100 Rozdział 3.
Wiele aktywności i intencji

Filtry intencji informują system, które aktywności


mogą obsługiwać poszczególne akcje
Kiedy do systemu Android zostanie przekazana intencja, musi on określić, która
aktywność (bądź które aktywności) jest w stanie ją obsłużyć. Ten proces jest
nazywany wyznaczaniem intencji (ang. intent resolution).

W razie zastosowania intencji jawnej wyznaczanie intencji jest banalnie proste.


W takim przypadku intencja bowiem jawnie określa, do którego komponentu jest
skierowana, zatem Android dysponuje jednoznacznymi instrukcjami dotyczącymi
tego, co należy zrobić. Na przykład poniższy kod jawnie informuje system, że należy
uruchomić aktywność ReceiveMessageActivity:

Intent intent = new Intent(this, ReceiveMessageActivity.class);


startActivity(intent);

W przypadku zastosowania intencji niejawnej Android określa komponenty, które


mogą ją obsłużyć, na podstawie informacji zawartych w tej intencji. W tym celu
system sprawdza filtry intencji umieszczone w plikach AndroidManifest.xml wszystkich
zainstalowanych aplikacji.

Filtr intencji (ang. intent filter) określa typy intencji, które mogą być obsługiwane
przez dany komponent. Na przykład poniższy fragment kodu dotyczy aktywności,
która potrafi obsługiwać akcje ACTION_SEND. Co więcej, aktywność ta obsługuje dane
typu MIME text/plain lub obrazy:
Ten wiersz informuje Androida,
że aktywność jest w stanie
<activity android:name=”ShareActivity”> obsługiwać akcję ACTION_SEND.

<intent-filter>
<action android:name=”android.intent.action.SEND”/> Ten filtr intencji musi
określać kategorię
<category android:name=”android.intent.category.DEFAULT”/> o wartości DEFAULT,
<data android:mimeType=”text/plain”/> gdyż w przeciwnym
razie nie będzie mógł
<data android:mimeType=”image/*”/> To są typy danych, obsługiwać intencji
które aktywność niejawnych.
</intent-filter>
potrafi obsługiwać.
</activity>

Filtry intencji określają także kategorię. Kategoria określa dodatkowe informacje


dotyczące aktywności, na przykład czy dana aktywność może być uruchamiana przez
przeglądarkę WWW bądź czy jest ona głównym punktem wejścia aplikacji. Filtr
intencji musi określać kategorię o wartości android.intent.category.DEFAULT,
jeśli ma zapewniać możliwość obsługi intencji niejawnych. Jeżeli aktywność nie
ma żadnego filtra intencji albo nie określa kategorii o wartości android.intent.
category.DEFAULT, to oznacza to, że nie będzie jej można uruchamiać przy użyciu
intencji niejawnych. Taką aktywność będzie można uruchomić wyłącznie za pomocą
intencji jawnej, określającej pełną nazwę komponentu.

jesteś tutaj  101


Filtry intencji

Jak Android korzysta z filtrów intencji? ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
W przypadku stosowania intencji niejawnych Android porównuje informacje
umieszczone w intencji z informacjami z filtrów intencji zapisanych w pliku
AndroidManifest.xml aplikacji.

W pierwszej kolejności Android bierze pod uwagę filtry określające kategorię typu
android.intent.category.DEFAULT:

<intent-filter>
<category android:name=”android.intent.category.DEFAULT”/>
...
</intent-filter>

Intencje, które nie określają tej kategorii, zostaną pominięte, gdyż nie mogą one obsługiwać
intencji niejawnych.
Oprócz tego, jeśli w intencji
Następnie Android porównuje intencje z filtrami intencji, uwzględniając przy tym została określona kategoria,
podaną w nich akcję i obsługiwany typ MIME. Na przykład jeśli intencja określa akcję to Android sprawdzi także
kategorię podaną w filtrze
Intent.ACTION_SEND, gdyż została utworzona w poniższy sposób: intencji. To rozwiązanie jest
jednak stosowane raczej
Intent intent = new Intent(Intent.ACTION_SEND); sporadycznie, dlatego nie
przedstawimy go tutaj.
to Android uwzględni wyłącznie aktywności, których filtry intencji określają akcję
android.intent.action.SEND, używając następującego kodu:

<intent-filter>
<action android:name=”android.intent.action.SEND”/>
...
</intent-filter>

I podobnie jeśli w intencji zostanie określony typ MIME o wartości ”text/plain”:

intent.setType(”text/plain”);

to Android uwzględni tylko te aktywności, które potrafią obsługiwać konkretny typ danych:

<intent-filter>
<data android:mimeType=”text/plain”/>
...
</intent-filter>

Jeśli w intencji nie został określony typ MIME, to system postara się go wywnioskować
na podstawie danych zapisanych w intencji.

Kiedy Android zakończy porównywanie intencji z filtrami intencji komponentów, będzie wiedział,
ile z nich udało mu się dopasować. Jeżeli uda mu się odnaleźć tylko jedno dopasowanie, to uruchomi
odpowiedni komponent (w naszym przypadku będzie to aktywność) i przekaże do niego intencję. Jeżeli
natomiast takich dopasowań będzie więcej, to Android poprosi użytkownika o wybranie jednego z nich.

102 Rozdział 3.
Wiele aktywności i intencji

Wczuj się w intencję! To jest intencja.


Twoim zadaniem jest wcielenie się
w rolę intencji przedstawionej Intent intent = new Intent(Intent.ACTION_SEND);
z prawej strony i wskazanie, które intent.setType(”text/plain”);
z zaprezentowanych intent.putExtra(Intent.EXTRA_TEXT, ”Witaj”);
poniżej aktywności
są zgodne z Twoją
aktywnością i typem
danych. Dla każdej
z aktywności podaj,
dlaczego jest lub dlaczego nie jest
zgodna z intencją.

<activity android:name=”SendActivity”>
<intent-filter>
<action android:name=”android.intent.action.SEND”/>
<category android:name=”android.intent.category.DEFAULT”/>
<data android:mimeType=”*/*”/>
</intent-filter>
</activity>

<activity android:name=”SendActivity”>
<intent-filter>
<action android:name=”android.intent.action.SEND”/>
<category android:name=”android.intent.category.MAIN”/>
<data android:mimeType=”text/plain”/>
</intent-filter>
</activity>

<activity android:name=”SendActivity”>
<intent-filter>
<action android:name=”android.intent.action.SENDTO”/>
<category android:name=”android.intent.category.MAIN”/>
<category android:name=”android.intent.category.DEFAULT”/>
<data android:mimeType=”text/plain”/>
</intent-filter>
</activity>

jesteś tutaj  103


Rozwiązanie

Wczuj się w intencję. Rozwiązanie


Twoim zadaniem jest wcielenie się
Intent intent = new Intent(Intent.ACTION_SEND);
w rolę intencji przedstawionej z prawej
strony i wskazanie, które intent.setType(”text/plain”);
z zaprezentowanych poniżej intent.putExtra(Intent.EXTRA_TEXT, ”Witaj”);
aktywności są zgodne z Twoją
aktywnością i typem danych.
Dla każdej z aktywności podaj,
dlaczego jest lub dlaczego nie
jest zgodna z intencją.
Ta aktywność akceptuje akcję ACTION_SEND
i potrafi obsługiwać dane dowolnego typu MIME,
<activity android:name=”SendActivity”> dlatego może odpowiedzieć na intencję.
<intent-filter>
<action android:name=”android.intent.action.SEND”/>
<category android:name=”android.intent.category.DEFAULT”/>
<data android:mimeType=”*/*”/>
</intent-filter>
</activity>

Ta aktywność nie zawiera kategorii


<activity android:name=”SendActivity”> typu DEFAULT, więc nie może odbierać
<intent-filter> aktywności niejawnych.
<action android:name=”android.intent.action.SEND”/>
<category android:name=”android.intent.category.MAIN”/>
<data android:mimeType=”text/plain”/>
</intent-filter>
</activity>

Ta aktywność nie obsługuje intencji z akcją ACTION_SEND,


<activity android:name=”SendActivity”> lecz z akcją ACTION_SENDTO. Ta akcja pozwala wysłać
wiadomość do odbiorcy określonego w danych intencji.
<intent-filter>
<action android:name=”android.intent.action.SENDTO”/>
<category android:name=”android.intent.category.MAIN”/>
<category android:name=”android.intent.category.DEFAULT”/>
<data android:mimeType=”text/plain”/>
</intent-filter>
</activity>

104 Rozdział 3.
Wiele aktywności i intencji

Musisz uruchomić aplikację ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
na PRAWDZIWYM urządzeniu
Dotychczas wszystkie nasze aplikacje uruchamialiśmy na emulatorze. Emulator
udostępnia niewielką liczbę aplikacji i jest całkiem możliwe, że wśród nich będzie
tylko jedna potrafiąca obsługiwać akcję ACTION_SEND. Aby prawidłowo przetestować
aplikację, będziemy musieli uruchomić ją na rzeczywistym, fizycznym urządzeniu,
mając przy tym pewność, że jest na nim zainstalowanych więcej aplikacji, które
potrafią obsługiwać naszą akcję — na przykład aplikacja umożliwiająca wysyłanie
e-maili i aplikacja do przesyłania komunikatów.

A oto, w jaki sposób możesz uruchomić tworzoną aplikację na fizycznym urządzeniu:

1. Włącz debugowanie USB


Na swoim urządzeniu otwórz opcję Opcje programistyczne
(począwszy od Androida 4.0 opcja ta jest domyślnie niedostępna).
Aby ją włączyć, wybierz opcję Ustawienia/Informacje o urządzeniu,
a następnie siedmiokrotnie kliknij pozycję Numer wersji. Kiedy
Właśnie tak,
serio! wrócisz do poprzedniego ekranu, powinna już być na nim
widoczna opcja Opcje programisty.

Na ekranie Opcje programistyczne zaznacz pole wyboru


Debugowanie USB.
Musisz włączyć opcję Debugowanie USB.

2. Skonfiguruj system, by wykrył urządzenie


Jeśli używasz komputera Mac, możesz pominąć te czynności.

Jeśli używasz komputera z systemem Windows, to musisz zainstalować


sterownik USB. Jego najnowszą wersję można pobrać ze strony:

http://developer.android.com/tools/extras/oem-usb.html

Jeśli używasz systemu Ubuntu, musisz utworzyć plik reguł udev.


Aktualne informacje o tym, jak to zrobić, można znaleźć na stronie:

http://developer.android.com/tools/device.html#setting-up

3. Podłącz urządzenie do komputera kablem USB


Twoje urządzenie może zapytać, czy chcesz zaakceptować klucz RSA
umożliwiający przeprowadzanie debugowania USB na danym komputerze. Ten komunikat zostanie wyświetlony,
Jeśli faktycznie tak się stanie, zaznacz opcję Zawsze zezwalaj temu jeśli używasz urządzenia z systemem
komputerowi, a następnie kliknij przycisk OK, by podłączyć urządzenie. Android 4.2.2 lub nowszym.

jesteś tutaj  105


Uruchamianie na prawdziwym urządzeniu

Uruchamianie aplikacji na prawdziwym urządzeniu ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
(ciąg dalszy)
4. W normalny sposób uruchom aplikację w Android Studio
Android Studio zainstaluje aplikację na urządzeniu i ją uruchomi. Być może zostaniesz
zapytany, na którym urządzeniu chcesz uruchomić aplikację. W takim przypadku wybierz
urządzenie z listy dostępnych urządzeń i kliknij przycisk OK.

Pierwszym z wyświetlonych
urządzeń jest emulator.

To jest nasze
fizyczne urządzenie.

A oto aplikacja uruchomiona


na fizycznym urządzeniu
Zauważysz zapewne, że aplikacja wygląda niemal
identycznie jak w przypadku uruchamiania jej na
emulatorze. Oprócz tego pewnie przekonasz się
także, że na fizycznym urządzeniu aplikacja działa
znacznie szybciej niż na emulatorze.

Skoro już wiesz, jak możesz uruchamiać własne


aplikacje na własnym, fizycznym urządzeniu,
możesz już przetestować aplikację Komunikatora.

106 Rozdział 3.
Wiele aktywności i intencji

Jazda próbna aplikacji


Spróbuj uruchomić aplikację w emulatorze, a następnie na własnym urządzeniu.
Uzyskane wyniki będą zależały od tego, jak dużo będzie dostępnych aktywności
obsługujących akcję wysyłania danych tekstowych.

Jeśli będzie jedna aktywność


W takim przypadku kliknięcie przycisku Wyślij
wiadomość spowoduje przejście bezpośrednio
do wybranej aplikacji.

Na emulatorze mamy dostępną


tylko jedną aktywność, która jest
w stanie wysyłać wiadomości
z danymi tekstowymi, dlatego kiedy
klikniemy przycisk Wyślij wiadomość,
Android automatycznie uruchomi tę
aktywność.
To jest wiadomość.
Jeśli będzie więcej niż jedna aktywność
W tym przypadku Android wyświetli okno dialogowe z listą wszystkich odszukanych
aktywności i poprosi o wskazanie tej, której chcemy użyć. Oprócz tego zostaniemy
zapytani, czy chcemy, by wybrana akacja została wykonana raz, czy też ma już być
wykonywana zawsze. W razie wybrania drugiej możliwości każde kolejne kliknięcie Na naszym fizycznym urządzeniu
dostępnych jest wiele aktywności
przycisku sprawi, że domyślnie zostanie wykonana ta sama akcja. spełniających nasze potrzeby.
Zdecydowaliśmy się wybrać aplikację
SMS/MMS (ang. Messaging). Oprócz
tego wybraliśmy opcję Zawsze — świetne
rozwiązanie, jeśli chcemy zawsze używać
tej samej aplikacji SMS/MMS, lecz już
nie tak dobre, jeśli za każdym razem
chcielibyśmy używać innej aplikacji.

jesteś tutaj  107


Niech wybierze użytkownik

A co, jeśli chcemy, by użytkownik ZAWSZE wybierał aktywność?


Przekonałeś się właśnie, że jeśli na urządzeniu jest więcej aktywności, Metoda createChooser()
które mogą odebrać i obsłużyć intencję, to Android automatycznie prosi
o wybór tej, której chcesz użyć. Co więcej, pyta nawet, czy ta aktywność umożliwia podanie tytułu
ma zostać użyta tylko jeden raz, czy też ma być wykonywana już zawsze.
okna dialogowego wyboru
Z tym domyślnym sposobem działania systemu wiąże się tylko jeden aktywności i jednocześnie
problem: co zrobić w przypadku, gdy chcemy zagwarantować, aby
użytkownik mógł wybrać aktywność po każdym kliknięciu przycisku ukrywa w nim opcje
Wyślij wiadomość? Jeśli użytkownik zdecyduje na przykład, że zawsze
pozwalające użytkownikowi
chce wysyłać wiadomości za pomocą aplikacji Gmail, to w przyszłości
nie zostanie już zapytany, czy chce użyć Twittera. określać, czy dana
Na szczęście ten problem można łatwo rozwiązać. Okazuje się, że można aktywność ma być używana
utworzyć okno dialogowe wyboru aktywności, które nie będzie pytało
użytkownika, czy wybrana aktywność ma zostać użyta tylko raz, czy zawsze.
domyślnie. Jednocześnie,
jeśli nie uda się znaleźć
Metoda Intent.createChooser() wyświetla żadnej pasującej aktywności,
okno dialogowe wyboru aktywności to użytkownik zostanie
Możliwość wyświetlenia okna dialogowego wyboru aktywności zapewnia
metoda Intent.createChooser(). Metoda ta pobiera utworzoną o tym poinformowany
intencję i przekazuje ją do okna dialogowego wyboru aktywności. odpowiednim komunikatem.
Podstawowa różnica pomiędzy stosowaniem tej metody a standardowym
sposobem uruchamiania aktywności polega na tym, że wyświetlone
okno dialogowe nie daje możliwości określania aktywności domyślnej
— użytkownik będzie proszony o wybór aktywności za każdym razem.
To jest utworzona wcześniej intencja.
Oto, jak wygląda wywołanie metody createChooser():

Intent chosenIntent = Intent.createChooser(intent, ”Wysyłanie wiadomości...”);

Metoda createChooser() pobiera dwa parametry: intencję i opcjonalny zać


łańcuch znaków określający tytuł wyświetlanego okna dialogowego. Możemy także przeka
tyt uł, któ ry zos tan ie
Możemy do niej przekazać utworzoną wcześniej intencję, tę, która wyświetlony na samej
korzysta z akcji ACTION_SEND i używa danych tekstowych. górze ekranu.

Metoda createChooser() zwraca nowiutki obiekt Intent. Ta nowa intencja


jawna zostanie skierowana bezpośrednio do aktywności wybranej przez
użytkownika. Będzie ona także zawierać wszelkie informacje dodatkowe
przekazane w początkowej intencji, w tym także wszystkie łańcuchy znaków.

Aby uruchomić wybraną aktywność, wystarczy użyć poniższego wywołania:

startActivity(chosenIntent);
Na kilku kolejnych stronach nieco dokładniej przyjrzymy się temu, co się dzieje
w momencie wywołania metody createChooser().

108 Rozdział 3.
Wiele aktywności i intencji

Co się dzieje w momencie wywołania ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
metody createChooser()?
Oto, co się stanie w momencie wykonania dwóch poniższych wierszy kodu:

Intent chosenIntent = Intent.createChooser(intent, ”Wysyłanie wiadomości...”);


startActivity(chosenIntent);
createChooser()
1 Zostaje wywołana metoda Intencja
createChooser().
W jej wywołaniu przekazywana jest intencja
określająca akcję, którą chcemy wykonać, ACTION_SEND
oraz informacje na temat typu danych type: “text/plain”
CreateMessageActivity message:”Cześć!” Android
MIME.

Widzę, że muszę
2 Android sprawdza, które aktywności
utworzyć okno dialogowe
mogą obsłużyć tę intencję, wyboru aktywności, które
sprawdzając ich filtry intencji. obsługują akcję SEND i operują
na danych typu text/plain.
Porównywane są akcja, typ danych
oraz kategoria, obsługiwane przez
poszczególne aktywności.

CreateMessageActivity Android

3 Jeśli aktywność może zostać obsłużona


przez więcej niż jedną aktywność, Hej, użytkowniku! Powiedz mi,
to Android wyświetla okno dialogowe
proszę, której aktywności
i prosi użytkownika o wybór jednej
chciałbyś użyć tym razem?
spośród odnalezionych aktywności.
Tym razem użytkownik nie będzie miał CreateMessageActivity
możliwości określenia, że wybrana
aktywność ma być używana za każdym
razem — w oknie dialogowym zostanie tylko
wyświetlony tytuł „Wysyłanie wiadomości…”.

Jeśli nie uda się znaleźć żadnych pasujących


aktywności, to Android i tak wyświetli okno
Android
dialogowe, a dodatkowo także komunikat Użytkownik
informujący użytkownika, że nie ma aplikacji,
które mogłyby obsłużyć wybraną akcję.
jesteś tutaj  109
Co się dzieje?

Historii ciąg dalszy ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru

4 Kiedy użytkownik wybierze, Ona chce użyć aktywności


której aktywności chce użyć, Aktywność2. Proszę bardzo,
Android zwraca nową intencję to odpowiednia intencja.
jawną skierowaną do wybranej
aktywności.
Ta nowa intencja zawiera wszelkie
informacje dodatkowe, które Intencja
były zapisane w początkowej
aktywności, na przykład wszystkie
teksty. WybranaAktywność
CreateMessageActivity message:”Cześć!”
Android
Użytkownik

Dzięki za intencję,
Androidzie. A czy możesz
teraz uruchomić tę
5 Aktywność prosi
intencję?
system o uruchomienie
aktywności określonej
Intencja
w intencji.

Do: WybranaAktywność
message:”Cześć!”
CreateMessageActivity Android

CreateMessageActivity
6 Android uruchamia
aktywność określoną
w intencji i przekazuje
do niej tę intencję.
Intencja

Do: WybranaAktywność Android


message:”Cześć!”
WybranaAktywność

110 Rozdział 3.
Wiele aktywności i intencji

Zmień kod, aby wyświetlać okno dialogowe


Zmienimy teraz kod naszej aktywności w taki sposób, by po każdym kliknięciu
przycisku Wyślij wiadomość użytkownik był proszony o wybór aktywności,
której chce użyć do wysłania wiadomości. W tym celu zmienimy kod metody
onSendMessage() w pliku CreateMessageActivity.java, dodając do niego
wywołanie metody createChooser(), a oprócz tego do pliku strings.xml
dodamy nowy zasób łańcuchowy określający tytuł okna dialogowego.

Komunikator
Zaktualizuj plik strings.xml…
Chcemy, by okno dialogowe do wyboru aktywności miało tytuł Wysyłanie app/src/main
wiadomości…, a zatem do pliku strings.xml dodaj zasób łańcuchowy o nazwie
chooser i wartości Wysyłanie wiadomości... (nie zapomnij także res
o zapisaniu zmienionego pliku).
... values
<string name=”chooser”>Wysyłanie wiadomości...</string> <xml>
</xml>
... strings.xml

…a następnie kod metody onSendMessage()


Musimy zmienić kod metody onSendMessage() w taki sposób, by pobierała
Komunikator
z pliku strings.xml zasób określający tytuł okna dialogowego, wywoływała
metodę createChooser(), a następnie uruchamiała wybraną aktywność.
A zatem zmodyfikuj kod aktywności w następujący sposób: app/src/main

...
java
// Metoda onSendMessage() jest wywoływana po kliknięciu przycisku
public void onSendMessage(View view) {
com.hfad.komunikator
EditText messageView = (EditText)findViewById(R.id.message);
String messageText = messageView.getText().toString();
Intent intent = new Intent(Intent.ACTION_SEND); CreateMessage
intent.setType(”text/plain”); Activity.java
intent.putExtra(Intent.EXTRA_TEXT, messageText); Pobranie tytułu okna.
String chooserTitle = getString(R.string.chooser);
Intent chosenIntent = Intent.createChooser(intent, chooserTitle);
startActivity(intent);
startActivity(chosenIntent); Uruchomienie aktywności Wyświetlenie okna dialogowego
} wybranej przez użytkownika. wyboru aktywności.
...
Metoda getString() służy do pobierania wartości zasobów łańcuchowych.
Ma ona jeden parametr określający identyfikator zasobu (w naszym przypadku
jest to R.string.chooser):
Jeśli zajrzysz do pliku R.java, znajdziesz
getString(R.string.chooser); chooser w klasie wewnętrznej o nazwie string.

Teraz, kiedy już zaktualizowaliśmy aplikację, zobaczmy, jak w działaniu wygląda nasze
nowe okno dialogowe wyboru aktywności.
jesteś tutaj  111
Jazda próbna

Jazda próbna aplikacji ¨  Określenie akcji


¨  Zapewnienie możliwości wyboru
Zapisz zmiany i uruchom aplikację.

Jeśli będzie jedna aktywność


W takim przypadku kliknięcie przycisku Wyślij
wiadomość spowoduje, jak wcześniej, przejście
bezpośrednio do wybranej aplikacji.

Tu nic się nie zmieniło


— Android bezpośrednio
uruchamia wybraną
aktywność.

Jeśli będzie więcej niż jedna aktywność


W tym przypadku Android wyświetli okno dialogowe z listą wszystkich
odszukanych aktywności, lecz nie zapyta, czy chcemy, by wybrana akacja
została wykonana raz, czy też ma już być wykonywana zawsze. Oprócz
, które
tego okno ma tytuł określony na podstawie zasobu łańcuchowego. To jest okno dialogowe wyboru aktywności er().
utworzyliśmy przy użyciu metody createChoos
lającej na
Jak widać, nie zawiera ono już opcji pozwa razem.
m
wykonywanie wybranej aktywności za każdy

112 Rozdział 3.
Wiele aktywności i intencji

Jeśli nie będzie ŻADNYCH pasujących aktywności


Jeśli na naszym urządzeniu nie będzie żadnych aktywności pozwalających wysyłać
wiadomości, to metoda createChooser() powiadomi nas o tym, wyświetlając
stosowny komunikat.

To kolejna zaleta stosowania metody createChooser() — doskonale radzi


sobie ona z sytuacjami, w których nie ma dostępnych aktywności służących
do wykonywania konkretnej akcji.

Jeśli chcesz samodzielnie uzyskać


podobne wyniki, to spróbuj uruchomić
swoją aplikację w emulatorze i wyłączyć
przy tym aplikację SMS/MMS.

Nie istnieją
głupie pytania

P: A zatem mogę uruchamiać  P: Czy należy używać intencji  O: Akcja określa, co dana aktywność


swoje aplikacje w emulatorze lub na  niejawnych czy jawnych? potrafi robić, natomiast kategoria
fizycznym urządzeniu. Które z tych 
rozwiązań jest lepsze? O: Wszystko sprowadza się do określa dodatkowe informacje na temat
wykonywanej czynności. Nie opisaliśmy
pytania, czy chcemy wykonać akcję,
O: Każde z nich ma swoje wady i zalety. używając konkretnej aktywności, czy
tych kategorii szczegółowo, gdyż
w praktyce rzadko kiedy pojawia się
W razie stosowania urządzenia zależy nam wyłącznie na wykonaniu potrzeba tworzenia intencji z kategoriami.
fizycznego testowane aplikacje zazwyczaj operacji. Załóżmy, że chcemy wysłać
wczytują się znacznie szybciej niż na e-mail. Jeśli nie interesuje nas, której P: Napisaliście, że w przypadku, 
emulatorze. To rozwiązanie jest także aplikacji użytkownik użyje, byleby tylko gdy nie ma aktywności, która 
przydatne w przypadku pisania kodu, wiadomość została wysłana, to możemy by mogła obsłużyć intencję, to 
który współpracuje z komponentami użyć intencji niejawnej. Natomiast jeśli wywołanie metody createChooser() 
sprzętowymi urządzenia. musimy przekazać intencję do konkretnej wyświetli komunikat. Co by się stało, 
Z kolei emulator pozwala testować
aktywności w aplikacji, to powinniśmy gdybym użył standardowego okna 
użyć intencji jawnej. W takim przypadku do wyboru aktywności i przekazał 
aplikację na wielu różnych wersjach
musimy jawnie określić aktywność, intencję niejawną w wywołaniu 
Androida, na ekranach o różnych
do której ma trafić intencja. metody startActivity()?
wielkościach oraz na urządzeniach
o różnych specyfikacjach. Pozwala zatem
P: Wspominaliście, że filtr intencji  O: Jeśli nie ma aktywności pasujących do
uniknąć konieczności kupowania wielu intencji przekazanej w wywołaniu metody
aktywności oprócz akcji może także 
różnych urządzeń. startActivity(), to zostanie zgłoszony
określać kategorię. Czym różni się 
Najważniejsze jest, by dokładnie kategoria od akcji? wyjątek ActivityNotFoundException.
przetestować aplikację, używając Jeśli nie przechwycimy go w bloku try/
kombinacji urządzeń emulowanych catch, to może on doprowadzić do awarii
i fizycznych, zanim udostępnimy ją aplikacji.
szerszemu gronu odbiorców.

jesteś tutaj  113


Przybornik

Twój przybornik do Androida Pełny kod przykładowe


aplikacji prezen tow ane
j
j
Rozdział 3.

ale mo żes z
w tym rozdzi
Opanowałeś już rozdział 3. i dodałeś  pobrać z serwe ra FTP
do swojego przybornika z narzędziami  wydawnictwa Helion:
klady/
umiejętności tworzenia aplikacji zawierających  ftp://ftp.helion.pl/przy
wiele aktywności i stosowania intencji. andrrg.zip

    CELNE SPOSTRZEŻENIA
 Zadanie to co najmniej dwie aktywności połączone w sekwencję.
 Element <EditText> definiuje pole służące do wpisywania tekstów. Klasa EditText dziedziczy po klasie View.
 W Android Studio nowe aktywności można dodawać, wybierając z menu opcję File/New.../Activity.
 Każdej tworzonej aktywności musi odpowiadać wpis w pliku AndroidManifest.xml.
 Intencje to komunikaty, których komponenty systemu Android używają do wzajemnej komunikacji.
 Intencja jawna w jawny sposób określa komponent, do którego jest skierowana. Takie intencje można tworzyć,
używając wywołania o postaci Intent intent = new Intent(this, KlasaDocelowa.class);.
 Aby uruchomić aktywność, wystarczy użyć wywołania startActivity(intent). Jeśli nie uda się znaleźć
odpowiednich aktywności, to takie wywołanie zgłosi wyjątek ActivityNotFoundException.
 Metoda putExtra() pozwala na dodawanie do intencji dodatkowych informacji.
 Intencję, która doprowadziła do uruchomienia aktywności, można pobrać przy użyciu metody getIntent().
 Grupa metod get*Extra() pozwala pobierać dodatkowe informacje zapisane w intencji metoda
getStringExtra() pobiera łańcuchy znaków, getIntExtra() pobiera liczby całkowite, i tak dalej.
 Akcja aktywności określa standardową czynność, którą dana aktywność potrafi wykonywać. Aby wysłać wiadomość,
należy użyć akcji ACTION_SEND.
 W celu utworzenia intencji niejawnej umożliwiającej wykonanie określonej akcji należy użyć wywołania Intent intent
= new Intent(akcja);.
 Metoda setType() pozwala określić typ danych zapisanych w intencji.
 Android wyznacza intencje na podstawie nazwy komponentu, akcji, typu danych oraz kategorii, określonych w intencji.
Wszystkie te informacje są porównywane z zawartością filtrów intencji zapisanych w pliku AndroidManifest.xml, którym
dysponuje każda aplikacja na Androida. Aby aktywność mogła odbierać i obsługiwać intencje niejawne, musi mieć
kategorię o wartości DEFAULT.
 Metoda createChooser() pozwala przesłonić domyślne systemowe okno dialogowe wyboru aktywności. Metoda ta
pozwala określić tytuł i nie zapewnia użytkownikowi możliwości ustawienia aktywności domyślnej. Jeśli żadna aktywność
nie jest w stanie obsłużyć intencji, to metoda wyświetli stosowny komunikat. Metoda createChooser() zwraca obiekt
klasy Intent.
 Wartość zasobu łańcuchowego można pobrać, używając wywołania getString(R.string.nazwalancucha);.

114 Rozdział 3.
4. Cykl życia aktywności

Była sobie aktywność


…wtedy mu
powiedziałam, że jeśli zaraz
nie wykona onStop(), to
pokażę mu, co z nim zrobi
moja onDestroy().

Aktywności stanowią podstawę wszystkich aplikacji na Androida.


Wiesz już, jak tworzyć aktywności i jak sprawić, by jedna aktywność uruchomiła drugą, używając
do tego celu intencji. Ale co tak naprawdę dzieje się za kulisami? W tym rozdziale nieco
dokładniej poznamy cykl życia aktywności. Co się dzieje, kiedy aktywność jest tworzona
i usuwana? Jakie metody są wywoływane, gdy aktywność jest wyświetlana i pojawia się na
ekranie, a jakie gdy aktywność traci miejsce wprowadzania i jest ukrywana? W jaki sposób
można zapisywać i odtwarzać stan aktywności?

to jest nowy rozdział  115


Jak działają aktywności?

Jak właściwie działają aktywności?


Dotychczas dowiedziałeś się jedynie, jak można tworzyć aktywności, które prowadzą interakcję
z użytkownikami, i aplikacje używające kilku aktywności do wykonywania akcji. Skoro dodałeś
już te podstawowe umiejętności do swojego przybornika z narzędziami, nadszedł czas, by nieco
dokładniej przyjrzeć się temu, jak tak naprawdę działają aktywności. Poniżej zamieściliśmy
podsumowanie tego, co już wiesz, dodając do niego kilka dodatkowych szczegółów.

 Aplikacja jest kolekcją aktywności, układów oraz innych zasobów.


Jedna z tych aktywności jest główną aktywnością aplikacji.

Aplikacja

Każda aplikacja ma aktywność


główną, wskazaną w pliku
manifestu AndroidManifest.xml.
Aktywność główna
Aktywność

Aktywność
Aktywność

 Domyślnie każda aplikacja jest uruchamia w odrębnym, własnym procesie.


Dzięki temu łatwiej jest zapewnić bezpieczeństwo aplikacji. Więcej informacji na ten temat
można znaleźć w „Dodatku A” (poświęconym środowisku uruchomieniowemu Androida — ART),
umieszczonym na końcu książki.
Proces 1 Proces 2
Aplikacja 1 Aplikacja 2

Aktywność Aktywność
Aktywność

Aktywność

Aktywność Aktywność
Aktywność

116 Rozdział 4.
Cykl życia aktywności

 Można uruchomić aktywność w innej aplikacji, przekazując,


przy użyciu metody startActivity(), odpowiednią intencję.
System Android dysponuje informacjami o wszystkich zainstalowanych aplikacjach
i o ich aktywnościach i potrafi użyć intencji do uruchomienia odpowiedniej aktywności.

Aplikacja 1 Aplikacja 2

startActivity()

Intencja
Aktywność Aktywność
Intencja
Aktywność Aktywność
Android

Aktywność Aktywność

 Kiedy trzeba uruchomić jakąś aktywność, Android sprawdza,


czy istnieje już proces danej aplikacji.
Jeśli taki proces istnieje, to system uruchamia w nim wybraną aktywność.
W przeciwnym razie, jeśli procesu nie ma, to system go utworzy.
Proces 1
Aplikacja 1
Już wykonuję aktywności
tej aplikacji w ramach
procesu 1. Zatem tę
aktywność także uruchomię
w tym procesie.
Android

 Kiedy Android uruchamia aktywność, wywołuje jej metodę onCreate().


Metoda onCreate() jest zawsze wywoływana w momencie tworzenia aktywności.

onCreate()

Aktywność
Android

Niemniej jednak cały czas nie wiemy jeszcze wielu rzeczy na temat sposobu
działania aktywności. Jak długo istnieją aktywności? Co się dzieje, kiedy
aktywność znika z ekranu? Czy wciąż działa? Czy wciąż zajmuje miejsce
w pamięci? I co się dzieje, gdy działanie aplikacji zostaje przerwane przez
nadchodzącą rozmowę telefoniczną? Chcielibyśmy mieć możliwość kontrolowania
działania aktywności w całym zakresie różnych sytuacji. Ale jak to zrobić?

jesteś tutaj  117


Stoper

Aplikacja stopera
W tym rozdziale przyjrzymy się dokładniej tajnikom działania aktywności,
często spotykanym przyczynom problemów z aplikacjami oraz sposobom
rozwiązywania tych problemów za pomocą metod cyklu życia aktywności.
Metody te mamy zamiar poznać, pisząc prostą aplikację — Stoper.

Nasza aplikacja będzie się składała z jednej aktywności i jednego układu.


Układ będzie zawierał tekst pokazujący, ile czasu upłynęło, oraz przyciski:
Start (rozpoczynający pomiar czasu), Stop (zatrzymujący stoper) oraz Kasuj
(zerujący licznik czasu).

To jest liczba
sekund.

Kiedy klikniesz
przycisk Start, liczba
sekund zaczyna być
inkrementowana.

Kiedy klikniesz
przycisk Stop, liczba
sekund przestaje być
inkrementowana.

Kiedy klikniesz przycisk


Kasuj, liczba sekund
zostaje zmniejszona do 0.

Implementacja aplikacji <Layout>

Dysponujesz już dostateczną wiedzą i doświadczeniem, </Layout>

by samodzielnie napisać tę aplikację, bez większej pomocy


activity_stopwatch.xml
z naszej strony. Pokażemy tu tylko tyle kodu, ile potrzebujesz,
by móc ją w całości zaimplementować, a następnie sprawdzić, Aplikacja składa
się z jednej
co się stanie, kiedy ją uruchomisz. aktywności
i jednego układu.
Zacznij od utworzenia nowego projektu aplikacji na Androida
o nazwie Stoper, która będzie umieszczona w pakiecie
com.hfad.stoper. Wybierz minimalny poziom API
o wartości 15, tak by aplikacja mogła działać na przeważającej
StopwatchActivity.java
większości urządzeń. Utwórz także aktywność o nazwie
StopwatchActivity i układ o nazwie activity_stopwatch.

118 Rozdział 4.
Cykl życia aktywności

Kod układu aplikacji stopera


Poniżej przedstawiliśmy kod XML układu aplikacji. Składa się on z jednego
widoku tekstowego używanego do wyświetlania czasu zmierzonego przez
stoper oraz trzech przycisków kontrolujących działanie stopera. Zastąp zatem
zawartość swojego pliku activity_stopwatch.xml poniższym kodem XML:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent” Stoper
android:paddingBottom=”16dp”
android:paddingLeft=”16dp” app/src/main

android:paddingRight=”16dp”
res
android:paddingTop=”16dp”
tools:context=”.StopwatchActivity” > layout
<xml>

Tego komponentu będziemy </xml>


<TextView używać do wyświetlania activity_
android:id=”@+id/time_view” liczby sekund. stopwatch.xml

android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentTop=”true”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”0dp”
android:text=””
android:textAppearance=”?android:attr/textAppearanceLarge”
android:textSize=”92sp” /> Te atrybuty sprawią, że czas
wyświetlany przez nasz stoper
będzie ładny i duży.
<Button
android:id=”@+id/start_button”
Ten kod dotyczy przycisku
android:layout_width=”wrap_content” Start. Jego kliknięcie
android:layout_height=”wrap_content” powoduje wywołanie
metody onClickStart().
android:layout_below=”@+id/time_view”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”32dp”
android:onClick=”onClickStart” Dalsza część
kodu układu
android:text=”@string/start” /> znajduje się
na następnej
stronie.

jesteś tutaj  119


Kod układu

Kod układu (ciąg dalszy)


Stoper
<Button
android:id=”@+id/stop_button” app/src/main
android:layout_width=”wrap_content”
res
android:layout_height=”wrap_content”
android:layout_below=”@+id/start_button” layout
Ten kod dotyczy przycisku
android:layout_centerHorizontal=”true” Stop. Jego kliknięcie <xml>
</xml>
powoduje wywołanie metody
android:layout_marginTop=”17dp” onClickStop(). activity_
android:onClick=”onClickStop” stopwatch.xml

android:text=”@string/stop” />

Ten kod dotyczy przycisku Kasuj.


<Button Jego kliknięcie powoduje wywołanie
metody onClickReset().
android:id=”@+id/reset_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/stop_button” Zrób to sam!
android:layout_centerHorizontal=”true”
android:layout_marginTop=”17dp”
android:onClick=”onClickReset”
Zanim przejdziesz do
android:text=”@string/reset” />
dalszych prac nad aplikacją,
koniecznie zaktualizuj
</RelativeLayout> plik układu i plik zasobów
strings.xml.
Plik strings.xml aplikacji stopera
Nasz układ korzysta z trzech dodatkowych łańcuchów znaków — każdy
z nich określa tekst jednego przycisku. Łańcuchy te zostaną zdefiniowane
jako zasoby łańcuchowe, co oznacza, że będziesz musiał je zapisać w pliku Stoper
strings.xml. A zatem dodaj do tego pliku poniższe wiersze kodu:
app/src/main
...
<string name=”start”>Start</string> res
To są etykiety
<string name=”stop”>Stop</string> przycisków.
<string name=”reset”>Kasuj</string> values
<xml>
... </xml>

strings.xml
Układ jest gotowy! Teraz zajmijmy się aktywnością.

120 Rozdział 4.
Cykl życia aktywności

Jak będzie działał kod aktywności?


Układ aplikacji definiuje trzy przyciski, których będziemy używać do
obsługi stopera. Każdy z tych przycisków korzysta z atrybutu onClick
i przy jego użyciu określa metodę aktywności, którą należy wywołać
w momencie kliknięcia danego przycisku. W przypadku kliknięcia
przycisku Start zostanie wywołana metoda onClikStart(), kliknięcie
przycisku Stop spowoduje wywołanie metody onClickStop(),
a rezultatem kliknięcia przycisku Kasuj będzie wywołanie metody
onClickReset(). Tych trzech metod będziemy używali do
uruchamiania, zatrzymywania oraz zerowania stopera.

Kiedy klikniesz przycisk Start, zostanie


wywołana metoda onClickStart().

Kiedy klikniesz przycisk Stop, zostanie


wywołana metoda onClickStop().

Kiedy klikniesz przycisk Kasuj, zostanie


wywołana metoda onClickReset().

Sam stoper będziemy aktualizować przy użyciu metody o nazwie


runTimer(). Będzie ona wywoływana co sekundę, by sprawdzić,
czy stoper działa, i inkrementować liczbę sekund przechowywaną
w aplikacji i wyświetlaną w widoku tekstowym. runTimer()

Obsługę aplikacji ułatwimy sobie, używając dwóch zmiennych


Aktywność
prywatnych, które będą przechowywały stan stopera. Pierwszą
z nich będzie zmienna typu int o nazwie seconds, która będzie
przechowywała liczbę sekund, jaka upłynęła od momentu
włączenia stopera, a drugą zmienna typu boolean o nazwie
running, określająca, czy stoper aktualnie działa, czy nie.

Zaczniemy od napisania kodu obsługującego przyciski,


a następnie zajmiemy się metodą runTimer().

jesteś tutaj  121


Przyciski

Dodanie kodu obsługującego przyciski


running=true
Kiedy użytkownik kliknie przycisk Start, przypiszemy zmiennej running
wartość true, dzięki czemu stoper zacznie działać. Następnie, gdy
użytkownik kliknie przycisk Stop, zmienimy wartość zmiennej running running=false
na false, co spowoduje zatrzymanie stopera. Kiedy zaś użytkownik
kliknie przycisk Kasuj, przypiszemy zmiennej running wartość false,
a zmiennej seconds wartość 0, co spowoduje, że stoper zostanie running=false
zatrzymany i wyzerowany. seconds=0

Zastąp zawartość swojego pliku StopwatchActivity.java następującym kodem:

package com.hfad.stoper;

import android.os.Bundle;
Stoper
import android.app.Activity;
import android.view.View;
app/src/main

public class StopwatchActivity extends Activity {


java

private int seconds = 0; Używamy zmiennych seconds


i running do przechowywania liczby com.hfad.stoper
private boolean running; sekund, które upłynęły, i informacji,
czy stoper jest włączony, czy nie.
@Override Stopwatch
Activity.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stopwatch);
}

// Metoda uruchamia stoper po kliknięciu przycisku Start


public void onClickStart(View view) {
Ta metoda jest wywoływana
running = true; po kliknięciu przycisku Start.
Ta instrukcja uruchamia stoper.
}

// Metoda zatrzymuje stoper po kliknięciu przycisku Stop


public void onClickStop(View view) { Ta metoda jest wywoływana
running = false; po kliknięciu przycisku Stop.
Ta instrukcja zatrzymuje stoper.
}

// Metoda zeruje stoper po kliknięciu przycisku Kasuj


public void onClickReset(View view) { Ta metoda jest wywoływana
po kliknięciu przycisku Kasuj.
running = false; Te instrukcje zatrzymują
seconds = 0; stoper i przypisują liczbie
sekund wartość 0.
}
}
122 Rozdział 4.
Cykl życia aktywności

Metoda runTimer()
Kolejną rzeczą, którą musimy napisać, jest metoda runTimer(). Metoda ta
pobierze referencję do komponentu TextView umieszczonego w układzie
aplikacji, sformatuje liczbę sekund do postaci łańcucha znaków prezentującego
liczbę godzin, minut i sekund, a następnie wyświetli ten łańcuch znaków
w układzie. Oprócz tego, jeśli wartość zmiennej running będzie wynosiła true,
to metoda runTimer() dodatkowo inkrementuje wartość zmiennej seconds.
Poniżej przedstawiliśmy kod tej metody:
Pobranie referencji do komponentu
private void runTimer() { TextView.
final TextView timeView = (TextView)findViewById(R.id.time_view);
...
int hours = seconds/3600;
int minutes = (seconds%3600)/60; Ten fragment formatuje liczbę
W tych
miejscach int secs = seconds%60; sekund do postaci godzin,
pominęliśmy minut i sekund. To zwyczajny
krótkie String time = String.format(”%d:%02d:%02d”, kod napisany w Javie.
fragmenty hours, minutes, secs);
kodu.
Przedstawimy timeView.setText(time);
je na Ta instrukcja wyświetla tekst w komponencie TextView.
następnej if (running) {
stronie. Jeśli zmienna running ma
seconds++;
wartość true, to ta instrukcja
} inkrementuje wartość zmiennej
seconds.
...
}

Musimy zapewnić, aby kod metody runTimer() był wykonywany cyklicznie,


tak by co sekundę inkrementował wartość zmiennej seconds i aktualizował
wyświetlany czas zmierzony przez stoper. Jednocześnie musimy to zrobić
w taki sposób, by nie zablokować głównego wątku aplikacji.

W aplikacjach pisanych w innych językach programowania niż Java takie


zadania można wykonywać przy użyciu wątków działających w tle. Jednak
w Androidowie takie rozwiązania nastręczają nieco problemów, gdyż tylko
główny wątek aplikacji może aktualizować jej interfejs użytkownika — jeśli
spróbuje to zrobić jakikolwiek inny wątek, to zostanie zgłoszony wyjątek
CalledFromWrongThreadException.

Rozwiązaniem jest zastosowanie obiektu klasy Handler. Przyjrzymy się mu


dokładniej na następnej stronie.

jesteś tutaj  123


Obiekty Handler

Obiekty Handler umożliwiają planowanie


wykonania kodu
Handler to klasa systemu Android umożliwiająca tworzenie kodu, który ma
zostać wykonany w przyszłości. Można jej także używać do przekazywania kodu,
który ma zostać wykonany w innym wątku. W naszym przypadku zastosujemy tę
klasę do utworzenia kodu stopera, który będzie wykonywany co sekundę.

By skorzystać z klasy Handler, kod, którego wykonanie chcemy zaplanować,


musimy umieścić w obiekcie Runnable, a następnie użyć metody post() lub
postDelayed() klasy Handler, by określić, kiedy kod ma zostać wykonany.
Przyjrzyjmy się nieco dokładniej każdej z tych metod.

Metoda post()
Metoda post() przekazuje kod, który ma zostać wykonany najszybciej jak to
możliwe (czyli zazwyczaj niemal natychmiast). Metoda ta ma jeden parametr
— obiekt typu Runnable. W Androidowie obiekty Runnable odpowiadają
obiektom tego samego typu stosowanym w zwyczajnych programach pisanych
w Javie — reprezentują zadanie, które należy wykonać. Kod, który chcemy
wykonać, należy umieścić w metodzie run() obiektu Runnable, a obiekt
Handler zadba o to, by został on wykonany możliwie jak najszybciej.
Oto, jak wygląda implementacja takiego rozwiązania:

final Handler handler = new Handler();


handler.post(Runnable); Kod, który chcemy wykonać, należy umieścić
w metodzie run() obiektu Runnable.

Metoda postDelayed()
Metoda postDelayed() działa bardzo podobnie do metody post(), z tym,
że używamy jej do przesyłania kodu, który ma zostać wykonany w przyszłości.
Metoda postDelayed() ma dwa parametry: obiekt Runnable oraz liczbę typu
long. Obiekt Runnale, w swojej metodzie run(), zawiera kod, który należy
wykonać, natomiast liczba określa o ile milisekund chcemy opóźnić jego
wykonanie. Kod zostanie wykonany możliwie jak najszybciej po upłynięciu
czasu określonego przez opóźnienie. Poniżej przedstawiliśmy przykład
zastosowania tej metody:

final Handler handler = new Handler();


Tej metody można użyć, aby opóźnić
handler.postDelayed(Runnable, long); wykonanie kodu o określoną liczbę milisekund.

Na następnej stronie użyjemy tych metod do cyklicznego


aktualizowania stopera.

124 Rozdział 4.
Cykl życia aktywności

Pełny kod metody runTimer()


W celu aktualizacji stopera będziemy cyklicznie planowali wykonanie kodu z użyciem
obiektu Handler, za każdym razem stosując opóźnienie o długości 1000 milisekund.
Każde wykonanie kodu będzie powodowało inkrementację wartości zmiennej seconds
i aktualizację tekstu wyświetlanego w widoku tekstowym.

Poniżej przedstawiamy pełny kod metody runTimer():

private void runTimer() {


final TextView timeView = (TextView)findViewById(R.id.time_view);
final Handler handler = new Handler(); Tworzy nowy obiekt Handler.
handler.post(new Runnable() { To wywołanie metody post() i przekazanie do niej nowego
obiektu typu Runnable(). Metoda post() wykonuje kod bez
@Override opóźnienia, zatem kod umieszczony w obiekcie Runnable
public void run() { zostanie wykonany niemal natychmiast.
int hours = seconds/3600;
int minutes = (seconds%3600)/60;
Metoda run() obiektu Runnable
int secs = seconds%60; zawiera kod, który chcemy
wykonać — w naszym
String time = String.format(”%d:%02d:%02d”, przypadku jest to kod, który
hours, minutes, secs); aktualizuje liczbę sekund
timeView.setText(time); wyświetloną w widoku
tekstowym.
if (running) {
seconds++;
} To wywołanie przekazuje kod w obiekcie Runnable
i planuje jego wykonanie z opóźnieniem wynoszącym
handler.postDelayed(this, 1000); 1000 milisekund, czyli 1 sekundę. Ponieważ ten
wiersz kodu jest umieszczony wewnątrz metody run()
} obiektu Runnable, będzie on wykonywany cyklicznie.
});
}

Zastosowanie metod post() i postDelayed() oznacza, że przekazany kod będzie


wykonywany najszybciej jak to możliwe, po upływie podanego opóźnienia. W praktyce
oznacza to, że będzie on wykonany niemal natychmiast po upływie zadanego okresu czasu.
Choć oznacza to, że im dłużej będzie działała aplikacja, tym opóźnienia mierzonego czasu
będą większe, to jednak rozwiązanie to będzie wystarczająco dokładne jak na potrzeby
zamieszczonej w tym rozdziale prezentacji cyklu życia aktywności.

Metodę runTimer() uruchomimy w momencie tworzenia aktywności StopwatchActivity,


a to oznacza, że wywołamy ją w metodzie onCreate() aktywności:

protected void onCreate(Bundle savedInstanceState) {


...
runTimer();
}
Pełny kod aktywności zamieściliśmy na następnej stronie.

jesteś tutaj  125


Kod aktywności StopwatchActivity

Kompletny kod aktywności StopwatchActivity


Poniżej przedstawiliśmy kompletny kod pliku StopwatchActivity.java.
Zaktualizuj ten plik w swojej aplikacji, by miał tę samą zawartość.

package com.hfad.stoper;

Stoper
import android.os.Bundle;
import android.os.Handler; app/src/main
import android.app.Activity;
java
import android.view.View;
import android.widget.TextView; com.hfad.stoper

public class StopwatchActivity extends Activity { Stopwatch


// Liczba sekund wyświetlana przez stoper Activity.java

private int seconds = 0;


Zmiennych seconds i running
// Czy stoper działa? używamy odpowiednio do
przechowywania liczby zmierzonych
private boolean running; sekund i informacji, czy aktualnie
stoper działa, czy nie.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stopwatch);
runTimer();
Do aktualizacji stopera używamy innej
} metody. Wywołujemy ją w momencie
tworzenia aktywności.

// Metoda uruchamia stoper po kliknięciu przycisku Start


public void onClickStart(View view) {
Ta metoda jest wywoływana
running = true; Ta instrukcja uruchamia stoper. po kliknięciu przycisku Start.
}

// Metoda zatrzymuje stoper po kliknięciu przycisku Stop


public void onClickStop(View view) { Ta metoda jest wywoływana
po kliknięciu przycisku Stop.
running = false; Ta instrukcja zatrzymuje stoper.
}

126 Rozdział 4.
Cykl życia aktywności

Kod aktywności (ciąg dalszy)


// Metoda zeruje stoper po kliknięciu przycisku Kasuj
public void onClickReset(View view) { Ta metoda jest Stoper
running = false; wywoływana
Te instrukcje zatrzymują po kliknięciu
seconds = 0; stoper i zerują liczbę przycisku Kasuj. app/src/main
zmierzonych sekund.
}
java
Ta instrukcja pobiera
referencję do widoku
// Wyświetla w stoperze liczbę sekund tekstowego. com.hfad.stoper
private void runTimer() {
final TextView timeView = (TextView)findViewById(R.id.time_view); Stopwatch
final Handler handler = new Handler(); Activity.java

handler.post(new Runnable() { Tu używamy obiektu Handler


@Override do przekazania kodu.

public void run() {


int hours = seconds/3600;
int minutes = (seconds%3600)/60; Ten fragment formatuje liczbę
sekund do postaci godzin,
int secs = seconds%60; minut i sekund.
String time = String.format(”%d:%02d:%02d”, hours, minutes, secs);
timeView.setText(time);
Ta instrukcja wyświetla tekst w komponencie TextView.
if (running) {
seconds++; Jeśli zmienna running ma wartość true,
to ta instrukcja inkrementuje wartość
} zmiennej seconds.

handler.postDelayed(this, 1000); To wywołanie ponownie przekazuje kod do


wywołania, określając przy tym opóźnienie
} o długości 1 sekundy.
});
}
}
Zrób to sam!
Zobaczmy, co się stanie, kiedy wykonamy ten kod.

Upewnij się, że zaktualizowałeś


kod swojej aktywności,
wprowadzając w nim wszystkie
pokazane zmiany.

jesteś tutaj  127


Co się dzieje?

Co się dzieje w trakcie działania aplikacji?


1 Użytkownik decyduje się uruchomić aplikację.
Klika zatem ikonę aplikacji.

Urządzenie
Użytkownik

2 Plik AndroidManifest.xml aplikacji określa, którą aplikację należy wykonać


podczas uruchamiania aplikacji.
System tworzy intencję, która spowoduje uruchomienie aktywności, i przekazuje ją
w wywołaniu metody startActivity(intent).

<xml>
</xml>

AndroidManifest.xml Android

3 Android sprawdza, czy jest uruchomiony inny proces tej samej aplikacji,
a jeśli takiego nie ma, to tworzy nowy proces.
Następnie system tworzy nowy obiekt aktywności — w naszym przypadku będzie to
obiekt aktywności StopwatchActivity.
Proces 1
Aplikacja 1

Android

128 Rozdział 4.
Cykl życia aktywności

Historii ciąg dalszy


4 Zostaje wywołana metoda onCreate() aktywności.
Kod onCreate() wywołuje metodę setContnetView(), określając używany układ,
a następnie uruchamia stoper, wywołując w tym celu metodę runTimer().

runTimer() <xml>
</xml>

StopwatchActivity Układ

5 Po zakończeniu wykonywania metody onCreate() na ekranie urządzenia zostaje


wyświetlony układ.
Metoda runTimer() określa tekst, który należy wyświetlić w widoku tekstowym na podstawie
wartości zmiennej seconds. Natomiast zmienna running pozwala jej określić, czy należy
inkrementować liczbę sekund, czy nie. Ponieważ zmienna running ma początkowo wartość
false, liczba sekund nie jest inkrementowana.

seconds=0

StopwatchActivity

Urządzenie running=false

Nie istnieją
głupie pytania

P: Dlaczego Android uruchamia aplikację w odrębnym P : Czy nie można by napisać w metodzie onCreate()
procesie? pętli, która aktualizowałaby stoper?

O: Ze względów bezpieczeństwa i dla zapewnienia stabilności. O: Nie. Wykonywanie metody onCreate() musi się zakończyć
Uniemożliwia to aplikacji uzyskanie dostępu do danych innych przed wyświetleniem ekranu aplikacji. Umieszczenie w niej
aplikacji. Oprócz tego oznacza to, że jeśli aplikacja ulegnie awarii, nieskończonej pętli uniemożliwiłoby zatem wyświetlenie układu.
nie będzie to miało zgubnych konsekwencji dla innych aplikacji.
P: Metoda runTimer() wygląda na naprawdę złożoną.
P: Do czego służy metoda onCreate()? Nie prościej by Czy naprawdę trzeba wykonywać te wszystkie operacje?

O: Fakt, jest trochę skomplikowana, jednak za każdym razem,


było umieszczać kod w konstruktorze?

O: Android musi przygotować środowisko na potrzeby gdy będziemy musieli zaplanować wykonanie kodu, używane
aktywności już po jej utworzeniu. Kiedy aktywność jest już gotowa, rozwiązanie będzie wyglądało podobnie do tego z metody
Android wywołuje jej metodę onCreate(). To właśnie dlatego runTimer().
kod przygotowujący ekran aplikacji jest wykonywany w metodzie
onCreate(), a nie w konstruktorze aktywności.

jesteś tutaj  129


Jazda próbna

Jazda próbna aplikacji


Kiedy uruchomimy tę aplikację w emulatorze, będzie działała doskonale.
Możemy uruchomić stoper, zatrzymać go, wyzerować, a wszystko to bez
najmniejszych problemów. Innymi słowy: aplikacja działa dokładnie
według oczekiwań.

Przyciski działają zgodnie z oczekiwaniami.


Kliknięcie przycisku Start rozpoczyna
działanie stopera, kliknięcie przycisku
Stop zatrzymuje go, a kliknięcie przycisku
Kasuj powoduje wyzerowanie stopera.

Ale jest jeden problem…


Kiedy uruchomimy aplikację na fizycznym urządzeniu, będzie ona
działała prawidłowo, dopóki nie obrócimy urządzenia. Gdy to zrobimy,
stoper zostanie wyzerowany.

Stoper działał, lecz po obróceniu


urządzenia został wyzerowany.

W androidowych aplikacjach wyjątkowo często występują problemy


po obróceniu urządzenia. Zanim zajmiemy się rozwiązaniem tego
problemu, przyjrzymy się dokładniej jego przyczynie.

130 Rozdział 4.
Cykl życia aktywności

Co się stało?
A więc co się stało, że po obróceniu urządzenia w aplikacji wystąpił błąd?

Przyjrzyjmy się nieco dokładniej temu, co się w niej stało.

1 Użytkownik uruchamia aplikację i klika przycisk Start, by rozpocząć działanie stopera.


Metoda runTimer() rozpoczyna inkrementację liczby sekund wyświetlanych w widoku tekstowym
o identyfikatorze time_view, wykorzystując przy tym obie zmienne: seconds i running.

seconds=12

StopwatchActivity

Urządzenie running=true

2 Użytkownik obraca urządzenie.


Android zauważa, że zmieniła się orientacja urządzenia i wielkość ekranu, i w efekcie usuwa
działającą aktywność, włącznie ze zmiennymi używanymi przez metodę runTimer().

Urządzenie

3 Aktywność StopwatchActivity zostaje ponownie utworzona.


Metoda onCreate() aktywności zostaje ponownie wykonana, co powoduje wywołanie
metody runTimer(). Ponieważ aktywność została ponownie utworzona, zmienne
seconds i running przyjmują wartości domyślne.

Zmiennej seconds zostaje


przypisana wartość 0,
seconds=0 a zmiennej running
wartość false. Ta zmiana
jest wynikiem usunięcia
StopwatchActivity i ponownego utworzenia
aktywności, spowodowanych
Urządzenie obróceniem urządzenia.
running=false
jesteś tutaj  131
Konfiguracje urządzenia

Obrót ekranu zmienia konfigurację urządzenia


Kiedy Android uruchamia aplikację i rozpoczyna wykonywanie aktywności,
uwzględnia przy tym konfigurację urządzenia. Konfiguracja ta obejmuje
zarówno konfigurację fizycznego urządzenia (czyli takie jego aspekty jak
wielkość ekranu, orientacja ekranu, dostępność podłączonej klawiatury),
jak i opcje konfiguracyjne określane przez użytkownika (takie jak wybrane
ustawienia lokalne).

Android musi znać konfigurację urządzenia w momencie uruchamiania


aktywności, gdyż może ona mieć wpływ na zasoby niezbędne dla działania
aplikacji. Na przykład inny układ może być używany, gdy urządzenie jest
trzymane w pionie, a inny gdy jest trzymane w poziomie, i podobnie inne
zasoby łańcuchowe mogą być używane w przypadku, kiedy na urządzeniu Konfiguracja urządzenia
zostaną wybrane francuskie ustawienia lokalne.
obejmuje opcje podane
przez użytkownika
(takie jak ustawienia
lokalne), jak również opcje
związane z fizycznym
urządzeniem (takie jak
jego orientacja i wielkość
Każda aplikacja
na Androida może ekranu). Zmiana dowolnej
zawierać wiele
plików zasobów, spośród tych opcji
przechowywanych
w katalogu /app/src/
main/res. Na przykład
spowoduje usunięcie
jeśli w urządzeniu
zostaną wybrane i ponowne odtworzenie
francuskie ustawienia
lokalne, to aplikacja
będzie używała pliku
aktywności.
strings.xml pobranego
z katalogu values-fr.

Kiedy zmienia się konfiguracja urządzenia, trzeba zaktualizować


wszystko, co prezentuje interfejs użytkownika, tak by odpowiadał
on nowej konfiguracji. Jeśli obrócimy urządzenie, Android
zauważy zmianę orientacji i wielkości ekranu i uzna to za zmianę
konfiguracji urządzenia. W efekcie system usunie aktualną aktywność
i ponownie ją utworzy, aby można było pobrać i zastosować zasoby
odpowiadające bieżącej konfiguracji urządzenia.

132 Rozdział 4.
Cykl życia aktywności

Od narodzin do śmierci: stany aktywności


Kiedy Android tworzy i usuwa aktywność, zmienia się jej stan: początkowo jest
uruchomiona, potem działa, a w końcu zostaje zniszczona.

Podstawowym stanem aktywności jest stan działania lub aktywności. Aktywność


działa, kiedy jest wyświetlona na ekranie (znajduje się na pierwszym planie),
dysponuje miejscem wprowadzania (ang. input focus), a użytkownik może z nią
prowadzić interakcje. Większość swojego życia aktywność spędza właśnie w tym
stanie. Aktywność zaczyna działać po wcześniejszym uruchomieniu, a potem,
pod koniec swojego istnienia, zostanie usunięta (lub zniszczona).

Aktywność Obiekt aktywności został już


utworzony, ale jeszcze nie działa.
uruchomiona

Aktywność Większość swojego Aktywność działa,


życia aktywność
działająca spędza w tym stanie. kiedy znajduje się na
pierwszym planie,
czyli jest widoczna
Aktywność Na tym etapie na ekranie.
aktywność już
usunięta nie istnieje.
Metoda onCreate()
Kiedy stan aktywności zmienia się z utworzonej na usuniętą, wykonywane jest wywoływana
są dwie kluczowe metody cyklu życia aktywności: onCreate() i onDestroy(). w momencie
Są to metody cyklu życia każdej aplikacji, które są dziedziczone i które,
w razie takie potrzeby, można przesłonić.
pierwszego tworzenia
aktywności i stanowi
Metoda onCreate() jest wywoływana bezpośrednio po uruchomieniu aktywności.
To właśnie w niej można wykonywać wszystkie standardowe czynności miejsce, w którym
związane z przygotowaniem działania aktywności, takie jak wywołanie metody aktywność jest
setContentView(). Tę metodę zawsze należy przesłaniać — w przeciwnym razie
przygotowywana
nie będziemy w stanie poinstruować systemu, którego układu używa aktywność.
do działania.
Z kolei onDestroy() to ostatnia metoda aktywności, która zostaje wywołana
przed jej ostatecznym usunięciem. Istnieje kilka różnych sytuacji, które mogą
skutkować usunięciem aplikacji — na przykład gdy nakazano jej zakończenie,
Metoda onDestroy()
gdy trzeba ją ponownie utworzyć z powodu zmiany konfiguracji urządzenia jest wywoływana
albo gdy Android zdecyduje się usunąć aktywność, aby zaoszczędzić miejsce.
przed usunięciem
Na następnej stronie przyjrzymy się powiązaniom tych dwóch metod ze stanami aktywności.
aktywności.
jesteś tutaj  133
Od narodzin do śmierci

Cykl życia aktywności: od utworzenia do usunięcia


Poniżej przedstawiony został cykl życia aktywności, od jej narodzin do śmierci. Jak się
przekonasz w dalszej części rozdziału, pominęliśmy tu kilka szczegółów, lecz na razie
koncentrujemy się wyłącznie na metodach onCreate() i onDestroy().

Aktywność 1 Aktywność zostaje uruchomiona.


uruchomiona System tworzy obiekt aktywności i zostaje
wywołany jej konstruktor.

2 Bezpośrednio po uruchomieniu aktywności


zostaje wywołana jej metoda onCreate().
onCreate() Metoda onCreate() jest miejscem, w którym należy
umieścić cały kod inicjujący działanie aktywności,
gdyż jest ona zawsze wywoływana po uruchomieniu
aktywności, lecz zanim zacznie ona działać.

Aktywność 3 Aktywność działa, kiedy jest widoczna


na ekranie i użytkownik może prowadzić
działająca z nią interakcję.
To właśnie w tym stanie aktywność spędza
przeważającą większość swego życia.

4 Metoda onDestroy() jest wywoływana


onDestroy() bezpośrednio przed usunięciem aktywności.
Metoda onDestroy() pozwala wykonać ostateczne
porządki po aktywności, na przykład zwolnić
używane przez nią zasoby.

Aktywność 5 Po wykonaniu metody onDestroy() aktywność


jest usuwana.
usunięta W tym momencie aktywność przestaje istnieć.

134 Rozdział 4.
Cykl życia aktywności

Twoja aktywność dziedziczy metody cyklu życia


Jak już wspomnieliśmy, wszystkie aktywności rozszerzają klasę android.app.Activity.
To właśnie ta klasa zapewnia naszym aktywnościom dostęp do metod cyklu życia.

Context Abstrakcyjna klasa Context


(android.content.Context)
startActivity(Intent) i. Zapewnia
Interfejs do globalnych informacji na temat środowiska aplikacj i.
dostęp do zasobów aplikacj i, klas oraz operacji na poziomie aplikacj

ContextWrapper Klasa ContextWrapper


(android.content.ContextWrapper)
startActivity(Intent) Implementacja pośrednika do klasy
Context.

ContextThemeWrapper Klasa ContextThemeWrapper


(android.view.ContextThemeWrapper)
Klasa ContextThemeWrapper umożliwia modyfikowanie
motywu z poziomu zawartości obiektu ContextWrapper.

Activity Klasa Activity


(android.app.Activity)
onCreate(Bundle)
Klasa Activity zawiera domyślne implementacje
onCreateOptionsMenu(Menu) metod cyklu życia aktywności. Oprócz tego definiuje
także takie metody jak findViewById(Int)
onStart() i setContentView(View).
onRestart()
onResume()
onPause()
onStop()
onDestroy()
onSaveInstanceState()
startActivity(Intent)
findViewById(Int)
setContentView(View)

MojaAktywnosc Klasa MojaAktywnosc


(com.hfad.foo)
onCreate(Bundle)
Większość zachowań aktywności jest obsługiwanych przez metody
onCreateOptionsMenu(Menu) klasy bazowej. Nam pozostaje jedynie przesłonięcie tych metod,
które będą nam potrzebne.
mojaMetoda()

jesteś tutaj  135


Pomijać, ale ostrożnie

W jaki sposób radzić sobie ze zmianami konfiguracji?


Jak się przekonałeś, aplikacja stopera przestała działać prawidłowo, gdy użytkownik
obrócił urządzenie. Aktywność została usunięta i ponownie utworzona, a to oznaczało
usunięcie używanych przez nią zmiennych. W jaki sposób można rozwiązać ten problem?
W jaki sposób mamy radzić sobie ze zmianami konfiguracji urządzenia, takimi jak zmiana
orientacji ekranu?

Istnieją dwa rozwiązania: można nakazać, żeby system pominął ponowne uruchamianie
aktywności, albo można zapisać stan aktywności w taki sposób, by nowa aktywność mogła
go odtworzyć. Teraz przyjrzymy się obu tym rozwiązaniom nieco dokładniej.

Pominięcie ponownego utworzenia aktywności


Z tego sposobu
Pierwszym rozwiązaniem jest nakazanie systemowi, by nie uruchamiał ponownie
obsługi zmian
aktywności po wykryciu zmiany konfiguracji urządzenia. Choć pokażemy Ci, jak to
zrobić, to jednak musisz pamiętać, że zazwyczaj nie jest to najlepsze rozwiązanie.
Obejrzyj to! konfiguracji można
korzystać wyłącznie
Wynika to z faktu, że gdy Android odtwarza aktywność, upewnia się, czy będzie w ostateczności.
ona korzystać z zasobów odpowiadających bieżącej konfiguracji urządzenia. Może
się zatem okazać, że jeśli pominiemy ten systemowy mechanizm, będziemy musieli Powoduje on bowiem pomijanie
sami napisać dodatkowy kod niezbędny do obsługi nowej konfiguracji. standardowego sposobu działania
systemu, co może być przyczyną
Aby poinformować Android, że nie powinien odtwarzać aktywności po zmianie problemów.
konfiguracji urządzenia, należy dodać do elementu activity w pliku manifestu
AndroidManifest.xml poniższy atrybut:

android:configChanges=”zmiana_konfiguracji”
Stoper
gdzie zmiana_konfiguracji jest typem zmiany konfiguracji urządzenia.
app/src/main
W naszym przypadku chcemy, by system pomijał zmiany wielkości i orientacji <xml>
ekranu, dlatego musimy dodać do pliku AndroidManifest.xml następujący fragment </xml>

kodu: AndroidManifest.xml

<activity Pionowa kreska (|) oznacza, że


należy pomijać
obie podane zmiany konfiguracji.
android:name=”com.hfad.stoper.StopwatchActivity” z faktu, że większość urządzeń Wynika to
ma prostokątne
ekrany, a zatem obrócenie takie
android:label=”@string/app_name” go
powoduje zmianę zarówno wymiaró urządzenia
jak i jego orientacji. w ekranu,
android:configChanges=”orientation|screenSize” >

Jeśli Android rozpozna zmianę konfiguracji, której typ został podany w atrybucie,
to zamiast standardowego odtwarzania aktywności zostanie wywołana metoda
onConfigurationChanged(Configuration):

public void onConfigurationChanged(Configuration config) {


}

Tę metodę można przesłonić, aby obsługiwać zmiany konfiguracji, o ile pojawi się
taka konieczność.

136 Rozdział 4.
Cykl życia aktywności

Albo zapisanie bieżącego stanu…


Znacznie lepszym sposobem obsługi zmian konfiguracji, którego
będziemy używali zdecydowanie najczęściej, jest zapisywanie bieżącego Aktywność
stanu aktywności, a następnie odtwarzanie go w metodzie onCreate(). uruchomiona
Aby zapisać bieżący stan aktywności, konieczne jest zaimplementowanie
metody onSaveInstanceState(). Metoda ta jest wywoływana przed
usunięciem aktywności, co oznacza, że zapewnia nam możliwość
zapisania wszelkich wartości, których nie chcemy utracić. onCreate()

Metoda onSaveInstanceState() ma jeden parametr — obiekt klasy


Bundle. Klasa Bundle pozwala gromadzić w jednym obiekcie dane
różnych typów:
Aktywność
public void onSaveInstanceState(Bundle savedInstanceState) { działająca
}
Metoda
onSaveInstanceState()
Obiekt klasy Bundle jest także przekazywany jako parametr do metody jest wywoływana przed
onCreate(). Oznacza to, że jeśli zapiszemy wartości zmiennych seconds metodą onDestroy().
i running w obiekcie Bundle, to metoda onCreate() będzie w stanie je Zapewnia nam ona onSaveInstanceState()
możliwość zapisania
odczytać i zastosować podczas odtwarzania aktywności. Do umieszczania stanu aktywności przed
informacji w obiekcie Bundle służą metody, które zapisują w nim pary jej usunięciem.
nazwa – wartość. Metody te mają następującą postać: onDestroy()
bundle.put*(”nazwa”, wartość)

gdzie bundle jest nazwą zmiennej typu Bundle, * określa typ zapisywanej
wartości, a nazwa i wartość to odpowiednio nazwa i wartość zapisywanej
danej. Na przykład aby zapisać w obiekcie Bundle wartość typu int
Aktywność
przechowywanej w zmiennej seconds, należałoby użyć następującego usunięta
wywołania:

bundle.putInt(”seconds”, seconds);

W jednym obiekcie typu Bundle można zapisać wiele takich par danych.

Poniżej przedstawiliśmy kod metody onSaveInstanceState() używanej Stoper


w naszej aplikacji:

@Override app/src/main
public void onSaveInstanceState(Bundle savedInstanceState)
{ java
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running); com.hfad.stoper
Te wywołania
} zapisują wartości
zmiennych seconds Stopwatch
Skoro już zapisaliśmy wartość naszych zmiennych w obiekcie Bundle, i running w obiekcie
Activity.java
Bundle.
możemy z nich skorzystać w metodzie onCreate().

jesteś tutaj  137


Odtworzenie stanu

…i następnie odtworzenie stanu w onCreate()


Jak już wspominaliśmy wcześniej, metoda onCreate() ma jeden parametr —
obiekt Bundle. Jeśli aktywność jest tworzona po raz pierwszy, parametr ten
Stoper
będzie miał wartość null. Jeśli jednak aktywność jest odtwarzana i wcześniej
została wywołana metoda onSaveInstanceState(), to w wywołaniu metody
app/src/main
onCreate() zostanie przekazany ten sam obiekt Bundle, który wcześniej został
użyty w metodzie onSaveInstanceState():
java
protected void onCreate(Bundle savedInstanceState) {
com.hfad.stoper
...
}
Stopwatch
Activity.java
Dane z obiektu Bundle można pobierać, używając metod o postaci:

bundle.get*(”nazwa”);

gdzie bundle to nazwa zmiennej typu Bundle, * to określenie typu pobieranej


wartości, a nazwa nazwa pary, którą zapisaliśmy na poprzedniej stronie.
Na przykład aby pobrać z obiektu Bundle liczbę całkowitą (typu int)
o nazwie seconds, należałoby użyć następującego wywołania:

int seconds = bundle.getInt(”seconds”);

Łącząc to wszystko w jedną całość, uzyskamy naszą nową metodę onCreate(),


która będzie miała następującą postać:

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState); bierają
Te instrukcje po wartości
z obiek tu Bu nd le
setContentView(R.layout.activity_stopwatch); s i running.
zmiennych second
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(“seconds”);
running = savedInstanceState.getBoolean(“running”);
} Zrób to sam!
runTimer();
}

A jak ten kod działa w praktyce?


Upewnij się, że zaktualizowałeś
swoją metodę onCreate()
i dodałeś metodę
onSaveInstanceState().

138 Rozdział 4.
Cykl życia aktywności

Co się stanie po uruchomieniu aplikacji?


1 Użytkownik uruchamia aplikację i klika przycisk Start, by rozpocząć działanie
stopera.
Metoda runTimer() zaczyna inkrementować liczbę sekund wyświetlanych na ekranie
w widoku tekstowym o identyfikatorze time_view.

seconds=8

StopwatchActivity

Urządzenie running=true

2 Użytkownik obraca urządzenie.


Android uznaje to za zmianę konfiguracji i przygotowuje się do usunięcia aktywności.
Zanim to jednak zrobi, wywołuje metodę onSaveInstanceState(). Metoda ta zapisuje
wartości zmiennych seconds i running w obiekcie Bundle.

Zaraz mnie
usuną, muszę
was zapisać…

seconds=8

StopwatchActivity
Urządzenie
running=true

bundle
“seconds”=8
“running”=true

jesteś tutaj  139


Co się dzieje?

Historii ciąg dalszy


3 Android usuwa aktywność, a następnie ją odtwarza.
Zostaje wywołana metoda onCreate(), a do niej przekazywany jest obiekt typu Bundle.

Urządzenie
seconds=0

StopwatchActivity

bundle
running=false
“seconds”=8
“running”=true

4 Obiekt Bundle zawiera wartości zmiennych seconds i running, zapisane przed


usunięciem aktywności.
Kod metody onCreate() przywraca wcześniejsze wartości zmiennych seconds i running,
odczytując je z obiektu Bundle.

seconds=8

StopwatchActivity
Urządzenie
running=true

bundle
“seconds”=8
“running”=true

5 Zostaje wywołana metoda runTimer(), a stoper rozpoczyna działanie


w miejscu, w którym wcześniej zostało ono przerwane.
Czas zmierzony przez stoper zostaje wyświetlony na ekranie urządzenia.

140 Rozdział 4.
Cykl życia aktywności

Jazda próbna aplikacji


Wprowadź zmiany w kodzie swojej aktywności, a następnie uruchom aplikację. Kiedy klikniesz
przycisk Start, stoper zacznie mierzyć czas i nie zatrzyma się nawet po obróceniu urządzenia.

Kiedy obrócimy urządzenie,


stoper wciąż będzie działał.

Nie istnieją
głupie pytania

P: Dlaczego Android chce P: Dlaczego Android nie zapisuje P: Czy Bundle to jakiś rodzaj mapy
odtwarzać aktywność tylko dlatego, automatycznie wartości wszystkich zaimplementowanej w Javie?
że obróciliśmy ekran? zmiennych? Dlaczego muszę ten kod
pisać ręcznie? O: Nie, ale ta klasa została
O: Metoda onCreate() jest normalnie zaprojektowana, by działać podobnie do
używana do przygotowywania ekranu. Jeśli O: Może się zdarzyć, że nie będziesz klasy java.util.Map. Obiekty klasy
jej kod jest zależny od konfiguracji ekranu (na chciał zapisywać wartości wszystkich Bundle mają dodatkowe możliwości,
przykład jeśli używamy odmiennych układów zmiennych. Na przykład możesz używać którymi nie dysponują mapy, na przykład
dla urządzeń działających w orientacji zmiennej zawierającej bieżącą szerokość można je przekazywać pomiędzy
pionowej i poziomej), to będziemy chcieli, ekranu. Zapewne nie chciałbyś odtwarzać procesami. To naprawdę bardzo przydatna
by metoda onCreate() była wywoływana wartości tej zmiennej podczas ponownego możliwość, gdyż pozwala Androidowi mieć
po każdej zmianie konfiguracji. Co więcej, wywołania metody onCreate(). dostęp do bieżącego stanu aktywności.
możemy także chcieć zmieniać teksty
wyświetlane w interfejsie użytkownika
aplikacji, kiedy użytkownik zmieni wybrane
ustawienia lokalne.

jesteś tutaj  141


Widzialny cykl życia

Tworzenie i usuwanie to nie cały cykl życia aktywności


Dotychczas przyjrzeliśmy się wyłącznie dwóm etapom cyklu życia aktywności,
a mianowicie tworzeniu i usuwaniu, oraz pokazaliśmy, jak można obsługiwać
zmiany konfiguracji urządzenia, takie jak zmiana jego orientacji. Cykl życia
aplikacji zawiera jednak także inne zdarzenia, które mogą się nam przydać
do zapewnienia prawidłowego działania aplikacji.

Na przykład załóżmy, że w trakcie pomiaru czasu za pomocą naszej aplikacji Nawet jeśli wcale nie chcesz,
by aplikacja stopera działała
stopera zadzwoni telefon. W takim przypadku, pomimo tego, że stoper w taki sposób, to jest to doskonały
nie będzie widoczny, wciąż będzie działał. A co zrobić, gdybyśmy chcieli, pretekst do przedstawienia kolejnych
metod cyklu życia aktywności.
aby niewidoczny stoper był zatrzymywany i wznawiał pomiar czasu po jego
ponownym wyświetleniu?

Uruchamianie, zatrzymywanie i restartowanie


Na szczęście dzięki metodom cyklu życia aplikacji obsługa akcji związanych
z widzialnością aplikacji jest całkiem łatwa. Oprócz metod onCreate() Aktywność jest
i onDestroy(), związanych z ogólnie pojętym cyklem życia aktywności,
zawiera on także grupę metod związanych z jej widzialnością. zatrzymana, gdy jest
Istnieją trzy podstawowe metody wywoływane, gdy aktywność staje się całkowicie przesłonięta
widoczna lub gdy znika z ekranu. Są to: onStart(), onStop() oraz
onRestart(). Wszystkie te metody, podobnie jak onCreate() i onDestroy(), przez inną aktywność
są dziedziczone po klasie Activity. i nie jest widoczna
Metoda onStart() jest wywoływana, gdy aktywność staje się widoczna dla
dla użytkownika.
użytkownika.

Metoda onStop() jest wywoływana, gdy aktywność przestaje być widoczna


Taka aktywność
dla użytkownika. Może się to zdarzyć na przykład, kiedy aktywność zostanie wciąż istnieje w tle
całkowicie przesłonięta przez inną aktywność wyświetloną na ekranie albo
kiedy aktywność ma zostać usunięta. Jeśli metoda onStop() jest wywoływana i zachowuje wszystkie
w wyniku planowanego usunięcia aktywności, to przed nią zostanie wywołana
metoda onSaveInstanceState(). informacje o swoim
Metoda onRestart() jest wywoływana, gdy aktywność jest już niewidoczna, stanie.
ale ma zostać ponownie wyświetlona

Na następnej stronie przyjrzymy się dokładniej, jak te nowe metody cyklu


życia aktywności pasują do przedstawionych wcześniej metod onCreate()
i onDestroy().

142 Rozdział 4.
Cykl życia aktywności

Cykl życia aktywności: widzialny czas życia


Spróbujmy teraz narysować ten sam diagram cyklu życia aktywności, który pokazaliśmy
już wcześniej, uwzględniając na nim metody onStart(), onStop() oraz onRestart()
(te elementy diagramu, na których masz się skoncentrować, zostały wytłuszczone):

Aktywność 1 Aktywność jest uruchamiana i zostaje


wywołana jej metoda onCreate().
uruchomiona Wykonywany jest kod inicjalizujący aktywność,
umieszczony w jej metodzie onCreate().
1 Na tym etapie aktywność jeszcze nie jest widoczna
i nie wywołano jeszcze jej metody onStart().
onCreate()

2 Po wywołaniu metody onCreate() zostaje


wywołana metoda onStart(). To wywołanie
jest wykonywane, gdy aktywność ma zostać
2 onStart() wyświetlona.
Po wykonaniu metody onStart() użytkownik może
już zobaczyć aktywność wyświetloną na ekranie.
4
Aktywność onRestart() 3 Metoda onStop() jest wywoływana, gdy
działająca aktywność przestaje być widoczna dla
użytkownika.
Po wykonaniu metody onStop() aktywność nie
będzie już widoczna.
3 onStop()
4 Jeśli aktywność ponownie ma być wyświetlona,
to zostaje wywołana metoda onRestart(),
a po niej metoda onStart().
onDestroy() Aktywność może wielokrotnie przejść ten cykl,
5 o ile wielokrotnie będzie ukrywana i ponownie
wyświetlana.

Aktywność 5 I w końcu aktywność jest usuwana.


usunięta Zazwyczaj metoda onStop() jest wywoływana przed
wywołaniem metody onDestroy(), jeśli jednak
w urządzeniu jest wyjątkowo mało dostępnej
pamięci, to wywołanie onStop() może zostać
pominięte.

Jeśli w urządzeniu jest wyjątkowo


mało pamięci, to może się zdarzyć,
Obejrzyj to! że przed usunięciem aktywności nie
zostanie wywołana metoda onStop().

jesteś tutaj  143


Zatrzymywanie i uruchamianie

Musimy zaimplementować dwie dodatkowe metody cyklu życia


Aby zaktualizować naszą aplikację stopera, musimy teraz wykonać dwie
modyfikacje. Przede wszystkim musimy zaimplementować metodę onStop(),
która zatrzyma stoper, gdy stanie się on niewidoczny. Kiedy to już zrobimy, Stoper
będziemy musieli zaimplementować metodę onStart(), która uruchomi
stoper, gdy tylko aplikacja ponownie zostanie wyświetlona. Zacznijmy od app/src/main
metody onStop().
java
Implementacja metody onStop() stopera
Metodę onStop() klasy Activity można przesłonić, dodając do własnej klasy com.hfad.stoper
aktywności następujący fragment kodu:
Stopwatch
@Override Activity.java
protected void onStop() {
super.onStop();
}

Gdy przesłaniamy metody cyklu życia aktywności, bardzo ważne jest, by w ich
kodzie w pierwszej kolejności wywołać przesłanianą metodę klasy bazowej
— w naszym przypadku będzie to metoda onStop():

super.onStop();
W przypadku przesłaniania
Istnieje kilka ważnych powodów, by to robić. Po pierwsze, musimy się
upewnić, że aktywność wykona wszystkie czynności zaimplementowane
metod cyklu życia
w metodzie klasy bazowej. Po drugie, Android nigdy nam nie wybaczy aktywności koniecznie
pominięcia tej czynności — jeśli nie wywołamy metody klasy bazowej,
Android zgłosi wyjątek. musimy wywoływać
W przypadku naszej aplikacji chcemy, by wywołanie metody onStop() metody klasy bazowej.
spowodowało zatrzymanie stopera. W tym celu wystarczy, że przypiszemy
zmiennej running wartość false: Jeśli tego nie zrobimy,
@Override Android zgłosi wyjątek.
protected void onStop() {
super.onStop();
running = false;
}

A zatem nasz stoper już się zatrzymuje, gdy aktywność staje się niewidoczna.
Naszym kolejnym zadaniem jest uruchomienie stopera po ponownym
wyświetleniu aktywności.

144 Rozdział 4.
Cykl życia aktywności

Zaostrz ołówek
Teraz Twoja kolej. Zmień kod aktywności w taki sposób, że jeśli przed
wywołaniem metody onStop() stoper działał, to zacznie on działać
także po ponownym wyświetleniu aktywności.

public class StopwatchActivity extends Activity {


private int seconds = 0;
private boolean running;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stopwatch);
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
}
runTimer();
z
To jest pierwsza część kodu aktywności. Musis
} zaimplementować metodę onStart() i wprow
adzić
.
drobne modyfikacje w kodzie innych metod

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(”wasRunning”, wasRunning);
}

@Override
protected void onStop() {
super.onStop();
running = false;
}

jesteś tutaj  145


Zaostrz ołówek — rozwiązanie

Zaostrz ołówek
Teraz Twoja kolej. Zmień kod aktywności w taki sposób, że jeśli przed
Rozwiązanie wywołaniem metody onStop() stoper działał, to zacznie on działać
także po ponownym wyświetleniu aktywności.

public class StopwatchActivity extends Activity {


private int seconds = 0;
Dodaliśmy nową zmienną, wasRunning, w której
private boolean running; będziemy zapisywali informację, czy stoper działał przed
private boolean wasRunning; wywołaniem metody onStop(), żebyśmy w razie potrzeby
mogli go ponownie uruchomić, gdy aktywność znowu
zostanie wyświetlona.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stopwatch);
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(“wasRunning”);
}
runTimer(); Jeśli aktywność jest ponownie tworzona,
} to odtwarzamy stan zmiennej wasRunning.

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(“wasRunning”, wasRunning); Zapisujemy stan zmiennej
} wasRunning.

@Override
protected void onStop() {
super.onStop(); Rejestrujemy, czy w momencie wywoływania
wasRunning = running; metody onStop() stoper działał, czy nie.
running = false;
}

@Override
protected void onStart() {
super.onStart();
To jest implementacja metody
if (wasRunning) { onStart(). Jeśli stoper działał,
to niech dalej działa.
running = true;
}
}

146 Rozdział 4.
Cykl życia aktywności

Zaktualizowany kod aktywności StopwatchActivity


Zmodyfikowaliśmy kod naszej aktywności w taki sposób, że jeśli stoper działał, zanim
aktywność zniknęła z ekranu, to będzie kontynuował działanie po jej ponownym Stoper
wyświetleniu. Wprowadź wyróżnione zmiany do kodu swojej aktywności:

public class StopwatchActivity extends Activity { app/src/main


private int seconds = 0; Nowa zmienna, wasRunning, przechowuje
private boolean running; informację, czy przed wywołaniem metody java
onStop() stoper działał, czy nie. Dzięki niej
private boolean wasRunning; będziemy wiedzieć, czy należy kontynuować
działanie stopera, gdy aktywność ponownie com.hfad.stoper
zastanie wyświetlona.
@Override
protected void onCreate(Bundle savedInstanceState) { Stopwatch
super.onCreate(savedInstanceState); Activity.java
setContentView(R.layout.activity_stopwatch);
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(“wasRunning”);
}
runTimer(); Ta instrukcja odtwarza stan zmiennej wasRunning
} po ponownym utworzeniu aktywności.

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(“wasRunning”, wasRunning); Tu zapisujemy stan
} zmiennej wasRunning.

@Override
protected void onStop() {
super.onStop(); Ta instrukcja zapisuje, czy w momencie
wasRunning = running; wywoływania metody onStop() stoper działał,
czy nie.
running = false;
}

@Override
protected void onStart() {
super.onStart();
To jest implementacja metody
if (wasRunning) { onStart(). Jeśli wcześniej
running = true; stoper działał, to tu go
ponownie uruchamiamy.
}
}
...
jesteś tutaj  147
Co się dzieje?

Co się dzieje podczas działania aplikacji?


1 Użytkownik uruchamia aplikację i klika przycisk Start, by rozpocząć działanie stopera.
Metoda runTimer() zaczyna inkrementować liczbę sekund wyświetlanych na ekranie w widoku tekstowym
o identyfikatorze time_text.

seconds=16

Stopwatch running=true
Activity
Urządzenie
wasRunning=false

2 Użytkownik przechodzi do ekranu głównego urządzenia, przez co nasza aplikacja


przestaje być widoczna.
W efekcie zostaje wywołana metoda onStop(), zmiennej wasRunning zostaje przypisana wartość true,
zmiennej running wartość false, a aktywność przestaje inkrementować liczbę sekund.

seconds=16
Aktywność wciąż W metodzie onStop()
istnieje, choć nie jest zmiennej running
widoczna. zostaje przypisana
wartość false.
Stopwatch running=false
Activity
Urządzenie
wasRunning=true

3 Użytkownik ponownie wyświetla naszą aplikację.


Powoduje to wywołanie metody onStart(), która przypisuje zmiennej running wartość true,
dzięki czemu aktywność ponownie będzie inkrementować liczbę sekund.

seconds=16

W metodzie onStart()
zmiennej running
running=true przypisywana jest
Stopwatch wartość true.
Activity

Urządzenie
wasRunning=true

148 Rozdział 4.
Cykl życia aktywności

Jazda próbna aplikacji


Zapisz zmiany wprowadzone w kodzie aktywności, a następnie uruchom aplikację. Kiedy klikniesz
przycisk Start, stoper zacznie mierzyć upływający czas; kiedy aplikacja będzie niewidoczna, pomiar
zostanie zatrzymany, a ponowne wyświetlenie aplikacji spowoduje jego wznowienie.

Włączyliśmy stoper,
a następnie przeszliśmy
do ekranu głównego
urządzenia.
Na czas, w którym
aplikacja była
niewidoczna, stoper
został zatrzymany. Po ponownym wyświetleniu
aplikacji stoper wznowił
pomiar czasu.

Nie istnieją
głupie pytania

P: Czy zamiast tych dwóch metod moglibyśmy P: A czym różni się ta sytuacja?
zastosować metodę onRestart()?
O: W przypadku obrócenia urządzenia dotychczasowa
O : Metoda onRestart() jest używana w sytuacjach, w których aktywność jest usuwana, a na jej miejsce jest tworzona nowa.
chcemy wykonać jakiś kod, gdy aplikacja zostaje wyświetlona po Gdybyśmy zatem umieścili kod w metodzie onRestart(), to nie
jej wcześniejszym ukryciu. Nie jest ona natomiast wywoływana zostałby on wykonany po ponownym utworzeniu aktywności.
podczas wyświetlania aktywności po raz pierwszy. W naszym Metoda onStart() jest wywoływana w obu tych przypadkach.
przypadku chcieliśmy, by aplikacja cały czas działała według
założeń, nawet po obróceniu urządzenia.

jesteś tutaj  149


Cykl życia na pierwszym planie

A co się dzieje, jeśli aplikacja jest tylko częściowo widoczna?


Wiesz już, co się stanie, kiedy aktywność zostanie utworzona i usunięta, a także co się dzieje,
gdy aktywność jest wyświetlana i ukrywana. Jednak jest jeszcze jedna sytuacja, którą musimy wziąć
pod uwagę: kiedy aktywność jest widoczna, lecz nie dysponuje miejscem wprowadzania.

Kiedy aktywność jest widoczna, lecz nie dysponuje miejscem wprowadzania, jej działanie zostaje
wstrzymane. Może się to stać, gdy aktywność zostanie przesłonięta przez inną, która nie zajmuje całego
ekranu albo jest przezroczysta. Aktywność wyświetlona na wierzchu będzie dysponować miejscem
wprowadzania, lecz ta pod nią wciąż będzie widoczna, choć jej działanie zostanie wstrzymane.

Nasza aktywność
stopera wciąż
jest widoczna,
lecz została
częściowo
przesłonięta
i nie dysponuje
miejscem
wprowadzania.
Aktywność zmienia stan
na wstrzymaną, gdy
nie dysponuje miejscem
wprowadzania, lecz
cały czas jest widoczna
dla użytkownika. Taka
To jest aktywność aktywność cały czas żyje
należąca do
innej aplikacji, i zachowuje wszystkie
wyświetlona na
naszym stoperze. informacje o swoim
stanie.

Istnieją dwie metody cyklu życia aktywności, które są związane ze wstrzymywaniem jej działania
i jego wznawianiem: onPause() i onResume(). Metoda onPause() jest wywoływana, gdy
aktywność jest widoczna, lecz inna aktywność dysponuje miejscem wprowadzania. Z kolei metoda
onResume() jest wywoływana bezpośrednio przed momentem, gdy aktywność ponownie zacznie
prowadzić interakcję z użytkownikiem. Jeśli aplikacja musi w jakiś sposób reagować na wstrzymanie
i wznowienie działania aktywności, konieczne będzie zaimplementowanie tych dwóch metod.

Na następnej stronie zobaczysz, jak te dwie metody pasują do pozostałych, przedstawionych


wcześniej metod cyklu życia aplikacji.

150 Rozdział 4.
Cykl życia aktywności

Cykl życia aktywności: życie na pierwszym planie


Kontynuujmy tworzenie przedstawionego już wcześniej diagramu metod cyklu życia
aktywności, a konkretnie dodajmy do niego dwie metody: onPause() i onResume()
(te nowe elementy schematu zostały wyróżnione pogrubieniem):

1 Aktywność jest uruchamiana i zostają wywołane


Aktywność jej metody onCreate() i onStart().
W tym momencie aktywność jest już widoczna,
uruchomiona ale jeszcze nie dysponuje miejscem wprowadzania.

1 2 Po metodzie onStart() zostaje wywołana


metoda onResume(). Jest ona wywoływana przed
onCreate() przeniesieniem aktywności na pierwszy plan.
Po wywołaniu metody onResume() aktywność
dysponuje już miejscem wprowadzania i użytkownik
może prowadzić z nią interakcję.
onStart()
3 Kiedy aktywność przestaje być wyświetlana na
2 pierwszym planie, zostaje wywołana metoda
onPause().
onResume() Po wywołaniu metody onPause() aktywność wciąż jest
widoczna, lecz nie dysponuje miejscem wprowadzania.

4 Jeśli aktywność zostanie ponownie przeniesiona


do pierwszego planu, to zostanie wywołana
4 Aktywność onRestart() metoda onResume().
działająca Ten cykl może się powtarzać wielokrotnie, jeśli tylko
6
aktywność będzie na przemian tracić i ponownie
odzyskiwać miejsce wprowadzania.

onPause() Kiedy aktywność przestanie być widoczna


5
3 dla użytkownika, zostanie wywołana metoda
onStop().
Po wywołaniu metody onStop() aktywność nie jest
5 onStop() już widoczna dla użytkownika.

6 Jeśli aktywność ponownie stanie się widoczna,


onDestroy() zostanie wywołana jej metoda onRestart(),
a następnie dwie kolejne metody, onStart()
i onResume().
7 Także ten cykl wywołań może być wykonywany
wielokrotnie.
Aktywność
usunięta 7 I w końcu aktywność zostaje usunięta.
Kiedy aktywność ma zostać usunięta, najpierw
wywoływana jest metoda onPasue(). Zazwyczaj
wywoływana jest także metoda onStop().

jesteś tutaj  151


Obroty
Wcześniej opisywaliście,
jak aktywność jest usuwana
i ponownie tworzona w efekcie obrócenia
ekranu urządzenia. A co się stanie, kiedy
urządzenie zostanie obrócone w momencie,
gdy aplikacja będzie wstrzymana? Czy wówczas
wykonywane będą te same metody cyklu życia
aktywności?

To jest świetne pytanie, dlatego zanim wrócimy do naszej


aplikacji, przyjrzymy się temu zagadnieniu dokładniej.
W początkowej aktywności wywoływane są wszystkie metody cyklu jej życia,
zaczynając od onCreate(), a kończąc na onDestroy(). Nowa aktywność
jest tworzona po usunięciu początkowej. Ponieważ ta nowa aktywność nie
jest jeszcze wyświetlona na pierwszym planie, zostają wywołane tylko dwie
metody cyklu jej życia, a mianowicie onCreate() i onStart():

Początkowa aktywność
1 Użytkownik uruchamia aktywność.
Zostają wywołane metody onCreate(), onStart()
Aktywność oraz onResume() cyklu życia aktywności.
uruchomiona
2 Nasza aktywność jest przesłaniana przez inną.
onCreate() Zostaje wywołana metoda onPause() aktywności.

1 onStart() 3 Użytkownik obraca urządzenie.


Android zauważa zamianę konfiguracji
onResume() urządzenia. Zostają wywołane metody onStop()
i onDestroy(), po czym system usuwa aktywność.
Na jej miejsce jest tworzona nowa aktywność.
Aktywność
działająca 4 Aktywność jest widoczna, lecz nie na
pierwszym planie.
Zostają wywołane metody onCreate()
2 onPause() i onStart(). Ponieważ aktywność jest widoczna,
lecz nie dysponuje miejscem wprowadzania,
onStop() metoda onResume() nie zostanie wywołana.

onDestroy() Aktywność zastępcza

Aktywność
Aktywność uruchomiona
usunięta
onCreate()
3
onStart() 4
152 Rozdział 4.
Cykl życia aktywności

Widzę, że aktywność zastępcza nie przeszła do


stanu „działająca”, gdyż nie została wyświetlona
na pierwszym planie. Ale co by się stało, gdybyśmy
całkowicie przeszli do innej aktywności, tak że ta
nasza w ogóle nie byłaby widoczna? Jeśli aktywność
została zatrzymana, to czy przed metodą onStop()
są wywoływane metody onResume() i onPause()?

Aktywność
Aktywności mogą przechodzić uruchomiona
bezpośrednio od wywołania
onStart() do wywołania
onStop(), pomijając przy tym
onCreate()
wywołania metod onPause()
i onResume().

Jeśli aktywność jest widoczna, lecz


onStart()
nigdy nie znalazła się na pierwszym
planie ani nie uzyskała miejsca
wprowadzania, to jej metody
onPause() i onResume() nigdy nie onResume()
zostaną wywołane.

Metoda onResume() jest wywoływana,


kiedy aktywność zostaje wyświetlona
na pierwszym planie i uzyskuje Aktywność onRestart()
miejsce wprowadzania. Jeśli aktywność działająca
jest widoczna, lecz przesłonięta
innymi aktywnościami, to jej metoda
onResume() nie zostanie wywołana.
onPause()
I podobnie metoda onPause()
jest wywoływana, kiedy aktywność
przestaje być widoczna na pierwszym
planie. A zatem jeśli aktywność nigdy onStop()
nie była wyświetlona na pierwszym
planie, to jej metoda onPause() nie
zostanie wywołana.
onDestroy()
Jeśli aktywność zostanie zatrzymana lub
usunięta, zanim pojawi się na pierwszym
planie, to po wywołaniu metody onStart()
zostaje wywołana metoda onStop(). W takim
przypadku wywołania metod onResume()
i onPause() są pomijane. Aktywność
usunięta

jesteś tutaj  153


Zmiana kodu metod

Zatrzymanie stopera w razie


wstrzymania aktywności onStart()

Wróćmy do naszej aplikacji stopera.

Na razie zatrzymujemy stoper, kiedy aplikacja nie jest widoczna,


onResume()
a następnie wznawiamy jego działanie, gdy aplikacja ponownie
pojawi się na ekranie. Oprócz tego chcemy zatrzymywać stoper,
kiedy działanie aktywności zostanie wstrzymane, i ponownie go
uruchamiać po wznowieniu działania aktywności. Których metod
cyklu życia aktywności powinniśmy w tym celu użyć? Aktywność onRestart()
działająca
Najprościej byłoby stwierdzić, że musimy w tym celu
zaimplementować metody onPause() i onResume(). Możemy
jednak pójść o krok dalej. Użyjemy tych metod do zastąpienia
zaimplementowanych wcześniej metod onStop() i onStart().
onPause()
Jeśli ponownie przyjrzymy się diagramowi cyklu życia aktywności,
to zauważymy, że podczas zatrzymywania i uruchamiania
aktywności oprócz metod onStop() i onStart() zawsze
wywoływane są także metody onPasue() i onResume(). onStop()
A zatem, ponieważ chcemy, by aplikacja działała tak samo
w obu sytuacjach, użyjemy tych samych metod.
Stoper
Poniżej przedstawiliśmy naszą wersję metody onPause():
app/src/main
@Override
protected void onPause() { java
super.onPause();
wasRunning = running; com.hfad.stoper
running = false;
} Stopwatch
Activity.java
A to jest kod metody onResume():

@Override
protected void onResume() {
super.onResume();
Zrób to sam!
if (wasRunning) {
running = true;
}
} W swoim kodzie zastąp
metody onStop() i onStart()
Przekonajmy się teraz, co się stanie po uruchomieniu aplikacji. przedstawionymi obok
metodami onPause() i onResume().

154 Rozdział 4.
Cykl życia aktywności

Co się stanie po uruchomieniu aplikacji?


1 Użytkownik uruchamia aplikację i klika przycisk Start, by rozpocząć mierzenie czasu.
Metoda runTimer() zaczyna inkrementować liczbę sekund, która jest wyświetlana w widoku tekstowym
o identyfikatorze time_view.

seconds=15

running=true
Stopwatch
Activity
Urządzenie
wasRunning=false

2 Inna aktywność zostaje wyświetlona na pierwszym planie, przez co nasza aktywność


StopwatchActivity staje się tylko częściowo widoczna.
Zostaje wywołana metoda onPause(), zmiennej wasRunning zostaje przypisana wartość true,
zmiennej running wartość false, a zmienna seconds przestaje być inkrementowana.

seconds=15
Aktywność zostaje
wstrzymana, gdyż W metodzie
jest widoczna, ale onPause() zmiennej
nie na pierwszym running zostaje
running=false przypisana wartość
planie. Stopwatch false.
Activity

Urządzenie
wasRunning=true

3 Kiedy aktywność StopwatchActivity ponownie zostaje wyświetlona na pierwszym planie,


wywoływana jest jej metoda onResume(), zmiennej running jest przypisywana wartość true,
a zmienna seconds ponownie zaczyna być inkrementowana.

seconds=15

W metodzie onResume()
zmiennej running zostaje
running=true przypisana wartość true.
Stopwatch
Activity

Urządzenie
wasRunning=true

jesteś tutaj  155


Jazda próbna

Jazda próbna aplikacji


Zapisz zmiany wprowadzone w kodzie aktywności, a następnie uruchom aplikację.
Kiedy klikniesz przycisk Start, stoper zacznie mierzyć upływające sekundy; pomiar
zostanie zatrzymany po częściowym przesłonięciu naszej aktywności przez jakąś inną
aktywność, a po jej ponownym wyświetleniu na pierwszym planie pomiar zostanie
wznowiony.

Uruchomiliśmy nasz stoper.


Jego działanie zostało
wstrzymane po częściowym
przesłonięciu aktywności
przez inną.

Nasz stoper wznowił


działanie, kiedy aktywność
ponownie została wyświetlona
na pierwszym planie.

Nie istnieją
głupie pytania

P: Niektóre z metod cyklu życia aktywności Metody onCreate() i onStart() zawsze będą wywoływane
w odpowiednim czasie, a to oznacza, że nasza aplikacja może
nie zawsze są wywoływane. Czy to nie może być
zadbać o to, by na początku jej działania wszystko z nią było
przyczyną dziwacznego działania aplikacji?
w porządku. A to jest znaczenie ważniejsze.
O: W niektórych okolicznościach Android może pominąć Najważniejsze jest jednak, by dobrze zrozumieć, które
wywołania metod onStop() i onPause(). Zazwyczaj metody cyklu życia aplikacji są wywoływane w różnych
zawierają one kod wykonujący operacje porządkowe. okolicznościach.

156 Rozdział 4.
Cykl życia aktywności

Kompletny kod aktywności


Poniżej przedstawiliśmy kompletny kod aktywności zapisany
w pliku StopwatchActivity.java:

package com.hfad.stoper;
Stoper
import android.os.Bundle;
import android.os.Handler;
app/src/main
import android.app.Activity;
import android.view.View;
java
import android.widget.TextView;

com.hfad.stoper
public class StopwatchActivity extends Activity {
// Liczba sekund wyświetlana przez stoper
private int seconds = 0; Stopwatch
Zmiennych seconds, running i wasRunning Activity.java
// Czy stoper działa? używamy odpowiednio do przechowywania liczby
private boolean running; zmierzonych sekund, określenia, czy stoper działa,
i określenia, czy stoper działał w momencie
private boolean wasRunning; wstrzymywania aktywności.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); Ten fragment kodu odtwarza
setContentView(R.layout.activity_stopwatch); stan stopera w razie usunięcia
i ponownego utworzenia
if (savedInstanceState != null) { aktywności.
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(”wasRunning”);
}
runTimer();
}

Jeśli aktywność zostaje wstrzymana,


@Override to zatrzymujemy stoper.
protected void onPause() {
super.onPause();
wasRunning = running;
running = false;
}

@Override Jeśli działanie aktywności


zostaje wznowione i podczas jej
protected void onResume() { wstrzymywania stoper działał,
super.onResume(); to teraz go uruchamiamy. Dalsza część kodu
if (wasRunning) { aktywności została
zamieszczona na
running = true; następnej stronie.
}
} jesteś tutaj  157
Kod aktywności StopwatchActivity

Kod aktywności (ciąg dalszy) Ta metoda zapisuje stan stopera,


jeśli aktywność ma zostać usunięta.
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(”wasRunning”, wasRunning);
}

// Metoda uruchamia stoper po kliknięciu przycisku Start


public void onClickStart(View view) {
running = true;
Ta metoda jest wywoływana w odpowiedzi
} na kliknięcie przycisku Start.

// Metoda zatrzymuje stoper po kliknięciu przycisku Stop


public void onClickStop(View view) {
Ta metoda jest wywoływana w odpowiedzi
running = false; na kliknięcie przycisku Stop.
}

// Metoda zeruje stoper po kliknięciu przycisku Kasuj


public void onClickReset(View view) { Ta metoda jest wywoływana w odpowiedzi
running = false; na kliknięcie przycisku Kasuj.
seconds = 0; Metoda runTimer() używa obiektu
} Handler do inkrementacji liczby sekund
i wyświetlania ich w widoku tekstowym.
// Wyświetla w stoperze liczbę sekund
private void runTimer() {
final TextView timeView = (TextView)findViewById(R.id.time_view);
final Handler handler = new Handler();
handler.post(new Runnable() {
Stoper
@Override
public void run() {
int hours = seconds/3600; app/src/main
int minutes = (seconds%3600)/60;
int secs = seconds%60; java
String time = String.format(”%d:%02d:%02d”, hours, minutes, secs);
timeView.setText(time); com.hfad.stoper
if (running) {
seconds++; Stopwatch
} Activity.java
handler.postDelayed(this, 1000);
}
});
}
}

158 Rozdział 4.
Cykl życia aktywności

Bądź aktywnością ...


class MyActivity extends Activity{
Po prawej stronie przedstawiliśmy kod
pewnej aktywności. Twoim zadaniem protected void onCreate(
jest wcielić się w rolę tej aktywności Bundle savedInstanceState) {
i podać, który kod zostanie // Wykonujemy kod A
A
wykonany w każdej ...
z poniższych sytuacji. }
Kod, który masz brać
pod uwagę, zaznaczyliśmy protected void onPause() {
literami w kółkach. Aby ułatwić // Wykonujemy kod B
Ci rozwiązanie ćwiczenia, podaliśmy B
...
odpowiedź na pierwsze pytanie. }

protected void onRestart() {


Użytkownik uruchamia aktywność // Wykonujemy kod C
C
i zaczyna jej używać. ...
}
Zostają wykonane fragmenty kodu A, G i D.
Aktywność jest tworzona, następnie zostaje
wyświetlona na pierwszym planie i uzyskuje protected void onResume() {
miejsce wprowadzania. // Wykonujemy kod D
D ...
}
Użytkownik uruchamia aktywność,
zaczyna jej używać, a następnie przechodzi protected void onStop() {
do innej aktywności. // Wykonujemy kod E
E
...
}

protected void onRecreate() {


dne.
To jest tru // Wykonujemy kod F
F ...
Użytkownik uruchamia aktywność, }
zaczyna jej używać, obraca urządzenie,
przechodzi do innej aktywności, protected void onStart() {
a następnie wraca do początkowej. // Wykonujemy kod G
G ...
}

protected void onDestroy() {


// Wykonujemy kod H
H
...
}
}

jesteś tutaj  159


Rozwiązanie

Bądź aktywnością. Rozwiązanie ...


class MyActivity extends Activity{
Po prawej stronie przedstawiliśmy kod
pewnej aktywności. Twoim zadaniem protected void onCreate(
jest wcielić się w rolę tej aktywności Bundle savedInstanceState) {
i podać, który kod zostanie // Wykonujemy kod A
A
wykonany w każdej ...
z poniższych sytuacji. }
Kod, który masz brać
pod uwagę, zaznaczyliśmy protected void onPause() {
literami w kółkach. Aby ułatwić // Wykonujemy kod B
Ci rozwiązanie ćwiczenia, podaliśmy B
...
odpowiedź na pierwsze pytanie. }

Użytkownik uruchamia aktywność i zaczyna jej protected void onRestart() {


używać. // Wykonujemy kod C
C
Zostają wykonane fragmenty kodu A, G i D. Aktywność jest ...
tworzona, następnie zostaje wyświetlona na pierwszym planie }
i uzyskuje miejsce wprowadzania.

protected void onResume() {


// Wykonujemy kod D
Użytkownik uruchamia aktywność, zaczyna jej D ...
używać, a następnie przechodzi do innej aktywności.
}
Zostaną wykonane fragmenty kodu A, G, D, B i E. Aktywność
jest tworzona, następnie zostaje wyświetlona i uzyskuje miejsce
wprowadzania. Potem użytkownik przechodzi do innej aplikacji, protected void onStop() {
więc aktywność traci miejsce wprowadzania i nie jest już dłużej // Wykonujemy kod E
widoczna dla użytkownika. E
...
W cyklu życia aktywności
} nie ma metody o nazwie
onRecreate().
Użytkownik uruchamia aktywność, zaczyna jej
protected void onRecreate() {
używać, obraca urządzenie, przechodzi do innej
// Wykonujemy kod F
aktywności, a następnie wraca do początkowej. F ...
Zostaną wykonane fragmenty kodu A, G, D, B, E, H, A, G, D, B, E,
}
C, G i D. Aktywność jest tworzona, następnie zostaje wyświetlona
i uzyskuje miejsce wprowadzania. Kiedy urządzenie zostaje
obrócone, aktywność traci miejsce wprowadzania, przestaje być protected void onStart() {
widoczna i zostaje usunięta. Potem jest ponownie tworzona, staje // Wykonujemy kod G
się widoczna i uzyskuje miejsce wprowadzania. Gdy użytkownik G ...
przechodzi do innej aplikacji, a następnie wraca do tej, aktywność
traci miejsce wprowadzania, jest ukrywana, po czym zostaje }
ponownie wyświetlona i uzyskuje miejsce wprowadzania.
protected void onDestroy() {
// Wykonujemy kod H
H
...
}
}

160 Rozdział 4.
Cykl życia aktywności

Wygodny przewodnik po metodach cyklu życia aktywności

Metoda Kiedy jest wywoływana? Następna metoda

onCreate() Gdy aktywność jest tworzona po raz pierwszy. Można jej używać onStart()
do normalnej, statycznej inicjalizacji, takiej jak tworzenie widoków.
Metoda udostępnia także obiekt Bundle, zapewniający dostęp do
zapisanego wcześniej stanu aktywności.

onRestart() Gdy aktywność została wcześniej zatrzymana, bezpośrednio przed onStart()


jej ponownym uruchomieniem.

onStart() Gdy aktywność staje się widoczna. Po niej jest wywoływana metoda onResume()
onResume(), jeśli aktywność ma zostać wyświetlona na pierwszym lub onStop()
planie, lub metoda onStop(), jeśli aktywność ma zostać ukryta.

onResume() Gdy aktywność jest wyświetlana na pierwszym planie. onPause()

onPause() Gdy aktywność nie jest już wyświetlana na pierwszym planie, onResume()
ponieważ ma zostać wznowione działanie innej aktywności. lub onStop()
Następna aktywność zostanie uruchomiona dopiero po wykonaniu
tej metody, zatem musi ona działać bardzo szybko. Jeśli aktywność
ma być ponownie wyświetlona na pierwszym planie, to w następnej
kolejności wywoływana jest metoda onResume(), jeśli natomiast
aktywność jest ukrywana, to kolejną wywoływaną metodą będzie
onStop().

onStop() Gdy aktywność nie jest już widoczna. Przyczyną może być onRestart()
przesłonięcie danej aktywności przez inną bądź też planowane lub onDestroy()
usunięcie aktywności. Jeśli aktywność ma zostać ponownie
wyświetlona, to po tej metodzie zostanie wywołana metoda
onRestart(), jeśli natomiast aktywność ma zostać usunięta,
to następną wywołaną metodą będzie onDestroy().

onDestroy() Gdy aktywność ma zostać usunięta lub gdy kończy działanie. żadna

jesteś tutaj  161


Przybornik

Twój przybornik do Androida Pełny kod przykładowe


aplikacji prezen tow ane
j
j
Rozdział 4.

ale mo żes z
w tym rozdzi
Opanowałeś już rozdział 4. i dodałeś pobrać z ser we ra FT P
do swojego przybornika z narzędziami wydawnictwa Helion:
klady/
znajomość metod tworzących cykl życia ftp://ftp.helion.pl/przy
aktywności. andrrg.zip

Aktywność
CELNE SPOSTRZEŻENIA uruchomiona
 Każda aplikacja domyślnie działa we własnym procesie.
 Tylko główny wątek aplikacji może aktualizować jej
interfejs użytkownika. onCreate()
 Do planowania wykonania kodu lub przekazywania
go do innego wątku można używać obiektów klasy
Handler. onStart()
 Zmiana konfiguracji urządzenia skutkuje usunięciem
i ponownym utworzeniem aktywności.
 Aktywność dziedziczy metody cyklu życia po klasie onResume()
Activity. W razie przesłaniania którejkolwiek z tych
metod należy wywołać przesłanianą metodę klasy
bazowej.
 Metoda onSaveInstanceState(Bundle) pozwala Aktywność onRestart()
zapisać stan aktywności przed jej usunięciem. Tego działająca
samego obiektu Bundle można użyć w metodzie
onCreate() do odtworzenia stanu aktywności.
 Do zapisywania wartości w obiekcie Bundle służą
onPause()
metody bundle.put*(”nazwa”, wartosc).
Wartości te można następnie odczytywać, używając
metod bundle.get*(”nazwa”).
 Metody onCreate() i onDestroy() obsługują onStop()
odpowiednio narodziny i śmierć aktywności.
 Metody onRestart(), onStart() oraz onStop()
są związane ze zmianami widzialności aktywności. onDestroy()
 Metody onResume() i onPause() są związane
z uzyskiwaniem i traceniem przez aktywność miejsca
wprowadzania.
Aktywność
usunięta

162 Rozdział 4.
5. Interfejs użytkownika

Podziwiaj widoki

Zapamiętaj: layout_row=”18”
i layout_column=”56”.
A nie: „za tym białym”.

Nie masz innego wyjścia, musisz tworzyć szałowe układy.


Jeśli chcesz pisać aplikacje, których inni będą używać, musisz zadbać o to, by wyglądały one
dokładnie tak, jak sobie tego życzysz. Zagadnienie tworzenia układów potraktowaliśmy
dotychczas bardzo powierzchownie, najwyższy zatem czas, by przyjrzeć mu się dokładniej.
W tym rozdziale pokażemy Ci różne typy układów, które można tworzyć, i zabierzemy Cię na
wycieczkę po najważniejszych komponentach GUI i sposobach ich stosowania. Pod koniec tego
rozdziału przekonasz się, że choć wszystkie te układy i komponenty wyglądają nieco inaczej,
to jednak mają ze sobą więcej wspólnego, niż można by przypuszczać.

to jest nowy rozdział  163


Interfejs użytkownika

Interfejs użytkownika aplikacji


składa się z układów i komponentów GUI
Jak już wiesz, układ definiuje, jak wygląda ekran aplikacji, a postać układu definiuje się, używając
kodu XML. Układy zazwyczaj zawierają komponenty użytkownika, takie jak przyciski i pola
tekstowe. Użytkownik prowadzi z nimi interakcje i sprawia, że aplikacja wykonuje jakieś operacje.

Wszystkie układy przedstawione w poprzednich rozdziałach książki wykorzystywały układy


względne, istnieją jednak także inne typy układów, których możemy używać, by nasze aplikacje
wyglądały dokładnie tak, jak byśmy sobie tego życzyli.

W tym rozdziale pokażemy kilka innych układów, których możesz używać, tworząc
aplikacje, oraz parę dodatkowych komponentów GUI, dzięki którym Twoje
aplikacje staną się bardziej interaktywne. Zacznijmy od układów.

164 Rozdział 5.
Interfejs użytkownika

Trzy kluczowe układy: względny, liniowy i siatki


Dostępnych jest kilka rodzajów układów, a każdy ma odmienne zasady rozmieszczania
widoków. Poniżej przedstawiliśmy trzy najważniejsze spośród tych układów. Na razie nie
przejmuj się szczegółami ich działania — wyjaśnimy je dokładnie na kilku następnych
stronach książki.

Widoki mogą być


rozmieszczane
względem układu
RelativeLayout nadrzędnego…

Układ względny, jak sama nazwa wskazuje,


rozmieszcza widoki w sposób względy. W jego
…bądź względem
przypadku położenie widoku definiowane jest innych widoków.
względem innego widoku należącego do układu bądź
względem układu nadrzędnego. Na przykład możemy
określić położenie pola tekstowego względem górnej
krawędzi układu nadrzędnego, pole typu Spinner Kolejne widoki są
wyświetlić poniżej pola tekstowego, a przycisk powyżej rozmieszczane jeden obok
drugiego, bądź to w pionie,
dolnej krawędzi układu. bądź w poziomie.

LinearLayout
Układ liniowy umieszcza poszczególne widoki
jeden obok drugiego, w pionie lub w poziomie. Jeśli
układ rozmieszcza widoki w pionie, to utworzą one
pojedynczą kolumnę. Jeśli natomiast układ rozmieszcza
widoki w poziomie, to utworzą one pojedynczy wiersz.

GridLayout Ekran jest dzielony


Układ siatki dzieli ekran na siatkę składającą się na wiersze i kolumny,
a podczas dodawania
z wierszy, kolumn oraz komórek. Tworząc układ tego do niego widoków
typu, określamy, z ilu kolumn ma się składać siatka, określamy, w której
gdzie mają być umieszczone poszczególne widoki oraz komórce powinny się
znaleźć i ile komórek
ile wierszy lub kolumn ma zajmować każdy z nich. zajmą.

jesteś tutaj  165


Układ względny

¨  RelativeLayout
Układ RelativeLayout rozmieszcza widoki ¨  LinearLayout
w sposób względny ¨  GridLayout

Jak już wiesz, układ względy pozwala określać położenie widoków względem układu
lub względem innych widoków w nim umieszczonych.

Układ względny definiuje się przy użyciu elementu <RelativeLayout>, takiego jak ten
przedstawiony poniżej:

Ten element <RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”


informuje system, android:layout_width=”match_parent” Atrybuty layout_width i layout_height
że używamy określają, jak szeroki i wysoki ma być
układu względnego. android:layout_height=”match_parent” definiowany układ.
...>
W elemencie można umieszczać także inne znaczniki.
...
</RelativeLayout>

Atrybut xmlns:android służy do określenia przestrzeni nazw Androida i zawsze


musi mieć wartość ”http://schemas.android.com/apk/res/android”.

Szerokość i wysokość układu MUSZĄ zostać określone


Atrybuty android:layout_width i android:layout_height
określają, jak szeroki i wysoki ma być układ. Te atrybuty są
obowiązkowe dla wszystkich rodzajów układów i widoków. Dla maniaków
Atrybutom android:layout_width i android:layout_height
można przypisać wartość ”match_partent”, ”wrap_content” bądź Co to są piksele niezależne od gęstości?
konkretny rozmiar, na przykład 10dp, czyli 10 pikseli niezależnych Niektóre urządzenia mają bardzo małe
od gęstości. Wartość ”wrap_content” oznacza, że układ ma być piksele, dzięki czemu mogą wyświetlać
na tyle duży, by pomieścił całą swoją zawartość, natomiast wartość bardzo ostre obrazy. Z kolei inne urządzenia
”match_parent” oznacza, że chcemy, by układ był tak duży jak są tańsze w produkcji, gdyż ich piksele są
jego element nadrzędny — w naszym przypadku tak duży jak ekran większe i jest ich mniej. Pikseli niezależnych
urządzenia pomniejszony o wypełnienie. Jak można się przekonać, od gęstości (dp) można używać do
podczas definiowania układów zazwyczaj są używane wartości tworzenia interfejsów użytkownika, które
”match_parent”. nie będą zbyt małe na jednych urządzeniach,
a zbyt duże na innych. Wielkości wyrażone
Czasami można się także spotkać z układami, w których w pikselach niezależnych od gęstości są
atrybutom android:layout_width i android:layout_height są w przybliżeniu takie same na wszystkich
urządzeniach.
przypisywane wartości ”fill_parent”. Wartość ta była stosowana
we wcześniejszych wersjach systemu Android, lecz obecnie zamiast
niej należy używać ”match_parent”. Wartość ”fill_parent” jest
uznawana za przestarzałą.

166 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Dodawanie wypełnienia ¨  LinearLayout
¨  GridLayout
Jeśli chcesz, aby wokół układu pozostało troszkę wolnego miejsca,
możesz zastosować atrybuty padding. Pozwalają one określać,
ile miejsca chcemy pozostawić pomiędzy daną krawędzią układu
a jego elementem nadrzędnym. Poniżej pokazaliśmy, w jaki sposób
można nakazać zastosowanie wypełnienia o szerokości 16dp wokół
wszystkich krawędzi układu: paddingTop

<RelativeLayout ...
android:paddingBottom=”16dp” paddingLeft
android:paddingLeft=”16dp”
android:paddingRight=”16dp” Dodajemy wypełnienie Układ
android:paddingTop=”16dp”> o wielkości 16 dp.

...
</RelativeLayout>

Atrybuty android:padding* są opcjonalne i można ich używać w dowolnych paddingBottom paddingRight


układach i widokach.

W powyższym przykładzie określiliśmy wypełnienie na stałe, nadając mu


wielkość 16dp. Alternatywnym rozwiązaniem może być określenie wielkości
wypełnień w pliku zasobów wymiarów. Użycie tego pliku ułatwia spójne
stosowanie wypełnień we wszystkich układach używanych w aplikacji.

Aby użyć pliku zasobów wymiarów, w atrybutach wypełnienia należy podać


nazwy zasobów wymiarów, w sposób pokazany w poniższym przykładzie: Atrybutom paddingLeft
i paddingRight zostaje
przypisana wartość @dimen/
<RelativeLayout ... activity_horizontal_margin.
android:paddingLeft=”@dimen/activity_horizontal_margin”
android:paddingRight=”@dimen/activity_horizontal_margin”
android:paddingTop=”@dimen/activity_vertical_margin” Atrybutom paddingTop
i paddingBottom zostaje
android:paddingBottom=”@dimen/activity_vertical_margin”> przypisana wartość @dimen/
activity_vertical_margin.
W tym przypadku Android odczyta wielkości wypełnień z pliku zasobów
wymiarów w trakcie działania aplikacji. Plik ten jest umieszczony
w katalogu app/src/main/res/values i nosi nazwę dimens.xml:

<resources>
<dimen name=”activity_horizontal_margin”>16dp</dimen> Układ odczyta wartości
wypełnień z pliku zasobów
<dimen name=”activity_vertical_margin”>16dp</dimen> wymiarów.
</resources>

Zazwyczaj ten plik jest tworzony automatycznie przez Android Studio


podczas tworzenia nowego projektu i dodawania do niego aktywności.

jesteś tutaj  167


Względem elementu nadrzędnego

¨  RelativeLayout
Rozmieszczanie widoków względem układu nadrzędnego ¨  LinearLayout
¨  GridLayout
W przypadku stosowania układu względnego konieczne jest określenie położenia danego
widoku względem innego widoku należącego do układu bądź względem elementu nadrzędnego.
Elementem nadrzędnym widoku jest układ, w którym dany widok jest umieszczony.

Jeśli chcemy, by widok zawsze był umieszczony w konkretnym miejscu ekranu, niezależnie
od jego wielkości i orientacji, to jego położenie powinniśmy określać względem elementu
nadrzędnego. Na przykład poniżej pokazaliśmy, w jaki sposób można zapewnić, aby przycisk
zawsze był wyświetlony w prawym górnym rogu układu:

<RelativeLayout ... > layout_alignParentTop


<Button
Układ zawiera android:layout_width=”wrap_content”
przycisk, zatem android:layout_height=”wrap_content”
układ jest Układ
elementem nadrzędny
android:text=”@string/click_me”
nadrzędnym Widok podrzędny.
przycisku. android:layout_alignParentTop=”true”
android:layout_alignParentRight=”true” />
</RelativeLayout>
layout_alignParentRight

Dwa poniższe wiersze kodu:

android:layout_alignParentTop=”true”
android:layout_alignParentRight=”true”

oznaczają, że górna krawędź przycisku ma zostać wyrównana do górnej


krawędzi układu, a prawa krawędź przycisku — do prawej krawędzi układu.
Przycisk będzie wyświetlany w zadany sposób niezależnie od wielkości
i orientacji ekranu.

Przycisk będzie wyświetlany


w prawym górnym rogu układu Jeśli w układzie zostały zdefiniowane
niezależnie do orientacji wypełnienia, to pomiędzy krawędziami
urządzenia i wielkości ekranu. widoku i ekranu będą widoczne odstępy.

168 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Atrybuty do umiejscawiania widoków ¨  LinearLayout
względem układu nadrzędnego ¨  GridLayout

Poniżej przedstawiliśmy kilka najczęściej używanych atrybutów, które służą do


umiejscawiania widoków względem ich układu nadrzędnego. Aby zastosować
wybrany atrybut, należy go dodać do elementu i przypisać mu wartość ”true”:

android:atrybut=”true”

Atrybut Działanie

android: Wyrównuje dolną krawędź widoku Widok jest wyrównany


do lewej i dolnej
layout_alignParentBottom do dolnej krawędzi elementu nadrzędnego. krawędzi elementu
nadrzędnego.

android: Wyrównuje lewą krawędź widoku


layout_alignParentLeft do lewej krawędzi elementu nadrzędnego.

android: Wyrównuje prawą krawędź widoku


layout_alignParentRight do prawej krawędzi elementu nadrzędnego.

Widok jest wyrównany


do prawej i górnej
android: Wyrównuje górną krawędź widoku krawędzi elementu
layout_alignParentTop do górnej krawędzi elementu nadrzędnego. nadrzędnego.

android: Umieszcza widok pośrodku elementu


layout_centerInParent nadrzędnego, zarówno w poziomie,
jak i w pionie.

android: Umieszcza widok pośrodku elementu


layout_centerHorizontal nadrzędnego w poziomie.

android: Umieszcza widok pośrodku elementu


layout_centerVertical nadrzędnego w pionie.

jesteś tutaj  169


Względem elementów sąsiednich

¨  RelativeLayout
Rozmieszczanie widoków względem innych widoków ¨  LinearLayout
¨  GridLayout
Oprócz rozmieszania widoków względem układu nadrzędnego można je także
rozmieszczać względem innych widoków. To rozwiązanie doskonale sprawdza się,
gdy chcemy, by grupa widoków była wyrównana w taki sam sposób, niezależnie
od wielkości i orientacji ekranu.

Aby rozmieścić jeden widok względem innego, w widoku stanowiącym punkt odniesienia
należy określić jego identyfikator, używając w tym celu atrybutu android:id:

android:id=”@+id/button_click_me”
Składnia ”@+id” nakazuje, by Android dodał identyfikator do pliku R.java jako zasób.
Jeśli w tym wyrażeniu pominiemy znak +, to Android nie doda identyfikatora jako
zasobu, a podczas próby skompilowania aplikacji wystąpią błędy.

Poniżej pokazaliśmy, w jaki sposób można utworzyć dwa przyciski, z których pierwszy
jest umieszczony pośrodku układu, a drugi — poniżej pierwszego:
tu
Używamy tego przycisku jako punka
odniesienia do określenia położeni
imy
<RelativeLayout ... > drugiego przycisku, dlatego mus
określić jego identyfikator.
<Button
android:id=”@+id/button_click_me”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerInParent=”true”
android:text=”@string/click_me” />
<Button Drugi przycisk ma być
umieszczony poniżej
android:layout_width=”wrap_content” pierwszego, tak by lewe
android:layout_height=”wrap_content” krawędzie obu przycisków
były wyrównane.
android:layout_alignLeft=”@+id/button_click_me”
android:layout_below=”@+id/button_click_me”
android:text=”@string/new_button_text” />
</RelativeLayout>

Poniższe dwa wiersze kodu:

android:layout_alignLeft=”@+id/button_click_me”
android:layout_below=”@+id/button_click_me”

zapewnią, że drugi przycisk będzie umieszczony poniżej pierwszego, a jego


lewa krawędź będzie wyrównana do lewej krawędzi pierwszego przycisku.

170 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Atrybuty do rozmieszczania widoków ¨  LinearLayout
względem innych widoków ¨  GridLayout

Poniżej przedstawiliśmy kolejne atrybuty, których można używać do rozmieszczania jednych


widoków względem innych. Atrybuty te należy dodawać do elementu, którego położenie
określamy, a ich wartością ma być identyfikator widoku stanowiącego punkt odniesienia:

android:atrybut=”@+id/identyfikator_widoku”
Twój widok jest
umieszczony powyżej.
Atrybut Działanie
android:layout_above Umieszcza widok powyżej widoku stanowiącego
punkt odniesienia.

Widok stanowiący
punkt odniesienia.
android:layout_below Umieszcza widok poniżej widoku stanowiącego
punkt odniesienia.
Twój widok jest
umieszczony poniżej.

android:layout_alignTop Wyrównuje górną krawędź widoku z górną krawędzią


widoku stanowiącego punktu odniesienia.
Wyrównuje górne krawędzie widoków.
Wyrównuje dolne krawędzie widoków.
android:layout_alignBottom Wyrównuje dolną krawędź widoku z dolną krawędzią
widoku stanowiącego punktu odniesienia.

Wyrównuje
android:layout_alignLeft Wyrównuje lewą krawędź widoku z lewą krawędzią lewe
widoku stanowiącego punktu odniesienia. krawędzie
widoków.

Wyrównuje
android:layout_alignRight Wyrównuje prawą krawędź widoku z prawą krawędzią prawe
widoku stanowiącego punktu odniesienia. krawędzie
widoków.

android:layout_toLeftOf Umieszcza prawą krawędź widoku przy lewej krawędzi


widoku stanowiącego punkt odniesienia.
Twój widok jest umieszczony
z lewej strony.

android:layout_toRightOf Umieszcza lewą krawędź widoku przy prawej krawędzi


widoku stanowiącego punkt odniesienia.
Twój widok jest umieszczony
z prawej strony.

jesteś tutaj  171


Marginesy

¨  RelativeLayout
Stosowanie marginesów do oddalania widoków od siebie ¨  LinearLayout
¨  GridLayout
Jeżeli zastosujesz przedstawione wyżej atrybuty do rozmieszczania widoków, układ nie będzie
pozostawiał pomiędzy nimi zbyt dużo wolnego miejsca. Aby powiększyć odstępy między
widokami, należy dodać do widoku jeden lub kilka marginesów.

Na przykład załóżmy, że chcemy wyświetlić w układzie dwa widoki i jednocześnie zapewnić,


aby były one oddalone od siebie o 50dp. W tym celu możemy dodać margines o wielkości
50dp u góry dolnego widoku:

<RelativeLayout ... >


<Button
android:id=”@+id/button_click_me”
... />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/button_click_me”
android:layout_below=”@+id/button_click_me”
android:layout_marginTop=”50dp” Dodanie marginesu
powyżej dolnego 50dp
android:text=”@string/button_below” /> przycisku spowodowało
pojawienie się odstępu
</RelativeLayout> między przyciskami.

Poniżej przedstawiliśmy listę marginesów, których można używać w celu dodawania


do układu odstępów między widokami. Te atrybuty dodaje się do widoku, a następnie
przypisuje wartość określającą wielkość marginesu:

android:atrybut=”10dp”

Atrybut Działanie
android:layout_marginTop Dodaje dodatkowy obszar nad widokiem.

android:layout_marginBottom Dodaje dodatkowy obszar pod widokiem.

android:layout_marginLeft Dodaje dodatkowy obszar z lewej strony widoku.

android:layout_marginRight Dodaje dodatkowy obszar z prawej strony widoku.

172 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
RelativeLayout — podsumowanie ¨  LinearLayout
¨  GridLayout
Zanim przejdziemy do następnego typu układów, podsumujmy jeszcze, w jaki sposób są
tworzone układy względne.

Sposób określania układów względnych


Układ względny tworzymy, używając elementu <RelativeLayout>. Musimy przy tym określić
wysokość i szerokość układu, ale ewentualne wypełnienia są opcjonalne:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”...>
...
</RelativeLayout>

Widoki można rozmieszczać względem układu lub innych widoków


Położenie poszczególnych widoków w układzie określamy poprzez dodawanie do nich
odpowiednich atrybutów. Te atrybuty mogą określić położenie widoku względem układu
nadrzędnego — na przykład w jego prawym dolnym rogu lub pośrodku. Jednak są także dostępne
atrybuty pozwalające określić położenie jednego widoku względem innego. Widok stanowiący
punkt odniesienia określamy, podając jego identyfikator.

Do widoków można dodawać marginesy, by zwiększać odstępy między nimi


Sam układ nie dodaje do umieszczanych w nim widoków żadnego wolnego obszaru wokół nich.
Obszar ten można jednak dodać, stosując marginesy:

android:layout_marginTop=”5dp”
android:layout_marginBottom=”5dp”
android:layout_marginLeft=”5dp”
android:layout_marginRight=”5dp”

Dotychczas używaliśmy wyłącznie układów względnych, istnieje


jednak także inny, powszechnie używany typ układów: układ liniowy.
Przyjrzymy mu się teraz nieco bliżej.

jesteś tutaj  173


Układ liniowy

¨  RelativeLayout
Układ LinearLayout wyświetla widoki ¨  LinearLayout
w jednym wierszu lub kolumnie ¨  GridLayout

Układ liniowy wyświetla widoki jeden obok drugiego, umieszczając je bądź to w pionie,
bądź w poziomie. W przypadku rozmieszczania elementów w pionie utworzą one
pojedynczą kolumnę, natomiast w przypadku wyświetlania ich w poziomie — jeden wiersz.

Sposób definiowania układu liniowego


Układ liniowy definiujmy, używając elementu <LinearLayout>, pokazanego w poniższym
przykładzie:

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent” To są te same atrybuty, których
Aby utworzyć układ używaliśmy już w układzie
android:layout_height=”match_parent” względnym.
liniowy, musisz
użyć elementu android:orientation=”vertical” Widoki mają być rozmieszczane
<LinearLayout>.
...> w pionie.
...
</LinearLayout>

W przypadku układu liniowego obowiązkowe jest zastosowanie atrybutów


android:layout_width, android:layout_height oraz android:orientation.
Pierwsze dwa określają odpowiednio szerokość i wysokość układu (dokładnie tak
samo, jak było w przypadku układu względnego), a trzeci pozwala określić, w jakim
kierunku mają być rozmieszczane widoki.

Widoki będą rozmieszczane w pionie, jeśli zastosujemy atrybut o postaci:

android:orientation=”vertical”
Widoki będą rozmieszczane w poziomie, jeśli zastosujemy atrybut o postaci: Układ liniowy o orientacji
poziomej.
android:orientation=”horizontal”

Układ
liniowy
o orientacji
pionowej.

W przypadku orientacji poziomej


poszczególne widoki tworzą jeden wiersz.

W przypadku orientacji
pionowej poszczególne
widoki tworzą jedną kolumnę.

174 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Układ liniowy wyświetla widoki w kolejności ¨  LinearLayout
ich występowania w kodzie XML ¨  GridLayout

Podczas definiowania układu liniowego poszczególne widoki należy dodawać


do niego w takiej kolejności, w jakiej chcemy, aby były wyświetlane. A zatem
jeśli chcemy, by komponent TextView był umieszczony nad przyciskiem,
to musimy dodać go do układu jako pierwszy:
Jeśli w kodzie XML zdefiniujesz komponent
<LinearLayout ... > TextView przed przyciskiem, to po wyświetleniu
układu będzie on widoczny nad przyciskiem.
<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/textView1” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/click_me” />
</LinearLayout>

Tworząc układ liniowy, identyfikatory widoków trzeba określać wyłącznie


w przypadku, gdy będziemy się do nich jawnie odwoływać w kodzie
aktywności. Wynika to z faktu, że układ liniowy określa, gdzie należy Atrybuty
umieścić poszczególne widoki, na podstawie ich kolejności w kodzie
XML. Widoki nie muszą odwoływać się do siebie nawzajem w celu
android:layout_width
określenia miejsca, w którym należy je wyświetlić. i android:layout_height
Podobnie jak w przypadku układu względnego, także w układzie liniowym trzeba obowiązkowo
można określać wymiary poszczególnych widoków, używając atrybutów
android:layout_width i android:layout_height. Poniższy kod: stosować we wszystkich
android:layout_width=”wrap_content” widokach, niezależnie od
oznacza, że szerokość widoku ma jedynie wystarczyć do wyświetlenia typu używanego układu.
jego zawartości — na przykład aby wyświetlić tekst na przycisku
lub w komponencie TextView. Z kolei ten kod:
Mogą one przyjmować
android:layout_width=”match_parent”
wartości warp_content,
oznacza, że widok ma zająć całą dostępną szerokość układu nadrzędnego.
match_parent lub
konkretny rozmiar,
na przykład 16 dp.

jesteś tutaj  175


Zmiana układu

¨  RelativeLayout
Zmieńmy nieco prosty układ liniowy ¨  LinearLayout
¨  GridLayout
Na pierwszy rzut oka układ liniowy może się wydawać bardzo prosty i mało elastyczny.
W końcu jedyne, co potrafi, to organizowanie widoków w ściśle określonej kolejności.
Aby jednak zyskać nieco więcej elastyczności, możemy modyfikować wygląd układu,
korzystając z jego dodatkowych atrybutów. Aby pokazać te dodatkowe możliwości,
spróbujmy nieco zmodyfikować bardzo prosty układ liniowy.

Nasz układ składa się z dwóch pól tekstowych i przycisku. W początkowej


wersji układu wszystkie te widoki są wyświetlone w pionie, jeden pod drugim,
jak pokazaliśmy na poniższym rysunku:

Każdy widok przyjmuje


możliwie jak
najmniejszą wysokość.

Zmienimy ten początkowy układ w taki sposób, by przycisk był


wyświetlony w prawym dolnym rogu układu, a cały pozostały
dostępny obszar zajmowało jedno z pól tekstowych.
Pole tekstowe Treść wiadomości
zostało znacznie powiększone.

Przycisk Wyślij jest teraz


wyświetlany w prawym
dolnym rogu ekranu.

176 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Początkowa wersja naszego układu liniowego ¨  LinearLayout
¨  GridLayout
Nasz układ liniowy składa się z dwóch pól tekstowych i przycisku. Przycisk ma etykietę
o treści Wyślij, natomiast pola tekstowe zawierają odpowiednio następujące teksty
podpowiedzi: Do i Treść wiadomości.

Tekst podpowiedzi pola tekstowego to napis, który jest wyświetlany w polu, gdy jest
ono puste. Ma on stanowić dla użytkownika podpowiedź sugerującą, co należy w pisać
w danym polu. Definiuje się go przy użyciu atrybutu android:hint:

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:orientation=”vertical”
tools:context=”.MainActivity” >
Szerokość pól tekstowych
jest równa szerokości układu
<EditText nadrzędnego.
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
Atrybut android:hint określa tekst podpowiedz
sugerujący, co użytkownik powinien wpisa i,
android:hint=”@string/to” /> ć
w danym polu.
Wartości
tych zasobów <EditText
łańcuchowych
zostały android:layout_width=”match_parent”
standardowo
zdefiniowane
android:layout_height=”wrap_content”
w pliku android:hint=”@string/message” />
strings.xml.

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/send” />
</LinearLayout>

Każdy z widoków umieszczonych w układzie będzie miał jak najmniejszą


wysokość — niezbędną do wyświetlenia jego zawartości. A zatem w jaki
sposób możemy zwiększyć wysokość pola do podania treści wiadomości?

jesteś tutaj  177


Przybieramy na wadze

¨  RelativeLayout
Rozciągaaaaamy widok, zwiększając jego wagę ¨  LinearLayout
¨  GridLayout
Wysokość wszystkich widoków w naszym przykładowym układzie jest możliwie
jak najmniejsza — zajmują one w pionie tylko tyle miejsca, ile jest niezbędne
do wyświetlenia ich zawartości. My chcielibyśmy natomiast, by pole przeznaczone
do podania treści widomości zajęło całą wysokość układu dostępną po
wyświetleniu pozostałych widoków.

Chcemy, by pole Treść


wiadomości zajęło całą
wysokość układu dostępną
po wyświetleniu pozostałych
widoków.

W tym celu musimy dodać temu polu tekstowemu nieco wagi (ang. weight).
Dodając widokowi wagę, informujemy, że ma on zająć nieco więcej miejsca
w układzie.

Do określania wagi widoku służy następujący atrybut:

android:layout_weight=”liczba”

gdzie liczba to liczba większa od 0.

W przypadku określenia wagi jakiegoś widoku układ w pierwszej kolejności


upewnia się, czy każdy widok dysponuje dostatecznie dużym obszarem,
koniecznym do wyświetlenia jego zawartości. Innymi słowy: upewnia się, czy każdy
przycisk ma miejsce na wyświetlenie umieszczonego na nim tekstu, każde pole
tekstowe — na wyświetlenie tekstu podpowiedzi, i tak dalej. Następnie układ
określa wielkość pozostałego obszaru i dzieli go proporcjonalnie, rozdzielając
pomiędzy elementy, których waga jest większa od 1.

178 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Dodawanie wagi do widoków ¨  LinearLayout
¨  GridLayout
Chcemy, by pole tekstowe do podawania treści wiadomości zajmowało jak najwięcej
miejsca w układzie. W tym celu dodamy do niego atrybut layout_weight i przypiszemy
mu wartość 1. Ponieważ będzie on jedynym widokiem, w którym określimy wagę,
zostanie on rozciągnięty w pionie i zajmie całą dostępną wysokość układu pozostałą
po wyświetleniu reszty widoków. Poniżej przedstawiliśmy zmodyfikowany kod układu:

<LinearLayout ... >


<Button>
<EditText Do tych elementów <EditText> i
liśm y atry butu layo ut_w eight.
nie doda
sca,
android:layout_width=”match_parent” Zajmą one zatem tylko tyle miej
android:layout_height=”wrap_content” ile jest konieczne do wyświetlenia
ich zawartości.
android:hint=”@string/to” />

<EditText
To jest jedyny
widok w układzie, android:layout_width=”match_parent” Wysokość tego widoku zostanie określona
w którym android:layout_height=”0dp” przez układ liniowy na postawie wartości
określiliśmy wagę. atrybutu layout_weight. Przypisanie atrybutowi
Zostanie on zatem android:layout_weight=”1” layout_height wartości 0 dp jest bardziej
rozciągnięty i zajmie efektywne niż użycie wartości „wrap_content”.
cały wolny obszar android:hint=”@string/message” />
układu, który nie
został zużyty przez
pozostałe widoki. <Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/send” />
</LinearLayout>

Przypisanie polu tekstowemu do podania treści wiadomości wagi 1 oznacza, Domyślnie tekst
że ma ono zająć cały dostępny obszar ekranu, który nie został zajęty przez podpowiedzi, Treść
inne widoki. Dzieje się tak dlatego, że do żadnego innego widoku nie wiadomości, jest
wyświetlany w połowie
dodaliśmy atrybutu layout_weight. wysokości pola tekstowego.
Zajmiemy się nim już
niebawem.
Pole do podania treści
wiadomości ma wagę 1.
Ponieważ jest to jedyny widok
układu, w którym została
określona waga, zostanie
ono rozciągnięte i zajmie
cały dostępny obszar układu
pozostały po wyświetleniu
reszty widoków.

jesteś tutaj  179


Więcej wagi

¨  RelativeLayout
Dodawanie wagi do większej liczby widoków ¨  LinearLayout
¨  GridLayout
W naszym przykładzie atrybut layout_weight dodaliśmy tylko do jednego widoku.
A co by się stało, gdyby takich widoków było więcej?

Załóżmy, że pierwszemu polu tekstowemu, Do, przypiszemy wagę 1, a drugiemu,


Treść wiadomości, przypiszemy wagę 2:

<LinearLayout ... >


...
<EditText
android:layout_width=”match_parent”
android:layout_height=”0dp”
android:layout_weight=”1”
android:hint=”@string/to” />

<EditText
android:layout_width=”match_parent”
android:layout_height=”0dp”
android:layout_weight=”2”
android:hint=”@string/message” />
...
Pole Do ma wagę 1, zatem
</LinearLayout> zostanie mu przydzielona
1/3 pozostałego miejsca
w układzie.

Aby określić, ile dodatkowego miejsca zajmie każdy z widoków,


musimy zacząć od zsumowania wartości atrybutów layout_weight
wszystkich widoków. W tym przypadku suma ta wyniesie 1+2=3.
Wielkość dodatkowego miejsca przydzielonego każdemu widokowi Pole Treść
będzie odpowiadała jego wadze podzielonej przez sumę wszystkich wiadomości ma
wagę 2, więc
wag. Pole Do ma wagę 1, zatem zostanie mu przydzielona 1/3 zostaną mu
pozostałego miejsca w układzie. Pole Treść wiadomości ma wagę 2, przydzielone 2/3
pozostałego miejsca
więc zostaną mu przydzielone 2/3 pozostałego miejsca. w układzie.

180 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Wykorzystanie ciężkości do określania ¨  LinearLayout
miejsca wyświetlania tekstu ¨  GridLayout

Kolejną zmianą, którą musimy wprowadzić, jest przesunięcie tekstu


podpowiedzi wyświetlanego w polu tekstowym do wpisywania treści
wiadomości. Obecnie jest on wyśrodkowany w pionie. Chcemy to jednak
zmienić, tak aby był on wyświetlany u góry pola. Taki efekt możemy uzyskać, Musimy przesunąć
stosując atrybut android:gravity. tekst podpowiedzi ze
środka na samą górę
widoku.
Atrybut android:gravity służy do określania, gdzie wewnątrz widoku ma
być wyświetlana jego zawartość — na przykład gdzie ma być wyświetlany tekst
w polu tekstowym. Jeśli chcemy, by tekst był wyświetlany u góry pola, to cała
sztuka sprowadza się do zastosowania poniższego atrybutu:

android:gravity=”top”
A zatem dodamy teraz atrybut android:gravity do pola tekstowego Treść
wiadomości, tak by prezentowany w nim tekst podpowiedzi był wyświetlony
przy jego górnej krawędzi:
<LinearLayout ... >
...
<EditText
android:layout_width=”match_parent”
android:layout_height=”0dp”
android:layout_weight=”1” Tekst wyświetlany w polu tekstowym
android:gravity=”top” będzie widoczny u góry tego pola.

android:hint=”@string/message” />
...
</LinearLayout>

Teraz tekst podpowiedzi


jest widoczny u góry
pola.
Jazda próbna
Dodanie atrybutu android:gravity do pola tekstowego służącego do
wpisywania treści wiadomości powoduje, że prezentowany w nim tekst
podpowiedzi jest widoczny u góry widoku — dokładnie tak jak chcieliśmy.

Na następnej stronie podaliśmy listę innych wartości, których można


używać w atrybucie android:gravity.

jesteś tutaj  181


Ciężkość

¨  RelativeLayout
Stosowanie atrybutu android:gravity ¨  LinearLayout
— lista wartości ¨  GridLayout

Poniżej przedstawiliśmy kilka kolejnych wartości, których można używać


w atrybucie android:gravity. Wystarczy dodać ten atrybut do wybranego
widoku i przypisać mu jedną z poniższych wartości:

android:gravity=”wartosc”

Wartość Działanie

top Zawartość jest wyświetlona u góry widoku.

bottom Zawartość jest wyświetlona u dołu widoku.

left Zawartość jest wyświetlona z lewej strony widoku.

right Zawartość jest wyświetlona z prawej strony widoku.

center_vertical Zawartość widoku jest wyśrodkowana w pionie.

center_horizontal Zawartość widoku jest wyśrodkowana w poziomie.

center Zawartość widoku jest wyśrodkowana w pionie i w poziomie.

fill_vertical Zawartość wypełnia całą dostępną wysokość widoku.

fill_horizontal Zawartość wypełnia całą dostępną szerokość widoku.

fill Zawartość wypełnia cały dostępny obszar widoku.

Atrybut android:gravity pozwala określić, w którym


miejscu widoku ma być wyświetlana jego zawartość.

182 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Przesunięcie przycisku w prawo ¨  LinearLayout
za pomocą atrybutu layout_gravity ¨  GridLayout

Jest jeszcze jedna modyfikacja, którą musimy wprowadzić w naszym układzie.


Przycisk Wyślij jest obecnie umieszczony w jego lewym dolnym rogu. Musimy
przesunąć go w prawo, tak aby był wyświetlony w prawym, dolnym rogu układu.
Do tego celu zastosujemy atrybut android:layout_gravity.

Atrybut android:layout_gravity pozwala określać, w którym miejscu


obszaru otaczającego widok w układzie liniowym ma być wyświetlany dany
widok. Możemy go zastosować, by na przykład przesunąć widok w prawo lub
wyśrodkować w poziomie. Aby przesunąć przycisk w prawo, wystarczy użyć
atrybutu o następującej postaci:

android:layout_gravity=”right”

Ale dlaczego musimy


używać atrybutu layout_
gravity? Wcześniej używaliśmy
atrybutu layout_alignRight — czy
nie możemy zatem użyć go i tym
razem?

Atrybut android:layout_alignRight można stosować 
wyłącznie w układach względnych

Układy mają kilka wspólnych atrybutów, takich jak android:layout_


width lub android:layout_height. Jednak wiele atrybutów jest
charakterystycznych dla jednego, konkretnego typu układów.

Większości atrybutów, które przedstawiliśmy, prezentując układy


względne, nie można używać w układach liniowych. W układach
liniowych wykorzystywane jest pojęcie ciężkości, dlatego jeśli chcemy
przesunąć widok na prawą stronę układu, musimy użyć poniższego
atrybutu:

android:layout_gravity=”right”

Na następnej stronie zamieściliśmy listę innych wartości, których


można używać w atrybucie android:layout_gravity.

jesteś tutaj  183


Atrybut layout_gravity

¨  RelativeLayout
Inne wartości, których można używać ¨  LinearLayout
w atrybucie android:layout_gravity ¨  GridLayout

Poniżej przedstawiliśmy inne wartości, których można używać w atrybucie


android:layout_gravity. Wystarczy dodać ten atrybut do widoku,
a następnie przypisać mu jedną z poniższych wartości:

android:layout_gravity=”wartosc”

Wartość Działanie

top, bottom, left, right Umieszcza widok u góry, u dołu, z lewej bądź z prawej strony
pojemnika, czyli obszaru, w którym się on znajduje.

start, end Umieszcza widok na początku bądź na końcu pojemnika,


w którym się on znajduje.

center_vertical, center_horizontal Wyśrodkowuje widok w pojemniku w pionie lub w poziomie.

center Wyśrodkowuje widok w pojemniku w pionie i w poziomie.

fill_vertical, fill_horizontal Rozszerza widok, tak by zajmował on całą dostępną wysokość


lub szerokość pojemnika.

fill Rozszerza widok, tak by zajmował on cały dostępny obszar


pojemnika.

Atrybut android:layout_gravity pozwala określać, w którym


miejscu dostępnego obszaru ma być wyświetlany widok.

Atrybut android:layout_gravity określa rozmieszczenie


samego widoku, natomiast atrybut android:gravity
informuje, w którym miejscu widoku należy wyświetlać
jego zawartość.

184 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Kompletny układ liniowy ¨  LinearLayout
¨  GridLayout
Oto kompletny kod naszego układu liniowego:

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:orientation=”vertical”
tools:context=”.MainActivity” >

<EditText
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:hint=”@string/to” />

<EditText
android:layout_width=”match_parent”
android:layout_height=”0dp”
android:layout_weight=”1”
android:gravity=”top”
android:hint=”@string/message” />
Zawartość pola do
<Button wprowadzania treści
wiadomości będzie
android:layout_width=”wrap_content” wyświetlana na samej górze
android:layout_height=”wrap_content” tego widoku. Dzięki temu
będziemy mieć naprawdę
android:layout_gravity=”right” dużo miejsca do wpisywania.
android:text=”@string/send” />
</LinearLayout>

Atrybut android:gravity różni się Przycisk Wyślij jest


od atrybutu android:layout_gravity. wyświetlony w prawym
Pierwszy z nich odnosi się do dolnym rogu układu.
zawartości widoku, drugi zaś do
samego widoku.

jesteś tutaj  185


Podsumowanie

¨  RelativeLayout
LinearLayout — podsumowanie ¨  LinearLayout
¨  GridLayout
Poniżej zamieściliśmy podsumowanie informacji o tworzeniu układów liniowych.

Sposób określania układów liniowych


Układ liniowy tworzymy, używając elementu <LinearLayout>. Musimy przy tym określić
szerokość, wysokość oraz orientację układu; podawanie wypełnienia jest opcjonalne:

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
...>
...
</LinearLayout>

Widoki są wyświetlane w kolejności dodawania


Podczas definiowania układu liniowego widoki dodajemy do niego w takiej kolejności,
w jakiej chcemy, aby były wyświetlane.

Widoki można rozciągać, używając wagi


Domyślnie wszystkie widoki zajmują tylko tyle miejsca, ile jest niezbędne do wyświetlenia ich
zawartości. Jeśli chcemy, by któryś z widoków zajął więcej miejsca, możemy go rozciągnąć,
używając atrybutu layout_weight:

android:layout_weight=”1”

Położenie zawartości w widoku można określać, używając ciężkości


Położenie zawartości widoku wewnątrz danego widoku możemy określać za pomocą atrybutu
android:gravity. W ten sposób możemy określić na przykład, gdzie wyświetlić tekst wewnątrz
pola tekstowego.

Do określania położenia widoku w otaczającym go obszarze służy


atrybut layout_gravity
Atrybut android:layout_gravity pozwala określać, w którym miejscu obszaru otaczającego
widok w układzie liniowym ma zostać wyświetlony dany widok. Na przykład można go używać,
by przesunąć widok w prawo lub wyśrodkować w poziomie.

To już wszystko, co chcieliśmy napisać na temat układów liniowych. Istnieje jeszcze


jeden typ układów, który poznamy: układ siatki.

186 Rozdział 5.
Interfejs użytkownika

Zaostrz ołówek
Poniżej zamieściliśmy kod XML układu aplikacji Doradca piwny, którą
napisaliśmy w rozdziale 2. Zmień go na układ liniowy, którego wygląd
pokazaliśmy u dołu strony.

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
tools:context=”.FindBeerActivity” >

<Spinner
android:id=”@+id/color”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentTop=”true”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”37dp”
android:entries=”@array/beer_colors” />

<Button
android:id=”@+id/find_beer”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/color”
android:layout_below=”@+id/color”
android:text=”@string/find_beer”
android:onClick=”onClickFindBeer” />

<TextView
android:id=”@+id/brands”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/find_beer”
android:layout_below=”@+id/find_beer”
android:layout_marginTop=”18dp”
najbardziej
android:text=”@string/brands” /> Ten układ nie wygra nagrody dla
czy potrafisz
stylowego układu, ale ciekawe,
</RelativeLayout> kod XML tak,
zmodyfikować przedstawiony tu
by uzyskać taki sam wygląd.

jesteś tutaj  187


Rozwiązanie zadania

Zaostrz ołówek
Rozwiązanie Poniżej zamieściliśmy kod XML układu aplikacji Doradca piwny, którą
napisaliśmy w rozdziale 2. Zmień go na układ liniowy, którego wygląd
pokazaliśmy u dołu strony.

<RelativeLinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
Zmień układ android:layout_width=”match_parent”
na liniowy.
android:layout_height=”match_parent”
android:paddingBottom=”16dp” Układ liniowy musi zawierać atrybut
android:paddingLeft=”16dp” android:orientation. Użyj wartości
android:paddingRight=”16dp” „horizontal”, aby wyświetlać widoki
jeden obok drugiego w poziomie.
android:paddingTop=”16dp”
android:orientation=”horizontal”
tools:context=”.FindBeerActivity” >

<Spinner
android:id=”@+id/color”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentTop=”true” Te wiersze nie będą nam już potrzebne.
android:layout_centerHorizontal=”true”
android:layout_marginTop=”37dp”
android:entries=”@array/beer_colors” />

<Button
android:id=”@+id/find_beer”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/color” Te wiersze nie będą nam już potr
zebne.
android:layout_below=”@+id/color”
android:text=”@string/find_beer”
android:onClick=”onClickFindBeer” />

<TextView
android:id=”@+id/brands”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignLeft=”@+id/find_beer” zebne.
Te wiersze nie będą nam już potr
android:layout_below=”@+id/find_beer”
Zmień układ android:layout_marginTop=”18dp”
na liniowy.
android:text=”@string/brands” />
</RelativeLinearLayout>

188 Rozdział 5.
Interfejs użytkownika

¨  RelativeLayout
Układ GridLayout wyświetla widoki w siatce ¨  LinearLayout
¨  GridLayout
Układ GridLayout dzieli ekran na siatkę składającą się z wierszy i kolumn
i pozwala umieszczać widoki we wskazanych komórkach:

Każdy Układ GridLayout 
z tych wymaga stosowania 
obszarów
jest
Obejrzyj to! API poziomu 14  
komórką. lub wyższego.

Jeśli planujesz stosowanie układu


siatki, upewnij się, czy Twoje
aplikacje będą używać wersji SDK
obsługującej API poziomu 14
lub wyższego.

Jak jest definiowany układ siatki?


Układ siatki definiuje się podobnie jak układy innych typów,
lecz należy przy tym użyć elementu <GridLayout>:

<GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent” To są te same atrybuty, których
używaliśmy w układach innych
Tu należy użyć android:layout_height=”match_parent” typów.
znaczników
<GridLayout>. android:columnCount=”2” Ten atrybut określa, ile kolumn
ma mieć siatka (w tym przypadk
... > u
będą dwie kolumny).
...
</GridLayout>

Liczba kolumn, z których ma się składać siatka, jest określana w następujący sposób:

android:columnCount=”liczba”

gdzie liczba określa liczbę kolumn. W podobny sposób możemy określić liczbę
wierszy siatki:

android:rowCount=”liczba”

choć w praktyce można ją pominąć i pozwolić, by Android sam ją obliczył na podstawie


liczby widoków umieszczonych w układzie. W takim przypadku Android utworzy tyle
wierszy, ile będzie potrzebnych do wyświetlenia wszystkich widoków.

jesteś tutaj  189


Układ siatki

Dodawanie widoków do układu siatki ¨  RelativeLayout


¨  LinearLayout
Widoki dodajemy do układu siatki podobnie jak dodaje się je do układu liniowego:
¨  GridLayout

<GridLayout ... >

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/textview” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/click_me” />

<EditText
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:hint=”@string/edit” />

</GridLayout>

Podobnie jak w przypadku układu liniowego, także w przypadku


stosowania układu siatki umieszczane w nim widoki nie muszą
mieć identyfikatorów, chyba że planujemy odwoływać się do
nich w kodzie aktywności. Poszczególne widoki umieszczane
w układzie nie muszą odwoływać się do siebie nawzajem, więc
stosowanie identyfikatorów nie jest niezbędne.

Domyślnie układ siatki rozmieszcza widoki w kolejnych


komórkach w takiej samej kolejności, w jakiej są one zapisane
w kodzie XML. A zatem jeśli nasz układ będzie się składał
z dwóch kolumn, to pierwszy widok zostanie umieszczony
w pierwszej komórce siatki, drugi w drugiej i tak dalej.

Wadą takiego rozwiązania jest to, że usunięcie widoku z układu


może doprowadzić do drastycznej zmiany jego wyglądu.
Rozwiązaniem tego problemu jest określanie, w którym miejscu
układu mają być wyświetlane poszczególne widoki i ile kolumn ma
zajmować każdy z nich.

190 Rozdział 5.
Interfejs użytkownika

Utwórzmy nowy układ siatki ¨  RelativeLayout


¨  LinearLayout
Aby przyjrzeć się temu rozwiązaniu w akcji, utwórzmy układ siatki, który
¨  GridLayout
będzie określał, gdzie należy umieścić poszczególne widoki i ile kolumn
ma zajmować każdy z nich. Nasz układ będzie się składał z komponentu
TextView prezentującego słowo „Do”, pola tekstowego zawierającego tekst
podpowiedzi „Wpisz adres e-mail”, drugiego pola tekstowego zawierającego
tekst podpowiedzi „Treść wiadomości” oraz przycisku z etykietą „Wyślij”:

Ten przykład jest podobny do


przedstawionego wcześniej ukła
du
liniowego, choć różni się od nieg
o
napisem wyświetlonym z lewej
strony pola tekstowego w górnym
wierszu oraz wyśrodkowanym
przyciskiem u dołu.

Oto, co mamy zamiar zrobić:


1 Naszkicować wygląd interfejsu użytkownika i podzielić go na wiersze
i kolumny.
W ten sposób będzie nam łatwiej wyobrazić sobie, jak należy zdefiniować układ.

2 Utworzyć układ wiersz po wierszu.

jesteś tutaj  191


Szkicujemy układ

Zaczynamy od naszkicowania układu ¨  RelativeLayout


¨  LinearLayout
Pierwszym etapem prac nad naszym nowym układem będzie jego naszkicowanie. Dzięki temu
¨  GridLayout
łatwiej nam będzie określić, z ilu wierszy i kolumn ma się on składać, gdzie należy umieścić
poszczególne widoki oraz ile kolumn powinien zajmować każdy z tych widoków.

1. kolumna 2. kolumna

W pierwszym wierszu w pierwszej


kolumnie jest umieszczony napis „Do”,
1. wiersz Do Wpisz adres e-mail a w drugiej kolumnie znajduje się pole
testowe z tekstem podpowiedzi „Wpisz
adres e-mail”.
Treść wiadomości
W drugim wierszu jest umieszczone pole
tekstowe z tekstem podpowiedzi „Treść
2. wiersz wiadomości”. Pole to zaczyna się w pierwszej
kolumnie i rozciąga także na drugą. Musi ono
zająć cały dostępny obszar wiersza.

Trzeci wiersz zawiera przycisk z napisem


„Wyślij”. Przycisk jest wyśrodkowany
3. wiersz Wyślij
w obszarze obejmującym obie kolumny,
co oznacza, że musi zajmować obie kolumny
układu.

Nasz układ siatki musi się składać z dwóch kolumn


Wszystkie widoki będziemy mogli rozmieścić w planowany sposób, jeśli utworzymy
układ siatki składający się z dwóch kolumn:

<GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:columnCount=”2”
tools:context=”.MainActivity” >
</GridLayout>

Skoro zdefiniowaliśmy podstawę układu, możemy zacząć dodawać do niego poszczególne widoki.

192 Rozdział 5.
Interfejs użytkownika

Wiersz 0: dodajemy widoki do określonych wierszy i kolumn ¨  RelativeLayout


¨  LinearLayout
Pierwszy wiersz układu siatki składa się z komponentu TextView umieszczonego
¨  GridLayout
w pierwszej kolumnie oraz pola tekstowego umieszczonego w drugiej kolumnie.
Zacznij od dodania obu tych widoków do układu: Do Wpisz adres e-mail

<GridLayout...>
<TextView W układach siatki można używać
android:layout_width=”wrap_content”
atrybutów android:gravity
android:layout_height=”wrap_content”
android:text=”@string/to” /> i android:layout_gravity.

<EditText
android:layout_width=”wrap_content”
Atrybutu layout_gravity możemy używać
android:layout_height=”wrap_content” także w układach siatki. W tym układzie
zastosowaliśmy wartośc fill_horizontal, gdyż
android:layout_gravity=”fill_horizontal” chcemy, by pole tekstowe zajmowało całą
android:hint=”@string/to_hint” /> szerokość dostępnego obszaru.
</GridLayout>

Teraz użyjemy dwóch kolejnych atrybutów, android:layout_row i android:layout_


column, aby określić, w którym wierszu i której kolumnie układu ma zostać Indeksy wierszy
umieszczony konkretny widok. Indeksy wiersza i kolumny zawsze są liczone od 0,
a zatem aby widok został umieszczony w pierwszej kolumnie pierwszego wiersza, i kolumn są
musimy użyć poniższych atrybutów: numerowane od 0.
Numeracja wierszy i kolumn
android:layout_row=”0” zaczyna się od 0, a zatem te dwa A zatem atrybut
android:layout_column=”0” atrybuty wskazują na pierwszy
wiersz i pierwszą kolumnę. layout_column=”n”
Spróbujmy więc zastosować te atrybuty, by umieścić komponent TextView wskazuje na kolumnę
w kolumnie 0 i pole tekstowe w kolumnie 1.
n+1 w układzie.
<GridLayout...>

<TextView
...
Kolumna 0 Kolumna 1
android:layout_row=”0”
android:layout_column=”0”
android:text=”@string/to” />
Wiersz 0 Do Wpisz adres e-mail

<EditText
...
android:layout_row=”0”
android:layout_column=”1”
android:hint=”@string/to_hint” />
</GridLayout>
jesteś tutaj  193
Scalanie komórek

Wiersz 1: tworzymy widok zajmujący komórki kilku kolumn ¨  RelativeLayout


¨  LinearLayout
W drugim wierszu układu chcemy umieścić duże pole tekstowe,
¨  GridLayout
które będzie zajmowało obszar komórki z pierwszej i z drugiej
kolumny. W ten sposób pole zajmie całą szerokość wiersza.

Aby widok został wyświetlony w kilku kolumnach, najpierw musimy


określić, w której kolumnie ma się on zaczynać. Chcemy, by widok Kolumna 0 Kolumna 1
zaczynał się w pierwszej kolumnie drugiego wiersza, więc określimy
jego położenie, używając poniższych atrybutów:

android:layout_row=”1” Wiersz 1 Treść wiadomości


android:layout_column=”0”

Oprócz tego chcemy, by widok rozciągał się na obie kolumny


wiersza — ten efekt możemy uzyskać, stosując atrybut
android:layout_columnSpan, który jest używany w następujący
sposób:

android:layout_columnSpan=”liczba”

gdzie liczba określa, ile kolumn dany widok ma zajmować. Liczba zajmowanych kolumn: 2
W naszym przypadku zastosujemy atrybut o następującej postaci:

android:layout_columnSpan=”2”

Łącząc to wszystko w jedną całość, uzyskamy taki oto kod


definiujący pole tekstowe do wpisywania treści wiadomości:

<GridLayout...>
<TextView... />
To są widoki dodane na poprzedniej stronie do wiersza 0.
<EditText.../>
<EditText
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill” Chcemy, aby widok zajął cały dostępny obszar
i aby umieszczony w nim tekst był wyświetlany
android:gravity=”top” u góry pola.
android:layout_row=”1”
android:layout_column=”0” Widok zaczyna się w kolumnie 0 i obejmuje dwie kolumny.
android:layout_columnSpan=”2”
android:hint=”@string/message” />
</GridLayout>

Skoro załatwiliśmy sprawę dwóch pierwszych wierszy układu,


czas zająć się przyciskiem umieszczonym w trzecim wierszu.

194 Rozdział 5.
Interfejs użytkownika

Wiersz 2: tworzymy widok zajmujący komórki kilku kolumn ¨  RelativeLayout


¨  LinearLayout
Chcemy, by przycisk został wyśrodkowany w obszarze obejmującym obie kolumny wiersza,
¨  GridLayout
jak na poniższym rysunku:

Kolumna 0 Kolumna 2

Wiersz 2 Wyślij

Liczba zajmowanych kolumn: 2

Magnesiki układowe
Napisaliśmy kod, który służy do wyśrodkowania przycisku Wyślij w trzecim
wierszu układu siatki, ale nagły podmuch strącił niektóre magnesiki na ziemię.
Czy możesz odtworzyć kod, używając przedstawionych poniżej magnesików?

<GridLayout...>
<TextView... />
To są widoki, które dodaliśmy już wcześniej.
<EditText.../>
<EditText.../>

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
fill_horizontal
android:layout_row=...............
"0"
android:layout_column=............... "2"
"0"
android:layout_gravity=...............
"1"
"1"
android:layout_columnSpan=...............
"2"

android:text=”@string/send” />
center_horizontal
</GridLayout>

Odpowiedź znajdziesz na stronie 224.


jesteś tutaj  195
Kod układu

Pełny kod układu siatki ¨  RelativeLayout


¨  LinearLayout
<GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
¨  GridLayout
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:columnCount=”2”
tools:context=”.MainActivity” >

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”0”
android:layout_column=”0”
android:text=”@string/to” />

<EditText
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill_horizontal”
android:layout_row=”0”
android:layout_column=”1”
android:hint=”@string/to_hint” />

<EditText
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill”
android:gravity=”top”
android:layout_row=”1”
android:layout_column=”0”
android:layout_columnSpan=”2”
android:hint=”@string/message” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”2” Przycisk zajmuje dwie kolumny,
android:layout_column=”0” przy czym zaczyna się w pierwszej
kolumnie drugiego wiersza układu.
android:layout_gravity=”center_horizontal”
android:layout_columnSpan=”2”
android:text=”@string/send” />
</GridLayout>

196 Rozdział 5.
Interfejs użytkownika

GridLayout — podsumowanie ¨  RelativeLayout


¨  LinearLayout
Oto, w jaki sposób należy tworzyć układy siatki.
¨  GridLayout

Jak utworzyć układ siatki?


Układy siatki tworzy się za pomocą elementu <GridLayout>. Musimy przy tym określić liczbę
kolumn układu, do czego służy atrybut android:columnCount. Liczbę wierszy można określić
przy użyciu atrybutu android:rowCount:

<GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”2”
... >
...
</GridLayout>

Określanie, w której kolumnie i którym wierszu ma się znaleźć widok


Wiersz i kolumnę, w których ma się znaleźć dany widok, określamy za pomocą atrybutów
android:layout_row i android:layout_column. Indeksy wierszy i kolumn zaczynają się
od 0, a zatem aby umieścić widok w pierwszym wierszu i pierwszej kolumnie, trzeba użyć
atrybutów o postaci:

android:layout_row=”0”
android:layout_column=”0”

Określanie liczby kolumn zajmowanych przez widoki


Atrybut android:layout_columnSpan służy do określania liczby kolumn, które ma zajmować
dany widok. Na przykład jeśli chcemy, by widok zajmował dwie kolumny, to musimy użyć
poniższego atrybutu:

android:layout_columnSpan=”2”

jesteś tutaj  197


Ćwiczenie

Bądź układem 3
Trzy spośród pięciu układów pokazanych
na tej stronie zostały utworzone przez
układy przedstawione na następnej
stronie. Twoim zadaniem
jest dopasowanie każdego
z trzech układów do ekranów
reprezentujących ich wygląd.

198 Rozdział 5.
Interfejs użytkownika

A <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”3”
tools:context=”.MainActivity” >
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill”
android:layout_columnSpan=”3”
android:text=”@string/hello” />
</GridLayout>

B <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”2”
tools:context=”.MainActivity” >
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill”
android:layout_columnSpan=”2”
android:text=”@string/hello” />
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/hi” />
</GridLayout>

C <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”2”
tools:context=”.MainActivity” >
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”0”
android:layout_column=”0”
android:layout_columnSpan=”2”
android:text=”@string/hello” />
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”1”
android:layout_column=”0”
android:text=”@string/hi” />
</GridLayout>

jesteś tutaj  199


Rozwiązanie

Bądź układem. Rozwiązanie


Trzy spośród pięciu układów pokazanych
na tej stronie zostały utworzone przez
układy przedstawione na następnej
stronie. Twoim zadaniem
jest dopasowanie każdego Żaden z układów
z trzech układów do ekranów nie pozwala
wygenerować
reprezentujących ich wygląd. ekranu o takiej
postaci.

1
A <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”3”
tools:context=”.MainActivity” >
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill”
android:layout_columnSpan=”3” Ten układ
android:text=”@string/hello” /> zawiera tylko
jeden przycisk
</GridLayout> zajmujący cały
obszar ekranu.

3 B <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”2”
tools:context=”.MainActivity” >
<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”fill” Ten przycisk
android:layout_columnSpan=”2” wypełnia prawie
android:text=”@string/hello” /> cały ekran,
zostawiając
<Button jedynie u dołu
android:layout_width=”wrap_content” nieco miejsca na
drugi przycisk.
android:layout_height=”wrap_content”
android:text=”@string/hi” />
</GridLayout>

200 Rozdział 5.
Interfejs użytkownika

4 C <GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:columnCount=”2”
tools:context=”.MainActivity” >

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
Choć przycisk
android:layout_row=”0” zajmuje dwie
android:layout_column=”0” kolumny, to
android:layout_columnSpan=”2” jednak nie
android:text=”@string/hello” /> kazaliśmy go
rozciągnąć
na całą tę
<Button szerokość.
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”1”
android:layout_column=”0”
android:text=”@string/hi” />
</GridLayout>

Układy i komponenty GUI mają wiele wspólnego


Być może zauważyłeś, że układy wszystkich typów mają kilka wspólnych atrybutów.
Niezależnie od typu używanego układu musimy określić jego szerokość i wysokość,
używając do tego celu atrybutów android:layout_width i android:layout_
height. Wymóg określania wymiarów nie ogranicza się jednak do układów
— dokładnie te same dwa atrybuty, android:layout_width i android:layout_
height, muszą być dodawane do wszystkich komponentów GUI.

Dzieje się tak dlatego, że wszystkie układy i komponenty GUI są klasami


dziedziczącymi po klasie View. Przyjrzyjmy się temu zagadnieniu nieco
dokładniej.

jesteś tutaj  201


Widoki

Komponenty GUI są typami pochodnymi klasy View


Przekonałeś się już, że komponenty GUI są typami widoków — okazuje się,
że wszystkie one są klasami pochodnymi klasy android.view.View. Oznacza to,
że wszystkie komponenty GUI, których używamy w interfejsie użytkownika, mają
wspólne atrybuty i zachowania. Wszystkie one mogą być wyświetlane na ekranie
i wszystkie pozwalają określać swoją szerokość i wysokość. Wszystkie komponenty Klasa android.view.View
GUI, których używamy do tworzenia interfejsów użytkownika, dziedziczą te jest klasą bazową wszystkich
podstawowe funkcjonalności i je rozszerzają. komponentów GUI używanych do
tworzenia interfejsów użytkownika
aplikacji.
android.view.View
... Listy typu spinner są
android.widget.TextView bardziej złożonymi typami
jest bezpośrednią klasą pochodnymi klasy View.
pochodną klasy View.

android.widget.TextView android.widget.Spinner
... ...

android.widget.EditText android.widget.Button
... ...

Układy dziedziczą po klasie ViewGroup


Nie tylko komponenty GUI są widokami — klasami pochodnymi klasy View.
Okazuje się, że także widoki są wyspecjalizowanymi typami widoków, Komponent GUI
a nazywamy je grupami widoków. Wszystkie widoki są klasami pochodnymi jest typem widoku,
klasy android.view.ViewGroup. Grupa widoków to wyspecjalizowany widok,
który może zawierać inne widoki. obiektem zajmującym
android.view.View
miejsce na ekranie.
...
Układ jest obiektem
Układy są typami określanym jako grupa
android.view.ViewGroup pochodnymi klasy
ViewGroup. ViewGrup widoków — to widok
... jest klasą pochodną
klasy View. specjalnego typu,
który może zawierać
android.widget. android.widget. android.widget.
GridLayout LinearLayout RelativeLayout
inne widoki.
... ... ...

202 Rozdział 5.
Interfejs użytkownika

Co nam daje bycie widokiem?


Obiekt View zajmuje prostokątny obszar na ekranie. Zawiera on
wszystkie funkcjonalności, których potrzebują widoki, by móc prowadzić
szczęśliwy i użyteczny żywot w Androidowie. Poniżej wymieniliśmy kilka
spośród tych funkcjonalności, które według nas są najważniejsze.

Odczytywanie i ustawianie właściwości Ten schemat przedstawia kilka


metod klasy View, których
możemy używać w kodzie
Za kulisami każdy widok jest obiektem Javy. Oznacza to, że w kodzie aktywności. Ponieważ zostały
aktywności możemy odczytywać i ustawiać wartości jego właściwości. one zdefiniowane w klasie
Na przykład możemy odczytać wartość wybraną z listy rozwijanej lub bazowej View, są dostępne
także we wszystkich widokach
zmienić tekst prezentowany w widoku tekstowym. Konkretne właściwości i grupach widoków.
i metody, jakie są dostępne, zależą od typu widoku.

Aby umożliwić wykonywanie takich operacji, z każdym widokiem można


android.view.View
skojarzyć identyfikator, dzięki któremu będzie można odwoływać się do
danego widoku w kodzie aktywności. getId()
getHeight()
Wielkość i położenie getWidth()
Możemy określać szerokość i wysokość widoku, tak by Android wiedział, setVisibility(int)
jaką ma on mieć wielkość. Oprócz tego możemy określić, czy widok ma
findViewById(int)
mieć jakieś wypełnienie, czy nie.
isClickable()
Po wyświetleniu widoku możemy odczytać jego położenie i faktyczne
wymiary na ekranie. isFocused()
requestFocus()
Obsługa miejsca wprowadzania ...
Android obsługuje przenoszenie miejsca wprowadzania w zależności od
czynności wykonywanych przez użytkownika. Obejmuje to odpowiednie
reakcje na widoki ukryte, usuwane lub wyświetlane.

Obsługa zdarzeń i obiekty nasłuchujące


Każdy z widoków może odpowiadać na zdarzenia. Możemy także
tworzyć obiekty nasłuchujące, które reagują na to, co się dzieje
z widokiem. Na przykład wszystkie widoki mogą reagować na utratę
miejsca wprowadzania, a przyciski (i ich klasy pochodne) mogą reagować
na kliknięcia.

Ponieważ grupa widoków sama jest typem widoku, wszystkie


układy i komponenty GUI dysponują tymi samymi, podstawowymi
możliwościami funkcjonalnymi.

jesteś tutaj  203


Hierarchia widoków

Układ jest tak naprawdę hierarchią widoków


Układy, które definiujemy w kodzie XML, dają nam w rzeczywistości hierarchiczne drzewo
widoków i grup widoków. Na przykład poniżej przedstawiliśmy układ względny zawierający
przycisk i pole tekstowe. Układ względny jest grupą widoków, natomiast przycisk i pole tekstowe
są widokami. Grupa widoków jest elementem nadrzędnym, widoki zaś są jego elementami
podrzędnymi:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
... >
Układ względny
<Button
W tym przykładzie ViewGroup
pominęliśmy duże android:id=”@+id/send”
fragmenty kodu
XML. Kluczowe ... />
znaczenie mają tu Przycisk
widoki umieszczone <EditText
w grupie widoków. View View
android:id=”@+id/message”
Pole tekstowe
... />
</RelativeLayout>

Podczas tworzenia aplikacji kod XML układu jest za kulisami przekształcany na obiekt
ViewGroup, który zawiera drzewo obiektów View. W powyższym przykładzie przycisk
zostanie przekształcony na obiekt Button, a pole tekstowe — na obiekt EditText.
Button i TextView to dwie klasy pochodne klasy View.

<Layout>
Układ względny

</Layout>
ViewGroup
layout.xml

Pole tekstowe
Przycisk

View View

To właśnie dzięki temu możemy operować na widokach w kodzie Javy.


Za kulisami wszystkie widoki są bowiem przekształcane do postaci
obiektów typu View.

204 Rozdział 5.
Interfejs użytkownika

Zabawy z widokami
Przyjrzyjmy się najpopularniejszym komponentom GUI. Znasz już kilka z nich, lecz
mimo to przedstawimy je jeszcze raz. Nie będziemy tu prezentować pełnego API
każdego z tych komponentów, a jedynie ich najważniejsze elementy, dzięki którym
będziesz mógł zacząć ich używać.

Widok tekstowy
android.view.View
Widok tekstowy, komponent
...
TextView, służy do wyświetlania
fragmentów tekstu na ekranie.

android.widget.TextView
Definiowanie w kodzie XML ...
Ten typ komponentów definiuje się w kodzie XML za pomocą elementu
<TextView>. Do określenia tekstu wyświetlanego w komponencie używany jest
atrybut android:text, przy czym sam tekst jest zazwyczaj definiowany jako zasób
łańcuchowy:

<TextView
android:id=”@+id/text_view”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/text” />

Interfejs API kontrolki TextView zawiera wiele atrybutów pozwalających


kontrolować jej wygląd, na przykład określać wielkości prezentowanego tekstu.
By zmienić wielkość tekstu wyświetlonego w widoku tekstowym, należy użyć
atrybutu android:textSize w sposób pokazany poniżej:

android:textSize=”14sp”

Wielkość tekstu określa się za pomocą pikseli niezależnych od skali (sp). Piksele
niezależne od skali uwzględniają, czy użytkownik chce używać dużych czcionek,
czy nie. Tekst o wielkości 14sp będzie większy na urządzeniach skonfigurowanych
tak, by używały dużych czcionek, niż na tych, na których mają być używane małe
czcionki.

Stosowanie w kodzie aktywności


Aby zmienić tekst wyświetlany w komponencie TextView, należy użyć fragmentu
kodu o następującej postaci:

TextView textView = (TextView) findViewById(R.id.text_view);


textView.setText(”Jakiś inny tekst”);

jesteś tutaj  205


Edytowalne pola tekstowe

Pola tekstowe android.view.View


...
Przypominają widoki tekstowe, lecz
pozwalają edytować wyświetlany w nich
tekst.
android.widget.TextView
Definiowanie w kodzie XML ...

Pola tekstowe definiuje się w kodzie XML za pomocą elementu <EditText>.


Atrybut android:hint pozwala określić tekst podpowiedzi, dzięki któremu
użytkownik będzie mógł się zorientować, co ma wpisać w danym polu. android.widget.EditText
<EditText ...
android:id=”@+id/edit_text”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:hint=”@string/edit_text” />

Atrybut android:inputType pozwala zdefiniować, jakiego typu dane użytkownik


ma wpisać w polu, dzięki czemu system będzie mu w stanie pomóc. Na przykład jeśli
użytkownik ma wpisać liczbę, to możemy użyć atrybutu o postaci:

android:inputType=”number”

a Android wyświetli klawiaturę numeryczną. Oto kilka naszych ulubionych wartości,


które można przypisać temu atrybutowi:
Kompletną listę dostępnych wartości tego atrybutu
można znaleźć w dokumentacji Androida dla
Wartość Działanie programistów dostępnej w internecie.

phone Wyświetla klawiaturę do wybierania numeru telefonu.


textPassword Wyświetla klawiaturę do wpisywania tekstu, przy czym zawartość pola jest maskowana.
textCapSentences Sprawia, że pierwsze słowo każdego zdania zaczyna się od wielkiej litery.
textAutoCorrect Automatycznie poprawia błędy we wpisywanym tekście.

Definiując pole, można zastosować w nim jednocześnie wiele typów danych — wystarczy rozdzielić je
znakiem pionowej kreski (|). Na przykład aby wpisywane zdania zaczynały się od wielkiej litery,
a tekst był automatycznie poprawiany, należałoby użyć atrybutu o następującej postaci:

android:inputType=”textCapSentences|textAutoCorrect”

Stosowanie w kodzie aktywności


Tekst wpisany w polu tekstowym można pobrać, używając następującego fragmentu kodu:

EditText editText = (EditText) findViewById(R.id.edit_text);


String text = editText.getText().toString();

206 Rozdział 5.
Interfejs użytkownika

Przycisk
Przyciski są zazwyczaj stosowane po to, by aplikacja mogła
wykonać jakąś operację.

android.view.View
Definiowanie w kodzie XML ...
W kodzie XML układów przyciski definiuje się za pomocą elementu <Button>.
Tekst wyświetlany na przycisku jest określany w atrybucie android:text:

<Button android.widget.TextView
android:id=”@+id/button”
...
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/button_text” />
android.widget.Button
...
Stosowanie w kodzie aktywności
Aby przycisk zareagował na kliknięcie, w kodzie XML układu należy dodać
atrybut android:onClick i podać w nim nazwę metody zdefiniowanej w kodzie
aktywności, którą należy wywołać:

android:onClick=”onButtonClicked”

Tę metodę należy zdefiniować w kodzie aktywności w następujący sposób:

/** Metoda wywoływana po kliknięciu przycisku */


public void onButtonClicked(View view) {
// Robimy coś w odpowiedzi na kliknięcie przycisku
}

onButtonClicked()
<Layout>

</Layout>
Aktywność
Układ

jesteś tutaj  207


Przycisk przełącznika

Przycisk przełącznika
Przycisk przełącznika może się znajdować w jednym z dwóch dostępnych stanów,
wybranym przez kliknięcie. android.view.View
...
Tak wygląda przycisk
przełącznika, kiedy Kiedy klikniesz
jest wyłączony. przycisk,
zostanie on
włączony.
android.widget.TextView

Definiowanie w kodzie XML ...

Przycisk przełącznika definiuje się w kodzie XML za pomocą elementu <ToggleButton>.


Dwa atrybuty, android:textOn i android:textOff, służą do określania tekstów
wyświetlanych na przycisku w zależności od jego stanu: android.widget.Button

<ToggleButton ...

android:id=”@+id/toggle_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” android.widget.
android:textOn=”@string/on” CompoundButton
android:textOff=”@string/off” /> ...

Stosowanie w kodzie aktywności android.widget.


Jeśli chcemy, by przycisk przełącznika odpowiadał na kliknięcia, do jego kodu XML ToggleButton
musimy dodać atrybut android:onClick. Wartością tego atrybutu musi być ...
nazwa funkcji zdefiniowanej w kodzie aktywności:
To jest dokładnie to samo co
android:onClick=”onToggleButtonClicked” wywoływanie metody w odpowiedzi
na kliknięcie normalnego przycisku.

Metoda zdefiniowana w kodzie aktywności może mieć następującą postać:


/** Metoda wywoływana po kliknięciu przycisku przełącznika */
public void onToggleClicked(View view) {
// Pobieramy stan przycisku przełącznika
boolean on = ((ToggleButton) view).isChecked();
if (on) {
// Przycisk włączony To wywołanie zwraca wartość true, jeśli
} else { przycisk jest włączony, i wartość false,
jeśli jest on wyłączony.
// Przycisk wyłączony
}
}

208 Rozdział 5.
Interfejs użytkownika

Przełącznik
Przełącznik to rodzaj małego suwaka, który działa bardzo podobnie
do przycisku omawianego wcześniej przełącznika.
Przełączniki 
wymagają API 
Obejrzyj to!
Tak wygląda A tak wygląda
przełącznik, kiedy przełącznik, poziomu 14  
jest wyłączony. gdy jest lub wyższego.
włączony.
Jeśli planujesz stosowanie
przełączników w swojej aplikacji,
upewnij się, czy będzie ona używać
Definiowanie w kodzie XML wersji SDK obsługującej API
Przełączniki definiuje się w kodzie XML za pomocą elementów <Switch>. poziomu 14 lub wyższego.
Korzystając z atrybutów android:textOn i android:textOff, można
określać teksty wyświetlane przez przełącznik w zależności od jego stanu.
android.view.View
<Switch ...
android:id=”@+id/switch_view”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android.widget.TextView
android:textOn=”@string/on”
android:textOff=”@string/off” /> ...

Stosowanie w kodzie aktywności android.widget.Button


Jeśli chcemy, by przełącznik odpowiadał na kliknięcia, do jego kodu XML ...
musimy dodać atrybut android:onClick. Wartością tego atrybutu musi być
nazwa funkcji zdefiniowanej w kodzie aktywności:

android:onClick=”onSwitchClicked” android.widget.
CompoundButton
Metoda zdefiniowana w kodzie aktywności może mieć następującą postać: ...
/** Metoda wywoływana po kliknięciu przełącznika */
public void onToggleClicked(View view) {
// Czy przełącznik jest włączony? android.widget.Switch
boolean on = ((Switch) view).isChecked(); ...

if (on) { Ten kod jest bardzo podobny do


metody obsługującej kliknięcia
// Przełącznik włączony przycisku przełącznika.
} else {
// Przełącznik wyłączony
}
}

jesteś tutaj  209


Pola wyboru

Pola wyboru
Pola wyboru umożliwiają prezentowanie użytkownikom grupy opcji. android.view.View
Użytkownicy mogą następnie wybierać z tej grupy dowolne opcje.
...
Każde pole wyboru można zaznaczyć niezależnie od pozostałych.

Oto dwa pola wyboru. Użytkownik


może zaznaczyć mleko, cukier android.widget.TextView
lub oba te pola albo może nie ...
zaznaczać żadnego z nich.

android.widget.Button
...

Definiowanie w kodzie XML android.widget.


Pola wyboru definiuje się w kodzie XML za pomocą elementów <CheckBox>. Tekst CompoundButton
wyświetlany obok danego pola można określić, używając atrybutu android:text: ...

<CheckBox android:id=”@+id/checkbox_milk”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” android.widget.CheckBox
android:text=”@string/milk” /> ...

<CheckBox android:id=”@+id/checkbox_sugar”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/sugar” />

Stosowanie w kodzie aktywności


Do określenia, czy konkretne pole wyboru jest zaznaczone, czy nie, służy metoda
isChecked(). Jeśli pole jest zaznaczone, jej wywołanie zwróci wartość true:

CheckBox checkbox = (CheckBox) findViewById(R.id.checkbox_milk);


boolean checked = checkbox.isChecked();
if (checked) {
// Pole jest zaznaczone — reagujemy odpowiednio
}

210 Rozdział 5.
Interfejs użytkownika

Pola wyboru (ciąg dalszy)


Podobnie jak w przypadku przycisków, istnieje także możliwość reagowania na kliknięcie
pola wyboru. W tym celu należy dodać do kodu XML układu atrybut android:onClick
i przypisać mu nazwę metody zdefiniowanej w kodzie aktywności, którą należy wywołać:

<CheckBox android:id=”@+id/checkbox_milk”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/milk”
android:onClick=”onCheckboxClicked”/>
W tym przypadku metoda onCheckboxClicked()
<CheckBox android:id=”@+id/checkbox_sugar” zostanie wywołana niezależnie od tego,
które pole wyboru zostało kliknięte.
android:layout_width=”wrap_content” W razie potrzeby w każdym polu wyboru
można zastosować inną metodę.
android:layout_height=”wrap_content”
android:text=”@string/sugar”
android:onClick=”onCheckboxClicked”/>

Metoda obsługująca kliknięcia pól tekstowych mogłaby zostać zdefiniowana


w następujący sposób:

public void onCheckboxClicked(View view) {


// Czy kliknięte pole wyboru jest zaznaczone?
boolean checked = ((CheckBox) view).isChecked();

// Określamy, które pole zostało kliknięte


switch(view.getId()) {
case R.id.checkbox_milk:
if (checked)
// Kawa z mlekiem
else
// Czarna jak niebo w bezksiężycową noc
break;
case R.id.checkbox_sugar:
if (checked)
// Słodziutka
else
// Lepiej niech będzie gorzka
break;
}
}

jesteś tutaj  211


Przyciski opcji

Przyciski opcji
Także ten rodzaj przycisków pozwala wyświetlać grupy opcji. Jednak w ich
przypadku użytkownik może w danej chwili wybrać tylko jedną opcję z grupy. android.view.View
...
Zastosuj przyciski
opcji, aby ograniczyć
możliwości użytkownika
do wyboru tylko jednej
spośród dostępnych opcji.
android.widget.TextView
Definiowanie w kodzie XML ...

Definiowanie przycisków opcji należy zacząć od dodania specjalnego typu grupy widoków:
<RadioGroup>. Wewnątrz elementu <RadioGroup> można następnie definiować
poszczególne przyciski opcji, używając w tym celu elementów <RadioButton>: android.widget.Button

<RadioGroup android:id=”@+id/radio_group” ...


android:layout_width=”match_parent”
Możemy wybrać, czy
android:layout_height=”wrap_content” przyciski opcji mają być
wyświetlane w układzie
android:orientation=”vertical”> pionowym czy poziomym. android.widget.
CompoundButton
<RadioButton android:id=”@+id/radio_cavemen” ...
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/cavemen” /> android.widget.
RadioButton
<RadioButton android:id=”@+id/radio_astronauts” ...
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/astronauts” />
</RadioGroup>

Stosowanie w kodzie aktywności


Identyfikator zaznaczonego przycisku opcji można określić za pomocą metody
getCheckedRadioButtonId():

RadioGroup radioGroup = (RadioGroup)findViewById(R.id.radioGroup);


int id = radioGroup.getCheckedRadioButtonId();
if (id == -1){
// Nie wybrano żadnej opcji
}
else{
RadioButton radioButton = (RadioButton)findViewById(id);
}

212 Rozdział 5.
Interfejs użytkownika

Przyciski opcji (ciąg dalszy)


Aby odpowiadać na kliknięcia przycisku opcji, należy dodać do kodu XML układu Grupa widoków
atrybut android:onClick i podać w nim nazwę metody, którą należy wywołać,
zdefiniowaną w kodzie aktywności: zawierająca przyciski
<RadioGroup android:id=”@+id/radio_group” opcji jest klasą
android:layout_width=”match_parent”
android:layout_height=”wrap_content” pochodną klasy
android:orientation=”vertical”> LinearLayout.
<RadioButton android:id=”@+id/radio_cavemen” Można w niej
android:layout_width=”wrap_content” zatem używać tych
android:layout_height=”wrap_content”
android:text=”@string/cavemen” samych atrybutów,
android:onClick=”onRadioButtonClicked” /> które są dostępne
<RadioButton android:id=”@+id/radio_astronauts” w układach liniowych.
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@string/astronauts”
android:onClick=”onRadioButtonClicked” />
</RadioGroup>

Metodę obsługującą kliknięcia przycisku można zdefiniować w następujący sposób:

public void onRadioButtonClicked(View view) {


RadioGroup radioGroup = (RadioGroup)findViewById(R.id.radioGroup);
int id = radioGroup.getCheckedRadioButtonId();
switch(id) {
case R.id.radio_cavemen:
// Wygrali jaskiniowcy
break;
case R.id.radio_astronauts:
// Wygrali astronauci
break;
}
}

jesteś tutaj  213


Listy rozwijane

Lista rozwijana
Jak już wiesz, komponent Spinner tworzy rozwijaną listę wartości, spośród których android.view.View
użytkownik może wybrać jedną.
...

Listy rozwijanej
używaliśmy android.view.ViewGroup
w rozdziale 2. ...

android.widget.
AdapterView
Definiowanie w kodzie XML ...

Listy rozwijane tworzy się za pomocą elementu <Spinner>. Statyczną tablicę


danych, używanych jako opcje listy, określa się przy użyciu android:entries,
w którym należy podać nazwę tablicy łańcuchów znaków. android.widget.
AbsSpinner
<Spinner Są także inne sposoby
android:id=”@+id/spinner” określania opcji list — poznasz ...
je w dalszej części książki.
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:entries=”@array/spinner_values” /> android.widget.Spinner
...
Tablicę łańcuchów znaków można zdefiniować w pliku strings.xml
w następujący sposób:

<string-array name=”spinner_values”>
<item>jasne</item>
<item>bursztynowe</item>
<item>brązowe</item>
<item>ciemne</item>
</string-array>

Stosowanie w kodzie aktywności


Wartość opcji aktualnie wybranej na liście można pobrać za pomocą metody
getSelectedItem(), a następnie skonwertować do łańcucha znaków
w następujący sposób:

Spinner spinner = (Spinner) findViewById(R.id.spinner);


String string = String.valueOf(spinner.getSelectedItem());

214 Rozdział 5.
Interfejs użytkownika

Widoki obrazów
Jak sama nazwa wskazuje, ten typ widoków służy do wyświetlania obrazów. android.view.View
...

Widok obrazu przedstawia android.widget.ImageView


wskazany obraz. ...

ImageView jest bezpośrednią


klasą pochodną klasy View.
Dodawanie obrazu do projektu
W pierwszej kolejności musimy dodać plik obrazka do projektu jako zasób graficzny.
Jeśli wyświetlimy katalog app/src/main/res projektu, to przekonamy się, że wewnątrz
niego znajduje się katalog o nazwie drawable. Stanowi on domyślne miejsce, w którym
są umieszczane zasoby graficzne. Aby umieścić plik obrazu w tym katalogu, wystarczy
go do niego przeciągnąć i upuścić.

Możemy także używać różnych plików graficznych zależnie od gęstości ekranu


ży
urządzenia. Oznacza to, że możemy wyświetlać obrazy o większej rozdzielczości na Aby utworzyć nowy katalog, nale
wyświet lić stru ktur ę kata logó w
urządzeniach, których ekrany mają większą gęstość, i obrazy o mniejszej rozdzielczości
w widoku Project, zaznaczyć
na urządzeniach z ekranami o mniejszej gęstości. W tym celu w katalogu app/src/main/ katalog res, a następnie wybrać
res projektu należy utworzyć kolejne katalogi drawable dla poszczególnych gęstości opcję File/New…/Android resource
ekranów. Nazwy tych katalogów odpowiadają gęstościom ekranów: directory.

android-ldpi Ekrany o niskiej gęstości, około 120 dpi. W zależności od tego, jaka wersja
Android Studio jest używana, IDE
android-mdpi Ekrany o średniej gęstości, około 160 dpi. może automatycznie utworzyć dla
nas niektóre z tych katalogów.
android-hdpi Ekrany o wysokiej gęstości, około 240 dpi.
android-xhdpi Ekrany o bardzo wysokiej gęstości, około 320 dpi.
android-xxhdpi Ekrany o bardzo, bardzo wysokiej gęstości, około 480 dpi.
android-xxxhdpi Ekrany o ultrawysokiej gęstości, około 640 dpi.

Następnie wystarczy umieścić obrazy o różnej rozdzielczości w odpowiednich katalogach


drawable*, upewniając się przy tym, że obrazy będą miały takie same nazwy. Android
określi, którego z obrazów użyć podczas działania aplikacji, na podstawie gęstości ekranu
używanego urządzenia. Na przykład jeśli urządzenie jest wyposażone w ekran o bardzo
wysokiej gęstości, system użyje obrazów z katalogu drawable-xhdpi.

Jeżeli plik obrazu zostanie dodany tylko do jednego katalogu, to Android będzie go
używał zawsze — na wszystkich urządzeniach. Najczęściej do tego celu używany jest
katalog drawable.

jesteś tutaj  215


Widoki obrazów

Obrazy: kod XML układu


Widok obrazu dodaje się do kodu XML układu, używając elementu <ImageView>.
Do określenia obrazu, który należy w danym widoku wyświetlić, służy atrybut
android:src. Można także zastosować atrybut android:contentDescription, aby
dodać łańcuch znaków opisujący obraz i poprawić w ten sposób dostępność aplikacji:

<ImageView
android:layout_width=”200dp”
android:layout_height=”100dp”
android:src=”@drawable/starbuzz_logo”
android:contentDescription=”@string/starbuzz_logo” />

Wartość atrybutu android:src przyjmuje następującą postać: ”@drawable/nazwa_


obrazu”, gdzie nazwa_obrazu to nazwa pliku obrazu bez rozszerzenia. Zasoby
graficzne są poprzedzane prefiksem @drawable. Prefiks ten informuje system,
że zasób jest umieszczony w jednym lub kilku katalogach drawable.

Stosowanie w kodzie aktywności


Zarówno źródło obrazu, jak i jego opis można ustawić programowo w kodzie
aktywności, używając w tym celu odpowiednio metod setImageResource()
i setContentDescription():

ImageView photo = (ImageView)findViewById(R.id.photo);


int image = R.drawable.starbuzz_logo;
String description = ”To jest logo.”;
photo.setImageResource(image);
photo.setContentDescription(description);

Ten fragment kodu odnajduje zasób graficzny o nazwie starbuzz_logo


przechowywany w którymś z katalogów drawable, a następnie używa go jako źródła
dla widoku o identyfikatorze photo. Aby odwołać się do zasobu graficznego w kodzie
aktywności, należy użyć zapisu R.drawable.nazwa_obrazu, gdzie nazwa_obrazu to
nazwa pliku obrazu (bez rozszerzenia).

216 Rozdział 5.
Interfejs użytkownika

Dodawanie obrazów do przycisków


Oprócz wyświetlania obrazów w widokach typu ImageView można je także
wyświetlać na przyciskach.

Wyświetlanie na przycisku tekstu i obrazu


Aby wyświetlić na przycisku tekst, a po jego prawej stronie obraz, należy
skorzystać z atrybutu android:drawableRight, podając w nim nazwę obrazu:

<Button
android:layout_width=”wrap_content” ej
Wyświetla logo Androida po praw
android:layout_height=”wrap_content” stronie tekstu na przycisku.
android:drawableRight=”@drawable/android”
android:text=”@string/click_me” />

Aby wyświetlić obraz po lewej stronie tekstu, należy użyć atrybutu


android:drawableLeft:

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:drawableLeft=”@drawable/android”
android:text=”@string/click_me” />

Używając atrybutu android:drawableBottom, można wyświetlić obraz


poniżej tekstu na przycisku:

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:drawableBottom=”@drawable/android”
android:text=”@string/click_me” />

I w końcu atrybut android:drawableTop pozwala wyświetlić obraz


nad tekstem na przycisku:

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:drawableTop=”@drawable/android”
android:text=”@string/click_me” />

jesteś tutaj  217


Przyciski z obrazami

Przyciski z obrazami
Przyciski tego typu przypominają normalne przyciski, z tym,
że zamiast tekstu są na nich wyświetlane wyłącznie obrazy.

Definiowanie w kodzie XML android.view.View


Przycisk z obrazem tworzy się, dodając do kodu XML układu element ...
<ImageButton>. Źródło obrazu wyświetlanego na przycisku określa się
za pomocą atrybutu android:src:

<ImageButton android.widget.ImageView
android:id=”@+id/button” ...
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:src=”@drawable/button_icon />
android.widget.
ImageButton
Stosowanie w kodzie aktywności
...
Aby przycisk ImageButton odpowiadał na kliknięcia, do jego kodu XML
należy dodać atrybut android:onClick i podać w nim nazwę metody
zdefiniowanej w kodzie aktywności: Klasa ImageButton
rozszerza klasę ImagView,
android:onClick=”onButtonClicked” a nie Button. Czy to Cię
zaskoczyło?
Tę metodę można zdefiniować w kodzie aktywności w następujący sposób:

/** Metoda wywoływana po kliknięciu przycisku */


public void onButtonClicked(View view) {
// Robimy coś w odpowiedzi na kliknięcie przycisku
}

onButtonClicked()
<Layout>

</Layout>
Aktywność
Układ

218 Rozdział 5.
Interfejs użytkownika

Widoki przewijane
Jeśli do układu dodamy bardzo dużo widoków, to na urządzeniach
z niewielkimi ekranami możemy mieć pewien problem — większość
układów nie udostępnia pasków przewijania pozwalających na przewijanie
ich zawartości. Na przykład jeśli dodamy do układu liniowego kilka dużych
przycisków, to zapewne nie będziemy w stanie zobaczyć ich wszystkich.

Układ liniowy nie udostępnia


pasków przewijania. Kiedy
spróbowaliśmy wyświetlić
w takim układzie siedem
przycisków na naszym
urządzeniu, nie mogliśmy
zobaczyć ich wszystkich.

Aby dodać pionowy pasek przewijania do układu, należy umieścić ten


układ wewnątrz elementu <ScrollView>, jak w poniższym przykładzie:

<ScrollView xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent” Przenieś te atrybuty z początko
wego układu
do elementu <ScrollView>, gdyż
tools:context=”.MainActivity” > to
obecnie głównym elementem pliku on jest
.

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:orientation=”vertical” >
Umieszczenie układu wewnątrz
... elementu <ScrollView>
</LinearLayout> spowodowało dodanie fajnego,
pionowego paska przewijania.
</ScrollView> Teraz użytkownik może już
wyświetlić wszystkie widoki.

Aby dodać do układu poziomy pasek przewijania, należy cały układ


umieścić wewnątrz elementu <HorizontalScrollView>.
jesteś tutaj  219
Wyświetlanie komunikatów

Krótkie komunikaty
Jest jeszcze jeden widżet, który chcielibyśmy przedstawić w tym rozdziale, java.lang.Object
a służy on do wyświetlania tak zwanych tostów (ang. toast). Tosty to zwyczajne,
...
krótkie komunikaty tekstowe wyświetlane na ekranie.

Takie komunikaty mają wyłącznie charakter informacyjny, a użytkownik


nie może prowadzić z nimi żadnych interakcji. Podczas prezentowania tych
komunikatów aktywność jest cały czas widoczna i zachowuje pełne możliwości android.widget.Toast
interakcji. Komunikaty są ukrywane automatycznie po określonym czasie. ...

Stosowanie w kodzie aktywności Jak widać, klasa Toast nie


Komunikaty są wyświetlane w kodzie aktywności. Nie można zdefiniować ich dziedziczy po klasie View,
więc nie jest widokiem. Ale
w układzie. ponieważ zapewnia nam ona
bardzo przydatną możliwość
Do ich tworzenia używana jest metoda Toast.makeText(), do której wyświetlania użytkownikom
przekazywane są trzy parametry: obiekt Context (zazwyczaj jest to referencja krótkich komunikatów,
przemyciliśmy ją do tego
this odwołująca się do bieżącej aktywności), obiekt CharSequence rozdziału.
zawierający treść wyświetlanego komunikatu oraz liczba typu int określająca
czas prezentowania komunikatu. Po utworzeniu obiektu Toast możemy
wyświetlić komunikat na ekranie, używając metody show().

Poniższy fragment kodu pokazuje, jak można utworzyć komunikat i wyświetlić


go na krótki okres czasu:

CharSequence text = ”Uwielbiam tosty!”;


int duration = Toast.LENGTH_SHORT;

Toast toast = Toast.makeText(this, text, duration);


toast.show();

Domyślnie komunikaty są
wyświetlane u dołu ekranu.

220 Rozdział 5.
Interfejs użytkownika

To doskonały moment, żeby przećwiczyć stosowanie widoków poznanych w tym rozdziale.


Utwórz zatem układ, który pozwoli wyświetlić ekran o następującej postaci:

Ćwiczenie
Pewnie nie będzie Ci się chciało
pisać kodu układu tutaj, ale może io?
Stud
poeksperymentujesz w Android

jesteś tutaj  221


Rozwiązanie

Oto jeden z wielu sposobów, na które można utworzyć przedstawiony układ.


Nie przejmuj się, jeśli Twój kod wygląda inaczej, gdyż to ćwiczenie ma wiele rozwiązań.

Ćwiczenie
Rozwiązanie

<GridLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
My zastosowaliśmy układ siatki,
,
android:layout_width=”match_parent” ale nic nie stoi na przeszkodzie
by użyć układu względnego.
android:layout_height=”match_parent”
android:paddingBottom=”16dp”
android:paddingLeft=”16dp”
android:paddingRight=”16dp”
android:paddingTop=”16dp”
android:columnCount=”2” Nasz układ ma dwie kolumny.

tools:context=”.MainActivity” >

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
ujący obie kolumny
android:layout_row=”0” Na górze wyświetlamy tekst zajm
pierwsze go wier sza ukła du.
android:layout_column=”0”
android:layout_columnSpan=”2”
android:text=”@string/message” /> Wszystkie widoki wymagają
łańcuchów znaków, dodanych
do pliku zasobów strings.xml.

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”1” W pierwszej kolumnie drugiego
wiersza umieściliśmy etykietę
android:layout_column=”0” Temperatura.
android:text=”@string/temp” />

222 Rozdział 5.
Interfejs użytkownika

dać użytkownikowi
Zastosowaliśmy przycisk przełącznika, by
gorąca czy zimna;
możliwość określenia, czy herbata ma być
<ToggleButton umieś ciliśm y go w wiers zu 1 i kolum nie 1.

android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”1”
android:layout_column=”1”
android:textOn=”@string/hot”
android:textOff=”@string/cold” />

<CheckBox android:id=”@+id/checkbox_milk”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”2”
android:layout_column=”0”
android:text=”@string/milk” />

<CheckBox android:id=”@+id/checkbox_sugar”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_row=”3”
android:layout_column=”0”
android:text=”@string/sugar” />

<CheckBox android:id=”@+id/checkbox_lemon” Do wyświetlenia poszczególnych


opcji (Mleko, Cukier i Cytryna)
android:layout_width=”wrap_content” zastosowaliśmy pola wyboru. Każe
z nich umieściliśmy w osobnym
android:layout_height=”wrap_content” wierszu układu.
android:layout_row=”4”
android:layout_column=”0”
android:text=”@string/lemon” />
</GridLayout>

jesteś tutaj  223


Jeszcze jedno rozwiązanie

Magnesiki układowe. Rozwiązanie


Napisaliśmy kod, który służy do wyśrodkowania przycisku Wyślij w trzecim
wierszu układu siatki, ale nagły podmuch strącił niektóre magnesiki na ziemię.
Czy możesz odtworzyć kod, używając przedstawionych poniżej magnesików?

<GridLayout...>
<TextView... />
<EditText.../>
<EditText.../>

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”

android:layout_row=...............

android:layout_column=...............

android:layout_gravity=...............

android:layout_columnSpan=...............

224 Rozdział 5.
e
Interfejs użytkownika

Twój przybornik do Androida

Rozdział 5.
Opanowałeś już rozdział 5. i dodałeś 
do swojego przybornika z narzędziami 
znajomość widoków i grup widoków.

    CELNE SPOSTRZEŻENIA

 Wszystkie komponenty GUI są rodzajami widoków.  Za pomocą atrybutu android:layout_gravity


Są to klasy dziedziczące po kasie android.view.View. można określać, w którym miejscu dostępnego obszaru
ma zostać wyświetlony dany widok.
 Wszystkie układy są klasami dziedziczącymi po
android.view.ViewGroup. Grupa widoków to widok  Atrybut android:gravity określa, w którym miejscu
specjalnego typu, który może zawierać inne widoki. widoku ma zostać wyświetlona jego zawartość.
 Plik XML układu jest konwertowany do postaci obiektu  Element <ToggleButton> definiuje przyciski
ViewGroup zawierającego hierarchiczne drzewo przełączników, które udostępniają dwa stany wybierane
widoków. kliknięciem.
 Układ względy określa położenie widoków względem  Element <Switch> definiuje kontrolkę przełącznika,
innych widoków lub względem samego układu która działa analogicznie do przycisku przełącznika.
nadrzędnego. Wymaga ona użycia API poziomu 14 lub wyższego.
 Układ liniowy wyświetla widoki w pionie lub w poziomie.  Element <Checkbox> definiuje pole wyboru.
Orientację układu można określić za pomocą atrybutu  Aby zdefiniować grupę przycisków opcji, w pierwszej
android:orientation.
kolejności trzeba zdefiniować ich grupę, używając
 Układ siatki dzieli ekran na siatkę komórek i pozwala elementu <RadioGroup>. Wewnątrz tej grupy można
określać, w której (lub w których) z nich mają dodawać poszczególne przyciski opcji, definiowane
zostać wyświetlone poszczególne widoki. Liczbę za pomocą elementów <RadioButton>.
kolumn układu można określić za pomocą atrybutu  Aby wyświetlić obraz, należy użyć elementu
android:columnCount. Do określania komórki,
<ImageView>.
w której ma zostać wyświetlony dany widok służą
atrybuty android:layout_row i android:layout_  Element <ImageButton> definiuje przycisk,
column. Za pomocą atrybutu android:layout_ który zamiast tekstu przedstawia obraz.
columnSpan można określać, ile kolumn ma zajmować  Paski przewijania można dodawać do układów
dany widok.
za pomocą elementów <ScrollView>
 Atrybuty android:padding* pozwalają określać, i <HorizontalScrollView>.
jak duże wypełnienie pozostanie przy poszczególnych  Klasa Toast pozwala wyświetlać komunikaty tekstowe.
krawędziach widoku.
 Aby wybrany element zajął więcej miejsca w układzie,
można do niego dodać atrybut android:layout_
weight. Atrybutu tego można używać wyłącznie
w układach liniowych.

jesteś tutaj  225


226 Rozdział 5.
6. Widoki list i adaptery

Zorganizuj się
Rany! Mam tyle pomysłów…
Tylko czy uda mi się je zmienić
w najpopularniejszą
aplikację roku?

Chcesz wiedzieć, jaki jest najlepszy sposób na określenie struktury aplikacji?


Znasz już podstawowe elementy konstrukcyjne używane do tworzenia aplikacji, więc teraz
nadszedł czas, żebyś się lepiej zorganizował. W tym rozdziale pokażemy Ci, jak możesz
przekształcić zbiór pomysłów w niesamowitą aplikację. Zobaczysz, że listy danych mogą
stać się kluczowym elementem projektu aplikacji i że łączenie ich może prowadzić do powstania
aplikacji łatwej w użyciu i zapewniającej ogromne możliwości. Przy okazji zapoznasz się
z obiektami nasłuchującymi i adapterami, dzięki którym Twoja aplikacja stanie się bardziej
dynamiczna.

to jest nowy rozdział  227


Pomysły

Każda aplikacja zaczyna się od pomysłu


Kiedy po raz pierwszy zaświta Ci myśl o napisaniu aplikacji, będziesz
zapewne mieć wiele pomysłów dotyczących tego, co powinna zawierać.

Na przykład szefowie kafeterii Coffeina chcieliby mieć nową aplikację,


by przyciągnąć liczniejszą klientelę do swoich lokali. Poniżej przedstawiliśmy
kilka ich pomysłów na zawartość aplikacji:

łowe
Szczegó żdym
cje o ka
informa
napoju
Lista wszystkich
naszych lokali Adresy i godz
iny
otwarcia
Menu zawierając wszystkich na
e szych
wszystko, co lokali
można u nas zjeś
ć
Lista oferowanych
napojów
Ekran początkowy
e z listą opcji
Szczegółow
je o każdej
informac
nu
pozycji me

To wszystko są pomysły, które użytkownicy aplikacji na pewno


uznają za przydatne. Ale w jaki sposób możemy je wszystkie zebrać
i przekształcić w intuicyjną, dobrze zorganizowaną aplikację?

228 Rozdział 6.
Widoki list i adaptery

Skategoryzuj swoje pomysły — aktywności:


poziom główny, kategoria i szczegóły/edycja
Przydatnym sposobem, który pomoże zaprowadzić porządek w naszych
pomysłach, będzie podzielenie ich na trzy różne typy aktywności: aktywności
poziomu głównego, aktywności kategorii oraz aktywności szczegółów/edycji.

Aktywności poziomu głównego Ekran


Aktywność poziomu głównego zawiera te rzeczy, które początkowy
są najważniejsze dla użytkownika, i pozwala mu na łatwe z listą opcji
przechodzenie do nich. W większości przypadków pierwszą
aktywnością, którą użytkownik zobaczy po uruchomieniu
aplikacji, będzie właśnie aktywność poziomu głównego.

Lista wszystkich
Aktywności kategorii Menu zawierające naszych lokali
Aktywności kategorii prezentują dane należące do wszystko, co
konkretnej kategorii i często przybierają postać list. można u nas Lista
Aktywności tego typu zazwyczaj zapewniają użytkownikom zjeść oferowanych
możliwość przejścia do kolejnej aktywności — szczegółów/
napojów
edycji. Przykładem aktywności kategorii może być lista
wszystkich napojów oferowanych przez kafeterię Coffeina.

Aktywności szczegółów/edycji łowe


Szczegó Adresy i godz
iny
cje
Aktywności typu szczegółów/edycji wyświetlają informa otwarcia
y m Szczegółowe
szczegółowe informacje o konkretnym rekordzie i pozwalają o każd wszystkich
użytkownikom dodawać nowe rekordy. Przykładem napoju informacje naszych lokali.
zycji
aktywności tego typu może być aktywność wyświetlająca o każdej po
szczegółowe informacje o konkretnym napoju. menu

Określiwszy, do której z tych kategorii należą poszczególne


aktywności, możemy ich użyć do utworzenia hierarchii pokazującej,
jak użytkownik będzie nawigował po aplikacji i jej poszczególnych
aktywnościach.

Zastanów się, jaką aplikację chcesz napisać. Jakie aktywności powinna zawierać?
Podziel je na aktywności poziomu głównego, kategorii oraz szczegółów/edycji.
Ćwiczenie

jesteś tutaj  229


Zorganizuj swoje pomysły

Nawigowanie po aktywnościach
Podczas organizowania pomysłów i klasyfikowaniu ich na aktywności
poziomu głównego, kategorii oraz szczegółów/edycji możemy na podstawie
tej klasyfikacji od razu określić, w jaki sposób będzie można poruszać się
po projektowanej aplikacji. Ogólnie rzecz biorąc, chcemy, by użytkownik
przechodził od aktywności poziomu głównego przez aktywności kategorii
do aktywności szczegółów/edycji.

Aktywności poziomu głównego Ekran


początkowy
są na samej górze z listą opcji
To właśnie te aktywności użytkownik
zobaczy jako pierwsze.

Aktywności kategorii
Lista Menu zawierające Lista wszystkich
znajdują się pomiędzy oferowanych wszystko, co naszych lokali
aktywnościami poziomu napojów można u nas zjeść
głównego i aktywnościami
szczegółów/edycji
Użytkownicy będą przechodzili od
aktywności poziomu głównego do
aktywności kategorii. W złożonych
aplikacjach może występować kilka
poziomów kategorii i podkategorii.

Aktywności Szczegółowe Szczegółowe Adresy i godziny


szczegółów/edycji informacje informacje otwarcia
o każdym napoju o każdej pozycji wszystkich
Tworzą one dolną warstwę
menu naszych lokali
hierarchii aktywności. Użytkownicy
będą do nich docierali
z aktywności kategorii.

W ramach przykładu załóżmy, że użytkownik chce wyświetlić szczegółowe


informacje o jednym z napojów dostępnych w kafeteriach Coffeina. W tym celu
uruchamia aplikację, która najpierw wyświetla aktywność poziomu głównego —
ekran powitalny z listą opcji. Następnie użytkownik naciska opcję wyświetlenia listy
napojów. W końcu, aby wyświetlić szczegółowe informacje o konkretnym napoju,
użytkownik musi odnaleźć go na liście i kliknąć.
230 Rozdział 6.
Widoki list i adaptery

Użyj ListViews do nawigowania po danych


W przypadku zastosowania takiej struktury aplikacji będziemy potrzebowali sposobu
pozwalającego na przechodzenie do poszczególnych aktywności. Popularnym rozwiązanie
stosowanym w takich sytuacjach jest widok listy. Widok listy pozwala wyświetlić listę
danych, których następnie można użyć do nawigowania po aplikacji.

A teraz mały przykład. Na poprzedniej stronie napisaliśmy, że w skład aplikacji


dla kafeterii Coffeina wchodzi aktywność kategorii prezentująca listę napojów.
Poniżej pokazaliśmy, jak ta aktywność może wyglądać.

To jest widok ListView


zawierający listę napojów.

Aktywność wykorzystuje widok listy do wyświetlania wszystkich napojów oferowanych


w kafeteriach Coffeina. Aby przejść do konkretnego napoju, użytkownik musi kliknąć
odpowiednią opcję listy, a w efekcie zostaną wyświetlone szczegółowe informacje na
jego temat.

Jeśli klikniesz opcję


Latte na liście ListView,
to aplikacja wyświetli
szczegółowe informacje
na temat latte.

Całą pozostałą część rozdziału poświęcimy na pokazanie, jak


używać widoków list, by zaimplementować aplikację działającą
w opisany powyżej sposób.
jesteś tutaj  231
Witamy w kafeterii Coffeina
część
Napiszemy aplikacjęi dla kafeterii Coffeina
Zamiast tworzyć wszystkie aktywności kategorii i szczegółów/edycji składające
się na całą aplikację dla kafeterii Coffeina, skoncentrujemy się wyłącznie na
napojach. Napiszemy aktywność poziomu głównego wyświetlaną bezpośrednio
po uruchomieniu aplikacji, aktywność kategorii prezentującą listę napojów
oraz aktywność szczegółów/edycji przedstawiającą szczegółowe informacje
o wybranym napoju.

Aktywność poziomu głównego


Kiedy użytkownik uruchamia aplikację, zostanie
wyświetlona aktywność poziomu głównego
stanowiąca punkt wejścia do całej aplikacji.
Aktywność ta będzie zawierała logo kafeterii
Coffeina i listę elementów nawigacyjnych:
Napoje, Przekąski oraz Kafeterie.
Logo kafeterii Coffeina
Gdy użytkownik kliknie któryś z elementów tej i lista opcji. My zajmiemy
się zaimplementowaniem
listy, aplikacja odczyta klikniętą opcję i na jej opcji Napoje.
podstawie uruchomi inną, wybraną aktywność.
Na przykład jeśli użytkownik klinie element
Napoje, to zostanie uruchomiona aktywność
prezentująca listę napojów.

Aktywność kategorii z listą napojów


Ta aktywność będzie uruchamiana, kiedy
użytkownik kliknie opcję Napoje na liście
nawigacyjnej prezentowanej przez aktywność
poziomu głównego. Jej zadaniem jest
wyświetlanie listy wszystkich napojów, które są
dostępne w kafeteriach Coffeina. Użytkownik Wyświetlimy tylko trzy
może kliknąć jeden z napojów, aby wyświetlić napoje, choć w ofercie
kafeterii Coffeina jest ich
szczegółowe informacje na jego temat. na pewno znacznie więcej.

232 Rozdział 6.
Widoki list i adaptery

Aktywność szczegółów napoju


Aktywność napoju jest uruchamiana, kiedy użytkownik
kliknie jedną z opcji listy napojów wyświetlanej przez
aktywność kategorii.

Ta aktywność odpowiada za wyświetlenie szczegółowych


informacji o napoju wybranym przez użytkownika.
Do tych informacji będzie należało zdjęcie napoju,
jego nazwa oraz opis.

Ta aktywność wyświetla
szczegółowe informacje
dotyczące konkretnego
napoju.

Jak użytkownik porusza się po aplikacji?


Użytkownik zaczyna nawigowanie po aplikacji od aktywności poziomu
głównego, a następnie, klikając opcję Napoje, przechodzi do aktywności
kategorii. Gdy kliknie wybrany napój, może przejść do kolejnej
aktywności, prezentującej szczegółowe informacje o napoju.

Kiedy użytkownik
kliknie wybrany
napój, zostaną
Użytkownik klika wyświetlone
element Napoje, informacje o tym
co powoduje napoju.
wyświetlenie listy
napojów.

jesteś tutaj  233


Struktura aplikacji

Struktura aplikacji dla kafeterii Coffeina


Nasza aplikacja składa się z trzech aktywności. Aktywność głównego poziomu nosi
nazwę TopLevelActivity i umożliwia użytkownikowi poruszanie się po aplikacji.
Aktywność DrinkCategoryActivity to aktywność kategorii, która wyświetla
listę napojów. A ostatnia aktywność, DrinkActivity, służy do wyświetlania
szczegółowych informacji o wybranym napój.

Na razie wszystkie informacje dotyczące napoju będą przechowywane w klasie Javy.


W jednym z kolejnych rozdziałów przeniesiemy je do bazy danych, a teraz chcemy
się skoncentrować na implementacji innych aspektów aplikacji, nie zawracając sobie
głowy bazami danych.

1 Podczas uruchamiania aplikacji zostaje wyświetlona aktywność TopLevelActivity.


Ta aktywność korzysta z układu activity_top_level.xml. Jej działanie sprowadza się do wyświetlenia
listy opcji: Napoje, Przekąski oraz Kafeterie.
Nie musimy
tw
specjalnego uk orzyć
2 Na liście prezentowanej przez aktywność TopLevelActivity użytkownik dla aktywnośc ładu
DrinkCategoryAi
klika opcję Napoje.
Już niebawem ctivity.
W efekcie zostaje uruchomiona aktywność DrinkCategoryActivity, się, dlaczego przekonasz
nie jest to
która wyświetla listę napojów. konieczne.

3 Szczegółowe informacje o napoju są zapisane w klasie Drink zdefiniowanej w pliku Drink.java.


Aktywność DrinkCategoryActivity uzyskuje wartości wyświetlane na liście napojów właśnie z tej klasy.

4 Użytkownik klika jeden z napojów wyświetlony na liście w aktywności DrinkCategoryActivity.


W efekcie zostaje uruchomiona aktywność DrinkActivity. Korzysta ona z układu o nazwie activity_drink.xml.

5 Aktywność DrinkActivity pobiera szczegółowe informacje o napoju z klasy zdefiniowanej


w pliku Drink.java.

<Layout>
<Layout>

</Layout>
</Layout>
Ale dlaczego ta
aktywność nie Drink.java activity_drink.xml
activity_top_level.xml wymaga definiowania 5
układu? Dowiesz się
3
tego już niebawem.

1
2 4

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie

234 Rozdział 6.
Widoki list i adaptery

Oto czynności, które wykonamy


W ramach prac nad naszą aplikacją musimy wykonać kilka czynności:

1 Dodać do projektu klasę Drink i zasoby graficzne.


Ta klasa zawiera szczegółowe informacje o wszystkich
dostępnych napojach. Jeśli chodzi o zasoby graficzne,
to należy do nich logo kafeterii Coffeina i zdjęcia
poszczególnych napojów.

2 Utworzyć klasę TopLevelActivity i jej układ.


To jest punkt wejścia do aplikacji. Aktywność musi wyświetlić
logo kafeterii Coffeina i listę z opcjami do nawigowania po
aplikacji. Oprócz tego po kliknięciu opcji Napoje aktywność
TopLevelActivity musi spowodować uruchomienie
aktywności DrinkCategoryActivity.

3 Utworzyć aktywność DrinkCategoryActivity.


Aktywność DrinkCategoryActivity zawiera listę wszystkich
dostępnych napojów. Po kliknięciu któregoś z napojów ma
ona uruchomić aktywność DrinkActivity.

4 Utworzyć aktywność DrinkActivity.


Aktywność DrinkActivity wyświetla szczegółowe informacje
o napoju klikniętym przez użytkownika na liście w aktywności
DrinkCategoryActivity.

Utworzenie projektu ¨  Dodanie zasobów


Projekt naszej nowej aplikacji możesz utworzyć w dokładnie taki sam ¨  TopLevelActivity
sposób, w jaki tworzyłeś projekty w poprzednich rozdziałach. ¨  DrinkCategoryActivity
¨  DrinkActivity
Utwórz projekt aplikacji o nazwie Coffeina i umieść go w pakiecie
com.hfad.coffeina. Jako minimalną wersję SDK wybierz API
poziomu 15. Utwórz od razu aktywność o nazwie TopLevelActivity
i plik układu o nazwie activity_top_level.xml.

jesteś tutaj  235


Klasa Drink

Klasa Drink ¨  Dodanie zasobów


¨  TopLevelActivity
¨  DrinkCategoryActivity
Zaczniemy od dodania do aplikacji klasy Drink. Drink.java to zupełnie zwyczajny plik ¨  DrinkActivity
zawierający klasę napisaną w Javie, z której aktywności będą pobierać dane o napojach.
Klasa Drink definiuje tablicę trzech napojów, przy czym każdy z nich będzie zawierał
informacje o nazwie napoju, jego opis oraz identyfikator zasobu graficznego z jego zdjęciem.
Dodaj zatem klasę do pakietu com.hfad.coffeina, zapisz ją w projekcie, w katalogu app/src/
main/java, a samej klasie nadaj nazwę Drink. Po wpisaniu kodu klasy zapisz plik.

package com.hfad.coffeina;
Każdy Drink ma nazwę i opis oraz
identyfikator zasobu graficznego. zawiera
public class Drink { Te identyfikatory
zasobów określają zdjęcia napojów,
private String name; do projektu na następnej stronie. które dodamy
private String description;
private int imageResourceId;
drinks to tablica trzech obiektów klasy Drink.
// drinks to tablica obiektów klasy Drink
public static final Drink[] drinks = {
To są zdjęcia new Drink(”Latte”, ”Czarne espresso z gorącym mlekiem i mleczną pianką.”,
napojów. R.drawable.latte),
Zaraz się nimi new Drink(”Cappuccino”, ”Czarne espresso z dużą ilością spienionego mleka.”,
zajmiemy. R.drawable.cappuccino),
new Drink(”Espresso”, ”Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”,
R.drawable.filter)
}; Konstruktor klasy Drink.

// Każdy Drink ma nazwę, opis oraz zasób graficzny


private Drink(String name, String description, int imageResourceId) {
this.name = name;
this.description = description;
this.imageResourceId = imageResourceId;
}
Coffeina
public String getDescription() {
return description;
} app/src/main

To są metody get, akcesory,


public String getName() { do pobierania informacji ze java
return name; zmiennych prywatnych.
} com.hfad.coffeina

public int getImageResourceId() {


return imageResourceId; Drink.java
}

public String toString() { Łańcuchową reprezentacją


return this.name; obiektu Drink jest nazwa
} napoju.
}

236 Rozdział 6.
Widoki list i adaptery

Pliki graficzne
Kod klasy Drink zawiera trzy zasoby graficzne, które są używane
w informacjach o napojach i mają następujące identyfikatory:
R.drawable.latte, R.drawable.cappuccino oraz R.drawable.
filter. Identyfikator R.drawable.latte odwołuje się do pliku
graficznego o nazwie latte, identyfikator R.drawable.cappuccino do
pliku cappuccino, a identyfikator R.drawable.filter do pliku filter.

Te trzy pliki graficzne musimy teraz dodać do projektu, wraz z logo


kafeterii Coffeina, które będzie używane w aktywności głównego
poziomu. Wszystkie te pliki graficzne można znaleźć w przykładach
dołączonych do książki, które można pobrać z serwera FTP
wydawnictwa Helion — ftp://ftp.helion.pl/przyklady/andrrg.zip.
Zaznacz je, a następnie przeciągnij do katalogu app/src/main/res/
drawable swojego projektu aplikacji kafeterii Coffeina.

Po dodaniu obrazów do projektu musimy zdecydować, czy chcemy


wyświetlać różne obrazy na ekranach o różnej gęstości. My mamy
zamiar zawsze wyświetlać obrazy o tej samej rozdzielczości,
niezależnie od gęstości ekranu, dlatego do projektu dodaliśmy tylko
jeden egzemplarz każdego z niezbędnych pików i umieściliśmy
je wszystkie w jednym katalogu. Jeśli we własnych aplikacjach
zdecydujesz się używać różnych wersji obrazów dla różnych gęstości
ekranów, będziesz musiał umieszczać odpowiednie wersje plików
graficznych w odpowiednich katalogach drawable* (zgodnie
z informacjami podanymi w rozdziale 5.).

Oto cztery pliki graficzne. Dodałeś je


do Android Studio, przeciągając je do
katalogu drawable.

Po dodaniu do projektu Android przypisuje każdemu z nich


identyfikator o postaci R.drawable.nazwa_pliku. Na przykład
plikowi latte.png zostanie nadany identyfikator R.drawable.latte,
który będzie odpowiadał identyfikatorowi zasobu obrazka latte
podanemu w kodzie klasy Drink.

name: „Latte”

description: „Czarne espresso


z gorącym mlekiem i mleczną pianką.”

Drink imageResourceId: R.drawable.latte


Plikowi latte.png
został nadany
Skoro dodaliśmy już do projektu klasę Drink i pliki graficzne, identyfikator
R.drawable.latte.
zajmijmy się implementacją aktywności. Zaczniemy od R.drawable.latte
aktywności głównego poziomu.

jesteś tutaj  237


TopLevelActivity

Układ aktywności głównego poziomu ¨  Dodanie zasobów


¨  TopLevelActivity
składa się z obrazka i listy ¨  DrinkCategoryActivity
¨  DrinkActivity
Podczas tworzenia projektu nadaliśmy naszej aktywności głównego poziomu nazwę
TopLevelActivity, a układ, z którego ona korzysta, zapisaliśmy w pliku activity_top_level.xml.
Musimy teraz zmienić ten układ w taki sposób, by prezentował obrazek i listę.

To jest logo kafeterii Coffeina.


Ten obrazek dodaliśmy do
projektu na poprzedniej stronie.

Statyczna lista opcji.

Sposób wyświetlania obrazów za pomocą komponentów ImageView


znasz z poprzedniego rozdziału. W tym przypadku chcemy wyświetlić
logo kafeterii Coffeina, więc utworzymy komponent ImageView,
dla którego źródłem danych będzie plik graficzny starbuzz_logo.png.

Oto kod definiujący ten komponent w pliku układu:


<ImageView To są wymiary, które chcemy
android:layout_width=”200dp” nadać obrazkowi.

android:layout_height=”100dp” Źródłem obrazka będzie plik starbuzz_logo.png,


który wcześniej dodaliśmy do aplikacji.
android:src=”@drawable/starbuzz_logo”
android:contentDescription=”@string/starbuzz_logo” /> Dodanie opisu zawartości poprawi
dostępność aplikacji.

Stosując w aplikacjach komponenty ImageView, należy dodawać do nich atrybut


android:contentDescription zawierający opis prezentowanego obrazu, takie Coffeina
rozwiązanie poprawia bowiem dostępność aplikacji. W naszym przypadku atrybut ten
zawiera odwołanie do zasobu łańcuchowego, ”@string/starbuzz_logo”. Musimy
app/src/main
zatem dodać ten zasób do pliku strings.xml:

<resources> res
...
<string name=”starbuzz_logo”>Logo kafeterii Coffeina</string> values
<xml>
</resources> </xml>

strings.xml
To już wszystko, co trzeba zrobić, by dodać obraz do układu, zajmijmy się więc listą.

238 Rozdział 6.
Widoki list i adaptery

Użycie widoku listy do wyświetlania listy opcji


Jak już wspominaliśmy wcześniej, widok listy umożliwia wyświetlenie pionowej listy android.view.View
danych, której można użyć do nawigowania po aplikacji. Dlatego też dodamy do
...
układu widok listy prezentujący trzy opcje, których później użyjemy do przechodzenia
do innych aktywności.

Jak zdefiniować widok listy w kodzie XML? android.view.ViewGroup


Widok listy możemy dodać do układu, używając elementu <ListView>. Zawartość ...
tak utworzonego widoku listy określamy za pomocą atrybutu android:entries,
zawierającego identyfikator tablicy łańcuchów znaków. Poszczególne łańcuchy z tej
tablicy zostaną następnie wyświetlone w widoku listy jako lista widoków tekstowych.
android.widget.
Poniżej pokazaliśmy, w jaki sposób można dodać do układu widok listy, którego AdapterView
zawartość zostanie określona na podstawie tablicy łańcuchów znaków o nazwie options: ...
Ten element definiuje widok listy.
<ListView
android:id=”@+id/list_options”
Wartości android.widget.ListView
android:layout_width=”match_parent” wyświetlane
android:layout_height=”wrap_content” na liście zostały ...
zdefiniowane
android:entries=”@array/options” /> w tablicy options.

Samą tablicę możemy zdefiniować w dokładnie taki sam sposób, w jaki robiliśmy
to już wcześniej — dodając odpowiedni element do pliku strings.xml:

<resources>
.... Coffeina
<string-array name=”options”>
<item>Napoje</item> app/src/main
<item>Przekąski</item>
<item>Kafeterie</item> res

</string-array>
values
....
<xml>
</resources> </xml>

strings.xml
W ten sposób w widoku listy zostaną wyświetlone trzy wartości: Napoje,
Przekąski oraz Kafeterie.
@array/options Atrybut entries wypełnia
komponent ListView
<resources> wartościami pobranymi
z tablicy options. Każdy
</resources> z elementów komponentu
Napoje ListView jest widokiem
ListView strings.xml tekstowym.
Przekąski
Kafeterie

jesteś tutaj  239


Kod układu

Kompletny kod układu aktywności głównego poziomu ¨  Dodanie zasobów


¨  TopLevelActivity
¨  DrinkCategoryActivity
Oto pełny kod układu (upewnij się, czy Twój układ będzie dokładnie taki sam jak nasz): ¨  DrinkActivity

<LinearLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
Używamy układu liniowego o układzie
android:layout_height=”match_parent” pionowym. Dzięki temu lista opcji zostanie
android:orientation=”vertical” wyświetlona bezpośrednio poniżej logo
kafeterii Coffeina.
tools:context=”.TopLevelActivity” >

<ImageView Coffeina
android:layout_width=”200dp”
android:layout_height=”100dp” app/src/main
android:src=”@drawable/starbuzz_logo”
android:contentDescription=”@string/starbuzz_logo” /> res

layout
<ListView
<xml>
android:id=”@+id/list_options” </xml>

android:layout_width=”match_parent” activity_
top_level.xml
android:layout_height=”wrap_content”
android:entries=”@array/options” />
</LinearLayout>

Jazda próbna
Upewnij się, że wprowadziłeś wszystkie modyfikacje w plikach
activity_top_level.xml i strings.xml. Kiedy uruchomisz aplikację,
powinieneś zobaczyć na ekranie urządzenia logo kafeterii
Coffeina, a pod nim widok listy. Ten widok powinien
prezentować trzy wartości pobrane z tablicy options.

Gdy klikniesz dowolną opcję z tej listy, nic się nie stanie —
To są wartości
wynika to z prostego faktu, że jeszcze nie kazaliśmy widokowi zapisane w tablicy
listy reagować na kliknięcia. Teraz mamy zamiar pokazać, jak options.
sprawić, by widok listy reagował na kliknięcia i uruchamiał inną
aktywność.

240 Rozdział 6.
Widoki list i adaptery

Zapewnianie reakcji ListView na kliknięcia


za pomocą obiektu nasłuchującego Komponent ListView musi wiedzieć,
że aktywność obchodzi to, co się
z nim dzieje.
Elementy widoku listy mogą reagować na kliknięcia poprzez
zaimplementowanie obiektu nasłuchującego (ang. event listener).

Obiekt nasłuchujący pozwala nam oczekiwać na zdarzenia zachodzące


w aplikacji, takie jak kliknięcie widoku, utrata lub uzyskanie miejsca
wprowadzania, a nawet naciśnięcie przycisku sprzętowego na
urządzeniu. Jeśli zaimplementujemy taki obiekt nasłuchujący, możemy Aktywność ListView
określić, kiedy użytkownik wykonał określoną czynność — taką jak
kliknięcie widoku — i zareagować na nią. Komponent ListView informuje aktywność
o tym, że jeden z jego elementów został
kliknięty, aby ta mogła na to zdarzenie
OnItemClickListener oczekuje na kliknięcia zareagować.

Jeśli chcemy, aby elementy listy reagowały na kliknięcia, musimy


zaimplementować obiekt typu OnItemClickListener i jego metodę
onItemClick(). Obiekt nasłuchujący typu OnItemClickListener oczekuje na
zdarzenia związane z kliknięciem, a jego metoda onItemClick() pozwala nam
określić, jak należy na te kliknięcia zareagować. Metoda ta ma kilka parametrów,
których możemy użyć do określenia, który element został kliknięty, na przykład
pobrać referencję do klikniętego widoku, jego położenie na liście (liczone od 0)
czy też nieprzetworzony identyfikator prezentowanych na nim danych.

W przypadku naszej aplikacji chcemy, by po kliknięciu pierwszego elementu


listy, elementu w położeniu o indeksie 0, została uruchomiona aktywność
DrinkCategoryActivity. Jeśli zostanie kliknięty ten element, musimy
utworzyć intencję odwołującą się do aktywności DrinkCategoryActivity. OnItemClickListener jest klasą
Poniżej przedstawiliśmy kod obiektu nasłuchującego: zagnieżdżoną, która jest umieszczona
wewnątrz klasy AdapterView.
ListView jest klasą pochodną klasy
AdapterView.OnItemClickListener itemClickListener = AdapterView.

new AdapterView.OnItemClickListener(){
public void onItemClick(AdapterView<?> listView, To widok, który został kliknięty
Napoje to pierwszy element wyświetlony (w naszym przypadku jest to widok listy).
View itemView,
w widoku listy, co oznacza, że znajduje
się on w miejscu o indeksie 0. int position, Te parametry dają nam nieco więcej informacji o tym,
long id) { który element listy został kliknięty i jakie jest jego
położenie na liście.
if (position == 0) {
Intent intent = new Intent(TopLevelActivity.this, DrinkCategoryActivity.class);
startActivity(intent);
Intencja pochodzi z aktywności Ma ona spowodować
} TopLevelActivity. uruchomienie aktywności
} DrinkCategoryActivity.
};

Po utworzeniu obiektu nasłuchującego musimy go dodać do komponentu ListView.

jesteś tutaj  241


setOnItemClickListener()

Dodanie obiektu nasłuchującego do widoku listy ¨  Dodanie zasobów


¨  TopLevelActivity
¨  DrinkCategoryActivity
Po utworzeniu obiektu OnItemClickListener musimy powiązać go z widokiem ¨  DrinkActivity
listy. Służy do tego metoda ListView.setOnItemClickListener(). Ma ona tylko
jeden argument — sam obiekt nasłuchujący:

AdapterView.OnItemClickListener itemClickListener =
new AdapterView.OnItemClickListener(){
public void onItemClick(AdapterView<?> listView,
...
}
};
ListView listView = (ListView) findViewById(R.id.list_options);
listView.setOnItemClickListener(itemClickListener); To jest utworzony wc
ześniej
obiekt nasłuchujący.
Dodanie obiektu nasłuchującego do widoku listy ma kluczowe znaczenie, gdyż
zapewnia on, że obiekt ten będzie powiadamiany o kliknięciach elementów
listy. Jeśli tego nie zrobimy, to elementy naszego widoku listy nie będą w stanie
reagować na kliknięcia.

Teraz wiesz już wszystko, co trzeba, by sprawić, że widok listy w aktywności


TopLevelActivity będzie reagował na kliknięcia.

Co się stanie po uruchomieniu kodu?


1 Metoda onCreate() aktywności TopLevelActivity tworzy obiekt OnItemClickListener
i dodaje go do widoku listy.

TopLevelActivity ListView onItemClickListener

2 Kiedy użytkownik klika jeden z elementów widoku listy,


zostaje wywołana metoda onItemClick() obiektu OnItemClickListener.
Jeśli użytkownik kliknie opcję Napoje, to obiekt nasłuchujący OnItemClickListener
utworzy intencję, która uruchomi aktywność DrinkCategoryActivity.

onItemClick() Intencja

ListView onItemClickListener DrinkCategoryActivity


242 Rozdział 6.
Widoki list i adaptery

Kompletny kod aktywności TopLevelActivity ¨  Dodanie zasobów


¨  TopLevelActivity
¨  DrinkCategoryActivity
Poniżej przedstawiliśmy pełny kod aktywności TopLevelActivity, zapisany ¨  DrinkActivity
w pliku TopLevelActivity.java. Zastąp nim kod wygenerowany przez kreator
Android Studio i zapisz wprowadzone zmiany.

package com.hfad.coffeina;
Coffeina

import android.app.Activity;
app/src/main
import android.content.Intent;
import android.os.Bundle;
java
import android.widget.AdapterView;
import android.widget.ListView; Korzystamy z tych dodatkowych klas. com.hfad.coffeina
import android.view.View;
TopLevel
public class TopLevelActivity extends Activity { Activity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_top_level);
Tworzymy obiekt
// Tworzymy obiekt nasłuchujący OnItemClickListener nasłuchujący.
AdapterView.OnItemClickListener itemClickListener =
new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> listView,
View v,
Implementujemy jego
int position, metodę onItemClick().
long id) {
if (position == 0) {
Intent intent = new Intent(TopLevelActivity.this,
DrinkCategoryActivity.class);
startActivity(intent);
} Jeśli użytkownik kliknie element
Napoje, to uruchamiamy aktywność
} DrinkCategoryActivity. Utworzymy ją już
za chwilę, więc nie przejmuj się, jeśli
}; Android Studio zacznie Cię ostrzegać,
że jej nie ma.
// Dodajemy obiekt nasłuchujący do widoku listy
ListView listView = (ListView) findViewById(R.id.list_options);
listView.setOnItemClickListener(itemClickListener); Dodajemy obiekt nasłuchujący
} do widoku listy.

}
jesteś tutaj  243
Jesteś tutaj

Dokąd dotarliśmy? ¨  Dodanie zasobów


¨  TopLevelActivity
¨  DrinkCategoryActivity
Dotychczas utworzyliśmy plik Drink.java oraz zaimplementowaliśmy aktywność
¨  DrinkActivity
TopLevelActivity i używany przez nią układ.
Ten plik dodaliśmy
w pierwszej kolejności.

<Layout>
<Layout>

</Layout>
</Layout>
Utworzyliśmy
aktywność Drink.java activity_drink.xml
TopLevelActivity activity_top_level.xml
i jej układ.

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie

Tą aktywnością zajmiemy
się w następnej kolejności.

Kolejną rzeczą, którą musimy się zająć, będzie utworzenie aktywności


TopLevelActivity, tak by aplikacja mogła ją uruchomić po kliknięciu
opcji Napoje w aktywności TopLevelActivity.

Nie istnieją
głupie pytania

P: Dlaczego musieliśmy tworzyć obiekt nasłuchujący,  O : Atrybutu android:onClick można używać w kodzie XML


żeby elementy komponentu ListView zaczęły reagować  układów wyłącznie w przyciskach lub w innych widokach, które
na kliknięcia? Czy nie wystarczyłoby użyć atrybutu  są klasami pochodnymi klasy Button, takich jak CheckBox lub
android:onClick w kodzie XML układu? RadioButton.
ListView nie jest klasą pochodną klasy Button, zatem w jej
przypadku zastosowanie atrybutu android:onClick nic by nie
dało. To właśnie dlatego musimy zaimplementować własny obiekt
nasłuchujący.

244 Rozdział 6.
Widoki list i adaptery

Poniżej przedstawiliśmy kod aktywności pochodzący z innego projektu. Kiedy użytkownik kliknie jeden
z elementów widoku listy, jej kod ma wyświetlić w komponencie TextView tekst prezentowany w danym
elemencie listy. Czy przedstawiony kod działa zgodnie z założeniami? A jeśli nie działa, to dlaczego?
Ćwiczenie Komponent TextView ma identyfikator text_view, a widok listy list_view.

package com.hfad.ch06_ex;

import android.app.Activity;
import android.os.Bundle;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.view.View;

public class MainActivity extends Activity {


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView = (TextView) findViewById(R.id.text_view);
AdapterView.OnItemClickListener itemClickListener =
new AdapterView.OnItemClickListener(){
public void onItemClick(AdapterView<?> listView,
View v,
int position,
long id) {
TextView item = (TextView) v;
textView.setText(item.getText());
}
};
ListView listView = (ListView) findViewById(R.id.list_view);
}
}

jesteś tutaj  245


Rozwiązanie ćwiczenia

Poniżej przedstawiliśmy kod aktywności pochodzący z innego projektu. Kiedy użytkownik kliknie jeden
z elementów widoku listy, jej kod ma wyświetlić w komponencie TextView tekst wyświetlony w danym
elemencie listy. Czy przedstawiony kod działa zgodnie z założeniami? A jeśli nie działa, to dlaczego?
Ćwiczenie Komponent TextView ma identyfikator text_view, a widok listy list_view.
Rozwiązanie

package com.hfad.ch06_ex;

import android.app.Activity;
import android.os.Bundle;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.view.View;

public class MainActivity extends Activity {


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView = (TextView) findViewById(R.id.text_view);
AdapterView.OnItemClickListener itemClickListener =
new AdapterView.OnItemClickListener(){
public void onItemClick(AdapterView<?> listView,
View v,
int position,
To jest kliknięty elementy long id) {
listy. To komponent TextView,
więc możemy odczytać TextView item = (TextView) v;
wyświetlany w nim tekst textView.setText(item.getText());
przy użyciu metody getText().
}
};
ListView listView = (ListView) findViewById(R.id.list_view);
}
}

Kod nie działa zgodnie z założeniami, gdyż na jego końcu brakuje wywołania
metody listView.setOnItemClickListener(itemClickListener);.
Z wyjątkiem tego jednego błędu kod jest w porządku.

246 Rozdział 6.
Widoki list i adaptery

Aktywność kategorii wyświetla dane jednej kategorii ¨  Dodanie zasobów


¨  TopLevelActivity
Jak już zaznaczyliśmy wcześniej, DrinkCategoryActivity jest przykładem aktywności
¨  DrinkCategoryActivity
¨  DrinkActivity
kategorii. Aktywności tego rodzaju prezentują dane należące do jednej kategorii, przy czym
bardzo często wyświetlają je w formie listy. Takiej aktywności można użyć, by przejść od
aktywności prezentującej informacje szczegółowe.

W naszym przykładzie użyjemy aktywności DrinkCategoryActivity, by wyświetlić listę


napojów. Kiedy użytkownik kliknie jeden z tych napojów, zostaną wyświetlone szczegółowe
informacje o tym napoju.

Aktywność DrinkCategoryActivity
wyświetla listę napojów. Kiedy
Kiedy użytkownik klinie użytkownik kliknie jeden z nich,
element Napoje, zostanie zostanie uruchomiona aktywność
uruchomiona aktywność DrinkActivity, która wyświetli
DrinkCategoryActivity. szczegółowe informacje
o wybranym napoju.

W tym celu utworzymy aktywność zawierającą pojedynczy widok listy, w którym


wyświetlimy listę wszystkich napojów. Ponieważ nasza aktywność ma zawierać tylko
i wyłącznie widok listy, bez żadnych dodatkowych komponentów GUI, możemy
użyć aktywności specjalnego typu, nazywanej aktywnością listy. Czym zatem jest
ta aktywność listy?

jesteś tutaj  247


ListActivity

ListActivity to aktywność zawierająca ¨  Dodanie zasobów


¨  TopLevelActivity
jedynie listę ¨  DrinkCategoryActivity
¨  DrinkActivity
Aktywność listy to typ aktywności, która została stworzona do
obsługi list. Aktywność tego typu jest automatycznie kojarzona
ListActivity jest klasą
z widokiem listy, dzięki czemu nie musimy go tworzyć samodzielnie. pochodną klasy Activity.
Oto przykładowy wygląd takiej aktywności:

android.app.Activity
...
Aktywność listy
zawsze dysponuje
swoim własnym
widokiem listy, więc
nie musimy go tworzyć
własnoręcznie. Wciąż android.app.ListActivity
musimy dostarczyć getListView()
dane na listę i już
zaraz pokażemy, onListItemClick()
jak to należy robić.
...

Stosowanie aktywności listy do wyświetlania danych kategorii ma dwie


podstawowe zalety:

 Nie musimy tworzyć własnego układu.


Aktywności list definiują używane układy w sposób programowy, zatem
stosując je, nie musimy tworzyć ani utrzymywać żadnych plików układów. ListActivity to
Układ generowany przez te aktywności składa się z pojedynczego widoku klasa pochodna
listy. W kodzie aktywności można odwołać się do tego widoku, wywołując
metodę getListView(). Metoda ta jest bardzo potrzebna, gdyż pozwala klasy Activity,
określić, które dane mają być widoczne na liście. wyspecjalizowana do
obsługi list. Korzysta
 Nie musimy implementować własnych obiektów nasłuchujących.
Klasa ListActivity implementuje obiekt nasłuchujący, który obsługuje ona z domyślnego
zdarzenia kliknięcia elementów listy. A więc zamiast samodzielnie tworzyć
układu zawierającego
taki obiekt i dodawać go do widoku listy, wystarczy zaimplementować
metodę onListItemClick() aktywności. Dzięki temu znacznie łatwiej pojedynczy
można sprawić, że lista będzie reagować na kliknięcia jej elementów.
komponent ListView.
Sam się o tym przekonasz już niebawem, kiedy zastosujemy metodę
onListItemClick() do uruchamiania innej aktywności.

Aktywności kategorii przeważnie muszą prezentować pojedynczy widok listy, którego


użytkownik może użyć, by przejść do szczegółowych informacji o wybranym elemencie,
dlatego doskonale nadają się do takich zastosowań.

A więc jak wygląda kod aktywności listy?

248 Rozdział 6.
Widoki list i adaptery

Jak utworzyć aktywność listy?


Poniżej pokazaliśmy, jak wygląda podstawowy kod aktywności listy. Jak Android Studio może automatycznie
widać, aktywności tego typu są tworzone niemal tak samo jak wszystkie inne. wygenerować dla nas plik układu.
Nie skorzystamy jednak z tej
Skorzystaj teraz z kreatora New Activity, aby dodać do swojego projektu możliwości, gdyż aktywności listy
nową aktywność o nazwie DrinkCategoryActivity, a następnie zastąp jej definiują swój własny układ.
wygenerowany kod kodem przedstawionym poniżej:
package com.hfad.coffeina;
Coffeina
import android.app.ListActivity; Ta aktywność musi dziedziczyć
po klasie ListActivity, a nie
import android.os.Bundle; Activity. app/src/main

public class DrinkCategoryActivity extends ListActivity {


java

@Override
com.hfad.coffeina
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Klasa ListActivity dziedziczy metodę
} onCreate() po klasie Activity. DrinkCategory
Już zaraz dodamy kod tej metody. Activity.java
}
Powyższy kod tworzy prostą aktywność listy o nazwie DrinkCategoryActivity. Ponieważ
jest to aktywność listy, musi dziedziczyć po klasie ListActivity, a nie Activity.

Kolejną różnicą między aktywnościami typu ListActivity a aktywnościami


dziedziczącymi bezpośrednio po klasie Activity jest to, że w przypadku tych pierwszych
nie musimy używać metody setContentView() do określenia układu, którego aktywność
ma używać. Sama aktywność potrafi o to zadbać.

Podobnie jak wszystkie inne aktywności, także aktywności list muszą być zarejestrowane
w pliku manifestu — AndroidManifest.xml. Jest to niezbędne, by mogły być używane
w aplikacji. Podczas tworzenia aktywności listy Android Studio automatycznie dodaje
odpowiedni kod do pliku manifestu.
<application
... >
<activity
android:name=”.TopLevelActivity” To jest pierwsza z aktywności,
android:label=”@string/app_name” które utworzyliśmy.
...
To jest nowa aktywność. Każdej
</activity> aktywności musi odpowiadać wpis
<activity w pliku AndroidManifest.xml.
Coffeina
android:name=”.DrinkCategoryActivity”
android:label=”@string/title_activity_drink_category” >
app/src/main
</activity> <xml>
</application> </xml>

AndroidManifest.xml
Skoro już utworzyliśmy aktywność listy, musimy teraz wypełnić ją danymi.
jesteś tutaj  249
Adaptery

android:entries działa na statycznych tablicach ¨  Dodanie zasobów


¨  TopLevelActivity
łańcuchów znaków zdefiniowanych w strings.xml ¨  DrinkCategoryActivity
¨  DrinkActivity
Kiedy pisaliśmy pierwszą aktywność, TopLevelActivity, mogliśmy powiązać dane
z widokiem listy, używając atrybutu android:entries zapisywanego w kodzie
XML układu. Rozwiązanie to działało, gdyż dane były przechowywane w formie
zasobu, jako statyczna tablica łańcuchów znaków. Tablica ta została zdefiniowana
w pliku strings.xml, dzięki czemu bez problemów mogliśmy się do niej odwołać
w następujący sposób:

android:entries=”@array/options”

gdzie options było nazwą tablicy łańcuchów znaków.


Dane o napojach
muszą pochodzić
Jednak atrybut android:entries można stosować wyłącznie w przypadku, Widok listy należy z tablicy drinks
gdy dane pochodzą ze statycznej tablicy zdefiniowanej w pliku strings.xml. wypełnić danymi zdefiniowanej
o napojach. w klasie Drink.
A co we wszystkich innych sytuacjach? Co zrobić, gdy dane pochodzą
z tablicy utworzonej programowo w kodzie Javy albo z bazy danych?
W takim przypadku zastosowanie atrybutu android:entries nie zadziała. drinks

Jeśli musimy powiązać widok listy z danymi pochodzącymi z innego


źródła niż zasób będący tablicą łańcuchów znaków, to będziemy musieli
zastosować inne rozwiązanie — będziemy musieli napisać w aktywności Drink.java
ListView
kod, który odpowiednio powiąże te dane z widokiem listy. W naszym
przypadku musimy powiązać widok listy z tablicą drinks w klasie Drink.

Jeśli dane nie są statyczne, to należy użyć adaptera


Jeśli w widoku listy musimy wyświetlić dane, które nie pochodzą z zasobu statycznego,
lecz na przykład z tablicy zdefiniowanej w kodzie Javy lub z bazy danych, to musimy
użyć adaptera. Adapter działa jak swoisty most łączący źródło danych z widokiem listy:

ListView Adapter Źródło Naszym źródłem


danych będzie
danych tablica, ale równie
dobrze moglibyśmy
użyć bazy danych lub
usługi internetowej.

Adapter stanowi most łączący widok listy ze źródłem


danych. Adaptery pozwalają widokom list wyświetlać
dane pochodzące z wielu różnych źródeł.

Dostępnych jest kilka różnych rodzajów adapterów. Na razie skoncentrujemy się


na adapterze ArrayAdapter.
250 Rozdział 6.
Widoki list i adaptery

Łączenie widoków list z tablicami za pomocą adaptera ArrayAdapter


ArrayAdapter to typ adaptera, który służy do wiązania tablic z widokami. Można Adapter działa jak
go używać z wieloma klasami pochodnymi klasy AdapterView, czyli na przykład
z widokami list i listami rozwijanymi. most łączący widok,
W naszym przypadku zastosujemy adapter ArrayAdapter do wyświetlenia obiekt typu View,
w widoku listy danych pochodzących z tablicy Drink.drinks. ze źródłem danych.
Utworzymy adapter ArrayAdapter, ArrayAdapter jest
To jest nasz widok listy. aby powiązać widok listy z naszą To jest
tablicą. nasza tablica.
typem adaptera
ListView Array Drink.
wyspecjalizowanego
Adapter drinks do operowania
na tablicach.

Aby skorzystać z adaptera ArrayAdapter, należy go najpierw zainicjować


i dołączyć do widoku listy.

Inicjalizacja adaptera ArrayAdapter wymaga w pierwszej kolejności określenia


typu danych przechowywanych w tablicy, którą chcemy powiązać z widokiem
listy. W wywołaniu konstruktora adaptera ArrayAdapter można podać trzy
parametry: obiekt Context (którym zazwyczaj jest bieżąca aktywność), zasób
układu określającego, jak mają być prezentowane poszczególne elementy tablicy,
oraz samą tablicę.

Poniżej przedstawiliśmy kod, który tworzy adapter do wyświetlania danych typu


Tablica zawiera dane typu Drink.
Drink przechowywanych w tablicy Drink.drinks:

ArrayAdapter<Drink> listAdapter = new ArrayAdapter<Drink>(


this, To jest wbudowany zasób
this odwołuje się do
bieżącej aktywności. android.R.layout.simple_list_item_1, układu. Informuje on adapter, że
Activity jest klasą poszczególne elementy tablicy mają
pochodną klasy Context. Drink.drinks); Tablica być wyświetlane w pojedynczych
widokach tekstowych.

Po utworzeniu adaptera musimy go powiązać z widokiem listy; służy do tego


metoda setAdapter() klasy ListView:

ListView listView = getListView();


listView.setAdapter(listAdapter);
Za kulisami adapter odczyta każdy element tablicy, skonwertuje elementy do
postaci łańcuchów znaków za pomocą metody toString(), a następnie umieści
je w widokach tekstowych. Na koniec wszystkie utworzone w ten sposób widoki
tekstowe zostaną wyświetlone jako odrębne wiersze widoku ListView.
jesteś tutaj  251
Zastosowanie adaptera ArrayAdapter

Dodanie adaptera ArrayAdapter do aktywności ¨  Dodanie zasobów


¨  TopLevelActivity
DrinkCategoryActivity ¨  DrinkCategoryActivity
¨  DrinkActivity
Zmienimy teraz kod aktywności zapisany w pliku DrinkCategoryActivity.java w taki
sposób, by używał adaptera ArrayAdapter do pobierania danych o napojach
zdefiniowanych w klasie Drink. Ten kod umieścimy w metodzie onCreate(),
tak by widok listy był wypełniany danymi podczas tworzenia aktywności.

Poniżej przedstawiliśmy kompletny kod aktywności (zaktualizuj swój plik,


tak aby był taki sam jak nasz, a następnie zapisz zmiany):

package com.hfad.coffeina;

Coffeina
import android.app.ListActivity;
import android.os.Bundle; app/src/main
import android.widget.ArrayAdapter;
import android.widget.ListView; Używamy tych dodatkowych klas. java

com.hfad.coffeina
public class DrinkCategoryActivity extends ListActivity {

DrinkCategory
@Override Activity.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listDrinks = getListView();
ArrayAdapter<Drink> listAdapter = new ArrayAdapter<Drink>(
this,
Ten fragment kodu
android.R.layout.simple_list_item_1, wypełnia widok listy
danymi pochodzącymi
Drink.drinks); z tablicy drinks.
listDrinks.setAdapter(listAdapter);
}
}

To wszystkie zmiany, które musisz wprowadzić


w kodzie aktywności, by w widoku listy były
wyświetlane dane napojów pochodzących
z tablicy Drink.drinks.

To są napoje pochodzące
z tablicy Drink.drinks.

252 Rozdział 6.
Widoki list i adaptery

Co się stanie po wykonaniu kodu?


1 Kiedy użytkownik klika opcję Napoje, zostaje uruchomiona aktywność DrinkCategoryActivity.
Ponieważ klasa DrinkCategoryActivity jest aktywnością listy, dysponuje domyślnym układem
zawierającym pojedynczy komponent ListView. Ten układ jest tworzony przez kod Javy w niewidoczny
dla nas sposób, a zatem nie jest definiowany przez kod XML.

DrinkCategoryActivity ViewGroup ListView

2 Aktywność DrinkCategoryActivity tworzy adapter ArrayAdapter<Drink> — adapter,


który operuje na tablicy obiektów Drink.

DrinkCategoryActivity ArrayAdapter<Drink>

3 Źródłem danych dla adaptera ArrayAdapter jest tablica drinks zdefiniowana w klasie Drink.
Adapter używa metody Drink.toString(), by zwrócić nazwę każdego z napojów.

Drink.toString()

DrinkCategoryActivity ArrayAdapter<Drink> Drink.drinks

4 Aktywność DrinkCategoryActivity wywołuje metodę setAdapter(), określając w ten


sposób, że widok listy ma używać utworzonego wcześniej adaptera ArrayAdapter.
Widok listy używa adaptera do wyświetlenia listy nazw napojów.

ListView

Drink.toString()
DrinkCategoryActivity

ArrayAdapter<Drink> Drink.drinks

jesteś tutaj  253


Jazda próbna

Jazda próbna aplikacji


Po uruchomieniu aplikacji wyświetlana jest aktywność TopLevelActivity, zupełnie tak
samo, jak było wcześniej. Po kliknięciu elementu Napoje zostaje uruchomiona aktywność
DrinkCategoryActivity. Wyświetla ona listę nazw wszystkich napojów zdefiniowanych
w klasie Drink.

Kliknij element Napoje,


by wyświetlić tablicę
napojów.

Przegląd aplikacji, czyli dokąd dotarliśmy


Do tej pory dodaliśmy do aplikacji plik Drink.java i utworzyliśmy
aktywności TopLevelActivity i DrinkCategoryActivity.

<Layout>
<Layout>

</Layout>
</Layout>
Te pliki już napisaliśmy.
activity_top_level.xml Drink.java activity_drink.xml

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie
Teraz zadbamy o to, by aktywnoś
ć
DrinkCategoryActivity uruchamiała
Kolejnym zadaniem, którym się zajmiemy, będzie zaimplementowanie aktywność DrinkActivity.
w kodzie aktywności DrinkCategoryActivity uruchamiania
aktywności DrinkActivity i przekazywanie do niej szczegółowych
danych o tym, który napój został kliknięty.

254 Rozdział 6.
Widoki list i adaptery

Zagadkowy basen
Twoim celem jest napisanie aktywności, która powiąże
zdefiniowaną w kodzie Javy tablicę kolorów z listą
rozwijaną. Wybierz kawałki kodu pływające
w basenie i umieść jest w pustych miejscach
w kodzie aktywności. Każdego fragmentu
z basenu możesz użyć tylko raz, lecz nie
wszystkie fragmenty będą Ci potrzebne.

...

public class MainActivity extends Activity {


String[] colors = new String[] {”czerwony”, ”pomarańczowy”, ”żółty”, ”zielony”,
”niebieski”};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Spinner spinner = ( ........ ) findViewById(R.id.spinner);
ArrayAdapter< ...... > adapter = new ArrayAdapter< ......... >(
. ....... ,
android.R.layout.simple_spinner_item,
colors);
spinner. ...........(adapter); Ten układ wyświetla każdą wartość
z tablicy jako pojedynczy wiersz listy
} rozwijanej.
}
Uwaga: Każdego fragmentu
kodu z basenu można użyć
tylko jeden raz!

String
this
colors Spinner
colors
setAdapter String

Odpowiedź znajdziesz na stronie 267.


jesteś tutaj  255
Obsługa kliknięć

Jak obsługiwaliśmy kliknięcia w aktywności ¨  Dodanie zasobów


¨  TopLevelActivity
TopLevelActivity? ¨  DrinkCategoryActivity
¨  DrinkActivity
We wcześniejszej części rozdziału musieliśmy zadbać o to, by aktywność TopLevelActivity
reagowała na kliknięcia elementów wyświetlonych w widoku listy. W tym celu musieliśmy
utworzyć obiekt typu OnItemClickListener, zaimplementować jego metodę onItemClick(),
a następnie przypisać go do widoku listy:

AdapterView.OnItemClickListener itemClickListener = new AdapterView.OnItemClickListener(){


public void onItemClick(AdapterView<?> listView, To jest widok listy.
View itemView, To są: widok reprezentujący kliknięty element listy,
int position, indeks określający jego położenie na liście oraz
identyfikator wiersza nieprzetworzonych danych.
long id) {
// Robimy coś w odpowiedzi na kliknięcie elementu
}
};
ListView listView = (ListView) findViewById(R.id.list_options);
listView.setOnItemClickListener(itemClickListener); Dodajemy obiekt nasłuchujący
do widoku listy.
Musieliśmy utworzyć obiekt nasłuchujący w taki sposób, gdyż w odróżnieniu na przykład
od przycisków widoki listy nie są domyślnie przygotowywane, by odpowiadać na kliknięcia.

A zatem w jaki sposób możemy sprawić, aby nasza aktywność DrinkCategoryActivity


obsługiwała kliknięcia?

Klasa ListActivity domyślnie implementuje interfejs obiektu


nasłuchującego kliknięć
Istnieje pewna kluczowa różnica między aktywnościami TopLevelActivity i DrinkCategoryActivity.
O ile pierwsza z nich jest normalnym obiektem klasy Activity, to druga jest typu ListActivity —
czyli jest aktywnością specjalnego typu, zaprojektowaną do obsługi widoków list.

Ten fakt ma bardzo duże znaczenie w kontekście obsługi kliknięć elementów listy. Podstawowa różnica
między aktywnościami Activity i ListActivity polega na tym, że klasa ListActivity domyślnie
implementuje interfejs obiektu nasłuchującego kliknięć. Dzięki temu, kiedy używamy aktywności
ListActivity, zamiast tworzyć własny obiekt nasłuchujący, wystarczy zaimplementować metodę
onListItemClick().
public void onListItemClick(ListView listView,
View itemView, To są te same argumenty, którymi
dysponuje przedstawiona powyżej
int position, metoda onItemClick(): widok listy,
widok reprezentujący kliknięty element
long id) { listy oraz identyfikator wiersza
// Tu coś robimy nieprzetworzonych danych.
}
256 Rozdział 6.
Widoki list i adaptery

Przekazywanie danych do aktywności


za pomocą metody onListItemClick klasy ListActivity
W razie stosowania aktywności listy do wyświetlania kategorii zazwyczaj
będziemy także używali metody onListItemClick() do uruchamiania
innej aktywności prezentującej szczegółowe informacje o elemencie
Intencja
klikniętym przez użytkownika. W tym celu musimy stworzyć intencję,
która uruchomi tę drugą aktywność. Następnie musimy dodać do
intencji identyfikator klikniętego elementu, tak by druga aktywność, drinkNo
kiedy zostanie już uruchomiona, mogła z niego skorzystać.

W naszym przypadku chcemy uruchomić aktywność DrinkActivity DrinkCategoryActivity DrinkActivity


i przekazać do niej identyfikator wybranego napoju. Aktywność
DrinkActivity będzie mogła skorzystać z tych informacji,
by wyświetlić szczegółowe informacje o wybranym napoju.
Poniżej przedstawiliśmy kod metody onListItemClick():
Ta metoda jest wywoływana po kliknięciu
public void onListItemClick(ListView listView, elementu listy.
View itemView,
Aktywność DrinkCategoryActivity musi
int position, uruchomić aktywność DrinkActivity.
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int) id);
Do intencji dodajemy
startActivity(intent); identyfikator klikniętego
elementu listy. To indeks
} napoju w tablicy drinks.
Nazwę informacji dodatkowej zapisywanej w intencji
zdefiniowaliśmy jako stałą, tak aby obie aktywności,
DrinkCategoryActivity i DrinkActivity, używały tego
samego łańcucha znaków. Tę stałą dodamy do klasy
DrinkActivity już niedługo, kiedy się nią zajmiemy.

Przekazywanie identyfikatora klikniętego elementu jest często


stosowanym rozwiązaniem, gdyż jednocześnie jest to identyfikator
nieprzetworzonych danych, na których operuje widok listy. Jeśli te
dane pochodzą z tablicy, to ten indeks będzie jednocześnie indeksem
elementu tej tablicy. Jeśli dane pochodzą z bazy danych, to zazwyczaj
będzie to identyfikator rekordu tabeli. Przekazywanie identyfikatora
klikniętego elementu w taki sposób zapewnia drugiej aktywności
możliwość prostego pobrania i wyświetlenia danych.

To już wszystko, czego nam potrzeba, by sprawić, aby aktywność


DrinkCategoryActivity uruchomiła aktywność DrinkActivity
i przekazała jej informację o tym, który napój został kliknięty.
Pełny kod aktywności zamieściliśmy na następnej stronie.

jesteś tutaj  257


Kod DrinkCategoryActivity

Kompletny kod aktywności DrinkCategoryActivity ¨  Dodanie zasobów


¨  TopLevelActivity
Oto kompletny kod aktywności DrinkCategoryActivity, umieszczony w pliku
¨  DrinkCategoryActivity
¨  DrinkActivity
DrinkCategoryActivity.java (dodaj do niego nową metodę, a następnie zapisz zmiany).

package com.hfad.coffeina;
Coffeina

import android.app.ListActivity;
app/src/main
import android.os.Bundle;
import android.widget.ArrayAdapter; java
import android.widget.ListView;
klas. com.hfad.coffeina
import android.view.View; Używamy tych dwóch dodatkowych
import android.content.Intent;
DrinkCategory
Activity.java
public class DrinkCategoryActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listDrinks = getListView();
ArrayAdapter<Drink> listAdapter =
new ArrayAdapter<Drink>(this,
android.R.layout.simple_list_item_1,
Drink.drinks);
listDrinks.setAdapter(listAdapter);
}

Implementujemy metodę
@Override onListItemClick(), tak aby kliknięcie
któregoś z napojów wyświetlonych
public void onListItemClick(ListView listView, na liście spowodowało uruchomienie
aktywności DrinkActivity.
View itemView,
int position,
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int) id);
startActivity(intent); Aktywność DrinkActivity dodamy
} do naszej aplikacji już za chwilę,
oid
więc nie przejmuj się, gdy Andr u.
Stud io poin form uje Cię o jej brak
}

258 Rozdział 6.
Widoki list i adaptery

¨  Dodanie zasobów
Aktywność szczegółów wyświetla informacje ¨  TopLevelActivity
o jednym rekordzie ¨  DrinkCategoryActivity
¨  DrinkActivity
Jak już zaznaczyliśmy wcześniej, aktywność DrinkActivity jest przykładem aktywności
szczegółów. Aktywności tego rodzaju prezentują szczegółowe informacje o konkretnym
rekordzie, a użytkownik zazwyczaj przechodzi do nich z aktywności kategorii.

Nasza aktywność DrinkActivity będzie wyświetlała szczegółowe informacje o napoju


klikniętym przez użytkownika. Obiekty klasy Drink przechowują nazwę napoju, jego opis
oraz identyfikator zasobu graficznego. I to właśnie te dane wyświetlimy w naszym układzie.
Umieścimy w nim obraz napoju określony przez identyfikator zasobu oraz dwa widoki
tekstowe prezentujące odpowiednio nazwę i opis napoju.

Poniżej przedstawiliśmy kod układu. Dodaj zatem do swojego projektu aktywność Nie zapomnij utworzyć
nowej aktywności.
o nazwie DrinkActivity, korzystającą z układu o nazwie activity_drink, a następnie
zastąp zawartość pliku activity_drink.xml poniższym kodem:

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent” Coffeina
android:layout_height=”match_parent”
android:orientation=”vertical” app/src/main
tools:context=”com.hfad.coffeina.DrinkActivity” >
res

<ImageView
layout
android:id=”@+id/photo” <xml>
android:layout_width=”190dp” </xml>

activity_drink.xml
android:layout_height=”190dp” />

<TextView
android:id=”@+id/name”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

<TextView
android:id=”@+id/description”
android:layout_width=”match_parent”
android:layout_height=”wrap_content” />

</LinearLayout>

Skoro już utworzyliśmy układ dla naszej aktywności szczegółów,


możemy się zająć wyświetleniem informacji w jego widokach.
jesteś tutaj  259
Pobranie napoju

Pobranie danych z intencji ¨  Dodanie zasobów


¨  TopLevelActivity
Jak się przekonałeś, aby sprawić, że aktywność kategorii będzie uruchamiać
¨  DrinkCategoryActivity
aktywność szczegółów, zadbaliśmy o to, by elementy widoku listy w aktywności
¨  DrinkActivity
kategorii odpowiadały na kliknięcia. Kiedy któryś z elementów tej listy zostanie
kliknięty, tworzymy intencję, która spowoduje uruchomienie aktywności szczegółów.
W tej intencji, jako informacja dodatkowa, przekazywany jest jednocześnie
identyfikator klikniętego elementu.

Po uruchomieniu aktywności szczegółów aktywność ta może pobrać te dodatkowe


informacje z intencji i użyć ich do pobrania danych i wyświetlenia ich w widokach
używanego układu. W naszym przypadku możemy użyć informacji przekazanych
w intencji, która doprowadziła do uruchomienia aktywności DrinkActivity,
aby pobrać szczegółowe informacje o napoju klikniętym przez użytkownika.

Pisząc kod aktywności DrinkCategoryActivity, identyfikator klikniętego napoju


dodaliśmy do intencji jako informacje dodatkowe. Nadaliśmy mu przy tym nazwę
DrinkActivity.EXTRA_DRINKNO, którą teraz musimy zdefiniować w klasie
DrinkActivity jako stałą:

public static final String EXTRA_DRINKNO = ”drinkNo”;

Jak się dowiedziałeś w rozdziale 3., intencję, która doprowadziła do uruchomienia


aktywności, można pobrać zapomocą metody getIntent(). Jeśli taka intencja
zawiera dodatkowe informacje, to można je pobrać, używając jednej z metod get*().
Poniżej przedstawiliśmy kod, który pozwala pobrać wartość EXTRA_DRINKNO:

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Po pobraniu informacji z intencji możemy ich użyć do pobrania danych,


które chcemy wyświetlić.

W naszym przypadku użyjemy zmiennej drinkNo, by pobrać informacje o napoju


wybranym przez użytkownika. Zmiana drinkNo zawiera identyfikator napoju —
indeks informacji o napoju zapisanych w tablicy drinks. Oznacza to, że szczegółowe
dane napoju klikniętego przez użytkownika możemy pobrać w następujący sposób:

Drink drink = Drink.drinks[drinkNo];

Ta instrukcja zwróci obiekt Drink zawierający wszystkie informacje, których


potrzebujemy do zaktualizowania widoku w aktywności:

name=”Latte”
description=”Czarne espresso z gorącym mlekiem i mleczną pianką.”
imageResourceId=R.drawable.latte

drink

260 Rozdział 6.
Widoki list i adaptery

Wypełnienie widoków danymi


Aktualizując widoki w aktywności szczegółów, musimy się upewnić, że wyświetlane
przez nie dane będą odpowiadać tym, które zostały przekazane w intencji. name
description
Nasza aktywność szczegółów zawiera dwa widoki tekstowe i jeden ImageView. imageResourceId
Musimy się zatem upewnić, że każdy z tych widoków zostanie zaktualizowany
i będzie prezentował szczegółowe informacje o wybranym napoju. drink

Magnesiki z napojami
Przekonajmy się, czy potrafisz użyć przedstawionych poniżej
magnesików do uzupełnienia kodu aktywności DrinkActivity
i wypełnienia jej widoków odpowiednimi danymi.

...

// Pobieramy identyfikator napoju z intencji

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Drink drink = Drink.drinks[drinkNo];

// Wyświetlamy zdjęcie napoju

ImageView photo = (ImageView)findViewById(R.id.photo);

photo. ................ (drink.getImageResourceId());


setText
photo. . ................... (drink.getName());
setContentDescription

// Wyświetlamy nazwę napoju


setContent
TextView name = (TextView)findViewById(R.id.name);

name. . .......... (drink.getName());


setImageResourceId

// Wyświetlamy opis napoju


setImageResource
TextView description = (TextView)findViewById(R.id.description);

description. ............... (drink.getDescription());


setText
...

jesteś tutaj  261


Rozwiązanie magnesików

Magnesiki z napojami. Rozwiązanie


Przekonajmy się, czy potrafisz użyć przedstawionych poniżej magnesików
do uzupełnienia kodu aktywności DrinkActivity i wypełnienia jej widoków
odpowiednimi danymi.

...

// Pobieramy identyfikator napoju z intencji

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Drink drink = Drink.drinks[drinkNo];

Źródło obrazka // Wyświetlamy zdjęcie napoju


określiliśmy,
używając metody ImageView photo = (ImageView)findViewById(R.id.photo);
setImageResource().
photo. . setImageResource .. (drink.getImageResourceId());
To wywołanie
jest potrzebne, photo. setContentDescription (drink.getName());
żeby poprawić
dostępność
aplikacji.
// Wyświetlamy nazwę napoju

TextView name = (TextView)findViewById(R.id.name);

name. setText (drink.getName());

Użyj metody // Wyświetlamy opis napoju


setText(), by
ustawić tekst
wyświetlany TextView description = (TextView)findViewById(R.id.description);
w widokach
tekstowych. description. .. setText . (drink.getDescription());

...

Te magnesiki nie były potrzebne.

setImageResourceId
setContent

262 Rozdział 6.
Widoki list i adaptery

Kod aktywności DrinkActivity ¨  Dodanie zasobów


¨  TopLevelActivity
Poniżej przedstawiliśmy kod zapisany w pliku DrinkActivity.java
¨  DrinkCategoryActivity
(zastąp cały kod wygenerowany przez kreator kodem przedstawionym
¨  DrinkActivity
poniżej, a następnie zapisz plik):
package com.hfad.coffeina;

Coffeina
import android.app.Activity;
import android.os.Bundle; app/src/main
import android.widget.ImageView;
import android.widget.TextView; java

public class DrinkActivity extends Activity { com.hfad.coffeina

public static final String EXTRA_DRINKNO = ”drinkNo”; DrinkActivity.java

@Override Dodaj EXTRA_DRINKNO jako stałą.

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drink);

// Pobieramy identyfikator napoju z intencji


int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);
Drink drink = Drink.drinks[drinkNo];
Użyj zmiennej drinkNo do pobrania
szczegółowych informacji o napoju
// Wyświetlamy zdjęcie napoju klikniętym przez użytkownika.

ImageView photo = (ImageView)findViewById(R.id.photo);


photo.setImageResource(drink.getImageResourceId());
photo.setContentDescription(drink.getName());

// Wyświetlamy nazwę napoju Wypełnij widoki


TextView name = (TextView)findViewById(R.id.name); danymi o napoju.

name.setText(drink.getName());

// Wyświetlamy opis napoju


TextView description = (TextView)findViewById(R.id.description);
description.setText(drink.getDescription());
}
}
jesteś tutaj  263
Co się dzieje?

¨  Dodanie zasobów
Co się stanie po uruchomieniu aplikacji? ¨  TopLevelActivity
¨  DrinkCategoryActivity
¨  DrinkActivity
1 Kiedy użytkownik uruchamia aplikację, zostaje wyświetlona aktywność TopLevelActivity.

TopLevelActivity
Urządzenie

2 Metoda onCreate() aktywności TopLevelActivity tworzy obiekt OnItemClickListener


i dodaje go do komponentu ListView aktywności.

TopLevelActivity ListView onItemClickListener

3 Kiedy użytkownik klika na liście opcję Napoje, zostaje wywołana metoda onItemClick()
obiektu nasłuchującego OnItemClickListener.
Jeśli użytkownik kliknie na liście opcję Napoje, to obiekt OnItemClickListener utworzy intencję,
która doprowadzi do uruchomienia aktywności DrinkCategoryActivity.

Intencja
onItemClick()

ListView onItemClickListener DrinkCategoryActivity

4 DrinkCategoryActivity jest aktywnością typu ListActivity.


Widok listy aktywności DrinkCategoryActivity używa adaptera ArrayAdapter<Drink>
do wyświetlenia listy nazw napojów.

ListView

DrinkCategoryActivity

ArrayAdapter<Drink> Drink.drinks

264 Rozdział 6.
Widoki list i adaptery

Ciąg dalszy historii

5 Kiedy użytkownik wybiera jeden z napojów wyświetlony w widoku ListView,


zostaje wywołana metoda onListItemClick() aktywności.

onListItemClick()

ListView DrinkCategoryActivity

6 Metoda onListItemClick() aktywności DrinkCategoryActivity tworzy intencję,


która uruchamia aktywność DrinkActivity, dodając do niej identyfikator napoju
jako informację dodatkową.
Intencja

drinkNo=0

DrinkCategoryActivity
DrinkActivity

7 Zostaje uruchomiona aktywność DrinkActivity.


Aktywność ta pobiera identyfikator napoju przekazany w intencji, pobiera informacje
o tym napoju z klasy Drink, a następnie używa ich do zaktualizowania widoków.

drinks[0]? Latte,
świetny wybór. To
jest wszystko, czym
drinks[0] dysponuję na jego
temat.

Latte
DrinkActivity Drink

jesteś tutaj  265


Jazda próbna

Jazda próbna aplikacji


Kiedy uruchomisz aplikację, tak jak wcześniej
zostanie uruchomiona aktywność TopLevelActivity.

Zaimplementowaliśmy
wyłącznie część aplikacji
dotyczącą napojów. Klikanie
pozostałych opcji tej listy
nie da żadnych rezultatów.

Gdy klikniesz opcję Napoje, zostanie uruchomiona


aktywność DrinkCategoryActivity. Wyświetla ona Kliknęliśmy opcję
listę wszystkich napojów opisanych w klasie Drink. Latte…

Kiedy klikniesz któryś z wyświetlonych napojów,


zostanie uruchomiona aktywność DrinkActivity, …i oto
a aplikacja wyświetli szczegółowe informacje na temat szczegółowe
informacje
wybranego napoju. o latte.

Te trzy aktywności pokazują, w jaki sposób można tworzyć


strukturę aplikacji, dzieląc ją na aktywność głównego poziomu,
aktywność kategorii oraz aktywność szczegółów/edycji. W dalszej
części książki wrócimy jeszcze do aplikacji kafeterii Coffeina
i pokażemy, jak można pobierać napoje z bazy danych.

266 Rozdział 6.
Widoki list i adaptery

Zagadkowy basen. Rozwiązanie


Twoim celem jest napisanie aktywności, która powiąże
zdefiniowaną w kodzie Javy tablicę kolorów
z listą rozwijaną. Wybierz kawałki kodu pływające
w basenie i umieść jest w pustych miejscach
w kodzie aktywności. Każdego fragmentu
z basenu możesz użyć tylko raz, lecz nie
wszystkie fragmenty będą Ci potrzebne.

...

public class MainActivity extends Activity {


String[] colors = new String[] {„czerwony”, „pomarańczowy”, „żółty”, „zielony”,
„niebieski”};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Używamy tablicy
Spinner ) findViewById(R.id.spinner);
Spinner spinner = ( ........ typu String.
String > adapter = new ArrayAdapter< .........
ArrayAdapter< ...... String >(
this
......... ,
android.R.layout.simple_spinner_item,
colors);
setAdapter (adapter);
spinner. ...........
}
} Aby komponent Spinner pobierał dane z adaptera,
musimy wywołać jego metodę setAdapter().

Te fragmenty kodu
nie były potrzebne.

colors
colors

jesteś tutaj  267


Przybornik

Twój przybornik do Androida Pełny kod przykładowe


aplikacji prezen tow ane
j
j
Rozdział 6.

ale mo żes z
w tym rozdzi
Opanowałeś już rozdział 6. i dodałeś  pobrać z ser we ra FT P
do swojego przybornika z narzędziami  wydawnictwa Helion:
klady/
znajomość widoków list i projektowania  ftp://ftp.helion.pl/przy
aplikacji. andrrg.zip

    CELNE SPOSTRZEŻENIA
 Podziel pomysły dotyczące aplikacji na aktywności  ListActivity to aktywność wykorzystująca
głównego poziomu, aktywności kategorii oraz komponent ListView. Referencję do tego
aktywności szczegółów/edycji. Zastosuj aktywność komponentu można pobrać, używając metody
kategorii, by przechodzić z aktywności głównego getListView().
poziomu do aktywności szczegółów/edycji.  Aktywności typu ListActivity dysponują
 Zasoby graficzne są umieszczane w jednym lub kilku domyślnym układem, który jednak można zastąpić
katalogach drawable*. Odwołujemy się do nich, własnym.
używając identyfikatorów o postaci @drawable/  Adaptery pełnią rolę mostów łączących widoki typu
nazwa_obrazu. W kodzie aktywności można ich
AdapterView ze źródłami danych. Widoki ListView
używać, stosując zapis R.drawable.nazwa_obrazu.
i Spinner są przykładami klas dziedziczących po
 Obrazy są wyświetlane za pomocą widoków AdapterView.
ImageView. Dodajemy je do układów,  ArrayAdapter to typ adaptera, który operuje na
stosując element <ImageView>. Do określenia
przekazanej tablicy.
źródła obrazu służy atrybut android:src,
a etykietę poprawiającą dostępność aplikacji  Sposób obsługi kliknięć przycisków można
możemy określić, używając atrybutu określać w kodzie układu, używając atrybutu
android:contentDescription. Atrybutom android:onClick.
tym odpowiadają metody setImageResource()  Kliknięcia elementów listy prezentowanej
i setContentDescription().
w aktywności ListActivity można obsługiwać,
 Komponent ListView wyświetla elementy w formie implementując metodę onListItemClick().
listy. Dodajemy go do układów, używając elementu  W pozostałych przypadkach obsługa kliknięć
<ListView>.
wymaga utworzenia obiektu nasłuchującego
 Do określania źródła elementów listy w kodzie i zaimplementowania jego odpowiedniej metody.
układu służy atrybut android:entries; należy
w nim podać identyfikator tablicy łańcuchów znaków
zdefiniowanych w pliku zasobów strings.xml.

268 Rozdział 6.
7. Fragmenty

Zadbaj o modularyzację
Robię to samo w wielu
różnych miejscach…
To chyba czyni mnie
fragmentem.

Wiesz już, jak tworzyć aplikacje, które działają tak samo niezależnie od tego,
na jakim urządzeniu zostały uruchomione…
…ale co zrobić w przypadku, kiedy akurat chcesz, by aplikacja wyglądała i działała inaczej
w zależności od tego, czy zostanie uruchomiona na telefonie, czy na tablecie? W tym rozdziale
pokażemy Ci, co zrobić, aby aplikacja wybierała układ, który najlepiej pasuje do wielkości
ekranu urządzenia. Oprócz tego przedstawimy fragmenty, czyli modularne komponenty
kodu, które mogą być wielokrotnie używane przez różne aktywności.

to jest nowy rozdział  269


Różne wielkości ekranów

Twoja aplikacja musi wyglądać świetnie


na wszystkich urządzeniach
Jedną z zalet pisania aplikacji na Androida jest to, że można zainstalować dokładnie tę samą
aplikację na urządzeniach o zupełnie różnych rozdzielczościach ekranu lub z całkiem innymi
procesorami i cieszyć się tym, że aplikacja ta zawsze będzie działać tak samo. Nie oznacza to
jednak, że aplikacja musi zawsze wyglądać tak samo.

Na telefonie
Te obrazki pokazują aplikację
uruchomioną na telefonie. Prezentuje
ona listę treningów, a po kliknięciu
jednego z nich zostają wyświetlone
szczegółowe informacje na jego temat.

Kliknij któryś z elementów,


a zostanie uruchomiona druga
aktywność.

Na tablecie
Na większych urządzeniach,
takich jak tablety, mamy
do dyspozycji znacznie
więcej miejsca na ekranie. Na tablecie jest znacznie
więcej miejsca, więc
Optymalnym rozwiązaniem dostępny obszar możemy
będzie zatem wyświetlanie wykorzystać w inny sposób.
wszystkich informacji
na tym samym ekranie.
Na tabletach lista
treningów zajmuje tylko
część ekranu — po
kliknięciu któregoś z nich
szczegółowe informacje na
jego temat są wyświetlane
z prawej strony ekranu.

Aby interfejsy aplikacji uruchomionej na tablecie i telefonie różniły


się od siebie, możemy utworzyć odrębne pliki układów, które będą
używane na dużych i na małych urządzeniach.

270 Rozdział 7.
Fragmenty

Może się zdarzyć, że aplikacja będzie musiała także działać inaczej


Proste stosowanie różnych układów dla różnych urządzeń może jednak nie wystarczyć.
Czasami będziemy musieli także stosować inny kod Javy dostosowany do układów,
gdyż dzięki temu aplikacja będzie mogła zmieniać swoje działanie w zależności od
urządzenia. W naszej aplikacji Trenażer będziemy używali jednej aktywności na
tabletach i dwóch aktywności na telefonach.

Na telefonie

Tu mamy dwie
aktywności: jedna
obsługuje listę,
a druga prezentuje
szczegóły.

Na tablecie

ąca
To jest jedna aktywność, obsługuj
zarówno listę, jak i informacje
szczegółowe.

Ale to może oznaczać powielanie kodu


Druga aktywność, która jest używana tylko na telefonach, będzie musiała
prezentować w układzie szczegółowe informacje o treningu. Jednak ten sam
kod będzie musiał znaleźć się także w aktywności głównej aplikacji działającej
na tabletach. Oznacza to, że ten sam kod będzie musiał być używany w kilku
różnych aktywnościach.

Zamiast go powielać i wstawiać do obu aktywności możemy jednak zastosować


tak zwane fragmenty. Czym zatem są te fragmenty?
271
Fragmenty

Fragmenty umożliwiają wielokrotne stosowanie kodu


Fragmenty można by porównać do komponentów lub podaktywności, których
można wielokrotnie używać. Taki fragment służy do kontrolowania fragmentu
ekranu i można go używać na wielu ekranach. Oznacza to, że możemy
stworzyć fragment obsługujący listę treningów i fragment wyświetlający
szczegółowe informacje o wybranym treningu. Te fragmenty mogą być
następnie używane w kilku układach.

Jeśli zastosujemy
fragment Inny fragment
prezentujący listę może służyć
treningów, będziemy do wyświetlania
mogli używać go szczegółów
w wielu konkretnego
aktywnościach. treningu.

Fragment ma swój układ


Tak jak aktywność, również fragment ma swój układ. Jeśli zaprojektujemy
go uważnie, to całą zawartość prezentowaną w jego obszarze będziemy mogli
kontrolować, używając kodu Javy. Jeżeli kod fragmentu zawiera wszystko,
co niezbędne do kontroli jego układu, to szanse, że będzie go można
z powodzeniem używać w innych miejscach aplikacji, znacznie wzrosną.

W następnej części rozdziału pokażemy Ci na podstawie aplikacji Trenażer,


jak można tworzyć fragmenty i ich używać.

272 Rozdział 7.
Fragmenty

Struktura aplikacji Trenażer


W przeważającej części tego rozdziału skoncentrujemy się na tworzeniu wersji aplikacji,
która w ramach jednej aktywności będzie wyświetlała obok siebie dwa fragmenty.
Oto podstawowe informacje na temat struktury tej aplikacji i jej możliwości:

1 Bezpośrednio po uruchomieniu aplikacji będzie uruchamiana aktywność MainActivity.


Aktywność będzie używała układu zdefiniowanego w pliku activity_main.xml.

2 Aktywność będzie używała dwóch fragmentów: WorkoutListFragment i WorkoutDetailFragment.

3 Fragment WorkoutListFragment będzie wyświetlał listę treningów.


Będzie on korzystał z układu zdefiniowanego w pliku fragment_workout_list.xml.

4 Fragment WorkoutDetailFragment będzie wyświetlał szczegółowe informacje o treningu.


Ten fragment będzie korzystał z układu zdefiniowanego w pliku fragment_workout_detail.xml.

5 Oba fragmenty będą pobierały dane treningów z pliku Workout.java.


Plik Workout.java będzie zawierał tablicę obiektów typu Workout.

<Layout>

</Layout>

fragment_
<Layout>
workout_list.xml
</Layout>
3
activity_main.xml
5

1 WorkoutList
2 Fragment.java
Workout.java

Urządzenie MainActivity.java

WorkoutDetail
Fragment.java

4 <Layout>

</Layout>

fragment_
workout_detail.xml

273
Etapy pracy

Oto kolejne etapy pracy nad aplikacją


Utworzenie tej aplikacji wymaga kilku kroków. Oto one:

1 Utworzenie fragmentów.
Utworzymy dwa fragmenty. Pierwszego,
WorkoutListFragment, użyjemy do wyświetlania listy
treningów, natomiast drugiego, WorkoutDetailFragment,
do wyświetlania szczegółowych informacji o konkretnym
treningu. Oba te fragmenty wyświetlimy w jednej
aktywności. Oprócz nich dodamy do aplikacji klasę
Workout, z której oba fragmenty będą pobierały
prezentowane dane.

2 Powiązanie obu fragmentów.


Chcemy, by po kliknięciu któregoś
z treningów wyświetlanych we
fragmencie WorkoutListFragment
szczegółowe informacje na jego temat
zostały wyświetlone we fragmencie
WorkoutDetailFragment.

3 Utworzenie układów dla urządzeń.


I na koniec zmodyfikujemy naszą
aplikację w taki sposób, by wyglądała
i działała inaczej w zależności od
rodzaju urządzenia, na którym zostanie
uruchomiona. Jeśli zostanie uruchomiona
na urządzeniu o dużym ekranie, to oba
fragmenty zostaną wyświetlone obok
siebie. Jeśli zaś urządzenie będzie miało
niewielki ekran, to oba fragmenty będą
działały w oddzielnych aktywnościach.

Utworzenie projektu
Projekt na potrzeby tej aplikacji utworzysz dokładnie tak samo
jak wszystkie inne projekty, które tworzyliśmy w poprzednich ¨ Utworzenie fragmentów
rozdziałach. ¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Utwórz zatem nowy projekt aplikacji z pustą aktywnością,
nadaj aplikacji nazwę Trenażer i umieść jej kod w pakiecie
com.hfad.trenazer. Minimalną wersją SDK musi być API
poziomu 17 lub wyższego. Tworzonej aktywności nadaj nazwę
MainActivity, a plik układu nazwij activity_main.
274 Rozdział 7.
Fragmenty

Klasa Workout ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Zacznijmy od dodania do aplikacji klasy Workout. Plik Workout.java to
zwyczajny plik źródłowy klasy napisanej w Javie, którego aplikacja będzie
używać do pobierania danych. W klasie Workout zdefiniujemy tablicę czterech
treningów, z których każdy będzie się składał z nazwy i opisu. Klasę musimy
dodać do pakietu com.hfad.trenazer, a plik zapisać w projekcie, w katalogu
app/src/main/java. Po przygotowaniu kodu klasy koniecznie zapisz plik.

package com.hfad.trenazer;
Każdy trening — obiekt Workout
— zawiera nazwę i opis.
public class Workout {
private String name;
private String description; workouts to tablica czterech obiektów Workout.

public static final Workout[] workouts = {


new Workout(”Rozciąganie kończyn”,
”5 pompek w staniu na rękach,\n10 przysiadów na jednej nodze,\n15 podciągnięć.”),
new Workout(”Ogólna agonia”,
”100 podciągnięć,\n100 pompek,\n100 brzuszków,\n100 przysiadów.”),
new Workout(”Tylko dla mięczaków”,
”5 podciągnięć,\n10 pompek,\n15 przysiadów.”),
new Workout(”Siła i dystans”,
”Bieg na 500 metrów,\n21 wydźwignięć ciężarka,\n21 podciągnięć.”)
};

// Każdy trening (Workout) zawiera nazwę i opis


private Workout(String name, String description) {
this.name = name;
this.description = description;
}
Trenazer
public String getDescription() {
return description;
app/src/main
}

public String getName() { To są metody get, java


pobierające prywatne dane
return name; obiektów.
} com.hfad.trenazer

public String toString() {


Łańcuchową reprezentacją Workout.java
return this.name; obiektu Workout jest jego
} nazwa.
}

Te dane będą używane przez fragment WorkoutDetailFragment.


Zajmiemy się nim na następnej stronie.

275
Dodajemy fragment

Jak dodać fragment do projektu? ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Teraz zajmiemy się dodaniem do projektu fragmentu WorkoutDetailFragment,
którego zadaniem będzie wyświetlanie informacji o wybranym treningu.
Nowy fragment możemy dodać do projektu podobnie jak dodajemy aktywności.
W Android Studio wybierz opcję File/New…/Fragment/Fragment (Blank).

Po wyświetleniu kreatora zostaniesz poproszony o podanie informacji


temu
i zaznaczenie opcji związanych z tworzonym fragmentem. Fragmentowi nadaj Sugerujemy, żebyś przyjrzał się u
doda tkow emu kodo wi gene rowa nem
nazwę WorkoutDetailFragment, zaznacz pole wyboru, nakazujące utworzenie to
przez Android Studio, ale zrób
pliku XML układu (Create layout XML?), a układ, który będzie używany przez żki.
dopiero po przeczytaniu tej ksią on
tworzony fragment, nazwij fragment_workout_detail. Usuń zaznaczenia z pól wyboru W niektórych sytuacjach może się
umożliwiających utworzenie metod wytwórczych (Include fragment factory methods?) okazać pomocny.
i metod zwrotnych interfejsu (Include interface callbacks?). Obie te opcje powodują
dodanie do generowanego fragmentu dodatkowego kodu, który nie będzie nam
potrzebny. Po podaniu tych informacji kliknij przycisk Finish.

To jest nazwa fragmentu.

To jest nazwa układu


używanego przez
fragment. Nie chcemy, by Andoid Studio
tworzyło dla nas masę dodatkowego
kodu, dlatego usuń zaznaczenie tych
dwóch pól wyboru.

Tworzymy pusty
fragment.

Kiedy klikniesz przycisk Finish, Android Studio utworzy nowy plik fragmentu o nazwie
WorkoutDetailFragment.java i nowy plik układu o nazwie fragment_workout_detail.xml,
zapisując je odpowiednio w katalogach app/src/main/java i app/src/res/layout.

276 Rozdział 7.
Fragmenty

Kod układu fragmentu wygląda


jak kod układu aktywności Trenazer

Zaczniemy od zmodyfikowania kodu układu używanego przez utworzony


przed chwilą fragment. A zatem otwórz plik fragment_workout_detail.xml app/src/main
zapisany w katalogu app/src/res/layout i zastąp jego zawartość kodem
przedstawionym poniżej: res

<?xml version=”1.0” encoding=”utf-8”?>


layout
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android” <xml>
</xml>
android:layout_height=”match_parent”
fragment_
android:layout_width=”match_parent” workout_detail.xml
android:orientation=”vertical”>

<TextView
android:layout_width=”wrap_content”
Nazwę i opis android:layout_height=”wrap_content”
treningu
wyświetlamy android:textAppearance=”?android:attr/textAppearanceLarge”
w dwóch
android:text=”” To jest nazwa treningu.
odrębnych
komponentach
TextView. android:id=”@+id/textTitle” />

<TextView
android:layout_width=”wrap_content” To jest opis
treningu.
android:layout_height=”wrap_content”
android:text=””
android:id=”@+id/textDescription” />
To jest fragment,
</LinearLayout> którego będziemy
mogli używać
w aktywnościach.

Jak widać, kod układów używanych przez fragmenty wygląda tak samo jak kod
układów używanych przez aktywności. Układ naszego fragmentu jest bardzo
prosty — składa się z dwóch komponentów TextView: pierwszy prezentuje nazwę
treningu wyświetloną dużą czcionką, a drugi wyświetla opis treningu mniejszą
czcionką. Tworząc układy na potrzeby fragmentów, możemy używać wszystkich
poznanych układów i widoków, z których korzystaliśmy już przy tworzeniu układów
wykorzystywanych przez aktywności.

Skoro przygotowaliśmy już układ dla naszego fragmentu, zajmijmy się teraz jego
kodem.

277
Kod fragmentu

Jak wygląda kod fragmentu? ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Kod fragmentu jest zapisany w pliku WorkoutDetailFragment.java,
przechowywanym w katalogu app/src/main/java. Otwórz teraz ten plik.

Jak się zapewne spodziewałeś, Android Studio wygenerowało kod tego pliku.
Zastąp tę wygenerowaną zawartość kodem przedstawionym poniżej:

package com.hfad.trenazer;

import android.app.Fragment; Trenazer


import android.os.Bundle;
import android.view.LayoutInflater; app/src/main
import android.view.View;
Klasa dziedziczy po klasie
import android.view.ViewGroup; Fragment systemu Android. java

public class WorkoutDetailFragment extends Fragment { com.hfad.trenazer


To jest metoda onCreateView(). System
wywołuje ją, gdy potrzebuje
układu fragmentu.
@Override WorkoutDetail
public View onCreateView(LayoutInflater inflater, ViewGroup container, Fragment.java
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
} To wywołanie informuje system o tym, którego
} układu używa ten fragment (w naszym przypadku
jest to układ fragment_workout_detail).

Powyższy kod tworzy prosty fragment. Jak widać, klasa fragmentu dziedziczy
po klasie android.app.Fragment. Wszystkie fragmenty są klasami
pochodnymi klasy Fragment. Wszystkie fragmenty

Obejrzyj to!
muszą definiować
Nasz fragment implementuje także metodę onCreateView(). Metoda ta jest
publiczny konstruktor
wywoływana za każdym razem, gdy system potrzebuje układu fragmentu, i to
bezargumentowy.
właśnie wewnątrz tej metody musimy określić, którego układu używa dany
fragment. Metoda onCreateView() jest opcjonalna, ponieważ jednak trzeba ją Wynika to z faktu, że Android używa
implementować we wszystkich fragmentach używających układów, w praktyce tego konstruktora do tworzenia instancji
trzeba ją implementować niemal zawsze. fragmentu za każdym razem, gdy jest ona
potrzebna, i jeśli takiego konstruktora nie
Układ używany przez fragment należy określać w następujący sposób:
uda się znaleźć, to w trakcie działania
inflater.inflate(R.layout.fragment_workout_detail, aplikacji system zgłosi wyjątek.
container, false);
W praktyce konstruktor bezargumentowy
Ta instrukcja stanowi odpowiednik wywołania metody setContentView() trzeba definiować w przypadkach, gdy
aktywności i podobnie jak ta metoda, także powyższe wywołanie jest fragment zawiera także inny konstruktor
używane do określania układu, którego ma używać dany fragment. Argument z co najmniej jednym argumentem. Dzieje
container jest przekazywany przez aktywność używającą fragmentu — jest to się tak dlatego, że jeśli klasa nie definiuje
obiekt typu ViewGroup należący do tej aktywności, do której należy wstawić żadnego konstruktora, to kompilator Javy
układ fragmentu. automatycznie dodaje do niej publiczny
278 Rozdział 7. konstruktor bezargumentowy.
Fragmenty

Dodawanie fragmentu do układu aktywności


Trenazer
Kiedy tworzyliśmy projekt, Android Studio utworzyło dla nas aktywność o nazwie
MainActivity.java i układ o nazwie activity_main.xml. Teraz zmienimy układ w taki app/src/main
sposób, by korzystał z utworzonego wcześniej fragmentu.
res
Aby zmodyfikować układ, otwórz plik activity_main.xml umieszczony w katalogu
app/src/main/layout i zastąp jego zawartość wygenerowaną przez Android Studio
layout
kodem przedstawionym poniżej:
<xml>
</xml>

activity_main.xml
<?xml version=”1.0” encoding=”utf-8”?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”horizontal”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<fragment
class=”com.hfad.trenazer.WorkoutDetailFragment”
android:id=”@+id/detail_frag” Ten fragment kodu dodaje do
układu aktywności fragment
android:layout_width=”match_parent” WorkoutDetailFragment.
android:layout_height=”match_parent” />
</LinearLayout>

Jak widać, układ aktywności zawiera tylko jeden element — <fragment>.


Użyliśmy tego elementu, by dodać fragment do układu aktywności. Jego
atrybut class pozwala określić, który fragment należy dodać, a jego
wartością musi być pełna nazwa klasy fragmentu. W naszym przypadku
mamy zamiar użyć fragmentu WorkoutDetailFragment, zatem atrybut
class ma postać:

class=”com.hfad.trenazer.WorkoutDetailFragment”

Udało się nam już utworzyć fragment i przygotować aktywność, która


wyświetla go w swoim układzie. Jednak jak na razie nasz fragment
nic nie robi. A więc kolejnym zadaniem, którym się zajmiemy, będzie
przekazywanie z aktywności do fragmentu informacji o tym, który
trening należy wyświetlić, oraz zmodyfikowanie fragmentu w taki sposób,
by wyświetlał szczegółowe informacje o wskazanym treningu.

279
Metoda setWorkout()

Przekazywanie identyfikatora treningu ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
do fragmentu ¨ Utworzenie odrębnych układów

Kiedy aktywność używa fragmentu, to zazwyczaj musi w jakiś sposób się z nim
komunikować. Na przykład jeśli fragment prezentuje szczegółowe informacje
o konkretnym rekordzie, to aktywność musi przekazać do niego informację o tym,
który rekord ma zostać wyświetlony.

W naszym przypadku chcemy, by fragment WorkoutDetailFragment wyświetlał


szczegółowe informacje o wybranym treningu. W tym celu dodamy do niego prostą
metodę, która pozwoli zapisać we fragmencie identyfikator treningu. Aktywność będzie
mogła ją wywołać, aby określić identyfikator treningu, który ma zostać wyświetlony przez
fragment. W dalszej kolejności użyjemy tego identyfikatora do pobrania danych treningu
i aktualizacji informacji wyświetlanych w widokach fragmentu.

A oto zmodyfikowany kod klasy WorkoutDetailFragment (zaktualizuj tę klasę


w swojej wersji aplikacji):

package com.hfad.trenazer;

Trenazer
import android.app.Fragment;
import android.os.Bundle; app/src/main
import android.view.LayoutInflater;
import android.view.View; java
import android.view.ViewGroup;
com.hfad.trenazer
public class WorkoutDetailFragment extends Fragment {
private long workoutId; To jest identyfikator treningu wybranego przez WorkoutDetail
użytkownika. Za chwilę użyjemy go do wyświetlenia Fragment.java
w widokach fragmentu informacji o wybranym
@Override treningu.
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
}

public void setWorkout(long id) { To jest metoda do ustawiania


this.workoutId = id; identyfikatora treningu. Aktywność
będzie jej używać do określania
} identyfikatora treningu.
}

Aktywność musi teraz wywołać metodę setWorkout() fragmentu i przekazać


do niej identyfikator konkretnego treningu. Zobacz, jak to zrobić.

280 Rozdział 7.
Fragmenty

Określanie identyfikatora treningu w kodzie aktywności


Zanim aktywność będzie mogła porozmawiać ze swoimi fragmentami, musi najpierw zdobyć
referencje do nich. Aby pobrać referencje do fragmentu, najpierw musimy pobrać referencję do
menedżera fragmentów. Służy do tego metoda aktywności o nazwie getFragmentManager().
Dysponując menedżerem fragmentów, możemy już pobrać referencję interesującego nas
fragmentu za pomocą metody findFragmentById(): To jest identyfikator fragmentu ci.
umieszczonego w układzie aktywnoś
getFragmentManager().findFragmentById(R.id.fragment_id)
Metoda findFragmentById()
Menedżer fragmentów służy do zarządzania wszystkimi fragmentami używanymi przypomina nieco metodę
findViewById(), z tą różnicą,
przez daną aktywność. Stosujemy go do pobierania referencji do fragmentów że zwraca referencje do
i przeprowadzania transakcji związanych z tymi fragmentami. fragmentów.

Poniżej przedstawiliśmy kompletny kod aktywności (zastąp nim zawartość pliku MainActivity.java):
package com.hfad.trenazer;

Trenazer
import android.app.Activity;
import android.os.Bundle; app/src/main

public class MainActivity extends Activity { java

@Override com.hfad.trenazer
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); MainActivity.java
setContentView(R.layout.activity_main);
WorkoutDetailFragment frag = (WorkoutDetailFragment)
getFragmentManager().findFragmentById(R.id.detail_frag);
frag.setWorkout(1);
To wywołanie zwraca referencję do
} Już zaraz zmienimy fragment WorkoutDetailFragment tak, fragmentu WorkoutDetailFragment.
} by wyświetlał informacje o treningu, aby sprawdzić, Identyfikatorem tego fragmentu w kodzie
czy to działa. układu aktywności jest detail_frag.

Jak widać, referencję do fragmentu pobraliśmy po wywołaniu metody setContentView().


Ta kolejność jest bardzo ważna, gdyż przed wywołaniem tej metody fragment jeszcze nie
istnieje.

Do przekazania fragmentowi informacji o treningu, którego dane ma pobrać i wyświetlić,


używamy wywołania o postaci frag.setWorkout(1). To nasza metoda, którą
zaimplementowaliśmy wcześniej w klasie fragmentu. Na razie identyfikator treningu
przekazujemy w metodzie onCreate(), aby fragment wyświetlał jakieś dane po uruchomieniu
aplikacji. Nieco później zmodyfikujemy kod aktywności w taki sposób, by użytkownik mógł
wybierać, który trening chce wyświetlić.

Kolejną rzeczą, którą się zajmiemy, będzie modyfikacja widoków fragmentu w momencie jego
wyświetlania. Zanim jednak będziemy mogli to zrobić, musimy poznać cykl życia fragmentu.
281
Powtórka z aktywności

Przypomnienie stanów aktywności ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Podobnie jak aktywności, fragmenty mają kilka kluczowych metod cyklu życia,
które są wywoływane w ściśle określonych momentach. Znajomość tych metod
oraz momentów, w których są one wywoływane, jest bardzo ważna, gdyż pozwoli
zapewnić, że tworzone fragmenty będą działały zgodnie z naszymi oczekiwaniami.

Fragmenty są umieszczane w aktywnościach i przez nie kontrolowane, dlatego


cykl życia fragmentów jest ściśle powiązany z cyklem życia aktywności. Poniżej
przedstawiliśmy przypomnienie wszystkich stanów aktywności, natomiast na
następnej stronie pokażemy, jaki jest ich związek z fragmentami.

Aktywność jest utworzona, kiedy zostanie


Aktywność wykonana jej metoda onCteate().
utworzona Na tym etapie aktywność jest zainicjalizowana,
lecz jeszcze nie jest widoczna.

Aktywność jest uruchomiona, kiedy


Aktywność zostanie wykonana jej metoda onStart().
uruchomiona Aktywność jest widoczna, lecz nie dysponuje
miejscem wprowadzania.

Aktywność jest wznowiona, kiedy zostanie


Aktywność wykonana jej metoda onResume().
wznowiona Aktywność jest widoczna i dysponuje miejscem
wprowadzania.

Aktywność jest wstrzymana, kiedy zostanie


Aktywność wykonana jej metoda onPause().
wstrzymana Aktywność wciąż jest widoczna, lecz nie
dysponuje już miejscem wprowadzania.

Aktywność jest zatrzymana, kiedy zostanie


Aktywność wykonana jej metoda onStop().
zatrzymana Aktywność nie jest już widoczna, ale wciąż
istnieje.

Aktywność jest usunięta, kiedy zostanie


Aktywność wykonana jej metoda onDestroy().
usunięta Po tym wywołaniu aktywność przestaje istnieć.

282 Rozdział 7.
Fragmenty

Cykl życia fragmentów


Cykl życia fragmentów jest bardzo podobny do cyklu życia aktywności, choć zawiera
kilka dodatkowych etapów. Wynika to z faktu, że fragment musi prowadzić interakcję
z aktywnością, w której jest umieszczony. Poniżej przedstawiliśmy metody tworzące
cykl życia fragmentu i ich relacje z poszczególnymi stanami aktywności.

Stany aktywności Metody zwrotne fragmentów


Aktywność utworzona onAttach() onAttach(Activity)
Ta metoda jest wywoływana, kiedy fragment jest kojarzony
z aktywnością.
onCreate(Bundle)
onCreate()
Ta metoda jest bardzo podobna do metody onCreate() aktywności.
Można jej używać do przeprowadzania wstępnej inicjalizacji fragmentu.
onCreate(LayoutInflater, ViewGroup, Bundle)
onCreateView()
Na tym etapie działania fragmenty używają obiektu
LayoutInflater do utworzenia swojego widoku.
onActivityCreated(Bundle)
onActivityCreated() Ta metoda jest wywoływana po zakończeniu wywołania
metody onCreate() aktywności.
onStart()
Aktywność
onStart() Ta metoda jest wywoływana bezpośrednio przed
uruchomiona wyświetleniem fragmentu.

onResume()
Aktywność wznowiona onResume() Ta metoda jest wywoływana, kiedy fragment jest widoczny
i aktywnie działa.

onPause()
Aktywność wstrzymana onPause() Ta metoda jest wywoływana, kiedy fragment jest widoczny,
lecz nie prowadzi już interakcji z użytkownikiem.

onStop()
Aktywność zatrzymana onStop() Ta metoda jest wywoływana, kiedy fragment przestaje być
widoczny dla użytkownika.

onDestroyView()
Aktywność usunięta onDestroyView() Ta metoda zapewnia fragmentowi możliwość usunięcia
wszystkich zasobów, które mogą być skojarzone z jego
widokiem.
onDestroy() onDestroy()
W tej metodzie fragment może zwolnić wszystkie inne
używane zasoby.
onDetach()
onDetach() Ta metoda jest wywoływana, gdy fragment ostatecznie traci
kontakt ze swoją aktywnością.
283
Klasa Fragment

Nasze fragmenty dziedziczą metody cyklu życia ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Jak już się przekonałeś, nasz fragment dziedziczy po systemowej klasie Fragment.
Ta klasa zapewnia mu dostęp do metod cyklu życia fragmentu.

Object Klasa Object


(java.lang.Object)

Fragment Klasa Fragment


(android.app.Fragment)
onAttach(Activity)
Klasa Fragment implementuje domyślne wersje
onCreate(Bundle) metod cyklu życia fragmentu. Definiuje także
inne metody, których fragment może potrzebować,
onCreateView(LayoutInflater, takie jak getView().
ViewGroup, Bundle)
onActivityCreated(Bundle)
onStart()
onResume()
onPause()
onStop()
onDestroyView()
onDestroy()
onDetach()
getView()

TwojFragment Klasa TwojFragment


(com.hfad.foo)
onCreateView(LayoutInflater,
ViewGroup, Bundle)
Przeważająca większość zachowania fragmentu jest
twojaMetoda() implementowana przez metody klasy bazowej. Nam pozostaje
przesłonięcie wyłącznie tych metod, których potrzebujemy.

Choć fragmenty mają całkiem sporo wspólnego z aktywnościami, to jednak klasa


Fragment nie dziedziczy po klasie Activity. Oznacza to, że niektóre spośród
metod dostępnych w aktywnościach nie będą dostępne we fragmentach.

Zwróć uwagę, że klasa Fragment nie dziedziczy po klasie Context.


W odróżnieniu od aktywności fragmenty nie są jednym z typów kontekstu,
a co za tym idzie, nie mają bezpośredniego dostępu do globalnych informacji
o środowisku aplikacji. Zamiast tego muszą one uzyskiwać dostęp do tych
informacji, używając kontekstu innych obiektów, takich jak aktywność,
w której dany fragment jest umieszczony.
284 Rozdział 7.
Fragmenty

Określenie zawartości widoków w metodzie onStart() fragmentu


Musimy zadbać o to, by nasz fragment WorkoutDetailFragment wyświetlał szczegółowe informacje
o wybranym treningu. Operacja ta musi być wykonywana podczas uruchamiania aktywności, dlatego
zaimplementujemy ją w metodzie onStart() fragmentu. Oto zmodyfikowany kod fragmentu
WorkoutDetailFragment:

package com.hfad.trenazer;
Trenazer

import android.app.Fragment;
import android.os.Bundle; app/src/main
import android.view.LayoutInflater;
import android.view.View; Używamy tej klasy
w metodzie onStart(). java
import android.view.ViewGroup;
import android.widget.TextView;
com.hfad.trenazer
public class WorkoutDetailFragment extends Fragment {
private long workoutId;
WorkoutDetail
@Override Fragment.java
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
}

@Override
public void onStart() { Metoda getView() pobiera główny widok fragmentu.
super.onStart(); Możemy go później użyć do pobrania referencji do
widoków wyświetlających tytuł i opis treningu.
View view = getView();
if (view != null) {
TextView title = (TextView) view.findViewById(R.id.textTitle);
Workout workout = Workout.workouts[(int) workoutId];
title.setText(workout.getName());
TextView description = (TextView) view.findViewById(R.id.textDescription);
description.setText(workout.getDescription());
}
}

public void setWorkout(long id) {


this.workoutId = id;
}
}
Implementując
Jak napisaliśmy na poprzedniej stronie, fragmenty różnią się od aktywności i dlatego
nie dysponują wszystkimi metodami, które mają aktywności. Na przykład fragmenty metody cyklu życia
nie udostępniają metody findViewById(). Dlatego aby pobrać referencje do fragmentu, zawsze
widoków fragmentu, musimy najpierw pobrać referencję do widoku głównego,
musisz wywoływać
używając w tym celu metody getView(), a następnie pobrać jego widoki podrzędne.
analogiczną metodę
Skoro zadbaliśmy już o aktualizację widoków fragmentu, możemy wziąć naszą
aplikację na małą jazdę próbną. klasy bazowej.
285
Jazda próbna

Jazda próbna aplikacji ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Kiedy uruchomisz aplikację, na ekranie urządzenia zostaną
wyświetlone szczegółowe informacje o treningu.

Aplikacja wygląda dokładnie tak samo, jak gdyby informacje były


wyświetlane bezpośrednio w aktywności. Jednak nasza aktywność Szczegółowe informacje
wyświetla informacje o treningu, używając fragmentu, a to oznacza, o treningu są wyświetlone
we fragmencie.
że gdyby zaszła taka potrzeba, moglibyśmy użyć tego samego
fragmentu w innej aktywności.

Co się dzieje po uruchomieniu aplikacji?


1 Po uruchomieniu aplikacji zostaje utworzona aktywność MainActivity.

Urządzenie MainActivity

2 Aktywność MainActivity w swojej metodzie onCreate() przekazuje identyfikator


treningu do fragmentu WorkoutDetailFragment, używając do tego metody setWorkout().

setWorkout(1)

WorkoutDetail
MainActivity Fragment

3 W swojej metodzie onStart() fragment używa przekazanego identyfikatora


do zaktualizowania wartości wyświetlanych w widokach.

textTitle: Ogólna agonia


textDescription: 100 podciągnięć,
100 pompek,
WorkoutDetail 100 brzuszków,
MainActivity Fragment 100 przysiadów.

286 Rozdział 7.
Fragmenty

Dokąd dotarliśmy?
Do tej pory utworzyliśmy plik MainActivity.java, układ aktywności głównej
activity_main.xml, klasę fragmentu zapisaną w pliku WorkoutDetailFragment.java, układ
używany przez ten fragment — fragment_workout_detail.xml oraz zwyczajną klasę
Javy zawierającą dane treningów i zapisaną w pliku Workout.java. Aktywność
MainActivity używa fragmentu WorkoutDetailFragment do wyświetlania
szczegółowych informacji o treningu, a ten pobiera dane treningu z klasy Workout.

<Layout> <Layout>

</Layout> </Layout>

activity_main.xml fragment_
workout_detail.xml

WorkoutDetail
MainActivity.java Fragment.java Workout.java

Kolejnym naszym zadaniem jest utworzenie fragmentu WorkoutListFragment,


który będzie wyświetlał listę treningów.

Nie istnieją
głupie pytania
P: Dlaczego aktywność nie może pobrać fragmentu, P: Dlaczego fragmenty nie dysponują metodą
używając metody findViewById()? findViewById()?

O : Dlatego, że metoda findViewById() zawsze zwraca obiekt O: Ponieważ fragmenty nie są ani widokami, ani aktywnościami.
typu View, a choć to zadziwiające, okazuje się, że fragmenty Zamiast niej musimy użyć metody getView() fragmentu, by
nie są widokami. pobrać jego widok główny, a następnie skorzystać z jego metody
findViewById(), by odszukać jego widoki podrzędne.
P : Skoro aktywności mają metodę findViewById(),
to czemu nie mają metody findFragmentById()? P: Aktywności trzeba rejestrować w pliku manifestu
AndroidManifest.xml, gdyż w przeciwnym razie aktywność
O: To dobre pytanie. Fragmenty nie były dostępne we wczesnych nie będzie w stanie ich używać. Czy podobnie trzeba
wersjach systemu Android. Dlatego wprowadzono do systemu rejestrować fragmenty?
menedżery fragmentów, stanowiące sposób dodania do niego
przydatnego kodu do zarządzania fragmentami, a jednocześnie O: Nie. Aktywności muszą być rejestrowane w pliku
pozwalające uniknąć konieczności dodawania obszernego kodu AndroidManifest.xml, a fragmenty nie.
do klasy bazowej aktywności.

287
Dodajemy listę

Musimy utworzyć fragment z listą ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Skoro już uruchomiliśmy fragment WorkoutDetailFragment, musimy zająć się
przygotowaniem drugiego fragmentu wyświetlającego listę dostępnych treningów.
Dzięki temu będziemy mogli użyć obu tych fragmentów do przygotowania
odrębnych interfejsów użytkownika dla tabletów i dla telefonów.

Na tabletach oba fragmenty Później, z myślą o uruchamianiu


będą mogły być wyświetlane aplikacji na telefonach,
jednocześnie, obok siebie. zastosujemy oba te fragmenty
w odrębnych aktywnościach.

Wiesz już, jak dodać do aktywności widok listy. Możemy więc utworzyć
fragment, którego układ będzie zawierał pojedynczy widok listy,
a następnie wyświetlić w nim nazwy dostępnych treningów.

A zatem fragment będzie


zawierał jedną listę. Kiedy chcieliśmy
użyć aktywności zawierającej pojedynczą
listę, zastosowaliśmy aktywność typu ListActivity.
Zastanawiam się, czy przypadkiem nie istnieje jakiś
typ fragmentu stanowiący odpowiednik tej klasy.

On dobrze kombinuje. Możemy użyć specjalnego


typu fragmentu — klasy ListFragment.

Przyjrzymy się jej na następnej stronie.

288 Rozdział 7.
Fragmenty

ListFragment to fragment zawierający


pojedynczą listę ListFragment jest klasą
pochodną klasy Fragment.

ListFragment, nazywany także fragmentem listy, to specjalny typ fragmentu


przeznaczony do pracy z listami. Podobnie jak aktywność ListActivity,
android.app.Fragment
także fragment ListFragment jest automatycznie kojarzony z widokiem
listy, dzięki czemu nie musimy go tworzyć samodzielnie. A oto, jak wygląda ...
taki fragment:

android.app.ListFragment
Fragment listy
dysponuje swoim getListView()
własnym widokiem
listy, dzięki czemu nie getListAdapter()
musimy sami tworzyć
takiej listy. Wystarczy, setListAdapter()
że dostarczymy
niezbędnych danych. onListItemClick()
...

Podobnie jak było w przypadku aktywności ListActivity, także stosowanie


fragmentu typu ListFragment do wyświetlania kategorii danych zapewnia
dwie znaczące zalety:

 Nie musimy tworzyć własnego układu. ListFragment jest


Fragmenty typu ListFragment definiują swoje własne układy w sposób
programowy, dlatego nie zmuszają nas do tworzenia i utrzymywania typem fragmentu
układów w formie plików XML. Układ generowany przez fragmenty
tego typu składa się z pojedynczego widoku listy. W kodzie aktywności przystosowanym
do tego widoku listy odwołujemy się za pomocą metody getListView()
fragmentu listy. Jest ona niezbędna do określenia danych, które będą
do korzystania
wyświetlane na liście. z list. Dysponuje on
 Nie musimy implementować własnych obiektów nasłuchujących domyślnym układem
zdarzeń związanych z listą.
Obiekty klasy ListFragment są rejestrowane w widoku listy jako obiekty zawierającym pojedynczy
nasłuchujące i oczekują na zdarzenia związane z kliknięciami elementów komponent ListView.
listy. Aby fragment reagował na kliknięcia elementów listy, należy
wywołać jego metodę onListItemClick(). Już za chwilę zobaczysz ją
w akcji.

A zatem jak wygląda kod fragmentu typu ListFragment?

289
Tworzymy ListFragment

Jak utworzyć fragment typu ListFragment? ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Fragment typu ListFragment możesz dodać do projektu tak samo jak każdy
inny fragment. Wybierz z menu opcje File/New.../Fragment/Fragment (Blank).
Następnie nadaj fragmentowi nazwę WorkoutListFragment i usuń zaznaczenia
opcji generowania układu i dwóch pozostałych opcji: metod wytwórczych i metod
zwrotnych interfejsu. Fragmenty typu ListFragment generują swoje układy
programowo, zatem Android Studio nie musi generować ich za nas.
Po kliknięciu przycisku Finish Android Studio wygeneruje plik fragmentu,
WorkoutListFragment.java, i zapisze go w katalogu app/src/main/java.

Poniżej pokazaliśmy, jak wygląda podstawowy kod tworzący fragment typu


ListFragment. Jak widać, jest on bardzo podobny do kodu normalnego fragmentu.
Zastąp kod klasy WorkoutListFragment wygenerowany przez Android Studio
następującym kodem:

package com.hfad.trenazer;

Trenazer
import android.os.Bundle;
import android.app.ListFragment; app/src/main
import android.view.LayoutInflater;
import android.view.View; Nasz fragment musi dziedziczyć java
po klasie ListFragment, a nie
import android.view.ViewGroup; po klasie Fragment.
com.hfad.trenazer
public class WorkoutListFragment extends ListFragment {
WorkoutList
@Override Fragment.java

public View onCreateView(LayoutInflater inflater, ViewGroup container,


Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
} Wywołanie metody onCreateView()
} klasy bazowej zwraca domyślny układ
fragmentu ListFragment.

Powyższy kod tworzy prosty fragment listy o nazwie WorkoutListFragment. Ponieważ


jest to fragment listy, musi dziedziczyć po klasie ListFragment, a nie po klasie Fragment.

Implementowanie metody onCreateView() jest opcjonalne. Metoda ta jest wywoływana


w momencie tworzenia widoku używanego przez fragment. W naszym przykładzie
implementujemy ją, gdyż chcemy wypełnić listę fragmentu danymi jak najszybciej po
jej utworzeniu. Jeśli nasz kod nie musi w tym momencie wykonywać żadnych operacji,
to także implementowanie tej metody nie będzie konieczne.

A teraz zobaczmy, w jaki sposób można dodawać dane do widoku listy.

290 Rozdział 7.
Fragmenty

Użyjemy adaptera ArrayAdapter do określenia wartości


wyświetlanych w ListView
Zgodnie z tym, co napisaliśmy w rozdziale 6., do połączenia danych z widokiem listy można użyć adaptera.
Dokładnie to samo dotyczy sytuacji, gdy komponent listy jest wyświetlany we fragmencie — ListView jest klasą
pochodną klasy AdapterView i to właśnie ta klasa zapewnia temu widokowi możliwość korzystania z adapterów.

Aktualnie naszym celem jest przekazać do widoku listy wyświetlanego we fragmencie WorkoutListFragment
tablicę z nazwami treningów. Do powiązania tej tablicy z widokiem listy użyjemy adaptera.
Utworzymy adapter typu
ArrayAdapter i użyjemy go, aby
To jest widok listy. powiązać widok listy z tablicą. To jest tablica.

ListView Array Nazwy


Adapter treningów

Fragment nie jest typem kontekstu


Jak wiadomo, aby utworzyć adapter ArrayAdapter, który będziemy mogli powiązać z komponentem
ListView, musimy użyć wywołania o następującej postaci:

ArrayAdapter<TypDanych> listAdapter = new ArrayAdapter<TypDanych>(


kontekst, android.R.layout.simple_list_item_1, tablica);
gdzie TypDanych to typ danych, tablica to tablica z danymi, a kontekst to bieżący kontekst.

Jeżeli stosujemy powyższe wywołanie w kodzie aktywności, jako kontekstu możemy użyć referencji this.
To możliwe, gdyż aktywność jest typem kontekstu — klasa Activity dziedziczy po klasie Context.

Jak już się jednak przekonałeś, Fragment nie jest klasą dziedziczącą po Context, dlatego w tym
przypadku nie możemy użyć this. Oznacza to, że bieżący kontekst musimy zdobyć w jakiś inny sposób.
Jeśli tak jak my tworzysz adapter w metodzie onCreateView() fragmentu, to możesz pobrać kontekst,
używając metody getContext() obiektu LayoutInflator:

ArrayAdapter<DataType> listAdapter = new ArrayAdapter<DataType>(


To wywołanie zwraca inflator.getContext(), android.R.layout.simple_list_item_1, array);
bieżący kontekst.
Po utworzeniu adaptera możesz powiązać go z komponentem ListView za pomocą metody
setListAdapter() fragmentu:

setListAdapter(listAdapter);
Spróbujmy zatem użyć adaptera ArrayAdapter do określenia zawartości naszego fragmentu
prezentującego listę treningów.
291
Kod klasy WorkoutListFragment

Zaktualizowany kod klasy WorkoutListFragment ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Zaktualizowaliśmy kod klasy WorkoutListFragment i zaimplementowaliśmy
w nim wyświetlanie nazw treningów na liście. Wprowadź wyróżnione zmiany
do kodu swojej aplikacji:

package com.hfad.trenazer;

Trenazer
import android.os.Bundle;
import android.app.ListFragment; app/src/main
import android.view.LayoutInflater;
import android.view.View; java
Używamy tej klasy
import android.view.ViewGroup; w metodzie onCreate().
com.hfad.trenazer
import android.widget.ArrayAdapter;

WorkoutList
public class WorkoutListFragment extends ListFragment { Fragment.java

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
String[] names = new String[Workout.workouts.length];
for (int i = 0; i < names.length; i++) {
names[i] = Workout.workouts[i].getName();
} Tworzymy adapter Tworzymy tablicę łańcuchów znaków
ArrayAdapter. z nazwami treningów.

ArrayAdapter<String> adapter = new ArrayAdapter<String>(


Pobieramy kontekst inflater.getContext(), android.R.layout.simple_list_item_1,
z obiektu LayoutInflater.
names);
setListAdapter(adapter); Przekazujemy adapter do widoku listy.

return super.onCreateView(inflater, container, savedInstanceState);


}
}

Teraz, kiedy fragment WorkoutListFragment wyświetla już listę nazw


treningów, przekonajmy się, jak on działa w praktyce. W tym celu
dodamy go do aktywności.

292 Rozdział 7.
Fragmenty

Wyświetlenie fragmentu WorkoutListFragment


w układzie aktywności MainActivity
Zamierzamy teraz dodać nasz nowy fragment
WorkoutListFragment do układu aktywności MainActivity,
tak by był on wyświetlany po lewej stronie fragmentu
WorkoutDetailFragment. Takie rozmieszczanie fragmentów
jest rozwiązaniem bardzo często stosowanym w aplikacjach
na tablety.

W naszym rozwiązaniu zastosujemy układ liniowy o orientacji


poziomej. Do kontroli szerokości obszaru zajmowanego przez
oba fragmenty zastosujemy wagi.

Oto zmodyfikowany kod układu (zmodyfikuj swoją wersję


pliku activity_main.xml, tak by odpowiadała naszej):

<?xml version=”1.0” encoding=”utf-8”?>


<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”horizontal”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
Najpierw ma być
wyświetlana lista Trenazer
<fragment treningów.
class=”com.hfad.trenazer.WorkoutListFragment”
app/src/main
android:id=”@+id/list_frag”
android:layout_width=”0dp” res
android:layout_weight=”2”
android:layout_height=”match_parent”/> layout
<xml>
A następnie szczegóły </xml>

<fragment wybranego treningu. activity_main.xml


class=”com.hfad.trenazer.WorkoutDetailFragment”
android:id=”@+id/detail_frag”
android:layout_width=”0dp” Do kontroli wielkości obszaru zajmowanego
przez poszczególne fragmenty w układzie
android:layout_weight=”3” używamy ich wag.
android:layout_height=”match_parent” />
</LinearLayout>

A teraz sprawdźmy, jak wygląda nasza aplikacja.

293
Jazda próbna

Jazda próbna aplikacji


Aby łatwiej nam
Gdy uruchomimy aplikację, po lewej stronie było sprawdzić, czy
ekranu zostanie wyświetlona lista wszystkich informacje o treningu
treningów, a po prawej — szczegółowe są wyświetlane,
identyfikator
informacje o konkretnym treningu. wybranego treningu
Identyfikator treningu wyświetlanego po prawej podaliśmy na stałe
w kodzie.
stronie układu podaliśmy na stałe w pliku
MainActivity.java, więc niezależnie od tego, Oba fragmenty są prezentowane
który z treningów na liście klikniemy, z prawej jeden obok drugiego.
strony zawsze będą prezentowane informacje
o treningu Ogólna agonia.

Musimy zadbać, by fragment WorkoutDetailFragment


odpowiadał na kliknięcia we fragmencie WorkoutListFragment
Poniżej zamieściliśmy krótkie przypomnienie tego, co udało się nam już zrobić w naszej
aplikacji. Jak widać, stworzyliśmy już wszystkie jej elementy.

<Layout>

</Layout>

fragment_
<Layout>
workout_list.xml
</Layout>

activity_main.xml

WorkoutList
Fragment.java

Workout.java

Urządzenie MainActivity.java

WorkoutDetail
Fragment.java
<Layout>
Niemniej jednak jeszcze nie zakończyliśmy prac nad kodem
naszej aplikacji. Musimy zadbać o to, aby we fragmencie </Layout>

WorkoutDetailFragment zamiast szczegółów jednego, określonego


na stałe treningu były wyświetlane szczegóły treningu klikniętego fragment_
przez użytkownika na liście we fragmencie WorkoutListFragment. workout_detail.xml

294 Rozdział 7.
Fragmenty

¨ Utworzenie fragmentów
Powiązanie listy z widokiem szczegółów ¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Zmienianie szczegółowych informacji o treningu w odpowiedzi na kliknięcie
elementu listy można zaimplementować na kilka sposobów. My zastosujemy
następujący:

1 Dodamy do fragmentu WorkoutListFragment kod obsługujący kliknięcia


elementów listy.

2 W momencie wykonywania tego kodu wywołamy jakiś kod w aktywności


MainActivity, który…

3 … zmieni informacje wyświetlane we fragmencie WorkoutDetailFragment.

Nie chcemy dodawać do fragmentu WorkoutListFragment


kodu, który będzie bezpośrednio operował na fragmencie
WorkoutDetailFragment. Czy potrafisz powiedzieć dlaczego?

Odpowiedzią jest możliwość wielokrotnego stosowania. Zależy


nam na tym, by nasze fragmenty wiedziały możliwie jak najmniej
o środowisku, w którym działają. Im więcej fragmenty wiedzą
o aktywności, w której są umieszczone, tym mniej nadają się
do wielokrotnego stosowania.

Chwileczkę! Mówicie, że nie


chcemy, by fragment wiedział
o aktywności, w której został umieszczony?
A co z drugim punktem listy, którą podaliście
na początku tej strony? Czy w ten sposób
nie uzależniamy fragmentu od aktywności
MainActivity? Czy to nie oznacza, że
nie będziemy mogli używać go w innej
aktywności?

Musimy użyć interfejsu, by oddzielić


fragment od aktywności.

295
Zastosowanie interfejsu

Musimy oddzielić fragment, używając interfejsu


Mamy dwa obiekty, które muszą się ze sobą komunikować — fragment i aktywność
— i chcemy, by komunikowały się ze sobą, lecz jednocześnie wiedziały o sobie jak
najmniej. W języku Java w takich sytuacjach są stosowane interfejsy. Definiując
interfejs, określamy minimalne wymagania konieczne do tego, by jeden obiekt mógł
bezpiecznie komunikować się z innym. Oznacza to, że nasz fragment będzie mógł
komunikować się z dowolną aktywnością, o ile tylko będzie ona implementować
odpowiedni interfejs.

W tym celu stworzymy interfejs o nazwie WorkoutListListener, który będzie miał


następującą postać:

interface WorkoutListListener {
void itemClicked(long id);
};

O ile tylko aktywność będzie implementować ten interfejs, my będziemy mogli


poinformować ją, że został kliknięty któryś z elementów listy wyświetlanej we
fragmencie. Oto, co będzie się działo podczas działania aplikacji:

1 Interfejs WorkoutListListener poinformuje fragment, że ma oczekiwać


na zdarzenia.

2 Użytkownik kliknie któryś z treningów wyświetlonych na liście.

3 We fragmencie prezentującym listę zostanie wywołana metoda


onListItemClicked().

4 Metoda ta wywoła z kolei metodę itemClicked() interfejsu


WorkoutListListener, przekazując do niej identyfikator klikniętego treningu.

Ale kiedy aktywność określi, że jest obiektem nasłuchującym?


A kiedy aktywność poinformuje fragment, że jest gotowa na odbieranie informacji
o kliknięciach elementów listy? Jeśli spojrzymy jeszcze raz na cykl życia fragmentu,
to przekonamy się, że podczas dołączania fragmentu do aktywności jest wywoływana
metoda onAttach(), do której przekazywana jest aktywność:

@Override
public void onAttach(Activity activity) {
....
}

To właśnie tej metody możemy użyć, by zarejestrować aktywność we fragmencie.


Zobaczmy, jak to zrobić w kodzie.

296 Rozdział 7.
Fragmenty

Najpierw dodamy interfejs do fragmentu z listą ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Zmodyfikowaliśmy kod pliku WorkoutListFragment.java, dodając do niego nowy
interfejs (wprowadź te zmiany w swoim projekcie i zapisz plik):
package com.hfad.trenazer;

import android.os.Bundle; Trenazer


import android.app.ListFragment;
import android.view.LayoutInflater; app/src/main
import android.view.View;
import android.view.ViewGroup;
java
import android.widget.ArrayAdapter;
import android.app.Activity;
Zaimportuj te klasy. com.hfad.trenazer
import android.widget.ListView;

public class WorkoutListFragment extends ListFragment {


WorkoutList
Fragment.java
static interface WorkoutListListener {
void itemClicked(long id);
}; Dodaj do fragmentu interfejs obiektu
nasłuchującego i sam obiekt.
private WorkoutListListener listener;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
String[] names = new String[Workout.workouts.length];
for (int i = 0; i < names.length; i++) {
names[i] = Workout.workouts[i].getName();
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
inflater.getContext(), android.R.layout.simple_list_item_1,
names);
setListAdapter(adapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
Ta metoda jest wywoływana
@Override w momencie dołączania
fragmentu do aktywności.
public void onAttach(Activity activity) {
super.onAttach(activity);
this.listener = (WorkoutListListener)activity;
}

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
if (listener != null) {
Ta metoda przekazuje obiektowi
listener.itemClicked(id); nasłuchującemu informację
} o kliknięciu jednego
} z elementów ListView.
}
297
Implementacja interfejsu WorkoutListListener

A potem zaimplementujemy interfejs w aktywności


Teraz musimy zaimplementować nasz nowy interfejs WorkoutListListener
w aktywności MainActivity. W tym celu zaktualizuj kod pliku MainActivity.java
w poniższy sposób:

package com.hfad.trenazer;

import android.app.Activity;
import android.os.Bundle;
Zaimplementuj interfejs
WorkoutListListener.
public class MainActivity extends Activity
implements WorkoutListFragment.WorkoutListListener {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WorkoutDetailFragment frag = (WorkoutDetailFragment)
getFragmentManager().findFragmentById(R.id detail_frag);
frag.setWorkout(1);
Usuniemy te wiersze, gdyż
} nie podajemy już na stałe
identyfikatora wyświetlanego
treningu.
@Override
Trenazer
public void itemClicked(long id) {
// Tu umieścimy kod aktualizujący wyświetlane informacje o treningu
app/src/main
}
Ta metoda została zdefiniowana
} w interfejsie WorkoutListListener.
java

W odpowiedzi na kliknięcie jednego z elementów listy wyświetlanej we fragmencie com.hfad.trenazer


zostaje wywoływana metoda itemClicked() aktywności. W tej metodzie możemy
umieścić kod, który zaktualizuje szczegółowe informacje o wybranym treningu.
MainActivity.java

Ale jak zaktualizować wyświetlane szczegóły treningu?


Fragment WorkoutDetailFragment aktualizuje swoje widoki w momencie,
gdy jest uruchamiany. Ale w jaki sposób możemy zaktualizować prezentowane
w nim informacje, kiedy już zostanie wyświetlony na ekranie?

Być może myślisz, że moglibyśmy wykorzystać do tego którąś z metod cyklu


życia fragmentu. Jednak zamiast tego za każdym razem, gdy będziemy chcieli
zaktualizować prezentowane informacje o treningu, zastąpimy dotychczasowy
fragment WorkoutDetailFragment nowym fragmentem tego samego typu.

Takie rozwiązanie ma bardzo dobre uzasadnienie.

298 Rozdział 7.
Fragmenty

Chcemy, by fragmenty współpracowały ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
z przyciskiem Wstecz ¨ Utworzenie odrębnych układów

Załóżmy, że użytkownik najpierw kliknie jeden trening, a następnie inny. W takiej


sytuacji użytkownik będzie oczekiwał, że po kliknięciu przycisku Wstecz urządzenia
wróci do pierwszego z wybranych treningów.

Użytkownik klika trening Następnie klika


Rozciąganie kończyn. trening Tylko dla
mięczaków. Chcemy, by po kliknięciu
przez użytkownika przycisku
Wstecz urządzenia ponownie
został wyświetlony trening
Rozciąganie kończyn.

We wszystkich aplikacjach, które do tej pory napisaliśmy, naciskanie przycisku Wstecz


powodowało powrót do poprzedniej aktywności. Teraz, kiedy zaczęliśmy używać fragmentów,
musimy dobrze zrozumieć, co się dzieje w momencie naciskania tego przycisku.

Witamy na stosie cofnięć


Stos cofnięć (ang. back stack) jest listą miejsc, które użytkownik odwiedził na urządzeniu.
Każde z tych miejsc jest transakcją umieszczaną na tym stosie.

Wiele spośród tych transakcji przenosi nas z jednej aktywności do drugiej:


Transakcja: Przejdź do aktywności „skrzynka odbiorcza”.
To wszystko są niezależne
Transakcja: Przejdź do aktywności „utwórz nową wiadomość e-mail”. transakcje.
Transakcja: Przejdź do aktywności „wyślij wiadomość e-mail”.

A zatem kiedy przechodzimy do nowej aktywności, transakcja związana z tym przejściem


jest dodawana do stosu cofnięć. Jeśli w którymś momencie naciśniemy przycisk Wstecz, to ta
transakcja zostanie odwrócona, a my wrócimy do aktywności, która była wyświetlona wcześniej.

Jednak transakcjami umieszczanymi na tym stosie nie muszą być wyłącznie aktywności.
Mogą nimi być także zmiany fragmentów wyświetlanych na ekranie urządzenia:
Transakcja: Zastąp fragment szczegółów Siła i dystans fragmentem szczegółów Ogólna agonia.
Transakcja: Zastąp fragment szczegółów Ogólna agonia fragmentem szczegółów Tylko dla mięczaków.

A to oznacza, że zmiany fragmentów mogą być odtwarzane za pomocą przycisku Wstecz


tak samo jak zmiany aktywności.
299
Zastąpienie fragmentu

Nie aktualizuj, lecz zastępuj ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Zamiast aktualizować widoki wyświetlane we fragmencie WorkoutDetailFrament
utworzymy zupełnie nową instancję tego fragmentu i wyświetlimy w niej informacje
następnego treningu wybranego przez użytkownika. Dzięki temu będziemy Dodawaj fragmenty
mogli zapisać nowy fragment w transakcji stosu cofnięć, zapewniając tym samym
użytkownikowi możliwość wycofania zmiany poprzez naciśnięcie przycisku Wstecz.
do aktywności,
Ale jak zastąpić jeden fragment drugim? używając elementu
Przede wszystkim będziemy musieli zacząć od wprowadzenia pewnej zmiany <fragment>, jeśli nie
w kodzie układu activity_main.xml. Zamiast umieszczać w nim fragment muszą one reagować
WorkoutDetailFragment bezpośrednio, zastosujemy układ FrameLayout, nazywany
na zmiany zachodzące
także układem ramki.
w interfejsie
Układ FrameLayout to grupa widoków służąca do blokowania fragmentu ekranu.
Definiujemy go, używając elementu <FrameLayout>. Służy on do wyświetlania
użytkownika aplikacji.
pojedynczego elementu — w naszym przypadku będzie to fragment. Umieścimy W przeciwnym
fragment w układzie FrameLayout, abyśmy mogli programowo kontrolować jego
razie należy
zawartość. W reakcji na kliknięcie elementu listy prezentowanej we fragmencie
WorkoutListFragment zastąpimy zawartość układu FrameLayout nową instancją stosować element
fragmentu WorkoutDetailFragment, prezentującą szczegóły wybranego treningu: <FrameLayout>.
<?xml version=”1.0” encoding=”utf-8”?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”horizontal”
android:layout_width=”match_parent”
Trenazer
android:layout_height=”match_parent”>

app/src/main
<fragment
class=”com.hfad.trenazer.WorkoutListFragment”
res
android:id=”@+id/list_frag”
android:layout_width=”0dp”
layout
android:layout_weight=”2”
<xml>
android:layout_height=”match_parent”/> </xml>

<fragment Planujemy wyświetlać fragment activity_main.xml


<FrameLayout wewnątrz układu FrameLayout.
Fragment dodamy do układu
class=”com.hfad.trenazer.WorkoutDetailFragment” programowo.
android:id=”@+id/detail_frag”
android:id=”@+id/fragment_container”
android:layout_width=”0dp”
android:layout_weight=”3”
android:layout_height=”match_parent” />
</LinearLayout>

Teraz zajmiemy się napisaniem kodu, który doda fragment do układu FrameLayout.

300 Rozdział 7.
Fragmenty

Stosowanie transakcji fragmentu


Podczas działania aplikacji fragmenty są zmieniane w ramach transakcji fragmentu. Transakcja
fragmentu to seria zmian dotyczących fragmentu, które chcemy wykonać w tym samym momencie.

Aby utworzyć transakcję fragmentu, musimy zacząć od pobrania z menedżera fragmentów obiektu
FragmentTransaction:

WorkoutDetailFragment fragment = new WorkoutDetailFragment();


To wywołanie rozpoczyna
FragmentTransaction transaction = getFragmentManager().beginTransaction(); transakcję fragmentu.

Następnie musimy określić wszystkie akcje, które chcemy wykonać w ramach tej transakcji.
W przypadku naszej aplikacji chcemy zastąpić fragment wyświetlony w układzie FrameLayout,
a możemy to zrobić za pomocą metody replace() transakcji:
To jest fragment umieszczony
transaction.replace(R.id.pojemnik_fragmentu, fragment); w pojemniku fragmentów.

gdzie R.id.pojemnik_fragmentu jest identyfikatorem pojemnika zawierającego fragment.


Można także dodać fragment do pojemnika, wywołując metodę add(), lub usunąć go,
wywołując metodę remove():
Jeśli chcesz, to możesz także dodać
transaction.add(R.id.pojemnik_fragmentu, fragment); fragment. W naszej przykładowej
transaction.remove(fragment); aplikacji nie jest to konieczne.

Za pomocą metody setTransition() możemy określić, jaką animację przejścia chcemy


zastosować w tworzonej transakcji.
Efektu przejścia nie trzeba określać.
transaction.setTransition(przejscie);

gdzie parametr przejscie określa typ animacji. Może on przyjmować następujące wartości:
TRANSIT_FRAGMENT_CLOSE (fragment jest usuwany ze stosu), TRANSIT_FRAGMENT_OPEN (fragment
jest dodawany), TRANSIT_FRAGMENT_FADE (fragment powinien zniknąć i ponownie się pojawić)
lub TRANSIT_NONE (brak animacji).

Po określeniu wszystkich czynności, które mają zostać wykonane w ramach transakcji, musimy
wywołać metodę addToBackStack(), aby dodać ją do stosu cofnięć. Dzięki temu, klikając
przycisk Wstecz, użytkownik będzie mógł cofnąć się do wcześniejszego stanu fragmentu.
Metoda addToBackStack() ma jeden parametr typu String — nazwę identyfikującą transakcję:
W przeważającej większości przypadków nie będziemy musieli
transaction.addToBackStack(null); pobierać transakcji, a to oznacza, że zamiast określać ich etykiety,
w wywołaniu tej metody możemy przekazywać wartość null.
Aby zatwierdzić zmiany w aktywności, należy wywołać metodę commit():
transaction.commit();

Wywołanie tej metody spowoduje wprowadzenie zmian określonych w transakcji.

301
Kod aktywności MainActivity

Zaktualizowany kod aktywności MainActivity ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Chcemy utworzyć nową instancję fragmentu WorkoutDetailFragment, która
będzie prezentowała dane odpowiedniego treningu, wyświetlić ją w aktywności
w ramach transakcji fragmentu, a następnie dodać tę transakcję do stosu
cofnięć. Poniżej przedstawiliśmy kompletny kod aktywności: Trenazer

package com.hfad.trenazer; app/src/main

import android.app.Activity; java


Używamy transakcji fragmentu,
ę
import android.os.Bundle; więc musimy zaimportować klas
FragmentTransaction. com.hfad.trenazer
import android.app.FragmentTransaction;

public class MainActivity extends Activity MainActivity.java


implements WorkoutListFragment.WorkoutListListener {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
To wywołanie
public void itemClicked(long id) { rozpoczyna transakcję
WorkoutDetailFragment details = new WorkoutDetailFragment(); fragmentu.
FragmentTransaction ft = getFragmentManager().beginTransaction();
details.setWorkout(id);
Zastępujemy fragment
ft.replace(R.id.fragment_container, details); i dodajemy go do stosu cofnięć.
ft.addToBackStack(null); Wygaszamy stary
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); i wyświetlamy
nowy fragment.
ft.commit();
Zatwierdzamy
} transakcję.
}

Sprawdźmy teraz, co się stanie, kiedy uruchomimy aplikację.

302 Rozdział 7.
Fragmenty

Jazda próbna aplikacji


Po uruchomieniu aplikacji z lewej strony ekranu zostaje wyświetlona lista
treningów. Kiedy użytkownik kliknie jeden z elementów tej listy, szczegółowe
informacje na temat wybranego treningu zostaną wyświetlone po prawej
stronie ekranu. Jeśli teraz wybierzemy inny trening z listy, a następnie
naciśniemy przycisk Wstecz na urządzeniu, to na ekranie ponownie zostaną
wyświetlone informacje o treningu, który był wybrany wcześniej.

Bezpośrednio po uruchomieniu
aplikacji prawa część
ekranu jest pusta, ponieważ
użytkownik nie wybrał jeszcze
żadnego treningu.

Gdy użytkownik kliknął trening


Rozciąganie kończyn, zostały
wyświetlone szczegółowe
informacje na jego temat.

Użytkownik kliknął
trening Tylko dla
mięczaków, więc
aplikacja wyświetliła
szczegółowe informacje
o tym treningu.

Kiedy użytkownik kliknął


przycisk Wstecz, ponownie
został wyświetlony trening
Wygląda na to, że nasza aplikacja działa Rozciąganie kończyn.
prawidłowo… aż do momentu, gdy spróbujemy
zmienić orientację urządzenia. Wówczas pojawią się
problemy. Zobaczmy, co się stanie.

303
Obracanie po raz wtóry

Obracanie urządzenia psuje działanie aplikacji ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
¨ Utworzenie odrębnych układów
Kiedy obrócisz urządzenie, w aplikacji pojawią się problemy. Niezależnie od
tego, który trening jest aktualnie wybrany, po obróceniu urządzenia zostaną
wyświetlone informacje o pierwszym treningu.

Wybierz jeden
z treningów,
a szczegółowe
informacje
o nim zostaną
wyświetlone Kiedy obrócimy
z prawej strony. urządzenie, zostaną
wyświetlone
szczegółowe informacje
o pierwszym treningu,
a nie treningu
Gdy po raz pierwszy przedstawialiśmy cykl życia aktywności, dowiedziałeś wybranym wcześniej.
Będzie to trening,
się, że w efekcie obrócenia urządzenia Android usuwa i ponownie tworzy który w tablicy
aktywność. Kiedy to się dzieje, wszystkie zmienne lokalne mogą zostać treningów ma indeks 0.
utracone. Jeśli aktywność zawiera fragmenty, to te fragmenty są usuwane
i ponownie odtwarzane wraz z aktywnością. Oznacza to, że wszystkie
zmienne lokalne używane przez fragmenty także tracą swój stan.

W naszym fragmencie WorkoutDetailFragment używamy zmiennej lokalnej


workoutId, w której przechowywany jest identyfikator treningu klikniętego przez
użytkownika na liście we fragmencie WorkoutListFragment. Kiedy użytkownik
obróci urządzenie, zmienna ta straci swoją dotychczasową wartość i zostanie jej
przypisana wartość domyślna, czyli 0. W efekcie fragment wyświetli szczegóły
treningu o identyfikatorze 0 — czyli pierwszego treningu na liście.

Problem utraty stanu fragmentu można rozwiązać w podobny sposób, w jaki


rozwiązaliśmy analogiczny problem dotyczący aktywności. W pierwszej kolejności
musimy przesłonić metodę onSaveInstanceState() fragmentu i zapisać
wartość interesującej nas zmiennej lokalnej w przekazanym obiekcie Bundle:
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putLong(”workoutId”, workoutId); Metoda onSaveInstanceState() jest
wywoływana przed usunięciem fragmentu.
}
Następnie możemy odczytać wartość z obiektu Bundle w metodzie onCreateView()
fragmentu:
if (savedInstanceState != null) {
workoutId = savedInstanceState.getLong(”workoutId”); nia
Możemy użyć tego identyfikatora do pobra
} zapisanego stanu zmiennej workoutId.
Zmodyfikowaną wersję kodu przedstawimy na następnej stronie.

304 Rozdział 7.
Fragmenty

Kod fragmentu WorkoutDetailFragment


package com.hfad.trenazer; Trenazer

y.
... Nie są potrzebne żadne nowe instrukcje import, więc je pomijam
app/src/main

public class WorkoutDetailFragment extends Fragment { java


private long workoutId;
com.hfad.trenazer
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { WorkoutDetail
if (savedInstanceState != null) { Fragment.java
workoutId = savedInstanceState.getLong(“workoutId”); Ustawiamy wartość zmiennej
} workoutId.
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
}

@Override
public void onStart() {
super.onStart();
View view = getView();
if (view != null) {
TextView title = (TextView) view.findViewById(R.id.textTitle);
Workout workout = Workout.workouts[(int) workoutId];
title.setText(workout.getName());
TextView description = (TextView) view.findViewById(R.id.textDescription);
description.setText(workout.getDescription());
}
} Przed usunięciem fragmentu zapisujemy wartość zmiennej workoutId
w obiekcie savedInstanceState typu Bundle. Wartość tę odtworzymy
@Override w metodzie onCreateView().
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putLong(“workoutId”, workoutId);
}

public void setWorkout(long id) {


this.workoutId = id;
}
}

Kiedy klikniesz jeden


z treningów, to informacje
o nim będą wyświetlane nawet
po obróceniu urządzenia.
Różne rozmiary ekranów

Telefon kontra tablet ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
Jest jeszcze jedna rzecz, którą chcemy zrobić z naszą aplikacją Trenażer.
¨ Utworzenie odrębnych układów
Otóż chcemy sprawić, by aplikacja działała inaczej w zależności od tego,
czy została uruchomiona na telefonie czy na tablecie.

Na tablecie
Jeśli uruchomimy aplikację
na tablecie, chcemy,
by wyglądała i działała tak
jak pokazaliśmy w tym
rozdziale. Fragmenty z listą
i ze szczegółami treningów
mają być wyświetlane obok
siebie i działać w ramach
tej samej aktywności. Jeśli
klikniemy trening, jego
szczegóły mają zostać
wyświetlone obok.

Na telefonie
Jeśli uruchomimy aplikację na telefonie, chcemy,
by działała nieco inaczej. Lista treningów ma
być wyświetlana w jednej aktywności i zajmować
cały obszar ekranu. Kiedy użytkownik kliknie
trening, ma zostać uruchomiona druga
aktywność, która wyświetli szczegółowe
informacje o tym treningu.
To są dwie
różne
aktywności,
każda
wyświetla
inny
fragment.

306 Rozdział 7.
Fragmenty

Struktury aplikacji na tablety i telefony


Oto, jak mają działać wersje aplikacji na tablety i na telefony:

Na tablecie <Layout>

</Layout>
Wersja aplikacji przeznaczona na tablety
będzie działała tak jak aktualnie działa: <Layout>
fragment_
</Layout>
workout_list.xml

activity_main.xml

WorkoutList
Fragment.java
Workout.java
MainActivity.java
Tablet
WorkoutDetail <Layout>

Na telefonie Fragment.java </Layout>

fragment_
W tym przypadku, zamiast używać obu fragmentów w aktywności workout_detail.xml
MainActivity, w tej aktywności umieścimy tylko fragment
WorkoutListFragment i dodamy drugą aktywność, DetailActivity,
w której użyjemy fragmentu WorkoutDetailFragment.

<Layout> <Layout>

</Layout> </Layout>

activity_main.xml activity_detail.xml

MainActivity.java DetailActivity.java
Telefon

<Layout> <Layout>
WorkoutList WorkoutDetail
</Layout> </Layout>
Fragment.java Fragment.java
fragment_ fragment_
workout_list.xml Workout.java workout_detail.xml

Nasza aplikacja musi wyglądać i działać nieco inaczej w zależności od tego,


czy zostanie uruchomiona na telefonie czy na tablecie. Aby ułatwić sobie
wykonanie tego zadania, sprawdźmy, co zrobić, by aplikacja wybierała różne
układy w zależności od typu urządzenia, na którym została uruchomiona.

307
Różne zasoby dla różnych ekranów

Umieszczaj zasoby przeznaczone dla różnych ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
rodzajów ekranów w odpowiednich katalogach ¨ Utworzenie odrębnych układów

Wcześniej w tej książce napisaliśmy, co zrobić, aby różne urządzenia używały


różnych zasobów graficznych w zależności od wielkości ich ekranów — wystarczy
umieszczać te zasoby w różnych katalogach drawable. W ramach przykładu
obrazy, które miały być używane na urządzeniach z ekranami o wysokiej gęstości,
umieściliśmy w katalogu drawable-hdpi.

W podobny sposób można postępować z innymi zasobami, takimi jak układy,


menu oraz wartości. Jeśli chcemy utworzyć więcej wersji tego samego zasobu
przeznaczonych dla różnych specyfikacji ekranów, wystarczy, że utworzymy katalogi
zasobów o odpowiednich nazwach. Dzięki temu urządzenie odczyta odpowiednie
zasoby w trakcie działania aplikacji, pobierając je z katalogu, który najlepiej
odpowiada bieżącej specyfikacji ekranu.

Na przykład gdybyśmy chcieli używać jednego układu na urządzeniach o dużym


ekranie i kilku innych układów na pozostałych urządzeniach, to pierwszy z nich
musielibyśmy umieścić w katalogu app/src/main/res/layout-large, a pozostałe
w katalogu app/src/main/res/layout. Jeśli aplikacja zostanie uruchomiona na
urządzeniu wyposażonym w duży ekran, to pobierze układ z katalogu layout-large:

app/src/main Wszystkie urządzenia wyposażone


w mniejsze ekrany będą wczytywały
układy z katalogu layout.
res

layout
<xml>
Te dwa układy będą </xml>
używane na urządzeniach
z mniejszymi ekranami. activity_main.xml
<xml>
</xml>

activity_detail.xml

Duże urządzenia, takie jak layout-large Ten układ będzie używany


tablety, będą wczytywały przez urządzenia z dużymi
pliki układów z katalogu
<xml>
</xml>ekranami.
layout-large.
activity_main.xml

Na następnej stronie pokażemy wszystkie opcje, których można używać,


określając nazwy katalogów zasobów.

308 Rozdział 7.
Fragmenty

Różne opcje katalogów


Zasoby wszystkich dostępnych rodzajów (zasoby graficzne i rysunki, układy, menu
oraz wartości) możemy umieszczać w różnych katalogach, aby określać, na jakich
urządzeniach mają one być używane. Nazwa katalogu przeznaczona dla urządzeń
o określonej specyfikacji ekranu może zawierać informacje o wielkości ekranu, jego
gęstości, orientacji, proporcjach, przy czym muszą one być od siebie oddzielone
znakami minusa (-). Na przykład gdybyśmy chcieli stworzyć układ, który będzie
używany wyłącznie na bardzo dużych tabletach w orientacji poziomej, musielibyśmy
utworzyć katalog layout-xlarge-land i umieścić w nim plik takiego układu. Poniżej
przedstawiliśmy różne opcje, które można dodawać do nazw katalogów:
Gęstość ekranu jest wyrażana
jako liczba punktów na cal.
Typ zasobu musi zostać określony.

Typ zasobu Wielkość ekranu Gęstość ekranu Orientacja Proporcje


drawable -small -ldpi -land -long

layout -normal -mdpi -port -notlong

menu -large -hdpi Wartość long odnosi


się do ekranów,
mipmap -xlarge -xhdpi które mają bardzo
dużą wysokość.
values -xxhdpi

Zasoby mipmap są używane -xxxhdpi


do przechowywania ikon Ta wartość odnosi się do zasobów
aplikacji. W starszych -nodpi niezależnych od gęstości ekranu.
wersjach Android Studio Używaj katalogu z tą opcją (czyli
były do tego używane drawable-nodpi) do przechowywania
zwyczajne zasoby graficzne. -tvdpi
wszelkich zasobów z obrazkami,
których nie chcesz skalować.

Android określi, które zasoby zastosować, w trakcie działania aplikacji, starając się dobrać
je jak najlepiej. Jeśli nie uda mu się znaleźć dokładnego dopasowania, to użyje zasobów
przeznaczonych dla mniejszych ekranów niż ten, w który jest wyposażone urządzenie.
Jeśli dostępne zasoby będą przeznaczone wyłącznie dla większych ekranów niż ekran
Więcej informacji na
danego urządzenia, to aplikacja przestanie działać.
temat przedstawionych
z
Jeśli chcemy, by aplikacja działała wyłącznie na urządzeniach z ekranami o określonej wielkości, tutaj ustawień możes
zna leź ć na str oni e
to możemy tego zażądać, dodając w pliku manifestu aplikacji, AndroidManifest.xml, odpowiednie
https://developer.
atrybuty do elementu <supported-screens>. Na przykład jeśli nie chcemy, by aplikacja działała
android.com/guide/
na urządzeniach z małymi ekranami, to możemy użyć elementu o poniższej postaci: practices/screens_
<supported-screens android:smallScreens=”false”>
support.html.

Korzystając z katalogów o odpowiednich nazwach, bez trudu możemy przygotować układy


przeznaczone do użycia na tabletach i na telefonach. Zacznijmy od wersji naszej aplikacji
przeznaczonej na tablety.

309
Ćwiczenie

Bądź strukturą katalogów


Poniżej przedstawiliśmy kod aktywności.
Chcesz, aby jeden układ był używany na
urządzeniach z dużymi ekranami, a drugi
na urządzeniach z ekranami
normalnej wielkości. Która
z przedstawionych struktur
katalogów zapewni takie
działanie aplikacji?
To jest aktywność.

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
}

A B
app/src/main app/src/main

res res

layout layout
<xml> <xml>
</xml> </xml>

activity_main.xml activity_main.xml

layout-tablet layout-large-land
<xml> <xml>
</xml> </xml>

activity_main.xml activity_main.xml

310 Rozdział 7.
Fragmenty

C D
app/src/main app/src/main

res res

layout layout
<xml> <xml>
</xml> </xml>

activity_main.xml activity_main.xml

<xml>
layout-large </xml>

<xml> activity_main_tablet.xml
</xml>

activity_main.xml

E F
app/src/main app/src/main

res res

layout-large layout
<xml> <xml>
</xml> </xml>

activity_main.xml activity_main.xml

layout-normal layout-large-land
<xml> <xml>
</xml> </xml>

activity_main.xml activity_main.xml

layout-large-port
<xml>
</xml>

activity_main.xml

311
Rozwiązanie

Bądź strukturą katalogów. Rozwiązanie


Poniżej przedstawiliśmy kod aktywności.
Chcesz, aby jeden układ był używany na
urządzeniach z dużymi ekranami,
a drugi na urządzeniach
z ekranami normalnej wielkości.
Która z przedstawionych
struktur katalogów zapewni
takie działanie aplikacji?

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
}

A B
app/src/main app/src/main

res Urządzenia z dużymi res


Android nie rozpoznaje ekranami będą używały
nazwy katalogu layout układu z katalogu layout- layout
layout-tablet. Układ <xml> large-land, kiedy będą <xml>
</xml> </xml>
activity_main.xml miały orientacją poziomą.
activity_main.xml activity_main.xml
z katalogu layout Po obróceniu urządzenia
będzie wyświetlany do pionu zostanie użyty
layout-tablet layout-large-land
na wszystkich układ z katalogu layout.
<xml> <xml>
urządzeniach. </xml> </xml>

activity_main.xml activity_main.xml

312 Rozdział 7.
Fragmenty

C D
app/src/main app/src/main

Urządzenia z dużymi res res

ekranami będą Aktywność używa


layout layout
używały układu układu o nazwie
<xml> <xml>
z katalogu layout-large. </xml> activity_main.xml. </xml>
Urządzenia z mniejszymi activity_main.xml Układ zapisany activity_main.xml
ekranami będą używały w pliku activity_
<xml>
układu z katalogu layout. layout-large main_tablet.xml </xml>

<xml> nie będzie używany. activity_main_tablet.xml


</xml>

activity_main.xml

E F
app/src/main app/src/main

Urządzenia z dużymi res Urządzenia z dużymi res

ekranami będą ekranami będą używały


layout-large layout
używały układu układu z katalogu
<xml> <xml>
z katalogu layout-large. </xml> layout-large-land, </xml>

Urządzenia z normalnymi activity_main.xml kiedy będą działały activity_main.xml


w orientacji poziomej,
ekranami będą używały
layout-normal i z katalogu layout- layout-large-land
układu z katalogu
<xml>
large-port, gdy będą <xml>
layout-normal. Nie ma </xml> działały w orientacji </xml>
żadnego katalogu activity_main.xml activity_main.xml
pionowej. Pozostałe
z układem dla urządzeń urządzenia będą
z małymi ekranami, używały układu layout-large-port
dlatego aplikacja nie zapisanego <xml>
będzie na nich działać. w katalogu layout. </xml>

activity_main.xml

313
Katalog layout-large

Tablety używają układów zapisanych ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
w katalogu layout-large ¨ Utworzenie odrębnych układów
Przygotowanie wersji naszej aplikacji przeznaczonej na tablety jest całkiem łatwe
— musimy jedynie skopiować aktualnie używany układ activity_main.xml do
katalogu app/src/main/res/layout-large. Układ umieszczony w tym katalogu będzie
używany wyłącznie przez urządzenia z dużymi ekranami.

Jeśli w Twoim projekcie aplikacji w Android Studio katalog app/src/main/res/


layout-large nie będzie istniał, to musisz go utworzyć. W tym celu wyświetl
strukturę katalogów projektu w widoku Project, zaznacz katalog app/src/main/
res, a następnie wybierz opcję File/New.../Directory. Kiedy zostaniesz poproszony
o podanie nazwy katalogu, wpisz layout-large. Po kliknięciu przycisku OK
Android Studio utworzy nowy katalog — app/src/main/res/layout-large.

Aby skopiować plik activity_main.xml, zaznacz go, a potem wybierz z menu opcję
Edit/Copy. Następnie zaznacz katalog layout-large i wybierz opcję Edit/Paste.
W rezultacie Android Studio skopiuje plik activity_main.xml do katalogu
app/src/main/res/layout-large.

Kiedy otworzysz skopiowany plik układu, jego zawartość powinna mieć


następującą postać:

<?xml version=”1.0” encoding=”utf-8”?>


<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”horizontal”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
Trenazer

<fragment
app/src/main
class=”com.hfad.trenazer.WorkoutListFragment”
android:id=”@+id/list_frag” res
Nie wprowadzaliśmy
android:layout_width=”0dp” w układzie żadnych
android:layout_weight=”2” zmian — po prostu layout-large
skopiowaliśmy go do
android:layout_height=”match_parent”/> katalogu layout-large. <xml>
</xml>
<FrameLayout activity_main.xml
android:id=”@+id/fragment_container”
android:layout_width=”0dp”
android:layout_weight=”3”
android:layout_height=”match_parent” />
</LinearLayout>

Ten układ będzie używany przez urządzenia z dużymi ekranami, a więc


kiedy nasza aplikacja zostanie uruchomiona na tablecie, oba fragmenty będą
wyświetlone jeden obok drugiego. Teraz zajmiemy się układami dla telefonów.

314 Rozdział 7.
Fragmenty

Układ MainActivity dla telefonów


W przypadku uruchamiania aplikacji na telefonie chcemy, żeby aktywność
MainActivity wyświetlała wyłącznie fragment WorkoutListFragment,
a drugi fragment, WorkoutDetailFragment, ma nie być wyświetlany
w aktywności głównej. W tym celu zaktualizujemy kod układu activity_main.xml
przechowywanego w katalogu app/src/main/res/layout w taki sposób,
by zawierał odwołanie wyłącznie do fragmentu WorkoutListFragment.
Aplikacja uruchomiona na telefonach będzie używała układu z katalogu Chcemy, by w przypadku
layout, natomiast w przypadku uruchomienia jej na tablecie zostanie użyty uruchamiania aplikacji
na telefonach aktywność
układ z katalogu layout-large. MainActivity wyświetlała
wyłącznie fragment
Aby wprowadzić niezbędne modyfikacje, otwórz plik activity_main.xml WorkoutListFragment.
zapisany w katalogu app/src/main/res/layout, a potem zastąp jego
wcześniejszą zawartość następującym kodem:

<?xml version=”1.0” encoding=”utf-8”?>


<fragment xmlns:android=”http://schemas.android.com/apk/res/android”
class=”com.hfad.trenazer.WorkoutListFragment”
android:id=”@+id/list_frag” Trenazer
android:layout_width=”match_parent”
android:layout_height=”match_parent”/> app/src/main

Upewnij się, że zmiany wprowad


zasz res
w pliku activity_main.xml zapisany
w katalogu layout. m

layout
<xml>
Ponieważ w przypadku uruchamiania aplikacji na telefonach </xml>

aktywność MainActivity musi wyświetlić wyłącznie fragment activity_main.xml


WorkoutListFragment, nie ma sensu używać odrębnego
układu zawierającego element <fragment>. To rozwiązanie jest
niezbędne wyłącznie w przypadku wyświetlania większej liczby
fragmentów.
Aby aplikacja
Zwróć uwagę, że wersja pliku activity_main.xml zapisana wyglądała inaczej
w katalogu layout nie zawiera układu FrameLayout fragment_ Spokojnie na telefonach i na
container, natomiast układ activity_main.xml z katalogu layout- tabletach, używamy
large zawiera taki element. Wynika to z faktu, że wyłącznie wersja dwóch układów zapisanych w dwóch
układu z katalogu layout-large musi wyświetlać drugi fragment plikach o tej samej nazwie.
— WorkoutDetailFragment. Nieco później wykorzystamy ten
Kilka kolejnych stron czytaj powoli
fakt, by określać, którego układu używa aplikacja na urządzeniu
i uważnie i koniecznie się upewnij,
użytkownika.
że będziesz modyfikować odpowiednią
Naszym kolejnym zadaniem będzie utworzenie drugiej aktywności, wersję układu.
która będzie używała fragmentu WorkoutDetailFragment.

315
Utworzenie aktywności DetailActivity

Na telefonach do wyświetlania szczegółów ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
będzie używana aktywność DetailActivity ¨ Utworzenie odrębnych układów

Teraz zajmiemy się utworzeniem aktywności DetailActivity. To właśnie


ona będzie korzystała z fragmentu WorkoutDetailFragment i będzie
używana na telefonach do wyświetlania informacji o wybranym treningu.
Trenazer
Użyj kreatora New Activity Android Studio, by utworzyć nową pustą
aktywność. Zapisz ją w pliku DetailActivity.java i określ, że ma ona używać app/src/main
pliku układu o nazwie activity_detail.xml. Ten plik układu musisz umieścić
w katalogu app/src/main/res/layout, tak by mogły z niego korzystać res
wszystkie urządzenia.
layout
Układ tej nowej aktywności musi zawierać wyłącznie fragment
<xml>
WorkoutDetailFragment. A zatem zastąp wygenerowaną zawartość pliku </xml>
activity_detail.xml następującym kodem: activity_detail.xml

<?xml version=”1.0” encoding=”utf-8”?>


<fragment xmlns:android=”http://schemas.android.com/apk/res/android” Aktywność DetailActivity będzie
ment
wyświetlać wyłącznie jeden frag
class=”com.hfad.trenazer.WorkoutDetailFragment” — WorkoutDetailFragment.
android:id=”@+id/detail_frag”
android:layout_width=”match_parent”
android:layout_height=”match_parent”/>

Oprócz układu activity_detail.xml musimy także zmodyfikować


kod samej aktywności DetailActivity. Kiedy aplikacja zostanie
uruchomiona na telefonie, aktywność MainActivity będzie musiała
uruchamiać aktywność DetailActivity za pomocą intencji. Intencja
ta będzie musiała zawierać informację dodatkową, określającą
identyfikator treningu wybranego przez użytkownika. Następnie
aktywność DetailActivity będzie musiała przekazać tę informację
do fragmentu WorkoutDetailFragment, wywołując w tym celu jego
metodę setWorkout().

Intencja
setWorkout(1)

id: 1

MainActivity DetailActivity WorkoutDetailFragment

wności.
Musimy zmodyfikować obie te akty
Zaczniemy do DetailActivity.

316 Rozdział 7.
Fragmenty

Zagadkowy basen
Twoim zadaniem jest wyłowienie fragmentów
z basenu i umieszczenie ich w pustych
miejscach kodu pliku DetailActivity.java.
Żadnego fragmentu kodu nie możesz użyć
więcej niż jeden raz i nie wszystkie z tych
fragmentów będą Ci potrzebne. Twoim
celem jest odczytanie identyfikatora
treningu zapisanego w intencji i przekazanie go
dalej do fragmentu WorkoutDetailFragment.
package com.hfad.trenazer;

import android.app.Activity;
import android.os.Bundle; Nazwę informacji dodatkowej
przekazywanej w intencji
określiliśmy za pomocą stałej,
public class DetailActivity extends Activity { aby mieć pewność, że ta sama
nazwa będzie używana w obu
public static final String EXTRA_WORKOUT_ID = ”id”; aktywnościach — MainActivity
i DetailActivity.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
. ...................workoutDetailFragment = (.....................)
. ..............................(R.id.detail_frag);
int workoutId = (int) getIntent().getExtras().get(EXTRA_WORKOUT_ID);
workoutDetailFragment.setWorkout(workoutId);
}
}

Wyłów odpowiednie fragmenty


kodu z basenu.

WorkoutDetailFragment

findFragmentById getFragmentManager()

.
findViewById

317
Rozwiązanie

Zagadkowy basen. Rozwiązanie ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
Twoim zadaniem jest wyłowienie fragmentów
¨ Utworzenie odrębnych układów
z basenu i umieszczenie ich w pustych
miejscach kodu pliku DetailActivity.java.
Żadnego fragmentu kodu nie możesz użyć
więcej niż jeden raz i nie wszystkie z tych
fragmentów będą Ci potrzebne. Twoim
celem jest odczytanie identyfikatora
treningu zapisanego w intencji i przekazanie go
dalej do fragmentu WorkoutDetailFragment.
package com.hfad.trenazer;

import android.app.Activity;
import android.os.Bundle;

public class DetailActivity extends Activity {


public static final String EXTRA_WORKOUT_ID = ”id”;

@Override
y,
protected void onCreate(Bundle savedInstanceState) { Referencję do fragmentu pobieram )
yId(
wywołując metodę findFragmentB
super.onCreate(savedInstanceState); menedżera fragmentów.
setContentView(R.layout.activity_detail);
WorkoutDetailFragment workoutDetailFragment = (..........................
........................ WorkoutDetailFragment )
getFragmentManager() . findFragmentById
. ................ ........... ..........(R.id.detail_frag);
int workoutId = (int) getIntent().getExtras().get(EXTRA_WORKOUT_ID);
workoutDetailFragment.setWorkout(workoutId);
}
}
Te fragmenty kodu
nie były potrzebne.

findViewById

318 Rozdział 7.
Fragmenty

Kompletny kod aktywności DetailActivity


Oto kompletny kod aktywności DetailActivity (zastąp nim kod
wygenerowany przez Android Studio):

package com.hfad.trenazer; Trenazer

import android.app.Activity; app/src/main


import android.os.Bundle;
java

public class DetailActivity extends Activity {


com.hfad.trenazer
public static final String EXTRA_WORKOUT_ID = ”id”;

@Override DetailActivity.java

protected void onCreate(Bundle savedInstanceState) {


Pobieramy
super.onCreate(savedInstanceState); referencję do
setContentView(R.layout.activity_detail); fragmentu.

WorkoutDetailFragment workoutDetailFragment = (WorkoutDetailFragment)


getFragmentManager().findFragmentById(R.id.detail_frag);
int workoutId = (int) getIntent().getExtras().get(EXTRA_WORKOUT_ID);
workoutDetailFragment.setWorkout(workoutId);
Pobieramy z intencji identyfikator
} treningu klikniętego przez
użytkownika.
} Przekazujemy do fragmentu
identyfikator treningu.

Kod aktywności DetailActivity pobiera identyfikator treningu z intencji,


która spowodowała uruchomienie aktywności. Kolejną rzeczą, którą musimy
się zająć, będzie uruchamianie aktywności DetailActivity z poziomu
kodu aktywności MainActivity, lecz wyłącznie jeśli aplikacja zostanie
uruchomiona na telefonie.

A jak możemy to określić?

319
Który układ

Skorzystaj z różnic w układach, ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
aby określić, który z nich został użyty ¨ Utworzenie odrębnych układów

Chcemy, by aktywność MainActivity wykonywała różne operacje po kliknięciu


treningu, zależnie od tego, czy został wyświetlony układ activity_main.xml
z katalogu layout, czy layout-large.

W przypadku uruchomienia aplikacji na telefonie zostanie


użyty układ activity_main.xml z katalogu layout. Ten układ
nie zawiera fragmentu WorkoutDetailFragment, dlatego
jeśli użytkownik kliknie któryś z wyświetlonych treningów,
to aktywność MainActivity ma uruchomić aktywność
DetailActivity.

Natomiast gdy aplikacja zostanie uruchomiona na tablecie, zostanie


użyty układ activity_main.xml z katalogu layout-large. Zawiera on
układ FrameLayout o identyfikatorze fragment_container,
w którym prezentowany jest fragment WorkoutDetailFragment.
W tym przypadku po kliknięciu treningu chcemy wyświetlić
w układzie frgment_container nową instancję fragmentu
WorkoutDetailFragment.

Układ fragment_container,
w którym jest wyświetlany
fragment WorkoutDetailFragment,
jest używany wyłącznie w układzie
activity_main.xml zapisanym
w katalogu layout-large.

Oba te przypadki możemy obsłużyć w kodzie MainActivity, sprawdzając,


który z układów jest używany na danym urządzeniu. To z kolei możemy określić,
próbując pobrać widok o identyfikatorze fragment_container.

Jeśli widok fragment_container zostanie odnaleziony, będzie to oznaczać,


że aplikacja używa układu activity_main.xml z katalogu layout-large. Wówczas
w odpowiedzi na kliknięcie któregoś z treningów będziemy musieli wyświetlić
nową instancję fragmentu WorkoutDetailFragment. Jeśli jednak widok
fragment_container nie zostanie odnaleziony, będzie to sygnałem, że urządzenie
używa wersji układu activity_main.xml z katalogu layout, a my w odpowiedzi na
kliknięcie treningu musimy uruchomić aktywność DetailFragment.
320 Rozdział 7.
Fragmenty

Zmodyfikowany kod aktywności MainActivity


Poniżej przedstawiliśmy kompletny kod aktywności MainActivity
(pamiętaj, aby w swoim projekcie wprowadzić zmiany wyróżnione pogrubieniem):
Trenazer

package com.hfad.trenazer;
app/src/main

import android.app.Activity;
java
import android.app.FragmentTransaction;
import android.content.Intent;
com.hfad.trenazer
import android.os.Bundle; Tych dodatkowych klas
import android.view.View; używamy w kodzie metody
itemClicked(). MainActivity.java

public class MainActivity extends Activity


implements WorkoutListFragment.WorkoutListListener {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); układu
Próbujemy pobrać referencję do fragment
o
} typu FrameLayout, zawierająceg
ona dostępna,
WorkoutDetailFragment. Będzie na urządzeniu
jeśli aplik acja zost ała uruc hom iona
@Override z dużym ekra nem .
public void itemClicked(long id) {
View fragmentContainer = findViewById(R.id.fragment_container);
if (fragmentContainer != null) {
Ten kod WorkoutDetailFragment details = new WorkoutDetailFragment();
ma być
wykonywany FragmentTransaction ft = getFragmentManager().beginTransaction();
wyłącznie,
jeśli uda details.setWorkout(id);
się odnaleźć ft.replace(R.id.fragment_container, details);
poszukiwany
układ ft.addToBackStack(null);
FrameLayout. ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
} else {
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra(DetailActivity.EXTRA_WORKOUT_ID, (int)id);
startActivity(intent);
Jeśli układu FrameLayout nie uda się znaleźć, będzie
} to oznaczało, że aplikacja została uruchomiona na
} urządzeniu z mniejszym ekranem. Wówczas musimy
uruchomić aktywność DetailActivity, przekazując do
} niej identyfikator klikniętego treningu.
Sprawdźmy teraz, co się stanie po uruchomieniu aplikacji.
321
Jazda próbna

Jazda próbna aplikacji ¨ Utworzenie fragmentów


¨ Powiązanie fragmentów
Kiedy uruchomimy aplikację na tablecie, będzie ona wyglądała tak
¨ Utworzenie odrębnych układów
jak wcześniej. Z lewej strony ekranu będzie widoczna lista dostępnych
treningów, a gdy użytkownik kliknie jeden z nich, to informacje o nim
zostaną wyświetlone po prawej stronie ekranu.

lądała
Na tabletach aplikacja będzie wyg
j.
dokładnie tak samo jak wcześnie

W przypadku uruchomienia aplikacji na telefonie najpierw na ekranie


zostanie wyświetlona lista nazw treningów. Po kliknięciu jednego z nich
szczegółowe informacje o nim zostaną wyświetlone w osobnej aktywności.

W przypadku uruchomienia
aplikacji na telefonie będzie …a po kliknięciu jednego
ona wyglądała inaczej. z wyświetlonych na niej treningów
Aktywność MainActivity informacje o nim zostaną
wyświetla pojedynczą listę… wyświetlone w osobnej aktywności.

322 Rozdział 7.
Fragmenty

Twój przybornik do Androida

Rozdział 7.
Opanowałeś już rozdział 7. i dodałeś
do swojego przybornika z narzędziami
umiejętność tworzenia i stosowania
fragmentów. Metody cyklu życia fragmentów
onAttach()
CELNE SPOSTRZEŻENIA

 Fragment służy do zarządzania  Fragmenty nie dysponują metodą onCreate()


pewnym obszarem ekranu. findViewById(). Zamiast niej
Można go używać w wielu definiują metodę getView(),
aktywnościach. która zwraca referencję do widoku
głównego; używając tej referencji, onCreateView()
 Z fragmentem jest skojarzony
można wywoływać metodę
układ.
findViewById().
 Fragmenty są klasami pochodnymi  Fragment listy to fragment onActivityCreated()
klasy android.app.Fragment.
używający komponentu
 Metoda onCreateView() jest ListView. Taki fragment można
wywoływana za każdym razem, utworzyć, definiując klasę
gdy Android potrzebuje układu dziedziczącą po ListFragment. onStart()
fragmentu.  Jeśli fragment ma reagować na
 Fragment można dodać do układu zmiany zachodzące w interfejsie
aktywności, używając elementu użytkownika, to należy użyć
onResume()
<fragment> z atrybutem class. elementu <FrameLayout>.
 Metody cyklu życia fragmentu  Gdy wprowadzamy zmiany
są ściśle powiązane ze stanami w istniejącym fragmencie, należy
aktywności, która ten fragment zastosować transakcję fragmentu onPause()
zawiera. i dodać ją do stosu cofnięć.
 Klasa Fragment nie rozszerza  Aplikacje mogą wyglądać inaczej
klasy Activity ani nie na różnych urządzeniach onStop()
implementuje interfejsu Context. wystarczy umieścić różne układy
w odpowiednich katalogach.

j
Pełny kod przykładowe onDestroyView()
tow ane j
aplikacji prezen
dzi ale mo żes z
w tym roz
pob rać z ser we ra FT P
wydawnictwa Helion: onDestroy()
ftp://ftp.helion.pl/
przyklady/andrrg.zip

onDetach()

323
324 Rozdział 7.
8. Fragmenty zagnieżdżone

Zadbaj o potomstwo
Przycisk Wstecz wprost
oszalał, wszędzie wokół
pełno było transakcji. Więc
walnąłem w nie wywołaniem
getChildFragmentManager()
i — BUM! Wszystko wróciło
do normalnego stanu.

Wiesz już, że stosowanie fragmentów w aktywnościach pozwala na


wielokrotne wykorzystywanie kodu i zwiększa elastyczność aplikacji.
W tym rozdziale mamy zamiar pokazać Ci, jak zagnieżdżać fragmenty w innych
fragmentach. Dowiesz się, jak używać menedżera fragmentów podrzędnych,
by poskromić niesforne transakcje. Ponadto dowiesz się, dlaczego tak ważna jest
znajomość różnic między aktywnościami i fragmentami.

to jest nowy rozdział  325


Fragmenty zagnieżdżone

Tworzenie zagnieżdżonych fragmentów


W rozdziale 7. pokazaliśmy Ci, jak tworzyć fragmenty, jak używać ich w aktywnościach
oraz jak łączyć je ze sobą. Do tego celu stworzyliśmy fragment listy prezentujący listę
treningów i fragment wyświetlający szczegółowe informacje o konkretnym treningu.

Ale nie tylko aktywności mogą zawierać fragmenty — okazuje się, że fragmenty można
umieszczać także wewnątrz innych fragmentów. Aby się o tym przekonać, napiszemy
teraz fragment stopera, który następnie umieścimy we fragmencie ze szczegółowymi
informacjami o treningu.

Fragment
WorkoutDetailFragment
wyświetla szczegółowe
informacje o treningu
klikniętym przez użytkownika.

Do fragmentu
WorkoutDetailFragment
dodamy fragment stopera.
Fragment
WorkoutListFragment
prezentuje listę
treningów.

Dodamy nowy fragment stopera To wszystko


napisaliśmy <Layout>
w rozdziale 7.
Planujemy dodać nowy fragment o nazwie </Layout>

StopwatchFragment.java, który będzie używał układu <Layout>


fragment_
fragment_stopwatch.xml. Jego implementację oprzemy </Layout>
workout_list.xml
na aktywności stopera, którą zaimplementowaliśmy activity_main.xml
w rozdziale 4.

Jak już wiesz, aktywności WorkoutList


i fragmenty zachowują się Fragment.java
podobnie, lecz są różnymi typami Workouts.java
obiektów — fragmenty nie są MainActivity.java
bowiem klasami pochodnymi Tablet
klasy Activity. Czy możemy WorkoutDetail <Layout>

w jakiś sposób przepisać kod Fragment.java </Layout>

aktywności, by działała ona fragment_


jako fragment? <Layout> workout_detail.xml
</Layout> Stopwatch
Teraz zajmiemy się stworzeniem
Fragment.java fragmentu StopwatchFragment.java
fragment_
i jego układu.
326 Rozdział 8. stopwatch.xml
Fragmenty zagnieżdżone

Cykle życiowe aktywności i fragmentów są podobne…


Aby zrozumieć, jak przepisać aktywność, nadając jej formę fragmentu, musimy pomyśleć
o podobieństwach pomiędzy nimi. Kiedy przyjrzymy się cyklom życia fragmentów
i aktywności, to zauważymy, że są one bardzo podobne.

Metoda cyklu życia Aktywności? Fragmenty?


onAttach()

onCreate()

onCreateView()

onActivityCreated()

onStart()

onPause()

onResume()

onStop()

onDestroyView()

onRestart()

onDestroy()

onDetach()

…choć tworzące je metody są nieco inne


Metody tworzące cykl życia fragmentu są niemal takie same jak metody cyklu
życia aktywności — z jedną podstawową różnicą: metody cyklu życia aktywności są
chronione, natomiast metody cyklu życia fragmentów są publiczne. Przekonaliśmy
się także, że fragmenty tworzą swoje układy w nieco inny sposób.

Oprócz tego we fragmentach nie można bezpośrednio wywoływać takich metod jak
findViewById(). Zamiast tego najpierw należy pobrać referencję do obiektu View,
a dopiero potem można używać wywołania o postaci view.findViewById().

Nadszedł czas, by pamiętając o tych wszystkich podobieństwach i różnicach, napisać


trochę kodu…
jesteś tutaj  327
Ćwiczenie

Zaostrz ołówek
Poniżej przedstawiliśmy napisany już wcześniej kod aktywności StopwatchActivity.
Teraz masz za zadanie skonwertować go na fragment o nazwie StopwatchFragment.
Weź do ręki ołówek i wprowadź niezbędne zmiany. Pamiętaj przy tym o następujących
zagadnieniach:
– Zamiast pliku układu o nazwie activity_stopwatch.xml fragment ma używać pliku układu
o nazwie fragment_stopwatch.xml.
– Upewnij się, że zastosowałeś odpowiednie ograniczenia dostępu do metod.
– W jaki sposób określisz używany układ?
– Metoda runTimer() nie będzie mogła korzystać z metody findViewById(), dlatego
może wygodnie będzie przekazać do metody runTimer() jakiś obiekt klasy View.

public class StopwatchActivity extends Activity {


// Liczba sekund wyświetlana przez stoper
private int seconds = 0; Liczba zmierzonych sekund.
// Czy stoper działa?
private boolean running; Zmienna running informuje,
czy stoper aktualnie działa, czy
private boolean wasRunning; nie.
Zmienna wasRunning informuje,
czy przed wstrzymaniem
aktywności stoper działał, czy nie.
@Override
protected void onCreate(Bundle savedInstanceState) {
Jeśli aktywność została usunięta
super.onCreate(savedInstanceState); i ponownie utworzona,
setContentView(R.layout.activity_stopwatch); to odtwarzamy wartości
ze
zmiennych zapisane w parametr
save dIns tanc eSta te typu Bun dle.
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(”wasRunning”);
}
().
runTimer(); Tu wywołujemy metodę runTimer

@Override
protected void onPause() { W tej metodzie zatrzymujemy stop
jeśli działanie aktywności zostało er,
super.onPause(); wstr zymane.
wasRunning = running;
running = false;
}

328 Rozdział 8.
Fragmenty zagnieżdżone

działanie
@Override Ta metoda uruchamia stoper, jeśli
protected void onResume() { aktywności zostało wznowione.
super.onResume();
if (wasRunning) {
running = true;
} W tej metodzie zapisujemy stan
} aktywności przed jej usunięciem.

@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(”wasRunning”, wasRunning);
}

public void onClickStart(View view) {


running = true;
}
iają,
Te metody odpowiednio urucham
public void onClickStop(View view) { zatr zym ują i kasu ją stop er,
cisk
running = false; w zależności od tego, który przy
} został kliknięty.

public void onClickReset(View view) {


running = false;
seconds = 0; Tu używamy obiektu Handler, by przekazać kod,
który będzie inkrementował liczbę sekund
} i odpowiednio, cyklicznie co sekundę, aktualizował
zmierzony czas wyświetlany przez stoper.
private void runTimer() {
final TextView timeView = (TextView)findViewById(R.id.time_view);
final Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
int hours = seconds/3600;
int minutes = (seconds%3600)/60;
int secs = seconds%60;
String time = String.format(”%d:%02d:%02d”, hours, minutes, secs);
timeView.setText(time);
if (running) {
seconds++;
}
handler.postDelayed(this, 1000);
}
});
}
}

jesteś tutaj  329


Rozwiązanie

Zaostrz ołówek
Rozwiązanie Poniżej przedstawiliśmy napisany już wcześniej kod aktywności StopwatchActivity.
Teraz masz za zadanie skonwertować go na fragment o nazwie StopwatchFragment.
Weź do ręki ołówek i wprowadź niezbędne zmiany. Pamiętaj przy tym o następujących
zagadnieniach:
– Zamiast pliku układu o nazwie activity_stopwatch.xml fragment ma używać pliku układu
o nazwie fragment_stopwatch.xml.
– Upewnij się, że zastosowałeś odpowiednie ograniczenia dostępu do metod.
– W jaki sposób określisz używany układ?
– Metoda runTimer() nie będzie mogła korzystać z metody findViewById(), dlatego
może wygodnie będzie przekazać do metody runTimer() jakiś obiekt klasy View.
To jest nowa nazwa.

public class StopwatchActivity StopwatchFragment extends Activity Fragment {


// Liczba sekund wyświetlana przez stoper
private int seconds = 0; Teraz rozszerzamy klasę
// Czy stoper działa? Fragment, a nie Activity.
private boolean running;
private boolean wasRunning;

Ta metoda musi być publiczna.


@Override
protected public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); Układ używany przez fragment
setContentView(R layout.activity_stopwatch); nie jest określany w metodzie
if (savedInstanceState != null) { onCreate().
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(”wasRunning”);
}
runTimer(); Ten kod możemy zostawić w metodzie onCreate().
} Nie wywołujemy metody runTimer(), gdyż jeszcze nie został
przygotowany układ — widoki nie są jeszcze dostępne.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Układ fragmentu określamy
Bundle savedInstanceState) { w metodzie onCreateView().
View layout = inflater.inflate(R.layout.fragment_stopwatch, container, false);
runTimer(layout);
Widok układu przekazujemy
return layout; w wywołaniu metody runTime().
}

Ta metoda musi być publiczna.


@Override
protected public void onPause() {
super.onPause();
wasRunning = running;
running = false;
}

330 Rozdział 8.
Fragmenty zagnieżdżone

Tametoda musi być publiczna.


@Override
protected public void onResume() {
super.onResume();
if (wasRunning) {
running = true;
}
}

Ta metoda musi być publiczna.


@Override
protected public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt(”seconds”, seconds);
savedInstanceState.putBoolean(”running”, running);
savedInstanceState.putBoolean(”wasRunning”, wasRunning);
}

public void onClickStart(View view) {


running = true;
}

public void onClickStop(View view) {


running = false;
}

public void onClickReset(View view) {


running = false;
seconds = 0;
Teraz do metody runTimer() przekazywany
} jest obiekt typu View.

private void runTimer(View view) {


final TextView timeView = (TextView) view.findViewById(R.id.time_view);
final Handler handler = new Handler();
Użyj parametru view do wywołania
handler.post(new Runnable() { metody findViewById().
@Override
public void run() {
int hours = seconds/3600;
int minutes = (seconds%3600)/60;
int secs = seconds%60;
String time = String.format(”%d:%02d:%02d”, hours, minutes, secs);
timeView.setText(time);
if (running) {
seconds++;
}
handler.postDelayed(this, 1000);
}
});
}
}

jesteś tutaj  331


Kod StopwatchFragment

Kod fragmentu StopwatchFragment


Teraz dodamy do naszego projektu aplikacji Trenażer fragment StopwatchFragment,
tak byśmy mogli go zastosować w aplikacji. Zrobimy to dokładnie w taki sam sposób,
w jaki dodaliśmy fragmenty w rozdziale 7. Wybierz z menu opcje File/New…/Fragment/
Fragment (Blank). Tworzonemu fragmentowi nadaj nazwę StopwatchFragment, a używanemu
przez niego układowi nazwę fragment_stopwatch; usuń także zaznaczenia z pól wyboru
umożliwiających wygenerowanie metod wytwórczych i funkcji zwrotnych interfejsu.

Po kliknięciu przycisku Finish Android Studio wygeneruje nowy fragment, zapisany w pliku
StopwatchFragment.java umieszczonym w katalogu app/src/main/java. Teraz zastąp zawartość
tego pliku wygenerowaną przez Android Studio poniższym kodem (to zaktualizowany kod
aktywności, który zmodyfikowaliśmy w ramach ostatniego ćwiczenia):

package com.hfad.trenazer;
Trenazer
import android.os.Bundle;
import android.os.Handler; app/src/main
import android.app.Fragment;
import android.view.LayoutInflater; java
import android.view.View;
import android.view.ViewGroup; com.hfad.trenazer
import android.widget.TextView;
Stopwatch
public class StopwatchFragment extends Fragment { Fragment.java
// Liczba sekund wyświetlana przez stoper
private int seconds = 0; Liczba zmierzonych sekund.
// Czy stoper działa? Zmienna running informuje, czy
stoper
private boolean running; aktualnie działa, czy nie.

private boolean wasRunning; Zmienna wasRunning informuje,


czy przed wstrzymaniem
aktywności stoper działał, czy nie.
@Override
public void onCreate(Bundle savedInstanceState) { Odtworzenie wartości zmiennych
super.onCreate(savedInstanceState); zapisanych w parametrze
savedInstanceState typu Bundle.
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”);
wasRunning = savedInstanceState.getBoolean(”wasRunning”);
if (wasRunning) {
running = true;
}
}
}

332 Rozdział 8.
Fragmenty zagnieżdżone

Kod aktywności StopwatchFragment (ciąg dalszy)


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_stopwatch, container, false);
runTimer(layout); Metoda określa układ fragmentu
return layout; i wywołuje metodę runTimer(),
przekazując do niej obiekt układu.
}

@Override
public void onPause() {
super.onPause(); Trenazer
Jeśli działanie fragmentu
wasRunning = running; zostało wstrzymane, zapisujemy,
running = false; czy stoper był włączony, app/src/main
po czym go zatrzymujemy.
}
java

@Override
com.hfad.trenazer
public void onResume() {
super.onResume();
Stopwatch
if (wasRunning) { Fragment.java

running = true; Jeśli stoper działał, zanim zost
wstr zym any, to tuta j go pono wnie
} uruchamiamy.
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) { Ta metoda zapisuje
wartości zmiennych
savedInstanceState.putInt(”seconds”, seconds); w obiekcie Bundle
przed usunięciem
savedInstanceState.putBoolean(”running”, running); aktywności. Są one
savedInstanceState.putBoolean(”wasRunning”, wasRunning); używane w przypadku,
gdy użytkownik zmieni
} orientację urządzenia.

public void onClickStart(View view) {


running = true;
y
} Ten kod ma zostać wykonany, kied
t.
użytkownik kliknie przycisk Star

Dalszy ciąg kodu


znajduje się na
następnej stronie.

jesteś tutaj  333


Ciąg dalszy kodu

Kod aktywności StopwatchFragment (ciąg dalszy)


public void onClickStop(View view) {
Trenazer
running = false;
Ten kod ma zostać wykonany
} po kliknięciu przycisku Stop. app/src/main

public void onClickReset(View view) { java


running = false;
Ten kod ma zostać wykonany com.hfad.trenazer
seconds = 0; po kliknięciu przycisku Kasuj.
}
Stopwatch
Fragment.java
private void runTimer(View view) {
final TextView timeView = (TextView) view.findViewById(R.id.time_view);
final Handler handler = new Handler();
Umieszczenie kodu w obiekcie Handler
handler.post(new Runnable() { oznacza, że będzie on mógł być
wykonywany w wątku działającym w tle.
@Override
public void run() {
int hours = seconds / 3600;
int minutes = (seconds % 3600) / 60;
int secs = seconds % 60;
String time = String.format(”%d:%02d:%02d”,
hours, minutes, secs);
timeView.setText(time); Wyświetlamy liczbę sekund
zmierzonych przez stoper.
if (running) {
seconds++; Jeśli stoper działa, to ta instrukc
} inkrementuje liczbę zmierzonych ja
sekund.
handler.postDelayed(this, 1000);
}
}); Kod w obiekcie Handler ma być
wykonywany co sekundę.
}
}

To już cały kod niezbędny do zapewniania prawidłowego


funkcjonowania fragmentu StopwatchFragment. Naszym kolejnym
zadaniem będzie określenie wyglądu fragmentu, a zrobimy to,
modyfikując plik układu wygenerowany przez Android Studio.

334 Rozdział 8.
Fragmenty zagnieżdżone

Układ fragmentu StopwatchFragment


Na potrzeby fragmentu StopwatchFragment użyjemy tego samego układu,
który wcześniej przygotowaliśmy dla aplikacji Stoper. A więc zastąp zawartość
pliku fragment_stopwatch.xml kodem przedstawionym poniżej:

<?xml version=”1.0” encoding=”utf-8”?>


<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
Trenazer
<TextView
android:id=”@+id/time_view” app/src/main
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” res
android:layout_alignParentTop=”true”
android:layout_centerHorizontal=”true” layout
<xml>
android:layout_marginTop=”0dp” </xml>
android:text=”” fragment_
android:textAppearance=”?android:attr/textAppearanceLarge” stopwatch.xml
android:textSize=”92sp” /> Liczba zmierzonych godzin,
minut i sekund
<Button
android:id=”@+id/start_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/time_view”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”20dp”
android:onClick=”onClickStart”
android:text=”@string/start” />
Przycisk Start
<Button
android:id=”@+id/stop_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/start_button”
Kod tego
android:layout_centerHorizontal=”true” przycisku
android:layout_marginTop=”10dp” został
Przycisk Stop przedstawiony
android:onClick=”onClickStop” na następnej
android:text=”@string/stop” /> stronie.

jesteś tutaj  335


Ciąg dalszy układu

Kod układu StopwatchFragment (ciąg dalszy)


<Button
Przycisk Kasuj
android:id=”@+id/reset_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/stop_button”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”10dp”
android:onClick=”onClickReset”
android:text=”@string/reset” />
</RelativeLayout>

Układ fragmentu StopwatchFragment używa zasobów łańcuchowych


W kodzie XML układu fragment_stopwatch.xml zostały zastosowane odwołania
do trzech zasobów łańcuchowych określających teksty wyświetlane na przyciskach
Start, Stop oraz Kasuj. Musimy zatem dodać poniższe zasoby do pliku strings.xml:
Trenazer
...
<string name=”start”>Start</string>
app/src/main
<string name=”stop”>Stop</string> To są etykiety
przycisków.
<string name=”reset”>Kasuj</string> res
...
values
Fragment StopwatchFragment wygląda tak samo jak w rozdziale 4., gdy stoper
<xml>
był wyświetlany w aktywności. Teraz różnica polega na tym, że możemy go używać </xml>
w innych aktywnościach i fragmentach. strings.xml

Stoper wygląda tak


samo jak wówczas,
gdy był wyświetlany
w ramach aktywności.
Ponieważ teraz jest on
fragmentem, możemy
go wielokrotnie używać
w różnych aktywnościach
i fragmentach.

Kolejną rzeczą, o którą musimy zadbać, będzie wyświetlanie naszego nowego


fragmentu wraz ze szczegółowymi informacjami o wybranym treningu.

336 Rozdział 8.
Fragmenty zagnieżdżone

Dodanie fragmentu stopera


do fragmentu WorkoutDetailFragment
Teraz zajmiemy się dodaniem fragmentu StopwatchFragment do układu używanego
przez fragment WorkoutDetailFragment. Po wprowadzeniu tej zmiany interfejs
użytkownika naszej aplikacji wyświetlonej na tablecie będzie wyglądał tak:

dwa fragmenty:
Aktywność MainActivity zawiera lFragment.
ent i Work outD etai
WorkoutListFragm

To jest fragment
WorkoutDetailFragment.

Fragment
WorkoutDetailFragment
będzie teraz
To jest fragment zawierał fragment
WorkoutListFragment. StopwatchFragment.

Ten fragment musimy dodać programowo


Już wiesz, że fragmenty można dodawać na dwa sposoby: w kodzie układu Jeśli jeden fragment
i w kodzie Javy. Jeśli fragment umieścimy w kodzie układu innego fragmentu,
to uzyskane efekty mogą być dziwaczne. Dlatego nasz nowy fragment umieszczamy w innym,
StopwatchFragment dodamy do fragmentu WorkoutDetailFragment,
używając kodu Javy. To oznacza, że mamy zamiar zrobić to prawie w taki sam
to musimy to zrobić
sposób, w jaki do aktywności dodaliśmy fragment WorkoutDetailFragment. programowo.
Jest tylko jedna różnica, do której niebawem dojdziemy.

jesteś tutaj  337


Dodajemy układ FrameLayout

Dodaj FrameLayout tam, gdzie ma być


Trenazer
wyświetlony fragment stopera
app/src/main
Jak już wiesz z rozdziału 7., aby dodać fragment programowo
w kodzie Javy, należy najpierw umieścić w wybranym miejscu
res
układu zagnieżdżony układ FrameLayout.

W naszym przypadku fragment StopwatchFragment chcemy layout


dodać do układu fragmentu WorkoutDetailFragment pod nazwą <xml>
</xml>
i opisem treningu. A zatem układ FrameLayout zawierający
fragment_
fragment StopwatchFragment dodamy poniżej widoków workout_detail.xml
prezentujących nazwę i opis treningu:

<?xml version=”1.0” encoding=”utf-8”?>


<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_height=”match_parent”
android:layout_width=”match_parent”
android:orientation=”vertical”>

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:textAppearance=”?android:attr/textAppearanceLarge”
android:text=””
android:id=”@+id/textTitle” />

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=””
android:id=”@+id/textDescription” />

<FrameLayout To jest układ


android:id=”@+id/stopwatch_container” FrameLayout,
w którym
android:layout_width=”match_parent” wyświetlimy
nasz fragment.
android:layout_height=”match_parent” />
</LinearLayout>

Skoro już dodaliśmy do układu zagnieżdżony układ FrameLayout, musimy


zająć się napisaniem kodu, który doda fragment StopwatchFragment.

338 Rozdział 8.
Fragmenty zagnieżdżone

A potem wyświetl fragment w kodzie Javy


Chcemy dodać fragment StopwatchFragment do układu FrameLayout
w momencie tworzenia widoku fragmentu WorkoutDetailFragment.
Zrobimy to w sposób podobny to tego, który zastosowaliśmy w rozdziale 7.,
czyli zastępując fragment wyświetlony we FrameLayout w ramach transakcji
fragmentu. Poniżej przypominamy kod, który zastosowaliśmy w rozdziale 7.: tu,
Tworzymy nową instancję fragmen
który chcemy wyświetlić.

WorkoutDetailFragment details = new WorkoutDetailFragment();


Rozpoczęcie
FragmentTransaction ft = getFragmentManager().beginTransaction(); transakcji fragmentu.

ft.replace(R.id.fragment_container, details); Zastąpienie fragmentu


i dodanie transakcji
ft.addToBackStack(null); do stosu cofnięć.
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); Stopniowe ukrycie starego
i wyświetlenie nowego
ft.commit(); Zatwierdzenie transakcji. fragmentu.

Powyższego kodu użyliśmy, by zastąpić fragment, który był wyświetlany


w aktywności, a nasza obecna sytuacja różni się od tamtej pod jednym
kluczowym względem. Otóż zamiast zastępować fragment wyświetlany
w aktywności, chcemy zastąpić fragment wyświetlany wewnątrz innego
fragmentu. To oznacza, że musimy wprowadzić niewielką zmianę w sposobie
tworzenia transakcji fragmentu.

Chcąc wyświetlić fragment w aktywności, tworzyliśmy transakcję fragmentu To wywołanie zwraca


za pomocą menedżera fragmentów aktywności: referencję do menedżera
fragmentów aktywności.
FragmentTransaction ft = getFragmentManager().beginTransaction();

Metoda getFragmentManager() pobiera menedżera fragmentów skojarzonego


z aktywnością nadrzędną rodzica fragmentu. Oznacza to, że transakcja
fragmentu jest powiązana z aktywnością.

Kiedy chcemy wyświetlić jeden fragment wewnątrz innego, to musimy użyć nieco
innego menedżera fragmentów. W tym przypadku musimy użyć menedżera
fragmentów powiązanego z fragmentem nadrzędnym. Oznacza to, że wszelkie
transakcje fragmentów będą powiązane z fragmentem nadrzędnym, a nie
z aktywnością.

Aby pobrać menedżer fragmentów skojarzony z fragmentem nadrzędnym,


musimy użyć metody getChildFragmentManager(). A zatem nowy kod To wywołanie zwraca
rozpoczynający transakcję fragmentu będzie wyglądał tak: referencję do menedżera
fragmentów fragmentu.
FragmentTransaction ft = getChildFragmentManager().beginTransaction();

A na czym w praktyce polega różnica spowodowana zastosowaniem metody


getChildFragmentManager()?
jesteś tutaj  339
Transakcje fragmentów

Metoda getFragmentManager()
tworzy transakcje na poziomie aktywności
W pierwszej kolejności sprawdźmy, co by się stało, gdyby nasz fragment
WorkoutDetailFragment utworzył transakcję wyświetlającą fragment
StopwatchFragment, używając metody getFragmentManager().

Chcemy, by po kliknięciu treningu aplikacja wyświetliła szczegółowe


informacje o nim i stoper. Aktywność MainActivity tworzy transakcję,
której zadaniem jest wyświetlenie fragmentu WorkoutDetailFragment.
Jeśli do wyświetlenia fragmentu StopwatchFragment użyjemy metody
getFragmentManager(), to na stosie cofnięć pojawią się dwie transakcje:

Wyświetl fragment szczegółów. Wyświetl fragment stopera.

W przypadku użycia metody getFragmentManager()


obie transakcje zostaną dodane do stosu cofnięć.
Uważaj na przycisk Wstecz
Problem związany z zastosowaniem dwóch transakcji do wyświetlenia
danych treningu polega na tym, że w takim przypadku po kliknięciu
przycisku Wstecz mogą się dziać dziwne rzeczy.

Kiedy użytkownik kliknie trening, a następnie naciśnie przycisk Wstecz,


to będzie oczekiwał, że ekran wróci do poprzedniego wyglądu. Jednak
naciśnięcie przycisku Wstecz wycofuje jedynie ostatnią transakcję
ze stosu. A to oznacza, że jeśli do wyświetlenia szczegółów treningu
i stopera użyjemy dwóch transakcji, to po naciśnięciu przycisku Wstecz
zniknie jedynie fragment stopera. Usunięcie szczegółowych informacji
o treningu będzie wymagało ponownego naciśnięcia przycisku.

Jednak aby wrócić do wcześniejszej postaci


aplikacji, użytkownik musi dwa razy
nacisnąć przycisk Wstecz. Jednokrotne
naciśnięcie tego przycisku spowoduje tylko
usunięcie fragmentu stopera.

Użytkownik jeden raz


klika element listy,
aby wyświetlić szczegóły
treningu i stoper.

340 Rozdział 8.
Fragmenty zagnieżdżone

Zagnieżdżone fragmenty wymagają zagnieżdżonych transakcji


Problemy związane z tworzeniem wielu transakcji w przypadku stosowania
zagnieżdżonych fragmentów były podstawowym powodem opracowania
menedżera fragmentów podrzędnych. Transakcje tworzone przy użyciu tego
menedżera istnieją wewnątrz transakcji głównych. A więc kiedy dodajemy
fragment StopwatchFragment do fragmentu WorkoutDetailFragment,
używając w tym celu transakcji utworzonej za pomocą wywołania
getChildFragmentManager().beginTransaction(), to uzyskana transakcja
będzie miała następującą postać:

Wyświetl fragment szczegółów.


Zastosowanie metody getChildFragmentManager()
do wyświetlenia fragmentu stopera oznacza,
Wyświetl fragment stopera. że tworzona w tym celu transakcja zostanie
umieszczona wewnątrz transakcji używanej
do wyświetlenia fragmentu prezentującego
szczegółowe informacje o treningu.

W takim przypadku na stosie cofnięć zostaje umieszczona tylko jedna


transakcja, która zawiera kolejną transakcję. Kiedy użytkownik naciśnie
przycisk Wstecz, transakcja wyświetl-fragment-szczegółów zostanie
wycofana, a to będzie oznaczało jednoczesne wycofanie transakcji
wyświetl-fragment-stopera. A zatem tym razem po naciśnięcia przycisku
Wstecz aplikacja zachowa się prawidłowo: Tym razem, aby wycofać obie
transakcje, wyświetlającą
fragment szczegółów
i wyświetlającą fragment stopera,
użytkownik musi nacisnąć
przycisk Wstecz tylko jeden raz.

jesteś tutaj  341


Zastąpienie fragmentu

Wyświetl fragment w metodzie onCreateView()


Trenazer
fragmentu nadrzędnego
app/src/main
Chcemy dodać fragment StopwatchFragment do układu FrameLayout
w momencie tworzenia widoku fragmentu WorkoutDetailFragment.
W momencie tworzenia tego widoku zostaje wywołana metoda onCreateView() java
fragmentu WorkoutDetailFragment, dlatego to właśnie w tej metodzie dodamy
transakcję wyświetlającą fragment StopwatchFragment. Oto zmodyfikowany com.hfad.trenazer
kod tej metody:
WorkoutDetail
@Override Fragment.java

public View onCreateView(LayoutInflater inflater, ViewGroup container,


Bundle savedInstanceState) {
if (savedInstanceState != null) {
workoutId = savedInstanceState.getLong(”workoutId”); Rozpoczynamy
transakcję.
}
FragmentTransaction ft = getChildFragmentManager().beginTransaction();
Dodajemy StopwatchFragment stopwatchFragment = new StopwatchFragment();
transakcję do ft.replace(R.id.stopwatch_container, stopwatchFragment); Zastępujemy fragment
stosu cofnięć. wyświetlony w układzie
ft.addToBackStack(null); FrameLayout.
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); Określamy
styl animacji
Zatwierdzamy ft.commit(); transakcji.
transakcję.
return inflater.inflate(R.layout.detail, container, false);
}

Jak widać, kod jest niemal identyczny z tym, którego używaliśmy do wyświetlania
fragmentów w aktywnościach. Najważniejsza różnica polega na tym, że w tym przypadku
wyświetlamy jeden fragment wewnątrz drugiego i to właśnie z tego względu zamiast
wywołania getFragmentManager() używamy wywołania getChildFragmentManager().

Kompletny kod fragmentu WorkoutDetailFragment przedstawimy na następnej stronie


i zaraz potem sprawdzimy, jak działa nasza aplikacja po wprowadzeniu tych zmian.

Nie istnieją
głupie pytania

P: Menedżer fragmentów podrzędnych obsługuje O: W takim przypadku poszczególne transakcje będą


sytuacje, w których umieszczamy jeden fragment zagnieżdżane, przez co na poziomie aktywności wciąż będzie
wewnątrz drugiego. Ale co się stanie, jeśli umieszczę jeden dostępna tylko jedna transakcja. Dzięki temu cały zbiór
fragment wewnątrz drugiego, a ten wewnątrz trzeciego, zagnieżdżonych transakcji zostanie wycofany po jednokrotnym
a ten wewnątrz czwartego… i tak dalej? naciśnięciu przycisku Wstecz.

342 Rozdział 8.
Fragmenty zagnieżdżone

Kompletny kod fragmentu WorkoutDetailFragment


Oto kompletna zawartość pliku WorkoutDetailFragment.java:
Trenazer
package com.hfad.trenazer;
import android.app.Fragment;
import android.os.Bundle; app/src/main
import android.view.LayoutInflater;
import android.view.View; Używamy klasy java
import android.view.ViewGroup; FragmentTransaction, więc
import android.widget.TextView; musimy ją zaimportować.
import android.app.FragmentTransaction; com.hfad.trenazer

public class WorkoutDetailFragment extends Fragment {


private long workoutId; WorkoutDetail
Fragment.java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (savedInstanceState != null) {
workoutId = savedInstanceState.getLong(”workoutId”);
}
Używamy FragmentTransaction ft = getChildFragmentManager().beginTransaction();
transakcji, by StopwatchFragment stopwatchFragment = new StopwatchFragment();
dodać fragment
stopera ft.replace(R.id.stopwatch_container, stopwatchFragment);
do układu ft.addToBackStack(null);
FrameLayout. ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
}

@Override
public void onStart() {
super.onStart();
View view = getView();
if (view != null) {
TextView title = (TextView) view.findViewById(R.id.textTitle);
Workout workout = Workout.workouts[(int) workoutId];
title.setText(workout.getName());
TextView description = (TextView) view.findViewById(R.id.textDescription);
description.setText(workout.getDescription());
}
} d nie musimy zmieniać.
Kodu tych meto
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putLong(”workoutId”, workoutId);
}

public void setWorkout(long id) {


this.workoutId = id;
}
}

jesteś tutaj  343


Jazda próbna

Jazda próbna aplikacji


Skoro już dodaliśmy kod służący do wyświetlania fragmentu stopera,
spróbujmy uruchomić aplikację i sprawdzić, jak działa.

Kiedy wybierzemy jeden z treningów, zostaną wyświetlone szczegółowe


informacje o nim wraz ze stoperem. Jeśli naciśniemy przycisk Wstecz,
cały ekran powróci do wcześniejszej postaci:

W przypadku naciśnięcia
przycisku Wstecz zostają
wycofane obie transakcje:
transakcja wyświetlająca
Kiedy klikniesz stoper i transakcja
trening, zostaną wyświetlająca szczegółowe
wyświetlone informacje o treningu. Dzieje
szczegółowe się tak, gdyż transakcja
informacje o nim wyświetlająca stoper została
wraz ze stoperem. zagnieżdżona wewnątrz
transakcji wyświetlającej
szczegółowe informacje
o treningu.

Ale w przypadku próby interakcji ze stoperem


pojawią się problemy
Jeśli spróbujesz nacisnąć jeden z przycisków stopera, stanie się coś
dziwnego: aplikacja ulegnie awarii.

…a aplikacja ulegnie awarii.

Kliknij jeden
z przycisków
stopera…

Zobaczmy, co poszło nie tak.

344 Rozdział 8.
Fragmenty zagnieżdżone

Dlaczego kliknięcie przycisku powoduje awarię aplikacji?


Kiedy konwertowaliśmy kod aktywności stopera do postaci fragmentu, nie zmieniliśmy żadnej
z metod obsługujących przyciski. Doskonale pamiętamy, że wcześniej, gdy stoper był aktywnością,
metody te działały idealnie. Dlaczego więc teraz kliknięcie przycisku powoduje awarię aplikacji?

Poniżej przedstawiliśmy informacje o błędzie wyświetlane przez Android Studio.


Czy na ich podstawie potrafisz powiedzieć, co było powodem awarii? Ups.

01-24 17:37:00.326 2400-2400/com.hfad.trenazer E/AndroidRuntime: FATAL EXCEPTION: main


Process: com.hfad.fraghack, PID: 2400
java.lang.IllegalStateException: Could not find a method onClickStart(View) in the activity
class com.hfad.trenazer.MainActivity for onClick handler on view class android.widget.
Button with id ‚start_button’
at android.view.View$1.onClick(View.java:3994)
at android.view.View.performClick(View.java:4756)
at android.view.View$PerformClick.run(View.java:19749)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.NoSuchMethodException: onClickStart [class android.view.View]
at java.lang.Class.getMethod(Class.java:664)
at java.lang.Class.getMethod(Class.java:643)
at android.view.View$1.onClick(View.java:3987)
at android.view.View.performClick(View.java:4756)
at android.view.View$PerformClick.run(View.java:19749)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

jesteś tutaj  345


Metoda onClick raz jeszcze

Przyjrzyjmy się kodowi układu StopwatchFragment


W kodzie układu fragmentu StopwatchFragment kojarzymy przyciski z metodami
w taki sam sposób, w jaki robiliśmy to w przypadku aktywności, a mianowicie
używając atrybutu android:onClick i podając w nim nazwę metody,
która ma obsługiwać kliknięcia danego przycisku: tem,
Teraz, kiedy stoper jest fragmen ego
używamy tego samego układu, któr cią.
<?xml version=”1.0” encoding=”utf-8”?> używ aliśm y, gdy był on akty wnoś

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
...
<Button
android:id=”@+id/start_button”
Trenazer
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
app/src/main
android:layout_below=”@+id/time_view”
android:layout_centerHorizontal=”true” res
android:layout_marginTop=”20dp”
android:onClick=”onClickStart” layout
android:text=”@string/start” /> <xml>
</xml>
<Button
fragment_
android:id=”@+id/stop_button” stopwatch.xml
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/start_button”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”10dp”
Do określenia metod, które mają
android:onClick=”onClickStop” być wywołane po kliknięciu
android:text=”@string/stop” /> przycisku, używamy atrybutów
android:onClick umieszczonych
<Button w kodzie układu.
android:id=”@+id/reset_button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/stop_button”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”10dp”
android:onClick=”onClickReset”
android:text=”@string/reset” />
</RelativeLayout>

A więc co jest źródłem problemów teraz, kiedy stoper


został zaimplementowany jako fragment?

346 Rozdział 8.
Fragmenty zagnieżdżone

Atrybut onClick wywołuje metody


aktywności, a nie fragmentu
Stosowanie atrybutu android:onClick do określania metod, które mają Zawsze, gdy widzę
być wykonywane w odpowiedzi na kliknięcie widoku, wiąże się z jednym atrybut android:onClick,
dużym problemem. Otóż atrybut ten określa metodę bieżącej aktywności. zakładam, że chodzi o mn .
Nie stanowi to żadnego problemu, jeżeli widoki są zdefiniowane To oje metody mają zostać
w układzie aktywności. Jeśli jednak widoki należą do układu fragmentu, wywołane, a nie metody
fragmentu.
to pojawiają się problemy. W takim przypadku Android zamiast metod
fragmentu będzie próbował wywoływać metody nadrzędnej aktywności.
Jeśli nie będzie w stanie znaleźć tych metod, to aplikacja ulegnie awarii.

Ten problem pojawia się niezależnie od tego, czy fragment zostanie


zagnieżdżony wewnątrz aktywności, czy wewnątrz innego fragmentu.
Dotyczy on wszystkich fragmentów.
Aktywność
Co więcej, ten problem nie dotyczy wyłącznie przycisków. Atrybut
android:onClick może być stosowany we wszystkich widokach, które
są klasami pochodnymi klasy Button, czyli na przykład pól wyboru,
przycisków opcji, przełączników oraz przycisków przełącznika.

Oczywiście moglibyśmy przenieść metody z fragmentu do aktywności,


ale takie rozwiązanie ma jedną, kluczową wadę. Oznaczałoby ono
bowiem, że fragment nie jest już samodzielną całością — gdybyśmy chcieli
użyć go w innej aktywności, musielibyśmy zaimplementować te metody
także w niej. Chcemy jednak rozwiązać ten problem, używając metod
zdefiniowanych we fragmencie.

Jak sprawić, by kliknięcie przycisku wywoływało


metodę fragmentu?
Aby klikanie przycisków umieszczonych we fragmencie powodowało
wywołanie metod zdefiniowanych w tym fragmencie, a nie w aktywności,
musimy zrobić dwie rzeczy:

1 Usunąć z układu fragmentu atrybuty android:onClick.


W przypadku zastosowania atrybutu android:onClick przyciski próbują
wywoływać metody zdefiniowane w aktywności. Dlatego musimy usunąć te
atrybuty z kodu układu.

2 Powiązać przyciski z metodami zdefiniowanymi we fragmencie,


implementując interfejs OnClickListener.
W ten sposób zapewnimy, że kliknięcie przycisku będzie powodowało
wywołanie odpowiedniej metody.

A zatem poprawmy kod naszego fragmentu StopwatchFragment.

jesteś tutaj  347


Usunięcie atrybutów onClick

Najpierw usuń atrybuty onClick z kodu


układu fragmentu
Pierwszą modyfikacją, którą wprowadzimy, będzie usunięcie z kodu
układu wierszy z atrybutami android:onClick. To sprawi, że reagując
na klikanie przycisków, Android nie będzie już próbował wywoływać
metod aktywności.

<?xml version=”1.0” encoding=”utf-8”?>


<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
...
<Button
android:id=”@+id/start_button” Trenazer
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
app/src/main
android:layout_below=”@+id/time_view”
android:layout_centerHorizontal=”true”
res
android:layout_marginTop=”20dp”
android:onClick=”onClickStart” layout
android:text=”@string/start” /> <xml>
<Button </xml>

android:id=”@+id/stop_button” fragment_
stopwatch.xml
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/start_button”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”10dp” Z układu fragmentu
stopera, ze
android:onClick=”onClickStop” wszystkich
android:text=”@string/stop” /> przycisków,
usuń wiersz
<Button zawierający atrybut
android:id=”@+id/reset_button” andr oid:onClick.

android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/stop_button”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”10dp”
android:onClick=”onClickReset”
android:text=”@string/reset” />
</RelativeLayout>

Kolejnym zadaniem będzie zmodyfikowanie fragmentu w taki sposób,


by odpowiadał na kliknięcia przycisków.

348 Rozdział 8.
Fragmenty zagnieżdżone

Zaimplementuj we fragmencie interfejs OnClickListener


Aby klikanie przycisków powodowało wywoływanie metod zdefiniowanych we
fragmencie StopwatchFragment, musimy zaimplementować we fragmencie interfejs To zmienia fragment
w obiekt mogący
View.OnClickListener w sposób przedstawiony w poniższym przykładzie: obsługiwać kliknięcia.

public class StopwatchFragment extends Fragment implements View.OnClickListener {


...
}

W ten sposób zmieniamy nasz fragment w obiekt typu View.OnClickListener, dzięki


czemu będzie on mógł odpowiadać na kliknięcia widoków.

Aby określić, jak fragment ma reagować na kliknięcia, musimy zaimplementować w nim


metodę onClick() interfejsu View.OnClickListener. Metoda ta jest wywoływana
w reakcji na kliknięcie każdego widoku używanego we fragmencie.

@Override
public void onClick(View v) {
W kodzie naszego fragmentu musimy
... przesłonić metodę onClick().
}

Metoda onClick() ma jeden parametr typu View. Reprezentuje on widok kliknięty


przez użytkownika. Możemy użyć metody getId() klasy View, by się zorientować,
który widok został kliknięty, i na tej podstawie określić, jak należy zareagować.

Magnesiki z kodem @Override

Przekonajmy się, czy potrafisz uzupełnić kod public void onClick(View v) {


metody onClick() fragmentu StopwatchFragment. switch (........ . ..................) {
W przypadku kliknięcia przycisku Start musi
zostać wywołana metoda onClickStart(), case R.id.start_button:
w przypadku kliknięcia przycisku Stop — metoda onClickStart(............);
onClickStop(), a w przypadku kliknięcia przycisku
Kasuj — metoda onClickReset(). break;
case R.id.stop_button:
getName()
getId() .................(.............);
onClickStop
break;
onClickReset v
case R.id.reset_button:
v
.................(.............);
true
true
}
true v v
}

jesteś tutaj  349


Rozwiązanie magnesików

Magnesiki z kodem. Rozwiązanie @Override

Przekonajmy się, czy potrafisz uzupełnić public void onClick(View v) {


kod metody onClick() fragmentu switch (.. v . . getId() .......) {
StopwatchFragment. W przypadku kliknięcia
przycisku Start musi zostać wywołana metoda case R.id.start_button:
onClickStart(), w przypadku kliknięcia onClickStart(...... v ..);
przycisku Stop — metoda onClickStop(),
a w przypadku kliknięcia przycisku Kasuj break;
— metoda onClickReset(). case R.id.stop_button:
getName()
onClickStop (...... v ...);
true true break;

Te magnesiki okazały
case R.id.reset_button:
true
się niepotrzebne.
onClickReset (...... v ...);
}
}

Metoda onClick fragmentu StopwatchFragment


Poniżej przedstawiliśmy kod stanowiący implementację metody onClick()
fragmentu StopwatchFragment; dzięki niemu po kliknięciu każdego
z przycisków zostanie wywołana odpowiednia metoda:

To jest widok kliknięty przez użytkownika.


@Override
public void onClick(View v) {
switch (v.getId()) { Sprawdzamy, który widok został kliknięty.

case R.id.start_button:
onClickStart(v); Jeśli użytkownik kliknął przycisk
Start, to wywołujemy metodę
break; onClickStart().
case R.id.stop_button: Jeśli użytkownik kliknął przycisk
Stop, to wywołujemy metodę
onClickStop(v); onClickStop().
break;
case R.id.reset_button: Jeśli użytkownik kliknął przycisk
Kasuj, to wywołujemy metodę
onClickReset(v); onClickReset().
}
}

Pozostaje nam jeszcze do zrobienia tylko jedna rzecz: musimy powiązać


obiekt nasłuchujący z przyciskami wyświetlanymi we fragmencie.

350 Rozdział 8.
Fragmenty zagnieżdżone

Powiązanie obiektu nasłuchującego


OnClickListener z przyciskami
Aby widok mógł odpowiadać na kliknięcia, musimy wywołać jego metodę
setOnClickListener(). Metoda ta ma jeden parametr — obiekt typu OnClickListener.
Ponieważ nasz fragment StopwatchFragment implementuje interfejs OnClickListener,
w wywołaniu metody setOnClickListener() możemy przekazać this.

Na przykład w ten oto sposób możemy skojarzyć obiekt nasłuchujący z przyciskiem Start:
Pobieramy referencję
do przycisku.
Button startButton = (Button) layout.findViewById(R.id.start_button);
startButton.setOnClickListener(this);
Dołączamy do przycisku
obiekt nasłuchujący.
Metodę setOnClickListener() poszczególnych przycisków należy wywoływać
dopiero po utworzeniu widoków fragmentu. Oznacza to, że operację dołączenia
obiektu nasłuchującego do przycisków musimy wykonać w metodzie onCreateView()
fragmentu StopwatchFragment:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.stopwatch, container, false);
runTimer(layout);
Button startButton = (Button)layout.findViewById(R.id.start_button);
startButton.setOnClickListener(this);
Button stopButton = (Button)layout.findViewById(R.id.stop_button);
stopButton.setOnClickListener(this);
Button resetButton = (Button)layout.findViewById(R.id.reset_button);
resetButton.setOnClickListener(this);
return layout;
} Te wywołania dołączają
do poszczególnych prz obiekt nasłuchujący
ycisków. Trenazer

app/src/main
Kompletny kod aktywności StopwatchFragment przedstawiliśmy
na następnej stronie.
java

com.hfad.trenazer

Stopwatch
Fragment.java

jesteś tutaj  351


Kod fragmentu StopwatchFragment

Kod fragmentu StopwatchFragment


Oto kompletny kod fragmentu StopwatchFragment, zapisany w pliku
StopwatchFragment.java:

package com.hfad.trenazer;
Używamy klasy Button, więc
... musimy ją zaimportować.
Fragment musi implementować
interfejs View.OnClickListener.
import android.widget.Button;

public class StopwatchFragment extends Fragment implements View.OnClickListener {


// Liczba sekund wyświetlana przez stoper
private int seconds = 0;
// Czy stoper działa?
Trenazer
private boolean running;
private boolean wasRunning;
Kodu metody onCreate() nie mus app/src/main
imy zmieniać.
@Override
java
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
com.hfad.trenazer
if (savedInstanceState != null) {
seconds = savedInstanceState.getInt(”seconds”);
running = savedInstanceState.getBoolean(”running”); Stopwatch
Fragment.java
wasRunning = savedInstanceState.getBoolean(”wasRunning”);
if (wasRunning) {
running = true;
}
(),
} Zaktualizuj metodę onCreateViewcisku
tak by dołą czał a do każd ego przy
}
obiekt nasłuchujący.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.stopwatch, container, false);
runTimer(layout);
Button startButton = (Button) layout.findViewById(R.id.start_button);
startButton.setOnClickListener(this);
Button stopButton = (Button) layout.findViewById(R.id.stop_button);
stopButton.setOnClickListener(this);
Button resetButton = (Button) layout.findViewById(R.id.reset_button);
resetButton.setOnClickListener(this);
return layout;
}

352 Rozdział 8.
Fragmenty zagnieżdżone

Kod fragmentu StopwatchFragment (ciąg dalszy)


@Override
Ponieważ implementujemy
public void onClick(View v) { interfejs OnClickListener,
switch (v.getId()) { musimy przesłonić jego
metodę onClick().
case R.id.start_button:
onClickStart(v);
break;
case R.id.stop_button: W zależności od tego, który
przycisk został kliknięty,
onClickStop(v); wywołujemy odpowiednią
break; metodę fragmentu.
case R.id.reset_button:
onClickReset(v);
break; Trenazer
}
} app/src/main

... java

com.hfad.trenazer
public void onClickStart(View view) {
running = true;
} Stopwatch
Fragment.java
To są dokładnie te
same metody co
public void onClickStop(View view) { wcześniej. Będą
running = false; one wywoływane
w celu obsługi kliknięć
} przycisków fragmentu.

public void onClickReset(View view) {


running = false;
seconds = 0;
}

...
}

A teraz zobaczmy, co się stanie po uruchomieniu aplikacji.

jesteś tutaj  353


Jazda próbna

Jazda próbna aplikacji


Teraz, kiedy uruchomisz aplikację, będzie ona działać prawidłowo.

er
Kiedy klikniesz przycisk Start, stopTakże
.
zacznie mierzyć upływający czas da
j
klikanie przycisków Stop i Kasu .
prawidłowe, oczekiwane rezultaty

Ale po obróceniu urządzenia pojawia się problem


Jeśli uruchomimy stoper, a następnie obrócimy urządzenie, stanie się coś dziwnego
— stoper zostanie wyzerowany:

Uruchom stoper, a potem


obróć urządzenie.

Kiedy obrócisz
urządzenie, stoper
zostanie wyzerowany.
Co się z nim dzieje?

Wyjaśniliśmy już wcześniej, że zmiana orientacji ekranu powoduje


odtworzenie widoków. A więc co się dzieje z fragmentami po zmianie
orientacji ekranu?

354 Rozdział 8.
Fragmenty zagnieżdżone

Obrócenie urządzenia powoduje ponowne


utworzenie aktywności
Jak już wiesz, kiedy uruchomisz aplikację i obrócisz urządzenie, to działająca
aktywność zostanie usunięta i ponownie utworzona. Wszystkie zmienne używane
w kodzie tej aktywności zostaną zatem przywrócone do ich wartości domyślnych.
Chcąc zapisać wartości tych zmiennych przed usunięciem aktywności, musimy to
zrobić w jej metodzie onSaveInstanceState().

A co w przypadku, gdy aktywność zawiera fragment? Jak już się przekonałeś,


cykle życia aktywności i fragmentu są ze sobą ściśle powiązane. Wciąż jednak
musimy się dowiedzieć, co się dzieje z fragmentem po obróceniu urządzenia.

Co się dzieje z fragmentem po obróceniu urządzenia?


1 Aktywność zawiera fragment.

Aktywność Fragment

2 Kiedy użytkownik obraca urządzenie, aktywność wraz


z fragmentem zostaje usunięta.

jesteś tutaj  355


Co się dzieje?

Historii ciąg dalszy


3 Aktywność zostaje utworzona ponownie, w efekcie wywoływana
jest jej metoda onCreate().
Metoda onCreate() zawiera wywołanie metody setContentView().

onCreate()

Aktywność

4 Wywołanie metody setContentView() powoduje wczytanie układu


aktywności i odtworzenie transakcji fragmentu.
Fragment jest ponownie tworzony, a wraz z nim zostaje odtworzona jego
ostatnia transakcja.

Jak widzę, pojawiły się


jakieś transakcje fragmentu.
Muszę je natychmiast
zastosować.
Transakcje
fragmentu

Aktywność Fragment

Po obróceniu urządzenia fragment powinien znaleźć się w takim


samym stanie, w jakim był przed zmianą orientacji urządzenia.
A więc dlaczego w naszej aplikacji fragment stopera został
wyzerowany? By uzyskać wskazówki dotyczące tego problemu,
przyjrzyjmy się metodzie onCreateView() fragmentu
WorkoutDetailFragment.

356 Rozdział 8.
Fragmenty zagnieżdżone

Metoda onCreateView() jest wykonywana


PO odtworzeniu transakcji
Metoda onCreateView() jest wykonywana po tym, jak aktywność odtworzy wszystkie
transakcje fragmentów. Poniżej zamieściliśmy kod tej metody. Czy na jego podstawie
potrafisz powiedzieć, dlaczego obrócenie urządzenia powoduje wyzerowanie stopera?

Metoda onCreateView() fragmentu jest wykonywana po tym,


jak aktywność odtworzy transakcje wszystkich używanych
... w niej fragmentów.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Ta instrukcja zostanie
if (savedInstanceState != null) { wykonana, jeśli fragment
WorkoutDetailFragment
workoutId = savedInstanceState.getLong(”workoutId”); zapisał swój stan, zanim
został usunięty.
}
FragmentTransaction ft = getChildFragmentManager().beginTransaction();
Ta grupa StopwatchFragment stopwatchFragment = new StopwatchFragment();
instrukcji
zastępuje ft.replace(R.id.stopwatch_container, stopwatchFragment);
fragment
stopera ft.addToBackStack(null);
jego nową ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
instancją.
ft.commit();
return inflater.inflate(R.layout.fragment_workout_detail, container, false);
}
...

Trenazer

Metoda onCreateView() zawiera transakcję fragmentu, która zastępuje fragment stopera


app/src/main
jego nową instancją. A to oznacza, że zachodzą dwa zdarzenia:

1 Aktywność odtwarza transakcje fragmentów, co powoduje przywrócenie fragmentu java


stopera do stanu, w jakim się znajdował przed obróceniem urządzenia.
com.hfad.trenazer
2 W wywołaniu metody onCreateView() pozbywamy się dotychczasowego fragmentu
stopera odtworzonego po obróceniu urządzenia i zastępujemy go zupełnie nowym
WorkoutDetail
fragmentem. Ponieważ jest to zupełnie nowy fragment, początkowo jest on Fragment.java
wyzerowany.

Aby uchronić się przed tym niepożądanym działaniem aplikacji, musimy zadbać o to,
by fragment był zastępowany wyłącznie, jeśli parametr saveInstanceState typu Bundle
ma wartość null. W efekcie całkowicie nowa wersja fragmentu stopera będzie wyświetlana
tylko w momencie, gdy aktywność zostanie utworzona po raz pierwszy.

jesteś tutaj  357


Kod fragmentu WorkoutDetailFragment

Kod fragmentu WorkoutDetailFragment Trenazer

Oto kompletny kod zapisany w pliku WorkoutDetailFragment.java:


app/src/main
package com.hfad.trenazer;
java
...
com.hfad.trenazer
public class WorkoutDetailFragment extends Fragment {
private long workoutId;
WorkoutDetail
@Override Fragment.java
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Jedyną zmianą, if (savedInstanceState != null) {
którą musimy workoutId = savedInstanceState.getLong(”workoutId”);
wprowadzić, jest } else {
umieszczenie FragmentTransaction ft = getChildFragmentManager().beginTransaction();
transakcji fragmentu
wewnątrz instrukcji StopwatchFragment stopwatchFragment = new StopwatchFragment();
else. Transakcja ft.replace(R.id.stopwatch_container, stopwatchFragment);
zostanie wykonana ft.addToBackStack(null);
wyłącznie ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
w przypadku,
gdy paramet r ft.commit();
savedInstanceState }
przyjmie wartość return inflater.inflate(R.layout.fragment_workout_detail, container, false);
null. }

@Override
public void onStart() {
super.onStart();
View view = getView();
if (view != null) {
TextView title = (TextView) view.findViewById(R.id.textTitle);
Workout workout = Workout.workouts[(int) workoutId];
title.setText(workout.getName());
TextView description = (TextView) view.findViewById(R.id.textDescription);
description.setText(workout.getDescription());
}
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putLong(”workoutId”, workoutId);
}

public void setWorkout(long id) {


this.workoutId = id;
}
}
Sprawdźmy teraz, co się stanie po uruchomieniu aplikacji.

358 Rozdział 8.
Fragmenty zagnieżdżone

Jazda próbna aplikacji


Uruchom aplikację i włącz stoper, a następie obróć urządzenie.
Przyjrzyj się, co się stanie ze stoperem.

Włącz stoper i obróć


urządzenie.
Kiedy obrócisz urządzenie,
stoper dalej będzie mierzył
czas. Jego wcześniejszy
stan zostanie prawidłowo
odtworzony.

Nawet po obróceniu urządzenia stoper działa prawidłowo. Choć zmiana orientacji


urządzenia oznacza usunięcie aktywności, to jednak transakcje fragmentu zostają
prawidłowo odtworzone. Nie zastępujemy już fragmentu StopwatchFragment jego
nową instancją.

Nie istnieją
głupie pytania

P: Czy jeśli użyję atrybutu android:onClick w kodzie P: Czy należy używać fragmentów we własnych
układu fragmentu, to Android faktycznie spróbuje aplikacjach?
wykonać metodę zdefiniowaną w aktywności?
O: To zależy od aplikacji i od tego, co chcesz uzyskać. Jedną
O: Tak, spróbuje. Dlatego zamiast określać metody z głównych zalet fragmentów jest to, że można ich używać,
obsługujące kliknięcia widoków fragmentu przy użyciu atrybutu by zapewnić działanie aplikacji na szerokiej gamie urządzeń,
android:onClick, należy zaimplementować interfejs z ekranami o różnych wielkościach. Na przykład możesz
OnClickListener. zdecydować, by na tabletach fragmenty były wyświetlane jeden
obok drugiego, a na urządzeniach z mniejszymi ekranami
P : Czy to dotyczy zagnieżdżonych fragmentów, w osobnych aktywnościach. W kilku następnych rozdziałach
czy w ogóle wszystkich fragmentów? poznasz także inne, bardzo przydatne sposoby używania
O: To dotyczy wszystkich fragmentów, niezależnie od tego, fragmentów.
czy są one zagnieżdżone w innych, czy nie.
jesteś tutaj  359
Ćwiczenie

Wczuj się we fragment


Poniżej przedstawiliśmy kod dwóch układów
używanych we fragmentach, a na następnej
stronie dwie implementacje
fragmentów napisane w Javie.
Twoim zadaniem jest wcielić
się w rolę fragmentu i wskazać,
która kombinacja tych kodów
spowoduje wyświetlenie
komunikatu, kiedy przełącznik w układzie
zostanie włączony.

A <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”com.hfad.ch10ex.SwitchFragment”>

<Switch
android:id=”@+id/switch_view”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>

To są dwa układy fragmentów.

B <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”com.hfad.ch10ex.SwitchFragment”>
<Switch
android:id=”@+id/switch_view”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:onClick=”onClick” />
</LinearLayout>

360 Rozdział 8.
Fragmenty zagnieżdżone

C public class SwitchFragment extends Fragment implements View.OnClickListener{

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_switch, container, false);
}

@Override
public void onClick(View v) {
if (v.getId() == R.id.switch_view) {
if (((Switch) v).isChecked()) {
Toast.makeText(v.getContext(), ”Włączony”, Toast.LENGTH_SHORT).show();
}
}
}
}
To są dwie implementacje
fragmentów napisane w Javie.

D public class SwitchFragment extends Fragment implements View.OnClickListener{

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_switch, container, false);
Switch switchView = (Switch) layout.findViewById(R.id.switch_view);
switchView.setOnClickListener(this);
return layout;
}

@Override
public void onClick(View v) {
if (v.getId() == R.id.switch_view) {
if (((Switch) v).isChecked()) {
Toast.makeText(v.getContext(), ”Włączony”, Toast.LENGTH_SHORT).show();
}
}
}
}
jesteś tutaj  361
Rozwiązanie

Wczuj się we fragment. Rozwiązanie


Poniżej przedstawiliśmy kod aktywności.
Chcesz, aby jeden układ był używany na
urządzeniach z dużymi ekranami,
a drugi na urządzeniach
z ekranami normalnej wielkości.
Która z przedstawionych
struktur katalogów zapewni
takie działanie aplikacji?

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
A
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”com.hfad.ch10ex.SwitchFragment”> To jest prawidłowy kod układu.

<Switch
android:id=”@+id/switch_view”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>

B <LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
tools:context=”com.hfad.ch10ex.SwitchFragment”>
<Switch
android:id=”@+id/switch_view” Element Switch umieszczony w kodzie
układu zawiera atrybut android:onClick.
android:layout_width=”wrap_content” Jego użycie spowoduje wywołanie kodu
zdefiniowanego w aktywności, a nie we
android:layout_height=”wrap_content” fragmencie.
android:onClick=”onClick” />
</LinearLayout>

362 Rozdział 8.
Fragmenty zagnieżdżone

C public class SwitchFragment extends Fragment implements View.OnClickListener{

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_switch, container, false);
}
Ten kod implementuje interfejs View.OnClickListener,
@Override ale nie dołącza do przycisku przełącznika żadnego
obiektu nasłuchującego. Metoda onClick() nigdy nie
public void onClick(View v) { zostanie wywołana.
if (v.getId() == R.id.switch_view) {
if (((Switch) v).isChecked()) {
Toast.makeText(v.getContext(), ”Włączony”, Toast.LENGTH_SHORT).show();
}
}
}
}

D public class SwitchFragment extends Fragment implements View.OnClickListener{

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_switch, container, false);
Switch switchView = (Switch) layout.findViewById(R.id.switch_view);
switchView.setOnClickListener(this);
return layout;
} To jest prawidłowa implementacja fragmentu
napisana w Javie. Po kliknięciu przycisku
przełącznika zostanie wywołana metoda onClick().
@Override
public void onClick(View v) {
if (v.getId() == R.id.switch_view) {
if (((Switch) v).isChecked()) {
Toast.makeText(v.getContext(), ”Włączony”, Toast.LENGTH_SHORT).show();
}
}
}
}
jesteś tutaj  363
Przybornik

Twój przybornik do Androida Pełny kod przykładowe


aplikacji prezen tow ane
j
j
Rozdział 8.

ale mo żes z
w tym rozdzi
Opanowałeś już rozdział 8. i dodałeś pobrać z ser we ra FT P
do swojego przybornika z narzędziami wydawnictwa Helion:
klady/
umiejętność tworzenia i stosowania ftp://ftp.helion.pl/przy
zagnieżdżonych fragmentów. andrrg.zip

CELNE SPOSTRZEŻENIA
 Fragmenty mogą zawierać inne fragmenty.
 Jeśli zagnieżdżamy fragment wewnątrz innego fragmentu, to musimy go dodać
programowo, pisząc odpowiedni kod Javy.
 Gdy wykonujesz transakcje na zagnieżdżonym fragmencie, tworząc je należy skorzystać
z metody getChildFragmentManager().
 W przypadku zastosowania we fragmencie atrybutu android:onClick Android będzie
szukał metody o podanej nazwie w aktywności, w której fragment jest używany.
 Zamiast używać we fragmencie atrybutu android:onClick, lepiej będzie
zaimplementować w klasie fragmentu interfejs View.OnClickListener i definiowaną
przez niego metodę onClick().
 W przypadku zmiany konfiguracji urządzenia zarówno aktywność, jak i używane
w niej fragmenty są usuwane. Kiedy aktywność jest ponownie tworzona, transakcje jej
fragmentów zostają odtworzone podczas wykonywania metody setContentView()
wywoływanej w metodzie onCreate() aktywności.
 Metoda onCreateView() fragmentu jest wykonywana po tym, jak aktywność
odtworzy transakcje fragmentów.

364 Rozdział 8.
9. Paski akcji

Na skróty

Bylibyśmy tu już
Widzisz, kilka godzin temu, gdyby
Kasztanko, mówiłam, wiedziała o przycisku
że kiedyś dojedziemy Strona główna. Iiiiha!
do domu.

Każdy lubi chodzić na skróty.


Z tego rozdziału dowiesz się, jak korzystając z pasków akcji, wzbogacić aplikację o możliwość
chodzenia na skróty. Pokażemy Ci, jak uruchamiać inne aplikacje za pomocą elementów akcji
dodawanych do pasków akcji, jak udostępniać treści innym aplikacjom, używając dostawcy
akcji współdzielenia, oraz jak poruszać się w górę hierarchii aplikacji za pomocą przycisku W górę
umieszczonego na pasku akcji. Dowiesz się też, jak można nadawać tworzonym aplikacjom spójny
wygląd i sposób działania, korzystając z motywów, i poznasz pakiet biblioteki wsparcia systemu
Android.

to jest nowy rozdział  365


Struktura aplikacji

Świetne aplikacje mają przejrzystą strukturę


W rozdziale 6. pokazaliśmy, jak określać strukturę aplikacji, by zapewnić
użytkownikowi maksymalne doznania. Pamiętaj, że tworząc aplikację,
będziesz korzystać z trzech rodzajów ekranów, które opisaliśmy poniżej.

Ekrany głównego poziomu Włoskie


To zazwyczaj pierwsza aktywność aplikacji, Co Nieco
To jest orientacyjny szkic aplikacji
która jest wyświetlana zaraz po jej uruchomieniu. o.
Pizze dla restauracji Włoskie Co Niec
macje
Aplikacja będzie zawierała infor ma
Makarony o daniach i lokalach. Oprócz tego e
Restauracje umożliwiać użytkownikom składani
zamówień.
Złóż zamówienie

Ekrany kategorii Pizze Makarony Restauracje


Ekrany tego typu
przedstawiają dane należące
do określonej kategorii,
często używając do tego
celu list. Zapewniają także
użytkownikom możliwość
przejścia do ekranów
szczegółów/edycji.

Ekrany Diavolo Spaghetti Wrocław Złóż


szczegółów/edycji bolognese zamówienie

Te ekrany wyświetlają
szczegółowe informacje
o wybranym rekordzie,
umożliwiają użytkownikom
edycję wybranego rekordu
lub dodanie nowego.

366 Rozdział 9.
Paski akcji

Różne typy nawigacji


Na ekranie najwyższego poziomu naszej aplikacji dla Włoskiego Co Nieco znajduje
się lista opcji zawierająca te miejsca aplikacji, do których może przejść użytkownik.

Włoskie Co Nieco To jest ekran głównego poziomu


aplikacji.

Pizze

Makarony To są odnośniki do ekranów kate


gorii.

Restauracje

Złóż zamówienie ekranu


Ta opcja przenosi użytkownika do on
szczegółów/edycji, na którym może
złożyć zamówienie.

Pierwsze trzy opcje stanowią połączenia z aktywnościami kategorii: pierwsza


powoduje wyświetlenie listy pizz, druga — listy dań z makaronu, a trzecia
— listy lokali. Te aktywności kategorii można sobie wyobrażać jako pasywne.
Ich działanie polega na wyświetlaniu informacji i zapewnianiu użytkownikowi
pomocy w poruszaniu się po aplikacji.
Czwarta opcja stanowi połączenie z aktywnością szczegółów/edycji,
która pozwala użytkownikowi złożyć zamówienie. Jest to opcja aktywna
— pozwala bowiem użytkownikowi coś utworzyć.
Ogólnie rzecz biorąc, pasywne i aktywne opcje nawigacyjne są obsługiwane
w nieco inny sposób. W tym rozdziale zajmiemy się obsługą aktywnych opcji
nawigacyjnych.

Stosowanie akcji do celów nawigacyjnych


W aplikacjach na Androida aktywne opcje nawigacyjne są przeważnie
dodawane do pasków akcji. Pasek akcji to element wyświetlany zazwyczaj
na samej górze aktywności. Stanowi on miejsce, w którym są prezentowane
To jest pasek akcji.
najczęściej używane akcje, dlatego zwykle zawiera przyciski opisywane
za pomocą czasowników, takich jak Utwórz, Szukaj bądź Edytuj.
W naszej aplikacji dla restauracji Włoskie Co Nieco możemy ułatwić
użytkownikom składanie zamówień z dowolnego miejsca w aplikacji
— musimy w tym celu wyświetlić pasek akcji u góry każdej aktywności.
Ten pasek będzie zawierał przycisk Złóż zamówienie, dzięki czemu
użytkownik zawsze będzie miał do niego dostęp.
To jest przycisk Złóż zamówienie.
Przyjrzyjmy się teraz nieco dokładniej, w jaki sposób można dodać
do aplikacji pasek akcji.
jesteś tutaj  367
Paski narzędzi i motywy

Zacznijmy od paska akcji


Pasek akcji ma kilka zastosowań:

 Służy do wyświetlania nazwy aplikacji lub aktywności, tak by użytkownik wiedział,


w którym miejscu aplikacji aktualnie się znajduje. Na przykład aplikacja do obsługi
poczty elektronicznej może używać paska akcji do wyświetlania, czy użytkownik
znajduje się w skrzynce odbiorczej, czy w koszu ze spamem.
 Umożliwia umieszczanie kluczowych czynności w widocznym i przewidywalnym
miejscu — chodzi o takie operacje jak udostępnianie lub wyszukiwanie treści.
 Służy jako element nawigacyjny pozwalający przechodzić do innych aktywności
w celu wykonywania jakichś akcji.

Aby dodać do aplikacji pasek akcji, musimy wybrać motyw (ang. theme), który taki pasek
zawiera. Motyw to styl, który odnosi się do całej aktywności lub aplikacji, dzięki czemu będzie
ona miała spójny wygląd i sposób obsługi. Motywy określają takie aspekty wyglądu aplikacji
jak kolor tła aktywności i paska akcji czy też postać wyświetlanych tekstów.

API poziomu 11 lub wyższego


Motywy
Jeśli chcemy, aby aplikacja działała w wersjach Androida obsługujących API co najmniej Android ma grupę .
poziomu 11, to w celu dodania paska akcji musimy zastosować klasę Theme.Holo lub wbudowanych motywów
Ich peł ną list ę mo żna
jedną z jej klas pochodnych. Właśnie w taki sposób stosuje się paski akcji w przeważającej i
znaleźć w dokumentacj
większości przypadków. W przypadku API poziomu 21 lub wyższego dysponujemy R.style, na stronie .
dodatkową możliwością zastosowania motywów Theme.Material. Istnieje kilka różnych http://developer.android
/
com/reference/android
motywów, które można wybrać zależnie od wyglądu, jaki chcemy nadać tworzonej aplikacji. R.style.html.
Na przykład zastosowanie motywu Theme.Material.Light.DarkActionBar sprawi,
że aktywności będą miały jasne tło, a pasek akcji — ciemne.

Motyw Motyw
Theme. Theme.
Material. Holo.
Light Light.

To są przykłady dwóch różnych motywów.


API poziomu 7 lub wyższego
Jeśli chcemy, by aplikacja działała na starszych urządzeniach, z systemem obsługującym
To rozwiązanie trzeba stosować
API poziomu co najmniej 7, to wciąż możemy korzystać z paska akcji, lecz musimy to tylko w tych przypadkach, gdy
robić nieco inaczej. W pierwszej kolejności trzeba zmodyfikować aktywności, tak by chcemy, by aplikacja działała na
dziedziczyły po klasie android.support.v7.app.ActionBarActivity, a nie po klasie starszych urządzeniach z systemem
obsługującym API poziomu 7, 8,
android.app.Activity. Dopiero potem możemy zastosować motywy Theme.AppCompat. 9 i 10. Przeważająca większość
aktualnie używanych urządzeń jest
Klasa ActionBarActivity i motywy AppCompat wchodzą w skład zestawu bibliotek wyposażona w wersje Androida
obsługujące API wyższego poziomu.
Support Libraries dla systemu Android. Przyjrzyjmy się im zatem nieco dokładniej.

368 Rozdział 9.
Paski akcji

Pakiet bibliotek Support Libraries


Wraz z upływem lat do systemu Android dodawane są coraz to nowe możliwości. Co jednak
zrobić, jeśli chcielibyśmy skorzystać z najnowszych widżetów na urządzeniu, które ma już
dwa lub trzy lata? Pakiet Support Libraries, czyli biblioteki wsparcia systemu Android, to
pakiet bibliotek z kodem, które można dołączać do własnych projektów. Ich podstawowym
przeznaczeniem jest zapewnienie zgodności wstecznej, gdyż pozwalają na stosowanie
możliwości nowszych wersji Androida na starszych urządzeniach.

Niektóre możliwości systemu są udostępniane wyłącznie przez te biblioteki, dlatego


jeśli zechcemy z tych możliwości skorzystać, to będziemy musieli użyć bibliotek wsparcia.
Na przykład API klasy DrawerLayout pozwala stworzyć szufladę nawigacyjną
(ang. navigation drawer), którą można wysunąć spoza krawędzi ekranu; te API
są obecnie dostępne wyłącznie w bibliotece wsparcia v4.

Pakiet Support Libraries zawiera kilka takich bibliotek. Każda z nich jest przeznaczona dla
konkretnego, bazowego poziomu API i implementuje ściśle określony zestaw możliwości.
Nazwa każdej z tych bibliotek wskazuje najniższą wersję systemu Android, w której danej
biblioteki można używać. Na przykład biblioteka wsparcia v4 może być używana z API
poziomu 4 lub wyższym, a biblioteka wsparcia v7 — z API poziomu 7 lub wyższym.
Każda z tych bibliotek jest dostępna w wielu wersjach, uzupełnianych stopniowo o nowe
możliwości systemu i wprowadzane do niego poprawki.

Klasy należące do tych bibliotek są umieszczane w pakietach o nazwie android.support.v*.


Na przykład klasy biblioteki wsparcia v4 należą do pakietu android.support.v4.

Poniższy rysunek przedstawia wybrane biblioteki pakietu Support Libraries.

Biblioteka wsparcia v4
Biblioteka gridlayout v7
Obejmuje największy zestaw możliwości,
Zapewnia wsparcie dla klasy
takich jak obsługa komponentów aplikacji
GridLayout.
i różnych możliwości interfejsu użytkownika.

Biblioteka appcompat v7
Zawiera wsparcie dla pasków akcji Bibolioteka recycleview v7
w systemach obsługujących API s Zapewnia wsparcie dla widżetu
poziomu 7 i wyższego oraz dla tworzenia arie RecyclerView.
ibr
i stosowania Material Design.
rtl
po
Biblioteka cardview v7 Sup Biblioteka leanback v17
Dodaje wsparcie dla widżetu Zawiera API umożliwiające
To są tylko wybrane
CardView, pozwalającego spośród wszystkich tworzenie interfejsów użytkownika
prezentować informacje na kartach. bibliotek wsparcia. aplikacji przeznaczonych do
działania na telewizorach.
Android Studio bardzo często domyślnie dodaje biblioteki z pakietu Support
Libraries do tworzonych projektów. Aby się o tym przekonać, utwórzmy nowy
projekt, który będzie stanowił prototyp aplikacji dla restauracji Włoskie Co Nieco,
i zobaczmy, czy faktycznie pojawią się w nim odwołania do tych bibliotek.
jesteś tutaj  369
Biblioteki wsparcia

Twój projekt może już używać bibliotek wsparcia


Mamy zamiar napisać prototyp aplikacji dla restauracji Włoskie Co Nieco, która
będzie korzystała z API poziomu 17 i wyższego. Utwórz zatem nowy projekt
aplikacji, nadaj jej nazwę Włoskie Co Nieco, a jej kod umieść w pakiecie
com.hfad.wloskieconieco. Tworząc aplikację, jako minimalną wersję SDK wybierz
API poziomu 17. Aktywności głównej nadaj nazwę MainActivity, układ, z którego
ona korzysta, nazwij activity_main, a zasób menu — menu_main.

Chcemy się teraz przekonać, czy ten nowy projekt będzie domyślnie korzystał
z jakichś bibliotek wsparcia. W pierwszej kolejności zajrzyj do pliku MainActivity.java.
Poniżej przedstawiliśmy fragmenty kodu wygenerowanego przez Android Studio.
Domyślnie klasa MainActivity dziedziczy po klasie android.support.v7.app.
ActionBarActivity. Innymi słowy: używa biblioteki wsparcia v7.
package com.hfad.wloskieconieco; Fragment android.support.v7 w instrukcji
importu klasy ActionBarActivity informuje,
że pochodzi ona z biblioteki appcompat v7.
import android.support.v7.app.ActionBarActivity;
...
public class MainActivity extends ActionBarActivity { u MainActivity.java
Kod zapisany w Twoim plik
... e wyg ląda ć niec o inac zej. Jego postać
moż
działania używanego
} może bowiem zależeć od o środowiska
przez Ciebie zint egr owa neg
Klasa ActionBarActivity jest używana w połączeniu z motywami Theme. programistycznego.
AppCompat, by zapewnić możliwość stosowania pasków akcji w aplikacjach
obsługujących API poziomów od 7 do 10. Jeśli zastosujemy klasę
ActionBarActivity jako klasę bazową naszej aktywności, to musimy użyć
jednego z tych motywów, gdyż w przeciwnym razie aplikacja nie będzie działać.
Nie możemy jednak używać bardziej nowoczesnych motywów, takich jak Material.

Nawet jeśli usuniemy z aplikacji odwołania do klasy ActionBarActivity, to Android Studio automatycznie dodało
bibliotekę appcompat v7 do zależności
jednak biblioteka wsparcia v7 wciąż będzie stanowiła zależność projektu. Możesz aplikacji. Zależnie od używanej wersji
się o tym przekonać, wybierając z menu głównego Android Studio opcję File/ Android Studio biblioteka ta może,
Project structure. Kiedy klikniesz moduł app i przejdziesz na kartę Dependencies, lecz nie musi, być używana.
zobaczysz na niej odwołanie do biblioteki appcompat v7.

370 Rozdział 9.
Paski akcji

Zadbamy, by aplikacja używała aktualnych motywów


Chcemy, by prototyp aplikacji używał pasków akcji. Nasza aplikacja może działać na urządzeniach
wykorzystujących API co najmniej poziomu 17, dzięki czemu nie musimy zapewniać zgodności
wstecznej poprzez stosowanie klas ActionBarActivity i Theme.AppCompat. Zamiast tego
nadamy aplikacji bardziej nowoczesny wygląd poprzez domyślne wykorzystanie motywu Holo
i automatyczne użycie motywu Material, jeśli urządzenie używa API poziomu 21 lub wyższego.

W tym celu musimy wykonać dwie operacje:

1 Upewnić się, że w kodzie aktywności nie ma odwołań do klasy ActionBarActivity.


Jeśli takie odwołania się pojawią, to będziemy mogli używać co najwyżej motywu Theme.AppCompat.

2 Zastosować odpowiednie motywy.


Zadbamy o to, by aplikacja wybierała odpowiedni motyw, zależny do obsługiwanego poziomu API.

Zachowamy zależność od biblioteki appcompat v7, gdyż będzie ona miała


wpływ na kod, który niebawem napiszemy.

Zmiana MainActivity, aby rozszerzała klasę Activity


Zaczniemy od upewniania się, że klasa MainActivity, której kod jest zapisany
w pliku MainActivity.java, używa klasy Activity, a nie ActionBarActivity.
Zaktualizuj zatem kod tego pliku w swojej aplikacji tak, by miał następującą postać:

package com.hfad.wloskieconieco; ć dziedziczy


Upewnij się, że Twoja aktywnoś rActivity.
po klasie Acti vity, a nie Acti onBa
y ActionBarActivity
import android.app.Activity; W przypadku zastosowania klas Holo lub Material
nie będziesz mógł użyć moty wów
motywu AppCompat.
import android.os.Bundle; — Android wymusi zastosowanie

public class MainActivity extends Activity {


WloskieCoNieco
@Override
protected void onCreate(Bundle savedInstanceState) { app/src/main
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); java
}
com.hfad.wloskieconieco
}

MainActivity.java
Teraz, kiedy już mamy pewność, że aktywność MainActivity nie używa
klasy ActionBarActivity, pokażemy Ci, jak można zastosować motyw.

jesteś tutaj  371


Zastosowanie motywu

Zastosowanie motywu w pliku AndroidManifest.xml


Jak już wiesz, plik manifestu, AndroidManifest.xml, zawiera ważne informacje dotyczące WloskieCoNieco
aplikacji, na przykład o tym, jakie aktywności ją tworzą. Oprócz tego plik manifestu
zawiera także szereg atrybutów mających wpływ na paski akcji.
app/src/main
Poniżej przedstawiliśmy kod pliku AndroidManifest.xml, który wygenerowało dla nas <xml>
</xml>
Android Studio (jego kluczowe fragmenty wyróżniliśmy pogrubioną czcionką):
AndroidManifest.xml
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.hfad.wloskieconieco” >
<application
android:allowBackup=”true” Ikona aplikacji (Android Studio
określa ją domyślnie)
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name” Przyjazna dla użytkownika nazwa aplikacji

android:theme=”@style/AppTheme” > Motyw


<activity
android:name=”.MainActivity”
Przyjazna dla użytkownika nazwa aktywności
android:label=”@string/app_name” >
... Etykieta
</activity>
</application> Ikona
</manifest>

Atrybut android:icon służy do określenia ikony aplikacji. Ikona ta jest prezentowana


w programie uruchomieniowym, a jeśli używany motyw korzysta z paska akcji, to także na nim.
Ikona może być plikiem graficznym lub zasobem typu mipmap. Mipmap to obrazy, które mogą
być używane jako ikony aplikacji i są przechowywane w katalogach mipmap*, w katalogu Android Studio tworzy
domyślne ikony aplikacji
app/src/main/res. Podobnie jak w przypadku zwyczajnych zasobów graficznych, także obrazy przeznaczone do użycia
mipmap mogą występować w różnych wersjach dostosowanych do ekranów o różnych cechach na ekranach o różnej
gęstości. W starszych
— wystarczy je umieszczać w odpowiednich katalogach, z nazwami rozpoczynającymi się wersjach Android Studio
od mipmap. Na przykład ikona umieszczona w katalogu mipmap-hdpi będzie używana na ikony te były umieszczane
urządzeniach wyposażonych w ekran o wysokiej gęstości. Do zasobów tego typu odwołujemy się, w katalogach drawable,
natomiast nowsze zapisują
używając prefiksu @mipmap. je w katalogach mipmap.
Atrybut android:label określa przyjazną dla użytkownika nazwę aplikacji lub aktywności,
zależnie od tego, czy został umieszczony w elemencie <application> czy <activity>.
Paski akcji wyświetlają nazwę bieżącej aktywności; jeśli jednak bieżąca aktywność nie ma nazwy,
to na pasku jest wyświetlana nazwa aplikacji.
Atrybut android:theme służy do określania motywu. Umieszczenie tego atrybutu w elemencie
<application> powoduje określenie motywu używanego w całej aplikacji. W razie umieszczenia
atrybutu w elemencie <activity> podany motyw będzie używany tylko w danej aktywności.
W naszym przypadku atrybut android:theme ma wartość ”@style/AppTheme”. Prefiks @style
oznacza, że motyw został zdefiniowany w pliku zasobów stylów. A co to za plik?

372 Rozdział 9.
Paski akcji

Definiowanie stylów w pliku zasobów stylów


Plik zasobów stylów zawiera szczegółowe informacje o używanych motywach.

Podczas tworzenia projektu Android Studio tworzy domyślny plik zasobów stylów
o nazwie styles.xml i zapisuje go w katalogu app/src/main/res/values. Jego kod będzie
przypominał ten przedstawiony poniżej: Studio
Nie przejmuj się, jeśli Android
użyło inne go tema tu — i tak go
<resources>
zmienimy na następnej stronie.
<!-- Base application theme. -->
<style name=”AppTheme” parent=”Theme.AppCompat.Light.DarkActionBar”>
<!-- Customize your theme here. -->
</style>
</resources>

Plik zasobów stylów zawiera co najmniej jeden styl. Każdy styl jest definiowany
przy użyciu elementu <style>.

Każdy styl musi mieć nazwę, którą definiuje się za pomocą atrybutu name. WloskieCoNieco
Określenie nazwy stylu jest niezbędne, gdyż umożliwia ona odwołanie się
do danego stylu w atrybucie android:theme, w pliku AndroidManifest.xml. app/src/main
W naszym przypadku styl ma nazwę ”AppTheme”, dlatego w pliku manifestu,
AndroidManifest.xml, możemy się do niego odwołać, używając zapisu
res
”@style/AppTheme”.

Atrybut parent określa, skąd dany styl dziedziczy swoje właściwości. values
<xml>
W przedstawionym przykładzie jest to ”Theme.AppCompat.Light.DarkActionBar”. </xml>

styles.xml
Pliku zasobów stylów można także używać do dostosowywania wyglądu
aplikacji poprzez modyfikowanie właściwości istniejącego motywu. W tym celu
do elementu <style> należy dodać element <item> opisujący wprowadzaną
modyfikację. Na przykład poniższy przykład pokazuje, w jaki sposób można
zmodyfikować motyw, by aktywności miały czerwone tło:

<resources>
<style name=”AppTheme” parent=”Theme.AppCompat.Light.DarkActionBar”>
<item name=”android:background”>#FF0000</item>
określa, że
</style> Ten wiersz ści ma mieć
tło ak ty w no
</resources> lor.
czerwony ko

Nie będziemy się tu wgłębiać w tajniki modyfikowania motywów. Jeśli chcesz


się dowiedzieć czegoś więcej na ten temat, zajrzyj do dokumentacji dostępnej
w internecie, na stronie http://developer.android.com/guide/topics/ui/themes.html.

Na następnej stronie zmienimy motyw używany przez aplikację.

jesteś tutaj  373


Ze stylem

Określenie domyślnego motywu w pliku styles.xml


Teraz zmienimy aplikację w taki sposób, by domyślnie używała motywu Theme.Holo.Light
i zmieniała go na motyw Theme.Material.Light, jeśli zostanie uruchomiona w wersji
systemu obsługującej API poziomu 21.

Zaczniemy od zmiany domyślnego tematu aplikacji. W tym celu otwórz plik zasobów stylów,
styles.xml, zapisany w katalogu app/src/main/res/values. Jest to domyślny plik zasobów stylów.
Chcemy, by aplikacja domyślnie używała motywu Theme.Holo.Light, więc musimy to
podać w atrybucie elementu <style>:
WloskieCoNieco
<resources>
<style name=”AppTheme” parent=”android:Theme.Holo.Light”> app/src/main
<!-- Customize your theme here. -->
</style> res

</resources>
values
<xml>

Na nowszych urządzeniach użyjemy motywu Material </xml>

styles.xml
Jak już wiesz z rozdziału 8., możemy skorzystać z różnych struktur katalogów,
by aplikacja używała różnych zasobów odpowiednio dobieranych w trakcie jej
działania. W ramach przykładu pokazaliśmy, jak można używać w aplikacji
różnych układów, wybieranych w zależności od rozmiaru ekranu urządzenia.

Teraz chcemy, by aplikacja używała różnych stylów w zależności od


obsługiwanego poziomu API. Aby aplikacja skorzystała z określonego zasobu
w przypadku uruchomienia w wersji systemu obsługującej API poziomu 21, Przekonasz się,
możemy utworzyć nowy plik zasobów i dodać do niego interesujący nas zasób. że najłatwiej
jest dodawać
A zatem w katalogu app/src/main/res utwórz nowy katalog o nazwie values-21. nowe katalogi
do projektu,
Następnie skopiuj do niego plik styles.xml przechowywany w katalogu values. wyświetlając
eksplorator plików
Chcemy, żeby aplikacja używała motywu Material, jeśli zostanie uruchomiona Android Studio
w widoku Project.
w wersji systemu obsługującej API poziomu 21. Dlatego otwórz plik styles.xml
z katalogu values-v21 i zmień w nim używany motyw na Theme.Material.Light:

<resources> WloskieCoNieco
<style name=”AppTheme” parent=”android:Theme.Material.Light”>
<!-- Customize your theme here. --> app/src/main
</style> Zastosujemy ten motyw, jeśli
</resources> używana wersja systemu
obsługuje API poziomu 21. res

Nazwa stylu podana w każdym z plików zasobów stylów powinna być taka values-v21
sama, gdyż dzięki temu wybrany motyw będzie mógł być stosowany <xml>
</xml>
w trakcie działania aplikacji. Zobaczmy, jak to się dzieje.
styles.xml

374 Rozdział 9.
Paski akcji

Co się dzieje podczas działania aplikacji?


1 Po uruchomieniu aplikacji system dowiaduje się, że musi zastosować motyw
opisany w zasobie @style/AppTheme.

Muszę użyć stylu o nazwie


AppTheme, który najlepiej
nadaje się dla tego urządzenia.

Android

2 Jeśli aplikacja została uruchomiona w wersji systemu obsługującej API poziomu 21,
to ma użyć stylów o nazwie AppTheme z katalogu values-v21.
Ten styl określa, że należy użyć motywu o nazwie Theme.Material.Light, więc zostanie on zastosowany.

API 21? values-v21


Idealnie pasuje. <xml> Name: AppTheme
</xml>
Parent: Theme.Material.Light
styles.xml
Android

3 Jeśli aplikacja została uruchomiona w wersji systemu obsługującej API poziomu niższego
od 21, to zastosowany zostanie styl o nazwie AppTheme z katalogu values.
Ten styl określa, że należy użyć motywu o nazwie Theme.Holo.Light, więc zostanie on zastosowany.

Użyję stylu
z katalogu values. values
<xml> Name: AppTheme
</xml>
Parent: Theme.Holo.Light
Android styles.xml

Jazda próbna aplikacji


Kiedy uruchomisz aplikację, aktywność główna, MainActivity, będzie miała pasek
akcji. Jeśli uruchomisz aplikację na urządzeniu z systemem obsługującym API
poziomu 21, to użyje ona motywu Theme.Material.Light. Jeśli natomiast aplikacja
zostanie uruchomiona w systemie obsługującym API poziomu niższego od 21,
to używanym motywem będzie Theme.Holo.Light.

W systemie obsługującym
API poziomu niższego od
W systemie obsługującym API 21 zostanie użyty motyw
poziomu 21 zostanie użyty Theme.Holo.Light.
motyw Theme.Material.Light.

jesteś tutaj  375


Elementy akcji

Dodawanie elementów do paska akcji


W przeważającej większości przypadków będziemy chcieli dodawać do paska akcji jakieś
elementy. Będą to głównie przyciski i teksty, których kliknięcie sprawi, że coś się stanie.
W ramach przykładu dodamy teraz do naszego paska akcji przycisk Złóż zamówienie.

Stworzymy nowy przycisk


akcji o nazwie Złóż
zamówienie.

Aby dodać do paska akcji elementy akcji, musimy wykonać następujące czynności:

1 Zdefiniować element akcji w pliku zasobu menu.

2 Zadbać, by aktywność przygotowała zasób menu.


W tym celu musisz zaimplementować metodę onCreateOptionsMenu().

3 Dodać kod, który określi, co ma się dziać po kliknięciu poszczególnych


elementów paska akcji.
W tym celu musisz zaimplementować metodę onOptionsItemSelected().

376 Rozdział 9.
Paski akcji

Plik zasobów menu Nasz domyślny pasek akcji.

Kiedy tworzymy projekt zawierający aktywność, Android Studio tworzy także


domyślny plik zasobu menu. Podczas tworzenia obecnego projektu kazaliśmy
nadać temu plikowi nazwę menu_main.xml, a Android Studio zapisało go
w katalogu aap/src/main/res. W tym katalogu będą umieszczane wszystkie pliki
zasobów menu.

Poniżej przedstawiliśmy plik zasobów menu stworzony na potrzeby naszego


projektu przez Android Studio. Opisuje on pojedynczy element akcji o nazwie
Ustawienia, który jest wyświetlany w obszarze nadmiarowym paska akcji:

<menu xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
xmlns:app=”http://schemas.android.com/apk/res-auto”
WloskieCoNieco
tools:context=”.MainActivity”>

app/src/main
<item android:id=”@+id/action_settings”
To jest
element akcji android:title=”@string/action_settings”
Settings. res
android:orderInCategory=”100”
app:showAsAction=”never” /> menu
</menu> <xml>
</xml>

menu_main.xml
W każdym pliku zasobów menu elementem głównym jest <menu>. Każdy taki plik definiuje
pojedyncze menu bądź zbiór elementów, które należy dodać do paska akcji. Aplikacja
może zawierać wiele takich plików zasobów menu, co jest bardzo przydatne, gdy chcemy,
by na paskach akcji w poszczególnych aktywnościach były wyświetlane różne elementy.

Poszczególne elementy dodaje się do menu za pomocą elementów <item>. Każdy taki
element akcji jest opisywany przez odrębny element <item>. Elementy <item> mogą mieć
kilka różnych atrybutów, a najczęściej używane przedstawiliśmy poniżej:

android:id Określa unikalny identyfikator elementu menu. Identyfikator ten jest niezbędny,
by można było odwoływać się do elementu menu w kodzie aktywności.

android:icon Ikona elementu. Może to być zasób graficzny lub zasób typu mipmap.
android:title Tekst elementu. Ten tekst może nie być prezentowany, jeśli w elemencie menu
została określona ikona i jeśli na pasku akcji nie będzie dostatecznie dużo miejsca
zarówno na ikonę, jak i na tekst. Jeżeli element jest wyświetlany
w obszarze nadmiarowym paska akcji, to będzie wyświetlany wyłącznie jego tekst.
android:orderInCategory Liczba całkowita, która pozwala systemowi określać kolejność wyświetlania
poszczególnych elementów na pasku akcji.

W powyższym przykładzie został zastosowany jeszcze inny atrybut: showAsAction.


Przyjrzymy mu się dokładniej na następnej stronie.
jesteś tutaj  377
showAsAction

Atrybut showAsAction menu


Atrybut showAsAction służy do określania, w jaki sposób dany element ma być
prezentowany na pasku akcji. Na przykład możemy zażądać, aby był on wyświetlany
w obszarze nadmiarowym, a nie na głównym pasku akcji, albo aby był umieszczany na
głównym pasku akcji tylko wówczas, gdy będzie tam dostatecznie dużo miejsca.
Atrybut ten może przyjmować następujące wartości:

“ifRoom” Sprawia, że element zostanie umieszczony na pasku akcji, jeśli będzie tam dostatecznie
dużo miejsca, a w przeciwnym razie zostanie umieszczony w obszarze nadmiarowym.
“withText” Oznacza, że ma zostać dołączony tytuł elementu.
“never” Oznacza, że element ma zostać umieszczony w obszarze nadmiarowym i nigdy nie ma
być prezentowany na głównym pasku akcji.
“always” Sprawia, że element zawsze będzie umieszczany w głównym obszarze paska akcji.
Ta wartość powinna być używana sporadycznie — jeśli dodamy ją do zbyt wielu
elementów, to będą one wzajemnie na siebie nachodzić.

Spójrzmy jeszcze raz na atrybut showAsAction umieszczony w kodzie menu. Zauważ, że poprzedza go
prefiks app:, a nie android:.

<menu xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto” Ten atrybut dodaje
przestrzeń nazw app.
...>

ategory
<item android:id=”@+id/action_settings” Atrybuty ID, title oraz orderInC
android:title=”@string/action_settings” używają przestrzeni nazw android.

android:orderInCategory=”100”
app:showAsAction=”never” /> Atrybut showAsAction używa
przestrzeni nazw app.
</menu>

Wcześniej w tym rozdziale przekonałeś się, że nasz projekt jest zależny od biblioteki
appcompat v7. W przypadku tej biblioteki atrybut showAsAction nie jest dostępny WloskieCoNieco
w przestrzeni nazw android.
app/src/main
Jeśli projekt jest zależny od biblioteki appcompat v7, to atrybut showAsAction musi być
poprzedzony prefiksem app:, a do elementu <menu> należy dodać poniższy atrybut:
res
xmlns:app=”http://schemas.android.com/apk/res-auto”
Jeśli projekt nie jest zależny od biblioteki appcompat v7, to atrybut showAsAction menu
może być poprzedzony prefiksem android:, a nie app:; oprócz tego w elemencie <xml>
</xml>
<menu> można pominąć atrybut
menu_main.xml
xmlns:app=”http://schemas.android.com/apk/res-auto”

378 Rozdział 9.
Paski akcji

Dodawanie nowego elementu akcji


Zamierzamy teraz dodać do paska akcji nowy element, który będzie umożliwiał
złożenie zamówienia. Element ten będzie miał swoją ikonę i tytuł Złóż zamówienie.
W przypadku stosowania ikon w elementach akcji możemy tworzyć własne
bądź skorzystać z ikon dostępnych w specjalnym, systemowym zestawie ikon
przeznaczonych do stosowania na paskach tego typu. Zestaw ten zawiera wiele
standardowych ikon, których możemy używać we własnych aplikacjach.
My skorzystamy z ikony ic_action_new_event dostępnej w zestawie.
Aby to zrobić, w pierwszej kolejności musimy pobrać ten zestaw ze strony Nowy element akcji.
https://developer.android.com/design/downloads/index.html. Po pobraniu
i rozpakowaniu pliku przekonamy się, że jest w nim dostępnych wiele ikon
przeznaczonych dla różnych motywów i wielkości ekranów.
Ikony ic_action_new_event są dostępne w katalogu Action Bar Icons/
holo_light/05_content_new_event. Dostępne są cztery różne ikony, przeznaczone nie utworz
yło
dla urządzeń wyposażonych w ekrany o różnej wielkości i umieszczone ś li A n d ro id Studio as tworzenia
Je logów pod
cz

w katalogach o odpowiednich nazwach. Ikony te trzeba skopiować do tych kata to musisz to zrob
proje kt u ,
odpowiednich katalogów projektu. A zatem skopiuj ikonę z katalogu ie.
samodzieln
drawable-hdpi do katalogu drawable-hdpi swojego projektu, i tak dalej.
Ten zestaw ikon można także
znaleźć na stronie https://github.com/
Po dodaniu ikon dodaj do pliku strings.xml nowy zasób łańcuchowy johnjohndoe/Android-Design-Downloads.
action_create_order:
Tego zasobu użyjemy do określen
<string name=”action_create_order”>Złóż zamówienie</string> tytułu elementu akcji. ia

I na koniec dodaj do pliku menu_main.xml kod definiujący nowy element


paska akcji:
<menu xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
xmlns:app=”http://schemas.android.com/apk/res-auto” WloskieCoNieco
tools:context=”.MainActivity”>
app/src/main
<item android:id=”@+id/action_create_order”
android:title=”@string/action_create_order” res
android:icon=”@drawable/ic_action_new_event”
android:orderInCategory=”1” menu
Jeśli na pasku akcji nie będzie
<xml>
app:showAsAction=”ifRoom” /> dostatecznie dużo miejsca, </xml>
to nowy element zostanie
wyświetlony tylko jako ikona. menu_main.xml
<item android:id=”@+id/action_settings”
android:title=”@string/action_settings”
android:orderInCategory=”100”
app:showAsAction=”never” />
</menu>
Teraz, kiedy już dodałeś element do pliku zasobów menu, będziesz go musiał
dodać do paska akcji w kodzie aktywności. Zobacz, jak należy to zrobić.
jesteś tutaj  379
Metoda onCreateOptionsMenu()

Przygotowanie menu w kodzie aktywności


za pomocą metody onCreateOptionsMenu()
Po utworzeniu pliku zasobów menu zdefiniowane w nim opcje można umieścić na pasku akcji,
implementując metodę onCreateOptionsMenu(). Jest ona wywoływana podczas tworzenia
paska akcji aktywności i ma tylko jeden parametr — obiekt Menu reprezentujący pasek akcji.

Oto kod naszej metody onCreateOptionsMenu():

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.view.Menu; Klasa Menu jest używana przez
metodę onCreateOptionsMenu().
... app/src/main

public class MainActivity extends Activity { java


spowoduje
Zaimplementowanie tej metody h
... dodanie do paska akcji wszystkicbów menu. com.hfad.wloskieconieco
elementów z podanego pliku zaso
@Override MainActivity.java
public boolean onCreateOptionsMenu(Menu menu) {
// Przygotowujemy menu; jeśli jest pasek akcji to dodajemy do niego elementy.
getMenuInflater().inflate(R.menu.menu_main, menu);
return super.onCreateOptionsMenu(menu);
}
}

Elementy do paska akcji dodajemy, używając wywołania o następującej postaci:

To jest obiekt Menu


reprezentujący pasek akcji.
getMenuInflater().inflate(R.menu.menu_main, menu);

To jest plik zasobów menu.

Powyższe wywołanie pobierze wszystkie elementy menu zdefiniowane w pliku


menu_main.xml i doda je do paska akcji reprezentowanego przez obiekt Menu.

380 Rozdział 9.
Paski akcji

Reagowanie na kliknięcia elementów akcji


za pomocą metody onOptionsItemSelected()
Aby nasza aktywność reagowała na kliknięcia elementów umieszczonych na pasku
akcji, musimy zaimplementować metodę onOptionsItemSelected(). Jest ona
wywoływana za każdym razem, gdy zostanie kliknięty element paska akcji.

Metoda onOptionsItemSelected() ma jeden parametr — obiekt typu MenuItem


reprezentujący kliknięty element paska akcji. Korzystając z metody getItemId(),
można pobrać identyfikator elementu paska akcji i na jego podstawie określić,
jakie czynności należy wykonać, na przykład uruchomić nową aktywność.

Oto kod naszej metody onOptionsItemSelected():

package com.hfad.wloskieconieco;

Tej klasy używa metoda WloskieCoNieco


import android.view.MenuItem; onOptionsItemSelected().
... app/src/main

public class MainActivity extends Activity { java

... Obiekt MenuItem reprezentuje com.hfad.wloskieconieco


kliknięty element paska akcji.

@Override MainActivity.java
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { Sprawdzamy, który element
case R.id.action_create_order: został kliknięty.
// Kod wykonywany po kliknięciu przycisku Złóż zamówienie.
Android Studio return true;
utworzyło Musimy zadbać o to, by przycisk Złóż zamówienie coś robił.
element Settings case R.id.action_settings:
(Ustawienia) za // Kod wykonywany po kliknięciu elementu Settings (ustawienia).
nas. Możemy
tu umieścić kod, return true;
który coś zrobi Zwrócenie wartości true informuje system, że kliknięcie
po kliknięciu tego default: elementu paska akcji zostało już obsłużone.
elementu. return super.onOptionsItemSelected(item);
}
}
}

W naszym przypadku sprawimy, że kliknięcie przycisku Złóż zamówienie


spowoduje uruchomienie nowej aktywności — OrderActivity.

jesteś tutaj  381


Utworzenie OrderActivity

Utworzenie aktywności OrderActivity


A zatem musimy utworzyć nową aktywność, OrderActivity, którą będzie mógł
uruchomić przycisk Złóż zamówienie umieszczony na pasku aktywności.

Zacznij od utworzenia nowej, pustej aktywności. Nadaj jej nazwę OrderActivity,


a jej plik układu nazwij activity_order; oprócz tego nadaj aktywności tytuł Złóż
zamówienie, a nazwę pliku zasobów menu ustaw na menu_order.

Poniżej przedstawiliśmy kod pliku OrderActivity.java. Upewnij się, że zawartość


tego pliku w Twoim projekcie będzie taka sama. W szczególności zwróć uwagę
na to, by aktywność OrderActivity dziedziczyła po klasie Activity, a nie
ActionBarActivity, ponieważ ActionBarActivity pozwala na stosowanie tylko
jednego motywu Theme.AppCompat, my zaś chcemy używać dwóch: Holo i Material.

package com.hfad.wloskieconieco;

Upewnij się, że aktywność


import android.app.Activity; OrderActivity dziedziczy
po klasie Activity, a nie
import android.os.Bundle; ActionBarActivity.
WloskieCoNieco

public class OrderActivity extends Activity {


app/src/main

@Override
java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); com.hfad.wloskieconieco

setContentView(R.layout.activity_order);
} OrderActivity.java

W kodzie aktywności OrderActivity nie zaimplementowaliśmy metod


onCreateOptionsMenu() ani onOptionsItemSelected(), gdyż ta
aktywność nie musi wyświetlać niczego na swoim pasku akcji. Gdybyśmy
jednak kiedyś uznali, że chcemy wyświetlić jakieś elementy menu na pasku
akcji w tej aktywności, to musielibyśmy zaimplementować te metody.

A teraz, skoro już utworzyliśmy aktywność OrderActivity, zadbajmy o to,


by kliknięcie przycisku Złóż zamówienie ją uruchamiało.

382 Rozdział 9.
Paski akcji

Uruchomienie aktywności OrderActivity


po kliknięciu przycisku Złóż zamówienie
Chcemy, by kliknięcie przycisku Złóż zamówienie umieszczonego na pasku akcji
aktywności MainActivity uruchamiało aktywność OrderActivity. W tym celu
musimy zmodyfikować kod metody onOptionsItemSelected() zdefiniowanej
w aktywności MainActivity. Aktywność OrderActivity uruchomimy, używając intencji.

Oto kod, który należy dodać do aktywności:

package com.hfad.wloskieconieco;

Klasa Intent będzie nam


import android.content.Intent; potrzebna.
...
WloskieCoNieco
public class MainActivity extends Activity {
app/src/main
...
java

@Override
com.hfad.wloskieconieco
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_create_order: MainActivity.java

// Kod wykonywany po kliknięciu przycisku Złóż zamówienie


Intent intent = new Intent(this, OrderActivity.class);
startActivity(intent); Ta intencja posłuży nam do uruchomienia
return true; aktywności OrderActivity w odpowiedzi
na kliknięcie przycisku Złóż zamówienie.
case R.id.action_settings:
// Kod wykonywany po kliknięciu elementu Settings (Ustawienia)
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

W odpowiedzi na kliknięcie umieszczonego na pasku akcji elementu Złóż


zamówienie tworzymy intencję, która uruchomi aktywność OrderActivity.

Na następnej stronie przedstawimy kompletny kod aktywności MainActivity,


zapisany w pliku MainActivity.java.

jesteś tutaj  383


Kod aktywności MainActivity

Kompletny kod aktywności MainActivity


package com.hfad.wloskieconieco;

WloskieCoNieco
import android.app.Activity;
import android.content.Intent;
app/src/main
import android.os.Bundle;
import android.view.Menu; java
import android.view.MenuItem;
com.hfad.wloskieconieco
public class MainActivity extends Activity {
MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
} W tej metodzie dodajemy elementy
do paska akcji.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Przygotowujemy menu; jeśli jest pasek akcji, to dodajemy do niego elementy
getMenuInflater().inflate(R.menu.menu_main, menu);
return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_create_order:
// Kod wykonywany po kliknięciu przycisku Złóż zamówienie
Intent intent = new Intent(this, OrderActivity.class);
startActivity(intent);
Tu uruchamiamy aktywność Orde
return true; rActivity
w odpowiedzi na kliknięcie przy
cisku
case R.id.action_settings: Złóż zamówienie.
// Kod wykonywany po kliknięciu elementu Settings (Ustawienia)
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

384 Rozdział 9.
Paski akcji

Jazda próbna aplikacji


Kiedy uruchomisz aplikację, na pasku akcji aktywności głównej, Nie przejmuj się, jeśli
MainActivity, zostanie wyświetlony element akcji Złóż zamówienie. Spokojnie Twój element nie pojawi
Jego kliknięcie spowoduje uruchomienie aktywności OrderActivity. się na pasku akcji.

Może się zdarzyć, że element akcji zostanie


umieszczony w obszarze nadmiarowym.
Przyczyną takiego stanu rzeczy jest
błąd występujący w niektórych wersjach
biblioteki appcompat v7. Jeśli tak się stanie
w Twojej aplikacji, prześlij firmie Google
To jest element akcji
Złóż zamówienie. raport z informacjami na ten temat.

Kliknięcie elementu
Złóż zamówienie
spowoduje uruchomienie
aktywności
OrderActivity.

Nie istnieją
głupie pytania
P: Moja aplikacja już ma etykietę P: Dlaczego muszę używać klasy P: Twierdzicie, że można używać
i ikonę. Skąd one się wzięły? ActionBarActivity, jeśli chcę, by moja różnych motywów w różnych
aplikacja mogła działać w systemie aktywnościach. Czy może się
O: Gdy tworzymy projekt aplikacji na obsługującym API poziomu niższego zdarzyć, że zechcę zastosować takie
Androida w IDE takim jak Android Studio, od 11? rozwiązanie?
środowisko programistyczne tworzy za nas
pewne fragmenty kodu. Należy do nich O: Ponieważ wyświetlenie paska akcji O: Owszem. Motywy Holo i Material
także określenie nazwy aplikacji i jej ikony. wymaga zastosowania biblioteki Android mają kilka podrzędnych klas motywów,
Support Library. które nadają aktywnościom nieco
P: Czy można używać pasków akcji odmienny wygląd. Gdybyś chciał nadać
w aplikacjach korzystających z API P: Czy kiedykolwiek naprawdę będę jednej z aktywności odmienny wygląd,
poziomu niższego od 7? chciał używać różnych motywów dla to faktycznie mógłbyś chcieć zastosować
różnych poziomów API?
O: Nie, nie można. Nie jest to jednak w niej inny motyw.

wielki problem, gdyż aktualnie jest już O: Tak, to się może zdarzyć. Motyw
bardzo mało urządzeń używających tak Material został wprowadzony w API
starych wersji systemu Android. poziomu 21, więc możesz chcieć,
by aplikacje używały tego motywu,
jeśli będzie dostępny.
jesteś tutaj  385
Dzielenie się to troska

Dzielenie się treściami z poziomu paska akcji


Następnym zagadnieniem, którym się zajmiemy, będzie stosowanie
dostawcy akcji z paskiem akcji. Dostawca akcji to umieszczany na pasku
akcji element, który samodzielnie obsługuje swój wygląd i działanie.
Właśnie tak wygląda akcja
W tym rozdziale skoncentrujemy się na dostawcy akcji udostępniania. udostępniania wyświetlona na
pasku akcji. Po jej kliknięciu
Pozwala on użytkownikom na udostępnianie zawartości naszej aplikacji wyświetlana jest lista aplikacji,
ci.
innym aplikacjom, takim jak Gmail. Na przykład możemy użyć tego którym możemy udostępnić treś
dostawcy, by zapewnić użytkownikom możliwość wysyłania szczegółowych
informacji o wybranej pizzy do ich znajomych.

Dostawca akcji udostępniania definiuje swoją własną ikonę, dzięki czemu


nie musimy jej określać własnoręcznie. Po jej kliknięciu dostawca wyświetla
listę wszystkich aplikacji, którym dane treści mogą być udostępnione.

Treści są udostępniane za pomocą intencji


Aby dostawca akcji udostępniania udostępnił treści, musimy przekazać
do niego intencję. Ta intencja definiuje treści, które chcemy udostępnić,
i określa ich typ. Na przykład jeśli zdefiniujemy intencję, która będzie
zawierała tekst, i zastosujemy akcję ACTION_SEND, to akcja udostępniania
wyświetli nam listę zainstalowanych na danym urządzeniu aplikacji, które są
w stanie udostępniać dane tego typu.

1 Aktywność tworzy intencję i przekazuje ją do dostawcy akcji udostępniania.


Intencja określa treść, która ma być udostępniona, jej typ i akcję.
Intencja

ACTION_SEND
type: “text/plain”
messageText:”Cześć!” ShareAction
TwojaAktywnosc
Provider

2 Kiedy użytkownik kliknie akcję udostępniania, użyje ona przekazanej intencji,


by wyświetlić użytkownikowi listę aplikacji, które są w stanie obsłużyć tę intencję.
Użytkownik wybiera aplikację, a dostawca akcji udostępniania przekazuje intencję do
odpowiedniej aktywności tej aplikacji, która jest w stanie ją obsłużyć.

Intencja

ACTION_SEND
type: “text/plain”
ShareAction messageText:”Cześć!”
AktywnoscAplikacji
Provider

386 Rozdział 9.
Paski akcji

Dodanie dostawcy akcji udostępniania do menu_main.xml


Akcję udostępniania dodajemy do paska akcji poprzez wstawienie jej do pliku
zasobów menu.

W pierwszej kolejności do pliku strings.xml dodaj nowy zasób łańcuchowy


— action_share. Użyjemy go do dodania tytułu, na wypadek gdyby została WloskieCoNieco
ona wyświetlona w obszarze nadmiarowym paska akcji:

<string name=”action_share”>Udostępnij</string> app/src/main

Akcję udostępniania możemy dodać do pliku zasobów menu dokładnie tak


res
samo jak wcześniej, czyli używając elementu <item>. Tym razem jednak
musimy zaznaczyć, że używamy dostawcy akcji udostępniania. Dlatego
values
musimy dodać do elementu <item> atrybut android:actionProviderClass
<xml>
o wartości android.widget.ShareActionProvider. </xml>

strings.xml
Oto kod, który dodaje do paska akcję udostępniania:

<menu xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
tools:context=”.MainActivity”>
WloskieCoNieco

<item android:id=”@+id/action_create_order” app/src/main


... />
res
<item android:id=”@+id/action_share”
menu
android:title=”@string/action_share”
<xml>
android:orderInCategory=”2” Ten element wyświetli dostawcę akcji </xml>
udostępniania na pasku akcji, jeśli będzie menu_main.xml
app:showAsAction=”ifRoom” na nim dostatecznie dużo miejsca.
android:actionProviderClass=”android.widget.ShareActionProvider” />

To jest klasa dostawcy akcji


<item android:id=”@+id/action_settings” udostępniania.
... />
</menu>

Jeśli dodajemy dostawcę akcji udostępniania do pliku zasobów menu,


to nie musimy dołączać do zasobów aplikacji jego ikony — jest ona
definiowana przez samego dostawcę.

A zatem, skoro już dodaliśmy akcję udostępniania do paska akcji,


musimy określić, co chcemy udostępniać.

jesteś tutaj  387


I znowu intencje

Określanie treści za pomocą intencji


Aby akcja udostępniania faktycznie udostępniała jakieś treści po kliknięciu, musimy
te treści określić w kodzie aktywności. W tym celu musimy przekazać dostawcy akcji
udostępniania intencję, wywołując jego metodę setShareIntent(). Oto, jak użyć
akcji udostępniania do udostępnienia domyślnego tekstu:

package com.hfad.wloskieconieco;
WloskieCoNieco
...
import android.widget.ShareActionProvider; app/src/main

public class MainActivity extends Activity { java

private ShareActionProvider shareActionProvider; com.hfad.wloskieconieco

ShareActionProvider.
... Dodaliśmy zmienną prywatną typu MainActivity.java

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem menuItem = menu.findItem(R.id.action_share);
shareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
setIntent(“To jest przykładowy tekst.”); Ten fragment pobiera dostawcę akcji
return super.onCreateOptionsMenu(menu); udostępniania i zapisuje go w zmiennej
prywatnej. Następnie wywołuje jego metodę
} setIntent().

private void setIntent(String text) {


Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(“text/plain”);
Utworzyliśmy także metodę
intent.putExtra(Intent.EXTRA_TEXT, text); setIntent(). Jej działanie polega na
utworzeniu intencji i przekazaniu
shareActionProvider.setShareIntent(intent); jej do dostawcy akcji udostępniania
} poprzez wywołanie jego metody
setShareIntent().
}

Metodę setShareIntent() trzeba wywoływać za każdym razem, gdy zmieni


się treść, którą chcemy udostępniać. Na przykład gdybyśmy przechodzili do
kolejnych zdjęć w aplikacji fotograficznej, musielibyśmy pamiętać o tym,
by udostępniać aktualnie prezentowane zdjęcie.

Na następnej stronie przedstawimy kompletny kod aktywności, a potem


sprawdzimy, co się stanie po uruchomieniu aplikacji.

388 Rozdział 9.
Paski akcji

Kompletny kod aktywności MainActivity


Oto kompletna zawartość pliku MainActivity.java:

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.app.Activity;
import android.content.Intent; app/src/main
import android.os.Bundle;
import android.view.Menu; Używamy klasy java
ShareActionProvider,
import android.view.MenuItem; więc musimy ją com.hfad.wloskieconieco
import android.widget.ShareActionProvider; zaimportować.

MainActivity.java
public class MainActivity extends Activity {

private ShareActionProvider shareActionProvider;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Przygotowujemy menu; jeśli jest pasek akcji, to dodajemy do niego elementy
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem menuItem = menu.findItem(R.id.action_share);
shareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
setIntent(“To jest przykładowy tekst.”);
return super.onCreateOptionsMenu(menu);
Te fragmenty kodu określają
} domyślny tekst, który
dostawca powinien udostępnić.

private void setIntent(String text) {


Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(“text/plain”);
intent.putExtra(Intent.EXTRA_TEXT, text); Dalsza część kodu
znajduje się na
shareActionProvider.setShareIntent(intent); następnej stronie.
}
jesteś tutaj  389
Kod aktywności MainActivity

Ciąg dalszy kodu aktywności MainActivity


WloskieCoNieco
@Override
public boolean onOptionsItemSelected(MenuItem item) { app/src/main
switch (item.getItemId()) {
case R.id.action_create_order: java
// Kod wykonywany po kliknięciu przycisku Złóż zamówienie
Intent intent = new Intent(this, OrderActivity.class); com.hfad.wloskieconieco

startActivity(intent);
return true; MainActivity.java
case R.id.action_settings:
// Kod wykonywany po kliknięciu elementu Settings (Ustawienia)
return true;
default: Tej metody nie zmienialiśmy.
return super.onOptionsItemSelected(item);
}
}
}

Jazda próbna aplikacji


Po uruchomieniu aplikacji na pasku akcji zostanie wyświetlona akcja Pamiętaj, że akcja udostępniania może
zostać wyświetlona nie w głównym, lecz
udostępniania. Jej kliknięcie spowoduje wyświetlenie listy aplikacji, których w nadmiarowym obszarze paska akcji.
można użyć do udostępnienia przygotowanej intencji. Wybrana aplikacja
zapewni użytkownikowi możliwość udostępnienia domyślnego tekstu.

Intencja, którą Po wybraniu aplikacji


przekazaliśmy dostawcy zostanie w niej
akcji udostępniania, wyświetlony domyślny
określa, że chcemy tekst. My wybraliśmy
udostępnić tekst, aplikację SMS/MMS, więc
używając akcji ACTION_ przekazany tekst został
SEND. Zostanie zatem użyty jako treść wysyłanej
wyświetlona lista wiadomości.
aplikacji, które dają nam
tę możliwość.

390 Rozdział 9.
Paski akcji

Włączanie nawigacji w górę To jest przycisk W górę.

Jeśli tworzona aplikacja zawiera hierarchię aktywności, to możemy włączyć


na pasku akcji przycisk W górę, który pozwoli poruszać się po aplikacji
według hierarchicznych relacji pomiędzy aktywnościami. Na przykład
w naszej aplikacji na pasku akcji aktywności MainActivity umieściliśmy
przycisk uruchamiający aktywność OrderActivity. Jeśli włączymy
przycisk W górę na pasku akcji aktywności OrderActivity, to klikając go,
użytkownik będzie mógł wrócić do aktywności MainActivity.

Kliknij przycisk Złóż


zamówienie, by przejść do Kliknij przycisk
aktywności OrderActivity. W górę, by…
vity.
…wrócić do aktywności MainActi

Można by sądzić, że ten sposób nawigowania po aplikacji nie różni


się od możliwości, jakie daje przycisk Wstecz, ale w rzeczywistości jest
inaczej. Przycisk Wstecz pozwala użytkownikom cofać się przez historię
uruchomionych wcześniej aktywności. Natomiast działanie przycisku
W górę bazuje wyłącznie na hierarchicznej strukturze aplikacji.

Aktywność nadrzędna.

Użyj przycisku Wstecz,


by przejść do poprzedniej
aktywności.

Aktywność podrzędna.
Użyj przycisku
Klikając przycisk
W górę w aktywności
W górę, by przejść
podrzędnej, przechodzimy
w górę hierarchii do w górę hierarchii
aktywności nadrzędnej.
aktywności.
Aby móc w praktyce wypróbować ten sposób poruszania się po aplikacji,
dodamy teraz przycisk W górę do paska akcji naszej aktywności OrderActivity.
Kliknięcie tego przycisku spowoduje wyświetlenie aktywności MainActivity.
jesteś tutaj  391
Hierarchia aktywności

Określanie aktywności nadrzędnej


Przycisk W górę pozwala użytkownikowi poruszać się ku górze hierarchii aktywności w danej
aplikacji. Ta hierarchia jest deklarowana w pliku manifestu aplikacji, AndroidManifest.xml,
poprzez określanie aktywności nadrzędnych. Na przykład w naszej aplikacji chcemy, by
klikając przycisk W górę, użytkownik miał możliwość przejścia z aktywności OrderActivity
do aktywności MainActivity. Oznacza to, że MainActivity jest aktywnością nadrzędną
aktywności OrderActivity.

Począwszy od API poziomu 16, aktywność nadrzędną określamy za pomocą atrybutu


android:parentActivityName. W starszych wersjach systemu, aby określić nazwę
aktywności nadrzędnej, musimy skorzystać z elementu <meta-data>. Poniżej przedstawiliśmy
kod naszego pliku AndroidManifest.xml, w którym zostały zastosowane oba te sposoby
określania aktywności nadrzędnej:

<?xml version=”1.0” encoding=”utf-8”?>


<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.hfad.wloskieconieco” >
<application
android:allowBackup=”true” WloskieCoNieco
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name” app/src/main
android:theme=”@style/AppTheme” > <xml>
</xml>

AndroidManifest.xml
<activity
android:name=”.MainActivity”
android:label=”@string/app_name” >
<intent-filter>
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>
<activity
omu 16 lub
android:name=”.OrderActivity” Aplikacje korzystające z API pozi Określa on,
wyżs zego używ ają tego atrybutu.
android:label=”@string/title_activity_order” nadrzędną
że MainActivity jest aktywnością
android:parentActivityName=”.MainActivity”> akty wnoś ci Orde rAct ivity .
<meta-data
android:name=”android.support.PARENT_ACTIVITY”
android:value=”.MainActivity” /> Elementu <meta-data> trzeba używ
</activity> w przypadkach, gdy aplikacja ma ać wyłącznie
</application> z API poziomu niższego od 16. korzystać
Dodaliśmy go
tu tylko po to, byś mógł poznać
ten
</manifest> a poza tym dołączanie go niczemu element,
nie szkodzi.
Na koniec musimy wyświetlić przycisk W górę na pasku akcji
aktywności OrderActivity.

392 Rozdział 9.
Paski akcji

Dodawanie przycisku W górę


Przycisk W górę jest wyświetlany z poziomu kodu aktywności. W pierwszej
kolejności musimy pobrać referencję do paska akcji aktywności. Służy
Jeśli chcesz wyświetlić
do tego metoda getActionBar(). Następnie musimy wywołać metodę
w aktywności przycisk
setDisplayHomeAsUpEnabled(), przekazując do niej wartość true.
Obejrzyj to! W górę, musisz określić
ActionBar actionBar = getActionBar(); jej aktywność nadrzędną.
actionBar.setDisplayHomeAsUpEnabled(true); W przeciwnym razie wywołanie metody
setDisplayHomeAsUpEnabled()
W naszej aplikacji chcemy wyświetlać przycisk W górę w aktywności spowoduje zgłoszenie wyjątku
OrderActivity, dlatego powyższy fragment kodu dodamy do metody NullPointerException.
onCreate() w pliku OrderActivity.java. Oto kompletny kod tej aktywności:

package com.hfad.wloskieconieco;

Używamy klasy ActionBar,


import android.app.ActionBar; więc musimy ją zaimportować.
import android.app.Activity;
import android.os.Bundle;

WloskieCoNieco
public class OrderActivity extends Activity {

app/src/main
@Override
protected void onCreate(Bundle savedInstanceState) { java
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_order); com.hfad.wloskieconieco

ActionBar actionBar = getActionBar();


actionBar.setDisplayHomeAsUpEnabled(true); OrderActivity.java

}
} Ten fragment kodu wyświetla
przycisk W górę na pasku
akcji aktywności.

Przekonajmy się teraz, co się stanie po uruchomieniu aplikacji.

jesteś tutaj  393


Jazda próbna

Jazda próbna aplikacji


Kiedy uruchomisz aplikację i klikniesz przycisk Złóż zamówienie,
to podobnie jak wcześniej zostanie wyświetlona aktywność OrderActivity.

Kliknij przycisk
Złóż zamówienie,
by uruchomić
aktywność
OrderActivity.
Na pasku akcji aktywności
OrderActivity będzie
dostępny przycisk W górę.
Kiedy go klikniesz…
…zostanie wyświetlona
aktywność nadrzędna
aktywności OrderActivity.

Na pasku akcji aktywności OrderActivity zostanie teraz wyświetlony przycisk


W górę. Kliknięcie tego przycisku spowoduje wyświetlenie aktywności nadrzędnej
określonej w hierarchii aplikacji, czyli aktywności MainActivity.

394 Rozdział 9.
Paski akcji

Twój przybornik do Androida

Rozdział 9.
Opanowałeś już rozdział 9. i dodałeś
do swojego przybornika z narzędziami j
Pełny kod przykładowe
umiejętność tworzenia i stosowania aplikacji pre zen tow ane j
pasków akcji. roz dzi ale mo żes z
w tym
pob rać z ser we ra FT P
wydawnictwa Helion:
klady/
ftp://ftp.helion.pl/przy
CELNE SPOSTRZEŻENIA andrrg.zi p

 Aby dodać pasek akcji do aplikacji zgodnych  Elementy paska akcji dodajemy, zapisując je
z API poziomu 11 lub wyższego, wystarczy w pliku zasobów menu.
wybrać motyw Holo lub Material.  Elementy z pliku zasobów menu dodajemy
 Aby dodać pasek akcji do aplikacji zgodnych do paska akcji, implementując metodę
z API poziomu 7 lub wyższego, należy onCreateOptionsMenu().
zastosować motyw AppCompat i klasę  Aby określić czynności, jakie należy wykonać
ActionBarActivity. W razie zastosowania
po kliknięciu poszczególnych elementów
klasy ActionBarActivity musimy wybrać
paska akcji, należy zaimplementować metodę
motyw AppCompat.
onOptionsItemSelected().
 Klasa ActionBarActivity i motyw  Treści możemy udostępniać, dodając do paska
AppCompat są dostępne w bibliotece Android
akcji dostawcę akcji udostępniania. Należy go
Support Library v7.
dodać do pliku zasobów menu. Następnie należy
 Używany motyw można określić w pliku wywołać jego metodę setShareIntent(),
manifestu aplikacji, AndroidManifest.xml, przekazując do niej intencję opisującą treści,
za pomocą atrybutu android:theme. które chcemy udostępnić.
 Style definiujemy w pliku zasobów stylów,  Na pasku akcji można także wyświetlać
używając elementu <style>. Nazwę stylu przycisk W górę pozwalający na przechodzenie
określa atrybut name. Atrybut parent pozwala do aktywności nadrzędnej. Hierarchię
określić, skąd będą dziedziczone właściwości aktywności definiujemy w pliku manifestu,
definiowanego stylu. AndroidManifest.xml. W celu wyświetlenia
przycisku W górę musimy wywołać metodę
 Pliki zasobów stylów są domyślnie umieszczane
setDisplayHomeAsUpEnabled() obiektu
w katalogu app/src/main/res/values. Aby plik
klasy ActionBar.
zasobów stylów był stosowany w API poziomu 21,
należy go umieścić w katalogu app/src/main/res/
values-v21.

jesteś tutaj  395


396 Rozdział 9.
10. Szuflady nawigacyjne

Z miejsca na miejsce
Wiem, że ze swoją
cudowną szufladą
nawigacyjną nigdy się
nie zgubię!

Aplikacje są nieporównanie lepsze, gdy można się po nich łatwo poruszać.


W tym rozdziale przedstawimy Ci szufladę nawigacyjną wysuwany panel, który jest
wyświetlany na ekranie po przesunięciu palcem lub kliknięciu ikony umieszczonej na pasku akcji.
Pokażemy Ci, jak można wyświetlać w niej listę odnośników umożliwiających przechodzenie
do kluczowych węzłów aplikacji. Oprócz tego przekonasz się, że przełączanie fragmentów
pozwala łatwo docierać do tych węzłów i je szybko wyświetlać.

to jest nowy rozdział  397


Więcej pizzy

Zmiany w aplikacji dla restauracji Włoskie Co Nieco


W rozdziale 9. przedstawiliśmy szkic głównego ekranu najwyższego poziomu aplikacji dla
Włoskiego Co Nieco. Zawierał on listę opcji umożliwiających użytkownikom przechodzenie
w różne miejsca aplikacji. Pierwsze trzy opcje prowadziły do ekranów kategorii prezentujących
odpowiednio pizze, dania z makaronów i restauracje, natomiast ostatnia opcja pozwalała
przechodzić do ekranu szczegółów/edycji, na którym użytkownik mógł złożyć zamówienie.

Włoskie Co Nieco To jest aktywność głównego pozi


omu
aplikacji.

Pizze

Makarony To są odnośniki do ekranów kate


gorii.

Restauracje

Złóż zamówienie Ten odnośnik pozwala wyświetlić a


ekran
szczegółów/edycji, na którym możn
9.
złożyć zamówienie. W rozdziale ci.
liśm y go na pase k akty wnoś
przenieś

Wiesz już, jak dodawać nowe elementy do paska akcji. To rozwiązanie najlepiej się
nadaje do umieszczania opcji aktywnych, takich jak tworzenie zamówień. Co jednak
można zrobić w przypadku odnośników do ekranów kategorii? Ponieważ są to opcje
pasywne, służące za elementy nawigacyjne w obrębie całej aplikacji, musimy je
potraktować nieco inaczej.

Pozostałe trzy opcje — Pizze, Makarony oraz Restauracje — umieścimy w szufladzie


nawigacyjnej (ang. navigation drawer). Szuflada nawigacyjna to wysuwany panel
zawierający odnośniki do głównych części aplikacji. Te główne części aplikacji są
określane jako jej kluczowe węzły i zazwyczaj odgrywają najważniejszą rolę
w poruszaniu się po niej; przeważnie są nimi ekrany głównego poziomu i ekrany
kategorii.
...

Włoskie Co Nieco

Ekran główny
To jest szuflada
nawigacyjna. Zawiera Po kliknięciu elementu szuflady
ona listę kluczowych Pizze nawigacyjnej odpowiadająca mu
węzłów aplikacji. zawartość jest wyświetlana
w tym miejscu ekranu.
Makarony

Restauracje

398 Rozdział 10.


Szuflady nawigacyjne

Szuflady nawigacyjne bez tajemnic


Do implementacji szuflad nawigacyjnych używany jest specjalny typ układów
— DrawerLayout. Układ typu DrawerLayout zarządza dwoma widokami:

 Widok zawartości głównej, którym zazwyczaj jest układ FrameLayout, dzięki


czemu w prosty sposób można wyświetlać i zmieniać prezentowane fragmenty.

 Widok szuflady nawigacyjnej, którym zazwyczaj jest ListView.

Domyślnie układ DrawerLayout wyświetla widok prezentujący główną


zawartość aplikacji. Wygląda on bardzo podobnie do normalnej aktywności:

To jest ikona szuflady


nawigacyjnej. Wystarczy
ją kliknąć lub przeciągnąć
palcem, by wyświetlić
zawartość szuflady.
To właśnie tu jest
umieszczona główna
zawartość aplikacji.

Kiedy klikniemy ikonę szuflady nawigacyjnej lub przeciągniemy palcem od


krawędzi ekranu do jego środka, widok zwierający szufladę nawigacyjną
zostanie wysunięty na ekran i częściowo przesłoni prezentowane na nim treści.

To jest szuflada nawigacyjna.


Jak widać, zawiera ona listę
opcji.
Szuflada jest
nasuwana na
prezentowaną
zawartość
główną.

To właśnie zawartość szuflady umożliwia nam poruszanie się po aplikacji.

A jaki to ma wpływ na strukturę naszej aplikacji dla Włoskiego Co Nieco?


jesteś tutaj  399
Struktura aplikacji

Struktura aplikacji dla restauracji Włoskie Co Nieco


Planujemy zmienić aktywność MainActivity w taki sposób, by korzystała z szuflady
nawigacyjnej. Będzie ona zawierała układ FrameLayout do wyświetlania fragmentów
i widok listy do prezentowania listy opcji.

Widok listy będzie zawierał opcje Ekran główny, Pizze, Makarony oraz Restauracje, dzięki
którym użytkownik będzie mógł w prosty sposób poruszać się pomiędzy głównymi węzłami
aplikacji. Dla każdej z tych opcji utworzymy następnie odrębny fragment. Oznacza to,
że będziemy mogli podmieniać fragmenty w trakcie działania aplikacji, a użytkownik będzie
w stanie uzyskać dostęp do szuflady nawigacyjnej z każdego ekranu aplikacji.

Włoskie Głównymi węzłami aplikacji są fragmenty, więc


Pizze
Co Nieco możemy wyświetlać jeden z nich, w zależności Diavolo
od opcji klikniętej przez użytkownika.
Top Fragment Funghi

Włoskie Co Nieco

...
Ekran główny

Pizze
TopFragment PizzasFragment
Makarony
Makarony Restauracje
Spaghetti Restauracje Wrocław
bolognese Kraków
Lasagne

MainActivity
PastaFragment StoresFragment

Oto czynności, które wykonamy w ramach wprowadzania tych modyfikacji:

1 Utworzymy fragmenty dla poszczególnych głównych węzłów aplikacji.


Dodanie szuflady
2 Utworzymy i zainicjujemy szufladę nawigacyjną.
Szuflada ta będzie zawierała widok ListView prezentujący listę opcji. Spokojnie nawigacyjnej
wymaga napisania
3 Zapewnimy, by widok ListView reagował na kliknięcia elementów. sporej ilości kodu.
Dzięki temu użytkownik będzie mógł przechodzić do głównych węzłów aplikacji.
Na omówienie sposobu dodawania
4 Dodamy ActionBarDrawerToggle. szuflady nawigacyjnej poświęcimy cały
Dzięki temu elementowi użytkownik będzie mógł kontrolować szufladę ten rozdział, a na samym jego końcu
przy użyciu paska akcji, a aktywność będzie mogła reagować na zdarzenia pokażemy kompletny kod aktywności
otwierania i zamykania szuflady. MainActivity.
Zaczniemy od utworzenia fragmentów.

400 Rozdział 10.


Szuflady nawigacyjne

Utworzenie fragmentu TopFragment ¨  Dodanie fragmentów


¨  Utworzenie szuflady
¨  Obsługa kliknięcia ListView
Fragmentu TopFragment użyjemy do prezentowania zawartości najwyższego ¨  Dodanie ActionBarDrawerToggle
poziomu. Na razie ograniczmy się jednak do wyświetlenia tekstu „Fragment
TopFragment”, dzięki czemu będziemy wiedzieli, który fragment jest aktualnie
widoczny. A zatem utwórz pusty fragment, nadaj mu nazwę TopFragment, Wszystkie nasze fragmenty utworzymy
a używany przez niego plik układu nazwij fragment_top. na bazie pustych fragmentów, gdyż
i tak w całości będziemy zastępować kod
wygenerowany przez Android Studio.
Oto zawartość pliku TopFragment.java:

package com.hfad.wloskieconieco;
WloskieCoNieco
import android.os.Bundle;
import android.app.Fragment; app/src/main
import android.view.LayoutInflater;
import android.view.View; java
Nasz TopFragment jest
import android.view.ViewGroup; zwyczajnym fragmentem.
com.hfad.wloskieconieco
public class TopFragment extends Fragment {
TopFragment.java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_top, container, false);
}
}

Oprócz tego dodaj do pliku strings.xml następujący zasób łańcuchowy


gs.xml.
(użyjemy go w układzie fragmentu): Dodaj ten łańcuch do pliku strin
my go do ukła du, aby wied zieć,
Doda t
<string name=”title_top”>Fragment TopFragment</string> kiedy będzie wyświetlany fragmen
TopFragment.

Oto kod pliku fragment_top.xml:


<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
WloskieCoNieco
android:layout_height=”match_parent”
tools:context=”.MainActivity”>
app/src/main
<TextView
android:text=”@string/title_top”
res
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” /> layout
</RelativeLayout> <xml>
</xml>

fragment_top.xml

jesteś tutaj  401


Utworzenie fragmentu PizzaFragment

Utworzenie fragmentu PizzaFragment ¨  Dodanie fragmentów


¨  Utworzenie szuflady
¨  Obsługa kliknięcia ListView
Do wyświetlania listy pizz użyjemy fragmentu PizzaFragment typu ListFragment. ¨  Dodanie ActionBarDrawerToggle
A zatem utwórz pusty fragment o nazwie PizzaFragment, jednocześnie usuń
zaznaczenie pola wyboru pozwalającego na utworzenie pliku układu. Rezygnujemy
z tworzenia układu dlatego, że fragmenty typu ListFragment używają swoich
własnych układów.

Następnie dodaj do pliku strings.xml nową tablicę łańcuchów o nazwie pizzas


(zapiszemy w niej nazwy dostępnych pizz):

<string-array name=”pizzas”>
<item>Diavolo</item>
Dodajemy tablicę z nazwami pizz
<item>Funghi</item> do pliku strings.xml.
</string-array>

Następnie zmodyfikuj kod w pliku PizzaFragment.java w taki sposób,


by tworzony fragment dziedziczył po klasie ListFragment. Lista
wyświetlana w tym fragmencie powinna zostać wypełniona nazwami pizz.
Oto kod tego pliku:

package com.hfad.wloskieconieco; WloskieCoNieco

import android.app.ListFragment; app/src/main


import android.os.Bundle;
import android.view.LayoutInflater; java
import android.view.View;
import android.view.ViewGroup; Do wyświetlenia listy pizz com.hfad.wloskieconieco
użyjemy fragmentu typu
import android.widget.ArrayAdapter; ListFragment.
PizzaFragment.java
public class PizzaFragment extends ListFragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
inflater.getContext(),
android.R.layout.simple_list_item_1,
getResources().getStringArray(R.array.pizzas));
setListAdapter(adapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
}

402 Rozdział 10.


Szuflady nawigacyjne

Utworzenie fragmentu PastaFragment


Fragmentu PastaFragment typu ListFragment użyjemy do wyświetlania listy
dań z makronu. Aby przygotować ten fragment, utwórz pusty fragment i nadaj mu
nazwę PastaFragment. Nie zapomnij także o usunięciu znaczników z pola wyboru
pozwalającego na utworzenie pliku układu, gdyż fragmenty typu ListFragment
dysponują własnym układem.

Następnie do pliku strings.xml dodaj tablicę łańcuchów o nazwie pasta


(będzie ona zawierała nazwy dań z makaronu):

<string-array name=”pasta”>
<item>Spaghetti bolognese</item> Dodajemy do pliku strings.xml tabli
z nazwami dań z makaronu. cę
<item>Lasagne</item>
</string-array>

Następnie zmień kod w pliku PastaFragment.java w taki sposób, by klasa


PastaFragment dziedziczyła po ListFragment. Widok ListView tego fragmentu
powinien zostać wypełniony nazwami dań z makaronu. Oto kod tej klasy:

package com.hfad.wloskieconieco;
WloskieCoNieco
import android.app.ListFragment;
import android.os.Bundle; app/src/main
import android.view.LayoutInflater;
import android.view.View; java
Fragmentu typu ListFragment
import android.view.ViewGroup; użyjemy do wyświetlenia listy
import android.widget.ArrayAdapter; dań z makaronu. com.hfad.wloskieconieco

public class PastaFragment extends ListFragment { PastaFragment.java

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
inflater.getContext(),
android.R.layout.simple_list_item_1,
getResources().getStringArray(R.array.pasta));
setListAdapter(adapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
}

jesteś tutaj  403


Utworzenie fragmentu StoresFragment

Utworzenie fragmentu StoresFragment ¨  Dodanie fragmentów


¨  Utworzenie szuflady
¨  Obsługa kliknięcia ListView
Fragmentu StoresFragment typu ListFragment użyjemy do wyświetlania ¨  Dodanie ActionBarDrawerToggle
listy restauracji. Aby przygotować ten fragment, utwórz pusty fragment i nadaj
mu nazwę StoresFragment. Nie zapomnij także o usunięciu znaczników
z pola wyboru pozwalającego na utworzenie pliku układu, gdyż fragmenty typu
ListFragment dysponują własnym układem.

Następnie do pliku strings.xml dodaj tablicę łańcuchów o nazwie stores


(będzie ona zawierała nazwy miast, w których są restauracje Włoskie Co Nieco):

<string-array name=”stores”>
<item>Wrocław</item> Dodajemy do pliku strings.xml tabli
z nazwami miast, w których są cę
<item>Kraków</item> restauracje.
</string-array>

Następnie zmień kod w pliku StoresFragment.java w taki sposób, by klasa


StoresFragment dziedziczyła po ListFragment. Widok ListView tego
fragmentu powinien zostać wypełniony nazwami miast. Oto kod tej klasy:

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.app.ListFragment;
import android.os.Bundle; app/src/main
import android.view.LayoutInflater;
import android.view.View; Fragmentu typu ListFragment java
użyjemy do wyświetlenia
import android.view.ViewGroup; listy miast, w których są
restauracje. com.hfad.wloskieconieco
import android.widget.ArrayAdapter;

StoresFragment.java
public class StoresFragment extends ListFragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
inflater.getContext(),
android.R.layout.simple_list_item_1,
getResources().getStringArray(R.array.stores));
setListAdapter(adapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
}

404 Rozdział 10.


Szuflady nawigacyjne

Dodanie układu DrawerLayout


Teraz zajmiemy się zmianą układu używanego przez aktywność główną, MainActivity,
tak by używała ona układu typu DrawerLayout. Zgodnie z tym, o czym wspominaliśmy
już wcześniej, będzie on zawierał układ FrameLayout, którego użyjemy do wyświetlania
fragmentów, i widok ListView prezentujący zawartość szuflady nawigacyjnej.

Układ DrawerLayout można utworzyć, używając następującego kodu:


Nasz układ korzysta z układu DrawerLayout
pochodzącego z biblioteki wsparcia v4. Biblioteka
<android.support.v4.widget.DrawerLayout appcompat v7 zawiera bibliotekę wsparcia v4.
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/drawer_layout”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
Układu FrameLayout będziemy używać
<FrameLayout do wyświetlania fragmentów.
android:layout_width=”match_parent”
android:layout_height=”match_parent”
... />
Element ListView opisuje szufladę
nawigacyjną.
<ListView
android:layout_width=”240dp”
android:layout_height=”match_parent”
... />
</android.support.v4.widget.DrawerLayout>
Szufladę
Elementem głównym naszego nowego układu jest DrawerLayout. To zrozumiałe, nawigacyjną
gdyż musi on kontrolować wszystko, co dzieje się na ekranie. Klasa DrawerLayout zdefiniowaliśmy
za pomocą
pochodzi z biblioteki Support Library v4, dlatego zastosujemy jej pełną nazwę: widoku ListView.
android.support.v4.widget.DrawerLayout. Po wysunięciu
powinna ona
częściowo
Pierwszy element umieszczony w układzie DrawerLayout będzie służył do przesłonić
wyświetlania zawartości. W naszym przypadku będzie to układ FrameLayout, pozostałą
w którym będziemy wyświetlali poszczególne fragmenty. Chcemy, aby był zawartość ekranu.
on możliwie jak największy, dlatego obu jego atrybutom — layout_width
i layout_height — przypisaliśmy wartość ”match_parent”.

Drugim elementem układu DrawerLayout jest sama szuflada nawigacyjna.


Jeśli utworzymy ją za pomocą widoku ListView, to będzie ona zawierała listę
opcji. Zazwyczaj będziemy chcieli, aby ta szuflada wysuwała się w poziomie Zawartość ekranu jest
i częściowo przykrywała dotychczasową zawartość ekranu. Dlatego atrybutowi wyświetlana w układzie
layout_height tego widoku przypisaliśmy wartość ”match_parent”, natomiast FrameLayout. Chcemy, by
wypełniała ona cały dostępny
w atrybucie layout_width podaliśmy stałą szerokość. obszar ekranu. W tym przykładzie
jest ona częściowo przesłonięta
Kompletny kod pliku activity_main.xml przedstawiamy na następnej stronie. przez szufladę nawigacyjną.
jesteś tutaj  405
Kod układu

Kompletna zawartość pliku activity_main.xml ¨  Dodanie fragmentów


¨  Utworzenie szuflady
¨  Obsługa kliknięcia ListView
Oto kompletna zawartość pliku układu activity_main.xml: ¨  Dodanie ActionBarDrawerToggle

<android.support.v4.widget.DrawerLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/drawer_layout”
android:layout_width=”match_parent”
android:layout_height=”match_parent”> WloskieCoNieco

Fragmenty będą app/src/main


<FrameLayout wyświetlane
w układzie
android:id=”@+id/content_frame” FrameLayout. res
android:layout_width=”match_parent”
android:layout_height=”match_parent” /> layout
<xml>
</xml>
<ListView android:id=”@+id/drawer” activity_main.xml
To jest szerokość szuflady.
android:layout_width=”240dp”
Szufladę
nawigacyjną android:layout_height=”match_parent”
opisuje Ten atrybut określa, gdzie należy umieścić szufladę.
android:layout_gravity=”start”
widok
Ten atrybut określa, że w danej chwili
ListView. android:choiceMode=”singleChoice” można wybrać tylko jeden element listy.
android:divider=”@android:color/transparent”
android:dividerHeight=”0dp” W tych wierszach wyłączamy linie oddzielające
poszczególne elementy listy i określamy kolor
android:background=”#ffffff”/> tła szuflady.
</android.support.v4.widget.DrawerLayout>

Zwróć baczną uwagę na ustawienia podane w elemencie <ListView>, gdyż


najprawdopodobniej wszystkie szuflady nawigacyjne będą wyglądały podobnie.
Jeśli Twój projekt 

Obejrzyj to!
Do określenia wymiarów szuflady używamy atrybutów layout_width nie zawiera 
i layout_height. Pierwszemu z nich przypisaliśmy szerokość ”240dp”, zależności 
dzięki czemu po otworzeniu szuflada będzie miała szerokość 240 dp. od biblioteki wsparcia 
appcompat v7, to 
Przypisanie atrybutowi layout_gravity wartości ”start” sprawi, że w razie przedstawiony w tym 
wyboru języka, w którym znaki są zapisywane od lewej do prawej, szuflada zostanie rozdziale kod szuflady 
umieszczona po lewej stronie ekranu, a w przypadku języków, w których znaki są nawigacyjnej nie będzie 
działał.
zapisywane od prawej do lewej, zostanie ona umieszczona po prawej stronie.
W Android Studio zależnościami
Atrybuty divider, dividerHeight oraz backgroud zastosowaliśmy, aby ukryć
możesz zarządzać, wybierając
linie oddzielające poszczególne elementy listy i określić kolor jej tła.
z menu opcje File/Project
I wreszcie przypisanie atrybutowi choiceMode wartości ”singleChoice” sprawi, Structure/App/Dependencies.
że w danym momencie użytkownik będzie mógł wybrać tylko jeden element listy.

406 Rozdział 10.


Szuflady nawigacyjne

Inicjalizacja listy szuflady nawigacyjnej


Skoro już dodaliśmy do pliku activity_main.xml układ DrawerLayout, musimy
teraz określić jego zachowanie w pliku MainActivity.java. Pierwszą rzeczą,
którą należy zrobić, jest dodanie opcji do widoku listy. W tym celu dodamy
do pliku strings.xml kolejną tablicę łańcuchów. Następnie użyjemy adaptera
tablicowego do określenia opcji listy.

Poniżej przedstawiliśmy tablicę łańcuchów znaków, którą musimy dodać do


pliku zasobów strings.xml (każdy element tej tablicy określa fragment,
który zostanie wyświetlony po kliknięciu danego elementu listy):
WloskieCoNieco
<string-array name=”titles”>
To są opcje, które zos
<item>Ekran główny</item> wyświetlone w szufladtaną app/src/main
nawigacyjnej. Dodaj tęzie
<item>Pizze</item> do pliku strings.xml. tablicę
<item>Makarony</item> res
<item>Resatuaracje</item>
</string-array> values
<xml>
</xml>
Zawartość listy określimy w kodzie pliku MainActivity.java, a konkretnie
strings.xml
w metodzie onCreate(). Tablicę łańcuchów znaków i widok listy zapiszemy
w zmiennych prywatnych, gdyż będą nam one jeszcze potrzebne w innych
miejscach kodu. Oto kod odpowiedzialny za wypełnienie listy:
...
import android.widget.ArrayAdapter; Używamy tych klas, WloskieCoNieco
więc musimy je
import android.widget.ListView; zaimportować.
app/src/main
public class MainActivity extends Activity {
... java
private String[] titles; Tych danych będziemy także
potrzebowali w innych metodach,
private ListView drawerList; więc zapiszemy je w prywatnych com.hfad.wloskieconieco
zmiennych klasowych.
@Override MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
...
titles = getResources().getStringArray(R.array.titles);
drawerList = (ListView)findViewById(R.id.drawer); Do określenia zawartości
widoku listy użyjemy
drawerList.setAdapter(new ArrayAdapter<String>(this, adaptera ArrayAdapter.
android.R.layout.simple_list_item_activated_1, titles));
}
... Zastosowanie simple_list_item_activated_1
oznacza, że element kliknięty przez użytkownika
} ma być wyróżniony.

Skoro już określiliśmy opcje wyświetlane na liście szuflady nawigacyjnej,


zadbajmy o to, by lista ta reagowała na kliknięcia.
jesteś tutaj  407
Reagowanie na kliknięcia

Zastosowanie OnItemClickListener, aby zapewnić ¨  Dodanie fragmentów


¨  Utworzenie szuflady
reagowanie na kliknięcia elementów listy ¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
Aby lista reagowała na kliknięcia, zastosujemy to samo rozwiązanie, którego
użyliśmy już w rozdziale 6. — zaimplementujemy interfejs OnItemClickListener.
Innymi słowy: utworzymy obiekt nasłuchujący, zaimplementujemy jego metodę
onItemClick(), a następnie przypiszemy ten obiekt nasłuchujący do widoku listy. WloskieCoNieco
Oto kod, który realizuje te operacje:

... app/src/main
Używamy tych klas,
import android.view.View; więc musimy je
import android.widget.AdapterView; zaimportować. java

com.hfad.wloskieconieco
public class MainActivity extends Activity { Ta klasa opisuje nasz obiekt
... nasłuchujący typu OnItemClickL
istener.
MainActivity.java
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
// Kod, który należy wykonać po kliknięciu elementu listy
}
}; Kiedy użytkownik kliknie któryś z elementów w szufladzie
nawigacyjnej, zostanie wywołana metoda onItemClick().

@Override
protected void onCreate(Bundle savedInstanceState) {
...
drawerList.setOnItemClickListener(new DrawerItemClickListener());
}
}; Do widoku ListView w szufladzie nawigacyjnej dodajemy
nową instancję obiektu OnItemClickListener.

Metoda onItemClick() musi zawierać kod, który ma zostać wykonany, kiedy użytkownik kliknie
któryś z elementów widoku listy. W naszym przypadku w odpowiedzi na kliknięcie wywołamy
metodę selectItem(), przekazując do niej pozycję klikniętego elementu na liście. Już zaraz
zajmiemy się zaimplementowaniem tej metody.

Metoda selectItem() będzie musiała wykonywać trzy operacje:

 Podmienić fragment wyświetlany w układzie FrameLayout.

 Zmienić tytuł wyświetlany na pasku akcji tak, by odpowiadał nowemu fragmentowi.


 Zamknąć szufladę nawigacyjną.

Obecnie wiesz już wszystko, co jest niezbędne do zaimplementowania pierwszej z tych operacji,
dlatego zajmiesz się tym samodzielnie w ramach ćwiczenia przedstawionego na następnej stronie.

408 Rozdział 10.


Szuflady nawigacyjne

Magnesiki z kodem Ekran główny

Kiedy użytkownik kliknie element wyświetlony na liście w szufladzie Pizze


nawigacyjnej, wówczas w układzie FrameLayout o identyfikatorze
content_frame musi zostać wyświetlony odpowiedni fragment. Makarony
Przekonajmy się, czy potrafisz uzupełnić poniższy kod. Restauracje
To jest widok ListView
prezentujący listę
private void selectItem(int position) { elementów.
Fragment fragment;
beginTrans
action()
switch(..................) {
case 1:
new position
fragment = ............................;
break; PizzaFragment()
case 2:
TopFragment()
fragment = ............................;
break; new fragment
case 3:

StoresFragment()
fragment = ............................;
break; PastaFragment()
default:

new commit()
fragment = ............................;
}
new

FragmentTransaction ft = getFragmentManager(). ......................;

ft.replace(R.id.content_frame, .................);

ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

ft. .................;
}

jesteś tutaj  409


Rozwiązanie magnesików

Magnesiki z kodem. Rozwiązanie Ekran główny

Kiedy użytkownik kliknie element wyświetlony na liście w szufladzie Pizze


nawigacyjnej, wówczas w układzie FrameLayout o identyfikatorze
content_frame musi zostać wyświetlony odpowiedni fragment. Makarony
Przekonajmy się, czy potrafisz uzupełnić poniższy kod. Restauracje
To jest widok ListView
prezentujący listę
elementów.
private void selectItem(int position) {
Fragment fragment;
Sprawdzamy pozycję klikniętego elementu
na liście ListView szuflady nawigacyjnej.
position
switch(... ..) {
case 1: Tworzymy fragment, którego
typ jest zależny od pozycji
new ) klikniętego elementu. Na
fragment = .. PizzaFragment( ..; przykład jeśli użytkownik
kliknął element Pizze, to
break; tworzymy fragment klasy
case 2: PizzaFragment.

new PastaFragment()
fragment = . .;
break;
case 3:

fragment = new .StoresFragment() .;


break;
default:
Domyślnie tworzymy fragment
TopFragment() klasy TopFragment.
fragment = . new . ;
}

beginTrans
FragmentTransaction ft = getFragmentManager(). . action() ..;

ft.replace(R.id.content_frame, .. fragment .); Rozpoczynamy transakcję


fragmentu, która podmieni
aktualnie prezentowany fragmen
t.
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

Zatwierdzamy transakcję.
ft. . commit() ..;
}

410 Rozdział 10.


Szuflady nawigacyjne

Metoda selectItem() w obecnej postaci WloskieCoNieco


Poniżej przedstawiliśmy zmodyfikowany kod pliku MainActivity.java
(kiedy zostanie kliknięty jeden z elementów prezentowanych w szufladzie app/src/main
nawigacyjnej, wywoływana jest metoda selectItem(), która wyświetla
odpowiedni fragment): java
...
com.hfad.wloskieconieco
import android.app.Fragment;
import android.app.FragmentTransaction;
MainActivity.java
public class MainActivity extends Activity {
...
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,long id){
selectItem(position); my
Po kliknięciu elementu wywołuje
} metodę selectItem().
};

Sprawdzamy pozycję klikniętego elementu na liście.


private void selectItem(int position) {
Fragment fragment;
switch(position) {
case 1: Na podstawie pozycji klikniętego
fragment = new PizzaFragment(); na liście określamy typ tworzone elementu
go
fragmentu. Na przykład opcja „Piz
break; znajduje się na pozycji 1, więc ze”
case 2: w przypadku jej kliknięcia utwo
rzymy
fragment PizzaFragment.
fragment = new PastaFragment();
break;
case 3:
fragment = new StoresFragment();
break;
default:
Domyślnie tworzymy fragment TopFragment.
fragment = new TopFragment();
}
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
Używamy transakcji, by podmienić
} aktualnie wyświet lany fragment.
}

Skoro metoda selectItem() wyświetla już odpowiedni fragment,


zajmiemy się zmianą tytułu paska akcji.
jesteś tutaj  411
Zmiana tytułu

Zmiana tytułu paska akcji ¨  Dodanie fragmentów


¨  Utworzenie szuflady
Oprócz podmieniania wyświetlanego fragmentu musimy także zmieniać tytuł
¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
wyświetlany na pasku akcji, tak by odpowiadał on aktualnie prezentowanemu
fragmentowi. Chcemy, by na pasku akcji domyślnie była wyświetlana nazwa aplikacji,
jeśli jednak użytkownik kliknie na przykład opcję Pizze, to będziemy chcieli zmienić
tytuł paska akcji na „Pizze”. Dzięki temu użytkownik będzie wiedział, w którym
miejscu aplikacji aktualnie się znajduje.

Implementując to rozwiązanie, wykorzystamy pozycję klikniętego elementu listy


i na jej podstawie określimy tekst, który zostanie wyświetlony na pasku akcji.
Następnie zmienimy tytuł paska, używając metody setTitle() klasy ActionBar.
Cały kod realizujący te operacje umieścimy w osobnej metodzie, gdyż będziemy jej
potrzebowali także w innych miejscach aplikacji. Oto implementacja zmiany tytułu
paska akcji:

private void selectItem(int position) {


...
WloskieCoNieco
// Ustawiamy tytuł paska akcji Wywołujemy metodę
setActionBarTitle(position); setActionBarTitle(),
przekazując do niej pozycję app/src/main
} klikniętego elementu listy.
java
private void setActionBarTitle(int position) {
String title; Jeśli użytkownik kliknie opcję „Ekran główny”, com.hfad.wloskieconieco
to na pasku akcji wyświetlamy tytuł aplikacji.
if (position == 0){
title = getResources().getString(R.string.app_name); MainActivity.java
} else {
W przeciwnym razie pobieramy łańcuch z tablicy titles,
title = titles[position]; określony na podstawie pozycji klikniętego elementu listy.
}
getActionBar().setTitle(title); To wywołanie wyświetla łańcuch znaków
w tytule paska akcji.
}

Jeśli użytkownik kliknie opcję


„Ekran główny”, w tytule paska Jeśli użytkownik kliknie opcję
akcji wyświetlimy nazwę aplikacji. „Pizze”, w tytule paska akcji
wyświetlimy napis „Pizze”.

412 Rozdział 10.


Szuflady nawigacyjne

Zamykanie szuflady nawigacyjnej


Ostatnią czynnością, którą wykonamy w kodzie metody selectItem(),
będzie zamknięcie szuflady nawigacyjnej. Dzięki temu użytkownik nie
będzie musiał robić tego samodzielnie.
WloskieCoNieco
Aby zamknąć szufladę, musimy pobrać referencję do układu DrawerLayotu
i wywołać jego metodę closeDrawer(). Ta metoda wymaga przekazania app/src/main
tylko jednego argumentu — obiektu View reprezentującego widok
stanowiący szufladę nawigacyjną. W naszym przypadku jest to widok java
ListView prezentujący listę opcji:
com.hfad.wloskieconieco
private void selectItem(int position) {
... Pobieramy referencję do układu DrawerLayout.
// Zamykamy szufladę nawigacyjną MainActivity.java

DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);


drawerLayout.closeDrawer(drawerList); drawerLayout to szuflada nawigacyjna układu
DrawerLayout. To wywołanie nakazuje zamknięcie
} tej szuflady.

Układ DrawerLayout
obejmuje cały ekran.
Zawiera on układ
FrameLayout używany do
wyświetlania zawartości
i widok ListView tworzący
szufladę nawigacyjną.

Musimy nakazać układowi


DrawerLayout zamknięcie
szuflady nawigacyjnej.

Znasz już wszystkie elementy niezbędne do zaimplementowania metody selectItem(),


zobaczmy więc, jak wygląda kompletna implementacja tego rozwiązania i jak jej użyć
w kodzie aktywności MainActivity.

jesteś tutaj  413


Kod aktywności MainActivity

Zaktualizowany kod pliku MainActivity.java ¨  Dodanie fragmentów


¨  Utworzenie szuflady
Oto zaktualizowana zawartość pliku MainActivity.java:
¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle

package com.hfad.wloskieconieco;
WloskieCoNieco
...
DrawerLayout należy app/src/main
import android.support.v4.widget.DrawerLayout; do biblioteki Support
Library v4.
java
public class MainActivity extends Activity {
... Dodajemy DrawerLayout jako zmienną com.hfad.wloskieconieco
prywatną, gdyż będziemy jej używać
private DrawerLayout drawerLayout; w wielu metodach.
MainActivity.java

private class DrawerItemClickListener implements ListView.OnItemClickListener {


@Override
public void onItemClick(AdapterView<?> parent, View view, int position,long id){
// Kod wykonywany po kliknięciu elementu szuflady nawigacyjnej
selectItem(position);
Wywołujemy metodę selectItem().
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
titles = getResources().getStringArray(R.array.titles); Pobieramy referencję do
drawerList = (ListView)findViewById(R.id.drawer); układu DrawerLayout.
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
// Określamy zawartość widoku ListView
drawerList.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_activated_1, titles));
drawerList.setOnItemClickListener(new DrawerItemClickListener());
if (savedInstanceState == null) {
selectItem(0); ała właśnie
Jeśli aktywność MainActivity zost selectItem(),
ia
} utworzona, to używamy wywołan ragment.
by wyświetlić w niej fragment TopF Dalsza część kodu
}
znajduje się na
następnej stronie.

414 Rozdział 10.


Szuflady nawigacyjne

Kod pliku MainActivity.java (ciąg dalszy)


private void selectItem(int position) {
// Aktualizujemy główną zawartość aplikacji, podmieniając prezentowany fragment
Fragment fragment;
switch(position) { Tworzymy odpowiedni fragment.
case 1:
fragment = new PizzaFragment(); WloskieCoNieco
break;
case 2: app/src/main
fragment = new PastaFragment();
break; java
case 3:
com.hfad.wloskieconieco
fragment = new StoresFragment();
break;
default: MainActivity.java
Wyświetlamy fragment, używając
fragment = new TopFragment(); w tym celu transakcji fragmentu.
}
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
// Ustawiamy tytuł paska akcji
Określamy tytuł paska akcji.
setActionBarTitle(position);
// Zamykamy szufladę nawigacyjną
drawerLayout.closeDrawer(drawerList); Zamykamy szufladę nawigacyjną
.
}

private void setActionBarTitle(int position) {


String title; Jeśli użytkownik kliknął opcję „Ekran główny”, to w tytule
paska akcji wyświetlamy nazwę aplikacji.
if (position == 0){
title = getResources().getString(R.string.app_name);
} else {
W przeciwnym razie wyświetlamy łańcuch z tablicy titles,
title = titles[position]; określony na podstawie pozycji klikniętego elementu listy.
}
getActionBar().setTitle(title); Określamy tytuł paska akcji.
} Pominęliśmy tutaj metody onCreateOptionsMenu()
... i onOptionsItemSelected() z wcześniejszego kodu
aktywności MainActivity, gdyż nie wprowadzaliśmy
} w nich żadnych zmian.

jesteś tutaj  415


Przypadki otwarcia i zamknięcia

Zamykanie i otwieranie szuflady ¨  Dodanie fragmentów


¨  Utworzenie szuflady
Do tej pory dodaliśmy szufladę nawigacyjną do aktywności
¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
MainActivity, dodaliśmy do jej listy główne węzły aplikacji oraz
zadbaliśmy o to, by szuflada reagowała na zdarzenia kliknięcia.
Następną sprawą, którą się zajmiemy, będzie poznanie sposobów
otwierania i zamykania szuflady oraz reagowania na jej stan.
Nie sposób oprzeć
Można wskazać kilka powodów, dla których moglibyśmy chcieć Spokojnie się wrażeniu, że
reagować na stan szuflady nawigacyjnej. Przede wszystkim możemy implementacja
chcieć zmieniać tytuł wyświetlany na pasku akcji w momencie szuflady nawigacyjnej
otwierania i zamykania szuflady. Na przykład kiedy szuflada będzie wymaga dużego nakładu pracy.
otworzona, możemy wyświetlać na pasku akcji nazwę aplikacji,
Choć kod tego rozwiązania wydaje się dosyć
a następnie po wyświetleniu wybranego fragmentu i zamknięciu
złożony, wystarczy go zastosować i wszystko
szuflady — tytuł fragmentu.
będzie dobrze.
Kolejny powód jest powiązany z elementami wyświetlanymi na pasku
akcji. Możemy uznać za stosowne, by po otworzeniu szuflady ukryć
wszystkie lub niektóre z nich, tak by użytkownik mógł z nich korzystać
wyłącznie wtedy, kiedy szuflada będzie zamknięta.

Na kilku następnych stronach pokażemy Ci, jak przygotować obiekt


nasłuchujący DrawerListener, który pozwoli nasłuchiwać zdarzeń
związanych z szufladą nawigacyjną i je obsługiwać. Zastosujemy je
do ukrywania akcji Udostępnij na pasku akcji w momencie otwierania
szuflady i do jej wyświetlania, gdy szuflada zostanie zamknięta.

Szufladę
otwieramy
i zamykamy,
używając
przycisku
umieszczonego
na pasku akcji.

Kiedy szuflada zostanie


zamknięta, wyświetlimy
akcję Udostępnij na pasku Kiedy szuflada
akcji. zostanie
otworzona,
ukryjemy akcję
Udostępnij.

416 Rozdział 10.


Szuflady nawigacyjne

Stosowanie ActionBarDrawerToggle
Najlepszym sposobem na utworzenie obiektu nasłuchującego DrawerListener
jest skorzystanie z przycisku klasy ActionBarDrawerToggle. Jest to
specjalny rodzaj komponentu implementującego interfejs DrawerListener
przystosowany do wykorzystania na pasku akcji. Pozwala on nasłuchiwać
zdarzeń generowanych przez układ DrawerLayout, jak robią to wszystkie
obiekty nasłuchujące DrawerListener, oraz otwierać i zamykać szufladę
poprzez klikanie ikony umieszczonej na pasku akcji.

Zaczniemy od utworzenia w pliku strings.xml dwóch zasobów łańcuchowych WloskieCoNieco


opisujących akcje otwarcia i zamknięcia szuflady; będą nam one potrzebne
ze względu na zapewnienie odpowiedniej dostępności aplikacji: app/src/main

<string name=”open_drawer”>Otwórz szufladę</string>


Dodaj te zasoby do pliku res
<string name=”close_drawer”>Zamknij szufladę</string>
strings.xml. Będą one
potrzebne dla przycisku
Następnie utwórz obiekt ActionBarDrawerToggle. W tym celu należy values
ActionBarDrawerToggle.
wywołać jego konstruktor i przekazać do niego cztery argumenty: <xml>
</xml>
obiekt Context (zazwyczaj jest to referencja this reprezentująca strings.xml
bieżący kontekst), obiekt DrawerLayout oraz dwa zasoby łańcuchowe.
Potem musisz przesłonić dwie metody klasy ActionBarDrawerToggle:
onDrawerClosed() i onDrawerOpened(): Tworzymy obiekt klasy
ActionBarDrawerToggle.

ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,


R.string.open_drawer, R.string.close_drawer) {
// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu zamknięciu
@Override
public void onDrawerClosed(View view) { Ta metoda jest wywoływana,
super.onDrawerClosed(view); kiedy szuflada zostanie zamknięta.

// Kod, który ma zostać wywołany, gdy szuflada zostanie zamknięta


} Ta metoda jest wywoływana, kiedy szuflada zostanie otworzona.
// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu otworzeniu
@Override
public void onDrawerOpened(View drawerView) { WloskieCoNieco
super.onDrawerOpened(drawerView);
// Kod, który ma zostać wywołany, gdy szuflada zostanie otworzona app/src/main
}
}; java

Po utworzeniu obiektu ActionBarDrawerToggle należy przekazać go do com.hfad.wloskieconieco


układu DrawerLayout, wywołując jego metodę setDrawerListener():

drawerLayout.setDrawerListener(drawerToggle); To wywołanie ustawia obiekt MainActivity.java


ActionBarDrawerToggle jako obiekt
nasłuchujący zdarzeń układu
DrawerLayout. jesteś tutaj  417
Metoda invalidateOptionsMenu()

Modyfikowanie elementów paska akcji ¨  Dodanie fragmentów


¨  Utworzenie szuflady
w trakcie działania aplikacji ¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
Jeśli na pasku akcji znajdują się przyciski przeznaczone do użycia z konkretnym
fragmentem, to możemy zdecydować się na ukrywanie ich w momencie
otwierania szuflady i ponowne wyświetlanie po zamknięciu szuflady. Chcąc
wprowadzać takie modyfikacje paska akcji, musimy wykonać dwie operacje.
Pierwszą z nich jest wywołanie metody invalidateOptionsMenu(). W ten
sposób informujemy system, że elementy, które mają się znajdować na pasku
akcji, uległy zmianie i należy je odtworzyć. WloskieCoNieco

W efekcie wywołania metody invalidateOptionsMenu() wywoływana


jest metoda onPrepareOptionsMenu() aktywności. Możemy ją przesłonić, app/src/main
by określić, jak mają się zmienić elementy paska akcji.
java
W naszej aplikacji chcemy zmienić widoczność akcji Udostępnij w zależności
od stanu szuflady nawigacyjnej. Aby to zrobić, w metodach onDrawerClosed()
com.hfad.wloskieconieco
i onDrawerOpened() obiektu ActionBarDrawerToggle musimy wywoływać
metodę invalidateOptionsMenu():
MainActivity.java
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
invalidateOptionsMenu();
} Metoda invalidateOptionsMenu() informuje system
o tym, że należy odtworzyć element menu.
W naszym przypadku chcemy zmieniać widoczność
public void onDrawerOpened(View drawerView) { opcji Udostępnij w zależności od tego, czy szuflada
będzie otwarta czy zamknięta, dlatego wywołujemy
super.onDrawerOpened(drawerView); ją w metodach onDrawerOpened()
invalidateOptionsMenu(); i onDrawerClosed().
}

Następnie musimy zaimplementować metodę onPrepareOptionsMenu()


i określić w niej widoczność akcji Udostępnij:

// Ta metoda jest wywoływana po każdym wywołaniu metody invalidateOptionsMenu()


@Override
public boolean onPrepareOptionsMenu(Menu menu) { Metoda onPrepareOptionsMenu()
jest wywoływana za każdym raze
// Jeśli szuflada jest otworzona, ukrywamy elementy akcji związane gdy zostanie wywołana metoda m,
invalidateOptionsMenu().
// z prezentowaną zawartością
boolean drawerOpen = drawerLayout.isDrawerOpen(drawerList);
menu.findItem(R.id.action_share).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
} Akcję Udostępnij ukrywamy, jeśli szuflada
jest widoczna (przekazując w wywołaniu
metody setVisibility() wartość false),
Na następnej stronie przedstawimy cały kod związany z określaniem i wyświetlamy, jeśli szuflada jest zamknięta
widoczności elementów paska akcji. (przekazując w wywołaniu wartość true).

418 Rozdział 10.


Szuflady nawigacyjne

Zaktualizowany kod aktywności MainActivity


Oto zmodyfikowana zawartość pliku MainActivity.java:

...
import android.support.v7.app.ActionBarDrawerToggle; WloskieCoNieco

public class MainActivity extends Activity { Klasa ActionBarDrawerToglle app/src/main


... należy do biblioteki appcompat v7.
private ActionBarDrawerToggle drawerToggle; java
Definiujemy zmienną prywatą, gdyż będziemy jej używać
@Override w wielu metodach. com.hfad.wloskieconieco
protected void onCreate(Bundle savedInstanceState) {
...
MainActivity.java
// Tworzymy obiekt ActionBarDrawerToggle
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
R.string.open_drawer, R.string.close_drawer) {
// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu zamknięciu
@Override
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
Metodę invalidateOptionsMenu() wywołujemy
invalidateOptionsMenu(); po każdym zamknięciu i otworzeniu szuflady.
}
// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu otworzeniu
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
invalidateOptionsMenu();
Ustawiamy ActionBarDrawerToggle jako obiekt
} nasłuchujący układu DrawerLayout.
};
drawerLayout.setDrawerListener(drawerToggle);
}

// Ta metoda jest wywoływana po każdym wywołaniu metody invalidateOptionsMenu()


@Override
public boolean onPrepareOptionsMenu(Menu menu) { Określamy widocz
Udostępnij w za ność akcji
// Jeśli szuflada jest otworzona, ukrywamy elementy akcji związane leż
od tego, czy szuf ności
lada jes
// z prezentowaną zawartością ot wo rzo na czy zamknięta t
.
boolean drawerOpen = drawerLayout.isDrawerOpen(drawerList);
menu.findItem(R.id.action_share).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
...
}

jesteś tutaj  419


Włączenie przycisku W górę

Włączenie możliwości otwierania i zamykania ¨  Dodanie fragmentów


¨  Utworzenie szuflady
szuflady nawigacyjnej ¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
Dodaliśmy już szufladę nawigacyjną do aktywności MainActivity, dodaliśmy opcje do
jej listy, zapewniliśmy, że aktywność reaguje na kliknięcia opcji, oraz dowiedzieliśmy ActionBarDrawerToggle pozwala używać
się, jak ukrywać elementy wyświetlane na pasku akcji po otworzeniu szuflady. Ostatnią przycisku W górę do otwierania
i zamykania szuflady nawigacyjnej.
rzeczą, jaką zrobimy, będzie zapewnienie użytkownikowi możliwości otwierania
i zamykania szuflady nawigacyjnej za pomocą ikony umieszczonej na pasku akcji.
Jak już zaznaczyliśmy wcześniej, ta funkcjonalność jest jedną z zalet stosowania
komponentu ActionBarDrawerToggle. Aby z niej skorzystać, musimy dodać do
aktywności trochę nowego kodu. Najpierw pokażemy Ci po kolei poszczególne
modyfikacje, które należy wprowadzić, a następnie, na samym końcu rozdziału,
przedstawimy kompletny kod aktywności MainActivity.
W pierwszej kolejności zadbamy o wyświetlenie na pasku akcji odpowiedniej ikony.
W tym celu musimy dodać do kodu metody onCreate() dwa poniższe wywołania:

getActionBar().setDisplayHomeAsUpEnabled(true); Włączamy przycisk W górę,


getActionBar().setHomeButtonEnabled(true); by móc używać go do otwierania
i zamykania szuflady.

Te dwa wywołania spowodują, że w aktywności będzie wyświetlany przycisk W górę.


Ponieważ używamy przycisku ActionBarDrawerToggle, przycisk W górę zamiast
nawigowania w górę hierarchii aplikacji będzie powodował otwieranie i zamykanie
szuflady nawigacyjnej.

Następnie musimy zadbać o to, by przycisk ActionBarDrawerToggle reagował na


kliknięcia. W tym celu w metodzie onOptionsItemSelected() aktywności musimy
wywołać metodę onOptionsItemSelected() przycisku. Poniżej pokazaliśmy,
jak należy to zrobić:
@Override
public boolean onOptionsItemSelected(MenuItem item) { Musimy dodać ten fragment kodu
do metody onOptionsItemSelected
if (drawerToggle.onOptionsItemSelected(item)) { aby przycisk ActionBarDrawerToggl(),
e
return true; zaczął reagować na kliknięcia.
}
// Kod obsługujący pozostałe elementy paska akcji
... WloskieCoNieco
}
}
app/src/main
Poniższe wywołanie:
drawerToggle.onOptionsItemSelected(item) java

zwraca wartość true, jeśli komponent ActionBarDrawerToggle obsłużył


com.hfad.wloskieconieco
kliknięcie. Jeżeli wywołanie zwróci wartość false, będzie to oznaczać, że
został kliknięty inny element umieszczony na pasku akcji, a zatem zostanie
wykonany dalszy kod metody onOptinsItemSelected(). MainActivity.java

420 Rozdział 10.


Szuflady nawigacyjne

Synchronizacja stanu przycisku ActionBarDrawerToggle


Musimy jeszcze wprowadzić dwie modyfikacje, by zapewnić poprawne
działanie przycisku ActionBarDrawerToggle.
igacyjna
Pierwszą z nich jest wywołanie metody syncState() przycisku Byłoby super, gdyby szuflada nawmoże.
ActionBarDrawerToggle w metodzie postCreate() aktywności. mogła sama o to zadbać, ale nie
Musimy to zrobić sami.
Metoda syncState() synchronizuje stan ikony szuflady ze stanem
układu DrawerLayout.

Synchronizacja stanu oznacza,


że ikona szuflady ma inną postać,
gdy szuflada jest otworzona,
a inną, gdy jest zamknięta.

Metodę syncState() musimy wywołać w metodzie


onPostCreate() aktywności, aby po utworzeniu aktywności
przycisk ActionBarDrawerToggle był w odpowiednim stanie: Musimy dodać tę metodę do
aktywności, aby stan przycisku
ActionBarDrawerToggle był
@Override zsynchronizowany ze stanem
protected void onPostCreate(Bundle savedInstanceState) { szuflady nawigacyjnej.
super.onPostCreate(savedInstanceState);
// Synchronizumemy stan przycisku przełącznika po wywołaniu metody onRestoreInstanceState
drawerToggle.syncState();
}
WloskieCoNieco

I w końcu, jeśli konfiguracja urządzenia ulegnie zmianie, musimy


przekazać klasie ActionDrawerToggle informacje na jej temat. Możemy app/src/main
to zrobić, wywołując w metodzie onConfigurationChanged() aktywności
metodę onConfigurationChanged() klasy ActionDrawerToggle: java

@Override
com.hfad.wloskieconieco
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
MainActivity.java
drawerToggle.onConfigurationChanged(newConfig);
}
Tę metodę musisz dodać do
aktywności, tak by wszystkie
Na kolejnej stronie pokażemy Ci, w którym miejscu pliku MainActivity.java zmiany konfiguracji były
przekazywane do klasy
należy wprowadzić ostatnie zmiany, a potem sprawdzimy, co się stanie po ActionBarDrawerToggle.
uruchomieniu aplikacji.

jesteś tutaj  421


Kod aktywności MainActivity

Zaktualizowany kod aktywności MainActivity ¨  Dodanie fragmentów


¨  Utworzenie szuflady
Oto zaktualizowana zawartość pliku MainActivity.java:
¨  Obsługa kliknięcia ListView
¨  Dodanie ActionBarDrawerToggle
...
import android.content.res.Configuration; WloskieCoNieco
Musimy zaim
public class MainActivity extends Activity { gdyż używam portować tę klasę, app/src/main
y
onConfiguratio jej w metodzie
... nChanged().
private ActionBarDrawerToggle drawerToggle; java

@Override com.hfad.wloskieconieco
protected void onCreate(Bundle savedInstanceState) {
... MainActivity.java
getActionBar().setDisplayHomeAsUpEnabled(true); Włączamy przycisk W górę,
getActionBar().setHomeButtonEnabled(true); dzięki czemu będzie on
mógł być używany przez
} ActionBarDrawerToggle.

@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
Synchronizujemy stan przycisku
drawerToggle.syncState(); ActionBarDrawerToggle ze stanem
szuflady nawigacyjnej.
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Ten parametr przekazuje wszelkie
drawerToggle.onConfigurationChanged(newConfig); zmiany konfiguracji do przycisku
} ActionBarDrawerToggle.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item)) {
Dzięki temu wywołaniu przycisk
return true; ActionBarDrawerToggle będzie mógł
} obsługiwać kliknięcia.
// Kod obsługujący pozostałe przyciski umieszczone na pasku akcji
switch (item.getItemId()) {
...
}
...
}

422 Rozdział 10.


Szuflady nawigacyjne

Jazda próbna aplikacji


Po uruchomieniu aplikacji zostaje wyświetlona aktywność MainActivity.
Jak widać, dysponuje już ona działającą szufladą nawigacyjną:

Po uruchomieniu
aplikacji na pasku akcji
jest wyświetlana ikona
szuflady nawigacyjnej.
Domyślnie zostaje
wyświetlony fragment
TopFragment, a na
pasku akcji jest
widoczna nazwa
aplikacji.
Po kliknięciu opcji Pizze
Po kliknięciu zostaje wyświetlony
ikony szuflady fragment PizzasFragment,
nawigacyjnej tytuł prezentowany na
szuflada jest pasku akcji zostaje
otwierana, a jej zmieniony na „Pizze”
ikona na pasku i w końcu szuflada
akcji zmienia zostaje zamknięta.
wygląd. Zostaje
wyświetlona lista
opcji dodanych
do szuflady
nawigacyjnej.

Element akcji Udostępnij jest widoczny w momencie, gdy szuflada


nawigacyjna jest zamknięta, natomiast po jej otworzeniu zostaje on
ukryty:

Akcja
Udostępnij
jest widoczna,
kiedy
szuflada jest
zamknięta.

Po otworzeniu szuflady akcja


Jest jeszcze jednak rzecz, którą trzeba się zająć: musimy upewnić Udostępnij zostaje ukryta.
się, że po obróceniu urządzenia lub kliknięciu przycisku Wstecz na
pasku akcji będzie wyświetlany odpowiedni tytuł. A jak to aktualnie
wygląda?

jesteś tutaj  423


Dalsze problemy z obrotami

Tytuł i fragment nie są zsynchronizowane


Kiedy klikniemy jedną z opcji dostępnych w szufladzie nawigacyjnej, tytuł wyświetlany
na pasku akcji odpowiada aktualnie prezentowanemu fragmentowi. Na przykład jeśli
klikniemy opcję Pizze, to na pasku akcji zostanie wyświetlony tytuł „Pizze”:

Kiedy klikniesz element


w szufladzie nawigacyjnej,
tytuł na pasku akcji zostanie
prawidłowo zaktualizowany.

Kiedy jednak klikniemy przycisk Wstecz, tytuł na pasku akcji nie jest aktualizowany
i przestaje odpowiadać wyświetlanemu fragmentowi. Na przykład załóżmy, że
w szufladzie nawigacyjnej kliknęliśmy opcję Restauracje, a następnie opcję Pizze.
W efekcie na ekranie zostanie wyświetlona lista pizz, a tytuł na pasku akcji będzie
temu odpowiadał. Jeśli teraz klikniemy przycisk Wstecz, to ponownie zostanie
wyświetlony fragment StoresFragment, ale tytuł na pasku akcji nie zmieni się
i dalej będzie na nim widoczny tekst „Pizze”:

Kiedy klikniemy przycisk


Wstecz, tytuł prezentowany
na pasku akcji nie zmienia
się. W tym przykładzie na
pasku wciąż jest widoczny
tytuł „Pizze”, choć poniżej
została wyświetlona lista
miast, w których znajdują się
restauracje.

Z kolei jeśli obrócimy urządzenie, to na pasku akcji, niezależnie od aktualnie prezentowanego


fragmentu, zostanie wyświetlona nazwa aplikacji, czyli „Włoskie Co Nieco”:

Po obróceniu urządzenia
tytuł na pasku akcji zostaje
przywrócony do stanu
początkowego.

Spróbujmy teraz rozwiązać oba te problemy, zaczynając od zachowania


synchronizacji paska akcji w przypadku zmiany orientacji urządzenia.

424 Rozdział 10.


Szuflady nawigacyjne

Obsługa zmian konfiguracji


Jak już wiesz, w przypadku obrócenia urządzenia aktualnie prezentowana aktywność jest
usuwana i ponownie tworzona. Oznacza to, że wszelkie zmiany wprowadzone w interfejsie
użytkownika zostają utracone; dotyczy to także zmian wprowadzanych na pasku akcji.

Aby rozwiązać ten problem, skorzystamy z rozwiązania, które stosowaliśmy już


w poprzednich rozdziałach — użyjemy metody onSaveInstanceState() do zapisania
pozycji aktualnie wybranego elementu szuflady nawigacyjnej. Następnie skorzystamy z tej
informacji w metodzie onCreate(), by zaktualizować tytuł wyświetlany na pasku akcji.

Poniżej pokazaliśmy zmiany, które należy wprowadzić w kodzie aktywności:

...
public class MainActivity extends Activity { WloskieCoNieco
...
private int currentPosition = 0; app/src/main

Zmiennej currentPosition przypisujemy java


@Override domyślnie wartość 0.
protected void onCreate(Bundle savedInstanceState) {
com.hfad.wloskieconieco
...
// Wyświetlamy odpowiedni fragment.
MainActivity.java
if (savedInstanceState != null) {
currentPosition = savedInstanceState.getInt(”position”);
setActionBarTitle(currentPosition);
Jeśli aktywność została usunięta
} else { i ponownie odtworzona, to wartość
selectItem(0); zmiennej currentPosition określam
podstawie zapisanego stanu akty y na
} a następnie używamy jej do okre wności,
tytułu na pasku akcji. ślenia
... Jeśli aktywność została utworzona
po raz pierwszy, to wyświetlamy
} fragment TopFragment.

private void selectItem(int position) {


currentPosition = position; Aktualizujemy wartość zmiennej currentPosition
... po wybraniu elementu z szuflady nawigacyjnej.
}

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(”position”, currentPosition);
}
... Jeśli aktywność ma zostać usunięta, to
zapisujemy stan zmiennej currentPosition.
}
jesteś tutaj  425
Obserwowanie stosu cofnięć

Reagowanie na zmiany stosu cofnięć


Ostatnią rzeczą, którą musimy się zająć, będzie aktualizowanie tytułu wyświetlanego
na pasku akcji w przypadku, gdy użytkownik naciśnie klawisz Wstecz. Możemy
WloskieCoNieco
to zrobić poprzez dodanie do menedżera fragmentów aktywności obiektu
nasłuchującego typu FragmentManager.OnBackStackChangedListener.
app/src/main
Interfejs FragmentManager.OnBackStackChangedListener pozwala nasłuchiwać
zmian zachodzących na stosie cofnięć. Dotyczy to takich zdarzeń jak dodanie na java
stos nowej transakcji fragmentu i naciśnięcie przycisku Wstecz w celu przywrócenia
poprzedniego elementu zapisanego na stosie. com.hfad.wloskieconieco

Obiekt nasłuchujący typu OnBackStackChangedListener można dodać do


menedżera fragmentów aplikacji w poniższy sposób: MainActivity.java

getFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() { mentManager.
Musimy dodać nowy obiekt Frag
// Kod wykonywany po zmianie stanu stosu cofnięć OnBackStackChangedListener
} i zaimplementować jego metodę
da jest
onBackStackChanged(). Ta meto
} oływ ana za każd ym raze m, gdy zmienia
wyw
się stan stos u cofn ięć.
);

Kiedy zmieni się stan stosu cofnięć, zostaje wywołana metoda onBackStackChanged()
obiektu OnBackStackChangedListener. To właśnie w niej należy umieścić kod,
który chcemy wykonywać w przypadku, gdy użytkownik naciśnie przycisk Wstecz.

W naszej aplikacji po naciśnięciu przez użytkownika przycisku Wstecz chcemy


wykonać trzy operacje:

 Zaktualizować wartość zmiennej currentPosition, tak by odpowiadała pozycji


aktualnie wyświetlonego fragmentu na liście opcji szuflady nawigacyjnej.

 Wywołać metodę setActionBarTitle(), przekazując przy tym do niej wartość


zmiennej currentPosition.

 Upewnić się, że na liście opcji szuflady nawigacyjnej zostanie zaznaczona


odpowiednia opcja, wywołując w tym celu metodę setItemChecked().

Wykonanie każdej z tych czynności wymaga znajomości pozycji, którą na liście opcji
szuflady nawigacyjnej zajmuje aktualnie wyświetlony fragment. A jak możemy tę
pozycję określić?

426 Rozdział 10.


Szuflady nawigacyjne

Dodawanie znaczników do fragmentów


Aby się dowiedzieć, jaką wartość powinna przyjąć zmienna currentPosition,
sprawdzimy typ aktualnie prezentowanego fragmentu. Na przykład jeśli tym WloskieCoNieco
fragmentem jest obiekt klasy PizzaFragment, to zmiennej currentPosition
przypiszemy wartość 1.
app/src/main
Referencję do fragmentu, który aktualnie jest dołączony do aktywności, pobierzemy,
dodając do każdego fragmentu łańcuchowy znacznik. Następnie użyjemy metody java
findFragmentByTag() menedżera fragmentów, by pobrać ten fragment.
com.hfad.wloskieconieco
Znacznik można dodawać do fragmentu w ramach transakcji fragmentu. Poniżej
przedstawiliśmy aktualny kod realizujący transakcję fragmentu, który jest
umieszczony w metodzie selectItem() i który zmienia aktualnie wyświetlony MainActivity.java
fragment:

FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();

Aby dodać znacznik, musimy umieścić w wywołaniu metody replace() dodatkowy


argument typu String:

FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment, ”visible_fragment”);
Ten dodatkowy argument
ft.addToBackStack(null); powoduje dodanie znacznika
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); „visible_fragment” do fragmentu,
który jest umieszczany na
ft.commit(); stosie cofnięć.

W powyższym kodzie dodaliśmy do wywołania metody replace() znacznik


”visible_fragment”. Znacznikiem tym zostanie opatrzony każdy fragment
wyświetlany w aktywności MainActivity.

Teraz zastosujemy metodę findFragmentByTag() menedżera fragmentów, by


pobrać referencję do fragmentu, który jest aktualnie dołączony do aktywności.

jesteś tutaj  427


Gra znaczonymi fragmentami

Odnajdywanie fragmentu na podstawie znacznika


Aby pobrać fragment, który w danym momencie jest dołączony do aktywności,
użyjemy metody findFragmentByTag(), przekazując w jej wywołaniu znacznik,
którego użyliśmy podczas wykonywania transakcji fragmentu: Poszukujemy fragmentu ze
znacznikiem „visible_fragment”.
FragmentManager fragMan = getFragmentManager();
Fragment fragment = fragMan.findFragmentByTag(”visible_fragment”);
Metoda findFragmentByTag() rozpoczyna działanie od przeszukania wszystkich
fragmentów dołączonych do aktywności. Jeśli nie uda się jej znaleźć fragmentu
opatrzonego podanym znacznikiem, to metoda przeszuka fragmenty umieszczone
na stosie cofnięć. Dzięki dodaniu tego samego znacznika ”visible_fragment”
do wszystkich fragmentów powyższy kod zwróci referencję do fragmentu, który jest
aktualnie dołączony do aktywności.
Poniżej przedstawiliśmy pełny kod naszej implementacji obiektu OnBackStackChangedListener.
Najpierw, przy użyciu metody findFragmentByTag(), pobieramy referencję do fragmentu
aktualnie dołączonego do aktywności. Potem sprawdzamy typ pobranej instancji fragmentu
i na jego podstawie określamy wartość zmiennej currentPosition:
getFragmentManager().addOnBackStackChangedListener( To wywołanie zwraca fragment,
new FragmentManager.OnBackStackChangedListener() { który w danej chwili jest
public void onBackStackChanged() { dołączony do aktywności.
FragmentManager fragMan = getFragmentManager();
Fragment fragment = fragMan.findFragmentByTag(”visible_fragment”);
if (fragment instanceof TopFragment) {
currentPosition = 0; Te instrukcje sprawdzają typ
fragmentu i na jego podstawie
} określają wartość zmiennej
if (fragment instanceof PizzaFragment) { currentPosition.
currentPosition = 1;
}
if (fragment instanceof PastaFragment) {
WloskieCoNieco
currentPosition = 2;
}
if (fragment instanceof StoresFragment) { app/src/main
currentPosition = 3;
} java
setActionBarTitle(currentPosition);
drawerList.setItemChecked(currentPosition, true); com.hfad.wloskieconieco
}
Te dwa ostatnie wywołania określają tytuł wyświetlany
} na pasku akcji i wyróżniają odpowiednie elementy listy
MainActivity.java
); w szufladzie nawigacyjnej.

To już cały kod niezbędny do zapewnienia prawidłowej synchronizacji tytułu wyświetlanego


na pasku akcji z aktualnie prezentowanym fragmentem w przypadku naciskania przycisku
Wstecz. Zanim sprawdzimy, jak działa nasza aplikacja po wprowadzeniu tych zmian,
pokażemy kompletny kod aktywności MainActivity.
428 Rozdział 10.
Szuflady nawigacyjne

Kompletny kod aktywności MainActivity


Oto kompletna zawartość pliku MainActivity.java:

package com.hfad.wloskieconieco;
r,
Używamy klasy FragmentManage WloskieCoNieco
import android.app.Activity; zatem musimy ją zaimportować.

import android.app.Fragment; app/src/main


import android.app.FragmentManager;
import android.app.FragmentTransaction; java
import android.content.Intent;
import android.content.res.Configuration; com.hfad.wloskieconieco
import android.os.Bundle;
import android.support.v4.widget.DrawerLayout; MainActivity.java
import android.support.v7.app.ActionBarDrawerToggle;
import android.view.Menu;
import android.view.MenuItem; To są wszystkie klasy używane
w kodzie aktywności.
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ShareActionProvider;

public class MainActivity extends Activity {

private ShareActionProvider shareActionProvider;


private String[] titles;
private ListView drawerList;
Używamy tych wszystkich
private DrawerLayout drawerLayout; zmiennych prywatnych.
private ActionBarDrawerToggle drawerToggle;
private int currentPosition = 0; Metoda onItemClick() interfejsu
OnItemClickListener jest wywoływana, gdy
użytkownik kliknie elementy listy ListView
umieszczonej w szufladzie nawigacyjnej.

private class DrawerItemClickListener implements ListView.OnItemClickListener {


@Override
public void onItemClick(AdapterView<?> parent, View view, int position,long id){
// Kod wykonywany po kliknięciu elementu w szufladzie nawigacyjnej
selectItem(position);
Kiedy element listy w szufladzie
} nawigacyjnej zostanie kliknięty, Dalszy ciąg kodu
}; wywołujemy metodę selectItem(). znajduje się na
następnej stronie.

jesteś tutaj  429


Kod aktywności MainActivity

Plik MainActivity.java (ciąg dalszy)


WloskieCoNieco
@Override
protected void onCreate(Bundle savedInstanceState) { app/src/main
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); java

titles = getResources().getStringArray(R.array.titles);
com.hfad.wloskieconieco
drawerList = (ListView)findViewById(R.id.drawer);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
MainActivity.java
// Określamy zawartość widoku ListView
drawerList.setAdapter(new ArrayAdapter<String>(this,
Wypełniamy elementy
android.R.layout.simple_list_item_activated_1, titles)); listy ListView
w szufladzie nawigacyjnej
drawerList.setOnItemClickListener(new DrawerItemClickListener()); i zapewniamy, aby
reagowała na kliknięcia.
// Wyświetlamy odpowiedni fragment
if (savedInstanceState != null) {
currentPosition = savedInstanceState.getInt(”position”);
setActionBarTitle(currentPosition); Jeśli aktywność została usunięta i ponownie odtworzona,
to ustawiamy prawidłowy tytuł na pasku akcji.
} else {
selectItem(0); Domyślnie jest wyświetlany
fragment TopFragment.
}
// Tworzymy obiekt ActionBarDrawerToggle
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
R.string.open_drawer, R.string.close_drawer) {
// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu zamknięciu
@Override
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
invalidateOptionsMenu(); Podczas otwierania i zamykania szuflady
nawigacyjnej wywołujemy metodę
} invalidateOptionsMenu(), gdyż chcemy zmieniać
elementy wyświetlane na pasku akcji.

// Wywoływana, kiedy stan szuflady odpowiada jej całkowitemu otworzeniu


@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
invalidateOptionsMenu();
} Dalszy ciąg kodu
}; znajduje się na
następnej stronie.

430 Rozdział 10.


Szuflady nawigacyjne

Plik MainActivity.java (ciąg dalszy)


dy onCreate().
Ten kod stanowi dalszą część meto

drawerLayout.setDrawerListener(drawerToggle);
getActionBar().setDisplayHomeAsUpEnabled(true);
To wywołanie włącza przycisk W
getActionBar().setHomeButtonEnabled(true); akcji, dzięki czemu będziemy moggórę na pasku
wyświetlania szuflady nawigacyjneli go używać do
j.
getFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
Ta metoda jest
wywoływana FragmentManager fragMan = getFragmentManager();
w przypadku zmiany Fragment fragment = fragMan.findFragmentByTag(”visible_fragment”);
stanu stosu cofnięć.
if (fragment instanceof TopFragment) {
currentPosition = 0;
} WloskieCoNieco
Te instrukcje if (fragment instanceof PizzaFragment) {
warunkowe
sprawdzają currentPosition = 1; app/src/main
klasę fragmentu
aktualnie }
dołączonego if (fragment instanceof PastaFragment) { java
do aktywności
i w zależności currentPosition = 2;
od niej com.hfad.wloskieconieco
odpowiednio }
ustawiają if (fragment instanceof StoresFragment) {
wartość zmiennej MainActivity.java
currentPosition. currentPosition = 3;
}
setActionBarTitle(currentPosition);
drawerList.setItemChecked(currentPosition, true);
}
Te dwa wywołania ustawiają tytuł wyświetlany
} na pasku akcji i zaznaczają odpowiedni element
); listy w szufladzie nawigacyjnej.
}

Dalszy ciąg kodu


znajduje się na
następnej stronie.

jesteś tutaj  431


Dalsza część kodu MainActivity

Plik MainActivity.java (ciąg dalszy)


ana, kiedy
Metoda selectItem() jest wywoływ w listy
użyt kown ik klikn ie jede n z elem entó
yjnej.
wyświetlonej w szufladzie nawigac

WloskieCoNieco
private void selectItem(int position) {
// Aktualizujemy główną zawartość aplikacji, podmieniając prezentowany fragment
app/src/main
currentPosition = position;
Fragment fragment; java
switch(position) {
case 1: com.hfad.wloskieconieco

fragment = new PizzaFragment();


break; MainActivity.java

case 2:
Na podstawie pozycji elementu
fragment = new PastaFragment(); zaznaczonego przez użytkownika
w szufladzie nawigacyjnej określam
break; który fragment należy wyświetlić. y,
case 3:
fragment = new StoresFragment();
break;
default:
Wyświetlamy fragment.
fragment = new TopFragment();
}
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment, ”visible_fragment”);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
// Ustawiamy tytuł paska akcji
Wyświetlamy odpowiedni tytuł na pasku akcji.
setActionBarTitle(position);
// Zamykamy szufladę nawigacyjną
drawerLayout.closeDrawer(drawerList);
}
Zamykamy szufladę nawigacyjną.

Dalszy ciąg kodu


znajduje się na
następnej stronie.

432 Rozdział 10.


Szuflady nawigacyjne

Plik MainActivity.java (ciąg dalszy)


@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Jeśli szuflada jest otworzona, ukrywamy elementy akcji związane
// z prezentowaną zawartością
boolean drawerOpen = drawerLayout.isDrawerOpen(drawerList);
menu.findItem(R.id.action_share).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
Akcję Udostępnij wyświetlamy, kiedy
} szuflada jest zamknięta, i chowamy,
gdy jest otwarta.

@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Synchronizumemy stan przycisku przełącznika po wywołaniu
// metody onRestoreInstanceState
drawerToggle.syncState(); Synchronizujemy stan przycisku
} ActionBarDrawerToggle ze stanem
szuflady nawigacyjnej.

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
drawerToggle.onConfigurationChanged(newConfig);
WloskieCoNieco
} Wszelkie szczegółowe informacje
o zmianach konfiguracji przekazujemy
do przycisku ActionBarDrawerToggle. app/src/main
@Override
public void onSaveInstanceState(Bundle outState) { java
super.onSaveInstanceState(outState);
outState.putInt(”position”, currentPosition); com.hfad.wloskieconieco
} Jeśli aktywność ma zostać usunięta, to zapisujemy
bieżący stan zmiennej currentPosition.
MainActivity.java
private void setActionBarTitle(int position) {
String title;
if (position == 0) {
title = getResources().getString(R.string.app_name);
} else {
title = titles[position];
} Ustawiamy tytuł wyświetlany Dalszy ciąg kodu
na pasku akcji, tak by znajduje się na
getActionBar().setTitle(title); odpowiadał on aktualnie następnej stronie.
} prezentowanemu fragmentowi.
jesteś tutaj  433
To już prawie koniec

Plik MainActivity.java (ciąg dalszy)


Ta metoda dodaje do paska
@Override akcji zawartość pliku zasobów
menu.
public boolean onCreateOptionsMenu(Menu menu) {
// Przygotowujemy menu; to wywołanie dodaje elementy do paska akcji, jeśli jest używany
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem menuItem = menu.findItem(R.id.action_share);
shareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
setIntent(”To jest przykładowy tekst.”);
return super.onCreateOptionsMenu(menu);
} Ta metoda tworzy intencję na potrzeby
akcji Udostępnij.
WloskieCoNieco
private void setIntent(String text) {
Intent intent = new Intent(Intent.ACTION_SEND); app/src/main
intent.setType(”text/plain”);
intent.putExtra(Intent.EXTRA_TEXT, text); java
shareActionProvider.setShareIntent(intent);
com.hfad.wloskieconieco
}
Ta metoda jest wywoływana, kiedy
użytkownik kliknie jakiś elementy paska akcji.
MainActivity.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item)) {
return true; Jeśli został kliknięty przycisk ActionBarDrawerToggle,
pozwalamy mu wykonać to, co do niego należy.
}
switch (item.getItemId()) {
case R.id.action_create_order:
// Kod wykonywany po kliknięciu przycisku Złóż zamówienie
Intent intent = new Intent(this, OrderActivity.class);
startActivity(intent); W razie kliknięcia przycisku
Złóż zamówienie uruchamiamy
return true; aktywność OrderActivity.
case R.id.action_settings:
// Kod wykonywany po kliknięciu przycisku Ustawienia
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

434 Rozdział 10.


Szuflady nawigacyjne

Jazda testowa aplikacji


Zobaczmy, co się stanie po uruchomieniu aplikacji.

Kiedy klikniemy opcję


Makarony, aplikacja wyświetli
fragment PastaFragment
i zmieni tytuł na pasku akcji
na „Makarony”.
Kiedy klikniemy opcję Pizze,
aplikacja wyświetli fragment
PizzaFragment i zmieni tytuł
na pasku akcji na „Pizze”.
Kiedy naciśniemy przycisk
Wstecz, zostanie ponownie
wyświetlony fragment
PastaFragment, a na pasku
akcji pojawi się tytuł
„Makarony”.

Po naciśnięciu przycisku Wstecz zostaje wyświetlony poprzedni


fragment, a tytuł na pasku akcji także jest odpowiednio zmieniany.
Tytuł jest również odpowiednio synchronizowany z prezentowanym
fragmentem w przypadku zmiany orientacji urządzenia.

Kiedy obrócimy urządzenie, tytuł


na pasku akcji nie ulega zmianie.

jesteś tutaj  435


Przybornik

Twój przybornik do Androida Pełny kod przykładowe


tow ane
j
j
aplikacji prezen
Rozdział 10.

ale mo żes z
w tym rozdzi
Opanowałeś już rozdział 10. i dodałeś  pobrać z ser we ra FT P
do swojego przybornika z narzędziami  wydawnictwa Helion:
klady/
układy DrawerLayout. ftp://ftp.helion.pl/przy
andrrg.zip

    CELNE SPOSTRZEŻENIA
 Użyj układu DrawerLayout, aby stworzyć aktywność z szufladą nawigacyjną.
Zastosuj tę szufladę, by przechodzić do głównych węzłów aplikacji.
 Jeśli używasz paska akcji, to zastosuj przycisk typu ActionBarDrawerToggle
jako obiekt nasłuchujący typu DrawerListener. Dzięki temu będziesz mógł
reagować na otwieranie i zamykanie szuflady nawigacyjnej i w odpowiedzi na
te zdarzenia odpowiednio modyfikować elementy widoczne na pasku akcji.
 Do zmieniania elementów paska akcji w trakcie działania aplikacji musisz
zastosować metodę invalidateOptionsMenu() i wprowadzić odpowiednie
zmiany w metodzie onPrepareOptionsMenu() aktywności.
 Aby reagować na zmiany zachodzące na stosie cofnięć, należy zaimplementować
interfejs FragmentManager.OnBackStackChangedListener.
 Metoda findFragmentByTag() menedżera fragmentów pozwala odszukać
fragment na podstawie znacznika.

436 Rozdział 10.


11. Bazy danych SQLite

Odpal bazę danych


Kiedy powiedziałam,
że zależy mi na czymś
trwałym, miałam na
myśli bazę danych.

Jeśli rejestrujesz najlepsze wyniki lub przesyłane komunikaty,


to Twoja aplikacja będzie musiała przechowywać dane.
A w Androidzie dane są zazwyczaj bezpiecznie i trwale przechowywane w bazach danych
SQLite. W tym rozdziale pokażemy Ci, jak utworzyć bazę danych, dodawać do niej tabele,
wypełnić ją wstępnie danymi, a wszystko to za pomocą pomocnika SQLite. Dowiesz
się też, w jaki sposób można bezproblemowo przeprowadzać aktualizacje struktury bazy
danych i jak w razie konieczności wycofania zmian wrócić do jej wcześniejszych wersji.

to jest nowy rozdział  437


Znowu w kafeterii

Znowu w kafeterii Coffeina


We wcześniejszej części książki, a konkretnie w rozdziale 6., napisaliśmy aplikację
na potrzeby kafeterii Coffeina. Aplikacja ta pozwalała użytkownikom przeglądać
sekwencję ekranów prezentujących napoje oferowane przez kafeterię.

Aktywność głównego
poziomu wyświetla
listę opcji. Kliknięcie opcji
Napoje powoduje
wyświetlenie listy
dostępnych napojów.

Po kliknięciu
konkretnego napoju
wyświetlane
są szczegółowe
informacje na jego
temat.

Baza danych aplikacji czerpie dane z klasy Drink zawierającej wybrane


napoje oferowane przez kafeterię. Choć zastosowanie takiego rozwiązania
znacznie ułatwiło nam napisanie pierwszej wersji aplikacji, to jednak
istnieje dużo lepszy sposób trwałego przechowywania danych.

W tym i w następnym rozdziale zajmiemy się zmianą źródła danych


aplikacji, tak by były one przechowywane i pobierane z bazy danych
SQLite. W tym rozdziale pokażemy, jak utworzyć bazę danych,
a w następnym — jak połączyć z nią aktywności.

438 Rozdział 11.


Bazy danych SQLite

Android trwale przechowuje dane, używając baz danych SQLite


Wszystkie aplikacje muszą przechowywać dane, a w Androidowie robi się to,
używając baz danych SQLite. Dlaczego SQLite?

 Jest nieskomplikowana. W tym rozdziale


Większość systemów baz danych wymaga do prawidłowego działania przedstawimy
specjalnego procesu serwera bazy danych. SQLite go nie potrzebuje
— baza danych SQLite jest zwyczajnym plikiem. Kiedy baza nie jest podstawowe informacje
używana, nie zużywa czasu procesora. W przypadku urządzeń mobilnych ma związane z bazami
to ogromne znaczenie, gdyż zależy nam, by nie zużywać niepotrzebnie baterii.
danych SQLite.
 Jest zoptymalizowana pod kątem wykorzystania przez jednego
użytkownika.
Nasza aplikacja jest jedynym programem, który będzie komunikował się
Jeśli planujesz
z bazą danych, więc przeważnie nie będziemy musieli identyfikować się w znacznym stopniu
przy użyciu nazwy użytkownika i hasła.
używać baz danych
 Jest stabilna i szybka. w swoich aplikacjach,
Bazy danych SQLite są zadziwiająco stabilne. Potrafią obsługiwać
transakcje, co oznacza, że jeśli aktualizujemy kilka różnych danych i coś to sugerujemy, byś
pójdzie nie tak, to SQLite potrafi wycofać wszystkie zmiany. Co więcej, poszukał dodatkowych
kod realizujący operacje odczytu i zapisu do bazy został napisany w języku C
i zoptymalizowany. Dlatego nie tylko działa szybko, lecz także ogranicza informacji o SQLite
ilość mocy procesora potrzebnej do jego działania. i języku SQL.

Gdzie są przechowywane bazy danych?


Android automatycznie tworzy odrębny katalog dla każdej aplikacji i w tym
katalogu jest przechowywana jej baza danych. W przypadku naszej aplikacji
dla kafeterii Coffeina będzie to katalog:
com.hfad.coffeina to unikalny identyfikator aplikacji.
/data/data/com.hfad.coffeina/databases

W katalogu aplikacji może być przechowywanych kilka baz danych. data


Każda składa się z dwóch plików.
data
Pierwszy to plik bazy danych, którego nazwa odpowiada nazwie bazy,
na przykład „coffeina”. Jest to główny plik bazy danych SQLite. To właśnie com.hfad.coffeina
w nim są przechowywane wszystkie dane.
databases
Drugim plikiem jest plik magazynu (ang. journal file). Ma on taką samą nazwę
jak plik bazy danych z dodatkową końcówką „-journal”, na przykład „coffeina- To jest plik bazy danych.
journal”. Ten plik zawiera wszelkie zmiany wprowadzane w bazie danych. Jeśli coffeina
w bazie wystąpią jakieś problemy, Android skorzysta z informacji zapisanych
w tym pliku, by odtworzyć (lub wycofać) ostatnio wprowadzone zmiany. To jest plik magazynu.
coffeina-journal

jesteś tutaj  439


Twoi nowi najlepsi przyjaciele

Android udostępnia kilka klas związanych z SQLite


Android udostępnia grupę klas pozwalających zarządzać bazami danych SQLite.
Większość związanych z tym operacji realizują obiekty trzech podstawowych typów.

Pomocnik SQLite
Pomocnika SQLite tworzymy
poprzez rozszerzenie klasy
Baza danych SQLite
SQLiteOpenHelper. Obiekty Obiekt klasy SQLiteDatabase
tego typu umożliwiają zapewnia dostęp do bazy
tworzenie baz danych SQLite danych. Można go porównać
i zarządzanie nimi. z obiektem SQLConnection
JDBC.

Kursory
Obiekty klasy Cursor służą
do odczytywania i zapisywania
informacji w bazie danych.
Można je porównać
z obiektami ResultSet JDBC.

Użyjemy tych obiektów, by pokazać Ci, w jaki sposób utworzyć bazę


danych SQLite, której nasza aplikacja będzie mogła używać do trwałego
przechowywania danych zamiast klasy Drink.

Nie istnieją
głupie pytania

P: Skoro baza danych nie używa O : Nie ma żadnych powodów, dla O: Wiemy, że będziemy używać baz
nazwy użytkownika ani hasła, których nie moglibyśmy uzyskać dostępu danych SQLite, dlatego stosowanie JDBC
to jak może być bezpieczna? do zewnętrznych baz danych za pomocą byłoby grubą przesadą. Te wszystkie

O: Zawartość katalogu, w którym połączeń sieciowych, trzeba jednak mieć na


uwadze oszczędność zasobów zużywanych
warstwy sterowników baz danych, które
sprawiają, że JDBC jest tak elastycznym
jest przechowywana baza danych, przez system. Na przykład może się rozwiązaniem, przyspieszałyby zużycie
może być odczytywana wyłącznie okazać, że korzystanie z usługi sieciowej prądu w urządzeniach z Androidem.
przez daną aplikację. Baza danych pozwala ograniczyć zużycie energii.
jest więc zabezpieczana na poziomie
systemowym.
Analogicznie, jeśli nie komunikujemy się P: Czy katalog bazy danych
cały czas z bazą danych, to nie zużywamy znajduje się w katalogu aplikacji?

P: Czy mogę napisać aplikację żadnych zasobów.


O: Nie. Bazy danych nie są
na Androida, która będzie mogła
komunikować się z jakąś zewnętrzną
P: Dlaczego w Androidzie dostęp przechowywane w tym samym katalogu,
w którym jest umieszczony kod aplikacji.
do baz danych SQLite nie jest
bazą danych, taką jak Oracle? realizowany za pomocą JDBC? Dzięki temu można zainstalować
nową wersję aplikacji, a informacje
przechowywane w bazie będą bezpieczne.
440 Rozdział 11.
Bazy danych SQLite

Obecna struktura aplikacji kafeterii Coffeina


Poniżej przypomnieliśmy obecną strukturę aplikacji napisanej dla kafeterii Coffeina:

1 Aktywność TopLevelActivity zawiera listę opcji Napoje,


Przekąski oraz Kafeterie.

2 Kiedy użytkownik kliknie opcję Napoje, uruchamiana jest


aktywność DrinkCategoryActivity.
Ta aktywność wyświetla listę napojów, których dane są pobierane
z klasy Drink.

3 Kiedy użytkownik kliknie konkretny napój, uruchamiana


jest aktywność DrinkActivity,
która wyświetla informacje na temat tego napoju.
Aktywność DrinkActivity pobiera szczegółowe informacje
o napoju z klasy Drink. Aktualnie aplikacja
pobiera dane z klasy Drink.

<Layout>
<Layout>

</Layout>
</Layout>

activity_top_level.xml Drink.java activity_drink.xml

1 2 3

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie

W jaki sposób musimy zmienić tę strukturę aplikacji,


abyśmy mogli skorzystać z bazy danych SQLite?
Zrób to sam!

W tym rozdziale planujemy zmodyfikować


aplikację kafeterii Coffeina, więc otwórz
pierwszą wersję tej aplikacji w Android Studio.
jesteś tutaj  441
Modyfikacja aplikacji

Zmienimy aplikację, by korzystała z bazy danych


Użyjemy pomocnika SQLite, by utworzyć bazę danych, której będziemy mogli
używać w naszej aplikacji. Planujemy zastąpić klasę Drink bazą danych, a zatem
musimy użyć pomocnika SQLite, aby wykonać następujące operacje:

1 Utworzenie bazy danych.


Zanim będziemy mogli wykonać jakiekolwiek inne operacje, musimy użyć
pomocnika SQLite do utworzenia pierwszej wersji bazy danych.

2 Utworzenie tabeli Drink i zapisanie w niej informacji o napojach.


Po utworzeniu bazy danych będziemy mogli utworzyć w niej tabelę. Struktura tej
tabeli będzie musiała odpowiadać atrybutom klasy Drink, a zatem dla każdego
napoju będziemy musieli zapisać jego nazwę, opis oraz identyfikator obrazka.
Następnie dodamy do tej tabeli dane trzech napojów.

Informacje o napojach
Aplikacja ma niemal taką samą strukturę jak wcześniej, z tą różnicą, że klasę zapiszemy teraz w bazie
Drink (Drink.java) zastąpimy pomocnikiem SQLite i bazą danych SQLite. danych, a nie w klasie
Drink.
Pomocnik będzie zarządzał bazą danych i zapewniał wszystkim aktywnościom
dostęp do niej. Modyfikacją kodu samych aktywności zajmiemy się
w następnym rozdziale.

Baza danych
Coffeina
<Layout>
<Layout>

</Layout>
</Layout>

activity_top_level.xml SQLite Helper activity_drink.xml

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie
W następnym rozdziale zmienimy
aktywności korzystające do tej pory
z klasy Drink, tak by informacje
Najpierw poznajmy pomocnik SQLite. o napojach pobierały z bazy danych.

442 Rozdział 11.


Bazy danych SQLite

Pomocnik SQLite zarządza Twoją bazą danych ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Klasa SQLiteOpenHelper pomaga nam w tworzeniu i utrzymaniu baz danych


SQLite. Można ją sobie wyobrazić jako osobistego kamerdynera, którego
zadaniem jest doglądanie bazy danych.

Przyjrzyjmy się kilku typowym zadaniom, w których realizacji klasa


SQLiteOpenHelper może nam pomóc:

Tworzenie Zapewnianie
bazy danych dostępu
Podczas pierwszego do bazy danych
instalowania aplikacji plik
Ponieważ nie ma potrzeby,
bazy danych nie będzie
by aplikacja dysponowała
istnieć. Pomocnik SQLite
wszystkimi informacjami
może zadbać o prawidłowe
o lokalizacji plików
utworzenie pliku
bazy danych za każdym
o odpowiedniej nazwie
razem, gdy będziemy tego
i zawierającego tabele
potrzebowali, pomocnik
o określonej strukturze.
SQLite może nam
udostępniać łatwy w użyciu
obiekt bazy danych.
O każdej porze dnia i nocy.

Pomocnik SQLite

Utrzymywanie bazy danych


w doskonałym stanie
Istnieje spore prawdopodobieństwo, że wraz z upływem
czasu struktura bazy danych będzie ulegała zmianie,
a pomocnik SQLite może, w sposób całkowicie godny
zaufania, zadbać o skonwertowanie starej bazy w śliczną,
nowiutką bazę o nowej strukturze.
jesteś tutaj  443
Tworzenie pomocnika

Tworzenie pomocnika SQLite ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Pomocnika SQLite możemy utworzyć, pisząc klasę dziedziczącą po klasie java.lang.Object


SQLiteOpenHelper. W tej klasie musimy przesłonić metody onCreate()
...
i onUpgrade(). Obie te metody są obowiązkowe.

Pierwsza z tych dwóch metod, onCreate(), jest wywoływana w momencie,


gdy baza danych zostaje po raz pierwszy utworzona na urządzeniu. Powinna
ona zawierać cały kod niezbędny do utworzenia tabel bazy danych. android.database.sqlite.
SQLiteOpenHelper
Z kolei metoda onUpgrade() jest wywoływana, gdy trzeba zaktualizować bazę onCreate(SQLiteDatabase)
danych. Na przykład jeśli trzeba wprowadzić jakieś zmiany w tabelach już po onUpgrade(SQLiteDatabase, int, int)
utworzeniu bazy danych, to należy je wykonywać właśnie w tej metodzie.
onDowngrade(SQLiteDatabase, int, int)
W naszej aplikacji napiszemy klasę pomocnika SQLite o nazwie onOpen(SQLiteDatabase)
CoffeinaDatabaseHelper. Aby utworzyć tę klasę w swoim projekcie getReadableDatabase()
aplikacji dla kafeterii Coffeina, w eksploratorze plików projektu zaznacz
getWritableDatabase()
katalog app/src/main/com.hfad.coffeina, a potem wybierz w menu opcję File/
New.../Java Class. Tworzonej klasie nadaj nazwę CoffeinaDatabaseHelper, ...
a następnie zastąp jej zawartość wygenerowaną przez Android Studio
poniższym kodem: Klasa SQLiteOpenHelper
dziedziczy po klasie Object.
package com.hfad.coffeina;

Klasa pomocnika SQLite


import android.database.sqlite.SQLiteOpenHelper; musi dziedziczyć po klasie
import android.content.Context; SQLiteOpenHelper.

import android.database.sqlite.SQLiteDatabase;

Coffeina
class CoffeinaDatabaseHelper extends SQLiteOpenHelper {

app/src/main
CoffeinaDatabaseHelper(Context context) {
}
java

@Override com.hfad.coffeina
public void onCreate(SQLiteDatabase db) {
} Metody onCreate() i onUpgrade() są obowiązkowe. CoffeinaDatabase
Na razie zostawiliśmy je puste, ale w dalszej części
Helper.java
rozdziału zajmiemy się nimi bardziej szczegółowo.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
Aby nasz pomocnik SQLite mógł coś zrobić, musimy uzupełnić kod jego
metod. Dlatego naszym pierwszym zadaniem będzie przekazanie pomocnikowi
informacji na temat bazy danych, którą będzie musiał utworzyć.

444 Rozdział 11.


Bazy danych SQLite

1. Określenie bazy danych


Aby pomocnik SQLite mógł utworzyć bazę danych, będzie potrzebował
dwóch informacji.

W pierwszej kolejności musimy określić nazwę tej bazy. Określenie


nazwy bazy danych sprawia, że pozostanie ona na urządzeniu po Tworzenie takich baz danych
jego zamknięciu. Jeśli nie podamy tej nazwy, to baza danych zostanie przechowywanych w pamięci może
być przydatne podczas testowania
utworzona tylko w pamięci, a po zamknięciu zniknie. aplikacji.
Drugą informacją, którą musimy podać, jest numer wersji bazy danych.
Musi to być liczba całkowita, a jej pierwszą wartością musi być 1.
Pomocnik SQLite będzie używał tego numeru do określania,
czy bazę danych należy zaktualizować.

Nazwę bazy danych i numer jej wersji określamy, przekazując je Nazwa: „coffeina”
w wywołaniu konstruktora klasy bazowej SQLiteOpenHelper. Wersja: 1
W naszej aplikacji bazie danych nadamy nazwę „coffeina”,
a ponieważ będzie to pierwsza wersja naszej bazy, będzie ona
miała numer wersji 1. Poniżej przedstawiliśmy kod, który definiuje
Pomocnik SQLite
te informacje i ich używa (wprowadź te modyfikacje w swojej wersji
pliku CoffeinaDatabaseHelper.java):

...
class CoffeinaDatabaseHelper extends SQLiteOpenHelper {

private static final String DB_NAME = “coffeina”; // Nazwa bazy danych


private static final int DB_VERSION = 1; // Numer wersji bazy danych

CoffinaDatabaseHelper(Context context) { Wywołujemy konstruktor klasy bazowej


SQLiteOpenHelper, przekazując do niego
super(context, DB_NAME, null, DB_VERSION); nazwę i numer wersji bazy danych.
}
Ten argument reprezentuje bardziej zaawa
nsowane
możliwości związane z kursorami. Kursory Coffeina
... poznamy
w następnym rozdziale.
}
app/src/main

Konstruktor określa szczegółowe informacje o bazie danych, ale java


sama baza nie jest tworzona podczas jego wywołania. Pomocnik
SQLite czeka z tym aż do momentu, gdy aplikacja będzie chciała com.hfad.coffeina
uzyskać dostęp do bazy.

Kiedy już określimy, jaką bazę danych chcemy utworzyć, musimy CoffeinaDatabase
także podać strukturę jej tabel. Helper.java

jesteś tutaj  445


Zrobiliśmy już wszystko, co niez
będn
by w chwili, gdy będziemy potrzebo e,
Tabele bazy danych, została ona utworzon wać
a.

Wnętrze bazy danych SQLite ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Dane umieszczane w bazie danych SQLite są zapisywane w tabelach. Tabela składa


się z grupy wierszy, a każdy wiersz jest podzielony na kolumny. Każda kolumna zwiera
pojedynczą informację, taką jak liczba lub łańcuch znaków.

Dla każdego z odrębnych rodzajów informacji, które chcemy przechowywać w bazie Kolumny tabeli mają odpowiednio
danych, musimy utworzyć tabelę. Na przykład w przypadku naszej aplikacji dla kafeterii nazwy _id, NAME, DESCRIPTION
i IMAGE_RESOURCE_ID.
Coffeina musimy utworzyć tabelę do przechowywania informacji o napojach. Klasa Drink definiuje atrybuty
Można by ją przedstawić jak na poniższym rysunku: o podobnych nazwach.

_id NAME DESCRIPTON IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym mlekiem 54543543
i mleczną pianką.”
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453
spienionego mleka.”
3 “Espresso” “Czarna kawa ze świeżo mielonych ziaren 44324234
najwyższej jakości.”

Niektóre kolumny można wskazać jako klucz główny tabeli. Klucz główny W systemie Android
w unikalny sposób identyfikuje każdy wiersz tabeli. Jeśli zatem określimy, że jakaś
kolumna pełni rolę klucza głównego, to baza danych nie pozwoli zapisać w tabeli stosowana jest
wierszy, w których wartość tego klucza się powtarza. konwencja,
Sugerujemy, żebyś w swoich tabelach stosował klucz główny składający się z jednej by kolumna klucza
kolumny o nazwie _id. Wynika to z faktu, że Andoid oczekuje, by tabele zawierały
kolumnę o nazwie _id typu liczbowego; brak takiej kolumny może być przyczyną głównego miała
późniejszych problemów.
nazwę _id. Kod
Klasy i typy danych systemu oczekuje,
Każda kolumna tabeli jest przeznaczona do przechowywania danych określonego typu. że kolumna _id
Na przykład w naszej tabeli DRINK kolumna DESCRIPTION będzie zawierała wyłącznie
dane tekstowe. Poniżej przedstawiliśmy zestawienie najczęściej używanych typów
będzie dostępna
danych SQLite wraz z informacjami, co przy ich użyciu można zapisywać w bazie: w tabelach.
INTEGER Dowolne liczby całkowite Zignorowanie tej
TEXT Dowolne typy znakowe
REAL Dowolne liczby zmiennoprzecinkowe
konwencji może
NUMERIC Wartości logiczne, daty oraz daty i godziny utrudnić pobieranie
BLOB Duże obiekty binarne (ang. Binary Large Objects)
danych z bazy
W odróżnieniu od większości systemów baz danych w SQLite nie musimy określać
wielkości kolumn. Za kulisami SQLite przekształca te typy danych na klasy i wyświetlanie
o znacznie szerszym zakresie. Dzięki temu rodzaj informacji, które mamy zamiar ich w interfejsie
przechowywać w kolumnach, możemy określać w sposób bardzo ogólny i nie musimy
podawać wielkości tych danych. użytkownika.
446 Rozdział 11.
Bazy danych SQLite

Tabele tworzymy w języku SQL


Każda aplikacja korzystająca z bazy danych SQLite musi się z nią komunikować,
używając standardowego języka: strukturalnego języka zapytań (ang. Structured
Query Language), określanego skrótowo jako SQL. Jest on używany przez niemal
wszystkie rodzaje baz danych. A zatem, chcąc utworzyć tabelę DRINK, będziemy
musieli zrobić to w SQL-u.

Poniżej przedstawiliśmy polecenie SQL, które służy do utworzenia tabeli:


Kolumna _id jest kluczem głównym tabeli.
CREATE TABLE DRINK (_id INTEGER PRIMARY KEY AUTOINCREMENT,
Nazwa tabeli. NAME TEXT,
DESCRIPTION TEXT,
To są kolumny tabeli.
IMAGE_RESOURCE_ID INTEGER)

Polecenie CREATE TABLE określa, jakie kolumny ma mieć tabela i jakiego typu ma
być każda z nich. Kolumna _id jest kluczem głównym tabeli, a zastosowane w niej
słowo kluczowe AUTOINCREMENT oznacza, że podczas zapisywania w tabeli nowego
wiersza SQLite automatycznie wygeneruje i zapisze w tej kolumnie unikalną wartość.

Metoda onCreate() jest wywoływana podczas tworzenia bazy danych


Za utworzenie bazy danych w momencie, gdy po raz pierwszy ma ona zostać
użyta, odpowiada pomocnik SQLite. Najpierw na urządzeniu jest tworzona
pusta baza danych, a dopiero potem zostaje wywołana metoda onCreate() Klasa
pomocnika SQLite. SQLiteDatabase
W wywołaniu metody onCreate() przekazywany jest jeden argument zapewnia dostęp
— obiekt SQLiteDatabase. Możemy go użyć do wykonania polecenia SQL:
do bazy danych.
To wywołanie wykonuje
SQLiteDatabase.execSQL(String sql); w bazie danych polecenie SQL.
Powyższa metoda ma tylko jeden parametr — polecenie SQL, które należy wykonać.

Oto kod metody onCreate():

@Override Coffeina
public void onCreate(SQLiteDatabase db) {
db.execSQL(“CREATE TABLE DRINK (_id INTEGER PRIMARY app/src/main
KEY AUTOINCREMENT, “
+ “NAME TEXT, “ java
+ “DESCRIPTION TEXT, “
+ “IMAGE_RESOURCE_ID INTEGER);”); com.hfad.coffeina
}
Wykonanie tej metody utworzy pustą tabelę DRINK, ale co moglibyśmy zrobić, CoffeinaDatabase
gdybyśmy chcieli wstępnie wypełnić ją jakimiś danymi? Helper.java

jesteś tutaj  447


Metoda insert()

Wstawianie danych za pomocą metody insert() ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Klasa SQLiteDatabase definiuje kilka metod pozwalających wstawiać,


aktualizować oraz usuwać dane. Przyjrzymy się im dokładniej na kilku następnych java.lang.Object
stronach, zaczynając od metody do wstawiania danych. ...

Jeśli musimy wstępnie zapisać w bazie danych SQLite jakieś informacje, to


możemy do tego użyć metody insert() klasy SQLiteDatabase. Metoda ta
pozwala wstawić dane do bazy i zwraca identyfikator dodanego wiersza. Jeśli android.database.sqlite.
okaże się, że metoda nie jest w stanie wstawić rekordu, to zwróci ona wartość -1. SQLiteDatabase
execSQL(String)
Aby skorzystać z metody insert(), musimy określić nazwę tabeli, w której
chcemy zapisać dane, jak również zapisywane wartości. Wartości te określamy insert(String, String, ContentValues)
poprzez przygotowanie obiektu ContentValues. Obiekt tego typu służy do update(String, ContentValues,
przechowywania par nazwa – wartość: String, String[])
delete(String, String, String[])
ContentValues drinkValues = new ContentValues();
...
Poszczególne pary nazwa – wartość dodajemy do obiektu ContentValues,
wywołując jego metodę put(). W przypadku naszej aplikacji chcemy użyć tego
obiektu, by dodać wiersz danych do tabeli DRINK, dlatego zapiszemy w nim nazwy Klasa SQLiteDatabase
dziedziczy po klasie Object.
poszczególnych kolumn tej tabeli i odpowiadające im wartości poszczególnych pól:

ContentValues drinkValues = new ContentValues(); To wywołanie zapisze w kolumnie


DESCRIPTION łańcuch znaków
drinkValues.put(“NAME”, “Latte”); “Czarne espresso z gorącym mlekiem
i mleczną pianką.”
drinkValues.put(“DESCRIPTION”, “Czarne espresso z gorącym
mlekiem i mleczną pianką.”);
drinkValues.put(”IMAGE_RESOURCE_ID”, R.drawable.latte); Każdą wartość zapisywaną
w wierszu musimy określić
za pomocą odrębnego
I na koniec użyjemy metody insert() klasy SQLiteDatabase, by zapisać tak wywołania metody put().
określone wartości w tabeli DRINK:

db.insert(”DRINK”, null, drinkValues);


Do tabeli został
To wywołanie spowoduje zapisanie w tabeli DRINK wiersza z danymi o latte: wstawiony lśniący
nowością rekord.

_id NAME DESCRIPTON IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym mlekiem i mleczną pianką.” 54543543

Metoda insert() ma następującą, ogólną postać:

db.insert(String tabela, String nullColumnHack, ContentValues wartosci);


To wywołanie wstawia
Argument nullColumnHack typu String jest opcjonalny i w przeważającej większości do podanej tabeli
przypadków nadajemy mu wartość null, tak jak zrobiliśmy to w powyższym przykładzie. Jest jeden wiersz danych.
Aby zapisać więcej
on używany w sytuacjach, gdy obiekt ContentValues jest pusty i trzeba zapisać w tabeli pusty wierszy, trzeba
rekord. Baza SQLite nie pozwala zapisywać pustych wierszy, jeśli nie została określona nazwa wielokrotnie wywołać
metodę insert().
przynajmniej jednej kolumny; parametr nullColumnHack pozwala wskazać tę kolumnę.

448 Rozdział 11.


Bazy danych SQLite

Aktualizacja rekordów za pomocą metody update()


Rekordy zapisane w bazie danych SQLite możemy aktualizować za pomocą metody
update() klasy SQLiteDatabase. Metoda ta umożliwia aktualizowanie rekordów
i zwraca liczbę określającą ilość rekordów, których zawartość została zmieniona.
Aby użyć tej metody, musimy podać nazwę tabeli zawierającej rekordy, które
chcemy zmodyfikować, nowe wartości oraz warunki przeprowadzenia modyfikacji.
Ogólna postać metody update() wygląda tak:

public int update(String tabela,


ContentValues wartosci,
String klauzulaWhere,
String[] argumentyWhere)

Na przykład poniższy fragment kodu pozwala zmodyfikować wiersz tabeli DRINK, którego pole
NAME ma wartość ”Latte” i zapisać w jego kolumnie DESCRIPTION łańcuch ”Pyszności”.

ContentValues drinkValues = new ContentValues();


Dzięki temu wywołaniu w kolumnie
drinkValues.put(”DESCRIPTION”, ”Pyszności”); DESCRIPTION zostanie zapisany
db.update(“DRINK”, łańcuch znaków “Pyszności”.
drinkValues,
Zmieniamy wartość kolumny DESCRIPT
“NAME = ?”, w wierszu tabeli DRINK, w którym poleION na “Pyszności”
NAME ma wartość “Latte”.
new String[] {“Latte”});

_id NAME DESCRIPTON IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym mlekiem i mleczną pianką.” 54543543
“Pyszności”

Pierwszym parametrem metody update() jest nazwa tabeli, której zawartość


należy zaktualizować (w naszym przypadku jest to tabela DRINK).

Drugi parametr metody określa modyfikowane wartości. Podobnie jak Jeśli dwa ostatnie
Obejrzyj to!
w przypadku metody insert(), także teraz wartości te określamy, parametry metody
tworząc obiekt ContentValues zawierający pary nazwa – wartość: update() przyjmą
wartość null, to zostaną
ContentValues drinkValues = new ContentValues(); zmodyfikowane WSZYSTKIE
drinkValues.put(”DESCRIPTION”, ”Pyszności”); rekordy tabeli.
Trzeci parametr określa warunki, które muszą zostać spełnione, aby można
Na przykład poniższe wywołanie:
było wykonać aktualizację. W powyższym przykładzie warunek ”NAME = ?”
oznacza, że zawartość kolumny NAME musi być równa pewnej określonej db.update(”DRINK”,
wartości. Znak pytajnika (?) jest symbolem zastępczym reprezentującym drinkValues,
tę wartość. Konkretna wartość, która zostanie zastosowana w miejscu tego
null, null);
symbolu, jest określana na podstawie ostatniego parametru metody update().
zmodyfikuje wszystkie wiersze tabeli
Istnieje także możliwość określenia większej liczby takich kryteriów, DRINK.
o czym przekonasz się na następnej stronie.
jesteś tutaj  449
Warunki

Określanie wielu warunków ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Jeśli w zapytaniu chcemy zastosować więcej warunków, to koniecznie musimy upewnić się, że podamy je
dokładnie w takiej samej kolejności, w jakiej podane są dane. Na przykład poniżej przedstawiliśmy kod, który
zaktualizuje te rekordy tabeli DRINK, w których pole NAME ma wartość ”Latte”, lub w polu DESCRIPTION jest
umieszczony napis ”Czarna kawa ze świeżo mielonych ziaren najwyższej jakości”.
ME = “Latte ” or
db.update(”DRINK”, To oznacza: Where NA a kawa ze świeżo
DE SC RIP TIO N = “Czarn
drinkValues, ższej jakości.”.
“NAME = ? OR DESCRIPTION = ?”, mielonych ziaren najwy
new String[] {“Latte”, “Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”});
Wartościami warunków muszą być łańcuchy znaków (String), niezależnie od tego, czy same kolumny,
których będą dotyczyły te warunki, zawierają łańcuchy znaków czy dane innych typów. Jeśli kolumny używane
w warunkach zawierają dane innych typów, to będziemy musieli skonwertować je na łańcuchy znaków. Na przykład
poniższe wywołanie metody update() aktualizuje rekord tabeli DRINK, który w kolumnie _id ma wartość 1:
db.update(”DRINK”, Konwertujemy liczbę całkowitą
drinkValues, na łańcuch znaków.
”_id = ?”,
new String[] {Integer.toString(1)});

Do usuwania rekordów służy metoda delete()


Metoda delete() klasy SQLiteDatabase działa bardzo podobnie do przedstawionej
wcześniej metody update(). Oto jej ogólna postać:
public int delete(String tabela,
String klauzulaWhere,
String[] argumentyWhere)
Na przykład poniższe wywołanie metody delete() usuwa z tabeli wszystkie rekordy,
w których nazwą napoju jest ”Latte”:
da delete()
Zwróciłeś uwagę, jak bardzo meto
db.delete(”DRINK”, przypomina metodę update()?
”NAME = ?”,
Usuwany jest cały rekord.
new String[] {”Latte”});

_id NAME DESCRIPTON IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym mlekiem i mleczną pianką.” 54543543

Pierwszym parametrem metody delete() jest nazwa tabeli, z której chcemy usuwać rekordy (w powyższym
przykładzie jest to tabela DRINK). Drugi i trzeci parametr pozwalają określić warunki, które muszą zostać
spełnione, aby można było usunąć rekord (w naszym przykładzie warunek ten ma postać: NAME = ”Latte”).
Skoro już znasz rodzaje operacji używanych do operowania na danych w tabelach SQLite, dysponujesz już całą
wiedzą niezbędną do utworzenia bazy danych SQLite wraz z jej tabelami oraz wstępnego wypełnienia tych tabel
danymi. Na następnej stronie wykorzystamy całą tę wiedzę, by przedstawić kompletny kod pomocnika SQLite,
który zastosujemy w aplikacji dla kafeterii Coffeina.

450 Rozdział 11.


Bazy danych SQLite

Kod klasy CoffeinaDatabaseHelper ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

Poniżej przedstawiliśmy kompletną zawartość pliku CoffeinaDatabaseHelper.java


(zaktualizuj ten plik w swoim projekcie, tak by odpowiadał zamieszczonemu poniżej):
Coffeina

package com.hfad.coffeina;
app/src/main
import android.content.ContentValues;
import android.content.Context; java
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; com.hfad.coffeina

class CoffeinaDatabaseHelper extends SQLiteOpenHelper {


CoffeinaDatabase
private static final String DB_NAME = “coffeina”; // Nazwa bazy danych Helper.java
private static final int DB_VERSION = 1; // Numer wersji bazy danych
Te stałe określają nazwę bazy danych
CoffeinaDatabaseHelper(Context context) { i numer jej wersji. To pierwsza wersja
super(context, DB_NAME, null, DB_VERSION); naszej bazy, więc ma numer 1.
} Metoda onCreate() zostaje wywołan a podczas pierwszego tworzenia
bazy danych, dlatego właśnie jej używamy do utworzenia tabeli
@Override bazy danych i zapisania w niej początkowych danych.
public void onCreate(SQLiteDatabase db) {
db.execSQL(“CREATE TABLE DRINK (_id INTEGER PRIMARY KEY AUTOINCREMENT, “
+ “NAME TEXT, “
Tworzymy tabelę DRINK.
+ “DESCRIPTION TEXT, “
+ “IMAGE_RESOURCE_ID INTEGER);”);
insertDrink(db, “Latte”, “Czarne espresso z gorącym mlekiem i mleczną pianką.”,
Każdy napój R.drawable.latte);
zapisujemy
jako osobny insertDrink(db, “Cappuccino”, “Czarne espresso z dużą ilością spienionego mleka.”,
wiersz tabeli. R.drawable.cappuccino);
insertDrink(db, “Espresso”, “Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”,
R.drawable.cappuccino);
}
Metoda onUpgrade() jest wywoływana, kiedy pojawi się potrzeba
@Override aktualizacji bazy danych. Zajmiemy się nią nieco później.
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}

private static void insertDrink(SQLiteDatabase db, String name,


String description, int resourceId) {
ContentValues drinkValues = new ContentValues();
drinkValues.put(“NAME”, name); W tabeli musimy zapisać
drinkValues.put(“DESCRIPTION”, description); kilka napojów, dlatego
drinkValues.put(“IMAGE_RESOURCE_ID”, resourceId); napiszemy specjalną metodę,
która nam to ułatwi.
db.insert(“DRINK”, null, drinkValues);
}
}

jesteś tutaj  451


Co się dzieje?

Co robi kod pomocnika SQLite? ¨


¨
Utworzenie bazy danych
Utworzenie tabeli

1 Użytkownik instaluje aplikację, a następnie ją uruchamia.


Kiedy aplikacja zażąda dostępu do bazy danych, pomocnik SQLite sprawdza,
czy baza już istnieje.

Jaśnie panie,
czy potrzebuje pan bazy
danych? Pozwolę sobie sprawdzić,
czy baza dla jaśnie pana została
już przygotowana.

Pomocnik SQLite

2 Jeśli baza danych jeszcze nie istnieje, to zostaje utworzona.


Nazwa bazy danych i numer jej wersji są przekazywane do pomocnika SQLite.

Nazwa: "coffeina"
Wersja: 1

Baza danych SQLite

Pomocnik SQLite

3 Podczas tworzenia bazy danych zostaje wywołana metoda onCreate()


pomocnika SQLite.
Pomocnik dodaje do bazy danych tabelę DRINK i zapisuje w niej dane.

Jaśnie panie,
oto pańska baza DRINK
danych. Czy mogę
jeszcze czymś służyć? Nazwa: "coffeina"
Wersja: 1

onCreate()

Baza danych SQLite

Pomocnik SQLite

452 Rozdział 11.


Bazy danych SQLite

Zaostrz ołówek
Oto metoda onCreate() klasy SQLiteOpenHelper. Twoim zadaniem jest
określenie, jakie wartości będą zapisane w kolumnach NAME i DESCRIPTION
tabeli DRINK po wykonaniu tej metody.

@Override
public void onCreate(SQLiteDatabase db) {
ContentValues cappuccino= new ContentValues();
cappucino.put(”NAME”, ”Cappuccino”);
ContentValues americano = new ContentValues();
americano.put(”NAME”, ”Americano”);
ContentValues latte = new ContentValues();
latte.put(”NAME”, ”Latte”);
ContentValues espresso = new ContentValues();
espresso.put(”DESCRIPTION”, ”Espresso”);
ContentValues mochachino = new ContentValues();
mochachino.put(”NAME”, ”Mochachino”);

db.execSQL(”CREATE TABLE DRINK (”


+ ”_id INTEGER PRIMARY KEY AUTOINCREMENT, ”
+ ”NAME TEXT, ”
+ ”DESCRIPTION TEXT);”);
db.insert(”DRINK”, null, cappuccino);
db.insert(”DRINK”, null, americano);
db.delete(”DRINK”, null, null);
db.insert(”DRINK”, null, latte);
db.update(”DRINK”, mochachino, ”NAME = ?”, new String[] {”Cappuccino”});
db.insert(”DRINK”, null, espresso);
}

_id NAME DESCRIPTION


Wartości
kolumny
_id nie
musisz
uzupełniać.

jesteś tutaj  453


Zaostrz ołówek — rozwiązanie

Zaostrz ołówek
Rozwiązanie Oto metoda onCreate() klasy SQLiteOpenHelper. Twoim
zadaniem jest określenie, jakie wartości będą zapisane w kolumnach
NAME i DESCRIPTION tabeli DRINK po wykonaniu tej metody.

@Override
public void onCreate(SQLiteDatabase db) {
ContentValues cappuccino = new ContentValues();
cappuccino.put(”NAME”, ”Cappuccino”);
ContentValues americano = new ContentValues();
americano.put(”NAME”, ”Americano”);
ContentValues latte = new ContentValues();
latte.put(”NAME”, ”Latte”);
ContentValues espresso = new ContentValues();
espresso.put(”DESCRIPTION”, ”Espresso”);
ContentValues mochachino = new ContentValues();
mochachino.put(”NAME”, ”Mochachino”);
mny _id,
Tworzymy tabelę zawierającą kolu
db.execSQL(”CREATE TABLE DRINK (” NAME oraz DESCRIPTION.
+ ”_id INTEGER PRIMARY KEY AUTOINCREMENT, ”
+ ”NAME TEXT, ”
+ ”DESCRIPTION TEXT);”);
Wstawiamy łańcuch Cappuccino do kolumny NAME.
db.insert(”DRINK”, null, cappuccino);
Wstawiamy łańcuch Americano do kolumny NAME.
db.insert(”DRINK”, null, americano);
db.delete(”DRINK”, null, null); Usuwamy wszystkie wiersze tabeli.
db.insert(”DRINK”, null, latte); Wstawiamy łańcuch Latte do kolumny NAME.
db.update(”DRINK”, mochachino, ”NAME = ?”, new String[] {”Cappuccino”});
db.insert(”DRINK”, null, espresso);
W wierszu, w którym pole NAME ma
} wartość Cappuccino, zmieniamy wartość
Wstawiamy łańcuch Espresso do kolumny DESCRIPTION. tego pola na Mochachino. Żaden rekord
tabeli nie zostaje zmieniony.

_id NAME DESCRIPTION


Latte
Espresso

454 Rozdział 11.


Bazy danych SQLite

Co zrobić, gdy trzeba będzie zmienić bazę?


Dotychczas dowiedziałeś się, jak można utworzyć bazę danych, której aplikacja
będzie mogła używać do trwałego zapisywania danych. Ale co zrobić,
jeśli w przyszłości trzeba będzie wprowadzić jakieś zmiany w tej bazie?

Załóżmy na przykład, że nasza aplikacja została już zainstalowana przez


wielu użytkowników, a my doszliśmy do wniosku, że dobrze byłoby dodać
do tabeli DRINK nową kolumnę — FAVOURITE. Jak wprowadzić tę zmianę
u wszystkich użytkowników aplikacji — zarówno nowych, jak i tych,
którzy już ją zainstalowali?

No cóż… Moglibyśmy
zmienić polecenie CREATE TABLE podane
w metodzie onCreate(), ale wydaje mi się,
że takie rozwiązanie nie jest całkowicie
prawidłowe. Chodzi o to, że przecież mogą
istnieć urządzenia, na których baza już
będzie zainstalowana.

Kiedy pojawia się konieczność zmiany bazy danych aplikacji,


to musimy uwzględnić dwa podstawowe scenariusze.
Pierwszy z nich odpowiada sytuacji, w której użytkownik jeszcze nie
zainstalował aplikacji, więc na jego urządzeniu nie ma utworzonej bazy
danych. W takim przypadku pomocnik SQLite utworzy bazę danych za
pierwszym razem, gdy aplikacja będzie chciała z niej skorzystać, a to
oznacza, że zostanie wywołana metoda onCreate() pomocnika.

Drugi scenariusz odpowiada sytuacji, gdy użytkownik instaluje na swoim


urządzeniu nową wersję aplikacji korzystającą z innej wersji bazy danych.
Kiedy pomocnik SQLite wykryje, że baza danych dostępna na urządzeniu
jest nieaktualna, to wywoła metodę onUpgrade() bądź onDowngrade().

A jak pomocnik SQLite się zorientuje, że baza danych jest nieaktualna?

jesteś tutaj  455


Numery wersji

Bazy danych SQLite mają numer wersji ¨ Aktualizacja bazy danych

Pomocnik SQLite może określić, czy baza danych wymaga aktualizacji, na podstawie
jej numeru wersji. Numer wersji bazy danych określamy w konstruktorze pomocnika
SQLite, w którym jest on przekazywany w wywołaniu konstruktora klasy bazowej
— SQLiteOpenHelper.

Wcześniej w tym rozdziale określiliśmy numer wersji naszej bazy danych


w następujący sposób:
...
private static final String DB_NAME = ”coffeina”;
private static final int DB_VERSION = 1; Dla maniaków
StarbuzzDatabaseHelper(Context context) {
Bazy danych SQLite mają numer
super(context, DB_NAME, null, DB_VERSION); wersji, który jest używany przez
} pomocnik SQLite, i wewnętrzny
... numer schematu. Każda zmiana
schematu bazy danych, na przykład
W momencie tworzenia bazy danych jej numer wersji jest ustawiany zmiana struktury tabeli, powoduje
na podstawie numeru podanego w pomocniku SQLite, a następnie jest inkrementację numeru wersji
wywoływana metoda onCreate(). schematu. Ta wartość jest poza
naszą kontrolę jest ona używana
Kiedy uznamy, że konieczna jest aktualizacja bazy danych, musimy zmienić wewnętrznie przez bazę SQLite.
numer podany w pomocniku SQLite. Aby zaktualizować bazę, musimy podać
numer wyższy od stosowanego do tej pory, jeśli natomiast chcemy przywrócić
starszą wersję bazy, to wystarczy zmienić numer wersji na niższy:
...
W tym przykładzie zmieniamy num
private static final int DB_VERSION = 2; wersji bazy danych na wyższy, er
więc
... zostanie ona zaktualizowana.

W większości przypadków będziemy chcieli aktualizować bazy danych,


więc będziemy podawać wyższe numery wersji. Wynika to z faktu, że
przywrócenie starszej wersji bazy jest potrzebne wyłącznie w sytuacjach,
gdy chcemy wycofać modyfikacje wprowadzone we wcześniejszej aktualizacji.
Kiedy użytkownik instaluje na swoim urządzeniu najnowszą wersję aplikacji,
to za pierwszym razem, gdy będzie ona chciała użyć bazy, pomocnik SQLite
sprawdzi numer wersji podany w jego kodzie, porównując go z numerem
wersji bazy zainstalowanej na urządzeniu.
Jeśli numer wersji podany w kodzie pomocnika SQLite jest wyższy od
numeru wersji bazy, to pomocnik SQLite wywoła metodę onUpgrade().
Jeśli zaś numer wersji podany w kodzie pomocnika jest niższy od numeru
wersji bazy, to wywołana zostanie metoda onDowngrade().
Wywołanie każdej z tych metod sprawi, że w bazie danych zostanie zapisany
ten sam numer wersji, który jest podany w kodzie pomocnika SQLite.

456 Rozdział 11.


Bazy danych SQLite

Aktualizacja bazy danych — omówienie


Poniżej ogólnie opisaliśmy, co się dzieje w przypadku udostępnienia
nowej wersji aplikacji, w której w kodzie pomocnika SQLite
zmieniliśmy numer wersji bazy z 1 na 2:

1 Użytkownik instaluje nową wersję aplikacji i ją uruchamia.

Urządzenie
Użytkownik

2 Jeśli użytkownik zainstalował aplikację po raz pierwszy, to baza


danych jeszcze nie istnieje, więc pomocnik SQLite ją tworzy.
Pomocnik SQLite nadaje bazie nazwę i numer określone w jego kodzie.

Nazwa: „coffeina”
Wersja: 2

Pomocnik SQLite przypisze


bazie danych numer wersji
2, o ile to właśnie ten numer
Baza danych SQLite został podany w jego kodzie.

Pomocnik SQLite

3 Po utworzeniu bazy danych zostaje wywołana metoda onCreate()


pomocnika SQLite.
Metoda onCreate() zawiera kod, który określa zawartość bazy danych.

DRINK

onCreate() Nazwa: „coffeina”


Wersja: 2

Baza danych SQLite

Pomocnik SQLite

jesteś tutaj  457


Co się dzieje?

Ciąg dalszy historii


4 Jeśli użytkownik zainstalował aplikację już wcześniej i jej używał,
to baza danych będzie już istnieć.
Jeśli baza danych już istnieje, to pomocnik SQLite nie tworzy jej ponownie.

Doskonale, jak widzę,


jaśnie pan dysponuje już
bazą danych o numerze
wersji 1.
DRINK

Nazwa: „coffeina”
Wersja: 1

Baza danych SQLite

Pomocnik SQLite

5 Pomocnik bazy danych porównuje jej numer z numerem wersji podanym


w jego kodzie.
Jeśli numer wersji podany w kodzie pomocnika SQLite jest wyższy od numeru wersji
bazy, to pomocnik wywołuje metodę onUpgrade(). Jeśli natomiast numer wersji
podany w kodzie pomocnika SQLite jest niższy od numeru wersji bazy, to pomocnik
wywołuje metodę onDowngrade(). Następnie pomocnik zmienia numer wersji bazy
danych na ten, który został podany w jego kodzie.

DRINK

Nazwa: „coffeina”
Wersja: 1 2

Pomocnik SQLite wywołuje


metodę onUpgrade() (jeśli numer
Baza danych SQLite wersji jest wyższy), a następnie
zmienia numer wersji bazy
Pomocnik SQLite danych.

458 Rozdział 11.


Bazy danych SQLite

Jak pomocnik SQLite podejmuje decyzje?


Poniżej zamieściliśmy podsumowanie operacji wykonywanych przez pomocnik
SQLite w zależności od tego, czy baza danych już istnieje, i jej numeru wersji.

Baza danych 1 Jeśli baza danych jeszcze nie


nie istnieje istnieje, to pomocnik SQLite ją
tworzy, a następnie jest wywoływana
1 jego metoda onCreate().

Utworzenie bazy danych


2 Jeśli baza danych już istnieje,
to pomocnik SQLite porównuje
Wywołanie metody onCreate() numer wersji bazy z numerem
wersji podanym w jego kodzie.

Baza została 3 Jeśli numer wersji podany w kodzie


pomocnika SQLite jest wyższy od
utworzona numeru wersji bazy, to zostaje
wywołana metoda onUpgrade().
Następnie pomocnik SQLite zmienia
numer wersji bazy danych.
2 5 Numer wersji
z pomocnika
Baza danych SQLite jest 4 Jeśli numer wersji podany w kodzie
istnieje równy numerowi pomocnika SQLite jest niższy od
wersji bazy numeru wersji bazy, to zostaje
wywołana metoda onDowngrade().
Numer wersji 3 4 Numer wersji Następnie pomocnik SQLite zmienia
z pomocnika z pomocnika numer wersji bazy danych.
SQLite jest SQLite jest niższy
wyższy od od numeru wersji
numeru wersji bazy
bazy 5 Jeśli numer wersji podany w kodzie
Wywołanie metody Wywołanie metody
pomocnika SQLite jest taki sam jak
onUpgrade() onDowngrade() numer wersji przechowywany w bazie
danych, to nie jest wywoływana
żadna z metod.
Oznacza to, że baza danych jest aktualna.
Baza została Przywrócono
wcześniejszą wersję
zaktualizowana bazy

Teraz, kiedy już wiesz, w jakich okolicznościach są wywoływane


metody onUpgrade() i onDowngrade(), warto, żebyś się
dowidział, jak ich używać.
jesteś tutaj  459
Metoda onUpgrade()

Aktualizacja bazy w metodzie onUpgrade() ¨ Aktualizacja bazy danych

Metoda onUpgrade() ma trzy parametry: bazę danych SQLite, numer wersji


samej bazy danych oraz nowy numer wersji bazy danych przekazany do
Nowy numer wersji
konstruktora klasy bazowej SQLiteOpenHelper: Aktualny numer podany w kodzie
wersji bazy danych. pomocnika SQLite.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Tu możemy umieścić swój kod.
} Pamiętaj, że aby baza danych
została zaktualizowana, nowy
numer wersji musi być wyższy
Numery wersji są ważne, gdyż na ich podstawie można określić, jakie od dotychczasowego.
zmiany należy wprowadzić w zależności od aktualnie używanej wersji
bazy. Na przykład załóżmy, że musimy wykonać jakiś fragment kodu,
jeśli jest używana baza o numerze wersji 1. Kod realizujący taką
operację mógłby wyglądać tak:

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 1) {
Ten kod zostanie wykonany
// Kod wykonywany, gdy używana jest baza danych o numerze wersji 1 tylko wówczas, gdy
} używana jest baza danych
o numerze wersji 1.
}

Tych numerów wersji możemy także używać do wprowadzania dalszych


modyfikacji bazy:
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 1) { Ten kod zostanie wykonany
// Kod wykonywany, gdy używana jest baza danych o numerze wersji 1 wyłącznie w przypadku,
gdy używana jest baza
} danych o numerze wersji 1.
if (oldVersion <3) {
// Kod wykonywany, gdy używana jest baza danych o numerach wersji 1 lub 2
}
} Ten kod zostanie
wykonany, jeśli jest
używana baza danych
Dzięki zastosowaniu takiego rozwiązania można mieć pewność, że o numerze wersji 1
lub 2. Jeśli używana
w bazie danych zostaną wprowadzone wszelkie niezbędne aktualizacje, jest baza o numerze
niezależnie od tego, która wersja bazy będzie zainstalowana. wersji 1, to zostaną
wykonane bloki kodu
w obu instrukcjach
Metoda onDowngrade() działa bardzo podobnie do metody warunkowych.
onUpgrade(). Przedstawimy ją na następnej stronie.

460 Rozdział 11.


Bazy danych SQLite

Przywracanie starszej wersji bazy


za pomocą metody onDowngrade()
Metoda onDowngrade() nie jest używana równie często jak metoda onUpgrade(),
gdyż służy do przywracania poprzedniej wersji bazy danych. Taka operacja może się
przydać, jeśli udostępniliśmy nową wersję aplikacji zawierającą modyfikacje bazy
danych, lecz okazało się, że w tych zmianach jest jakiś błąd. W takich sytuacjach
metoda onDowngrade() pozwala odtworzyć zmiany i przywrócić wcześniejszą
wersję bazy danych.

Podobnie jak onUpgrade(), także metoda onDowngrade() ma trzy parametry:


modyfikowaną bazę danych, numer wersji samej bazy danych oraz numer wersji
przekazany w wywołaniu klasy bazowej SQLiteOpenHelper:

@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Tu możemy umieścić swój kod
} Aby przywrócić wcześniejszą wersję bazy, nowy
numer wersji musi być niższy do poprzedniego.

Podobnie jak w przypadku metody onUpgrade(), także w metodzie


onDowngrade() możemy używać przekazanych numerów, by odtworzyć zmiany
wprowadzone w konkretnej wersji bazy. Na przykład poniższa metoda pokazuje,
jak odtworzyć zmiany w bazie danych o numerze wersji 3:

@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 3) {
// Kod wykonywany, jeśli używana jest baza danych o numerze wersji 3
}
Ten kod zostanie wykonany, jeśli
} użytkownik korzysta z bazy danych
o numerze wersji 3, lecz chcemy
przywrócić jej wcześniejszą wersję.
A teraz zastosujmy te metody do zmodyfikowania struktury bazy
danych.

jesteś tutaj  461


Aktualizacja bazy danych

Zaktualizujmy bazę danych ¨ Aktualizacja bazy danych

Załóżmy, że musimy zaktualizować naszą bazę danych, a konkretnie dodać do


tabeli DRINK nową kolumnę. Ponieważ chcemy, by zmiana ta trafiła do wszystkich
użytkowników aplikacji, i obecnych, i przyszłych, musimy zadbać o to, by uwzględnić ją
zarówno w metodzie onUpdate(), jak i w metodzie onCreate(). Metoda onCreate()
sprawi, że nowi użytkownicy aplikacji będą dysponowali bazą z dodatkową kolumną,
Coffeina
natomiast metoda onUpdate() wprowadzi niezbędną modyfikację u użytkowników,
którzy już wcześniej zainstalowali aplikację.
app/src/main
Zamiast umieszczać podobny kod w obu metodach, onCreate() i onUpdate(),
napiszemy nową metodę, updateMyDatabase(), a następnie wywołamy ją w obu java
metodach, onCreate() i onUpdate(). Do metody updateMyDatabase() przeniesiemy
aktualny kod metody onCreate(), a oprócz tego uzupełnimy go o kod tworzący com.hfad.coffeina
dodatkową kolumnę. Dzięki takiemu rozwiązaniu będziemy w stanie umieścić cały kod
związany z tworzeniem bazy danych w jednym miejscu i łatwiej nam będzie zapanować
nad zmianami wprowadzanymi w każdej kolejnej wersji bazy danych. CoffeinaDatabase
Helper.java
...
@Override
public void onCreate(SQLiteDatabase db){ Zamiast tworzyć tabelę DRINK tutaj, kod,
updateMyDatabase(db, 0, DB_VERSION); który to robi, przenieśliśmy do metody
updateMyDatabase().
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
updateMyDatabase(db, oldVersion, newVersion); Wywołujemy metodę updateMyDatabase()
} w metodzie onUpdate(), przekazując do niej
wszelkie niezbędne argumenty.
private void updateMyDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 1) {
db.execSQL(“CREATE TABLE DRINK (_id INTEGER PRIMARY KEY AUTOINCREMENT, “
+ “NAME TEXT, “
Ten kod + “DESCRIPTION TEXT, “
wcześniej był + “IMAGE_RESOURCE_ID INTEGER);”);
umieszczony insertDrink(db, “Latte”, “Czarne espresso z gorącym mlekiem i mleczną pianką.”,
w metodzie
onCreate(). R.drawable.latte);
insertDrink(db, “Cappuccino”, “Czarne espresso z dużą ilością spienionego mleka.”,
R.drawable.cappuccino);
insertDrink(db, “Espresso”, “Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”,
R.drawable.filter);
}
if (oldVersion < 2) { Ten kod zostanie wykonany, jeśli
// Kod, który dodaje nową kolumnę na urządzeniu użytkownika będzie
już zainstalowana baza danych
} o numerze wersji 1.
}
...

462 Rozdział 11.


Bazy danych SQLite
Wczuj się w pomocnika SQLite
Z prawej strony zamieściliśmy kod
pomocnika SQLite. Twoim zadaniem jest
wcielić się w rolę tego pomocnika ...
i określić, które fragmenty class MyHelper extends SQLiteOpenHelper{
kodu zostaną wykonane
dla poszczególnych, StarbuzzDatabaseHelper(Context context){
przedstawionych poniżej super(context, “kamerdyner”, null, 4);
użytkowników. Kod, na }
którym powinieneś się skoncentrować,
zaznaczyliśmy literami. Aby ułatwić @Override
Ci rozwiązanie ćwiczenia, pierwszą public void onCreate(SQLiteDatabase db){
odpowiedź podaliśmy za Ciebie. A // Wykonanie fragmentu kodu A
...
}
Użytkownik 1. uruchamia aplikację
po raz pierwszy. @Override
Fragment kodu A. Na urządzeniu użytkownika public void onUpgrade(SQLiteDatabase db,
nie ma jeszcze bazy danych, dlatego wykonywana
jest metoda onCreate(). int oldVersion,
int newVersion){
Użytkownik 2. dysponuje bazą danych if (oldVersion < 2) {
o numerze wersji 1. B // Wykonanie fragmentu kodu B
...
}
if (oldVersion == 3) {
Użytkownik 3. dysponuje bazą danych C // Wykonanie fragmentu kodu C
o numerze wersji 2. ...
}
D // Wykonanie fragmentu kodu D
...
}
Użytkownik 4. dysponuje bazą danych
o numerze wersji 3.
@Override
public void onDowngrade(SQLiteDatabase db,
int oldVersion,
int newVersion){
Użytkownik 5. dysponuje bazą danych if (oldVersion == 3) {
o numerze wersji 4.
E // Wykonanie fragmentu kodu E
...
}
if (oldVersion < 6) {
Użytkownik 6. dysponuje bazą danych F // Wykonanie fragmentu kodu F
o numerze wersji 5. ...
}
}
}
jesteś tutaj  463
Rozwiązanie

Wczuj się w pomocnika SQLite. Rozwiązanie


Z prawej strony zamieściliśmy kod
pomocnika SQLite. Twoim zadaniem jest ...
wcielić się w rolę tego pomocnika class MyHelper extends SQLiteOpenHelper{
i określić, które fragmenty
kodu zostaną wykonane StarbuzzDatabaseHelper(Context context){
dla poszczególnych, super(context, “kamerdyner”, null, 4);
przedstawionych poniżej }
użytkowników. Kod, na Nowy numer wersji
bazy danych to 4.
którym powinieneś się skoncentrować, @Override
zaznaczyliśmy literami. Aby ułatwić public void onCreate(SQLiteDatabase db){
Ci rozwiązanie ćwiczenia, pierwszą A // Wykonanie fragmentu kodu A
...
odpowiedź podaliśmy za Ciebie. Metoda onCreate() będzie
} wywoływana tylko w przypadku,
gdy na urządzeniu użytkownika
Użytkownik 1. uruchamia aplikację jeszcze nie ma bazy danych.
po raz pierwszy. @Override
Fragment kodu A. Na urządzeniu użytkownika
public void onUpgrade(SQLiteDatabase db,
nie ma jeszcze bazy danych, dlatego wykonywana int oldVersion,
jest metoda onCreate(). int newVersion){
if (oldVersion < 2) {
Użytkownik 2. dysponuje bazą danych
o numerze wersji 1.
B // Wykonanie fragmentu kodu B
... Ten fragment zostanie wywołany,
Fragment kodu B, a następnie D. Baza danych musi jeśli użytkownik dysponuje bazą
zostać zaktualizowana, parametr oldVersion ma } o numerze wersji 1.
wartość 1. if (oldVersion == 3) { Ten fragment
C // Wykonanie fragmentu kodu C zostanie
Użytkownik 3. dysponuje bazą danych ... wywołany, jeśli
użytkownik
o numerze wersji 2. } dysponuje bazą
Fragment kodu D. Baza danych musi zostać D // Wykonanie fragmentu kodu D o numerze
zaktualizowana, parametr oldVersion ma wartość 2. wersji 3.
...
} Ten fragment zostanie wywołany,
Użytkownik 4. dysponuje bazą danych jeśli użytkownik dysponuje bazą
o numerze wersji 1, 2 lub 3.
o numerze wersji 3. @Override
Fragment kodu C, a następnie D. Baza danych musi public void onDowngrade(SQLiteDatabase db,
zostać zaktualizowana, parametr oldVersion ma
wartość 3. int oldVersion,
int newVersion){
Użytkownik 5. dysponuje bazą danych if (oldVersion == 3) {
o numerze wersji 4. E // Wykonanie fragmentu kodu E
... Ten fragment kodu nigdy nie zostanie
Żaden. Użytkownik dysponuje aktualną wersją bazy wywołany. Jeśli użytkownik dysponuje
danych. } bazą danych o numerze wersji 3., to jego
baza musi zostać zaktualizowana, a nie
if (oldVersion < 6) { przywrócona do wcześniejszej postaci.
Użytkownik 6. dysponuje bazą danych
o numerze wersji 5. F // Wykonanie fragmentu kodu F
...
Fragment kodu F. Baza danych musi zostać
przywrócona do wcześniejszej wersji, parametr }
oldVersion ma wartość 5. Ten fragment kodu zostanie wykonany, jeśli użytkownik
} dysponuje bazą o numerze wersji 5. Aby została
} wywołana metoda onDowngrade(), użytkownik musi
464 Rozdział 11. mieć bazę o numerze wersji wyższym niż 4, gdyż to
jest bieżący numer wersji bazy.
Bazy danych SQLite

Aktualizacja istniejącej bazy danych ¨ Aktualizacja bazy danych

W ramach aktualizacji bazy danych możemy wykonywać operacje


dwóch różnych typów:

 Modyfikacje rekordów bazy.


Z wcześniejszej części rozdziału wiesz, jak za pomocą metod insert(),
update() oraz delete() klasy SQLiteDatabase można odpowiednio
wstawiać, modyfikować i usuwać rekordy z baz danych SQLite. W ramach
aktualizacji bazy można dodawać do niej nowe rekordy albo modyfikować
lub usuwać rekordy już istniejące.

 Modyfikacje struktury bazy danych.


Wiesz już, jak tworzyć tabele bazy danych. Może jednak się zdarzyć,
że zechcesz dodać nowe kolumny do istniejącej tabeli, zmieniać nazwy
tabel albo nawet całkowicie je usuwać z bazy.

Na kilku następnych stronach pokażemy, jak wykonywać wszystkie te operacje,


przy czym zaczniemy od modyfikowania struktury bazy danych poprzez
dodawanie nowych kolumn do istniejącej tabeli.

Dodawanie nowych kolumn za pomocą kodu SQL


Z wcześniejszej części rozdziału wiesz, jak tworzyć nowe tabele, używając
polecenia SQL CREATE TABLE, takiego jak poniżej:
Kolumna _id jest kluczem głównym tabeli.

CREATE TABLE DRINK (_id INTEGER PRIMARY KEY AUTOINCREMENT,

Nazwa tabeli. NAME TEXT,


DESCRIPTION TEXT,
Kolumny tabeli.
IMAGE_RESOURCE_ID INTEGER)

Język SQL zawiera także polecenie ALTER, które pozwala modyfikować


strukturę istniejących tabel. Na przykład poniższe polecenie pozwala
dodać do tabeli nową kolumnę:
Nazwa tabeli.
ALTER TABLE DRINK
ADD COLUMN FAVORITE NUMERIC Kolumna, którą chcemy dodać.

W powyższym przykładzie do tabeli DRINK dodajemy nową kolumnę,


FAVORITE, zawierającą wartości liczbowe.

Na następnej stronie pokażemy, jak zmienić nazwę tabeli i całkowicie


usunąć tabelę z bazy danych.

jesteś tutaj  465


Modyfikacja tabel

Zmiana nazwy tabeli ¨ Aktualizacja bazy danych

Polecenia ALTER TABLE można także używać do zmiany nazwy tabeli.


Poniższy przykład pokazuje, jak zmienić tabelę DRINK na NAPOJE :
Bieżąca nazwa tabeli.
ANTER TABLE DRINK
RENAME NAPOJE
Nowa nazwa tabeli.

Usuwanie tabel
Oprócz dodawania i modyfikowania tabel można je także usuwać.
Służy do tego polecenie DROP TABLE:

DROP TABLE DRINK Nazwa tabeli, którą chcemy usunąć.

To polecenie jest przydatne w sytuacjach, kiedy w strukturze bazy danych


znajduje się tabela, która nie będzie nam już dłużej potrzeba i chcemy ją
usunąć, by zaoszczędzić miejsce.

Wykonywanie poleceń SQL za pomocą metody execSQL()


Jak już wiesz z wcześniejszej części rozdziału, polecenia SQL można
wykonywać za pomocą metody execSQL() klasy SQLiteDatabase:

SQLiteDatabase.execSQL(String sql);

Na przykład poniższe wywołanie tej metody dodaje do tabeli DRINK


nową kolumnę o nazwie FAVORITE:

db.execSQL(”ALTER TABLE DRINK ADD COLUMN FAVORITE NUMERIC;”);

Metody tej możemy używać zawsze, gdy chcemy wykonać na bazie danych
jakieś polecenie SQL.

Skoro już znasz różnego rodzaju operacje, które można wykonywać


w ramach aktualizowania bazy danych, spróbujmy je zastosować
w kodzie klasy CoffeinaDatabaseHelper.

466 Rozdział 11.


Bazy danych SQLite

Pełny kod pomocnika SQLite


Poniżej przedstawiliśmy kompletny kod klasy CoffeinaDatabaseHelper,
który dodaje do tabeli DRINK kolumnę FAVORITE (zmiany wprowadzone
w kodzie zostały wyróżnione pogrubioną czcionką):
Coffeina
package com.hfad.coffeina;
app/src/main

import android.content.ContentValues;
java
import android.content.Context;
import android.database.sqlite.SQLiteDatabase; com.hfad.coffeina
import android.database.sqlite.SQLiteOpenHelper;

CoffeinaDatabase
class CoffeinaDatabaseHelper extends SQLiteOpenHelper { Helper.java

private static final String DB_NAME = ”coffeina”; // Nazwa bazy danych


private static final int DB_VERSION = 2; // Numer wersji bazy danych

Zmiana numeru wersji na wyższy pozwala


CoffeinaDatabaseHelper(Context context) { pomocnikowi SQLite zorientować się,
super(context, DB_NAME, null, DB_VERSION); że chcemy zaktualizować bazę danych.

@Override
public void onCreate(SQLiteDatabase db) {
updateMyDatabase(db, 0, DB_VERSION); Kod, który wcześniej był umieszczony
} w metodzie onCreate(), teraz przenieśliśmy
do metody updateMyDatabase().

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
updateMyDatabase(db, oldVersion, newVersion);
} Kod służący do aktualizacji bazy
danych zaimplementowaliśmy w
metodzie updateMyDatabase().

Dalsza część kodu znajduje się


na następnej stronie.
jesteś tutaj  467
Więcej kodu

Kod pomocnika SQLite (ciąg dalszy) ¨ Aktualizacja bazy danych

private void updateMyDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {


if (oldVersion < 1) {
db.execSQL(”CREATE TABLE DRINK (_id INTEGER PRIMARY KEY AUTOINCREMENT, ”
+ ”NAME TEXT, ”
+ ”DESCRIPTION TEXT, ”
+ ”IMAGE_RESOURCE_ID INTEGER);”);
insertDrink(db, “Latte”, “Czarne espresso z gorącym mlekiem i mleczną pianką.”,
R.drawable.latte);
insertDrink(db, “Cappuccino”, “Czarne espresso z dużą ilością spienionego mleka.”,
R.drawable.cappuccino);
insertDrink(db, “Espresso”, “Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”,
R.drawable.filter);
}
if (oldVersion < 2) {
db.execSQL(”ALTER TABLE DRINK ADD COLUMN FAVORITE NUMERIC;”);
}
Do tabeli DRINK dodajemy kolumnę
} FAVORITE typu liczbowego.

private static void insertDrink(SQLiteDatabase db, String name,


String description, int resourceId) {
ContentValues drinkValues = new ContentValues();
drinkValues.put(“NAME”, name);
drinkValues.put(“DESCRIPTION”, description);
drinkValues.put(“IMAGE_RESOURCE_ID”, resourceId);
db.insert(“DRINK”, null, drinkValues);
}
}

Ten nowy kod oznacza, że na urządzeniach użytkowników, którzy już wcześniej


zainstalowali naszą aplikację, kolumna FAVORITE zostanie dodana do tabeli
DRINK podczas następnej próby odwołania się do bazy danych. Oznacza to także,
że na urządzeniach użytkowników, którzy po raz pierwszy instalują aplikację,
zostanie utworzona baza danych zawierająca nową kolumnę.

Na następnej stronie szczegółowo przeanalizujemy, co się dziej podczas


wykonywania tego kodu.

468 Rozdział 11.


Bazy danych SQLite

Co się dzieje podczas działania kodu?


1 Kiedy po raz pierwszy aplikacja odwołuje się do bazy danych, pomocnik SQLite
sprawdza, czy baza w ogóle istnieje.

Czy jaśnie pan


życzy sobie bazy danych?
Zaraz sprawdzę, czy baza
dla jaśnie pana została już
przygotowana.

Pomocnik SQLite

2 Jeśli baza jeszcze nie istnieje, to pomocnik SQLite tworzy ją, a następnie wywołuje
swoją metodę onCreate().
W naszym przypadku metoda onCreate() wywołuje metodę updateMyDatabase(). Ta druga
metoda tworzy tabelę DRINK (włącznie z nową kolumną) i zapisuje w niej trzy wiersze danych.

DRINK

Nazwa: „coffeina”
onCreate()
Wersja: 2

Baza danych SQLite


Pomocnik SQLite

3 Jeśli baza danych już istnieje, to pomocnik SQLite porównuje numer wersji bazy
z numerem wersji podanym w jego kodzie.
Jeśli numer wersji podany w kodzie pomocnika SQLite jest wyższy od numeru wersji bazy danych,
to pomocnik wywołuje metodę onUpgrade(). Jeśli natomiast numer wersji podany w pomocniku
SQLite jest niższy od numeru wersji bazy, pomocnik wywołuje metodę onDowngrade(). W naszym
przypadku numer wersji podany w pomocniku SQLite jest wyższy od numeru wersji bazy, dlatego
wywołana zostanie metoda onUpgrade(). Ta metoda wywoła z kolei metodę updateMyDatabase(),
która doda do tabeli DRINK nową kolumnę FAVORITE.

DRINK

Nazwa: „coffeina”
onUpgrade()
Wersja: 1 2

Baza danych SQLite


Pomocnik SQLite

jesteś tutaj  469


Przybornik

Twój przybornik do Androida


Rozdział 11.

Opanowałeś już rozdział 11. i dodałeś j


do swojego przybornika z narzędziami Pełny kod przykładowe
lika cji pre zen tow ane j
ap
umiejętność tworzenia, aktualizacji mo żes z
w tym rozdziale
i modyfikacji baz danych SQLite. pobrać z ser we ra FT P
wydawnictwa Helion:
ftp://ftp.helion.pl/
przyklady/andrrg.zip

CELNE SPOSTRZEŻENIA
 W systemie Android do trwałego  Podczas tworzenia bazy danych
przechowywania danych używane są bazy wywoływana jest metoda onCreate().
danych SQLite.  W razie konieczności aktualizacji bazy
 Dostęp do baz danych SQLite zapewnia danych wywoływana jest metoda
klasa SQLiteDatabase. onUpgrade().
 W tworzeniu baz danych i zarządzaniu nimi  Polecenia SQL można wykonywać za
pomaga nam pomocnik SQLite. Aby go pomocą metody execSQL(String)
utworzyć, należy napisać klasę dziedziczącą zdefiniowanej w klasie SQLiteDatabase.
po SQLiteOpenHelper.  Nowe rekordy można dodawać do tabel
 Tworząc pomocnik SQLite, trzeba za pomocą metody insert().
zaimplementować metody onCreate()  Do modyfikacji istniejących rekordów służy
i onUpgrade() klasy SQLiteOpenHelper.
metoda update().
 Baza danych jest tworzona, gdy aplikacja  Istniejące rekordy można usuwać za pomocą
po raz pierwszy zażąda dostępu do niej.
metody delete().
Konieczne jest przy tym określenie nazwy
bazy i jej numeru wersji, którego wartości
zaczynają się od 1. Jeśli nie podamy nazwy
bazy, zostanie ona utworzona tylko
w pamięci.

470 Rozdział 11.


12. Kursory i zadania asynchroniczne
Nawiązywanie połączenia
z bazą danych
Moja metoda
doInBackground() jest niesamowita.
Czy wyobrażasz sobie, ile czasu
zajęłoby wykonanie tych wszystkich
operacji, gdybym zostawiła je
na głowie pana Wątku
Głównego?

Jak łączysz swoje aplikacje z bazami danych SQLite?


Dotychczas dowiedziałeś się, jak tworzyć bazy danych, używając pomocnika SQLite. Kolejnym
krokiem będzie uzyskanie dostępu do tych baz danych w aktywnościach. Z tego rozdziału
dowiesz się, jak tworzyć kursory, by pobierać dane z bazy danych, jak poruszać się po
kursorach oraz jak pobierać z nich dane. Oprócz tego dowiesz się, jak używać adapterów
kursorów, by łączyć kursory z widokami list. A na koniec przekonasz się, że pisanie wydajnego
kodu wielowątkowego korzystającego z klasy AsyncTask może zagwarantować wysoką
wydajność działania aplikacji.

to jest nowy rozdział  471


Na czym stanęliśmy?

W poprzednim rozdziale…
Z rozdziału 11. dowiedziałeś się, jak napisać pomocnika SQLite, który utworzy
bazę danych, oraz jak utworzyć w takiej bazie danych tabelę i wypełnić ją danymi.
Dowiedziałeś się także, w jaki sposób stworzyć pomocnika SQLite, który będzie
sobie radzić z aktualizacjami bazy danych — modyfikacjami jej struktury
i zmianami przechowywanych w niej informacji.

W tym rozdziale pokażemy Ci, jak aktywności mogą prowadzić interakcję z bazą
danych, aby użytkownik, za pośrednictwem aplikacji, mógł odczytywać informacje
przechowywane w bazie i je zapisywać.
Przygotowaliśmy pomocnika SQLite
Oto aktualny schemat aplikacji dla kafeterii Coffeina: i napisaliśmy kod, który tworzy
bazę danych kafeterii Coffeina.
Aktualnie nie jest ona jeszcze
używana przez żadną aktywność.
Baza danych
kafeterii
Coffeina

Pomocnik SQLite

Aplikacja ciągle używa


klasy Drink.
<Layout>
<Layout>

</Layout>
</Layout>

activity_top_level.xml Drink.java activity_drink.xml

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie

Klasy DrinkActivity
i DrinkCategoryActivity
wciąż używają klasy Drink.

Planujemy zmodyfikować aplikację kafeterii Coffeina w taki sposób,


aby zamiast z klasy Drink korzystała z bazy danych SQLite.

472 Rozdział 12.


Kursory i zadania asynchroniczne

Zmienimy aplikację, by korzystała z bazy danych


Klasa Drink jest obecnie używana przez dwie aktywności. Musimy je zatem zmienić
w taki sposób, by odczytywały informacje z bazy danych SQLite, korzystając przy tym
z pomocnika SQLite. Oto, co planujemy zrobić:

1 Zaktualizować kod klasy DrinkActivity, korzystający z klasy Drink.


Kod klasy DrinkActivity używa klasy Drink do wyświetlania szczegółowych
informacji o wybranym napoju. Zmienimy go w taki sposób, by rekord z informacjami
o napoju był pobierany z bazy danych.

2 Zaktualizować kod klasy DrinkCategoryActivity, korzystający z klasy Drink.


Kod klasy DrinkCategoryActivity używa klasy Drink do wyświetlania listy
wszystkich napojów. Zmienimy go w taki sposób, by dane używane do przygotowania
listy były pobierane z rekordów tabeli DRINK.

3 Zapewnić użytkownikowi możliwość wybierania ulubionego napoju.


W rozdziale 11. zmodyfikowaliśmy tabelę DRINK, dodając do niej kolumnę FAVORITE.
Teraz zmienimy aplikację kafeterii Coffeina w taki sposób, że użytkownik będzie
mógł zaznaczać swoje ulubione napoje, a ich listę wyświetlimy w aktywności
TopLevelActivity.

Oto, jak będzie wyglądała struktura naszej aplikacji:

Baza danych
kafeterii
<Layout> Coffeina <Layout>

</Layout>
</Layout>

activity_top_level.xml Pomocnik SQLite activity_drink.xml

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie
Zmienimy aktywności korzystające
z klasy Drink, tak aby pobierały
informacje z bazy danych.
Zacznijmy od pliku DrinkActivity.java.

jesteś tutaj  473


Aktualny kod aktywności DrinkActivity

Aktualny kod aktywności DrinkActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Poniżej przypomnieliśmy, jak obecnie wygląda kod aktywności
DrinkActivity. Metoda onCreate() pobiera numer napoju wybranego
przez użytkownika i pobiera szczegółowe informacje o tym napoju z klasy
Drink, a następnie używa ich do wypełnienia widoków układu.

package com.hfad.coffeina;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;

public class DrinkActivity extends Activity {

public static final String EXTRA_DRINKNO = ”drinkNo”; Coffeina

@Override app/src/main
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); java

setContentView(R.layout.activity_drink); To jest napój wybrany


przez użytkownika. com.hfad.coffeina

// Pobieramy identyfikator napoju z intencji


int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO); DrinkActivity.java

Drink drink = Drink.drinks[drinkNo]; Używamy numeru napoju przekazanego w intencji,


aby pobrać dane napoju z klasy Drink. Ten fragment
kodu będziemy musieli zmienić, tak by informacje
// Wyświetlamy zdjęcie napoju te były pobierane z bazy danych.
ImageView photo = (ImageView)findViewById(R.id.photo);
photo.setImageResource(drink.getImageResourceId());
photo.setContentDescription(drink.getName());

// Wyświetlamy nazwę napoju


TextView name = (TextView)findViewById(R.id.name); W poszczególnych widokach
układu będziemy musieli zapisać
name.setText(drink.getName()); informacje odczytane z bazy
danych, a nie z klasy Drink.

// Wyświetlamy opis napoju


TextView description = (TextView)findViewById(R.id.description);
description.setText(drink.getDescription());
}
}

474 Rozdział 12.


Kursory i zadania asynchroniczne

Pobranie danych z bazy za pomocą kursora


Kod aktywności DrinkActivity w jego obecnej postaci zależy od klasy Drink,
z której pobierane są informacje o napoju wybranym przez użytkownika.
W jaki sposób możemy go zmienić, aby szczegółowe informacje o napoju były
pobierane z bazy danych? Jak zmienisz aktywność, by odczytywała potrzebne informacje
z bazy danych?

Odpowiedzią na to pytanie jest użycie kursora.

Kursory zapewniają dostęp do informacji


zgromadzonych w bazie danych
Kursor zapewnia nam dostęp do zbioru rekordów przechowywanych w bazie danych.
My określamy, do jakich danych chcemy mieć dostęp, a kursor pobiera odpowiednie
rekordy z bazy. Następnie możemy nawigować po rekordach udostępnianych przez kursor.

To jest baza danych.

To są informacje zapisane w bazie


danych, które chcemy odczytywać.

Kursor odczytuje
interesujące nas
informacje zapisane
w bazie danych.

_id NAME DESCRIPTION IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym 54543543
mlekiem i mleczną pianką.”
Możemy nawigować
po rekordach 2 “Cappuccino” “Czarne espresso z dużą ilością 654334453
udostępnianych przez spienionego mleka.”
kursor i odczytywać
ich wartości. 3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234
ziaren najwyższej jakości.”

Kursor tworzymy za pomocą zapytania określającego, do których informacji


zapisanych w bazie chcemy uzyskać dostęp. A czym jest to zapytanie?

jesteś tutaj  475


Prezentacja zapytań

Zapytania pozwalają określać, ¨  DrinkActivity


¨  DrinkCategoryActivity
jakie rekordy chcemy pobrać z bazy danych ¨  Ulubione napoje

Zapytania dają nam możliwość precyzyjnego określenia, do których rekordów bazy danych
chcemy uzyskać dostęp. Na przykład możemy stwierdzić, że interesują nas wszystkie rekordy
zapisane w tabeli DRINK albo tylko te, których nazwa zaczyna się od litery „L”. Im bardziej
możemy ograniczyć zbiór danych do pobrania, tym wydajniej będzie działać zapytanie.

Określanie tabel i kolumn


Pierwszą informacją, jaką musimy określić w zapytaniu, jest tabela, z której chcemy z kolumn NAME
pobrać rekordy, i jej kolumny, które nas interesują. Interesują nas dane DRINK.
i DE SC RIP TIO N tab eli

_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE


1 “Latte” “Czarne espresso z gorącym 54543543 0
mlekiem i mleczną pianką.”
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
spienionego mleka.”
3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234 0
ziaren najwyższej jakości.”

Zadeklarowanie warunków ograniczających dane


Kiedy już określimy tabelę i interesujące nas kolumny, możemy przefiltrować dane,
określając dodatkowe warunki, które muszą one spełnić. Na przykład chcemy
pobrać napój wybrany przez użytkownika; możemy to zrobić, odczytując z bazy tylko
te rekordy, które w kolumnie _id mają konkretną wartość.

_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE


1 “Latte” “Czarne espresso z gorącym 54543543 0
mlekiem i mleczną pianką.”
Interesuje nas 2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
wiersz, który spienionego mleka.”
w kolumnie _id
ma wartość 1. 3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234 0
ziaren najwyższej jakości.”

Inne zastosowania zapytań


Jeśli oczekujemy, że zapytanie zwróci kilka rekordów danych, to możemy także określić,
w jakiej kolejności mają one być uporządkowane. Na przykład możemy zażądać, by zwracane
informacje o napojach były posortowane alfabetycznie według nazwy napoju. Ponadto
zapytania mogą grupować dane w jakiś sposób i używać na tych grupach funkcji agregujących.
Na przykład możemy pobrać liczbę napojów zapisanych w bazie i wyświetlić ją w aplikacji.
A w jaki sposób można tworzyć takie zapytania?

476 Rozdział 12.


Kursory i zadania asynchroniczne

Metoda query() klasy SQLiteDatabase pozwala


tworzyć kod SQL za pomocą budowniczego zapytań
Do tworzenia zapytań służy metoda query() klasy SQLiteDatabase. Metoda query()
zwraca obiekt typu Cursor, którego możemy następnie używać w aktywnościach do
odczytywania informacji pobranych z bazy.

Oto podstawowa postać metody query():

public Cursor query(String tabela, To jest tabela i jej kolumny, je.


z których chcemy pobrać informac
String[] kolumny,
Metoda query() zwraca
obiekt typu Cursor. String warunki, Te argumenty umożliwiają określenie warunków.
String[] warunkiArg,
String groupBy,
Te argumenty umożliwiają
String having, agregację pobieranych danych.
String orderBy) Czy dane mają być zwracane
w jakiejś konkretnej kolejności?

Tej wersji metody query() możemy używać, by określić, z jakiej tabeli mają
pochodzić dane, z jakich kolumn należy je pobrać, jakie warunki należy przy tym
sprawdzić oraz czy dane należy w jakiś sposób zagregować i posortować.

Dostępnych jest kilka innych, przeciążonych wersji tej metody, umożliwiających Za kulisami Android
podawanie dodatkowych szczegółów dotyczących zapytania, takich jak: czy
chcemy, by każdy wiersz był unikalny, albo jaka ma być maksymalna liczba używa metody query()
zwróconych wierszy. Nie będziemy tu przedstawiać wszystkich wariantów
tej metody, jeśli jednak Cię interesują, to informacje o nich znajdziesz do wygenerowania
w dokumentacji Androida na stronie:
polecenia SQL SELECT.
http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html

Na kilku następnych stronach przedstawimy parę najczęściej używanych sposobów


korzystania z metody query().

jesteś tutaj  477


Tabele i kolumny

Określanie tabeli i kolumn ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Najprostszym rodzajem zapytania, jakie można utworzyć, jest zapytanie zwracające
określone kolumny wszystkich rekordów tabeli bez określania żadnych dodatkowych
warunków. Aby utworzyć takie zapytanie, musimy podać nazwę tabeli jako pierwszy
argument wywołania metody query(), a następnie podać tablicę łańcuchów znaków
(String) określających nazwy interesujących nas kolumn. Na przykład poniższe
wywołanie metody query() zwróci nazwy i opisy ze wszystkich wierszy tabeli DRINK:
Każdą kolumnę, z której wartości
jako
chcemy uzyskać, musimy podać
osob ną wart ość w tej tabli cy.
Cursor cursor = db.query(“DRINK”,
new String[] {“NAME”, “DESCRIPTION”},
To zapytanie używa tylko dwóch
pierwszych argumentów, dlatego null, null, null, null, null);
pozostałe mają wartość null.

NAME DESCRIPTION
To zapytanie zwraca wartości
kolumn NAME i DESCRIPTION ze “Latte” “Czarne espresso z gorącym
wszystkich wierszy tabeli DRINK. mlekiem i mleczną pianką.”
“Cappuccino” “Czarne espresso z dużą ilością
spienionego mleka.”
“Espresso” “Czarna kawa ze świeżo mielonych
ziaren najwyższej jakości.”

Ograniczenie wyników poprzez zastosowanie warunków


W zapytaniach można także stosować warunki określające, jakie wartości muszą
przyjmować poszczególne kolumny. Warunki te określamy przy użyciu trzeciego
i czwartego argumentu metody query(). Na przykład poniższe wywołanie zwróci
tylko te rekordy tabeli DRINK, w których w polu z nazwą jest zapisane słowo ”Lattte”:

Cursor cursor = db.query(”DRINK”,


new String[] {”NAME”, ”DESCRIPTION”},
“NAME = ?”, To oznacza „wiersze,
new String[] {“Latte”}, w których w kolumnie NAME
jest zapisane “Latte””.
null, null, null);

Trzeci argument, ”NAME = ?”, oznacza, że zawartość kolumny NAME powinna


być równa pewnej podanej wartości. Zastosowany tu pytajnik jest symbolem
zastępczym reprezentującym podaną wartość. Wartość ta jest określana na
podstawie czwartego argumentu wywołania metody query() (w powyższym
przykładzie jest to łańcuch znaków ”Latte”). NAME DESCRIPTION
To zapytanie zwraca wartości kolumn NAME
i DESCRIPTION ze wszystkich wierszy tabeli “Latte” “Czarne espresso z gorącym
DRINK, w których w kolumnie NAME jest zapisane mlekiem i mleczną pianką.”
słowo “Latte”.

478 Rozdział 12.


Kursory i zadania asynchroniczne

Zapytania z wieloma warunkami


Jeśli w zapytaniu chcemy zastosować kilka warunków, to musimy upewnić się, że
ich kolejność będzie taka sama jak kolejność podawanych wartości. Na przykład
poniższe zapytanie zwróci te wiersze tabeli DRINK, w których nazwa napoju ma
wartość ”Latte” lub opis ma wartość ”Czarna kawa ze świeżo mielonych
ziaren najwyższej jakości.”:
To oznacza „zwróć rekordy, w których
Cursor cursor = db.query(”DRINK”, kolumna NAME ma wartość “Latte” lub
kolumna DESCRIPTION ma wartość
new String[] {”NAME”, ”DESCRIPTION”}, “Czarna kawa ze świeżo mielonych ziaren
“NAME = ? OR DESCRIPTION = ?”, najwyższej jakości.””.

new String[] {“Latte”, “Czarna kawa ze świeżo mielonych ziaren najwyższej jakości.”},
null, null, null);

NAME DESCRIPTION
To zapytanie zwróci wartości kolumn NAME
i DESCRIPTION ze wszystkich wierszy tabeli, “Latte” “Czarne espresso z gorącym
w których kolumna NAME zawiera łańcuch mlekiem i mleczną pianką.”
znaków “Latte” lub kolumna DESCRIPTION
zawiera tekst “Czarna kawa ze świeżo “Espresso” “Czarna kawa ze świeżo mielonych
mielonych ziaren najwyższej jakości.”. ziaren najwyższej jakości.”

Jeśli warunki zostaną podane w innej kolejności niż wartości, to zapytanie zwróci
błędne dane. Na przykład w takim przypadku słowo „Latte” byłoby poszukiwane
w kolumnie DESCRIPTION, a nie NAME. W naszym przykładzie takie zapytanie nie
zwróciłoby żadnych rekordów.

Wartości używane w warunkach muszą być łańcuchami znaków


Wartości używane w warunkach muszą być typu String. Jeśli kolumny używane
w warunkach nie zawierają łańcuchów znaków, to i tak wartości warunków muszą
zostać skonwertowane na łańcuchy. Poniższy przykład pokazuje, w jaki sposób zwrócić
rekordy tabeli DRINK, w których pole _id ma wartość 1:

Cursor cursor = db.query(”DRINK”,


new String[] {”NAME”, ”DESCRIPTION”},
”_id = ?”, Konwertujemy liczbę 1
na łańcuch znaków.
new String[] {Integer.toString(1)},
null, null, null);
_id NAME DESCRIPTION
Zapytanie zwraca wartości kolumn NAME 1 “Latte” “Czarne espresso z gorącym
i DESCRIPTION ze wszystkich wierszy tabeli
DRINK, w których kolumna _id ma wartość 1.
mlekiem i mleczną pianką.”

jesteś tutaj  479


Sortowanie

Porządkowanie wyników zapytania ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Jeśli zależy nam, by dane wyświetlane w aplikacji były uporządkowane w jakiś
konkretny sposób, to możemy je posortować na podstawie zawartości wybranej
kolumny. Takie rozwiązanie będzie przydatne jeśli, na przykład, zechcemy
wyświetlać nazwy napojów w kolejności alfabetycznej.

Domyślnie, dane w tabeli są zwracane w kolejności rosnących wartości pola _id,


gdyż właśnie w takiej kolejności były one zapisywane w tabeli:

_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE


1 “Latte” “Czarne espresso z gorącym 54543543 1
mlekiem i mleczną pianką.”
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
spienionego mleka.”
3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234 0
ziaren najwyższej jakości.”

Aby pobrać dane z kolumn NAME i FAVORITE posortowane w kolejności rosnącej NAME FAVORITE
według wartości kolumny NAME, musimy użyć następującego wywołania:
“Cappuccino” 0
Cursor cursor = db.query(”DRINK”,
“Espresso” 0
new String[] {”_id”, ”NAME”, ”FAVORITE”},
null, null, null, null, “Latte” 1
“NAME ASC”); Sortujemy na podstawie zawartoś
kolumny NAME w kolejności rosn ci
ącej .
Słowo kluczowe ASC oznacza, że zawartość podanej kolumny ma zostać
posortowana w kolejności rosnącej. Kolumny są domyślnie sortowane w kolejności
rosnącej, więc słowo kluczowe ASC możemy pomijać. Aby posortować kolumnę
w kolejności malejącej, należy użyć słowa kluczowego DESC.

Można także sortować zwracane wyniki na podstawie kilku kolumn. Poniższy


przykład pokazuje, jak zwrócić dane posortowane na podstawie wartości kolumny
FAVORITE w kolejności malejącej i na podstawie wartości kolumny NAME
w kolejności rosnącej:
NAME FAVORITE
Cursor cursor = db.query(”DRINK”,
new String[] {”_id”, ”NAME”, ”FAVORITE”}, “Latte” 1
null, null, null, null, “Cappuccino” 0
“FAVORITE DESC, NAME”);
“Espresso” 0
Sortujemy na podstawie wartości kolumny FAVORITE
w kolejności malejącej, a następnie według wartości
kolumny NAME w kolejności rosnącej.

Teraz już znasz wszystkie najpopularniejsze sposoby stosowania


metody query(), ale to jeszcze nie wszystkie możliwości.
480 Rozdział 12.
Kursory i zadania asynchroniczne

Stosowanie funkcji SQL w zapytaniach


Jeśli znasz funkcje języka SQL, to zapewne ucieszysz się na
wiadomość, że można ich używać w zapytaniach. Pozwalają one
wykonywać takie operacje jak zwracanie liczby wierszy w tabeli,
obliczanie średniej z wartości kolumny lub wyznaczanie największej
wartości w kolumnie.

Oto kilka najbardziej przydanych funkcji SQL, których można


używać w zapytaniach:

AVG() Wartość średnia

COUNT() Liczba wierszy

SUM() Suma

MAX() Wartość maksymalna

MIN() Wartość minimalna

Na przykład chcąc policzyć, ile napojów zostało zapisanych w tabeli


DRINK, wystarczy użyć funkcji COUNT(), by policzyć wartości zapisane
w kolumnie _id: Ten zapis oznacza, że liczba napojów ma zostać
zwrócona jako kolumna o nazwie “count”.

Cursor cursor = db.query(”DRINK”,


new String[] {“COUNT(_id) AS count”},
null, null, null, null, null);

To zapytanie zw count
ra
wierszy tabeli DR ca liczbę
INK. 3

Gdyby tabela DRINK zawierała dodatkową kolumnę PRICE,


w której byłyby zapisane ceny poszczególnych napojów, to
używając funkcji AVG(), moglibyśmy obliczyć ich średnią cenę:

Cursor cursor = db.query(”DRINK”,


new String[] {“AVG(PRICE) AS price”},
null, null, null, null, null);

price Nasza tabela DRINK nie zawiera


kolumny PRICE, ale gdyby zawierała,
4.17 to moglibyśmy jej użyć do wyliczenia
średniej ceny napojów.

jesteś tutaj  481


Jeszcze więcej SQL-a

Klauzule GROUP BY i HAVING ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Jeśli znasz klauzule GROUP BY i HAVING języka SQL, to możesz ich
używać, przekazując je odpowiednio jako piąty i szósty argument
wywołania metody query().
W tej książce nie
Na przykład załóżmy, że chcemy sprawdzić, ile jest w tabeli DRINK Spokojnie chcemy nauczyć Cię
rekordów dla poszczególnych wartości z kolumny FAVORITE. W tym języka SQL, a jedynie
celu musielibyśmy stworzyć zapytanie zwracające wartości kolumny pokazujemy, co można
FAVORITE i liczbę napojów. Następnie musielibyśmy zgrupować wyniki zrobić za jego pomocą.
na podstawie kolumny FAVORITE i zsumować liczbę rekordów dla
Jeśli uważasz, że znajomość SQL-a może
każdej wartości występującej w tej kolumnie:
Ci się przydać, to polecamy książkę SQL.
Rusz głową!.

Cursor cursor = db.query(”DRINK”,


new String[] {“FAVORITE”, “COUNT(_id) AS count”},
null, null,
“FAVORITE”, Zapytanie zwraca kolumnę
Grupujemy na podstawie FAVORITE i liczbę napojów.
kolumny FAVORITE. null, null);

Zakładając, że tabela DRINK ma następującą zawartość:

_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE


1 “Latte” “Czarne espresso z gorącym 54543543 1
mlekiem i mleczną pianką.”
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
spienionego mleka.”
3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234 0
ziaren najwyższej jakości.”

Powyższe zapytanie zwróci następujące wyniki:


órego kolumna
st ty lko je de n napój, dla kt a napoje, dla
Je wartość 1, i dw
FAVORITE count FAVORITE ma a FAVORITE ma wartość 0.
któryc h ko lu mn
1 1
0 2

Skoro już umiesz tworzyć kursory za pomocą metody query(),


nadszedł czas, by utworzyć taki kursor na potrzeby aplikacji dla
kafeterii Coffeina.

482 Rozdział 12.


Kursory i zadania asynchroniczne

Magnesiki z kodem
W naszej aktywności DrinkActivity chcemy pobrać nazwę, opis oraz
identyfikator zasobu graficznego dla napoju, którego identyfikator
został przekazany w intencji. Czy potrafisz napisać metodę query(),
która pobierze te dane?

...

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Cursor cursor = db.query( ................ ,

new String[] { ............ , ............ , ............ },

”.....................”,

new String[] { .................................. },

null, null,null);

...

Integer id "IMAGE_RESOURCE_ID"
"NAME" drinkNo

_id
?
=
"DESCRIPTION
"
.
toString

) "DRINK"

jesteś tutaj  483


Rozwiązanie magnesików

Magnesiki z kodem. Rozwiązanie ¨  DrinkActivity


¨  DrinkCategoryActivity
W naszej aktywności DrinkActivity chcemy pobrać nazwę, opis oraz ¨  Ulubione napoje
identyfikator zasobu graficznego dla napoju, którego identyfikator
został przekazany w intencji. Czy potrafisz napisać metodę query(),
która pobierze te dane?

...

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


K.
Chcemy pobrać dane z tabeli DRIN
Cursor cursor = db.query( "DRINK" . , Pobieramy wartości z kolumn NAME, DESCRIPTION oraz IMAGE_RESOURCE_ID.

new String[] { . "NAME" . , "DESCRIPTION" .. , . "IMAGE_RESOURCE_ID" . },

”. _id . = .. ? ”, Wartość kolumny _id odpowiada wartości zmiennej drinkNo.

new String[] { Integer . . toString . ( . drinkNo . ) .. },

null, null,null);
drinkNo jest liczbą całkowitą,
dlatego musimy skonwertować ją
... na łańcuch znaków.
id

Pobranie referencji do bazy danych


Na kilu poprzednich stronach pokazywaliśmy, jak tworzyć zapytania, które zwracają Kursor pozwala nam
kursory. Metoda query() została zdefiniowana w klasie SQLiteDatabase, co oznacza,
odczytywać dane
że aby ją wywołać, będziemy potrzebowali referencji do naszej bazy danych. Okazuje
się, że klasa SQLiteOpenHelper definiuje dwie metody, które mogą nam w tym pomóc: z bazy danych.
getReadableDatabase() i getWritableDatabase(). Każda z nich zwraca obiekt typu
SQLiteDatabase zapewniający dostęp do bazy danych. Tych metod można używać
wnastępujący sposób:

SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);


SQLiteDatabase db = coffeinaDatabaseHelper.getReadableDatabase();
oraz

SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);


SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();

A czym te dwie metody różnią się od siebie?

484 Rozdział 12.


Kursory i zadania asynchroniczne

getReadableDatabase() kontra getWritableDatabase()


Być może pomyślałeś sobie, że metoda getReadableDatabase() zwraca obiekt bazy
danych przeznaczony wyłącznie do odczytu, a metoda getWritableDatabase() — obiekt
bazy, którego można używać do zapisywania w niej informacji. Okazuje się jednak, że
w przeważającej większości przypadków obie te metody zwracają referencję do tego samego
obiektu bazy danych. Tego obiektu można używać zarówno do odczytywania informacji
z bazy, jak i do ich zapisywania. Dlaczego zatem istnieje metoda getReadableDatabase(),
skoro zwraca ten sam obiekt co metoda getWritableDatabase()?

Podstawowa różnica między metodami getReadableDatabase() i getWritableDatabase()


polega na tym, co się stanie, kiedy okaże się, że zapisanie informacji w bazie danych nie
jest możliwe. Taka sytuacja może się zdarzyć na przykład wtedy, kiedy zabraknie miejsca
na twardym dysku.

Jeśli w takiej sytuacji zostanie zastosowana metoda getWritableDatabase(), to jej


wywołanie zakończy się niepowodzeniem i zgłoszeniem wyjątku typu SQLiteException.
Jeśli jednak zastosujemy metodę getReadableDatabase(), to spróbuje ona pobrać
referencję do bazy danych pozwalającą wyłącznie na odczyt informacji. Wciąż może się
jednak zdarzyć, że wywołanie tej metody zgłosi wyjątek SQLiteException, jeśli się okaże,
że nie można uzyskać dostępu do bazy danych, nawet w trybie tylko do odczytu. Używając metody
getReadableDatabase(),
Jeśli zatem chodzi nam jedynie o odczyt danych z bazy, to najlepszym rozwiązaniem będzie prawdopodobnie będziemy
zastosowanie metody getReadableDatabase(). Jeśli zaś musimy zapisywać informacje mogli zapisywać dane w bazie,
ale nie ma takiej gwarancji.
w bazie, to będziemy musieli zastosować metodę getWritableDatabase().

getReadableDatabase() getWritableDatabase()

Próba uzyskania dostępu Powodzenie Próba uzyskania dostępu Powodzenie


do bazy w trybie do do bazy w trybie do
odczytu/zapisu odczytu/zapisu
Baza danych Baza danych
Niepowodzenie Niepowodzenie do zapisu
do zapisu

Próba uzyskania dostępu Powodzenie


Metoda getWritableDatabase() próbuje
do bazy w trybie tylko do uzyskać dostęp do bazy danych
odczytu w trybie do odczytu i zapisu.
Baza danych Jeśli to się nie uda, to metoda
zgłosi wyjątek SQLiteException.
Niepowodzenie do odczytu

SQLiteException SQLiteException

kolejności próbuje
Metoda getReadableDatabase() w pierwszej odczytu i zapisu.
uzyskać dostęp do bazy danych w trybie do p w trybie tylko
Jeśli to się nie uda, to próbuje uzyskać dostę niepowodzeniem,
się
do odczytu. Jeśli także ta próba zakończy
to metod a zgłosi wyjąt ek SQLit eExce ption.
jesteś tutaj  485
Kod używający kursora

Kod tworzący kursor ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Kiedy złożymy te wszystkie informacje w jedną całość, uzyskamy poniższy
fragment kodu, który tworzy kursor. Zastosujemy go w metodzie onCreate() Nie musimy zapisywać niczego
naszej aktywności: w bazie danych, więc używamy
metody getReadableDatabase().
try {
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
SQLiteDatabase db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor cursor = db.query (”DRINK”,
new String[] {”NAME”, ”DESCRIPTION”, ”IMAGE_RESOURCE_ID”},
”_id = ?”, Kursor zawiera
new String[] {Integer.toString(drinkNo)}, pojedynczy rekord,
gdyż kolumna _id
null, null,null); zawiera unikalne
wartości.
Dysponujemy już kursorem,
// Kod, który używa do czegoś kursora lecz wciąż musimy go
w jakiś sposób wykorzystać.
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
} Jeśli baza danych jest niedostępna,
at.
to wyświetlamy komunik

Co robi ten kod?


1 Tworzymy pomocnik SQLite coffeinaDatabaseHelper.

coffeinaDatabaseHelper

2 Pomocnik coffeinaDatabaseHelper tworzy obiekt bazy danych zapisywany w zmiennej db.

coffeinaDatabaseHelper db

3 Wywołanie metody query() klasy SQLiteDatabase powoduje utworzenie kursora.

query(...)

coffeinaDatabaseHelper db cursor

486 Rozdział 12.


Kursory i zadania asynchroniczne

Aby odczytać rekord z kursora,


najpierw należy do niego przejść
Już wiesz, jak utworzyć kursor: wystarczy wywołać metodę query() klasy
SQLiteDatabase, określając przy tym, jakie dane kursor ma zwrócić. Ale to
jeszcze nie koniec historii — musimy jakoś odczytać wartości z kursora.

Ilekroć chcemy pobrać wartości z konkretnego rekordu dostępnego


w kursorze, musimy najpierw do niego przejść. Trzeba to robić zawsze,
niezależnie od tego, ile rekordów zostało zwróconych w kursorze.

Rekordy, które chcemy pobrać, określamy,


tworząc odpowiednie zapytanie i wykonując
je w bazie danych.

Kursor zawiera
rekordy opisane
w zapytaniu.

_id NAME DESCRIPTION IMAGE_RESOURCE_ID


1 “Latte” “Czarne espresso z gorącym 54543543
mlekiem i mleczną pianką.”
Aby odczytać dane
z rekordu zwróconego 2 “Cappuccino” “Czarne espresso z dużą ilością 654334453
w kursorze, musimy spienionego mleka.”
najpierw do tego
rekordu przejść. 3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234
ziaren najwyższej jakości.”

Na następnej stronie pokażemy, jak poruszać się po kursorze.

jesteś tutaj  487


Poruszanie się po kursorze

Poruszanie się po kursorze ¨  DrinkActivity


¨  DrinkCategoryActivity
Istnieją cztery podstawowe metody służące do poruszania się po ¨  Ulubione napoje
kursorach: moveToFirst(), moveToLast(), moveToPrevious()
oraz moveToNext(). Przechodzimy do pierwszego wiersza.

Aby uzyskać dostęp do pierwszego rekordu zwróconego przez


kursor, należy wywołać metodę moveToFirst() (metoda ta zwraca NAME DESCRIPTION
wartość true, jeśli uda się jej znaleźć rekord, lub wartość false, “Latte” “Czarne espresso z gorącym
mlekiem i mleczną pianką.”
jeśli kursor nie zwrócił żadnych rekordów):
“Cappuccino” “Czarne espresso z dużą ilością
if (cursor.moveToFirst()) { spienionego mleka.”

// Coś robimy “Espresso” “Czarna kawa ze świeżo mielonych


ziaren najwyższej jakości.”
};

Jeśli chcemy przejść do ostatniego wiersza zwróconego przez kursor,


wystarczy wywołać metodę moveToLast() (podobnie jak metoda
moveToFirst(), także moveToLast() zwraca wartość true, jeśli uda
się jej znaleźć rekord, albo wartość false w przeciwnym razie):
if (cursor.moveToLast()) { NAME DESCRIPTION
// Coś robimy “Latte” “Czarne espresso z gorącym
}; mlekiem i mleczną pianką.”
“Cappuccino” “Czarne espresso z dużą ilością
Do poruszania się po kolejnych wierszach w kursorze służą metody spienionego mleka.”

moveToPrevious() i moveToNext(). “Espresso” “Czarna kawa ze świeżo mielonych


ziaren najwyższej jakości.”
Pierwsza z nich, moveToPrevious(), przenosi nas do poprzedniego
Przechodzimy do ostatniego wiersza.
wiersza w kursorze (jej wywołanie zwraca wartość true, jeśli
uda się znaleźć rekord, lub false, jeśli okaże się to niemożliwe
— na przykład dlatego, że kursor jest pusty albo znajdujemy się już
w pierwszym wierszu kursora):
NAME DESCRIPTION
if (cursor.moveToPrevious()) { “Latte” “Czarne espresso z gorącym
// Coś robimy mlekiem i mleczną pianką.”

}; “Cappuccino” “Czarne espresso z dużą ilością


spienionego mleka.”

Metoda moveToNext() działa tak samo jak moveToPrevious(), “Espresso” “Czarna kawa ze świeżo mielonych
ziaren najwyższej jakości.”
z tym, że zamiast do poprzedniego przenosi nas do następnego wiersza
w kursorze (jej wywołanie zwraca wartość true, jeśli udało się przejść Przechodzimy do poprzedniego wiersza.
do następnego wiersza, lub wartość false w przeciwnym razie):
if (cursor.moveToNext()) { NAME DESCRIPTION
// Coś robimy “Latte” “Czarne espresso z gorącym
mlekiem i mleczną pianką.”
};
“Cappuccino” “Czarne espresso z dużą ilością
Kiedy już przejdziemy do wiersza w kursorze, możemy uzyskać spienionego mleka.”

dostęp do zapisanych w nim wartości. Na następnej stronie “Espresso” “Czarna kawa ze świeżo mielonych
ziaren najwyższej jakości.”
pokazaliśmy, jak to zrobić.

488 Rozdział 12. Przechodzimy do następnego wiersza.


Kursory i zadania asynchroniczne

Pobieranie wartości z kursora


Kiedy już przejdziemy do wybranego rekordu w kursorze, będziemy mogli
odczytać zapisane w nim wartości, tak by później wyświetlić je w widokach
używanych przez aktywność. Do pobierania wartości z kursora służą metody
get*(). To, której metody użyjemy, będzie zależało od typu pobieranej
wartości. Na przykład metoda getString() zwraca wartość kolumny w postaci
łańcucha znaków, a metoda getInt() — w postaci liczby całkowitej.
Każda z tych metod pobiera jeden argument — indeks kolumny.

Oto zapytanie zastosowane do utworzenia kursora:

Cursor cursor = db.query (”Drink”,


new String[] {”NAME”, ”DESCRIPTION”, ”IMAGE_RESOURCE_ID”},
”_id = ?”,
new String[] {Integer.toString(1)},
null, null,null);
Kolumna 0 Kolumna 1 Kolumna 2
Utworzony kursor składa się z trzech kolumn: NAME, DESCRIPTION oraz
IMAGE_RESOURCE_ID. Pierwsze dwie kolumny zawierają dane typu String,
trzecia zaś — dane typu int. NAME DESCRIPTION IMAGE_
RESOURCE_
Załóżmy, że chcemy pobrać wartość kolumny NAME z bieżącego rekordu. ID
NAME jest pierwszą kolumną kursora i zawiera wartości typu String. “Latte” “Czarne espresso 54543543
Oznacza to, że jej wartość możemy pobrać w poniższy sposób, używając z gorącym mlekiem
metody getString(): i mleczną pianką.”

String name = cursor.getString(0); To jest pierwsza kolumna kursora.

A teraz załóżmy, że chcemy pobrać zawartość kolumny IMAGE_RESOURCE_ID.


To trzecia kolumna kursora, która zawiera wartości typu int, a zatem jej
zawartość możemy pobrać w następujący sposób:

int imageResource = cursor.getInt(2);


Szczegółowe informacje na temat
wszystkich metod służących do pobierania
Na końcu należy zamknąć kursor i bazę danych danych z kursorów można znaleźć na
stronie http://developer.android.com/
Po zakończeniu pobierania danych z kursora musimy zamknąć zarówno reference/android/database/Cursor.html.
kursor, jak i samą bazę danych, aby zwolnić przydzielone im zasoby.
Służą do tego metody close() klas SQLiteDatabase i Cursor:

cursor.close();
db.close();

W ten sposób przedstawiliśmy już cały kod, którym musimy zastąpić bieżący
kod aktywności DrinkActivity, aby pobierać informacje o napojach z bazy
danych. Zobaczmy zatem, jak wygląda ten kod.

jesteś tutaj  489


Kod aktywności DrinkActivity

Kod aktywności DrinkActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Oto kompletna zawartość pliku DrinkActivity.java (wprowadź zmiany
zaznaczone pogrubioną czcionką, a następnie zapisz plik):

package com.hfad.coffeina;

import android.app.Activity;
Coffeina
import android.os.Bundle;
import android.widget.ImageView;
app/src/main
import android.widget.TextView; Tych dodatkowych klas
używamy w kodzie
import android.widget.Toast; aktywności. java
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; com.hfad.coffeina

import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper; DrinkActivity.java

public class DrinkActivity extends Activity {

public static final String EXTRA_DRINKNO = ”drinkNo”;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drink);
To jest identyfikator napoju
wybranego przez użytkownika.
// Pobieramy identyfikator napoju z intencji
int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

// Tworzymy kursor.
try {
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
SQLiteDatabase db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor cursor = db.query (“DRINK”,
new String[] {“NAME”, “DESCRIPTION”,
Tworzymy kursor, który pobiera z tabeli “IMAGE_RESOURCE_ID”},
DRINK bazy danych wartości kolumn NAME,
DESCRIPTION oraz IMAGE_RESOURCE_ID “_id = ?”, Dalsza część kodu
wiersza, w którym wartość kolumny _id new String[] {Integer.toString(drinkNo)}, znajduje się na
odpowiada wartości zmiennej drinkNo. następnej stronie.
null, null,null);
490 Rozdział 12.
Kursory i zadania asynchroniczne

Kod aktywności DrinkActivity (ciąg dalszy)


Coffeina

// Przechodzimy do pierwszego rekordu w kursorze


app/src/main
if (cursor.moveToFirst()) { W kursorze znajduje się tylko jede
wiersz, ale i tak musimy do nieg n
Nazwa napoju o przejść.
jest pierwszym java
elementem kursora, // Pobieramy z kursora szczegółowe informacje o napoju
opis znajduje się
w drugiej kolumnie, String nameText = cursor.getString(0); com.hfad.coffeina
a identyfikator
zasobu graficznego String descriptionText = cursor.getString(1);
w trzeciej. To DrinkActivity.java
uporządkowanie int photoId = cursor.getInt(2);
wynika z faktu,
że kazaliśmy,
by kursor // Wyświetlamy nazwę napoju
pobrał z bazy
kolumny NAME, TextView name = (TextView)findViewById(R.id.name);
DESCRIPTION name.setText(nameText);
oraz IMAGE_
RESOURCE_ID W widokach zapisujemy
w podanej dane pobrane z kursora.
kolejności. // Wyświetlamy opis napoju
TextView description = (TextView)findViewById(R.id.description);
description.setText(descriptionText);

// Wyświetlamy zdjęcie napoju


ImageView photo = (ImageView)findViewById(R.id.photo);
photo.setImageResource(photoId);
photo.setContentDescription(nameText);
}
cursor.close();
Zamykamy kursor i bazę danych.
db.close();
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, “Baza danych jest niedostępna”,
Toast.LENGTH_SHORT);
ł problemy
toast.show(); Jeśli zostanie zgłoszony wyjątek SQLiteException, będzie on oznacza lić
z bazą danych. W takim przypad ku używam y klasy Toast, by wyświet
} użytkownikowi stosowny komunikat.
}
} Połączenie aktywności
Spokojnie z bazą danych wymaga
Tak oto zakończyliśmy prace nad kodem aktywności czegoś więcej niż użycia klasy.
DrinkActivity. Zobaczmy, czym musimy się zająć
Jeśli jednak poświęcisz czas na przyswojenie
w następnej kolejności.
informacji zamieszczonych w tym rozdziale,
to dasz sobie z tym radę.

jesteś tutaj  491


Na czym stanęliśmy?

Co udało się nam zrobić? ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Teraz, kiedy już zakończyliśmy aktualizowanie kodu aktywności DrinkActivity,
przyjrzyjmy się strukturze naszej aplikacji, aby sprawdzić, co już zrobiliśmy,
a co jeszcze przed nami.

<Layout> <Layout>

</Layout> </Layout>

Aktywność
activity_top_level.xml DrinkCategoryActivity Drink.java activity_drink.xml
wciąż pobiera dane
z klasy Drink.

TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java


Urządzenie

Aktywność
DrinkActivity już
zmodyfikowaliśmy,
dzięki czemu pobiera
dane z bazy danych
Obecnie aktywność DrinkActivity pobiera wszystkie kafeterii Coffeina,
używane dane z bazy danych kafeterii Coffeina. Naszym używając do tego
kolejnym zadaniem będzie aktualizacja kodu aktywności pomocnika SQLite.
DrinkCategoryActivity, tak by ona także pobierała Pomocnik
Baza danych
informacje z bazy danych, a nie z klasy Drink. SQLite
kafeterii Coffeina

Nie istnieją
głupie pytania

P: W jakim stopniu muszę znać język SQL, by tworzyć  P: Napisaliście, że jeśli nie uda się uzyskać dostępu do bazy 
kursory? danych, to zostanie zgłoszony wyjątek SQLiteException. 
W jaki sposób powinienem go obsługiwać?
O: Przydałaby się znajomość polecenia SELECT języka SQL, gdyż
za kulisami metoda query() tworzy właśnie takie polecenie. O: Przede wszystkim sprawdź szczegółowe informacje zapisane
Ogólnie rzecz biorąc, zapytania, które będziesz tworzyć, raczej nie w wyjątku. Przyczyną wyjątku może być także błąd w składni
będą zbyt skomplikowane, niemniej jednak znajomość języka SQL zapytania SQL, a ten możesz samodzielnie poprawić.
może być przydatną umiejętnością. Sposób obsługi wyjątków tego rodzaju zależy od znaczenia
bazy danych dla działania tworzonej aplikacji. Na przykład jeśli
możesz uzyskać dostęp do bazy w trybie tylko do odczytu, lecz
nie możesz w niej zapisywać danych, to wciąż możesz zapewnić
użytkownikom możliwość odczytu danych z bazy, a jednocześnie
ostrzec ich, że zmian, które wprowadzą, nie będzie można zapisać.
Innymi słowy: wszystko zależy od aplikacji.

492 Rozdział 12.


Kursory i zadania asynchroniczne

Aktualny kod aktywności DrinkCategoryActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Poniżej przypomnieliśmy, jak obecnie wygląda kod aktywności
DrinkCategoryActivity zapisany w pliku DrinkCategoryaActivity.java.
W metodzie onCreate() określona jest zawartość widoku ListView —
używamy przy tym tablicy napojów i adaptera ArrayAdapter. W metodzie Aktywność
onListItemClick() indeks napoju wybranego przez użytkownika dodajemy DrinkCategoryActivity
wyświetla listę napojów.
do intencji, a następnie uruchamiamy aktywność DrinkActivity:

package com.hfad.coffeina;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter; Coffeina
import android.widget.ListView;
import android.view.View; app/src/main
import android.content.Intent;
java
public class DrinkCategoryActivity extends ListActivity {
com.hfad.coffeina

@Override
protected void onCreate(Bundle savedInstanceState) { DrinkCategory
Activity.java
super.onCreate(savedInstanceState);
ListView listDrinks = getListView();
ArrayAdapter<Drink> listAdapter = new ArrayAdapter<Drink>(
this,
android.R.layout.simple_list_item_1,
Drink.drinks); Obecnie, aby powiązać tablicę
listDrinks.setAdapter(listAdapter); z widokiem ListView, używamy
adaptera ArrayAdapter. Musimy
} zmodyfikować ten kod, aby
informacje były pobierane
z bazy danych.
@Override
public void onListItemClick(ListView listView,
View itemView,
int position,
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int)id);
startActivity(intent);
}
}

jesteś tutaj  493


Adaptery kursorów

Jak zastąpić tablicę przekazywaną ¨  DrinkActivity


¨  DrinkCategoryActivity
do komponentu ListView? ¨  Ulubione napoje

Widok listy listDrinks pobiera dane z tablicy Drink.drinks. Obecnie


moglibyśmy odczytywać listę napojów z bazy danych, a następnie zapisywać
ich nazwy w tablicy i przekazywać ją do adaptera.

Takie rozwiązanie działałoby prawidłowo, ale może potrafisz powiedzieć,


dlaczego jego zastosowanie nie jest dobrym pomysłem?
W takim przypadku moglibyśmy dalej używać adaptera
ArrayAdapter i nie musielibyśmy wprowadzać dalszych
Moglibyśmy po prostu odczytać dane z bazy zmian w kodzie. Ale czy zastosowanie takiego
i zapisać je w tablicy. rozwiązania byłoby dobrym pomysłem?

Dane Tablica Adapter Widok


z bazy Array ListView
danych Adapter

W przypadku naszej bardzo małej bazy danych pobranie wszystkich


informacji z bazy i zapisanie ich w tablicy przechowywanej w pamięci
nie stanowi większego problemu. Gdyby jednak nasza aplikacja
przechowywała ogromne ilości danych, to odczytanie ich wszystkich
z bazy mogłoby zająć trochę czasu. Co więcej, zapisanie tych wszystkich
danych w pamięci mogłoby zająć sporo miejsca.

Dlatego w nowej wersji aplikacji adapter ArrayAdapter zastąpimy


adapterem CursorAdapter.

Dane Cursor Adapter Widok


z bazy Cursor ListView
danych Adapter

Adapter CursorAdapter w dużym stopniu przypomina ArrayAdapter, Nasze dane są przechowywane w kursorze,
dlatego możemy przekazać je do widoku
choć różni się od niego tym, że zamiast z tablicy pobiera dane z kursora. ListView, używając adaptera klasy
CursorAdapter.
Zobaczmy, jak działa taki adapter.

494 Rozdział 12.


Kursory i zadania asynchroniczne

Adapter CursorAdapter odczytuje tyle danych, ile trzeba


Wyobraźmy sobie, że nasza baza danych jest o wiele większa. Załóżmy, że kafeteria Coffeina,
wychodząc naprzeciw oczekiwaniom hipsterskiej klienteli, w znaczący sposób powiększyła
wybór oferowanych kaw. Teraz dostępne są różne gatunki kawy oraz rozmaite rodzaje mleka,
posypek i dodatków alkoholowych, dlatego zamiast trzech rodzajów kaw mamy ich w sumie
ponad trzysta w bazie danych. Jednak w danej chwili na liście widocznych jest tylko kilka.

Widok ListView może prezentować jedynie ograniczoną liczbę elementów w każdej chwili.
Na niewielkich urządzeniach w widoku tym będzie widocznych tylko pierwszych jedenaście
napojów. Gdybyśmy używali tablicy, to musielibyśmy wczytać z bazy wszystkie trzysta napojów,
zapisać je w tablicy, a następnie wyświetlić.

Jednak adaptery typu CursorAdapter działają inaczej.

1 Widok ListView zostaje wyświetlony na ekranie.


Kiedy lista jest wyświetlana po raz pierwszy, jej wielkość zostaje dopasowana
do rozmiarów ekranu. Załóżmy, że znajdzie się w niej miejsce na wyświetlenie
pięciu elementów.

To są elementy, które
będą widoczne po
wyświetleniu na
liście ListView. Aby
uprościć przykład,
ograniczymy tę liczbę
do pięciu, choć
w praktyce pewnie
będzie ich więcej. Widok ListView

2 Widok ListView prosi adapter o pięć pierwszych elementów.


Widok ListView nie wie, skąd pochodzą elementy — czy z tablicy, czy z bazy
danych — wie natomiast, że adapter dostarczy mu niezbędnych danych.
Prosi zatem adapter o przekazanie pierwszych pięciu elementów.

A pewnie,
Hej, zaraz je
adapterze, czy zdobędę.
możesz mi przekazać
pięć pierwszych
elementów?

Widok ListView Adapter


CursorAdapter

jesteś tutaj  495


Co się dzieje?

Ciąg dalszy historii ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje

3 Adapter CursorAdapter prosi kursor o odczytanie pięciu wierszy z bazy danych.


W momencie tworzenia adaptera CursorAdapter przekazujemy do niego kursor, dzięki
czemu adapter może pobierać dane z kursora tylko wtedy, gdy są one potrzebne.

Hej, kursorze,
potrzebuję pięciu
elementów danych.

Kursor
Adapter CursorAdapter

4 Kursor odczytuje pięć pierwszych wierszy z bazy danych.


Choć tabela bazy danych zawiera trzysta rekordów, to kursor musi odczytać tylko pięć z nich.
Takie rozwiązanie jest znacznie bardziej wydajne i oznacza, że informacje będą mogły zostać
wyświetlone na ekranie znacznie szybciej.

Kursor
Baza danych

5 Użytkownik przewija listę.


Kiedy użytkownik przewinie listę, to adapter CursorAdapter poprosi kursor o odczytanie
kilku kolejnych rekordów z bazy danych. Jeśli użytkownik przewinie listę tylko trochę
i wyświetli tylko jeden nowy element, to kursor pobierze z bazy tylko jeden element.

Kiedy użytkownik przewija listę,


enty,
wyświetlane są jej kolejne elem .
a zatem potrzebne są kolejne dane

Kursor
Widok ListView Adapter CursorAdapter Baza danych

Jak więc widać, adapter CursorAdapter jest znacznie bardziej wydajny od kursora ArrayAdapter. Odczytuje
on bowiem tylko te dane, które są mu potrzebne. Oznacza to, że ten rodzaj adaptera działa szybciej i zużywa
mniej pamięci, a szybkość działania i pamięć to dwa bardzo ważne zagadnienia, na które trzeba zwracać uwagę.

496 Rozdział 12.


Kursory i zadania asynchroniczne

SimpleCursorAdapter odwzorowuje dane na widoki


Spróbujemy zatem utworzyć prosty adapter kursora i zastosować go android.widget.Adapter
w naszej aplikacji. Klasa SimpleCursorAdapter jest implementacją
...
abstrakcyjnej klasy CursorAdapter, której można używać
w większości przypadków, gdy musimy wyświetlać dane z kursora
w widoku ListView. Adapter ten pobiera kolumny z kursora
i odwzorowuje je na komponenty TextView i ImageView. android.widget. android.widget.
ArrayAdapter CursorAdapter
Chcemy wyświetlić na liście w aktywności DrinkCategoryActivity ... ...
nazwy wszystkich napojów. Zastosujemy zatem klasę
SimpleCursorAdapter do utworzenia kursora, który odwzoruje
nazwę napoju na widok tekstowy w widoku ListView:
android.widget.
SimpleCursorAdapter
Latte ...

Cappuccino
Klasy ArrayAdapter
Espresso i CursorAdapter są
Widok ListView Adapter CursorAdapter różnymi typami adapterów
— obie implementują
interfejs Adapter. Klasa
Nasza lista ListView wyświetla nazwę SimpleCursorAdapter
każdego napoju w odrębnym widoku TextView. dziedziczy po klasie
CursorAdapter.

W pierwszej kolejności utworzymy kursor


Pierwszą rzeczą, o której trzeba pomyśleć, tworząc kursor, który ma być następnie
używany w adapterze typu CursorAdapter, jest określenie kolumn, które należy
pobrać z bazy. Kursor powinien zawierać wszystkie kolumny, których wartości
mają być wyświetlane, jak również kolumnę o nazwie _id. Dołączenie kolumny
_id jest konieczne, gdyż bez niej adapter kursora nie będzie działał. A dlaczego?

W rozdziale 11. wspominaliśmy, że w Androidowie stosowana jest konwencja, by


kolumnie klucza głównego tabeli nadawać nazwę _id. Okazuje się, że konwencja
ta jest tak mocno osadzona w samym systemie, że adaptery kursorów zakładają,
iż kolumna _id będzie dostępna, i używają jej w celu unikalnego identyfikowania
poszczególnych wierszy w kursorze. W przypadku stosowania adaptera kursora
w widoku listy widok używa tej kolumny do określania, który wiersz został
kliknięty przez użytkownika.

Ponieważ planujemy użyć adaptera kursora do wyświetlenia nazw napojów, nasz


kursor musi zawierać kolumny _id i NAME; możemy go utworzyć w następujący
sposób:

cursor = db.query(”DRINK”, new String[]{”_id”, ”NAME”}, Musimy dołączyć do wyników


chociaż jej dane nie będą kolumnę _id,
null, null, null, null, null); wyświetlane.

Na następnej stronie użyjemy tego kursora do utworzenia adaptera.

jesteś tutaj  497


SimpleCursorAdapter

Tworzenie adaptera SimpleCursorAdapter ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Aby utworzyć adapter typu SimpleCursorAdapter, musimy przekazać mu informacje
o tym, jak dane mają być wyświetlane, którego kursora należy przy tym używać oraz które
kolumny mają zostać skojarzone z poszczególnymi kolumnami. Oto, jak można utworzyć
taki adapter, który będzie wyświetlał na liście nazwy napojów: To jest kursor utworzony na poprzedniej stronie.

cursor = db.query(”DRINK”, new String[]{”_id”, ”NAME”},


null, null, null, null, null);
CursorAdapter listAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, To jest ten sam układ, którego używaliśmy
wcześniej wraz z adapterem tablicy. Wyświetla on
To jest kursor. cursor, w każdym wierszu listy wartość jednej kolumny.
new String[]{“NAME”},
W widokach tekstowych umieszczonych
new int[]{android.R.id.text1}, na liście ListView wyświetlamy
zawartość kolumny NAME.
0);
Do przekazania adaptera do kontrolki ListView należy użyć
listDrinks.setAdapter(listAdapter); metody setAdapter().

Podobnie jak w poprzedniej wersji aplikacji, w której używaliśmy adaptera


ArrayAdapter, także teraz używamy stałej android.R.layout.simple_list_item_1,
aby poinformować system, że każdy wiersz kursora chcemy wyświetlać jako pojedyncze Ten układ określa, jak należy
wyświetlać dane. Możemy zastosować
pole tekstowe w widoku listy. Ten widok tekstowy będzie miał identyfikator ten sam układ, którego używaliśmy
android.R.id.text1. w poprzedniej wersji aplikacji,
wraz z kursorem ArrayAdapter.
Oto ogólna postać konstruktora klasy SimpleCursorAdapter:
SimpleCursorAdapter adapter = new SimpleCursorAdapter(Context kontekst,
int uklad,
To jest utworzony kursor.
Powinien on zawierać Cursor kursor,
kolumnę _id i wyświetlane String[] zKolumn,
dane.
int[] doWidokow, Które kolumny kursora
int flagi) należy powiązać
Parametry kontekst i uklad są dokładnie tymi samymi, których używaliśmy z którymi widokami.
w poprzedniej wersji aplikacji, korzystającej z kursora typu ArrayAdapter.
Pierwszy z nich określa kontekst, a drugi definiuje, jak należy wyświetlać Ten parametr jest używany do
określania zachowania kursora.
dane. Zamiast określać, z której tablicy mają pochodzić dane, tym razem
używamy parametru kursor, aby określić kursor, który będzie je zawierał.
Kolejny parametr, zKolumn, określa, których kolumn kursora chcemy używać,
a parametr doWidokow określa widoki, w których te dane należy wyświetlać.
Ostatni parametr, flagi, przyjmuje zazwyczaj domyślną wartość 0.
Alternatywnym rozwiązaniem jest zastosowanie wartości FLAG_REGISTER_
CONTENT_OBSERVER w celu zarejestrowania obserwatora zawartości, który będzie
informowany o wszelkich zmianach zachodzących w danych. Tego rozwiązania
nie będziemy jednak przedstawiać w tej książce, gdyż może ono prowadzić do
występowania wycieków pamięci. W dalszej części rozdziału dowiesz się,
jak sobie radzić ze zmianami danych prezentowanych na liście.

498 Rozdział 12.


Kursory i zadania asynchroniczne

Zamykanie kursora i bazy danych


Przedstawiając kursory we wcześniejszej części tego rozdziału, wspominaliśmy, że po
zakończeniu używania kursora należy zamknąć zarówno kursor, jak i bazę danych, aby
zwolnić przydzielone im zasoby. W kodzie naszej aktywności DrinkActivity używaliśmy
kursora do pobrania z bazy danych szczegółowych informacji o napoju, a kiedy już
zapisaliśmy te informacje w widokach, natychmiast zwalnialiśmy kursor i bazę danych.

W przypadku stosowania adaptera kursora sprawy wyglądają jednak nieco inaczej —


adapter wymaga bowiem, by kursor był cały czas otworzony, na wypadek, gdyby konieczne
było pobranie dodatkowych danych. Taka sytuacja może wystąpić, kiedy użytkownik
zacznie przewijać listę w dół i konieczne będzie wyświetlenie jej dalszych elementów.

Hej, kursorze,
Hej, adapterze, potrzebuję więcej…
potrzebuję więcej Kursorze? Stary,
danych. jesteś tam?

Kursor
Widok ListView Adapter CursorAdapter
Jeśli zamkniemy kursor
zbyt szybko, to adapter
nie będzie mógł pobierać
z niego dodatkowych
To wszystko oznacza, że nie będziemy mogli zwolnić kursora i bazy danych.
danych bezpośrednio po wywołaniu metody setAdapter() i przekazaniu
adaptera do widoku ListView. Zamiast tego powinniśmy to zrobić
w metodzie onDestroy() aktywności. W momencie usuwania aktywności
nie ma już bowiem potrzeby dalszego używania kursora lub połączenia
z bazą danych, można je więc usunąć:

public void onDestroy() {


super.onDestroy();
cursor.close();
db.close(); Podczas usuwania aktywności
zamykamy kursor i bazę danych.
}

Na następnej stronie sprawdzimy, czy potrafisz poprawić kod


aktywności DrinkCategoryActivity.

jesteś tutaj  499


Ćwiczenie

Zagadkowy basen
Twoim zadaniem jest wyłowienie fragmentów kodu
z basenu i umieszczenie ich w pustych miejscach
kodu aktywności DrinkCategoryActivity.
Danego fragmentu kodu nie można użyć więcej
niż jeden raz, lecz nie wszystkie fragmenty
będą potrzebne. Twoim celem jest wypełnienie
listy ListView nazwami napojów odczytanymi
z bazy danych.

...
public class DrinkCategoryActivity extends ListActivity { Coffeina

private SQLiteDatabase db; app/src/main


private Cursor cursor;
java

@Override
com.hfad.coffeina
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listDrinks = getListView(); DrinkCategory
Activity.java

try {
. .............. coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper. . ..................;

Dalsza część kodu znajduje się


Uwaga: Każdego na następnej stronie.
fragmentu z basenu
można użyć tylko
jeden raz!

getWritableDatabase()
SimpleCursorAdapter "NAME"
db
, cursor
cursor SQLiteOpenHelper
"NAME" getReadableDatabase()
"DESCRIPTION" "_id" SQLiteException
, DatabaseException

500 Rozdział 12.


Kursory i zadania asynchroniczne

cursor = db.query(”DRINK”,
new String[]{ . ............................ },
null, null, null, null, null);
CursorAdapter listAdapter = new .................... (this,
android.R.layout.simple_list_item_1,
............. ,
new String[]{ ............... },
new int[]{android.R.id.text1},
0);
listDrinks.setAdapter(listAdapter);
} catch(.................. e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”,
Toast.LENGTH_SHORT);
toast.show();
}
}
Coffeina
@Override
public void onDestroy(){ app/src/main
super.onDestroy();
.......... .close(); java

.......... .close();
com.hfad.coffeina
}

@Override DrinkCategory
Activity.java
public void onListItemClick(ListView listView,
View itemView,
int position,
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int)id);
startActivity(intent);
}
}

jesteś tutaj  501


Rozwiązanie

Zagadkowy basen. Rozwiązanie


Twoim zadaniem jest wyłowienie fragmentów kodu
z basenu i umieszczenie ich w pustych miejscach
kodu aktywności DrinkCategoryActivity.
Danego fragmentu kodu nie można użyć więcej
niż jeden raz, lecz nie wszystkie fragmenty
będą potrzebne. Twoim celem jest wypełnienie
listy ListView nazwami napojów odczytanymi
z bazy danych.

...
public class DrinkCategoryActivity extends ListActivity { Coffeina

private SQLiteDatabase db; app/src/main


private Cursor cursor;
java

@Override
com.hfad.coffeina
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listDrinks = getListView(); DrinkCategory
Activity.java
Dzięki użyciu obiektu SQLiteOpenHelper
try { możesz pobrać referencję do bazy danych.
SQLiteOpenHelper
................ coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper. getReadableDatabase() ;
........................

Odczytujemy dane z bazy, a więc


wystarczy, że otworzymy ją w tryb
tylko do odczytu. ie

Te fragmenty kodu
nie były potrzebne.

getWritableDatabase()

"DESCRIPTION"
DatabaseException

502 Rozdział 12.


Kursory i zadania asynchroniczne
Kursor musi zawierać kolumnę _id, gdyż w przeciwnym
razie adapter nie będzie działał. Musi także zawierać
kolumnę NAME, dzięki której będziemy mogli wyświetlić
na liście nazwy napojów.
cursor = db.query(”DRINK”,
"_id" , "NAME"
new String[]{ .............................. },
null, null, null, null, null); Używamy adaptera typu SimpleCursorAdapter.

SimpleCursorAdapter
CursorAdapter listAdapter = new ....................... (this,
android.R.layout.simple_list_item_1,
cursor
............. , Wyświetlamy zawartość
j kursora.
Tu używamy utworzonego wcześnie kolumny NAME.
"NAME"
new String[]{ ............... },
new int[]{android.R.id.text1},
0);
listDrinks.setAdapter(listAdapter);
Jeśli baza danych nie jest dostępna,
SQLiteException e) { to przechwycimy wyjątek SQLiteException.
} catch(..................
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
}
} Coffeina

app/src/main
@Override
public void onDestroy(){
java
super.onDestroy();
.cursor
........ .close(); com.hfad.coffeina
Przed zamknięciem połączenia
db
. ........ .close(); z bazą danych zamykamy także
kursor.
} DrinkCategory
Activity.java

@Override
public void onListItemClick(ListView listView,
View itemView,
int position,
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int)id);
startActivity(intent);
}
}

jesteś tutaj  503


Kod aktywności DrinkCategoryActivity

Zmodyfikowany kod aktywności DrinkCategoryActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Poniżej przedstawiliśmy kompletny, zmodyfikowany kod aktywności
DrinkCategoryActivity (DrinkCategoryActivity.java), w którym adapter ArrayAdapter
zastąpiliśmy adapterem SimpleCurosorAdapter (wszystkie zmiany w kodzie zostały
wyróżnione pogrubioną czcionką):

package com.hfad.coffeina;
Coffeina
import android.app.ListActivity;
import android.content.Intent; app/src/main
import android.os.Bundle;
java
import android.view.View;
import android.widget.ListView;
com.hfad.coffeina
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
DrinkCategory
import android.database.sqlite.SQLiteException; Activity.java
import android.database.sqlite.SQLiteOpenHelper;
Używamy tych wszystkich
import android.widget.CursorAdapter; dodatkowych klas, więc
import android.widget.SimpleCursorAdapter; musimy je zaimportować.
import android.widget.Toast;
import android.widget.SimpleCursorAdapter;

public class DrinkCategoryActivity extends ListActivity {


private SQLiteDatabase db;
private Cursor cursor; jako zmienne prywatne,
Bazę danych i kursor dodajemy kursor będziemy
, jak i
dzięki czemu zarówno bazę onDestroy().
pote m mog li zam knąć w meto dzie
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listDrinks = getListView();

try {
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);

db = coffeinaDatabaseHelper.getReadableDatabase();
cursor = db.query(“DRINK”, Pobieramy referencję
do bazy danych.
new String[]{“_id”, “NAME”},
Tworzymy kursor.
null, null, null, null, null);
Dalsza część kodu znajduje się
na następnej stronie.
504 Rozdział 12.
Kursory i zadania asynchroniczne

Kod aktywności DrinkCategoryActivity (ciąg dalszy)


Tworzymy adapter
SimpleCursorAdapter.
CursorAdapter listAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
cursor,
new String[]{”NAME”},
Odwzorowujemy zawartość kolumny new int[]{android.R.id.text1},
NAME na widok tekstowy wyświetlany
w komponencie ListView. 0);
listDrinks.setAdapter(listAdapter); Wciąż używamy adaptera, ale tym razem jest
to adapter kursora typu SimpleCursorAdapter.
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, “Baza danych jest niedostępna”,
Toast.LENGTH_SHORT);
toast.show(); Jeśli został zgłoszony wyjątek
SQLiteException, to wyświetlamy
} komunikat dla użytkownika.
}

@Override Coffeina
public void onDestroy(){ W metodzie onDestroy() aktywności zamykamy
kursor i bazę danych. Kursor pozostanie
super.onDestroy(); otwarty aż do momentu, gdy adapter nie app/src/main
będzie go już potrzebował.
cursor.close();
db.close(); java

}
com.hfad.coffeina

@Override
Tej metody nie DrinkCategory
public void onListItemClick(ListView listView, musieliśmy zmieniać. Activity.java
View itemView,
int position,
long id) {
Intent intent = new Intent(DrinkCategoryActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int)id);
startActivity(intent);
}
}

A teraz spróbujmy uruchomić aplikację.

jesteś tutaj  505


Jazda próbna

Jazda próba aplikacji ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Wprowadź wszystkie zmiany w kodzie, następnie zbuduj aplikację i ponownie
ją zainstaluj na swoim urządzeniu. Kiedy to zrobisz i uruchomisz aplikację,
przekonasz się, że wygląda ona dokładnie tak samo jak poprzednio.

Kiedy uruchomimy aplikację,


uzyskamy dokładnie takie
same efekty jak wcześniej,
ale tym razem wszystkie
Tym razem dane są już pobierane z bazy danych. W zasadzie informacje będą pochodziły
możesz usunąć z projektu plik Drink.java, gdyż nie potrzebujemy z bazy danych Coffeina.
już tablicy z napojami — wszystkie informacje potrzebne aplikacji
są pobierane z bazy danych.

Dokąd dotarliśmy?
Poniżej przedstawiliśmy obecną strukturę aplikacji dla kafeterii
Coffeina.
Baza danych
kafeterii
<Layout> Coffeina <Layout>

</Layout>
</Layout>

activity_top_level.xml Pomocnik SQLite activity_drink.xml

Urządzenie TopLevelActivity.java DrinkCategoryActivity.java DrinkActivity.java

Jest jednak jeszcze jedna zmiana, którą planujemy wprowadzić w naszej Zmieniliśmy aktywności korzystające
aplikacji. Otóż chcemy, by aplikacja aktualizowała informacje zapisane w bazie. wcześniej z klasy Drink i obecnie
pobierają informacje z bazy danych.
506 Rozdział 12.
Kursory i zadania asynchroniczne

Najważniejsze informacje warto umieszczać


w aktywności najwyższego poziomu
Kiedy tworzyliśmy pierwszą wersję aplikacji dla kafeterii Coffeina, zaprojektowana przez
nas aktywność najwyższego poziomu była bardzo prosta. Aktywność najwyższego poziomu
to ta, którą użytkownik zobaczy bezpośrednio po uruchomieniu aplikacji, a w naszym
przypadku zawiera ona jedynie obrazek z logo kafeterii i trzy elementy nawigacyjne, które
z powodzeniem można by przenieść do szuflady nawigacyjnej. Zachowanie prostoty interfejsu
użytkownika to dobry pomysł, ale czy taki interfejs nie jest zbyt prosty?
Projekt aktywności głównego poziomu należy bardzo uważnie przemyśleć, gdyż aktywność ta
to pierwsza rzecz, jaką użytkownik zobaczy po uruchomieniu aplikacji. W idealnym przypadku
powinna ona zawierać treści, które są użyteczne zarówno dla nowych użytkowników, jak i dla
osób, które już korzystały z aplikacji. Jednym ze sposobów pozwalających osiągnąć ten cel
jest zastanowienie się nad tym, co użytkownicy będą chcieli robić w aplikacji, i zapewnienie
im na jej pierwszym ekranie narzędzi do wykonania tych czynności. Na przykład gdybyśmy
pisali aplikację do odtwarzania muzyki, to aktywność główna mogłaby zawierać listę ostatnio
odtwarzanych albumów, aby użytkownik mógł je łatwo odnaleźć.
W dalszej części rozdziału zmodyfikujemy aplikację dla kafeterii Coffeina, dodając do niej
możliwość wybierania ulubionych napojów i wyświetlając w aktywności głównej ich listę,
pozwalającą przejść bezpośrednio do szczegółowych informacji o danym napoju.

Te elementy doskonale
nadają się do umieszczenia
w szufladzie nawigacyjnej.
W tej aplikacji nie
zastosujemy jednak
szuflady nawigacyjnej,
gdyż jej implementacja
jest dosyć złożona,
a chcemy się skoncentrować
na bazach danych.

Do aktywności
TopLevelActivity
dodamy widok ListView
prezentujący ulubione Użytkownicy mogą wybierać swoje
napoje użytkownika. ulubione napoje, zaznaczając
to pole wyboru. Musimy je dodać
Kiedy użytkownik kliknie któryś do aktywności DrinkActivity
z ulubionych napojów, przejdzie i zadbać o to, by jego kliknięcie
bezpośrednio do szczegółowych odpowiednio modyfikowało bazę
informacji na jego temat, danych.
wyświetlanych w aktywności
DrinkActivity.

Aby zapewnić te wszystkie możliwości, w pierwszej kolejności musimy


pozwolić użytkownikom zaznaczać ich ulubione napoje.

jesteś tutaj  507


Dodawanie ulubionych napojów

Dodanie ulubionych napojów do aktywności DrinkActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
W rozdziale 11. dodaliśmy do tabeli DRINK w bazie danych kafeterii Coffeina
kolumnę FAVORITE. Użyjemy jej, by umożliwić użytkownikom zaznaczanie
wybranego napoju jako ulubionego, dzięki czemu zostanie on następnie
wyświetlony na liście, którą dodamy do aktywności TopLevelActivity.
Pozwolimy użytkownikom edytować napoje w aktywności DrinkActivity,
gdyż służy ona do wyświetlania szczegółowych informacji na temat
konkretnego napoju.

Musimy zacząć od dodania do układu activity_drink.xml nowego widoku,


którego użyjemy do edycji i wyświetlania wartości kolumny FAVORITE. Typ
widoku umieszczanego w układzie zależy od typu danych informacji, którą
chcemy powiązać z tym widokiem. W przypadku naszej aplikacji chcemy Planujemy dodać pole
użyć widoku, by zapewnić użytkownikom możliwość wybierania wartości wyboru, aby użytkownicy
mogli wybierać ulubione
logicznych — true lub false — dlatego zastosujemy pole wyboru. napoje.

Zacznijmy od dodania do pliku strings.xml nowego zasobu o nazwie


favorite (użyjemy go do określenia etykiety pola wyboru):

<string name=”favorite”>Ulubiony</string>

Następnie dodamy samo pole wyboru do układu activity_drink.xml. Nadamy


mu identyfikator favorite i skorzystamy z atrybutu android:text,
by wyświetlić jego etykietę. Oprócz tego atrybutowi android:onClick
przypiszemy wartość ”onFavoriteClicked”, żeby kliknięcie pola
powodowało wywołanie metody onFavoriteClicked() zdefiniowanej
w aktywności DrinkActivity:
<LinearLayout ...>
Coffeina
<ImageView android:id=”@+id/photo”
... /> app/src/main
To są widoki prezentujące
zdjęcie, nazwę oraz opis
<TextView android:id=”@+id/name” napoju, które dodaliśmy res
do aktywności, tworząc
... /> pierwszą wersję aplikacji.
layout
<xml>
<TextView android:id=”@+id/description” </xml>
... /> activity_drink.xml
rite.
Pole wyboru ma identyfikator favo
<CheckBox android:id=”@+id/favorite”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
Nasze pole wyboru musi mieć jakąś etykietę.
android:text=”@string/favorite”
android:onClick=”onFavoriteClicked”/> wywołana
Po kliknięciu pola wyboru zostanie
</LinearLayout> metoda onFa vorit eCli cked ().

508 Rozdział 12.


Kursory i zadania asynchroniczne

Dodanie nowej kolumny do kursora


Kolejną rzeczą, którą się zajmiemy, będzie zmiana kodu aktywności
DrinkActivity w taki sposób, by nasze nowe pole wyboru wyświetlało
Coffeina
wartość kolumny FAVORITE pobraną z bazy danych.

Wartość kolumny FAVORITE możemy pobrać z bazy danych dokładnie app/src/main


w taki sam sposób, w jaki pobieramy wartości wyświetlane w innych
widokach — poprzez dodanie tej kolumny do kursora. Dzięki temu java
później będziemy mogli odczytać wartość kolumny FAVORITE z kursora
i użyć jej do ustawienia wartości pola wyboru. Oto fragment kodu com.hfad.coffeina
metody onCreate(), który odpowiada za te operacje:

protected void onCreate(Bundle savedInstanceState) { DrinkActivity.java

...
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
SQLiteDatabase db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor cursor = db.query (”DRINK”,
new String[] {”NAME”,”DESCRIPTION”,”IMAGE_RESOURCE_ID”,”FAVORITE”},
”_id = ?”,
new String[] {Integer.toString(drinkNo)}, Dodajemy kolumnę
FAVORITE do kursora.
null, null,null);

// Przechodzimy do pierwszego rekordu w kursorze


if (cursor.moveToFirst()) {
// Pobieramy z kursora szczegółowe informacje o napoju
String nameText = cursor.getString(0);
String descriptionText = cursor.getString(1);
int photoId = cursor.getInt(2); Pobieramy wartość kolumny FAVORITE.
boolean isFavorite = (cursor.getInt(3) == 1); W bazie danych wartości logicznej true
odpowiada 1, a wartości false — 0.
...
// Zaznaczamy pole wyboru ulubionego napoju
CheckBox favorite = (CheckBox)findViewById(R.id.favorite);
favorite.setChecked(isFavorite);
... Ustawiamy wartość
pola wyboru favorite.
}

To wszystko, co musimy zrobić, by wartość kolumny FAVORITE


została odpowiednio wyświetlona w polu wyboru. Teraz musimy się
zająć aktualizacją zawartości bazy danych w odpowiedzi na kliknięcie
pola wyboru.
jesteś tutaj  509
Reagowanie na kliknięcia

Odpowiadanie na kliknięcia ¨  DrinkActivity


¨  DrinkCategoryActivity
w celu aktualizacji bazy danych ¨  Ulubione napoje

Dodając pole wyboru do układu activity_drink.xml, w atrybucie android:onClick


elementu <CheckBox> podaliśmy wartość ”onFavoriteClicked”. Oznacza ona, że
za każdym razem, gdy użytkownik kliknie pole wyboru, zostanie wywołana metoda
onFavoriteClicked() aktywności DrinkActivity. Musimy zaimplementować tę
metodę i zadbać, by aktualizowała ona zawartość bazy danych, zapisując w niej bieżącą
wartość pola wyboru. Każde zaznaczenie lub usunięcie zaznaczenia z pola wyboru
będzie zatem powodowało wywołanie metody onFavoriteClicked() i zapisanie
w bazie danych zmiany wprowadzonej przez użytkownika.

W rozdziale 11. poznałeś metody klasy SQLiteDatabase, których można używać do


modyfikacji danych przechowywanych w bazie danych SQLite. Znasz metodę insert()
pozwalającą wstawiać dane, metodę delete(), która służy do usuwania rekordów,
oraz metodę update(), która umożliwia ich aktualizację.

Tych trzech metod możemy używać do wprowadzania zmian w zawartości bazy danych
z poziomu naszych aktywności. Na przykład możemy używać metody insert(),
by dodawać nowe rekordy napojów do tabeli DRINK, bądź metody delete(), by
je usuwać. W przypadku naszej aplikacji chcemy aktualizować zawartość kolumny
FAVORITE w tabeli DRINK, zapisując w niej bieżącą wartość pola wyboru. Możemy
do tego zastosować metodę update().

W ramach przypomnienia przedstawiamy poniżej ogólną postać wywołania metody


update():

database.update(String tabela,
ContentValues wartosci,
String klauzulaWhere,
String[] argumentyWhere);

gdzie tabela jest nazwą tabeli, której zawartość chcemy zaktualizować, wartosci
to obiekt klasy ContentValues zawierający pary nazwa – wartość reprezentujące
aktualizowane kolumny i ich nowe wartości, a dwa ostatnie parametry, klauzulaWhere
i argumentyWhere, określają rekordy, które mają zostać zmodyfikowane.

Teraz już wiesz wszystko, co trzeba, by w kodzie aktywności DrinkActivity zapisać


bieżącą wartość pola wyboru w kolumnie FAVORITE tabeli.

update()

DrinkActivity Baza danych


kafeterii Coffeina

510 Rozdział 12.


Kursory i zadania asynchroniczne

Magnesiki z kodem
W kodzie aktywności DrinkActivity chcemy aktualizować wartość
kolumny FAVORITE bazy danych, zapisując w niej wartość pola
wyboru używanego do wybierania ulubionych napojów. Czy potrafisz
uzupełnić kod metody onFavoriteClicked() tak, aby to robiła?

public class DrinkActivity extends Activity {


...

// Aktualizujemy bazę danych po kliknięciu pola wyboru


public void onFavoriteClicked( .................. ){

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


CheckBox favorite = (CheckBox)findViewById(R.id.favorite);

.............. drinkValues = new .....................;

drinkValues.put( ............... , favorite.isChecked());

SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);

try {

SQLiteDatabase db = coffeinaDatabaseHelper. .......................;

db.update( ............., ..................,


.............. , new String[] {Integer.toString(drinkNo)});

db.close();
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”,Toast.LENGTH_SHORT);
toast.show();
}
} drinkValues
}
"FAVORITE"
View view "_id = ?" ContentValues
"DRINK"
ContentValues()
getReadableDatabase() favorite
getWritableDatabase()

jesteś tutaj  511


Rozwiązanie magnesików

Magnesiki z kodem. Rozwiązanie ¨  DrinkActivity


¨  DrinkCategoryActivity
W kodzie aktywności DrinkActivity chcemy aktualizować wartość ¨  Ulubione napoje
kolumny FAVORITE bazy danych, zapisując w niej wartość pola
wyboru używanego do wybierania ulubionych napojów. Czy potrafisz
uzupełnić kod metody onFavoriteClicked() tak, aby to robiła?

public class DrinkActivity extends Activity {


...

// Aktualizujemy bazę danych po kliknięciu pola wyboru


public void onFavoriteClicked( .. View view ... ){

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


CheckBox favorite = (CheckBox)findViewById(R.id.favorite);

ContentValues drinkValues = new . ContentValues() .;

drinkValues.put( "FAVORITE" , favorite.isChecked());

SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);

try {

getWritableDatabase()
SQLiteDatabase db = coffeinaDatabaseHelper. . .;

Aby aktualizować bazę danych,


db.update( . "DRINK" ., drinkValues , musimy mieć do niej dostęp
w trybie do odczytu i zapisu.
"_id = ?" , new String[] {Integer.toString(drinkNo)});

db.close();
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”,Toast.LENGTH_SHORT);
toast.show();
}
} Te magnesiki nie były potrzebne.

getReadableDatabase() favorite

512 Rozdział 12.


Kursory i zadania asynchroniczne

Kod aktywności DrinkActivity


Oto aktualna zawartość pliku DrinkActivity.java
(zmiany zostały wyróżnione pogrubioną czcionką):
Coffeina
package com.hfad.coffeina;
app/src/main
import android.app.Activity;
import android.os.Bundle;
java
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; com.hfad.coffeina
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
DrinkActivity.java
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.view.View; Używamy tych dodatkowych klas.
import android.widget.CheckBox;
import android.content.ContentValues;

public class DrinkActivity extends Activity {

public static final String EXTRA_DRINKNO = ”drinkNo”;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drink);

// Pobieramy identyfikator napoju z intencji


int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);
Do aktualizacji bazy danych potrzebujemy
// Tworzymy kursor dostępu w trybie odczytu i zapisu.
try {
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
Cursor cursor = db.query (”DRINK”,
new String[] {”NAME”, ”DESCRIPTION”, ”IMAGE_RESOURCE_ID”, “FAVORITE”},
”_id = ?”,
new String[] {Integer.toString(drinkNo)}, Do kursora dodajemy
null, null,null); kolumnę FAVORITE.

// Przechodzimy do pierwszego rekordu w kursorze


if (cursor.moveToFirst()) {
// Pobieramy szczegółowe informacje o napoju z kursora
String nameText = cursor.getString(0);
String descriptionText = cursor.getString(1);
int photoId = cursor.getInt(2); Dalsza część kodu
znajduje się na
boolean isFavorite = (cursor.getInt(3) == 1); następnej stronie.

Pobieramy wartość kolumny FAVORITE. jesteś tutaj  513


Kod aktywności DrinkActivity

Kod aktywności DrinkActivity (ciąg dalszy) ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
// Wyświetlamy nazwę napoju
TextView name = (TextView)findViewById(R.id.name);
name.setText(nameText);

// Wyświetlamy opis napoju


TextView description = (TextView)findViewById(R.id.description);
description.setText(descriptionText);

// Wyświetlamy zdjęcie napoju


ImageView photo = (ImageView)findViewById(R.id.photo);
photo.setImageResource(photoId);
photo.setContentDescription(nameText);

// Zaznaczamy pole wyboru ulubionego napoju


CheckBox favorite = (CheckBox)findViewById(R.id.favorite);
favorite.setChecked(isFavorite);
}; Ustawiamy wartość pola wyboru.
cursor.close();
db.close();
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
}
}

// Aktualizujemy bazę danych po kliknięciu pola wyboru


public void onFavoriteClicked(View view){

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


CheckBox favorite = (CheckBox)findViewById(R.id.favorite);

ContentValues drinkValues = new ContentValues();


Dodajemy wartość pola wyboru
drinkValues.put(“FAVORITE”, favorite.isChecked()); favorite do obiektu drinkValues
typu ContentValues.
SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);

try {
Aktualizujemy SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
kolumnę
FAVORITE, db.update( “DRINK”, drinkValues,
zapisując w niej “_id = ?”, new String[] {Integer.toString(drinkNo)});
wartość pola db.close();
wyboru. } catch(SQLiteException e) {
Toast toast = Toast.makeText(this, “Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
}
W razie wystąpienia problemów z bazą danych
} wyświetlamy komunikat.
}

514 Rozdział 12.


Kursory i zadania asynchroniczne

Wyświetlenie ulubionych napojów


w aktywności TopLevelActivity
Ostatnią modyfikacją, którą chcemy wprowadzić, jest wyświetlenie
ulubionych napojów w aktywności TopLevelActivity.

 Musimy dodać do układu nowy widok ListView.


Będzie on służył do wyświetlania listy nazw napojów, które użytkownik zaznaczył jako ulubione.

 Musimy wyświetlić w tym widoku nazwy ulubionych napojów.


Wyświetlimy w nim listę ulubionych napojów użytkownika pobraną z bazy danych.

 Musimy zadbać, by ten widok ListView reagował na kliknięcia.


Kiedy użytkownik kliknie jeden ze swoich ulubionych napojów, wyświetlimy szczegółowe
informacje o tym napoju w aktywności DrinkActivity.

Wprowadzenie tych wszystkich zmian pozwoli wyświetlić w aktywności


TopLevelActivity listę ulubionych napojów użytkownika.

Dane do listy ulubionych


napojów będą pobierane
z bazy danych przy użyciu
kursora.

Elementy tego widoku


Kursor ListView muszą
reagować na kliknięcia.
Baza danych
kafeterii
Coffeina

Kod, który umożliwia przygotowanie i wyświetlenie tej listy,


przedstawimy na kilku następnych stronach.

jesteś tutaj  515


Wyświetlanie ulubionych napojów

Wyświetlenie listy ulubionych napojów ¨  DrinkActivity


¨  DrinkCategoryActivity
w kodzie układu activity_top_level.xml ¨  Ulubione napoje

Jak już zaznaczyliśmy na poprzedniej stronie, planujemy dodać do układu


activity_top_level.xml widok ListView, którego użyjemy do wyświetlenia listy
ulubionych napojów użytkownika. Oprócz tego do układu dodamy także widok
tekstowy, który będzie zawierał nagłówek tej listy.

Zacznijmy jednak od dodania do pliku strings.xml poniższego zasobu łańcuchowego


(użyjemy go do określenia tekstu wyświetlanego w widoku tekstowym):

<string name=”favorites”>Twoje ulubione napoje</string>


Teraz musimy zmodyfikować kod pliku activity_top_level.xml i dodać do niego
widok tekstowy i widok listy:

<LinearLayout ... >


<ImageView
android:layout_width=”200dp”
android:layout_height=”100dp”
android:src=”@drawable/starbuzz_logo”
android:contentDescription=”@string/starbuzz_logo” />
Coffeina
<ListView Układ zawiera już logo kafeterii
Coffeina i jeden widok ListView
android:id=”@+id/list_options” . app/src/main
android:layout_width=”match_parent”
android:layout_height=”wrap_content” res
android:entries=”@array/options” />
layout
Dodamy do niego widok tekstowy
je”.
z napisem „Twoje ulubione napo
<xml>
<TextView </xml>
b
android:layout_width=”wrap_content” Ten tekst zdefiniujemy jako zasó rites.
łańcuchowy o identyfikatorze favo activity_
android:layout_height=”wrap_content” top_level.xml
android:layout_marginTop=”50dp”
android:textAppearance=”?android:attr/textAppearanceLarge”
android:text=”@string/favorites” />
Drugi widok ListView,
<ListView o identyfikatorze
android:id=”@+id/list_favorites” list_favorites, będzie
wyświetlał listę
android:layout_width=”match_parent” ulubionych napojów
android:layout_height=”wrap_content” /> użytkownika.
</LinearLayout>

To już wszystkie zmiany, jakie musimy wprowadzić w kodzie układu


activity_top_level.xml. Dalsze zmiany trzeba będzie wprowadzić w kodzie
aktywności, zapisanym w pliku TopLevelActivity.java.

516 Rozdział 12.


Kursory i zadania asynchroniczne

Jakie zmiany trzeba wprowadzić


w kodzie aktywności TopLevelActivity?
Kolejną zmianą, którą musimy wprowadzić, jest wyświetlanie ulubionych napojów
użytkownika na liście, którą przed chwilą dodaliśmy do układu, oraz zadbanie
o to, by lista ta reagowała na kliknięcia. W tym celu będziemy musieli wykonać
następujące modyfikacje:

1 Utworzyć kursor, którego użyjemy do wyświetlenia zawartości listy.


Kursor będzie zawierał wszystkie napoje, w których kolumna FAVORITE
będzie miała wartość 1 — czyli wszystkie napoje, które użytkownik
oznaczył jako ulubione. Podobnie jak zrobiliśmy to w kodzie aktywności
DrinkCategoryActivity, także tu możemy połączyć widok ListView
z kursorem, używając adaptera typu CursorAdapter.

Kursor
Widok Adapter Baza danych
ListView CursorAdapter

2 Zaimplementować obiekt nasłuchujący OnItemClickListener,


tak by widok ListView mógł reagować na kliknięcia.
Kiedy użytkownik kliknie któryś ze swoich ulubionych napojów,
utworzymy intencję, która uruchomi aktywność DrinkActivity,
i przekażemy do tej intencji identyfikator klikniętego napoju. Dzięki
temu aplikacja wyświetli szczegółowe informacje o wybranym napoju.

Intencja

drinkNo

TopLevelActivity DrinkActivity

Już znasz cały kod, który jest potrzebny do zaimplementowania tych


możliwości, dlatego na kilku następnych stronach przedstawimy
kompletny kod aktywności TopLevelActivity, zapisany w pliku
TopLevelActivity.java.

jesteś tutaj  517


Kod aktywności TopLevelActivity

Nowy kod aktywności głównego poziomu ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Oto nowy kod aktywności TopLevelActivity zapisany w pliku TopLevelActivity.java
(zawiera on sporo zmian, więc przeanalizuj go dokładnie i bez pośpiechu):

package com.hfad.coffeina;

Coffeina
import android.app.Activity;
import android.content.Intent; app/src/main
import android.os.Bundle;
import android.widget.AdapterView; java

import android.widget.ListView;
com.hfad.coffeina
import android.view.View;
import android.database.Cursor;
TopLevel
import android.database.sqlite.SQLiteOpenHelper; Activity.java
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteDatabase; Dodajemy te
wszystkie klasy.
import android.widget.SimpleCursorAdapter;
import android.widget.CursorAdapter;
import android.widget.Toast;

public class TopLevelActivity extends Activity {

private SQLiteDatabase db; Bazę danych i kursor dodajemy jako zmienne prywatne,
by mieć potem do nich dostęp w metodzie onDestroy().
private Cursor favoritesCursor;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_top_level);

Dalsza część kodu


znajduje się na
następnej stronie.

518 Rozdział 12.


Kursory i zadania asynchroniczne

Kod aktywności TopLevelActivity (ciąg dalszy)

// Implementacja obiektu OnItemClickListener


AdapterView.OnItemClickListener itemClickListener = Coffeina
new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> listView, app/src/main
View v,
int position, java

long id) {
com.hfad.coffeina
if (position == 0) {
Intent intent = new Intent(TopLevelActivity.this,
TopLevel
DrinkCategoryActivity.class); Activity.java
startActivity(intent);
}
To jest kod, który początkowo znajdował się
} w metodzie onCreate(). Określa on zawartość
}; listy z opcjami i zapewnia, że będzie ona
reagować na kliknięcia.

// Dodajemy obiekt nasłuchujący do widoku ListView


ListView listView = (ListView) findViewById(R.id.list_options);
listView.setOnItemClickListener(itemClickListener);

Pobieramy referencję do
// Zapisujemy na liście list_favorites ulubione napoje użytkownika widoku ListView z ulubionymi
napojami użytkownika.
ListView listFavorites = (ListView)findViewById(R.id.list_favorites);
try {
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper.getReadableDatabase();
favoritesCursor = db.query(“DRINK”,
new String[] { “_id”, “NAME”},
Tworzymy kursor,
który pobiera “FAVORITE = 1”, Pobieramy nazwy ulubionych
z bazy danych napojów użytkownika.
null, null, null, null);
wartości kolumn
_id i NAME tabeli
DRINK z wierszy,
w których kolumna
FAVORITE ma
wartość 1.

Dalsza część kodu


znajduje się na
następnej stronie.

jesteś tutaj  519


Ciąg dalszy kodu

Kod aktywności TopLevelActivity (ciąg dalszy) ¨  DrinkActivity


¨  DrinkCategoryActivity
CursorAdapter favoriteAdapter =
¨  Ulubione napoje
new SimpleCursorAdapter(TopLevelActivity.this,
Używamy kursora do
utworzenia adaptera typu
android.R.layout.simple_list_item_1,
SimpleCursorAdapter. favoritesCursor,
W widoku ListView chcemy new String[]{”NAME”},
wyświetlać nazwy napojów.
new int[]{android.R.id.text1}, 0);
listFavorites.setAdapter(favoriteAdapter);
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
Jeśli pojawi się jakiś problem z bazą danych, Metoda tego obiektu
} to wyświetlimy odpowiedni komunikat. nasłuchującego zostanie
wywołana, kiedy użytkownik
kliknie któryś z elementów listy.
// Po kliknięciu ulubionego napoju przechodzimy do aktywności DrinkActivity
listFavorites.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> listView, View v, int position, long id) {
Intent intent = new Intent(TopLevelActivity.this, DrinkActivity.class);
intent.putExtra(DrinkActivity.EXTRA_DRINKNO, (int) id);
startActivity(intent);
ulubionych
} Jeśli użytkownik kliknie jeden z e ListView,
napojów wyświetlonych w elemenci a uruchomi
któr
}); to ta metoda utworzy intencję, w tej intencji
aktywność DrinkActivity i zapisze Coffeina
} iden tyfik ator klikn ięte go napo ju.

app/src/main
// W metodzie onDestroy() zamykamy kursor i bazę danych
@Override java
public void onDestroy(){
super.onDestroy(); com.hfad.coffeina

favoritesCursor.close(); Podczas usuwania aktywności zamykamy


kursor i bazę danych.
db.close(); TopLevel
Activity.java
}
}

Powyższy kod wyświetla na liście napoje, które użytkownik oznaczył jako swoje ulubione. Kiedy
użytkownik kliknie nazwę jednego z tych napojów, to zostanie utworzona intencja, która uruchomi
aktywność DrinkActivity, a w tej intencji zostanie zapisany identyfikator klikniętego napoju. W efekcie
aplikacja wyświetli szczegółowe informacje o wybranym napoju. Na następnej stronie zobaczymy, jak
działa ta nowa wersja naszej aplikacji, a także pewien problem, który będziemy musieli rozwiązać.

520 Rozdział 12.


Kursory i zadania asynchroniczne

Jazda próbna aplikacji


Kiedy uruchomimy aplikację, to zgodnie z naszymi
oczekiwaniami w aktywności TopLevelActivity
zostanie wyświetlony nowy widok tekstowy i nowa
lista. Lista na razie jest pusta, ponieważ jeszcze nie
wybraliśmy żadnych ulubionych napojów.

Tu znajduje się widok


ListView o identyfikatorze
favorites. Na razie nie jest
on widoczny, gdyż nie mamy
żadnych ulubionych napojów.

Kiedy przejdziemy do aktywności DrinkActivity,


będzie ona prezentować nowe pole wyboru.
Kliknięcie tego pola wyboru spowoduje, że
prezentowany napój zostanie oznaczony jako
ulubiony.

To jest pole wyboru, które


dodaliśmy nieco wcześniej.
Jego kliknięcie powoduje
aktualizację zawartości bazy
danych kafeterii Coffeina.

Kiedy wrócimy do aktywności TopLevelActivity, okaże się jednak,


że napój, który przed chwilą oznaczyliśmy jako ulubiony, nie został
wyświetlony na liście. Pojawi się on jedynie wówczas, gdy obrócimy
urządzenie.

WYSIL
SZARE KOMÓRKI
Jak myślisz, dlaczego napój, który właśnie wybraliśmy jako ulubiony, nie
został od razu wyświetlony na liście w aktywności głównej i pojawił się
na niej dopiero po obróceniu urządzenia? Zastanów się nad tym, zanim Ulubiony napój wciąż
nie został wyświetlony.
przewrócisz stronę. Jak sądzisz, dlaczego
tak się dzieje?

jesteś tutaj  521


Nieaktualne dane

Kursory nie są odświeżane automatycznie ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Kiedy użytkownik zaznaczy nowy ulubiony napój — przechodząc w aplikacji do aktywności
DrinkActivity i zaznaczając pole wyboru — to nie zostanie on automatycznie wyświetlony
na liście ulubionych napojów prezentowanej w aktywności TopLevelActivity. Dzieje się tak,
ponieważ kursory pobierają dane w momencie, gdy są tworzone. W naszym przypadku kursor
jest tworzony w metodzie onCreate(), a zatem pobiera on swoją zawartość w momencie
tworzenia aktywności. Gdy użytkownik przechodzi do innych aktywności w aplikacji,
aktywność TopLevelActivity jest zatrzymywana, a nie usuwana i tworzona ponownie.

Kiedy uruchamiasz drugą


aktywność, przesłania ona
tę, która działała do tej pory.
Ta dotychczasowa aktywność
nie jest jednak usuwana.
Zamiast tego jej działanie jest
wstrzymywane, a następnie
zatrzymywane, gdyż traci
ona miejsce wprowadzania
i przestaje być widoczna
dla użytkownika.

Kursory nie śledzą automatycznie, czy dane przechowywane w bazie danych zostały
zmodyfikowane, czy nie. Jeśli ulegną one zmianie po utworzeniu kursora, to kursor
nie zostanie zaktualizowany. Wciąż będzie on zawierał początkowe rekordy w ich Jeśli zaktualizujesz dane
niezmienionej postaci. w tabeli bazy danych…

_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE …to kursor, który


został utworzony
1 “Latte” “Czarne espresso z gorącym 54543543 1 wcześniej, nie
mlekiem i mleczną pianką.” zauważy tej
zmiany.
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
_id NAME DESCRIPTION IMAGE_RESOURCE_ID FAVORITE
3 “Esp 1 “Latte” “Czarne espresso z gorącym 54543543 0
mlekiem i mleczną pianką.”
2 “Cappuccino” “Czarne espresso z dużą ilością 654334453 0
spienionego mleka.”
3 “Espresso” “Czarna kawa ze świeżo mielonych 44324234 0
ziaren najwyższej jakości.”

A jak rozwiązać ten problem?

522 Rozdział 12.


Kursory i zadania asynchroniczne

Kursor można zmieniać za pomocą metody changeCursor()


Rozwiązaniem naszego problemu jest modyfikowanie kursora używanego do
wyświetlania listy ulubionych napojów w momencie, gdy użytkownik ponownie
wróci do aktywności TopLevelActivity. Jeśli to zrobimy w metodzie
onRestart(), to kiedy użytkownik wróci do aktywności TopLevelActivity,
zawartość widoku ListView z listą ulubionych napojów zostanie odświeżona.
Innymi słowy: wszystkie nowe napoje, które użytkownik zaznaczył jako
ulubione, zostaną na niej wyświetlone, a wszystkie napoje, z których użytkownik
usunął zaznaczenie — znikną z listy.

Do tego celu możemy użyć metody changeCursor() klasy CursorAdapter.


Metoda ta zastępuje kursor aktualnie używany przez adapter nowym kursorem,
a stary zamyka. Oto ogólna postać tej metody:
To jest nowy kursor, który ma
public void changeCursor(Cursor nowyKursor) być używany przez adapter.

Metoda changeCursor() ma jeden parametr — nowy kursor. Oto przykład


zastosowania tej metody:

// Tworzymy nowy kursor


CoffeinaDatabaseHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor newCursor = db.query(”DRINK”, Tworzymy nowy kursor w dokładnie
new String[] { ”_id”, ”NAME”}, taki sam sposób jak wcześniej.

”FAVORITE = 1”,
null, null, null, null);
// Pobieramy adapter CursorAdapter używany przez widok ListView
ListView listFavorites = (ListView)findViewById(R.id.list_favorites); Używając metody
getAdapter(),
CursorAdapter adapter = (CursorAdapter) listFavorites.getAdapter(); pobieramy adapter
używany w widoku
// Zmieniamy kursor używany przez adapter CursorAdapter na nowy ListView.
adapter.changeCursor(newCursor);
Zmieniamy kursor używany w adapterze.

Kompletny, zmodyfikowany kod pliku TopLevelActivity.java przedstawiliśmy


na kilku następnych stronach.

jesteś tutaj  523


Kod aktywności TopLevelActivity

Zmodyfikowany kod aktywności TopLevelActivity ¨  DrinkActivity


¨  DrinkCategoryActivity
¨  Ulubione napoje
Oto zmodyfikowany kod aktywności TopLevelActivity
(zmiany zostały wyróżnione pogrubioną czcionką):
package com.hfad.coffeina;
...
Coffeina
public class TopLevelActivity extends Activity {
app/src/main
...
java
@Override
protected void onCreate(Bundle savedInstanceState) {
com.hfad.coffeina
...
} Te metody nie zostały zmienione.
TopLevel
// W metodzie onDestroy() zamykamy kursor i bazę danych Activity.java
@Override
public void onDestroy(){
... użytkownik
Ta metoda jest wywoływana, gdy vity.
} wraca do aktywności TopLevelActi

public void onRestart() {


super.onRestart();
try {
// Tworzymy nowy kursor
CoffeinaDatabaseHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor newCursor = db.query(“DRINK”, Tworzymy nowy kursor w dokładnie
taki sam sposób jak wcześniej.
new String[] { “_id”, “NAME”},
“FAVORITE = 1”,
null, null, null, null);
// Pobieramy adapter CursorAdapter używany przez widok ListView
ListView listFavorites = (ListView)findViewById(R.id.list_favorites);
CursorAdapter adapter = (CursorAdapter) listFavorites.getAdapter();
// Zmieniamy kursor używany przez adapter CursorAdapter na nowy
adapter.changeCursor(newCursor); Pobieramy adapter używany
Zmieniamy kursor w widoku ListView.
używany przez adapter
favoritesCursor = newCursor; na nowy.
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, “Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
} bazą danych,
i pojawią się jakieś problemy z
Jeśl
} to wyświetlany jest komunikat.
}

To już cały kod, który musimy zmienić w aktywności najwyższego poziomu.


Wypróbujmy go zatem i zobaczmy, jak się sprawdzi w działaniu.

524 Rozdział 12.


Kursory i zadania asynchroniczne

Jazda próbna aplikacji


Tym razem, kiedy zaznaczymy jakiś napój jako ulubiony, zostanie on
zgodnie z oczekiwaniami wyświetlony w aktywności TopLevelActivity.
Gdy klikniemy taki napój, aplikacja wyświetli szczegółowe informacje o nim.

Zaznaczamy pole
Początkowa lista ulubionych wyboru, oznaczając
napojów jest pusta. Latte jako ulubiony
napój.

Kiedy klikniemy Latte


na liście w aktywności
Kiedy wrócimy do TopLevelActivity, zostanie
aktywności TopLevelActivity, uruchomiona aktywność
Latte pojawi się na liście DrinkActivity, prezentując
ulubionych napojów. szczegółowe informacje
o tym napoju.

Tak sobie myślę…


Stosowanie baz danych
w aplikacji bez wątpienia ma wiele
zalet, ale czy takie otwieranie
i zamykanie bazy danych nie
spowalnia działania aplikacji?

Bazy danych zapewniają ogromne możliwości,


ale są powolne.
A to oznacza, że choć nasza aplikacja działa dobrze, to musimy
zwrócić uwagę na jej wydajność…

jesteś tutaj  525


W międzyczasie…

Bazy danych mogą spooowooolniiić działanie aplikacji


Zastanów się, co aplikacja musi zrobić podczas otwierania bazy danych. W pierwszej
kolejności musi przeszukać pamięć urządzenia i odnaleźć plik bazy danych. Jeśli nie
uda się go znaleźć, to aplikacja musi utworzyć nową, pustą bazę danych. Następnie musi
wykonać polecenia SQL w celu utworzenia w tej bazie danych tabeli i zapisania w niej
początkowych danych. I w końcu musi wykonać zapytania, by pobrać z bazy informacje.

Te wszystkie operacje trochę trwają. W przypadku małych baz, takich jak nasza baza
kafeterii Coffeina, wykonanie tych operacji nie zajmuje dużo czasu. Jednak wraz
z powiększeniem się rozmiarów bazy także czas konieczny na wykonanie tych czynności
będzie się wydłużał. W końcu, zanim się zorientujesz, Twoja aplikacja może stracić cały
powab i działać wolniej niż YouTube na święta Bożego Narodzenia.

Nie możemy wiele poradzić na szybkość wykonywania operacji tworzenia i odczytu z bazy
danych, ale możemy zrobić całkiem sporo, aby te operacje nie spowalniały interfejsu
użytkownika aplikacji.

Życie staje się prostsze, gdy wątki ze sobą współdziałają


Poważnym problemem związanym z korzystaniem z wolnej bazy danych jest to,
że może ona sprawić, iż aplikacja będzie wolno reagować na poczynania użytkownika.
Aby zrozumieć, dlaczego tak się dzieje, musimy pomyśleć o tym, w jaki sposób działają
wątki w systemie Android. Od momentu wprowadzenia Androida Lollipop musimy mieć
na uwadze trzy rodzaje wątków:
 Główny wątek aplikacji.
To prawdziwy koń roboczy Androida. To właśnie ten wątek nasłuchuje i odbiera intencje,
odbiera komunikaty generowane przez ekran urządzenia i wywołuje metody naszych aktywności.

 Wątek wyświetlania.
Zazwyczaj nie używamy tego wątku bezpośrednio, jednak odczytuje on listę żądań dotyczących
aktualizacji ekranu i komunikuje się z komponentami sprzętowymi ekranu w celu jego
odświeżania i dbania o to, by aplikacja ślicznie wyglądała.

 Wszystkie inne wątki tworzone przez aplikację.

Jeśli nie zachowamy należytej uwagi, to praktycznie wszystkie operacje realizowane przez
aplikację będą wykonywane w wątku głównym. Dlaczego? Ponieważ to jest wątek, w którym
są wykonywane metody obsługujące wszystkie zdarzenia. Jeśli zatem umieścimy kod tworzący
bazę danych w metodzie onCreate() (czyli dokładnie tak, jak zrobiliśmy to w aplikacji
kafeterii Coffeina), to właśnie wątek główny będzie się mozolnie zajmował komunikacją z bazą
danych zamiast pędzić do przodu i błyskawicznie odbierać i obsługiwać wszystkie zdarzenia
przesyłane do aplikacji przez ekran urządzenia. Jeśli wykonanie kodu obsługującego bazę
danych trwa długo, użytkownik może odnieść wrażenie, że aplikacja go ignoruje.

A zatem cała sztuczka polega na tym, by usunąć kod związany z obsługą bazy danych
z głównego wątku aplikacji i przenieść go do innego wątku, działającego w tle.

526 Rozdział 12.


Kursory i zadania asynchroniczne

Zaostrz ołówek
Planujemy wykonywać kod obsługujący bazę danych w aktywności DrinkActivity
w wątku działającym w tle, ale zanim zajmiemy się wprowadzaniem zmian w kodzie,
musimy poświęcić chwilę na zastanowienie się, co właściwie musimy zrobić.
Kod, którego obecnie używamy, wykonuje trzy podstawowe czynności. Jak uważasz:
w którym z wątków powinny być wykonywane poszczególne bloki kodu? Zapisz typ
wątku, w którym według Ciebie powinien działać każdy z przedstawionych bloków.

A Przygotowanie interfejsu użytkownika.


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drink);
int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Przy każdym z bloków kodu


Główny wątek zaznacz, czy według Ciebie
Wątek działający w tle powinien on być wykonywany
obsługi zdarzeń
w głównym wątku aplikacji
czy w wątku działającym w tle.

B Komunikacja z bazą danych.


CoffeinaDatabaseHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper.getReadableDatabase();
Cursor newCursor = db.query(”DRINK”, ...

Główny wątek
Wątek działający w tle
obsługi zdarzeń

C Aktualizacja widoków danymi pobranymi z bazy.


name.setText(...);
description.setText(...);
photo.setImageResource(...);

Główny wątek
Wątek działający w tle
obsługi zdarzeń

jesteś tutaj  527


Rozwiązanie zadania

Zaostrz ołówek
Rozwiązanie Planujemy wykonywać kod obsługujący bazę danych w aktywności DrinkActivity
w wątku działającym w tle, ale zanim zajmiemy się wprowadzaniem zmian w kodzie,
musimy poświęcić chwilę na zastanowienie się, co właściwie musimy zrobić.
Kod, którego obecnie używamy, wykonuje trzy podstawowe czynności. Jak uważasz:
w którym z wątków powinny być wykonywane poszczególne bloki kodu? Zapisz typ
wątku, w którym według Ciebie powinien działać każdy z przedstawionych bloków.

A Przygotowanie interfejsu użytkownika.


Interfejs użytkownika zawsze jest
super.onCreate(savedInstanceState); tworzony w wątku głównym.
setContentView(R.layout.activity_drink);
int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);

Główny wątek
Wątek działający w tle
obsługi zdarzeń

B Komunikacja z bazą danych.


CoffeinaDatabaseHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(this);
db = coffeinaDatabaseHelper.getReadableDatabase(); Kod obsługujący bazę
Cursor newCursor = db.query(”DRINK”, ... danych chcemy wykonywać
w wątku działającym
w tle, gdyż te czynności
Główny wątek mogą zajmować dużo
Wątek działający w tle
obsługi zdarzeń czasu.

C Aktualizacja widoków danymi pobranymi z bazy.


name.setText(...);
Kod aktualizujący widoki musimy wykonywać
description.setText(...); w wątku głównym, bo w przeciwnym razie
zostanie zgłoszony wyjątek.
photo.setImageResource(...);

Główny wątek
Wątek działający w tle
obsługi zdarzeń

528 Rozdział 12.


Kursory i zadania asynchroniczne

Który kod umieścić w którym wątku?


W przypadku używania w aplikacji bazy danych dobrym pomysłem jest umieszczanie
kodu obsługującego bazę w wątku działającym w tle i aktualizowanie widoków
w wątku głównym. Przerobimy teraz kod metody onFavoriteClicked() aktywności
DrinkActivity, aby Ci pokazać, jak rozwiązywać problemy tego typu.

Oto kod metody w jej dotychczasowej postaci (podzieliliśmy go przy tym na części,
które opisaliśmy u dołu strony):

// Aktualizujemy bazę danych po kliknięciu pola wyboru


public void onFavoriteClicked(View view){

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


CheckBox favorite = (CheckBox)findViewById(R.id.favorite);
1 ContentValues drinkValues = new ContentValues();
drinkValues.put(“FAVORITE”, favorite.isChecked());

SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);
try {
2 SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
db.update( "DRINK" , drinkValues ,
"_id = ?" , new String[] {Integer.toString(drinkNo)});
db.close();
} catch(SQLiteException e) {

Toast toast = Toast.makeText(this, “Baza danych jest niedostępna”,


Toast.LENGTH_SHORT);
3
toast.show();
}
}
1 Kod, który musi zostać wykonany przed kodem korzystającym z bazy danych.
Pierwszych kilka wierszy kodu pobiera wartość pola wyboru i zapisuje ją w obiekcie
drinkValues typu ContentValues. Ten kod musi zostać wykonany przed wykonaniem kodu
operującego na bazie danych.

2 Kod korzystający z bazy danych, który ma być wykonany w wątku działającym w tle.
Ten kod aktualizuje tabelę DRINK.

3 Kod, który ma być wykonany po wykonaniu kodu korzystającego z bazy danych.


Jeśli baza danych będzie niedostępna, to chcemy wyświetlić użytkownikowi stosowny komunikat.
Ten blok kodu musi zostać wykonany w głównym wątku aplikacji obsługującym zdarzenia.

Zaimplementujemy ten kod, używając obiektu klasy AsyncTask. Ale co to w ogóle jest?
jesteś tutaj  529
AsyncTask

Klasa AsyncTask służy do wykonywania java.lang.Object


...
operacji asynchronicznych
Klasa AsyncTask pozwala nam wykonywać operacje w tle. Po zakończeniu
takiej operacji możemy zaktualizować widoki w głównym wątku obsługi android.os.AsyncTask
zdarzeń. Jeśli takie czynności są realizowane cyklicznie, to w trakcie ich <Params, Progress, Result>
wykonywania możemy nawet publikować informacje o postępach prac. void onPreExecute ()
Zadanie asynchroniczne, czyli obiekt AsyncTask, tworzymy poprzez Result doInBackground (Params... params)
utworzenie klasy dziedziczącej po AsyncTask i zaimplementowanie jej void onProgressUpdate (Progress... values)
metody doInBackground(). Kod tej metody zostanie wykonany w wątku void onPostExecute (Result result)
działającym w tle, zatem stanowi ona doskonałe miejsce do umieszczenia
kodu obsługującego bazę danych. Klasa AsyncTask definiuje także metodę AsyncTask<Params, Progress, Result>
execute (Params... params)
onPreExecute(), wykonywaną przed metodą doInBackground(), i metodę
onPostExecute(), wykonywaną po wywołaniu metody doInBackground(). ...
Dostępna jest także metoda onProgressUpdate(), której można używać do
publikowania informacji o postępach prac.
Oto ogólna postać zadania asynchronicznego:

private class MyAsyncTask extends AsyncTask<Params, Progress, Result>


protected void onPreExecute() {
// Kod, który ma zostać wykonany przed rozpoczęciem zadania
}

protected Result doInBackground(Params... params) {


// Kod, który ma zostać wykonany w wątku działającym w tle
}

protected void onProgressUpdate(Progress... values) {


// Kod pozwalający na publikowanie informacji o postępach prac
}

protected void onPostExecute(Result result) {


// Kod, który ma zostać wykonany po zakończeniu zadania
}
}

Klasa AsyncTask jest definiowana przez trzy parametry ogólne: Params, Progress oraz
Results. Pierwszy z nich, Params, to typ obiektu używanego do przekazania parametrów do
metody doInBackground(), drugi, Progress, to typ obiektu stosowanego do przekazywania
informacji o postępach prac, a trzeci, Result, to typ wyniku zwracanego przez zadanie.
Jeśli nie zamierzamy używać któregoś z tych typów, to możemy go określić jako Void.
Sposobom stosowania tej klasy przyjrzymy się dokładniej na kilku następnych stronach,
na których utworzymy klasę UpdateDrinkTask dziedziczącą po AsyncTask i reprezentującą
zadanie służące do aktualizowania w tle informacji o napojach zapisanych w bazie danych.
W dalszej części rozdziału zastosujemy tę klasę w naszej aktywności DrinkActivity.

530 Rozdział 12.


Kursory i zadania asynchroniczne

Metoda onPreExecute()
Zaczniemy od metody onPreExecute(). Jest ona wywoływana przed
rozpoczęciem wykonywania zadania uruchamianego w tle i służy do
jego przygotowania. Metoda ta jest wywoływana w głównym wątku
onPreExecute
aplikacji, zatem ma dostęp do wszystkich widoków tworzących jej interfejs
użytkownika. Metoda onPreExecute() nie ma żadnych parametrów
i zwraca wynik typu void.

W naszej aplikacji użyjemy metody onPreExecute() do pobrania wartości


pola wyboru i zapisania jej w obiekcie drinkValues typu ContentValues.
Aby to zrobić, musimy mieć możliwość dostępu do pola wyboru, a co
więcej, musimy się do niego odwołać przed wykonaniem kodu operującego
na bazie danych. Oprócz tego zdefiniujemy poza metodą zmienną
drinkValues typu ContentValues, z której będą mogły korzystać także
inne metody tej klasy.

A oto nasz nowy kod:

private class UpdateDrinkTask extends AsyncTask<Params, Progress, Result> {

ContentValues drinkValues; na bazie


Zanim wykonamy kod operujący pola wyboru
danych, musimy odczytać wartość ch napojów.
protected void onPreExecute() { używanego do oznaczania ulubiony

CheckBox favorite = (CheckBox)findViewById(R.id.favorite);


drinkValues = new ContentValues();
drinkValues.put(“FAVORITE”, favorite.isChecked());
}

...

A teraz przyjrzymy się metodzie doInBackground().

jesteś tutaj  531


Metoda doInBackground()

Metoda doInBackground()
Metoda doInBackground() jest wywoływana i wykonywana w tle,
bezpośrednio po zakończeniu realizacji metody onPreExecute(). onPreExecute
Mamy możliwość zdefiniowania, jakiego typu będą parametry tej
metody i jakiego typu będzie zwracany przez nią wynik.

My planujemy użyć metody doInBackground() do wykonania kodu


operującego na bazie danych. Dzięki temu operacje na bazie zostaną
wykonane w wątku działającym w tle. Do tej metody przekażemy
identyfikator aktualizowanego napoju i zwrócimy z niej wartość typu doInBackground
Boolean, dzięki której zorientujemy się, czy kod został prawidłowo
wykonany:

private class UpdateDrinkTask extends AsyncTask<Integer, Progress, Boolean> {

ContentValues drinkValues; Zmieniamy ten parametr na Integer, Zmieniamy ten parametr na


aby odpowiadał parametrowi metody Boolean, aby odpowiadał typowi
doInBackground(). zwracanemu przez metodę
doInBackground().
... Ten kod zostanie wykonany w wątku
działającym w tle.
protected Boolean doInBackground(Integer... drinks) { choć
To jest tablica liczb typu Integer,
tylko
int drinkNo = drinks[0]; my będziemy w niej zapisywać napoju.
jedną informację — identyfikator
SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);
try {
SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
db.update(“DRINK”, drinkValues,
“_id = ?”, new String[] {Integer.toString(drinkNo)});
db.close();
Metoda update() używa obiektu
return true; drinkValues utworzonego w metodzie
} catch(SQLiteException e) { onPreExecute().
return false;
}
}

...

Teraz zajmiemy się metodą onProgressUpdate().

532 Rozdział 12.


Kursory i zadania asynchroniczne

Metoda onProgressUpdate()
Metoda onProgressUpdate() jest wywoływana w głównym wątku aplikacji
onPreExecute
używanym do obsługi zdarzeń, dzięki czemu ma dostęp do wszystkich
widoków tworzących jej interfejs użytkownika. Można jej używać do
prezentowania użytkownikom postępu w wykonywanych operacjach poprzez
aktualizowanie stanu widoków. Mamy możliwość zdefiniowania typu
parametrów tej metody. doInBackground
Metoda onProgressUpdate() zostanie wywołana, jeśli w kodzie metody
doInBackground(), w pokazany poniżej sposób, wywołamy metodę
publishProgress():

protected Boolean doInBackground(Integer... count) { onProgressUpdate


for (int i = 0; i < count; i++) {
publishProgress(i);
}
} onPostExecute
protected void onProgressUpdate(Integer... progress) {
setProgress(progress[0]);
}

W naszej aplikacji nie publikujemy informacji o postępach wykonywanych


operacji, dlatego nie musimy implementować tej metody. Aby zasygnalizować,
Nie używamy metody
że nie używamy żadnych obiektów informujących o postępach prac, zmienimy onProgressUpdate(),
sygnaturę klasy UpdateDrinkTask w następujący sposób: dlatego ten parametr
typu określimy jako Void.

private class UpdateDrinkTask extends AsyncTask<Integer, Void, Boolean> {

...

I na koniec przyjrzymy się metodzie onPostExecute().

jesteś tutaj  533


Metoda onPostExecute()

Metoda onPostExecute()
onPreExecute
Metoda onPostExecute() jest wywoływana po zakończeniu zadania
wykonywanego w tle. Jest ona wykonywana w głównym wątku aplikacji,
więc ma dostęp do wszystkich widoków tworzących jej interfejs użytkownika.
Można jej używać do zaprezentowania wyników wykonanych operacji.
Do metody onPostExecute() przekazywany jest wynik zwrócony przez doInBackground
metodę doInBackground(), zatem typ jej parametru musi odpowiadać
typowi wyniku zwracanego przez metodę doInBackground().

My zastosujemy metodę onPostExecute() do sprawdzenia, czy kod


operujący na bazie danych w metodzie doInBackground() został wykonany
prawidłowo. Jeśli coś poszło nie tak, to powiadomimy użytkownika onProgressUpdate
o problemach, wyświetlając odpowiedni komunikat. Operacje te wykonujemy
w metodzie onPostExecute(), gdyż ma ona dostęp do interfejsu
użytkownika aplikacji — metoda doInBackground() jest wykonywana
w wątku działającym w tle, więc nie może aktualizować żadnych widoków.
onPostExecute
Oto kod metody onPostExecute():

private class UpdateDrinkTask extends AsyncTask<Integer, Void, Boolean> {

ackground()
... To jest typ Boolean, gdyż nasza metoda doInB
zwraca właśn ie warto ść tego typu.

protected void onPostExecute(Boolean success) { Tworząc komunikat, przekazujemy do


niego kontekst aktywności DrinkActivity.
if (!success) {
Toast toast = Toast.makeText(DrinkActivity.this,
“Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
}
}
}

534 Rozdział 12.


Kursory i zadania asynchroniczne

Klasa AsyncTask
Kiedy po raz pierwszy przedstawialiśmy klasę AsyncTask, napisaliśmy, że jest
ona definiowana przez trzy parametry ogólne: Params, Progress oraz Results.
Określiliśmy je na podstawie typów parametrów metod doInBackground(),
onProgressUpdate() oraz onPostExecute(). Params to typ parametrów metody
doInBackground(), Progress jest typem parametrów metody onProgressUpdate(),
a Result — typem parametru metody onPostExecute():
private class MyAsyncTask extends AsyncTask<Params, Progress, Result>
protected void onPreExecute() {
// Kod, który ma zostać wykonany przed rozpoczęciem zadania
}

protected Result doInBackground(Params... params) {


// Kod, który ma zostać wykonany w wątku działającym w tle
}

protected void onProgressUpdate(Progress... values) {


// Kod pozwalający na publikowanie informacji o postępach prac
}

protected void onPostExecute(Result result) {


// Kod, który ma zostać wykonany po zakończeniu zadania
}
}

W naszym przypadku metoda doInBackground() ma parametry typu Integer, a metoda


onPostExecute() — parametr typu Boolean. W naszej prostej aplikacji dla kafeterii
Coffeina metoda onProgressUpdate() nie jest używana. A to oznacza, że w naszym
przypadku parametrem typu Params jest Integer, parametrem typu Progress jest Void,
a parametrem typu Result — Boolean:
private class UpdateDrinkTask extends AsyncTask<Integer, Void, Boolean> {
...
protected Boolean doInBackground(Integer... drinks) {
...
}

protected void onPostExecute(Boolean... success) {


...
}
}

Teraz już wiesz wszystko, co trzeba, by utworzyć zadanie — przekonajmy się zatem,
jak takie zadanie można wykonać.

jesteś tutaj  535


Wykonanie

Wykonanie zadania AsyncTask


Zadanie asynchroniczne można wykonać, wywołując metodę execute() klasy AsyncTask. Jeśli metoda
doInBackground() ma jakieś parametry, to należy je przekazać w wywołaniu metody execute().
Na przykład w naszej aplikacji do metody doInBackground() klasy AsyncTask chcemy przekazać
identyfikator napoju klikniętego przez użytkownika, dlatego powinniśmy uruchamiać zadanie
w następujący sposób:

int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);


new UpdateDrinkTask().execute(drinkNo);
Typ parametru przekazywanego do metody execute() powinien odpowiadać typowi parametrów
metody doInBackground(). Nasza metoda doInBackground() ma parametry typu Integer,
więc musimy do niej przekazać liczby całkowite:

protected Boolean doInBackground(Integer... drinks) {


...
}
Nasze asynchroniczne zadanie UpdateDrinkTask chcemy wykonać w metodzie onFavoritesClicked()
aktywności DrinkActivity. Oto, jak teraz będzie wyglądał kod tej metody:

// Aktualizujemy bazę danych po kliknięciu pola wyboru


public void onFavoriteClicked(View view){
int drinkNo = (Integer)getIntent().getExtras().get(EXTRA_DRINKNO);
CheckBox favorite = (CheckBox)findViewById(R.id.favorite);
ContentValues drinkValues = new ContentValues(); Cały ten kod zastępujemy
drinkValues.put(”FAVORITE”, favorite isChecked()); wywołaniem zadania
asynchronicznego.
SQLiteOpenHelper coffeinaDatabaseHelper =
new CoffeinaDatabaseHelper(DrinkActivity.this);
try {
SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
db.update( ”DRINK” , drinkValues ,
”_id = ?” , new String[] {Integer.toString(drinkNo)});
db.close();
} catch(SQLiteException e) {
Toast toast = Toast.makeText(this, ”Baza danych jest niedostępna”, Toast.LENGTH_SHORT);
toast.show();
}
new UpdateDrinkTask().execute(drinkNo); Wykonujemy zadanie AsyncTask, przekazują
c
do niego identyfikator napoju.
}

Na następnej stronie pokażemy kompletny kod aktywności DrinkActivity.

536 Rozdział 12.


Kursory i zadania asynchroniczne

Kod aktywności DrinkActivity


Kiedy stosujemy zadania typu AsyncTask, to definiujemy je jako klasę wewnętrzną,
umieszczoną w klasie aktywności, w której chcemy tego zadania używać. Dlatego też
dodamy naszą klasę UpdateDrinkTask jako klasę wewnętrzną klasy DrinkActivity.
Wykonamy zadanie w metodzie onFavoriteClicked() klasy DrinkActivity, tak by
po kliknięciu pola wyboru przez użytkownika zaktualizowało ono w tle bazę danych.

Oto zmodyfikowane fragmenty pliku DrinkActivity.java:

package com.hfad.coffeina;

Coffeina
...
Importujemy klasę AsyncTask.
import android.os.AsyncTask; app/src/main

public class DrinkActivity extends Activity { java

com.hfad.coffeina
... Metody onCreate() nie musimy zmieniać,
więc nie pokazywaliśmy jej tutaj.
DrinkActivity.java
// Aktualizujemy bazę po zaznaczeniu pola wyboru
public void onFavoriteClicked(View view){
int drinkNo = (Integer)getIntent().getExtras().get(”drinkNo”);
new UpdateDrinkTask().execute(drinkNo); Wykonujemy zadanie.
}

Dodajemy zadanie AsyncTask do


// Klasa wewnętrzna służąca do aktualizacji danych napoju aktywności jako klasę wewnętrzną.
private class UpdateDrinkTask extends AsyncTask<Integer, Void, Boolean> {
ContentValues drinkValues;
Przed wykonaniem kodu operując
ego na bazie
danych zapisujemy wartość pola
protected void onPreExecute() { drinkValues klasy ContentValuse. wyboru w obiekcie
CheckBox favorite = (CheckBox)findViewById(R.id.favorite);
drinkValues = new ContentValues();
drinkValues.put(“FAVORITE”, favorite.isChecked());
}

Dalsza część kodu


znajduje się na
następnej stronie.

jesteś tutaj  537


Kod aktywności DrinkActivity

Kod aktywności DrinkActivity (ciąg dalszy)


Kod wykonujący operacje
na bazie danych wykonujemy
protected Boolean doInBackground(Integer... drinks) { w wątku działającym w tle.
int drinkNo = drinks[0];
SQLiteOpenHelper coffeinaDatabaseHelper = new CoffeinaDatabaseHelper(DrinkActivity.this);
try {
SQLiteDatabase db = coffeinaDatabaseHelper.getWritableDatabase();
db.update(”DRINK”, drinkValues,
”_id = ?”, new String[] {Integer.toString(drinkNo)});
db.close();
return true; Aktualizujemy wartość Coffeina
kolumny FAVORITE.
} catch(SQLiteException e) {
return false; app/src/main
}
} java
protected void onPostExecute(Boolean success) {
if (!success) { com.hfad.coffeina
Toast toast = Toast.makeText(DrinkActivity.this,
”Baza danych jest niedostępna”, Toast.LENGTH_SHORT); DrinkActivity.java
toast.show();
}
Jeśli nie udało się prawidłowo wykonać kodu
} operującego na bazie danych, to wyświetlamy
} użytkownikowi stosowny komunikat.

To już wszystko, co musisz wiedzieć, by móc tworzyć i stosować W idealnym świecie cały kod
zadania asynchroniczne, używając do tego klasy AsyncTask. Kiedy
użytkownik kliknie pole wyboru ulubionego napoju w aktywności realizujący operacje na bazie
DrinkActivity, to baza danych zostanie zmodyfikowana przez danych byłby wykonywany
wątek działający w tle.
w wątkach działających
w tle. My nie mamy jednak
zamiaru zmieniać pozostałych
aktywności naszej aplikacji
tak, by operacje na bazie były
wykonywane w tle, ale może Ty
to zrobisz?

538 Rozdział 12.


Kursory i zadania asynchroniczne

Podsumowanie etapów działania zadań AsyncTask


1 Metoda onPreExecute() służy do przygotowania zadania.
onPreExecute Jest ona wywoływana przed uruchomieniem zadania
wykonywanego w tle i działa w głównym wątku aplikacji.

2 Metoda doInBackground() wykonuje zadanie w wątku


działającym w tle.
doInBackground Jest ona wywoływana bezpośrednio po metodzie onPreExecute().
Możemy określić, jakiego typu będą jej parametry i zwracany wynik.

3 Metoda onProgressUpdate() służy do przekazywania


informacji o postępach wykonywanych prac.
onProgressUpdate Jest ona wykonywana w głównym wątku aplikacji, kiedy w metodzie
doInBackground() wywołamy metodę publishProgress().

4 Metoda onPostExecute() służy do wyświetlania wyników


wykonania zadania po zakończeniu metody doInBackground().
onPostExecute Jest ona wykonywana w głównym wątku aplikacji. Jej parametrem
jest wartość zwrócona przez metodę doInBackground().

Nie istnieją
głupie pytania
P: Kiedyś już napisałem kod, który  O : Najkrócej rzecz ujmując, taka próba P: Jeśli odczytanie danych z bazy 
po prostu wykonywał operacje na  skończyłaby się zgłoszeniem wyjątku. Rozwijając zajmuje kilka sekund, to co zobaczy 
bazie danych, i wszystko działało  nieco ten temat, należałoby stwierdzić, że użytkownik?
świetnie. Czy takie operacje na bazie 
naprawdę trzeba wykonywać w tle?
wielowątkowe interfejsy użytkownika są
niezwykle podatne na występowanie błędów. O: Użytkownik najpierw zobaczy puste
widoki, a dopiero po jakimś czasie zostaną
O: W przypadku naprawdę małych Android eliminuje ten problem, uniemożliwiając
stosowanie takich rozwiązań. one wypełnione danymi.
baz danych, takich jak nasza, czas
dostępu do bazy danych będzie zapewne P: Które fragmenty kodu  P: Dlaczego w zadaniu AsyncTask 
niezauważalny. Ale będzie to wynikało wykonującego operacje na bazie  umieściliście kod wykonujący operacje 
wyłącznie z małej wielkości bazy. Jeśli baza danych są najwolniejsze? na bazie danych z tylko jednej 
będzie większa, albo jeśli uruchomimy aktywności?
aplikację na wolniejszym urządzeniu, czas O: Nie sposób tego ogólnie określić. Jeśli O: Chcieliśmy Ci pokazać sposób
dostępu do bazy zacznie mieć większe używana baza danych ma złożoną strukturę,
znaczenie. Dlatego owszem kod to jej pierwsze otwarcie może zająć sporo korzystania z klasy AsyncTask na jednej
operujący na bazie danych zawsze powinien czasu, bo będzie wymagało utworzenia aktywności w ramach przykładu. W praktyce
być wykonywany w tle. wszystkich tabel. Także wykonanie tak samo należy wykonywać wszystkie
złożonych zapytań może zajmować bardzo operacje na bazach danych we wszystkich
P: Przypomnijcie mi: dlaczego  dużo czasu. Ogólnie rzecz biorąc, należy aktywnościach.
aktualizowanie widoków z wątku  podejść do zagadnienia ostrożnie i wszystkie
wykonywanego w tle jest błędem? operacje na bazie danych wykonywać w tle.
jesteś tutaj  539
Przybornik

Twój przybornik do Androida


Rozdział 12.

Opanowałeś już rozdział 12. i dodałeś  j
do swojego przybornika z narzędziami  Pełny kod przykładowe
lika cji pre zen tow ane j
ap
umiejętność podłączania aplikacji do baz  mo żes z
w tym rozdziale
danych SQLite. pobrać z ser we ra FT P
wydawnictwa Helion:
ftp://ftp.helion.pl/
przyklady/andrrg.zip

    CELNE SPOSTRZEŻENIA
 Kursory obiekty klasy Cursor  Po zakończeniu korzystania z kursora
umożliwiają odczytywanie i zapisywanie i połączenia z bazą danych należy je
informacji w bazach danych. zamknąć.
 Kursory tworzy się, wywołując metodę  Do korzystania z kursorów służy adapter
query klasy SQLiteDatabase. klasy CursorAdapter. Do wypełnienia
W niewidoczny sposób kursory tworzą widoku ListView wartościami
polecenie SQL SELECT. z kursora można używać adaptera
SimpleCursorAdapter.
 Metoda getWritableDatabase() zwraca
obiekt SQLiteDatabase pozwalający  Aplikacje należy projektować w taki sposób,
odczytywać i zapisywać informacje w bazie by przydatne treści były umieszczane
danych. w aktywności najwyższego poziomu.
 Metoda getReadableDatabase() zwraca  Metoda changeCursor() klasy
obiekt SQLiteDatabase. Obiekt ten CursorAdapter zastępuje kursor aktualnie
zapewnia dostęp do bazy danych w trybie używany przez adapter nowym kursorem
tylko do odczytu. Może on zapewniać także przekazanym w wywołaniu. Stary kursor
możliwość zapisu danych w bazie, ale nie ma jest następnie automatycznie zamykany.
takiej gwarancji.  Kod wykonujący operacje na bazie danych
 Po zawartości kursora można się poruszać, należy wykonywać w tle, korzystając z klasy
używając metod moveTo*(). AsyncTask.
 Do odczytywania wartości z kursora służą
metody get*().

540 Rozdział 12.


13. Usługi
Do usług

Czy wspominałem,
że świadczę usługę
WymuszanieHaraczyService?

Są operacje, które będziemy chcieli realizować niezależnie od tego,


która aplikacja jest widoczna na ekranie.
Na przykład kiedy rozpoczniesz odtwarzanie pliku w aplikacji muzycznej, to najprawdopodobniej
będziesz oczekiwać, że będzie ona kontynuowała odtwarzanie nawet wówczas, gdy przejdziesz
do innej aplikacji. Z tego rozdziału dowiesz się, jak używać usług, by radzić sobie w sytuacjach
takich jak ta. Przy okazji nauczysz się korzystać z kilku wbudowanych usług systemu Android.
Dowiesz się, jak za pomocą usługi powiadomień sprawić, by użytkownik zawsze był dobrze
poinformowany, i jak poznać aktualne położenie geograficzne, korzystając z usługi lokalizacji.

to jest nowy rozdział  541


Usługi

Usługi działają za kulisami


Aplikacje na Androida są kolekcjami aktywności i innych zasobów. Znaczna
większość ich kodu jest związana z prowadzeniem interakcji z użytkownikiem,
czasami jednak może się pojawić konieczność wykonywania jakichś operacji w tle.
Na przykład możemy chcieć pobrać duży plik, strumieniować utwór muzyczny lub
nasłuchiwać wiadomości przesyłanych z serwera.

Aktywności raczej nie nadają się do wykonywania takich operacji. W prostych


przypadkach możemy utworzyć wątek i wykonywać w nim takie czynności,
ale jeśli nie zachowamy ostrożności, to kod aplikacji szybko może się stać złożony
i nieczytelny.

I właśnie dlatego opracowano usługi (ang. services). Usługa jest komponentem


aplikacji przypominającym nieco aktywność, lecz pozbawionym interfejsu
użytkownika. Cykl życia usług jest prostszy od cyklu życia aktywności, a oprócz tego
usługi są wyposażone w zestaw możliwości ułatwiających pisanie kodu, który będzie
działał w tle, podczas gdy użytkownik będzie mógł zajmować się czymś innym.

Istnieją dwa typy usług


Dostępne są dwa różne rodzaje usług:

 Usługi uruchomione.
Usługi uruchomione (ang. started services) mogą działać w tle przez dowolnie
długi okres czasu, i to nawet po tym, jak aktywność, która je uruchomiła,
zostanie usunięta. Na przykład gdybyśmy chcieli pobrać z internetu duży plik,
to moglibyśmy do tego użyć właśnie usługi uruchomionej. Po zakończeniu
operacji taka usługa jest zatrzymywana.

 Usługi powiązane.
Usługi powiązane (ang. bound services) są skojarzone z jakimś innym
komponentem, takim jak aktywność. Aktywność może prowadzić interakcję
z taką usługą — przesyłać do niej żądania i pobierać wyniki. Usługi tego
typu działają tak długo, jak długo działa komponent, z którym są powiązane.
Kiedy komponenty nie będą już powiązane, usługa jest niszczona. Gdybyśmy
chcieli napisać drogomierz zliczający przejechany dystans, to prawdopodobnie
użylibyśmy do tego właśnie usługi powiązanej. Dzięki temu wszystkie
aktywność powiązane z usługą mogłyby cyklicznie prosić usługę o przekazanie
informacji o pokonanym dystansie.

W tym rozdziale napiszemy dwie usługi: usługę uruchomioną i usługę powiązaną.


Zaczniemy od usługi uruchomionej.

542 Rozdział 13.


Usługi

Aplikacja z usługą uruchomioną


Utworzymy teraz nowy projekt aplikacji składającej się z jednej aktywności
o nazwie MainActivity i usługi o nazwie DelayedMessageService.
Za każdym razem, gdy aktywność odwoła się do usługi, ta poczeka
10 sekund, a następnie wyświetli fragment tekstu.
1… 2… 3… 4… 5…
<Layout> Aktywność MainActivity
6… 7… 8… 9… 10…
będzie używała tego układu. A oto i tekst.
</Layout>

activity_main.xml
Usługa wyświetli
tekst po upływie
Aktywność przekaże do 10 sekund.
usługi fragment tekstu.

MainActivity.java DelayedMessageService.java

Aplikację napiszemy w trzech etapach:

1 Wyświetlenie komunikatu w dzienniku.


Zaczniemy od wyświetlenia komunikatu w dzienniku, tylko po to, abyśmy
mogli się przekonać, że usługa działa prawidłowo. Ten dziennik możemy
przejrzeć w Android Studio.

2 Wyświetlenie tosta.
Zadbamy, by komunikat był wyświetlany na ekranie jako tost, dzięki
czemu urządzenie nie będzie musiało być podłączone do Android Studio,
żebyśmy mogli się przekonać, że usługa działa.

3 Wyświetlenie komunikatu w obszarze powiadomień.


Zmodyfikujemy naszą usługę DelayedMessageService tak, by korzystała
z wbudowanej usługi powiadomień Androida i wyświetlała komunikaty
w systemowym obszarze powiadomień. Dzięki temu użytkownik będzie
mógł przeczytać komunikat w dowolnym, późniejszym czasie.

Utworzenie projektu
Zaczniemy od utworzenia projektu. Utwórz zatem nowy projekt
aplikacji na Androida o nazwie Wic, a jej kod umieść w pakiecie
com.hfad.wic. Minimalną wersją SDK powinno być API poziomu 16,
dzięki czemu aplikacja będzie działać na większości urządzeń. Utwórz
w aplikacji pustą aktywność o nazwie MainActivity korzystającą
z układu o nazwie activity_main.

Kolejną rzeczą, którą się zajmiemy, będzie utworzenie usługi.


jesteś tutaj  543
IntentService

Tworzymy usługę IntentService ¨  Dziennik


¨  Tost
¨  Powiadomienie
Nową usługę można utworzyć, pisząc klasę dziedziczącą po klasie Service
bądź IntentService.
android.content.Context
Klasa Service jest klasą bazową używaną do tworzenia usług. Udostępnia
...
ona podstawowe możliwości funkcjonalne usług i to zazwyczaj jej będziemy
używać w przypadku tworzenia usług powiązanych.

IntentService jest klasą pochodną klasy Service, zaprojektowaną


do obsługi intencji. Jest ona przeważnie używana do tworzenia usług android.content.ContextWrapper
uruchomionych. ...

Ponieważ tworzymy właśnie taką usługę uruchomioną, dodamy ją do projektu.


W tym celu wybierz z menu opcje File/New/Service/Service (Intent Service).
Tworzonej usłudze nadaj nazwę DelayedMessageService i usuń zaznaczenie android.app.Service
pola wyboru Include helper start methods?. Usuwamy zaznaczenie tego pola, ...
gdyż i tak zastąpimy kod, który wygeneruje Android Studio.

Naszą usługę napiszemy, rozszerzając klasę IntentService i implementując


jej metodę onHandleIntent(). To właśnie ta metoda powinna zawierać
android.app.IntentService
cały kod, który chcemy wykonać w momencie odwołania do usługi:
...

rvice.
package com.hfad.wic; To jest hierarchia klasy IntentSe

import android.app.IntentService;
import android.content.Intent; Rozszerzamy klasę IntentService
.

public class DelayedMessageService extends IntentService {

public DelayedMessageService() {
super(”DelayedMessageService”);
} musimy Wic
Kod, który ma wykonywać usługa, nt.
umieścić w metodzie onHandleInte
@Override app/src/main
protected void onHandleIntent(Intent intent) {
// Tu coś robimy java

}
com.hfad.wic
}

DelayedMessage
Ogólny opis tej usługi zamieścimy na następnej stronie. Service.java

544 Rozdział 13.


Usługi

Usługa IntentService z wysokości 15 tysięcy metrów


Do utworzenia usługi uruchomionej używamy klasy IntentService, a więc przyjrzyjmy się,
jak działają usługi tego typu.

1 Aktywność określa usługę, do której musi się odwołać, tworząc jawną intencję.
Intencja określa usługę, do której ma zostać przekazana.

Intencja

Do: DelayedMessageService
MainActivity tekst:”Już czas!”

2 Intencja jest przekazywana do usługi.

Intencja Intencja

Do: DelayedMessage Do: DelayedMessage


Service Service
MainActivity tekst:”Już czas!” tekst:”Już czas!” DelayedMessageService
Android

3 Usługa jest uruchamiana i obsługuje intencję.


Zostaje wywołana metoda onHandleIntent() klasy IntentService. Metoda ta jest
wykonywana w odrębnym wątku. Jeśli do usługi zostanie przekazanych kilka intencji,
to są one obsługiwane kolejno, jedna po drugiej. Kiedy usługa zakończy obsługę
intencji, jest zatrzymywana.

Oho, dostałam jakiś


tekst. Świetnie wiem,
co z nim zrobić…
1… 2… 3… 4…

DelayedMessageService

Jak widać, usługi można uruchamiać tak samo jak intencje — poprzez
utworzenie odpowiedniej intencji. Różnica polega jednak na tym, że w przypadku
uruchamiania usługi na ekranie nic się nie zmienia, gdyż usługi nie mają
interfejsu użytkownika.

W naszej aplikacji chcemy, by usługa DelayedMessageService wyświetliła


komunikat w dzienniku systemowym. Dlatego zanim odpowiednio zmodyfikujemy
jej kod, przyjrzyjmy się, jak rejestrować komunikaty w dzienniku.

jesteś tutaj  545


Witamy w Fiskidagurinn

Jak rejestrować komunikaty? ¨  Dziennik


¨  Tost
¨  Powiadomienie
Dodawanie komunikatów do dziennika jest użytecznym sposobem sprawdzania,
czy kod działa zgodnie z oczekiwaniami. Komunikaty, które mają być rejestrowane,
podajemy w kodzie Javy, a podczas działania aplikacji sprawdzamy wyniki
w systemowym dzienniku Androida, określanym także jako logcat.
Dostępna jest także metoda
Komunikaty można rejestrować, używając poniższych metod klasy Android.util.Log: Log.wtf(), przeznaczona do
rejestrowania wyjątków, które
Log.v(String znacznik, String komunikat) Rejestruje tak zwany „przegadany” komunikat. nigdy nie powinny wystąpić.
Zgodnie z dokumentacją
Log.d(String znacznik, String komunikat) Rejestruje komunikat używany do debugowania. Androida „wtf” oznacza
„What a Terrible Failure” (czyli
Log.i(String znacznik, String komunikat) Rejestruje komunikat informacyjny. „Cóż za straszliwa awaria”).
My jednak wiemy, że
Log.w(String znacznik, String komunikat) Rejestruje komunikat ostrzegawczy. w rzeczywistości to skrót
Log.e(String znacznik, String komunikat) Rejestruje komunikat o błędzie. od słów „Welcome
to Fiskidagurinn” („Witamy
w Fiskidagurinn”), które
Każdy komunikat składa się z łańcucha znaków określanego jako „znacznik”, odwołują się do festiwalu
który identyfikuje źródło komunikatu, oraz z samego komunikatu. Na Święta Ryby, odbywającego
przykład aby zarejestrować „przegadany” komunikat pochodzący z usługi się co roku w miejscowości
DelayedMessageService, możemy użyć następującego wywołania metody Log.v(): Dalvik na Islandii. Często
można usłyszeć programistów
Log.v(”DelayedMessageService”, ”To jest komunikat.”); Androida, którzy mówią:
„Moje AVD uruchamia się
Zawartość dziennika można przeglądać w Android Studio, filtrując ją dodatkowo na 8 minut! WTF??”, oddając
podstawie typu komunikatów. Aby wyświetlić dziennik, należy kliknąć opcję Android w ten sposób hołd niewielkiej
u dołu okna Android Studio, a następnie przejść na kartę logcat. miejscowości, której nazwa
została użyta do określenia
formatu wykonywalnych kodów
Komunikaty można bajtowych Androida.
Wybierz opcję logcat. filtrować na podstawie
ich typu.

To jest obszar dziennika


logcat. Tu będą wyświetlane
wszystkie zarejestrowane
komunikaty.

Wybierz opcję Android.

546 Rozdział 13.


Usługi

Kompletny kod usługi DelayedMessageService


Chcemy, by nasza usługa pobrała fragment tekstu przekazany w intencji,
odczekała 10 sekund, a następnie zapisała pobrany tekst w dzienniku
systemowym. W tym celu napiszemy metodę showText(), która będzie
rejestrowała tekst, a potem wywołamy ją z odpowiednim opóźnieniem w kodzie
metody onHandleIntent().

Oto pełny kod naszej usługi, zapisany w pliku DelayedMessageService.java


(zastąp nim kod wygenerowany przez Android Studio):

package com.hfad.wic;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log; Rozszerzamy klasę IntentService
.

public class DelayedMessageService extends IntentService { Do przekazywania treści


public static final String EXTRA_MESSAGE = ”message”; komunikatu z aktywności
do usługi użyjemy stałej.
public DelayedMessageService() {
wej.
super(”DelayedMessageService”); Wywołujemy konstruktor klasy bazo

} Ta metoda zawiera kod, który chce


wywołać, kiedy usługa odbierze my
@Override intencję.

protected void onHandleIntent(Intent intent) { Wic

synchronized (this) {
app/src/main
try {
Czekamy 10 sekund.
wait(10000); java
} catch (InterruptedException e) {
com.hfad.wic
e.printStackTrace();
} Pobieramy tekst z intencji.
DelayedMessage
} Service.java
String text = intent.getStringExtra(EXTRA_MESSAGE);
showText(text);
} Wywołujemy metodę showText().

private void showText(final String text) {


Log.v(”DelayedMessageService”, ”Treść komunikatu: ” + text);
}
To wywołanie rejestruje fragment tekstu,
} dzięki czemu możemy go zobaczyć na karcie
logcat w Android Studio.
jesteś tutaj  547
Deklarowanie usługi

Usługi są deklarowane w pliku AndroidManifest.xml ¨  Dziennik


¨  Tost
¨  Powiadomienie
Tak samo jak aktywności, także usługi należy zadeklarować w pliku manifestu
— AndroidManifest.xml. W przypadku usług służy do tego element <service>.
Dzięki zadeklarowaniu usługi Android będzie w stanie odwołać się do niej;
gdyby usługa nie została zadeklarowana, skorzystanie z niej nie byłoby możliwe.

Android Studio automatycznie deklaruje usługi w pliku AndroidManifest.xml


podczas ich tworzenia. Oto, jak wygląda nasz kod pliku manifestu:

<?xml version=”1.0” encoding=”utf-8”?>


<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.hfad.joke” >
<application
Wic
... >

app/src/main
<activity
W taki sposób należy
<xml>
</xml>
... deklarować usługi w pliku
AndroidManifest.xml. Android AndroidManifest.xml
</activity> Studio powinno to zrobić za
nas automatycznie.
<service
android:name=”.DelayedMessageService”
android:exported=”false” >
Nazwa usługi jest poprzedzona
</service> znakiem kropki, aby Android móg
ł
umieścić przed nią nazwę pakietu,
</application> uzyskując w ten sposób pełną nazw
klasy. ę
</manifest>

Element <servive> ma dwa atrybuty.

Pierwszy z nich, android:name, określa nazwę usługi — w naszym przypadku jest


to DelayedMessageService.

Drugi atrybut, android:exported, informuje system, czy dana usługa może


być używana przez inne aplikacje. Przypisanie temu atrybutowi wartości false
oznacza, że usługa będzie używana wyłącznie wewnątrz bieżącej aplikacji.

A teraz, kiedy już mamy usługę, musimy ją uruchomić — czyli wywołać ją


z poziomu kodu aktywności.

548 Rozdział 13.


Usługi

Dodajemy przycisk do układu activity_main.xml


Chcemy, aby aktywność MainActivity uruchomiła usługę DelayedMessageService
po kliknięciu przycisku. Zaczniemy zatem od dodania przycisku do układu używanego
przez tę aktywność.

W pierwszej kolejności dodaj do pliku strings.xml poniższe zasoby łańcuchowe


(użyjemy ich w kodzie aktywności i układu):
aplikacji.
Obu tych łańcuchów używamy w
<string name=”button_response”>Już czas!</string>
<string name=”button_text”>Jaki jest sekret komedii?</string>

Teraz zajmiemy się aktualizacją układu activity_main.xml tak,


by w aktywności MainActivity został wyświetlony przycisk:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
Wic
android:layout_height=”match_parent”
tools:context=”.MainActivity”>
app/src/main

<Button res
android:layout_width=”wrap_content” Ten element tworzy przycisk. Po
jego
kliknięciu zostanie wywołana meto layout
android:layout_height=”wrap_content” onClick() aktywności. da
<xml>
android:text=”@string/button_text” </xml>

android:id=”@+id/button” activity_
main.xml
android:onClick=”onClick”
android:layout_alignParentTop=”true”
android:layout_centerHorizontal=”true” />
</RelativeLayout>

Na następnej stronie zajmiemy się aktualizacją kodu aktywności


MainActivity i uruchamianiem usługi.

jesteś tutaj  549


Uruchamianie usługi

Usługę uruchamiamy, wywołując metodę startService() ¨  Dziennik


¨  Tost
¨  Powiadomienie
Usługę uruchamiamy w kodzie aktywności w podobny sposób, w jaki uruchamiamy inne
aktywności. Musimy w tym celu utworzyć intencję jawną skierowaną do usługi, którą chcemy
uruchomić. Następnie wywołujemy metodę startService(), przekazując do niej utworzoną
intencję. Oto kod naszej aktywności:

Intent intent = new Intent(this, DelayedMessageService.class);


startService(intent); Uruchamianie usługi przypomina uruchamianie aktywności,
z tym że używamy w tym celu metody startService()
a nie startActivity().
Tego fragmentu kodu użyjemy w metodzie onClick() klasy
MainActivity, tak by każde kliknięcie przycisku powodowało
uruchomienie usługi. Oto kod tej klasy:

package com.hfad.wic;

Wic
import android.app.Activity;
import android.os.Bundle;
app/src/main
import android.view.Menu;
import android.view.MenuItem;
java

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
Używamy tych wszystkich klas.
import android.view.View; DelayedMessage
Service.java

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
} Ta metoda zostanie wywołana
po kliknięciu przycisku. Tworzymy intencję.
public void onClick(View view) {
Intent intent = new Intent(this, DelayedMessageService.class);
intent.putExtra(DelayedMessageService.EXTRA_MESSAGE,
getResources().getString(R.string.button_response));
startService(intent);
Dodajemy tekst do intencji.
}
} Uruchamiamy usługę.

I to już cały kod, którego potrzebujemy, by uruchomić usługę.


Przekonajmy się zatem, co się stanie, kiedy uruchomimy aplikację.

550 Rozdział 13.


Usługi

Jazda próbna aplikacji ¨  Dziennik


¨  Tost
¨  Powiadomienie
Po uruchomieniu aplikacji zostanie wyświetlona aktywność MainActivity:

A oto i przycisk.

A teraz kliknij przycisk i przejdź do Android Studio. W IDE wyświetl zawartość


dziennika — najpierw kliknij opcję Android, umieszczoną u dołu okna programu
z lewej strony, a następnie przejdź na kartę logcat. Po 10 sekundach w dzienniku
zostanie wyświetlony komunikat „Już czas!”.

To jest panel
z zawartością
dziennika.

11-25 12:47:33.987 1294-12943/com.hfad.wic V/DelayedMessageService: Treść komunikatu: Już czas!

Skoro wiemy, że usługa działa, spróbujmy wyświetlić komunikat Po 10-sekundowym opóźnieniu


na ekranie, żebyśmy mogli zobaczyć potwierdzenie działania usługi w dzienniku zostanie wyświetlony
bez podłączania urządzenia do komputera. nasz komunikat.

jesteś tutaj  551


Wyświetlanie tosta

Chcemy, by komunikat został wyświetlony na ekranie ¨  Dziennik


¨  Tost
¨  Powiadomienie
W odróżnieniu od aktywności usługi nie mają własnego interfejsu
użytkownika, co wcale nie oznacza, że nie muszą one informować
użytkownika o tym, co się dzieje. Na przykład użytkownik powinien
zostać poinformowany o tym, że plik został pomyślnie pobrany.

W naszym przypadku znacznie fajniej i atrakcyjniej byłoby wyświetlać


komunikat na ekranie zamiast w dzienniku. Jest tylko jeden problem:
każdy kod próbujący zaktualizować interfejs użytkownika musi być
wykonywany w głównym wątku aplikacji.

Aktualizacje ekranu wymagają użycia wątku głównego


Jak już wiemy, kod, który chcemy wykonywać w ramach usługi, należy
umieścić w jej metodzie onHandleIntent(). Ten kod jest następnie
wykonywany w tle, w odrębnym wątku. To rozwiązanie jest doskonałe Nasza usługa wyświetli
komunikat w formie tosta.
w przypadkach, gdy chcemy, by kod był wykonywany w tle; nie jest
jednak równie dobre, gdy w grę wchodzi aktualizacja interfejsu
użytkownika. Wynika to z faktu, że interfejs użytkownika aplikacji
można modyfikować wyłącznie w jej wątku głównym.

Aby rozwiązać ten problem, skorzystamy z obiektu klasy Handler.


Jak już napisaliśmy w rozdziale 4., obiektów tej klasy można używać do
wykonywania kodu w odrębnym wątku. Okazuje się jednak, że metody
post() klasy Handler możemy używać także w celu przekazania kodu
do wątku głównego. W ten sposób możemy przekazać kod wyświetlający
komunikat do wątku głównego, a tost zostanie prawidłowo wyświetlony.

Aby takie rozwiązanie zadziałało, musimy wykonać następujące czynności:

 Utworzyć obiekt Handler w wątku głównym.

 Wywołać metodę post() klasy Handler w metodzie


onHandleIntent() usługi, aby wyświetlić tost.

Najpierw zobaczmy, jak utworzyć obiekt Handler w wątku głównym.

552 Rozdział 13.


Usługi

Metoda onStartCommand() jest wykonywana w wątku głównym


Aby utworzyć obiekt Handler w wątku głównym, musimy to zrobić w metodzie,
która będzie wykonywana w tym wątku. Nie możemy użyć do tego metody
onHandleIntent(), gdyż jest ona wykonywana w wątku działającym w tle.
Rozwiązaniem tego problemu jest zastosowanie metody onStartCommand().

Metoda onStartCommand() jest wykonywana za każdym razem, gdy uruchamiana


jest usługa typu IntentService. Co więcej, metoda ta jest wykonywana w wątku
głównym przed wykonaniem metody onHandleIntent(). A zatem jeśli utworzymy
obiekt Handler w metodzie onStartCommand(), to będziemy mogli użyć go
w metodzie onHandleIntent() do przekazania kodu do wątku głównego:

...

public class DelayedMessageService extends IntentService {

Dodajemy prywatną zmienną typu Handler, aby


private Handler handler; mieć dostęp do tego obiektu w innych metodach.

... Ta metoda jest wykonywana w wątk


więc pozwoli utworzyć obiekt Han u głównym,
w głównym wątku aplikacji. dler
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handler = new Handler();
return super.onStartCommand(intent, flags, startId);
} Wic

Wywołujemy metodę onStartCommand() klasy IntentService.


@Override app/src/main

protected void onHandleIntent(Intent intent) {


java
// Używamy obiektu handler do przekazania kodu do wątku głównego
}
com.hfad.wic
...
}
DelayedMessage
Service.java
W przypadku stosowania metody onStartCommand() musimy wywołać jej implementację
w klasie bazowej:

super.onStartCommand(intent, flags, startId)

To wywołanie jest konieczne, by usługa mogła prawidłowo obsługiwać cykl życia


używanego przez nią wątku.

Na następnej stronie przedstawimy kompletny kod usługi DelayedMessageService,


a później spróbujemy uruchomić aplikację.

jesteś tutaj  553


Kod usługi DelayedMessageService

Kompletny kod usługi DelayedMessageService ¨  Dziennik


¨  Tost
¨  Powiadomienie
package com.hfad.wic;

import android.app.IntentService;
import android.content.Intent;
import android.os.Handler; klas.
Używamy tych dwóch dodatkowych
import android.widget.Toast;

public class DelayedMessageService extends IntentService {

public static final String EXTRA_MESSAGE = ”message”;


private Handler handler; Obiekt Handler będziemy przechow
ywać w zmiennej prywatnej.

public DelayedMessageService() {
super(”DelayedMessageService”);
}
Obiekt Handler tworzymy w wątku głównym.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handler = new Handler();
return super.onStartCommand(intent, flags, startId);
}
Tej metody nie zmieniamy.
@Override
protected void onHandleIntent(Intent intent) { Wic
synchronized (this) {
try { app/src/main
wait(10000);
} catch (InterruptedException e) { java
e.printStackTrace();
} com.hfad.wic
}
String text = intent.getStringExtra(EXTRA_MESSAGE);
showText(text); DelayedMessage
Service.java
}

private void showText(final String text) {


handler.post(new Runnable() { Używamy obiektu Handler, by przekazać
@Override kod wyświetlający tost do wątku głównego.
public void run() {
Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();
}
}); To jest kontekst, w którym chcemy wyświetlić komunikat.
} Więcej informacji na ten temat podamy na następnej stronie.
}

554 Rozdział 13.


Usługi

Kontekst aplikacji ¨  Dziennik


¨  Tost
¨  Powiadomienie
Przyjrzyjmy się nieco dokładniej wierszowi kodu, który wyświetla tost:

Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();

Pierwszym argumentem metody Toast.makeText() jest kontekst,


w którym ma być wyświetlony tost. Tworząc tost w aktywności, używamy
słowa kluczowego this, przekazując tym samym referencję do bieżącej
aktywności.

W przypadku usług takie rozwiązanie nie jest możliwe, ponieważ


kontekst usługi nie ma dostępu do ekranu. Dlatego zawsze, gdy będziemy
potrzebowali kontekstu wewnątrz usługi, w sytuacjach podobnych do tej,
będziemy musieli skorzystać z metody getApplicationContext(). Dzięki
temu uzyskamy kontekst dowolnej aplikacji, która będzie wyświetlona
w momencie wykonywania kodu. To oznacza, że usługa będzie w stanie
wyświetlić tost, nawet jeśli użytkownik przejdzie do innej aplikacji.

Jazda próbna aplikacji


Spróbujmy teraz uruchomić naszą aplikację.

Kiedy klikniesz przycisk wyświetlony w aktywności


Kliknij przycisk i poczekaj.
MainActivity, to po 10 sekundach zostanie wyświetlony
komunikat. Pojawi się on niezależnie od tego, która aplikacja
będzie w danym momencie wyświetlona na ekranie.

Jeśli klikniesz przycisk kilka razy w krótkich odstępach czasu,


to na ekranie zostanie wyświetlonych kilka tostów, przy czym
odstęp czasu pomiędzy momentami wyświetlania kolejnych
komunikatów będzie wynosił około 10 sekund. Usługa
obsługuje każdą odebraną intencję niezależnie, jedna
po drugiej.
Po 10 sekundach pojawi się tost
.

jesteś tutaj  555


Powiadomienia

Czy można wymyślić coś lepszego od tostów? ¨  Dziennik


¨  Tost
¨  Powiadomienie
Teraz już wiemy, jak wyświetlić na ekranie fragment tekstu, używając tostów.
To przydatne rozwiązanie, jeśli chcemy na przykład poinformować użytkownika
o zakończeniu pobierania bardzo dużego pliku. Prawda jest jednak taka,
że tosty nie wyróżniają się aż tak bardzo i jeśli nie będziemy patrzeć na ekran
w odpowiednim momencie, to w ogóle możemy ich nie zauważyć. A zatem
w razie konieczności poinformowania użytkownika o ważnym zdarzeniu
będziemy musieli zastąpić tost powiadomieniem.

Powiadomienia są komunikatami wyświetlanymi na liście w górnej części


ekranu. Jeśli użytkownik nie zobaczy powiadomienia w momencie, gdy
zostało ono utworzone, to i tak nic nie straci — wciąż będzie mógł je obejrzeć,
wyświetlając szufladę powiadomień, wystarczy w tym celu przeciągnąć palcem
z góry na dół ekranu.

To jest szuflada powiadomień.


To jest powiadomienie. To są ikony powiadomień.

Aby wysłać powiadomienie, będziemy musieli użyć jednej z usług


wbudowanych w Androida, a konkretnie usługi powiadomień.

Android dysponuje grupą wbudowanych usług, których możemy używać


w tworzonych aplikacjach. Między innymi są to: usługa alarmów
(do zarządzania alarmami), usługa pobierania (do pobierania plików
przy użyciu protokołu HTTP) i usługa lokalizacyjna (do aktualizacji
informacji o położeniu urządzenia).

My skorzystamy z usługi powiadomień, służącej do zarządzania


powiadomieniami. Na następnej stronie wyjaśnimy, w jaki sposób
planujemy zastosować tę usługę w naszej aplikacji.

556 Rozdział 13.


Usługi

Jak używać usługi powiadomień?


Oto ogólny sposób, w jaki nasza aplikacja będzie korzystała z systemowej usługi powiadomień:

1 Aktywność MainActivity uruchamia usługę DelayedMessageService,


przesyłając do niej intencję.
Intencja

”Już czas!”

MainActivity DelayedMessageService

2 Usługa DelayedMessageService tworzy nowy obiekt Notification.


Obiekt Notification zawiera szczegółowe informacji o konfiguracji powiadomienia,
takie jak jego tekst, tytuł oraz ikonę.

tekst=”Już czas!”

DelayedMessageService Notification

3 Usługa DelayedMessageService tworzy obiekt NotificationManager, uzyskując


w ten sposób dostęp do systemowej usługi zarządzania powiadomieniami.
Usługa DelayedMessageService przekazuje obiekt Notification do obiektu
NotificationManager i powiadomienie zostaje wyświetlone.

NotificationManager
DelayedMessageService

tekst=”Już czas!”

Notification

Zaczniemy od utworzenia powiadomienia.

jesteś tutaj  557


Konstruowanie powiadomienia

Powiadomienia tworzymy, używając ¨  Dziennik


¨  Tost
budowniczego powiadomień ¨  Powiadomienie

Powiadomienia tworzymy, używając budowniczego powiadomień,


który zwraca obiekty Notification. Budowniczy pozwala tworzyć
powiadomienia o określonym zestawie cech, bez konieczności pisania
zbyt rozbudowanego kodu. Każde powiadomienie musi zawierać
niewielką ikonę, tytuł oraz jakiś tekst.

Poniżej przedstawiliśmy przykładowy kod używany do tworzenia


powiadomień. Generuje on powiadomienie o wysokim priorytecie,
które w momencie wyświetlania spowoduje wibrowanie urządzenia,
a po kliknięciu — zniknie:

To wywołanie Notification notification = new Notification.Builder(this)


spowoduje
wyświetlenie .setSmallIcon(R.mipmap.ic_launcher)
w powiadomieniu Te wywołania określają tytuł
małej ikony, w tym .setContentTitle(getString(R.string.app_name)) i tekst powiadomienia.
przypadku będzie .setContentText(text)
to zasób mipmap
o identyfikatorze .setAutoCancel(true) To wywołanie sprawi, że powiadomienie zniknie po kliknięciu.
ic_launcher.
.setPriority(Notification.PRIORITY_MAX)
Te wywołania określają maksymalny
.setDefaults(Notification.DEFAULT_VIBRATE) priorytet i ustawiają wibracje,
dzięki czemu powiadomienie na
.build(); pewno przyciągnie uwagę.

To tylko kilka wybranych właściwości powiadomień, które można ustawiać.


Oprócz nich można także określać takie cechy jak widoczność, zapewniającą
kontrolę nad tym, czy powiadomienie zostanie wyświetlone na ekranie blokady,
liczbę wyświetlaną obok powiadomienia w przypadku, gdy z tej samej aplikacji Niektóre właściwości 
zostanie przesłanych więcej powiadomień, czy też dźwięki, które zostaną powiadomień 
odtworzone podczas wyświetlania powiadomienia. Więcej informacji o tych Obejrzyj to!
wymagają 
wszystkich właściwościach powiadomień można znaleźć na stronie: stosowania API 
poziomu 16 lub wyższego.
https://developer.android.com/reference/android/app/Notification.Builder.html W razie konieczności obsługi starszych
urządzeń nie będziemy w stanie
Dobrym pomysłem jest także określanie aktywności, którą należy wyświetlić po używać wszystkich dostępnych
kliknięciu powiadomienia. Na przykład w naszym przypadku możemy zażądać, właściwości powiadomień.
by po kliknięciu powiadomienia Android wyświetlił aktywność MainActivity.
Na następnej stronie pokażemy, jak to zrobić.

558 Rozdział 13.


Usługi

Uruchamianie intencji przez powiadomienie


Możemy sprawić, że kliknięcie powiadomienia spowoduje uruchomienie aktywności,
używając tak zwanej intencji oczekującej. Jest to intencja, którą jedna aplikacja
może przekazać do innych aplikacji, które następnie, w późniejszym czasie,
będą mogły wysłać tę intencję zamiast aplikacji, która ją utworzyła.

Oto czynności, które należy wykonać, aby utworzyć intencję oczekującą:

1. Utworzenie zwyczajnej intencji


W pierwszej kolejności musimy utworzyć zwyczajną, jawną intencję skierowaną
do aktywności, którą chcemy uruchomić po kliknięciu powiadomienia.
W naszym przypadku chcemy uruchomić aktywność MainActivity:
To jest zwyczajna intencja, która
Intent intent = new Intent(this, MainActivity.class); uruchamia aktywność MainActivity.

Intencja

Do: MainActivity
DelayedMessageService

2. Przekazanie intencji do obiektu TaskStackBuilder


Następnie musimy skorzystać z obiektu TaskStackBuilder, aby upewnić się,
że po uruchomieniu aktywności przycisk Wstecz będzie działał prawidłowo.
Obiekt TaskStackBuilder zapewnia dostęp do historii aktywności używanej
przez przycisk Wstecz urządzenia. Musimy pobrać stos cofnięć odnoszący się
do aktywności i dodać do niego utworzoną wcześniej intencję:
Tworzymy obiekt TastStackBuilder.
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(intent);
że po
Te wiersze kodu zapewnią, ycisk
prz
uruchomieniu aktywności
widłowo.
Wstecz będzie działał pra
To jest intencja
uruchamiająca aktywność Już się
MainActivity. Czy możesz robi!
dodać ją do stosu cofnięć Intencja
tej aktywności?

Do: MainActivity

DelayedMessageService TaskStackBuilder

jesteś tutaj  559


Metoda getPendingIntent()

¨  Dziennik
3. Pobranie intencji oczekującej z obiektu TaskStackBuilder ¨  Tost
Następnie musimy pobrać intencję oczekującą z obiektu TaskStackBuilder, ¨  Powiadomienie
używając do tego metody getPendingIntent(). Metoda ta ma dwa parametry
typu int: kod żądania, którego można używać do identyfikacji intencji, i flagę,
określającą zachowanie intencji oczekującej.

Oto dostępne wartości, które może przyjmować flaga:

FLAG_CANCEL_CURRENT Jeśli pasująca intencja oczekująca już istnieje, to należy ją anulować


przed utworzeniem następnej.
FLAG_NO_CREATE Jeśli pasująca intencja oczekująca nie istnieje, to nie należy tworzyć
nowej, a operacja ma zwrócić wartość null.
FLAG_ONE_SHOT Intencja oczekująca może zostać użyta tylko jeden raz.
FLAG_UPDATE_CURRENT Jeśli pasująca intencja oczekująca już istnieje, to należy ją zachować,
a jej dane dodatkowe zastąpić zawartością nowej intencji.

W naszym przypadku użyjemy flagi FLAG_UPDATE_CURRENT, aby zmodyfikować


już istniejące intencje oczekujące. A oto kod, który to robi:

kującą.
To wywołanie tworzy intencję ocze
PendingIntent pendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

Intencja oczekująca

Do: MainActivity

DelayedMessageService TaskStackBuilder

4. Dodanie intencji do powiadomienia


Na koniec musimy dodać tak utworzoną intencję do powiadomienia,
wywołując w tym celu metodę setContentIntent():
Dodajemy intencję oczekującą do powiadomienia,
notification.setContentIntent(pendingIntent); dzięki czemu po jego kliknięciu zostanie
uruchomiona aktywność MainActivity.
Intencja oczekująca

Do: MainActivity

DelayedMessageService Notification

Skoro już przekazaliśmy do powiadomienia intencję oczekującą, która określa, co należy zrobić,
gdy powiadomienie zostanie kliknięte, pozostaje nam jedynie zająć się jego wyświetleniem.

560 Rozdział 13.


Usługi

Wysyłanie powiadomień
za pomocą usługi systemowej
Pokazaliśmy już, jak utworzyć i skonfigurować powiadomienie. Kolejnym
krokiem jest przekazanie go do systemowej usługi powiadomień, dzięki
czemu zostanie ono wyświetlone.

Dostęp do systemowej usługi powiadomień zapewnia metoda


getSystemService(). Wymaga ona przekazania jednego argumentu
— nazwy usługi, której chcemy użyć.

W naszym przypadku chcemy skorzystać z usługi powiadomień,


zatem musimy użyć następującego fragmentu kodu: iadomienia.
To jest identyfikator naszego pow

public static final int NOTIFICATION_ID = 5453;


W taki sposób uzyskujemy dostęp
... do systemowej usługi powiadomień.
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
powiadomień
Tu używamy systemowej usługi mienia.
do wyświetlenia nasz ego pow iado
Stałej NOTIFICATION_ID używamy do identyfikacji powiadomienia.
Jeśli wyślemy inne powiadomienie o tym samym identyfikatorze, to
zastąpi ono bieżące powiadomienie. To przydatne rozwiązanie, które
pozwala zaktualizować istniejące powiadomienie i zapisać w nim nowe
informacje.

Usługa powiadomień zajmuje się wszystkimi zagadnieniami związanymi


z wyświetlaniem na ekranie powiadomień przesyłanych przez usługi
działające w tle. Oznacza to, że nie musimy już używać obiektów klasy
Handler, by uzyskać możliwość aktualizacji interfejsu użytkownika —
zajmie się tym usługa powiadomień.

Na następnej stronie przedstawimy zaktualizowany kod usługi


DelayedMessageService.

Nie istnieją
głupie pytania

P: Dlaczego mam dodawać ikonę do  P: Co się stanie, jeśli nie określę priorytetu i nie 
powiadomienia? włączę wibracji?

O: System powiadomień potrzebuje ikony, by wyświetlić O: Takie powiadomienie także zostanie wysłane, lecz
powiadomienie na samej górze ekranu. nie pojawi się na ekranie. Oczywiście wciąż będzie można
zobaczyć je w szufladzie powiadomień.

jesteś tutaj  561


Kod usługi DelayedMessageService

Kompletny kod usługi DelayedMessageService ¨  Dziennik


¨  Tost
¨  Powiadomienie
Poniżej zamieściliśmy kompletny kod usługi zapisany w pliku DelayedMessageService.java.
Teraz zamiast tostu usługa generuje powiadomienie:

package com.hfad.wic;

Wic
import android.app.IntentService;
import android.app.Notification; app/src/main
import android.app.NotificationManager;
import android.app.PendingIntent; java
.
Używamy tych dodatkowych klas
import android.app.TaskStackBuilder;
import android.content.Context; com.hfad.wic
import android.content.Intent;
import android.os.Handler; Nie wyświetlamy już komunikatów DelayedMessage
używając klasy Toast, więc te inst, Service.java
import android.widget.Toast; import nie będą nam już potrzebn rukcje
e.

public class DelayedMessageService extends IntentService {

public static final String EXTRA_MESSAGE = ”message”;


private Handler handler; Nie potrzebujemy już obiektu Handler.
public static final int NOTIFICATION_ID = 5453;

public DelayedMessageService() { Ta wartość jest używana do


identyfikacji powiadomienia.
super(”DelayedMessageService”); Może to być dowolna liczba,
.
} my zdecydowaliśmy się na 5453
Tej metody nie musimy zmieniać
.
@Override
protected void onHandleIntent(Intent intent) {
synchronized (this) {
try {
wait(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String text = intent.getStringExtra(EXTRA_MESSAGE);
showText(text);
}

562 Rozdział 13.


Usługi

Kod usługi DelayedMessageService (ciąg dalszy)


@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handler = new Handler();
Nie używamy już obiektu
return super.onStartCommand(intent, flags, startId); Handler, więc ta metoda
nie jest nam potrzebna.
}

private void showText(final String text) {


handler.post(new Runnable() { Nie wyświetlamy już komunikatu
@Override za pomocą klasy Toast.

public void run() {


Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();
}
Tworzymy intencję.
});
Intent intent = new Intent(this, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class); Używamy obiektu TastStackBuilder
by zapewnić prawidłowe działanie ,
stackBuilder.addNextIntent(intent); przycisku Wstecz, i tworzymy
PendingIntent pendingIntent = intencję oczekującą.

stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.app_name))
.setAutoCancel(true)
Konstruujemy powiadomienie.
.setPriority(Notification.PRIORITY_MAX)
.setDefaults(Notification.DEFAULT_VIBRATE)
.setContentIntent(pendingIntent)
.setContentText(text)
.build();
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
}
Wyświetlamy powiadomienie, używając usługi systemowej.
}

I to już cały kod naszej usługi uruchomionej. Sprawdźmy, co się


stanie, kiedy uruchomimy aplikację.

jesteś tutaj  563


Co się dzieje?

Co się stanie po uruchomieniu aplikacji?


Zanim sprawdzimy, jak działa aplikacja, spróbujmy przeanalizować krok po kroku,
co się stanie po jej uruchomieniu:

1 Aktywność MainActivity uruchamia usługę DelayedMessageService,


wysyłając do niej intencję.
Intencja zawiera komunikat, który ma wyświetlić usługa DelayedMessageService.

Intencja

”Już czas!”

MainActivity DelayedMessageService

2 Usługa DelayedMessageService czeka 10 sekund.

1...2...3...4...

DelayedMessageService

3 Usługa DelayedMessageService tworzy intencję skierowaną do aktywności


MainActivity.
Intencja

Do: MainActivity
DelayedMessageService

4 Usługa DelayedMessageService tworzy obiekt TaskStackBuilder i prosi go


o dodanie intencji do stosu cofnięć aktywności MainActivity.

Intencja

Do: MainActivity

DelayedMessageService TaskStackBuilder

564 Rozdział 13.


Usługi

Ciąg dalszy historii


5 Obiekt TaskStackBuilder używa intencji do utworzenia intencji oczekującej
i przekazuje ją usłudze DelayedMessageService.

Intencja oczekująca

Do: MainActivity

DelayedMessageService TaskStackBuilder

6 Usługa DelayedMessageService tworzy obiekt Notification, określa szczegółowe


informacje dotyczące jego konfiguracji i przekazuje do niego intencję oczekującą.

Intencja oczekująca

tekst=”Już czas!”
Do: MainActivity

DelayedMessageService Notification

7 Usługa DelayedMessageService tworzy obiekt NotificationManager, by uzyskać dostęp


do systemowej usługi powiadomień, i przekazuje do niego obiekt Notification.
Usługa powiadomień wyświetla powiadomienie na ekranie urządzenia.
Intencja oczekująca

Do: MainActivity
tekst=”Już czas!”

DelayedMessageService NotificationManager Notification

8 Kiedy użytkownik kliknie powiadomienie, intencja oczekująca przekazana


w obiekcie Notification uruchamia aktywność MainActivity.

Intencja

Notification MainActivity

A teraz zabierzmy naszą nową wersję usługi na jazdę próbną.

jesteś tutaj  565


Jazda próbna

¨  Dziennik
Jazda próbna aplikacji ¨  Tost
¨  Powiadomienie
Kiedy klikniesz przycisk wyświetlony w aktywności MainActivity, po 10 sekundach
zostanie wyświetlone powiadomienie. Pojawi się ono na ekranie niezależnie od tego,
jaka aplikacja będzie w danej chwili widoczna.
Po opóźnieniu na ekranie zostanie się
e
wyświetlone powiadomienie. Moż iach,
zdarzyć, że na starszych urządzen
iał
aby je wyświetlić, będziesz mus
otworzyć szufladę powiadomień.

Kliknij przycisk.

Kiedy klikniesz powiadomienie, Android wróci do aktywności


MainActivity.

Kliknięcie powiadomienia spowoduj


uruchomienie aktywności MainActi e
— dokładnie tak, jak chcieliśmy. vity

A zatem wiesz już, jak utworzyć usługę uruchomioną, która wyświetla


powiadomienia, używając usługi systemowej. Teraz zaplanowaliśmy
dla Ciebie małe ćwiczenie, a po nim zajmiemy się usługami
powiązanymi.

566 Rozdział 13.


Usługi

Magnesiki usługowe
Poniżej przedstawiliśmy większość kodu niezbędnego do utworzenia
usługi uruchomionej o nazwie WombleService, która odtwarza w tle
pliki .mp3, i aktywności, która tej usługi używa. Przekonajmy się,
czy uda Ci się uzupełnić ich kod. To jest usługa.

public class WombleService extends . ................ {


public WombleService() {
super(”WombleService”);
}

@Override
protected void .................. (Intent intent) {
MediaPlayer mediaPlayer =
MediaPlayer.create(getApplicationContext(), R.raw.wombling_song);
mediaPlayer.start();
Ten fragment kodu używa dostępnej w systemie Android klasy
} MediaPlayer, aby odtworzyć plik wombling_song.mp3. Plik jest
} umieszczony w katalogu res/raw.

To jest aktywność.
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void onClick(View view) {


Intent intent = new Intent(this, . ..................... );
......................... (intent);
}
} onHandleIntent

startActivity

WombleService.class IntentService WombleService


startService

jesteś tutaj  567


Rozwiązanie magnesików

Magnesiki usługowe. Rozwiązanie


Poniżej przedstawiliśmy większość kodu niezbędnego do utworzenia
usługi uruchomionej o nazwie WombleService, która odtwarza w tle
pliki .mp3, i aktywności, która tej usługi używa. Przekonajmy się,
czy uda Ci się uzupełnić ich kod.
To jest usługa. Rozszerza ona
public class WombleService extends IntentService { klasę IntentService.

public WombleService() {
super(”WombleService”);
} Ten kod musi być wykonywany
w metodzie onHandleIntent.

@Override
onHandleIntent
protected void (Intent intent) {
MediaPlayer mediaPlayer =
MediaPlayer.create(getApplicationContext(), R.raw.wombling_song);
mediaPlayer.start();
}
}

To jest aktywność.
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
} Musisz utworzyć intencję jawną
skierowaną do WombleService.class.

public void onClick(View view) {


WombleService.cl
Intent intent = new Intent(this, . .................. ass .. );
.. startService .... (intent);
} Tych magnesików
nie musiałeś
} To wywołanie uruchamia usługę. używać.

startActivity
WombleService

568 Rozdział 13.


Usługi

Usługi powiązane są bardziej interaktywne


Jak już napisaliśmy, usługi uruchomione działają w tle przez dowolnie długi czas,
i to nawet jeśli aktywność, która je uruchomiła, zostanie zakończona. Usługi tego
typu zostają zakończone dopiero po wykonaniu realizowanej przez nie operacji.

Natomiast usługi powiązane są skojarzone z jakimś innym komponentem, takim


jak aktywność. Aktywność może prowadzić interakcję z takimi usługami, wysyłać
do nich żądania i pobierać wyniki. Aby zobaczyć, jak działają usługi powiązane,
utworzymy teraz nową aplikację, która będzie korzystała z usługi powiązanej,
działającą jak drogomierz, czyli mierzącą przejechany dystans.

Jak będzie działała aplikacja drogomierza?


Planujemy utworzyć nowy projekt, składający się z aktywności MainActivity
i usługi OdometerService. Aktywność będzie korzystała z usługi do wyświetlania
pokonanego dystansu.

1 Aktywność MainActivity zostaje powiązana z usługą OdometerService.


Aktywność MainActivity używa metody getDistance() usługi OdometerService,
by pobrać przejechany dystans wyrażony w kilometrach.

2 Usługa OdometerService korzysta z systemowej usługi lokalizacyjnej do określania


zmian położenia urządzenia.
Usługa używa danych o lokalizacji urządzenia, by obliczyć pokonany przez nie dystans.
To jest usługa
3 Usługa OdometerService zwraca do aktywności MainActivity przejechany dystans. systemowa.
Nasza usługa
Aktywność MainActivity wyświetla pokonany dystans na ekranie urządzenia. OdometerService
będzie odbierać od niej
informacje o zmianach
lokalizacji urządzenia.
<Layout>

</Layout>
2
activity_main.xml
1 getDistance() Systemowa
usługa
lokalizacyjna

3
1.11
MainActivity.java OdometerService.java

Liczba przebytych kilometrów.

Zacznijmy od utworzenia usługi. Najpierw zobaczmy,


co musimy w tym celu zrobić.

jesteś tutaj  569


Etapy

Etapy tworzenia usługi Odometer ¨  Obiekt Binder


¨  Położenie
¨  getDistance()
Implementacja usługi OdometerService będzie się składała
z kilku etapów. Oto one:

1 Zdefiniowanie obiektu OdometerBinder.


Obiekt typu Binder zapewnia możliwość powiązania aktywności z usługą.
W naszym projekcie zdefiniujemy klasę o nazwie OdometerBinder
dziedziczącą po Binder, która pozwoli nam powiązać aktywność z usługą
OdometerService.

OdometerBinder

2 Zaimplementowanie interfejsu LocationListener i zarejestrowanie


obiektu tego typu w systemowej usłudze lokalizacyjnej.
Dzięki temu nasza usługa OdometerService będzie mogła uzyskiwać
informacje o zmianach lokalizacji urządzenia i na ich podstawie określać
w metrach pokonany dystans.

LocationListener Systemowa
usługa
lokalizacyjna

3 Zaimplementowanie publicznej metody getDistance().


Aktywność będzie mogła używać tej metody do pobierania pokonanego
dystansu.

getDistance()

0.5
MainActivity OdometerService

Zaczniemy od utworzenia nowego projektu aplikacji Drogomierz.

570 Rozdział 13.


Usługi

Utworzenie projektu aplikacji Drogomierz


Utwórz nowy projekt aplikacji na Androida, a jej kod umieść w pakiecie android.content.Context
com.hfad.drogomierz. Jako minimalną wersję SDK wybierz API
...
poziomu 16, tak by aplikacja działała na większości urządzeń. Będziesz
potrzebować pustej aktywności o nazwie MainActivity, korzystającej
z układu o nazwie activity_main — dzięki temu struktura Twojej aplikacji
będzie taka sama jak aplikacji przedstawionej w książce.
android.content.ContextWrapper
Oprócz tego do projektu musisz dodać nową usługę. Tym razem będzie to ...
usługa dziedzicząca po klasie Service, a nie po IntentService. Wynika
to z faktu, że klasa IntentService jest przeznaczona do tworzenia
usług obsługujących intencje, takich jak ta, którą zaimplementowaliśmy
w poprzednim przykładzie. Teraz jednak będziemy uruchamiać usługę android.app.Service
poprzez powiązanie z nią aktywności, dlatego zastosowanie klasy ...
IntentService nie da nam żadnych korzyści.

Usługę dziedziczącą po klasie Service można dodać do projektu To jest hierarchia klasy Service.
w podobny sposób, jak zrobiliśmy to wcześniej. A zatem wybierz z menu
opcję File/New/Service/Service (a nie Service (IntentService)), a następnie
nadaj usłudze nazwę OdometerService. Usuń zaznaczenie z pola wyboru
Exported, gdyż ma ono być zaznaczone wyłącznie w przypadkach, kiedy
chcemy, by usługa była dostępna także poza bieżącą aplikacją. Upewnij się
także, że zaznaczone jest pole wyboru Enabled, ponieważ w przeciwnym
razie aktywność nie będzie w stanie uruchomić usługi.

Oto, jak wygląda kod usługi rozszerzającej klasę Service:

package com.hfad.drogomierz;
Drogomierz

import android.app.Service; app/src/main


import android.content.Intent;
import android.os.IBinder; Klasa naszej usługi dziedziczy java
po klasie Service.
com.hfad.drogomierz
public class OdometerService extends Service {
Metoda onBind() służy do wiązania
komponentów z daną usługą. Odometer
@Override Service.java
public IBinder onBind(Intent intent) {
// Kod obsługujący powiązanie usługi
}
}

Metoda onBind() służy od powiązania usługi z aktywnością.


Jej kodem zajmiemy się na następnej stronie.

jesteś tutaj  571


Wiązanie

Jak działa wiązanie? ¨  Obiekt Binder


¨  Położenie
¨  getDistance()
Oto, jak powiązać aktywność z usługą powiązaną:

1 Aktywność tworzy obiekt ServiceConnection.


Obiekt ServiceConnection służy do utworzenia połączenia z usługą.

MainActivity ServiceConnection

2 Aktywność przekazuje intencję do obiektu połączenia z usługą — ServiceConnection.


Intencja zawiera wszelkie dodatkowe informacje, które aktywność chce przekazać do usługi.

Intencja Intencja

MainActivity ServiceConnection OdometerService

3 Usługa powiązana tworzy obiekt Binder.


Obiekt Binder zawiera referencję do usługi powiązanej. Usługa przesyła ten obiekt
z powrotem, używając utworzonego wcześniej połączenia.

Binder Binder

MainActivity ServiceConnection OdometerService

4 Kiedy aktywność odbierze obiekt Binder, pobiera z niego obiekt


reprezentujący usługę i zaczyna korzystać bezpośrednio z niego.

MainActivity OdometerService

Aby zapewnić możliwość powiązania aktywności z usługą, usługa


ta musi utworzyć obiekt Binder i przekazać go do aktywności,
używając metody onBind().

572 Rozdział 13.


Usługi

Zdefiniowanie obiektu Binder


Kiedy aktywność prosi o powiązanie z usługą, używając obiektu połączenia
z usługą, obiekt ten wywołuje metodę onBind() usługi. Metoda onBind() zwraca
do połączenia obiekt Binder, który jest następnie przekazywany do aktywności.

Tworząc usługę powiązaną, obiekt Binder musimy zdefiniować samodzielnie.


W naszym przykładzie utworzymy obiekt tego typu, implementując w usłudze
klasę wewnętrzną o nazwie OdometerBinder. Oto jej kod: imy
Tworząc usługi powiązane, mus
arcz yć impl eme ntac ję klas y Binder.
dost
public class OdometerBinder extends Binder {
OdometerService getOdometer() { Aktywność użyje tej metody, by
pobr
return OdometerService.this; referencję do usługi OdometerServ ać
ice.
}
}

Teraz możemy już zwrócić instancję klasy OdometerBinder w metodzie


onBind() tworzonej usługi:

...
import android.os.Binder; Używamy tych dwóch klas.
Drogomierz
import android.os.IBinder;

app/src/main
public class OdometerService extends Service {
private final IBinder binder = new OdometerBinder();
java

public class OdometerBinder extends Binder {


com.hfad.drogomierz
OdometerService getOdometer() {
return OdometerService.this; Implementacja klasy Binder.
} Odometer
Service.java
}

...

@Override
public IBinder onBind(Intent intent) {
return binder;
} Metoda onBind() zwraca impleme
To właśnie ten interfejs ntację interfejsu IBinder.
} implementuje klasa Binder.

Kieda aktywność jest wiązana z usługą za pomocą obiektu połączenia —


ServiceConnection — obiekt ten wywołuje metodę onBind() usługi,
Przekonasz się, jak to działa, kiedy
która z kolei zwraca obiekt OdometerBinder. Gdy aktywność odbierze napiszemy aktywność korzystającą
z obiektu połączenia obiekt OdometerBinder, wywoła jego metodę z usługi.
getOdometer(), by pobrać obiekt usługi OdometerService.

jesteś tutaj  573


Aby coś zrobić

Wykonanie operacji w usłudze ¨  Obiekt Binder


¨  Położenie
¨  getDistance()
Kolejną rzeczą, którą musimy się zająć, będzie zadbanie o to, by nasza usługa coś robiła.
W naszym przypadku chcemy, by przekazywała ona aplikacji informacje o tym, jak długi
dystans pokonało urządzenie. W tym celu musimy wykonać dwie czynności:

1 W momencie tworzenia usługi przygotować obiekt Hej, stary! Znowu


nasłuchujący, który będzie odbierać informacje o zmianach nas przenieśli.
lokalizacji urządzenia.

Systemowa
usługa
lokalizacyjna

OdometerService LocationListener

2 Zwracać liczbę przebytych kilometrów, kiedy aktywność


o to poprosi.

Daleko
jeszcze? Przejechaliśmy
1 kilometr.
getDistance()

1
MainActivity OdometerService

Zacznijmy od sprawdzenia, jakie metody definiowanie


przez klasę Service mogą się nam przydać.

574 Rozdział 13.


Usługi

Klasa Service ma cztery kluczowe metody


Tworzymy usługę powiązaną, rozszerzając w tym celu klasę Service.
Klasa ta definiuje cztery metody, które mogą się nam przydać. Oto one:

Metoda Kiedy jest wywoływana? Do czego służy?

onCreate() Podczas pierwszego tworzenia usługi. Do jednokrotnych czynności


konfiguracyjnych, takich jak
utworzenie instancji usługi.

onStartCommand() Kiedy aktywność tworzy usługę, wywołując Jeśli nasza usługa nie jest usługą
metodę startService(). uruchomioną, to tej metody nie
musimy implementować. Jest ona
wykonywana tylko w przypadku
uruchamiania usługi za pomocą
metody startService().

onBind() Kiedy aktywność prosi o powiązanie Implementując tę metodę, zawsze


z usługą. musimy zwracać w niej wynik typu
IBinder. Jeśli nie chcemy,
by aktywność została powiązana
z usługą, wystarczy zwrócić
wartość null.

onDestroy() Kiedy usługa nie jest już używana Można jej używać do zwalniania
i ma zostać usunięta. zasobów.

W naszym przypadku chcemy, aby usługa, zaczynając od momentu jej


utworzenia, pobierała informacje o zmianach lokalizacji. Ponieważ te
przygotowania wystarczy wykonać jeden raz, umieścimy je w metodzie
onCreate():
Tak wygląda metoda onCreate()
@Override klasy Service.
public void onCreate() {
// Kod tworzący obiekt nasłuchujący
}

Na następnej stronie pokażemy, jak pobierać informacje


o zmianach położenia.
jesteś tutaj  575
Gdzie jesteśmy?

Położenie, położenie, położenie… ¨  Obiekt Binder


¨  Położenie
¨  getDistance()
Chcąc określić położenie urządzenia, musimy skorzystać z usługi lokalizacyjnej
Androida. Usługa ta używa informacji odczytywanych z odbiornika GPS oraz
nazw i siły sygnału pobliskich sieci Wi-Fi i na ich podstawie określa położenie
urządzenia na kuli ziemskiej.

Przygotowania do pobierania informacji o położeniu należy zacząć od utworzenia


obiektu LocationListener. Obiekt ten zapewnia możliwość uzyskiwania
informacji o zmianach położenia urządzenia. Oto, jak utworzyć taki obiekt:
ner.
To jest nowy obiekt LocationListe

LocationListener listener = new LocationListener() {


@Override
public void onLocationChanged(Location location) {
// Kod zliczający przebyty dystans
} Ta metoda zostaje wywołana za każdym razem, gdy obiekt
LocationListener jest informowany o zmianie położenia urządze
nia.
Parametr Location określa aktualne położenie.
@Override
public void onProviderDisabled(String arg0) {}
ić, choć
Także te metody trzeba przesłon ane
mog ą być pust e. Są one wyw oływ
odbiornika
@Override podczas włączania i wyłączania u.
GPS albo w razie zmia ny jego stan
reagować
public void onProviderEnabled(String arg0) {} W naszym przypadku nie musimy
na żadne z tych zdarzeń.

@Override
public void onStatusChanged(String arg0, int arg1, Bundle bundle) {}
};

Aby uzyskiwać informacje o zmianach położenia, musimy przesłonić metodę


onLocationChanged() obiektu LocationListener. Metoda ta ma tylko jeden
parametr — obiekt Location reprezentujący bieżące położenie urządzenia.

Odległość pomiędzy dwoma położeniami, wyrażoną w metrach, można obliczyć,


używając metody distanceTo() klasy Location. Na przykład jeśli wcześniejsze
położenie urządzenia zostało zapisane w zmiennej lastLocation typu Location,
to wyrażoną w metrach odległość między poprzednim i bieżącym położeniem
można wyznaczyć, używając następującego wywołania:

double distanceInMeters = location.distanceTo(lastLocation);


Na następnej stronie przedstawimy kompletny kod implementacji obiektu
LocationListener.

576 Rozdział 13.


Usługi

Dodanie obiektu LocationListener do usługi


Oto kod klasy OdometerService (jej metoda onCreate() zawiera obiekt
nasłuchujący LocationListener, który śledzi dystans pokonany przez urządzenie):

...
public class OdometerService extends Service { Drogomierz

private static double distanceInMeters; app/src/main


private static Location lastLocation = null;
... java
W statycznych zmiennych prywatnych
przechowujemy przebyty dystans i ostatnie
położenie urządzenia. com.hfad.drogomierz
@Override
Tworzymy obiekt nasłuchujący.
public void onCreate() {
Odometer
LocationListener listener = new LocationListener() { Service.java
@Override
public void onLocationChanged(Location location) {
if (lastLocation == null) { Jeśli to pierwsze informacje o położeniu,
to zapisujemy je w zmiennej lastLocation.
lastLocation = location;
}
distanceInMeters += location.distanceTo(lastLocation);
lastLocation = location;
} Do pokonanego dystansu, zapisanego w zmiennej
distanceInMeters, dodajemy odległość między
bieżącym i ostatnim położeniem.
@Override
public void onProviderDisabled(String arg0) {}

@Override Musimy przesłonić te metody,


public void onProviderEnabled(String arg0) {} gdyż definiuje je interfejs
LocationListener.

@Override
public void onStatusChanged(String arg0, int arg1, Bundle bundle) {}
};
}
}

Skoro już utworzyliśmy obiekt nasłuchujący LocationListener,


musimy go zarejestrować w systemowej usłudze lokalizacyjnej.

jesteś tutaj  577


Rejestracja obiektu nasłuchującego

Rejestracja obiektu LocationListener ¨  Obiekt Binder


¨  Położenie
¨  getDistance()
Nasz obiekt nasłuchujący możemy zarejestrować w systemowej usłudze lokalizacyjnej,
używając obiektu LocationManager. Obiekt LocationManager zapewnia dostęp do Właśnie w taki sposób można
usługi lokalizacyjnej, a można go utworzyć w następujący sposób: uzyskać dostęp do usługi
lokalizacyjnej Androida.
LocationManager locManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);

Metoda getSystemSerivce() zwraca referencję do usługi systemowej. W tym


przypadku chodzi nam o usługę określającą położenie urządzenia, dlatego używamy Metody getSystemService()
kać
następującego wywołania: użyliśmy już wcześniej, by uzys
dostęp do usługi zarządzającej
powiadomieniami.
getSystemService(Context.LOCATION_SERVICE);

Kiedy już pobierzemy obiekt LocationManager, możemy wywołać jego metodę


requestLocationUpdates(), aby zarejestrować nasz obiekt nasłuchujący w usłudze
lokalizacyjnej, podając przy tym kryteria określające, jak często obiekt nasłuchujący ma
otrzymywać powiadomienia o zmianach położenia. Metoda requstLocationUpdates()
ma cztery parametry: dostawcę GPS, minimalny odstęp czasu między kolejnymi
powiadomieniami wyrażony w milisekundach, minimalny dystans między
powiadomieniami wyrażony w metrach oraz obiekt nasłuchujący LocationListener.

Poniżej pokazaliśmy, jak uzyskiwać powiadomienia co 1 sekundę, zakładając,


że urządzenie zmieniło położenie o więcej niż 1 metr: To jest dostawca GPS.

locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
1000, To jest czas w milisekundach.
To jest odległość w metrach. 1,
obiekt
listener); A to jest utworzony przez nas
nasłuchujący LocationManager.

Teraz możemy już użyć metody onCreate() naszej usługi, aby zarejestrować obiekt
nasłuchujący w systemowej usłudze lokalizacyjnej i zagwarantować, że będzie on
otrzymywać regularne powiadomienia o zmianach położenia. Oto kod, który to robi:

@Override Podczas tworzenia usługi chcemy przygotować obiekt nasłuchujący


public void onCreate() { i zarejestrować go w systemowej usłudze lokalizacyjnej.
LocationListener listener = new LocationListener() {...};
LocationManager locManager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, listener);
}

To już wszystko, czego potrzebujemy, by zarejestrować obiekt nasłuchujący


w systemowej usłudze lokalizacyjnej i rejestrować dystans pokonany przez
urządzenie. Teraz musimy zająć się przekazywaniem informacji o pokonanym
dystansie z usługi z powrotem do aktywności.

578 Rozdział 13.


Usługi

Informujemy aktywność o pokonanym dystansie


Jak pamiętasz, nasza usługa musi robić dwie rzeczy.

Pierwszą jest śledzenie dystansu przebytego przez urządzenie. Tym już zajęliśmy
się, tworząc obiekt nasłuchujący LocationListener i rejestrując go w systemowej
usłudze lokalizacyjnej.

Drugą rzeczą, którą musimy się zająć, jest zapewnienie usłudze możliwości
przekazywania informacji o przebytym dystansie do aktywności, tak aby aktywność
mogła ten dystans wyświetlić użytkownikowi. W tym celu będziemy musieli napisać
w naszej usłudze metodę getDistance(), która będzie przeliczać dystans przebyty
przez urządzenie na kilometry. Aktywność będzie wywoływać tę metodę za każdym
razem, kiedy będzie chciała się dowiedzieć, jaki dystans pokonało urządzenie.

getDistance()

1.11
MainActivity OdometerService

Liczba przebytych kilometrów.

A oto, jak wygląda kod metody getDistance():

public double getDistance() { Ta instrukcja konwertuje przebyty


dystans z metrów na kilometry. Równie
return this.distanceInMeters / 1000; dobrze moglibyśmy przeliczać ten dystans
} na mile, wiorsty lub furlongi.

Metoda pobiera aktualnie przebyty dystans i dzieli go przez 1000,


uzyskując w ten sposób dystans wyrażony w kilometrach.

To już wszystko, jeśli chodzi o kod usługi OdometerService.


Na następnej stronie przedstawimy jej kompletny kod.

jesteś tutaj  579


Kod usługi OdometerService

Kompletny kod usługi OdometerService ¨  Obiekt Binder


¨  Położenie
Oto kompletny kod pliku OdometerService.java:
¨  getDistance()

package com.hfad.drogomierz;

Drogomierz
import android.app.Service;
import android.content.Context; app/src/main
import android.content.Intent;
import android.location.Location; java

import android.location.LocationListener;
com.hfad.drogomierz
import android.location.LocationManager;
import android.os.Binder;
Odometer
import android.os.Bundle; To są wszystkie używane klasy. Service.java
import android.os.IBinder;

public class OdometerService extends Service {

private final IBinder binder = new OdometerBinder();


private static double distanceInMeters;
To są zmienne prywatne używane
private static Location lastLocation = null; w kodzie usługi.

public class OdometerBinder extends Binder {


OdometerService getOdometer() { imy
Tworząc usługę powiązaną, mus ia on
return OdometerService.this; zdefiniować obiekt Binder. Zapewnz usługą.
ci
możliwość powiązania aktywnoś
}
}

@Override
public IBinder onBind(Intent intent) {
return binder;
Ta metoda jest wywoływana, kiedy aktywność
} prosi o powiązanie z usługą.

580 Rozdział 13.


Usługi

Kod usługi OdometerService (ciąg dalszy)


Podczas tworzenia usługi
@Override przygotowujemy także obiekt
nasłuchujący LocationListener.
public void onCreate() { Drogomierz
LocationListener listener = new LocationListener() {
@Override app/src/main
public void onLocationChanged(Location location) {
if (lastLocation == null) { java

lastLocation = location;
com.hfad.drogomierz
}
distanceInMeters += location.distanceTo(lastLocation);
Odometer
lastLocation = location; Service.java
}

@Override
To jest nasza implementacja
public void onProviderDisabled(String arg0) {} obiektu nasłuchującego
LocationListener.

@Override
public void onProviderEnabled(String arg0) {}

@Override
public void onStatusChanged(String arg0, int arg1, Bundle bundle) {}
};
LocationManager locManager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,1000,1,listener);
} Konwertujemy przebyty
dystans na kilometry. Rejestrujemy obiekt
nasłuchujący w systemowej
public double getDistance() { usłudze lokalizacyjnej.

return this.distanceInMeters / 1000;


}
}

Ten kod pozwala powiązać aktywność z usługą, a dodatkowo pozwala aktywności


zapytać usługę o dystans, który przebyło urządzenie. Jest jeszcze jedna rzecz
związana z naszą usługą, którą musimy zrobić: przydzielić aplikacji uprawnienia
do korzystania z odbiornika GPS.

jesteś tutaj  581


Deklaracja usługi

Aktualizacja pliku AndroidManifest.xml ¨  Obiekt Binder


¨  Położenie
Podczas tworzenia aplikacji Android domyślnie pozwala na wykonywanie większości
¨  getDistance()
operacji. Istnieją jednak także pewne operacje, w których przypadku system wymaga
uzyskania od użytkownika jawnej zgody na ich wykonywanie. Jedną z takich operacji
jest pobieranie danych z odbiornika GPS. Jeśli aplikacja musi korzystać z odbiornika
GPS zainstalowanego w urządzeniu, to użytkownik musi wyrazić na to zgodę podczas
instalowania aplikacji.

W celu poinformowania systemu Android o tym, że aplikacja wymaga uzyskania


uprawnień do korzystania z odbiornika GPS, musimy dodać do pliku manifestu element
<uses-permission>: Dodajemy ten element,
gdyż w aplikacji używamy
<manifest ... > odbiornika GPS.
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />
...
</manifest>

Jeśli nie dodamy tego uprawnienia do pliku AndroidManifest.xml, to aplikacja


ulegnie awarii.

Musimy także sprawdzić, czy Android Studio dodało do pliku manifestu,


AndroidManifest.xml, naszą usługę:

<manifest ... >

<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />

<application
... >
<activity Drogomierz

...
</activity> Wszystkie usługi należy app/src/main
zadeklarować w pliku <xml>
AndroidManifest.xml. </xml>
<service AndroidManifest.xml
android:name=”.OdometerService”
android:exported=”false” Temu atrybutowi przypisujemy
wartość false, gdyż tylko nasza
gi.
android:enabled=”true” > aplikacja będzie używać tej usłu
</service> Atrybutowi android:enabled należy przypisać
</application> wartość true lub całkowicie go pominąć. Jeśli
przypiszemy mu wartość false, to aplikacja nie
</manifest> będzie mogła używać usługi.

Teraz mamy dla Ciebie ćwiczenie, a po nim zobaczymy,


na jakim etapie znajdują się prace nad aplikacją.

582 Rozdział 13.


Usługi

Magnesiki usługowe
Przekonajmy się, czy potrafisz uzupełnić poniższy kod, aby utworzyć
usługę powiązaną o nazwie NumberService, która w odpowiedzi na
wywołanie metody getNumber() zwraca liczbę losową.

...
public class NumberService extends Service {

private final IBinder binder = new NumberBinder();


private final Random random = new Random();

public class ................... extends Binder {

................. getNumberService() {
return NumberService.this;
}
}

@Override
public . ........................ (Intent intent) {

........................;
}

public int getNumber() {


return random.nextInt(100);
}
}
Ten fragment kodu generuje liczbę losową.

binder
IBinder NumberBinder
void
onBind
onHandleIntent
onCreate
return NumberService

jesteś tutaj  583


Rozwiązanie magnesików

Magnesiki usługowe. Rozwiązanie


Przekonajmy się, czy potrafisz uzupełnić poniższy kod, aby utworzyć
usługę powiązaną o nazwie NumberService, która w odpowiedzi na
wywołanie metody getNumber() zwraca liczbę losową.

...
public class NumberService extends Service {

private final IBinder binder = new NumberBinder();


private final Random random = new Random();

Zdefiniuj klasę NumberBinder


public class .. NumberBinder .. extends Binder { dziedziczącą po Binder.

NumberService getNumberService() { Aktywność musi pobrać z obiektu


Binder referencję do usługi
return NumberService.this; NumberService, a zatem ta meto
da
musi zwracać obiekt NumberServic
} e.

} Przesłoń metodę onBind(), aby można


było powiązać aktywność z usługą.

@Override
public IBinder .
onBind
. (Intent intent) {
return . binder ...;
}
Metoda onBind() ma zwracać obie
kt Binder.

public int getNumber() {


return random.nextInt(100);
}
}

Te magnesiki okazały się niepotrzebne.

void

onHandleIntent
onCreate

584 Rozdział 13.


Usługi

Dokąd dotarliśmy?
Zastanówmy się jeszcze raz, jakimi możliwościami ma dysponować nasza aplikacja,
żebyśmy mogli określić, co jeszcze zostało do zrobienia:

1 Aktywność MainActivity zostaje powiązana z usługą OdometerService.


Aktywność MainActivity używa metody getDistance() usługi, by pobrać dystans
przebyty przez urządzenie.

2 Usługa OdometerService korzysta z systemowej usługi lokalizacyjnej,


by śledzić zmiany położenia urządzenia.
Na podstawie uzyskiwanych informacji o bieżącej lokalizacji usługa OdometerService
wylicza dystans przebyty przez urządzenie.

3 Usługa OdometerService zwraca do aktywności MainActivity dystans przebyty


przez urządzenie.
Aktywność MainActivity wyświetla przebyty dystans na ekranie, tak aby był on
widoczny dla użytkownika.

<Layout>

</Layout>
2
activity_main.xml
1 Systemowa
getDistance()
usługa
lokalizacyjna
Teraz zajmiemy się
aktualizacją tej 3 Usługa już jest gotowa.
aktywności
1.11
MainActivity.java OdometerService.java

Dotychczas udało się nam zaimplementować usługę OdometerService.


Używa ona systemowej usługi lokalizacyjnej do śledzenia położenia
urządzenia i na tej podstawie oblicza dystans, który ono pokonało.

Kolejną rzeczą, którą musimy się zająć, będzie zaimplementowanie


aktywności MainActivity. Musimy ją powiązać z usługą
OdomenterService, a następnie użyć metody getDistance()
tej usługi do wyświetlenia dystansu przebytego przez urządzenie.

jesteś tutaj  585


Wyświetlanie przebytego dystansu

Aktualizacja układu aktywności MainActivity ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
Nasza aktywność MainActivity musi wyświetlać dystans przebyty przez urządzenie.
To są zmiany, któ
Zaczniemy zatem od wprowadzenia zmian w jego układzie — activity_main.xml. Dodamy wprowadzić w ak re musimy
do niego widok tekstowy, którego następnie użyjemy do wyświetlania układu. Ten tekst MainActivity. tywności
będziemy potem cyklicznie — co sekundę — aktualizować w kodzie aktywności.

Oto kod pliku activity_main.xml:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
Drogomierz
tools:context=”.MainActivity”>

app/src/main
<TextView android:text=””
android:id=”@+id/distance”
res
android:textAppearance=”?android:attr/textAppearanceLarge”
android:layout_width=”match_parent” layout
android:layout_height=”match_parent” <xml>
</xml>
android:layout_centerHorizontal=”true” activity_
android:singleLine=”false” main.xml
android:textSize=”50dp”/>
</RelativeLayout>
Do wyświetlania pokonanego
dystansu użyjemy widoku
tekstowego.

Teraz musimy zająć się kodem aktywności, a konkretnie powiązać


aktywność z usługą i aktualizować dystans prezentowany w widoku
tekstowym. Zawartość widoków potrafimy już aktualizować, natomiast
nie wiemy, jak powiązać aktywność z usługą. Zobaczmy, jak to zrobić.

586 Rozdział 13.


Usługi

Tworzenie obiektu ServiceConnection


We wcześniejszej części rozdziału napisaliśmy, że aktywność zostaje
powiązana z usługą przy użyciu obiektu ServiceConnection. Obiekt
ten jest interfejsem deklarującym dwie metody: onServiceConnect()
i onServiceDisconnect().
Main Service Odometer
Metoda onServiceConnect() jest wywoływana, kiedy zostanie nawiązane Activity Connection Service
połączenie z usługą, a usługa zwróciła obiekt Binder. Tego obiektu
możemy użyć do pobrania referencji do usługi.

Z kolei metoda onServiceDisconnect() jest wywoływana, gdy połączenie


z usługą zostanie zerwane.

Chcąc powiązać aktywność z usługą, trzeba stworzyć własną implementację


interfejsu ServiceConnection. Poniżej przedstawiliśmy naszą:
Drogomierz
...
public class MainActivity extends Activity { app/src/main
Tej zmiennej użyjemy do
przechowywania referencji
private OdometerService odometer; do usługi OdometerService. java
private boolean bound = false; Tej zmiennej użyjemy do przechow
... informacji o tym, czy aktywność ywania com.hfad.drogomierz
powiązana z usługą, czy nie. została

private ServiceConnection connection = new ServiceConnection() { Main


@Override Activity.java

public void onServiceConnected(ComponentName componentName, IBinder binder) {


OdometerService.OdometerBinder odometerBinder =
(OdometerService.OdometerBinder) binder;
odometer = odometerBinder.getOdometer();
Rzutujemy przekazany obiekt Binder
bound = true; na typ OdometerBinder, dzięki czemu
Kiedy usługa zostanie będziemy mogli pobrać referencję
} powiązana, przypisujemy do usługi OdometerService.
@Override zmiennej bound wartość true.
public void onServiceDisconnected(ComponentName componentName) {
bound = false;
}
Kiedy powiązanie zostanie
}; zakończone, przypisujemy
} zmiennej bound wartość false.

Gdy uda się utworzyć połączenie z usługą, metoda onServiceConnected()


używa obiektu Binder, by pobrać referencję do niej. Oprócz tego metod
onServiceConnected() i onServiceDisconnected() używamy do zapisywania
informacji o tym, czy aktywność jest w danej chwili powiązana z usługą, czy nie.

jesteś tutaj  587


Tworzenie i przerywanie powiązania

Powiązanie z usługą należy utworzyć ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
podczas uruchamiania aktywności
Planujemy użyć obiektu połączenia, by powiązać aktywność z usługą
w momencie uruchamiania aktywności. W ramach przypomnienia: kiedy
aktywność staje się widoczna, wywoływana jest jej metoda onStart().

Aby powiązać aktywność z usługą, w pierwszej kolejności musimy


utworzyć intencję jawną, skierowaną do wybranej usługi, a następnie MainActivity OdometerService
wystarczy wywołać metodę bindService() aktywności:

@Override
protected void onStart() { To jest intencja skierowania
do usługi OdometerService.
super.onStart();
Intent intent = new Intent(this, OdometerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
} W tym wywołaniu używamy inte
ncji i obiektu
połączen ia, by powiązać aktywność z usłu
gą.
Stała Context.BIND_AUTO_CREATE informuje Androida, że jeśli usługa nie istnieje,
to powinien ją utworzyć.

Przerwanie powiązania w momencie zatrzymywania aktywności


Kiedy aktywność przestaje być widoczna, powinniśmy przerwać powiązanie
aktywności z usługą. W ramach przypomnienia: kiedy aktywność przestaje
być widoczna, wywoływana jest jej metoda onStop().

Aby przerwać powiązanie z usługą, należy wywołać metodę unbindService()


aktywności. Metoda ta ma jeden parametr — obiekt połączenia. W naszej
aplikacji, kiedy aktywność przestanie być widoczna, sprawdzimy, czy została
MainActivity OdometerService
ona powiązana z usługą, a jeśli tak, to przerwiemy to powiązanie:
@Override
protected void onStop() { y Drogomierz
W tym wywołaniu używam
erwać
super.onStop(); obiektu połączenia, by prz
pow iąza nie z usługą. app/src/main
if (bound) {
unbindService(connection);
bound = false; java
}
} com.hfad.drogomierz

Dysponujemy już aktywnością, która w momencie uruchamiania


zostaje powiązana z usługą. Powiązanie to przerywamy w momencie Main
zatrzymywania aktywności. Ostatnią rzeczą, którą musimy się zająć, Activity.java
jest zadbanie o to, by aktywność pytała usługę o dystans przebyty przez
urządzenie.

588 Rozdział 13.


Usługi

Wyświetlanie przebytego dystansu ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
Kiedy już utworzymy powiązanie z usługą, będziemy mogli wywoływać jej
metody. Planujemy co sekundę wywoływać metodę getDistance() usługi
OdometerService, aby pobierać przebyty dystans i wyświetlać tę informację
w widoku tekstowym.

W tym celu napiszemy nową metodę o nazwie watchMileage(). Będzie ona Aktywność użyje metody getDistance()
działać w dokładnie taki sam sposób jak metoda runTimer() z rozdziału 4. usługi OdometerService, by określić
zawartość komponentu TextView.
Jedyna różnica między nimi będzie polegała na tym, że w tym projekcie
metoda będzie wyświetlać przebyty dystans, a nie zmierzony czas.

Oto kod metody watchMileage():

Pobieramy widok tekstowy.


private void watchMileage() {
final TextView distanceView = (TextView)findViewById(R.id.distance);
final Handler handler = new Handler(); Tworzymy obiekt Handler.
handler.post(new Runnable() { Wywołujemy metodę post(), przekazując do niej obiekt Runnabl
e.
@Override
public void run() {
Jeśli dysponujemy referencją do usługi
double distance = 0.0; OdometerService, to wywołujemy jej
if (odometer != null) { metodę getDistance().

distance = odometer.getDistance(); Formatujemy wyświetlany tekst.


}
String distanceStr = String.format(”%1$,.2f kilometra”, distance);
distanceView.setText(distanceStr);
handler.postDelayed(this, 1000);
Drogomierz
}
Przekazujemy kod umieszczony w obiekcie Runnable, tak by został .
}); on ponownie wykonany po upływie 1000 milisekund, czy 1 sekundy app/src/main
} Ponieważ to wywołanie jest umieszczone w metodzie run() obiektu
.
Runnable, w efekcie metoda ta będzie wykonywana co sekundę
java

Metodę watchMileage() wywołamy następnie w metodzie onCreate() aktywności,


com.hfad.drogomierz
dzięki czemu umieszczony w niej kod zacznie być wykonywany w momencie
utworzenia aktywności:
Main
@Override Activity.java
protected void onCreate(Bundle savedInstanceState) {
...
watchMileage();
}
Na następnej stronie przedstawimy kompletny kod aktywności MainActivity.

jesteś tutaj  589


Kod aktywności MainActivity

Kompletny kod aktywności MainActivity ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
Oto kompletny kod aktywności MainActivity:

package com.hfad.drogomierz;

import android.app.Activity;
Drogomierz
import android.content.ComponentName;
import android.content.Context;
app/src/main
import android.content.Intent;
import android.content.ServiceConnection;
java
import android.os.Bundle;
import android.os.Handler; com.hfad.drogomierz
import android.os.IBinder;
import android.widget.TextView;
Main
Activity.java
public class MainActivity extends Activity {
nia
Ta zmienna służy do przechowywa ice.
private OdometerService odometer; referencji do usługi OdometerServ
private boolean bound = false; Ta zmienna przechowuje informację, czy
aktywność została powiązana z usługą, czy nie.

private ServiceConnection connection = new ServiceConnection() { Musimy zdefiniować


@Override obiekt ServiceConnection.
public void onServiceConnected(ComponentName componentName, IBinder binder) {
OdometerService.OdometerBinder odometerBinder =
(OdometerService.OdometerBinder) binder;
odometer = odometerBinder.getOdometer();
Po powiązaniu usługi
bound = true; OdometerService pobieramy
referencję do niej.
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
bound = false;
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
watchMileage(); Podczas tworzenia aktywności wywołujemy metodę watchMilage().
}

590 Rozdział 13.


Usługi

Kod aktywności MainActivity (ciąg dalszy)


@Override
protected void onStart() { Kiedy aktywność zostaje uruchomiona,
próbujemy ją powiązać z usługą.
super.onStart();
Intent intent = new Intent(this, OdometerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
Powiązanie przerywamy w momencie
zatrzymywania aktywności.
@Override Drogomierz

protected void onStop() {


super.onStop(); app/src/main

if (bound) {
java
unbindService(connection);
bound = false; com.hfad.drogomierz
}
} Ta metoda aktualizuje wyświetlany dystans, Main
jaki pokonało urządzenie. Activity.java

private void watchMileage() {


final TextView distanceView = (TextView)findViewById(R.id.distance);
final Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
double distance = 0.0; Jeśli dysponujemy referencją do usługi
OdometerService, to wywołujemy jej
if (odometer != null) { metodę getDistance().
distance = odometer.getDistance();
}
String distanceStr = String.format(”%1$,.2f kilometra”, distance);
distanceView.setText(distanceStr);
handler.postDelayed(this, 1000);
}
Informacje o pokonanym dystansie
}); aktualizujemy co sekundę.
}
}

To już cały kod niezbędny do wykorzystania usługi OdometerService


w aktywności MainActivity. A teraz przekonajmy się, co się stanie
po uruchomieniu aplikacji.
jesteś tutaj  591
Co się stanie?

Co się stanie po uruchomieniu aplikacji? ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
Zanim zobaczymy aplikację w akcji, przeanalizujmy jeszcze raz,
co się stanie po jej uruchomieniu:

1 Podczas uruchamiania aktywności MainActivity metoda onStart() tworzy obiekt


ServiceConnection.
Aktywność prosi o powiązanie z usługą OdometerService.

MainActivity ServiceConnection

2 Usługa OdmeterService rozpoczyna działanie i zostaje wywołana jej metoda


onBind(), do której jest przekazywana intencja utworzona przez aktywność
MainActivity.

Intencja Intencja

onBind()

MainActivity ServiceConnection OdometerService

3 Metoda onBind() zwraca obiekt Binder.

Binder Binder
onBind()

MainActivity ServiceConnection OdometerService

592 Rozdział 13.


Usługi

Ciąg dalszy historii

4 Aktywność MainActivity pobiera z obiektu Binder referencję do usługi


OdometerService i zaczyna korzystać bezpośrednio z niej.

getDistance()

0
MainActivity OdometerService

5 W trakcie działania aktywności MainActivity metoda watchMileage() co


sekundę wywołuje metodę getDistance() usługi OdometerService i aktualizuje
dystans wyświetlony na ekranie.

<xml>
watchMileage() </xml>

Layout
MainActivity

6 Kiedy aktywność MainActivity zostaje zatrzymana, wywoływana jest metoda


unbindService(), co powoduje przerwanie powiązania aktywności z usługą.

MainActivity OdometerService

A teraz spróbujmy uruchomić aplikację i sprawdzić, jak działa.

jesteś tutaj  593


Jazda próbna

Jazda próbna aplikacji ¨  Powiązanie z usługą


¨  Wyświetlenie dystansu
Aby sprawdzić, jak w praktyce działa nasza aplikacja, musimy ją uruchomić
na urządzeniu wyposażonym w odbiornik GPS. W przeciwnym razie aplikacja
nie zadziała.

Bezpośrednio po uruchomieniu aplikacja wyświetla przebyty dystans


wynoszący 0 kilometrów. U góry urządzenia zostanie wyświetlona ikona Usługa informacyjna
informująca o włączeniu usługi lokalizacyjnej. została uruchomiona
i działa.

Początkowo prezentowany
przez aplikację przebyty
dystans wynosi 0,00
kilometra.

Ta wartość może się


nieznacznie zwiększyć po
uruchomieniu aplikacji.
Wynika to z faktu, że
precyzyjne określenie Kiedy pójdziemy
położnia urządzenia przez z urządzeniem na spacer,
system GPS może zająć przebyty dystans będzie się
kilka sekund, a początkowe powoli powiększał.
odczyty mogą być nieco
niedokładne.

Kiedy się przejedziemy z urządzeniem, dystans będzie się stopniowo


powiększał.

Wiemy, że masz masę świetnych pomysłów na to,


jak udoskonalić aplikację Drogomierz. Spróbuj więc
swoich sił! Możesz na przykład dodać do aplikacji
przyciski Start, Stop i Kasuj.

594 Rozdział 13.


Usługi

Twój przybornik do Androida

Rozdział 13.
Opanowałeś już rozdział 13. i dodałeś 
do swojego przybornika z narzędziami  j
Pełny kod przykładowe
umiejętność tworzenia i stosowania usług. aplikacji pre zen tow ane j
roz dzi ale mo żes z
w tym
pob rać z ser we ra FT P
wydawnictwa Helion:
klady/
ftp://ftp.helion.pl/przy
    CELNE SPOSTRZEŻENIA andrrg.zi p

 Usługa jest komponentem, który może  Usługę powiązaną zazwyczaj tworzy się,
wykonywać zadania w tle. Usługi nie mają rozszerzając klasę Service. Należy przy tym
własnego interfejsu użytkownika. zdefiniować własny obiekt Binder i przesłonić
jego metodę onBind(). Ta metoda jest
 Usługa uruchomiona może działać w tle
wywoływana, gdy komponent chce utworzyć
dowolnie długo, nawet po zakończeniu
powiązanie z usługą.
działania aktywności, która ją uruchomiła.
Po zakończeniu wykonywanych operacji usługa  W momencie tworzenia usługi wywoływana jest
jest zatrzymywana. metoda onCreate() klasy Service. Można jej
używać do inicjalizacji usługi.
 Usługi należy zadeklarować w pliku manifestu
AndroidManifest.xml, używając elementu  W momencie usuwania usługi wywoływana jest
<service>. metoda onDestroy() klasy Service.
 Aby utworzyć prostą usługę uruchomioną,  Informacje o aktualnym położeniu urządzenia
należy napisać klasę dziedziczącą po można uzyskać przy użyciu systemowej
IntentService i zaimplementować jej metodę usługi lokalizacyjnej. W tym celu najpierw
onHandleIntent(). Klasa IntentService należy utworzyć obiekt LocationListener
została zaprojektowana do obsługi intencji. i zarejestrować go w usłudze lokalizacyjnej.
Można przy tym podać kryteria określające,
 Do uruchamiania usług służy metoda
jak często obiekt ten będzie informowany
startService().
o zmianach położenia. W razie używania
 Jeśli przesłonimy metodę onStartCommand() odbiornika GPS konieczne jest dodanie
klasy IntentService, to wewnątrz niej musimy odpowiednich uprawnień do pliku manifestu
wywołać metodę klasy bazowej. AndroidManifest.xml.
 Do tworzenia powiadomień służy budowniczy  Aby powiązać aktywność z usługą, należy
powiadomień. Aby powiadomienie spowodowało utworzyć obiekt ServiceConnection
uruchomienie aktywności, należy przygotować i przesłonić jego metodę
intencję oczekującą. Następnie trzeba wyświetlić onServiceConnected(), tak by zwracała
powiadomienie, używając odpowiedniej usługi referencję do usługi.
systemowej.  Aby utworzyć powiązanie, należy wywołać
 Usługa powiązana jest skojarzona z innym metodę bindService(). Do przerywania
komponentem, takim jak aktywność. Aktywność powiązania służy metoda unbindService().
może prowadzić interakcję z taką usługą
i pobierać z niej wyniki.

jesteś tutaj  595


596 Rozdział 13.
14. Material Design
W materialistycznym świecie

Jeden gracz schodzi z pola,


a inny wchodzi na jego miejsce.
Dokładnie jak w widoku
RecyclerView.

W API poziomu 21 Google wprowadził Material Design.


Z tego rozdziału dowiesz się, czym jest Material Design i jak sprawić, by nasze aplikacje
do niego pasowały. Zaczniemy od przedstawienia widoków kart (ang. card views), których
możemy wielokrotnie używać w całej aplikacji, zapewniając jej spójny wygląd i sposób
obsługi. Następnie przedstawimy widok RecyclerView elastycznego przyjaciela widoków
list. Przy okazji dowiesz się także, jak tworzyć własne adaptery i jak całkowicie zmienić wygląd
widoku RecyclerView, używając zaledwie dwóch wierszy kodu.

to jest nowy rozdział  597


Material Design

Przedstawiamy Material Design


Material Design został wprowadzony w API poziomu 21, a jego
przeznaczeniem było zapewnienie jednolitego wyglądu i sposobu
obsługi wszystkich aplikacji na Androida. Chodzi o to, by użytkownik
mógł się przełączyć z aplikacji napisanych przez Google’a, takich jak
sklep Google Play, do aplikacji zaprojektowanych przez innych autorów
i od razu czuć się wygodnie i wiedzieć, jak z nich korzystać. Słowo
Material w tej nazwie pochodzi od wizualnego stylu Material Design,
który sprawia, że poszczególne fragmenty interfejsu użytkownika
przypominają nakładające się na siebie kawałki materiału lub papieru:

Material Design używa animacji i efektów 3D, takich jak cienie,


aby wyraźnie pokazywać użytkownikom, jak mogą korzystać z aplikacji.
W tym celu do Material Design dołączono zestaw dodatkowych bibliotek,
zawierających różne widżety i motywy graficzne, których możemy używać
w aplikacjach korzystających z Material Design. W tym rozdziale poznasz
kilka spośród tych widżetów i zastosujemy je, aby przerobić aplikację dla
Włoskiego Co Nieco, którą napisaliśmy w rozdziałach 9. i 10., tak aby
była ona zgodna z wytycznymi Material Design.

598 Rozdział 14.


Material Design

Widżety CardView i RecyclerView


Dwoma najważniejszymi widżetami stosowanymi w Material Design są
widoki CardView i RecyclerView.

Widżet CardView, nazywany także widokiem karty, pełni rolę pojemnika,


w którym można umieszczać inne widoki. Ma on zaokrąglone wierzchołki
i cienie, które sprawiają wrażenie, jakby unosił się nad tłem. Widok CardView
można animować, dzięki czemu po naciśnięciu wygląda, jakby się poruszał.

Z kolei widżet RecyclerView jest nowym rodzajem listy. Jego nazwa


pochodzi stąd, że zapewnia on możliwość efektywnego wielokrotnego
używania (odzyskiwania) widoków, a prezentowana w nim zawartość
ma postać listy. Można w nim także wyświetlać widoki CardView.

W tym rozdziale zmienimy aplikację dla restauracji Włoskie Co Nieco w taki


sposób, by korzystała z widoków CardView i RecyclerView. Innymi słowy:
zmienimy ją tak, że lista pizz zmieni wygląd

z takiego:
Dla maniaków
To jest normalny
widok ListView.
Material Design używa wielu efektów 3D.
Ale czy to nie spowalnia działania urządzenia?
W przypadku większości urządzeń odpowiedź
na to pytanie brzmi: nie. O ile to tylko możliwe,
na taki: widoki Material Design będą się starały
korzystać z mocy obliczeniowej graficznych
komponentów sprzętowych urządzenia
i generować cienie mniej więcej w taki sam
sposób, jak robią to gry. Oznacza to, że
To jest widok
wygenerowane cienie nie tylko będą piękne
RecyclerView wyglądać, lecz również ich wyświetlenie nie
zawierający dwa zajmie więcej czasu. W przypadku starszych
widoki CardView. urządzeń widoki Material Design wciąż będą
Z kolei każdy
z widoków umieszczały pod każdym z prezentowanych
CardView widoków specjalne obrazki cieni. To faktycznie
prezentuje będzie wymagało nieco dodatkowej mocy
nazwę pizzy obliczeniowej i zajmowało trochę więcej
i jej zdjęcie.
pamięci. A zatem, jeśli chcemy, by tworzona
aplikacja działała nawet na naprawdę starych
urządzeniach, to przed udostępnieniem
najlepiej będzie sprawdzić jej wydajność
na rzeczywistym, fizycznym urządzeniu.

jesteś tutaj  599


Znowu pizza

Struktura aplikacji dla restauracji Włoskie Co Nieco


Planujemy zmienić aplikację w taki sposób, by zastosować w niej dwa nowe widoki:
CardView i RecyclerView. Oto analiza struktury aplikacji i jej działania:

1 W momencie uruchamiania aplikacji wyświetlana jest aktywność MainActivity.


Ta aktywność używa układu z pliku activity_main.xml i korzysta z szuflady nawigacyjnej. Kiedy użytkownik
kliknie jedną z opcji dostępnych w szufladzie nawigacyjnej, zostanie wyświetlony odpowiedni fragment.

2 Gdy użytkownik kliknie opcję Pizze, aplikacja wyświetli fragment PizzaMaterialFragment.


Fragment PizzaMaterialFragment zawiera widok RecyclerView.

3 Fragment PizzaMaterialFragment używa adaptera, CaptionedImagesAdapter,


i wyświetla zdjęcie i nazwę każdej pizzy.
Widoki kart są zdefiniowane w pliku układu card_captioned_image.xml. Dane o pizzach pochodzą
natomiast z pliku Pizza.java.

4 Kiedy użytkownik kliknie jedną z pizz, uruchamiana jest aktywność PizzaDetailActivity,


prezentująca szczegółowe informacje na temat wybranej pizzy.

5 Gdy użytkownik kliknie opcję Złóż zamówienie dostępną na pasku akcji aktywności
MainActivity lub PizzaDetailActivity, zostanie uruchomiona aktywność OrderDetail.

<Layout>

Większość tych </Layout>

plików utworzyliśmy <Layout>


Top
już wcześniej, Fragment.java fragment_
a teraz nie będziemy </Layout>
top.xml
musieli ich zmieniać.
activity_main.xml

1 Pasta
Fragment.java

MainActivity.java
Urządzenie <Layout>

Stores </Layout>
2
Fragment.java card_
5 CaptionedImages captioned_
3 Adapter.java
OrderActivity.java image.xml
<Layout>

</Layout> PizzaMaterial
Fragment.java
activity_order.xml 4
Pizza.java
<Layout> PizzaDetail Te pliki musimy
dodać do
</Layout> Activity.java istniejącego
activity_pizza_ projektu.
detail.xml
600 Rozdział 14.
Material Design

Dodanie danych pizz


Zaczniemy od dodania do projektu zdjęć pizz. Znajdziesz je w kodach źródłowych
przykładów dołączonych do książki, które można pobrać z serwera FTP wydawnictwa
Helion: ftp://ftp.helion.pl/przyklady/andrrg.zip. Przeciągnij je wszystkie do katalogu Zrób to sam!
app/src/main/res/drawable-nodpi swojego projektu. Jeśli Android Studio nie utworzyło
takiego katalogu automatycznie podczas tworzenia projektu, to musisz zrobić to
samodzielnie.

Zdjęcia pizz umieszczamy w katalogu drawable-nodpi, gdyż chcemy, by urządzenie W tym rozdziale
używało ich niezależnie od gęstości ekranu. Gdybyś chciał, to oczywiście mógłbyś zmodyfikujemy aplikację
przygotować różne wersje tych obrazków przeznaczone dla ekranów o różnych gęstościach
dla restauracji Włoskie
i umieścić je w odpowiednich katalogach drawable*.
Co Nieco, więc otwórz
w Android Studio już
Dodanie klasy Pizza istniejący projekt.
Teraz dodamy do projektu klasę Pizza, z której widok RecyclerView będzie pobierał
dane pizz. Klasa ta definiuje tablicę zawierającą informacje o dwóch pizzach, przy
czym dla każdej z nich została podana nazwa i identyfikator zasobu. A zatem dodaj do
W prawdziwej aplikacji
projektu nową klasę należącą do pakietu com.hfad.wloskieconieco, nadaj jej nazwę użylibyśmy do tego baz zapewne
Pizza i zapisz w katalogu app/src/main/java. Oto kod tej klasy: Tutaj jednak, dla upr y danych.
osz
przykładu, używamy kla czenia
package com.hfad.wloskieconieco; sy Javy.

public class Pizza { Informacje o każdej pizzy składają się


z nazwy i identyfikatora zasobu.
private String name; Identyfikatory zasobów odwołują się do zdjęć,
private int imageResourceId; które wcześniej dodaliśmy do projektu.

public static final Pizza[] pizzas = {


new Pizza(”Diavolo”, R.drawable.diavolo),
new Pizza(”Funghi”, R.drawable.funghi)
}; Konstruktor klasy Pizza.

private Pizza(String name, int imageResourceId) {


this.name = name;
this.imageResourceId = imageResourceId;
}
WloskieCoNieco
public String getName() {
To są metody pobierające
return name; wartości zmiennych
} prywatnych. app/src/main

public int getImageResourceId() { java


return imageResourceId;
}
com.hfad.wloskieconieco
}

W tej aplikacji użyjemy widoków RecyclerView i CardView, a one wymagają


Pizza.java
bibliotek Support Libraries. A zatem w następnej kolejności zajmiemy się
dodaniem tych bibliotek.
jesteś tutaj  601
Dodanie bibliotek

Dodanie bibliotek Support Libraries


Widoki CardView i RecyclerView
pochodzą z bibliotek wsparcia v7 CardView
i RecyclerView, dlatego będziemy musieli
dodać je do projektu aplikacji. W tym
celu wybierz w menu opcję File/Project
Structure. Potem na liście z lewej strony okna
dialogowego wybierz opcję app i przejdź Zależności można
na kartę Dependencies. Następnie dodaj Aplikacja wymaga dodania dodawać i usuwać
zależności od tych bibliotek. za pomocą tych
zależności od bibliotek (opcja Library przycisków.
dependency) recyclerview-v7 i cardview-v7.

Po dodaniu zależności Android Studio zapisze je w pliku app/build.gradle:

...
dependencies {
Dodanie bibliotek wsparcia
compile fileTree(include: [‚*.jar’], dir: ‚libs’) w oknie pokazanym na powyższym
rysunku powoduje dopisanie ich
compile ‚com.android.support:appcompat-v7:23.1.0’ do pliku build.gradle.
compile ‚com.android.support:recyclerview-v7:23.1.0’
compile ‚com.android.support:cardview-v7:23.1.0’ WloskieCoNieco
}
app

Jeśli chcesz, to nic nie stoi na przeszkodzie, byś samemu dodawał


zależności niezbędne do działania aplikacji, wpisując je bezpośrednio build.gradle
w tym pliku. Będzie to miało taki sam skutek jak dodanie ich w oknie
dialogowym Project Structure.

A teraz, skoro dodaliśmy już zależności, możemy zająć się zastosowaniem


widoku CardView.

602 Rozdział 14.


Material Design

Utworzenie widoku CardView


Widok CardView jest stosowany do wizualnej reprezentacji, w rozpoznawalny android.view.ViewGroup
i spójny sposób, podstawowych elementów danych używanych w aplikacji. ...
W przypadku naszej aplikacji dla restauracji Włoskie Co Nieco takimi podstawowymi
elementami danych są pizze, dania z makaronu oraz restauracje. Dlatego też
utworzymy widoki CardView umożliwiające wyświetlanie tych danych.
android.widget.
Widok CardView tworzymy poprzez umieszczenie go w układzie. Może to być bądź to FrameLayout
już istniejący układ, bądź zupełnie nowy plik układu. Utworzenie nowego pliku układu ...
na potrzeby widoku CardView zapewnia jednak dodatkową możliwość używania go
w widoku RecyclerView.

A ponieważ w naszej aplikacji chcemy także zastosować widok RecyclerView, nowy android.support.v7.widget.
widok CardView umieścimy w osobnym pliku. Dodaj zatem do projektu nowy pliku CardView
układu o nazwie card_captioned_image.xml i zapisz go w katalogu app/src/main/res/layout.
...
Postać widoku CardView zdefiniujemy w następujący sposób:
Ten element dodaje widok CardView
CardView. jest klasą
<android.support.v7.widget.CardView pochodną klasy
FrameLayout.
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:card_view=”http://schemas.android.com/apk/res-auto” WloskieCoNieco
android:id=”@+id/card_view”
android:layout_width=”match_parent” app/src/main
android:layout_height=”200dp” Ten atrybut
android:layout_margin=”5dp” sprawia, że res
widok będzie
card_view:cardCornerRadius=”4dp”> mieć zaokrąglone
layout
... wierzchołki.
<xml>
</android.support.v7.widget.CardView> </xml>

card_captioned_
image.xml
Klasa CardView pochodzi z biblioteki wsparcia v7 CardView, dlatego musimy podać jej
pełną nazwę: android.support.v7.widget.CardView.

Aby wyświetlane karty mogły mieć zaokrąglone wierzchołki, musimy dodać przestrzeń nazw:

xmlns:card_view=”http://schemas.android.com/apk/res-auto”

a następnie zastosować atrybut card_view:cardCornerRadius, określając w nim


wielkość zaokrąglonych wierzchołków. Oto przykład zastosowania tego atrybutu:

card_view:cardCornerRadius=”4dp”

W tym przypadku promień zaokrąglenia wynosi 4dp.

Postać widoku CardView definiujemy, dodając do powyższego elementu kolejne


widoki. Chcemy, by w każdym z widoków CardView został wyświetlony obrazek i tekst.
Kompletny kod pliku układu widoku CardView przedstawimy na następnej stronie.
jesteś tutaj  603
Kod układu card_captioned_image

Kompletny kod pliku card_captioned_image.xml


Poniżej przedstawiliśmy gotowy kod pliku card_captioned_image.xml (dodaliśmy
do niego układ liniowy zawierający widok obrazka oraz widok tekstowy;
takie rozwiązanie było konieczne, gdyż klasa CardView dziedziczy po klasie
FrameLayout, a układy tego typu dają możliwość umieszczania wewnątrz nich
tylko jednego elementu podrzędnego — w naszym przypadku elementem tym
będzie pojedynczy układ liniowy):
WloskieCoNieco
<?xml version=”1.0” encoding=”utf-8”?>
<android.support.v7.widget.CardView
app/src/main
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:card_view=”http://schemas.android.com/apk/res-auto”
res
android:id=”@+id/card_view”
android:layout_width=”match_parent”
ie layout
android:layout_height=”200dp” Szerokość widoku CardView będz zędny
elem ent nadr
ograniczona przez jego
<xml>
android:layout_margin=”5dp”
i będzie on miał wysokość 200dp.
</xml>

card_view:cardCornerRadius=”4dp”> card_captioned_
image.xml

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”fill_parent”
android:orientation=”vertical”> Szerokość zdjęcia będzie ograniczona
przez jego element nadrzędny.
Zastosowaliśmy także atrybut scaleType
<ImageView android:id=”@+id/info_image” o wartości centerCrop, aby zdjęcie
android:layout_height=”0dp” zostało przeskalowane proporcjonalnie.
android:layout_width=”match_parent”
android:layout_weight=”1.0”
android:scaleType=”centerCrop”/>

<TextView
android:id=”@+id/info_text”
android:layout_marginLeft=”5dp”
android:layout_marginBottom=”5dp”
android:layout_height=”wrap_content”
android:layout_width=”match_parent”/>
</LinearLayout>
</android.support.v7.widget.CardView>

Tego widoku CardView będziemy mogli używać do prezentowania wszystkich


elementów danych zawierających zdjęcie i podpis, takich jak nasze pizze.

Kolejną rzeczą, którą musimy się zająć, jest utworzenie widoku RecyclerView,
w którym będą wyświetlane powyższe widoki CardView.

604 Rozdział 14.


Material Design

Widok RecyclerView używa adaptera RecyclerView.Adapter


Widok RecyclerView jest nieco bardziej zaawansowaną wersją zwyczajnego
widoku listy. Czyli, podobnie jak listy, także widok RecyclerView jest
przewijalnym pojemnikiem służącym do wyświetlania zbioru danych.
W odróżnieniu jednak od zwyczajnych list widok RecyclerView jest bardziej
wydajny w przypadku prezentowania dużych zbiorów danych. Tę poprawę
wydajności uzyskano dzięki ponownemu wykorzystywaniu (odzyskiwaniu)
widoków, które już nie są widoczne na ekranie. Dla porównania w listach
dla każdego elementu wyświetlanego na ekranie tworzony jest nowy widok.

Podobnie jak w przypadku zwyczajnego widoku listy, także do widoku


RecyclerView dodajemy dane, używając adaptera. Niestety RecyclerView nie
współpracuje z żadnym z wbudowanych adapterów, takich jak ArrayAdapter
czy też CursorAdapter. Zamiast nich musimy napisać własny adapter, rozszerzając
w tym celu klasę RecyclerView.Adapter.

Adapter używa
Widok Adapter Dane określonych przez
Widok RecyclerView
używa adaptera. Recycler nas danych.
W naszym
View przypadku będą
one pochodziły
z klasy Pizza.

Musimy napisać własny adapter.

Adapter pełni dwa główne zadania: tworzy każdy z widoków prezentowanych w widżecie
i konfiguruje widok, by dopasować go do danych.

W naszym przypadku widok RecyclerView musi wyświetlić listę kart, z których każda zawiera
obrazek i widok tekstowy. Oznacza to, że adapter musi tworzyć widoki dla tych elementów
i zastępować ich zawartość, kiedy dany element nie będzie już widoczny na ekranie.

Na kilku następnych stronach będziemy tworzyć adapter dla widoku RecyclerView.


W tym celu musimy zrobić trzy rzeczy:

1 Określić typ danych, na których będzie operował adapter.


Musimy poinstruować adapter, że będzie pracował na widokach CardView,
a w każdym z nich będzie musiał określić zdjęcie i podpis.

2 Utworzyć widoki.
Adapter musi tworzyć wszystkie widoki, które będą wyświetlane na ekranie.

3 Powiązać dane z widokami.


Kiedy dane staną się dostępne, adapter musi zapisać je w odpowiednich widokach.

Zaczniemy od dodania do projektu klasy pochodnej klasy RecyclerView.Adapter.

jesteś tutaj  605


Utworzenie adaptera

Utworzenie prostego adaptera


Zajmiemy się teraz zaimplementowaniem prostego adaptera na potrzeby
widoku RecyclerView. Będzie on nosił nazwę CaptionedImagesAdapter.
Utwórz zatem nową klasę o tej nazwie, a wygenerowany przez Android Studio
kod zastąp tym przedstawionym poniżej:

package com.hfad.wloskieconieco;
oteki wsparcia.
Klasa RecyclerView należy do bibli
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder>{


// Metoda zwraca referencje do widoków używanych
public static class ViewHolder extends RecyclerView.ViewHolder {
// Definiujemy obiekt ViewHolder
Musimy zdefiniować obiekt
} ViewHolder. Zajmiemy się
@Override tym na następnej stronie.
WloskieCoNieco
public CaptionedImagesAdapter.ViewHolder onCreateViewHolder(
ViewGroup parent, int viewType){ app/src/main
// Tworzymy nowy widok
} Musimy zaimplementować te metody. java
@Override
public void onBindViewHolder(ViewHolder holder, int position){ com.hfad.wloskieconieco
// Ustawiamy wartości w konkretnym widoku
} Tę metodę też musimy
zaimplementować. CaptionedImages
@Override Adapter.java
public int getItemCount(){
// Metoda zwraca liczbę elementów w zbiorze danych
}
}

Jak widać, nasz adapter CaptionedImagesAdapter rozszerza klasę


RecyclerView.Adapter i implementuje jej metody getItemCount(),
onCreateViewHolder() oraz onBindViewHolder(). Pierwsza z nich,
metoda getItemCount(), służy do zwracania liczby elementów w zbiorze
danych, metoda onCreateViewHolder() jest używana do tworzenia
widoków, a metoda onBindViewHolder() do ustawiania wartości
poszczególnych widoków. Tworząc swoje własne adaptery używane
w widokach RecyclerView, zawsze musimy zaimplementować te metody.

Wewnątrz adaptera zdefiniowaliśmy także klasę ViewHolder, która określa


dane, na których będzie operował adapter. Tą klasą zajmiemy się
w następnej kolejności.

606 Rozdział 14.


Material Design

Zdefiniowanie obiektu ViewHolder Obiekt ViewHolder przechowuje jeden


lub kilka widoków.

na potrzeby adaptera
Obiekt ViewHolder udostępnia referencję do widoku lub widoków
każdego elementu danych prezentowanego w widżecie RecyclerView.
To właśnie w nim są przechowywane widoki, które chcemy wyświetlać.

Tworząc adapter dla widżetu RecyclerView, musimy jednocześnie


utworzyć w tym adapterze obiekt ViewHolder. W tym celu należy
napisać klasę dziedziczącą po RecyclerView.ViewHolder i określającą,
jakiego typu dane będą przechowywane w obiekcie ViewHolder.

W przypadku naszego widoku RecyclerView poszczególne elementy Każdy obiekt ViewHolder zawiera obiekt
danych są widokami CardView, dlatego to właśnie tego typu dane ma CardView.
przechowywać nasz obiekt ViewHolder. Oto jego kod:

package com.hfad.wloskieconieco;

...
import android.support.v7.widget.CardView;

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder>{


// Metoda zwraca referencje do widoków używanych
public static class ViewHolder extends RecyclerView.ViewHolder {
private CardView cardView;
public ViewHolder(CardView v) { WloskieCoNieco

super(v);
app/src/main
cardView = v; Nasz widok RecyclerView ma wyświetlać
widoki CardView, dlatego określamy, że obiekt
} ViewHolder używany w adapterze będzie
java
} zawierał obiekty CardView. Gdybyśmy w widoku
RecyclerView chcieli wyświetlać dane innego typu,
... to musielibyśmy go tutaj zdefiniować. com.hfad.wloskieconieco
}

CaptionedImages
Tworząc własny obiekt ViewHolder, musimy wywołać konstruktor jego Adapter.java
klasy bazowej:

super(v);

Wynika to z faktu, że ta klasa bazowa zawiera metadane, takie jak


położenie danego elementu w widoku RecyclerView, niezbędne do
prawidłowego działania adaptera.

Skoro utworzyliśmy już obiekt ViewHolder do przechowywania widoków


CardView, zadbamy o to, by adapter wyświetlał je w widoku RecyclerView.

jesteś tutaj  607


Utworzenie obiektów ViewHolder

Utworzenie obiektów ViewHolder


Widok RecyclerView przechowuje i zarządza ściśle określonym zestawem
obiektów ViewHolder, które z kolei zawierają widoki wyświetlane w liście na
ekranie. Liczba obiektów ViewHolder zależy od wielkości ekranu, na którym
mają być wyświetlane, i od wielkości obszaru zajmowanego przez poszczególne
elementy danych. Aby zapewnić widokowi RecyclerView możliwość określenia
liczby obiektów ViewHolder, którymi będzie musiał zarządzać, musimy określić,
który układ ma być używany do prezentacji elementów danych. Właśnie do tego
celu służy metoda onCreateViewHolder().

Kiedy widok RecyclerView jest tworzony po raz pierwszy, przygotowuje


on zestaw obiektów ViewHolder, cyklicznie wywołując metodę
onCreateViewHolder(), tak długo, aż zostaną utworzone wszystkie obiekty,
których będzie on potrzebował. Metoda onCreateViewHolder() ma dwa
parametry: nadrzędny obiekt ViewGroup (którym jest sam widok RecyclerView)
i liczba całkowita (int) o nazwie viewType. Można jej używać w przypadkach,
gdybyśmy chcieli wyświetlać różne widoki dla poszczególnych elementów listy.

W tym przypadku chcemy, by obiekty ViewHolder zawierały widoki CardView,


których postać określa plik układu card_captioned_image.xml. Oto kod, który to
zapewni:

...
import android.view.LayoutInflater;

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder>{

...
WloskieCoNieco
@Override
public CaptionedImagesAdapter.ViewHolder onCreateViewHolder( app/src/main
ViewGroup parent, int viewType){
CardView cv = (CardView) LayoutInflater.from(parent.getContext()) java
.inflate(R.layout.card_captioned_image, parent, false);
com.hfad.wloskieconieco
return new ViewHolder(cv);
any
} Określamy, jaki układ ma być używ
w widokach przechowywanych
} w obiektach ViewHolder. CaptionedImages
Adapter.java

Teraz, kiedy adapter obiektu RecyclerView może już tworzyć obiekty


ViewHolder, musimy zadbać o to, by adapter zapisał dane w widokach,
które przechowuje.

608 Rozdział 14.


Material Design

Każdy widok CardView wyświetla zdjęcie i podpis


Za każdym razem, gdy podczas przewijania zawartości widoku RecyclerView
zostanie wyświetlony jego nowy element, widok ten pobierze jeden z obiektów
ViewHolder dostępnych w jego puli i wywoła metodę onBindHolder(), by
powiązać go z danymi. Kod tej metody musi określić zawartość poszczególnych
widoków w obiekcie ViewHolder, tak by odpowiadały one danym.
TextView
W naszym przypadku obiekt ViewHolder zawiera widoki CardView, w których
musimy określić wyświetlane zdjęcie i jego podpis. W tym celu dodamy do
CardView
adaptera konstruktor, dzięki któremu widok RecyclerView będzie mógł ImageView
przekazywać do niego dane. Następnie użyjemy metody onBindViewHolder()
Każdy widok CardView zawiera
do powiązania danych z widokiem CardView. widoki TextView i ImageView.
Musimy w nich zapisać podpisy
Implementacja konstruktora i zdjęcia poszczególnych pizz.

Widok RecyclerView musi przekazywać do adaptera tablice podpisów


i identyfikatorów obrazków, dlatego dodamy do adaptera konstruktor mający
takie parametry. Każdą z tablic zapiszemy w zmiennej instancyjnej. Oprócz
tego użyjemy liczby przekazanych podpisów do określenia liczby elementów
zbioru danych:

...

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder>{

private String[] captions;


private int[] imageIds; Tych zmiennych będziemy
używać do przechowywania WloskieCoNieco
danych o pizzach.
...
app/src/main

public CaptionedImagesAdapter(String[] captions, int[] imageIds){


java
this.captions = captions;
this.imageIds = imageIds; Dane będziemy przekazywać
do adaptera, używając jego com.hfad.wloskieconieco
} konstruktora.

@Override CaptionedImages
Adapter.java
public int getItemCount(){
return captions.length; Wielkość tablicy podpisów odpowiada liczbie
} elementów danych prezentowanych w widoku
RecyclerView.

Teraz, kiedy adapter może już pobierać dane, sprawimy, by potrafił je


także wyświetlać w widokach. W tym celu zaimplementujemy metodę
onBindViewHolder().
jesteś tutaj  609
Dodanie danych

Dodanie danych do widoków CardView


Metoda onBindViewHolder() jest wywoływana za każdym razem, gdy widok
RecyclerView musi wyświetlić dane w obiekcie ViewHolder. Metoda ta ma
dwa parametry: obiekt ViewHolder, z którym należy powiązać dane, i indeks
tych danych w zbiorze danych adaptera.

A zatem musimy powiązać widok CardView z danymi. Widok ten zawiera


dwa widoki podrzędne: widok obrazka o identyfikatorze info_image i widok WloskieCoNieco
tekstowy o identyfikatorze info_text. Dane przeznaczone do powiązania
z tymi widokami podamy, odczytując je z tablic imageIds i captions. app/src/main

Oto kod, który będzie zapisywał dane pizzy w widokach obiektu CardView:
java
...
com.hfad.wloskieconieco
import android.widget.ImageView; Używamy tych dodatkowych klas.
import android.widget.TextView; CaptionedImages
import android.graphics.drawable.Drawable; Adapter.java

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder>{

private String[] captions;


private int[] imageIds; Te zmienne zawierają podpisy
i identyfikatory zasobów
obrazków poszczególnych pizz.
...
Ten kod wyświetla
zdjęcie w widoku
public void onBindViewHolder(ViewHolder holder, int position){ ImageView.
CardView cardView = holder.cardView;
ImageView imageView = (ImageView)cardView.findViewById(R.id.info_image);
Drawable drawable = cardView.getResources().getDrawable(imageIds[position]);
imageView.setImageDrawable(drawable);
imageView.setContentDescription(captions[position]);
TextView textView = (TextView)cardView.findViewById(R.id.info_text);
textView.setText(captions[position]);
}
} Ten fragment kodu wyświetla podpis
w widoku TextView.

I to już cały kod adaptera. Na następnej stronie przedstawimy go w całości.

610 Rozdział 14.


Material Design

Kompletny kod pliku CaptionedImagesAdapter.java


package com.hfad.wloskieconieco;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.support.v7.widget.CardView; To są wszystkie używane klasy.
import android.widget.ImageView;
import android.widget.TextView;
import android.graphics.drawable.Drawable;

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder> {

private String[] captions;


private int[] imageIds;
WloskieCoNieco

public static class ViewHolder extends RecyclerView.ViewHolder {


private CardView cardView; app/src/main
public ViewHolder(CardView v) {
super(v);
java
cardView=v; Każdy obiekt ViewHolder wyświet
widok CardView. la
}
} com.hfad.wloskieconieco

public CaptionedImagesAdapter(String[] captions, int[] imageIds){


this.captions = captions; CaptionedImages
this.imageIds = imageIds; Dane do adaptera przekazujemy Adapter.java
} w jego konstruktorze.

@Override
public CaptionedImagesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
CardView cv = (CardView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_captioned_image, parent, false);
return new ViewHolder(cv);
} W widokach CardView użyjemy
przygotowanego wcześniej układu.
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
CardView cardView = holder.cardView;
ImageView imageView = (ImageView)cardView.findViewById(R.id.info_image);
Drawable drawable = cardView.getResources().getDrawable(imageIds[position]);
imageView.setImageDrawable(drawable);
imageView.setContentDescription(captions[position]);
TextView textView = (TextView)cardView.findViewById(R.id.info_text);
textView.setText(captions[position]);
} Zapisujemy dane w widokach
ImageView i TextView
@Override wyświetlanych w widoku CardView.
public int getItemCount() {
return captions.length; To jest liczba elementów danych.
}
}
jesteś tutaj  611
Utworzenie widoku RecyclerView

Utworzenie widoku RecyclerView


Do tej pory udało się nam stworzyć widok CardView i adapter. Kolejną
rzeczą, którą musimy się zająć, jest stworzenie widoku RecyclerView.
Widok ten przekaże do adaptera dane o pizzach, dzięki czemu adapter
będzie mógł je zapisać w odpowiednich widokach.

Widok RecyclerView umieścimy w nowym fragmencie. Takie rozwiązanie


zastosujemy dlatego, że chcemy wyświetlać aktywność MainActivity za
każdym razem, gdy użytkownik kliknie w szufladzie nawigacyjnej opcję Pizze.

Zaczniemy od utworzenia fragmentu. W tym celu dodaj do projektu


nowy, pusty fragment, jego klasie nadaj nazwę PizzaMaterialFragment,
a używany przez niego układ nazwij fragment_pizza_material.

Na następnej stronie dodamy do tego układu widok RecyclerView.

612 Rozdział 14.


Material Design

Dodanie widoku RecyclerView do układu


Widok RecyclerView można dodać do układu, używając elementu
<android.support.v7.widget.RecyclerView>.

Poniżej przedstawiliśmy kod układu fragment_pizza_material.xml;


zawiera on widok RecyclerView o identyfikatorze pizza_recycler:
WloskieCoNieco

<?xml version=”1.0” encoding=”utf-8”?>


app/src/main
<android.support.v7.widget.RecyclerView
xmlns:android=”http://schemas.android.com/apk/res/android” res
android:id=”@+id/pizza_recycler”
android:scrollbars=”vertical” Ten element definiuje widok layout
android:layout_width=”match_parent” RecyclerView z pionowym <xml>
paskiem przewijania. </xml>
android:layout_height=”match_parent”/> fragment_pizza_
material.xml
Paski przewijania możemy dodać do widoku RecyclerView przy użyciu atrybutu
android:scrollbars. W powyższym przykładzie przypisaliśmy mu wartość
”vertical”, gdyż chcemy, żeby widok RecyclerView przedstawiał pionową listę
pizz, której zawartość użytkownik będzie mógł przewijać w pionie.

Skoro już dodaliśmy widok RecyclerView do pliku układu fragment_pizza_


material.xml, musimy dodać do pliku PizzaMaterialFragment.java kod, który
będzie określał jego zachowanie.

Zastosowanie adaptera
W pliku PizzaMaterialFragment.java zastosujemy adapter w zdefiniowanym
powyżej widoku RecyclerView. Musimy przy tym poinstruować adapter, jakich
danych ma używać — zastosujemy do tego konstruktor adaptera. Następnie
wywołamy metodę setAdapter() widoku RecyclerView, aby przypisać do
niego adapter.

Widok Adapter Dane


Recycler
View

Na następnej stronie przedstawimy kompletny kod z pliku


PizzaMaterialFragment.java.

jesteś tutaj  613


Kod klasy PizzaMaterialFragment

Kod klasy PizzaMaterialFragment


Poniżej przedstawiliśmy kompletny kod klasy PizzaMaterialFragment
(tworzy on instancję adaptera CaptionedImagesAdapter, przekazując tablice
nazw i zdjęć pizz, a następnie przypisuje adapter do widoku RecyclerView):

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.app.Fragment;
import android.os.Bundle;
app/src/main
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; java
import android.view.View;
Używamy tych klas.
import android.view.ViewGroup; com.hfad.wloskieconieco

public class PizzaMaterialFragment extends Fragment { PizzaMaterial


Fragment.java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RecyclerView pizzaRecycler = (RecyclerView)inflater.inflate(
R.layout.fragment_pizza_material, container, false);
Tu używamy układu, który
zmodyfikowaliśmy na
String[] pizzaNames = new String[Pizza.pizzas.length]; poprzedniej stronie.
for (int i = 0; i < pizzaNames.length; i++) {
pizzaNames[i] = Pizza.pizzas[i].getName(); Te dwa fragmenty kodu dodają
} nazwy pizz do tablicy typu String
i zdjęcia do tablicy typu int.

int[] pizzaImages = new int[Pizza.pizzas.length];


for (int i = 0; i < pizzaImages.length; i++) {
pizzaImages[i] = Pizza.pizzas[i].getImageResourceId();
}
To wywołanie przekazuje tablice
do adaptera.
CaptionedImagesAdapter adapter =
new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
return pizzaRecycler;
}
}

Pozostaje do zrobienia jeszcze jedna rzecz: musimy określić,


jak mają być rozmieszczone widoki w widżecie RecyclerView.
614 Rozdział 14.
Material Design

Do rozmieszczania zawartości RecyclerView


używa menedżera układu
Jednym z czynników, które sprawiają, że widok RecyclerView jest
bardziej elastyczny od zwyczajnych list, są możliwości rozmieszczania
jego zawartości. W przypadku listy wszystkie wyświetlane na niej widoki
są umieszczane w jednej pionowej kolumnie. Widok RecyclerView
daje nam pod tym względem znacznie więcej możliwości — możemy
zażądać, by widoki były w nim wyświetlane w formie listy, siatki lub siatki
nierównomiernej.

Sposób rozmieszczania widoków w widżecie RecyclerView jest określany


przez menedżer układu. To właśnie menedżer układu określa położenie
widoków w widżecie, a wybór konkretnego typu menedżera układu
determinuje, jak będą one rozmieszczane:

LinearLayoutManager GridLayoutManager StaggeredGridLayoutManager


Ten menedżer rozmieszcza Ten menedżer rozmieszcza Ten menedżer tworzy siatkę,
elementy w formie pionowej elementy w formie siatki. w której poszczególne elementy
lub poziomej listy. mogą mieć różne wielkości.

Na następnej stronie pokażemy, jak określić, który menedżer układu


zostanie użyty.

jesteś tutaj  615


Menedżery układu

Określanie menedżera układu


Menedżer układu, który ma zostać użyty w widoku RecyclerView,
określamy za pomocą poniższego fragmentu kodu:

LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());


pizzaRecycler.setLayoutManager(layoutManager);
i
To musi być obiekt Context. Jeśl
ci,
Powyższy kod informuje, że widok RecyclerView ma używać menedżera używamy tego kodu w aktywnoś
układu LinearLayoutManager, a to oznacza, że wszystkie widoki będą to powinniśmy użyć kontekstu
zamiast wywołania getActivity().
wyświetlane w formie listy:

W razie użycia menedżera


LinearLayoutManager
wszystkie widoki będą
rozmieszczane w formie listy.

Korzystając z menedżerów układu, bardzo łatwo można zmienić postać danych


wyświetlanych w widoku RecyclerView. Na przykład gdybyśmy uznali, że zawartość
widżetu RecyclerView ma być wyświetlana w formie siatki, to wystarczyłoby zmienić
używany menedżer układu na GridLayoutManager:

GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 2);


pizzaRecycler.setLayoutManager(layoutManager);
Ten argument określa, że
układ GridLayoutManager
ma wyświetlać widoki
w dwóch kolumnach.

Po zmianie menedżera układu


na GridLayoutManager widoki
będą rozmieszczane w formie
siatki.

616 Rozdział 14.


Material Design

Kompletny kod klasy PizzaMaterialFragment


Oto kompletny kod z pliku PizzaMaterialFragment.java:

package com.hfad.wloskieconieco;
Używamy tej klasy, więc
musimy ją zaimportować.
import android.app.Fragment;
import android.os.Bundle; WloskieCoNieco
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; app/src/main
import android.view.LayoutInflater;
import android.view.View; java
import android.view.ViewGroup;
com.hfad.wloskieconieco
public class PizzaMaterialFragment extends Fragment {
PizzaMaterial
@Override Fragment.java
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RecyclerView pizzaRecycler = (RecyclerView)inflater.inflate(
R.layout.fragment_pizza_material, container, false);

String[] pizzaNames = new String[Pizza.pizzas.length];


for (int i = 0; i < pizzaNames.length; i++) {
pizzaNames[i] = Pizza.pizzas[i].getName();
W tym kodzie nie
} wprowadzaliśmy
żadnych zmian.
int[] pizzaImages = new int[Pizza.pizzas.length];
for (int i = 0; i < pizzaImages.length; i++) {
pizzaImages[i] = Pizza.pizzas[i].getImageResourceId();
}

CaptionedImagesAdapter adapter =
new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
pizzaRecycler.setLayoutManager(layoutManager);
return pizzaRecycler; Widoki CardView chcemy wyświet
lać
} w formie liniowej listy, dlatego
używamy menedżera układu
} LinearLayoutManager.

Skoro zakończyliśmy prace nad kodem widoku RecyclerView, zajmijmy się


kodem aktywności MainActivity, tak by nasz nowy widok był wyświetlany po
kliknięciu opcji Pizze dostępnej w szufladzie nawigacyjnej aplikacji.

jesteś tutaj  617


Zastosowanie fragmentu PizzaMaterialFragment

Zastosowanie fragmentu PizzaMaterialFragment


w aktywności MainActivity
Obecnie, kiedy użytkownik kliknie opcję Pizze dostępną w szufladzie nawigacyjnej,
wyświetlany jest fragment PizzaFragment typu ListFragment. Aby zamiast niego
wyświetlać nasz nowy fragment PizzaMaterialFragment, w kodzie aktywności
MainActivity musimy zastąpić wszystkie odwołania do klasy PizzaFragment
odwołaniami do klasy PizzaMaterialFragment.

W kodzie pliku MainActivity.java klasa PizzaFragment jest używana dwa razy:


w metodach onCreate() i selectItem(). Musimy zatem zmienić te wiersze kodu,
używając w nich klasy PizzaMaterialFragment:

package com.hfad.wloskieconieco;
...
WloskieCoNieco
public class MainActivity extends Activity {
...
app/src/main
@Override
protected void onCreate(Bundle savedInstanceState) {
java
...
getFragmentManager().addOnBackStackChangedListener(
com.hfad.wloskieconieco
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
... MainActivity.java
if (fragment instanceof PizzaFragment) {
if (fragment instanceof PizzaMaterialFragment) {
currentPosition = 1;
}
...
} tu
Używamy naszego nowego fragmen
fragmentu
PizzaMaterialFragment zamiast y
PizzaFragment. To ozna cza, że kied
private void selectItem(int position) { szufladzie
użytkownik kliknie opcję Pizze w lony nasz
... nawigacyjnej, to zostanie wyświet iew.
switch(position) { lśniący nowością widżet RecyclerV

case 1:
fragment = new PizzaFragment();
fragment = new PizzaMaterialFragment();
break;
...
}

}
Zanim uruchomimy aplikację, przyjrzyjmy się jeszcze raz, jak będą działać
wszystkie zmiany, które do tej pory wprowadziliśmy w jej kodzie.

618 Rozdział 14.


Material Design

Co się stanie po uruchomieniu kodu?


1 Użytkownik klika opcję Pizze w szufladzie nawigacyjnej.
Kod aktywności MainActivity przygotowuje do wyświetlenia fragment
PizzaMaterialFragment i wywoływana jest metoda onCreateView() tego fragmentu.

onCreateView()

MainActivity PizzaMaterialFragment

2 Metoda onCreateView() fragmentu PizzaMaterialFragment tworzy menedżer


układu LinearLyoutManager i przekazuje go do widoku RecyclerView.
Zastosowanie menedżera LinearLayoutManager oznacza, że widoki będą rozmieszczone
w formie listy. Nasz widżet RecyclerView ma pionowy pasek przewijania, co oznacza,
że lista będzie miała układ pionowy.

RecyclerView

PizzaMaterialFragment

LinearLayoutManager

3 Metoda onCreateView() fragmentu PizzaMaterialFragment tworzy adapter


CaptionedImagesAdapter.
Za pomocą konstruktora adaptera przekazywane są do niego tablice z nazwami pizz
i ich zdjęciami, po czym sam adapter jest przekazywany do widoku RecyclerView.

RecyclerView LinearLayoutManager

Dane pizzy

PizzaMaterialFragment CaptionedImagesAdapter

jesteś tutaj  619


Co się dzieje?

Ciąg dalszy historii

4 Adapter tworzy obiekt ViewHolder dla każdego z widoków CardView,


które będą prezentowane w widżecie RecyclerView.

ViewHolder CardView

CaptionedImagesAdapter

ViewHolder CardView

5 Następnie w każdym z widoków CardView adapter zapisuje podpis i zdjęcie


pizzy z odpowiednimi widokami.

“Diavolo”
Diavolo data
TextView
Dane pizzy R.drawable.Diavolo
diavolo
ViewHolder CardView ImageView
Dane pizzy funghi
“Funghi”
Funghi data
CaptionedImagesAdapter
TextView
R.drawable.Funghi

ViewHolder CardView
ImageView

A teraz spróbujmy uruchomić aplikację i sprawdźmy, jak wygląda


w nowej wersji.

620 Rozdział 14.


Material Design

Jazda testowa aplikacji


Uruchom aplikację, otwórz szufladę nawigacyjną, a następnie kliknij opcję Pizze.

Po kliknięciu opcji
Pizze zostanie
wyświetlony fragment
PizzaMaterialFragment.
Zawiera on liniową
listę widoków CardView,
z których każdy
przedstawia dane jednej
pizzy.

Jak widać, aplikacja wyświetla widżet RecyclerView z liniową listą


widoków CardView. Z kolei każdy z widoków CardView zawiera
dane dotyczące jednej pizzy.

jesteś tutaj  621


Magnesiki

Magnesiki RecyclerView
Użyj magnesików umieszczonych na tej i następnej stronie,
by przygotować zupełnie nowy widok RecyclerView, który będzie służył do
prezentowania dań z makaronu. Powinien on zawierać liniową listę widoków
CardView, z których każdy będzie prezentował nazwę i zdjęcie dania.

To jest kod klasy Pasta.


package com.hfad.wloskieconieco;
WloskieCoNieco
public class Pasta {
private String name; app/src/main
private int imageResourceId;
java
public static final .......... [] pastas = {
com.hfad.wloskieconieco
new Pasta(”Spaghetti bolognese”, R.drawable.spag_bol),
new Pasta(”Lasagne”, R.drawable.lasagne)
}; Pasta.java

private Pasta(String name, int imageResourceId) {


this.name = name;
getName()
this.imageResourceId = imageResourceId; Pasta
}
RecyclerView
android:scrollbars
public String . .............. {
return name;
} android.support.v7.widget.RecyclerView

=
public int ........................ {
return imageResourceId; "vertical"
getImageResourceId()
}
}

To jest kod układu. WloskieCoNieco

<........................................
xmlns:android=”http://schemas.android.com/apk/res/android” app/src/main

android:id=”@+id/pasta_recycler”
res
.............................................
android:layout_width=”match_parent”
layout
android:layout_height=”match_parent”/> <xml>
</xml>

fragment_pasta_
material.xml
622 Rozdział 14.
Material Design
To jest kod z pliku PastaMaterialFragment.java
...
public class PastaMaterialFragment extends Fragment {
WloskieCoNieco

@Override app/src/main
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { java
RecyclerView pastaRecycler = (RecyclerView)inflater.inflate(
com.hfad.wloskieconieco

..............................., container, false);


Pasta
String[] pastaNames = new String[Pasta.pastas.length]; Material
Fragment.java
for (int i = 0; i < pastaNames.length; i++) {
pastaNames[i] = Pasta.pastas[i].getName();
}

int[] pastaImages = new int[Pasta.pastas.length];


for (int i = 0; i < pastaImages.length; i++) {
pastaImages[i] = Pasta.pastas[i].getImageResourceId();
}

.......................... adapter =

new .................... (pastaNames, ..............);


pastaRecycler.setAdapter(adapter);

. ................. layoutManager = new ..................(getActivity());


pastaRecycler.setLayoutManager(layoutManager);
return pastaRecycler;
}
}

CaptionedImagesAdapter

ArrayAdapter CaptionedImagesAdapter

LinearLayout
LinearLayoutManager ta_material
R.layout.fragment_pas

LinearLayout ArrayAdapter
pastaImages
LinearLayoutManager

jesteś tutaj  623


Rozwiązanie magnesików

Magnesiki RecyclerView. Rozwiązanie


Użyj magnesików umieszczonych na tej i następnej stronie, by
przygotować zupełnie nowy widok RecyclerView, który będzie służył do
prezentowania dań z makaronu. Powinien on zawierać liniową listę widoków
CardView, z których każdy będzie prezentował nazwę i zdjęcie dania.

package com.hfad.wloskieconieco;
WloskieCoNieco
public class Pasta {
private String name; app/src/main
private int imageResourceId; To jest tablica obiektów Pasta.
java
Pasta
public static final .... . [] pastas = {
com.hfad.wloskieconieco
new Pasta(”Spaghetti bolognese”, R.drawable.spag_bol),
new Pasta(”Lasagne”, R.drawable.lasagne)
}; Pasta.java

private Pasta(String name, int imageResourceId) {


this.name = name;
this.imageResourceId = imageResourceId;
}
RecyclerView
public String . getName() . {
return name; Te metody są używane przez kod
z pliku PastaMaterialFragment.jav To jest niepotrzebny
} a. magnesik.

public int getImageResourceId()


... {
return imageResourceId;
}
} k
Ten element dodaje do układu wido
Recy clerV iew.
WloskieCoNieco

<. android.support.v7.widget.RecyclerView
.........
xmlns:android=”http://schemas.android.com/apk/res/android” app/src/main

android:id=”@+id/pasta_recycler”
res
android:scrollbars = "vertical" Ten atrybut dodaje pionowy
................... .................. pasek przewijania.
android:layout_width=”match_parent” layout
<xml>
android:layout_height=”match_parent”/> </xml>

fragment_pasta_
material.xml
624 Rozdział 14.
Material Design

...
public class PastaMaterialFragment extends Fragment {
WloskieCoNieco

@Override app/src/main
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { java
RecyclerView pastaRecycler = (RecyclerView)inflater.inflate(
com.hfad.wloskieconieco
Zastosuj ten
układ. R.layout.fragment_pasta_material ... container, false);
Pasta
String[] pastaNames = new String[Pasta.pastas.length]; Material
Fragment.java
for (int i = 0; i < pastaNames.length; i++) {
pastaNames[i] = Pasta.pastas[i].getName();
}

int[] pastaImages = new int[Pasta.pastas.length];


for (int i = 0; i < pastaImages.length; i++) {
pastaImages[i] = Pasta.pastas[i].getImageResourceId();
} Używamy adaptera
CaptionedImagesAdapter, który
napisaliśmy już wcześniej. Do adaptera przekazujemy
dapter
CaptionedImagesA......... adapter = nazwy i zdjęcia dań.

CaptionedImagesAdapter
new ...... (pastaNames, pastaImages );
pastaRecycler.setAdapter(adapter);

ager LinearLayoutManager
LinearLayoutMan...... layoutManager = new ............ ..(getActivity());
pastaRecycler.setLayoutManager(layoutManager);
return pastaRecycler;
} Do wyświetlenia widoków CardView
w formie liniowej listy użyjemy ager.
} menedżera układu LinearLayoutMan

Te magnesiki nie
były potrzebne.
LinearLayout

LinearLayout ArrayAdapter ArrayAdapter

jesteś tutaj  625


Gdzie jesteśmy?

Dokąd dotarliśmy?
Poniżej zamieściliśmy krótkie podsumowanie tego, co już udało się nam zrobić w naszej aplikacji:

1 Po uruchomieniu aplikacji wyświetlana jest aktywność MainActivity.


Ta aktywność używa pliku układu activity_main.xml i ma szufladę nawigacyjną. Kiedy użytkownik
kliknie jedną z opcji prezentowanych w tej szufladzie, aplikacja wyświetli odpowiedni fragment.

2 Gdy użytkownik kliknie opcję Pizze, zostanie wyświetlony fragment PizzaMaterialFragment.


Fragment PizzaMaterialFragment zawiera widok RecyclerView.

3 Fragment PizzaMaterialFragment używa adaptera, CaptionedImagesAdapter,


pozwalającego na wyświetlanie widoków CardView prezentujących zdjęcie i nazwę pizzy.
Postać zawartości widoków CardView określa plik układu card_captioned_image.xml.

<Layout>

</Layout>

<Layout>
Top
Fragment.java fragment_
</Layout>
top.xml
activity_main.xml

1 Pasta
Fragment.java
Utworzyliśmy już
MainActivity.java wszystkie te pliki.
<Layout>
Urządzenie
Stores </Layout>
2
Fragment.java card_
CaptionedImages captioned_
3 Adapter.java image.xml
OrderActivity.java
<Layout>

</Layout> PizzaMaterial
Fragment.java
activity_order.xml
Pizza.java
<Layout> PizzaDetail
Tego pliku jeszcze nie przygotowaliśmy.
</Layout> Activity.java Zajmiemy się tym w następnej kolejności
i zadbamy, by ta aktywność była
activity_pizza_ uruchamiana z poziomu fragmentu
detail.xml PizzaMaterialFragment.

Kolejną rzeczą, którą musimy się zająć, jest zapewnienie, by widok RecyclerView reagował na
kliknięcia i w odpowiedzi na kliknięcie pizzy uruchamiał aktywność PizzaDetailActivity.
Aktywność ta będzie wyświetlać szczegółowe informacje o wybranej pizzy.

A zatem w następnej kolejności zaimplementujemy aktywność PizzaDetailActivity.


626 Rozdział 14.
Material Design

Utworzenie aktywności PizzaDetailActivity


Aktywność PizzaDetailActivity wyświetla nazwę pizzy i jej zdjęcie.

Utwórz więc nową, pustą aktywność. Jej klasie nadaj nazwę PizzaDetailActivity, plik
układu nazwij activity_pizza_detail, a jako tytuł podaj Informacje szczegółowe. Następnie
zmodyfikuj zawartość pliku układu activity_pizza_detail.xml, zapisując w nim poniższy kod
— umieszcza on w układzie używanym przez aktywność dwa widoki: TextView i ImageView.

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
WloskieCoNieco
android:layout_height=”match_parent”
android:orientation=”vertical”
app/src/main
tools:context=”com.hfad.bitsandpizzas.PizzaDetailActivity”>
res
Układ aktywności
<TextView PizzaDetailActivity zawiera
android:id=”@+id/pizza_text” widoki TextView i ImageView. layout
W pierwszym z nich wyświetlimy
android:layout_width=”wrap_content” nazwę pizzy, a w drugim jej <xml>
</xml>
zdję cie.
android:layout_height=”wrap_content” activity_pizza_
android:textAppearance=”?android:attr/textAppearanceLarge” /> detail.xml

<ImageView
android:id=”@+id/pizza_image”
android:layout_height=”wrap_content”
android:layout_width=”match_parent”
android:adjustViewBounds=”true”/>
</LinearLayout>

Na następnej stronie zobaczymy, jaki powinien być kod


naszej nowej aktywności PizzaDetailActivity.

jesteś tutaj  627


Wymagania

Co musi robić aktywność PizzaDetailActivity?


Aktywność PizzaDetailActivity musi wykonywać kilka operacji:

 Podstawowym przeznaczeniem aktywności PizzaDetailActivity jest wyświetlanie nazwy i zdjęcia


pizzy wybranej przez użytkownika. W tym celu pobierzemy identyfikator tej pizzy z intencji, która
doprowadziła do uruchomienia aktywności. Informację tę będziemy przekazywać do aktywności
PizzaDetailActivity z aktywności PizzaMaterialFragment w reakcji na kliknięcie jednej
z prezentowanych pizz.
 W rozdziale 9. utworzyliśmy plik zasobu menu opisujący elementy wyświetlane na pasku akcji.
Użyjemy teraz metody onCreateOptionsMenu(), aby umieścić te elementy na pasku akcji
aktywności PizzaDetailActivity.

 Plik zasobu menu opisuje akcję Udostępnij, której możemy użyć do udostępnienia informacji
o pizzy. Dodamy to tej akcji intencję, która pozwoli udostępnić nazwę pizzy wybranej przez
użytkownika.

 Plik zasobu menu zawiera także akcję Złóż zamówienie. Kiedy użytkownik kliknie tę opcję,
zostanie uruchomiona aktywność OrderActivity.
 Włączymy także w aktywności PizzaDetailActivity przycisk W górę, tak aby klikając go,
użytkownik mógł wrócić do aktywności MainActivity.

Aktualizacja pliku AndroidManifest.xml


Prace nad nową aktywnością zaczniemy od modyfikacji pliku AndroidManifest.xml
i zaznaczenia, że aktywnością nadrzędną nowej aktywności PizzaDetailActivity
jest MainActivity. Dzięki temu, kiedy użytkownik kliknie przycisk W górę,
gdy będzie wyświetlona aktywność PizzaDetailActivity, ponownie zostanie
uruchomiona aktywność MainActivity:

<activity
android:name=”.PizzaDetailActivity” WloskieCoNieco

android:label=”@string/title_activity_pizza_detail”
android:parentActivityName=”.MainActivity”> app/src/main
<xml>
</activity> </xml>
MainActivity jest aktywnością
nadrzędną aktywności AndroidManifest.xml
PizzaDetailActivity.

Skoro to mamy z głowy, przyjrzymy się teraz, jak widok RecyclerView ma


reagować na kliknięcia.

628 Rozdział 14.


Material Design

Kod pliku PizzaDetailActivity.java


Oto kompletny kod pliku PizzaDetailActivity.java (nie przejmuj się, jeśli wyda
Ci się, że to dużo — cały ten kod widziałeś już wcześniej):

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.app.Activity;
import android.content.Intent;
app/src/main
import android.os.Bundle;
Używamy tych klas.
import android.view.Menu; java
import android.view.MenuItem;
import android.widget.ImageView; com.hfad.wloskieconieco
import android.widget.ShareActionProvider;
import android.widget.TextView; PizzaDetailActivity.java

public class PizzaDetailActivity extends Activity {

private ShareActionProvider shareActionProvider;


Tej stałej będziemy używali do
public static final String EXTRA_PIZZANO = ”pizzaNo”; przekazywania identyfikatora pizz
y
w intencji jako informacji dodatkow
ej.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pizza_detail);

// Włączamy przycisk W górę


górę.
getActionBar().setDisplayHomeAsUpEnabled(true); To wywołanie włącza przycisk W

// Wyświetlamy szczegółowe informacje o pizzy


int pizzaNo = (Integer)getIntent().getExtras().get(EXTRA_PIZZANO);
String pizzaName = Pizza.pizzas[pizzaNo].getName();
Pobieramy z intencji
TextView textView = (TextView)findViewById(R.id.pizza_text); identyfikator pizzy
textView.setText(pizzaName); klikniętej przez
użytkownika.
int pizzaImage = Pizza.pizzas[pizzaNo].getImageResourceId();
ImageView imageView = (ImageView)findViewById(R.id.pizza_image);
imageView.setImageDrawable(getResources().getDrawable(pizzaImage));
imageView.setContentDescription(pizzaName);
} Używamy identyfikatora pizzy do
wyświetlenia danych w widokach
TextView i ImageView.

jesteś tutaj  629


Kod aktywności PizzaDetailActivity

Kod aktywności PizzaDetailActivity (ciąg dalszy)


@Override
public boolean onCreateOptionsMenu(Menu menu) { Dodajemy elementy z tego pliku
getMenuInflater().inflate(R.menu.menu_main, menu); zasobu menu do paska akcji.

// Udostępniamy nazwę pizzy


TextView textView = (TextView)findViewById(R.id.pizza_text);
CharSequence pizzaName = textView.getText();
MenuItem menuItem = menu.findItem(R.id.action_share);
shareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(”text/plain”);
intent.putExtra(Intent.EXTRA_TEXT, pizzaName);
WloskieCoNieco
shareActionProvider.setShareIntent(intent);
return true;
app/src/main
} Ustawiamy domyślny tekst jako
treść, której użyje akcja Udostępnij.
java
@Override
public boolean onOptionsItemSelected(MenuItem item) { com.hfad.wloskieconieco
switch (item.getItemId()) {
case R.id.action_create_order:
PizzaDetail
Intent intent = new Intent(this, OrderActivity.class); Activity.java
startActivity(intent);
return true; Uruchamiamy aktywność OrderActivity
po kliknięciu przycisku na pasku akcji.
default:
return super.onOptionsItemSelected(item);
}
}
}

Kiedy już uaktualnisz kod pliku PizzaDetailActivity.java, zajmiemy się


zaimplementowaniem reagowania widoku RecyclerView na kliknięcia.

630 Rozdział 14.


Material Design

Obsługa kliknięć w widoku RecyclerView


Chcemy, aby elementy wyświetlane w widoku RecyclerView reagowały na
kliknięcia, a konkretnie aby po kliknięciu którejś z prezentowanych pizz była
uruchamiana aktywność PizzaDetailActivity.

W przypadku tworzenia listy nawigacyjnej przy użyciu standardowego


widoku listy, aby zapewnić jej możliwość reagowania na kliknięcia, należy
utworzyć obiekt klasy OnItemClickListener. Widok listy nasłuchuje zdarzeń
generowanych przez poszczególne elementy listy i jeśli użytkownik kliknie
którykolwiek z nich, to zostanie wywołana metoda onItemClick() obiektu
OnItemClickListener. A to oznacza, że kod, który musimy napisać,
by reagować na kliknięcia elementów list, jest bardzo krótki.

Widoki list zapewniają takie możliwości, gdyż dziedziczą je po bardzo


rozbudowanej hierarchii klas bazowych. W odróżnieniu jednak od nich widok
RecyclerView nie ma tak bogatego zbioru wbudowanych metod, ponieważ nie
ma on tych samych klas bazowych co zwyczajny widok listy — ListView.

android.view.View
...

Obie klasy, ListView


i RecyclerView,
dziedziczą po klasach android.view.ViewGroup
View i ViewGroup.
...

android.widget. android.support.v7.widget.
AdapterView RecyclerView
... ...
Klasa ListView
dziedziczy także
po klasach
AdapterView Klasa RecyclerView nie
i AbsListView. android.widget. dziedziczy metod po żadnej
AbsListView innej klasie bazowej.

...

android.widget.ListView
...

Choć taka struktura klas bazowych zapewnia widokowi RecyclerView większą


elastyczność, to jednocześnie oznacza, że stosując go, będziemy musieli wykonać więcej
pracy samodzielnie. Jak więc sprawić, by widok RecyclerView reagował na kliknięcia?
jesteś tutaj  631
Nasłuchiwanie zdarzeń widoków

Do nasłuchiwania zdarzeń generowanych przez widoki


można użyć adaptera
Jeśli chcemy, by widok RecyclerView odpowiadał na kliknięcia, to musimy sami napisać niezbędny
kod. Aby napisać kod obsługujący zdarzenia, musimy mieć dostęp do widoków wyświetlanych
w widżecie RecyclerView. A jak możemy go uzyskać?

Wszystkie widoki są tworzone w klasie CaptionedImagesAdapter. Kiedy widok jest wyświetlany na


ekranie, RecyclerView wywołuje metodę onBindViewHolder(), aby zawartość widoku CardView
odpowiadała informacjom danego elementu listy.

Na przykład załóżmy, że chcemy, by za każdym razem, gdy użytkownik kliknie kartę pizzy, była
uruchamiana aktywność prezentująca szczegółowe informacje o tej pizzy. A to oznacza, że
moglibyśmy umieścić w klasie adaptera jakiś kod, który by uruchamiał odpowiednią aktywność:
class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder> {
...

@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
CardView cardView = holder.cardView;
ImageView imageView = (ImageView)cardView.findViewById(R.id.info_image);
Drawable drawable = cardView.getResources().getDrawable(imageIds[position]);
imageView.setImageDrawable(drawable);
TextView textView = (TextView)cardView.findViewById(R.id.info_text);
textView.setText(captions[position]);
cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(container.getContext(),
PizzaDetailActivity.class);
intent.putExtra(PizzaDetailActivity.EXTRA_PIZZANO, position);
container.getContext().startActivity(intent);
} magesAdapter
Dodanie tego kodu do adaptera CaptionedI
}); zostanie
sprawi, że po kliknięciu widoku CardView
} uruchomiona aktywność PizzaDetailActivity.
}
Niemniej jednak sama możliwość napisania takiego kodu nie oznacza
jeszcze, że powinniśmy go stosować.
WYSIL
SZARE KOMÓRKI
Moglibyśmy reagować na kliknięcia, dodając odpowiedni kod do klasy adaptera.
Ale czy jest jakiś powód, dla którego byłoby to niepożądane rozwiązanie?

632 Rozdział 14.


Material Design

Warto zadbać o możliwość wielokrotnego stosowania adapterów


Obsługując zdarzenia kliknięć w klasie adaptera CaptionedImagesAdapter,
ograniczylibyśmy możliwości jego stosowania. Zastanówmy się nad aplikacją, którą
tworzymy. Chcemy w niej wyświetlać listę pizz, dań z makaronu oraz restauracji.
Można sądzić, że w każdym z tych przypadków będziemy chcieli wyświetlić
listę obrazków z jakimś podpisem. Gdybyśmy zmodyfikowali kod adaptera
CaptionedImagesAdapter w taki sposób, że kliknięcie elementu listy zawsze
powodowałoby uruchomienie aktywności wyświetlającej szczegółowe informacje
o wybranej pizzy, to nie moglibyśmy go użyć do wyświetlania list dań z makaronu
ani restauracji — musielibyśmy utworzyć dla nich odrębne adaptery.

Separacja adaptera od interfejsu


Z tego względu kod odpowiadający za uruchomienie aktywności umieścimy poza
adapterem. Chcemy, aby w momencie kliknięcia elementu listy adapter odwołał
się do fragmentu zawierającego tę listę i aby kod tego fragmentu zgłosił intencję
uruchamiającą następną aktywność. Dzięki takiemu rozwiązaniu będziemy w stanie
zastosować ten sam adapter, CaptionedImagesAdapter, do wyświetlania listy
pizz, dań z makaronu oraz restauracji, a decyzję dotyczącą tego, co ma się stać
po kliknięciu elementu danej listy, pozostawić w gestii fragmentu.

Zastosujemy przy tym podobny wzorzec, którego użyliśmy już wcześniej w celu
oddzielenia fragmentu od aktywności — w klasie CaptionedImagesAdapter
utworzymy interfejs Listener, taki jak ten poniżej:

public static interface Listener {


public void onClick(int position);
}

Za każdym razem, gdy zostanie kliknięty jeden z widoków CardView wyświetlanych


na liście, wywołamy metodę onClick() tego interfejsu. Następnie zaimplementujemy
nasz interfejs Listener w klasie PizzaMaterialFragment; dzięki temu ten fragment
będzie mógł reagować na kliknięcia i uruchamiać wybraną aktywność.

Poniżej opisaliśmy jak to rozwiązanie będzie działało w praktyce:

1 Użytkownik klinie widok CardView wyświetlony w widżecie RecyclerView.

2 Zostanie wywołana metoda onClick() obiektu implementującego interfejs


Listener.

3 Metoda onClick() jest zaimplementowana we fragmencie


PizzaMaterialFragment. Jej kod uruchamia aktywność
PizzaDetailFragment.

Zacznijmy od zmodyfikowania kodu adaptera CaptionedImagesAdapter.

jesteś tutaj  633


Dodanie interfejsu

Dodanie interfejsu do adaptera


Zmodyfikowaliśmy kod adaptera zapisany w pliku CaptionedImagesAdapter.java —
dodaliśmy do niego interfejs Listener i kod, który wywołuje metodę onClick() tego
interfejsu w odpowiedzi na kliknięcie każdego z prezentowanych widoków CardView
(wprowadź wyróżnione zmiany w kodzie swojego projektu i zapisz plik):

package com.hfad.wloskieconieco;

WloskieCoNieco
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView; app/src/main
import android.view.LayoutInflater;
import android.view.View;
Używamy tej dodatkowej klasy, java
import android.view.ViewGroup; więc musimy ją zaimportować.
import android.support.v7.widget.CardView; com.hfad.wloskieconieco
import android.widget.ImageView;
import android.widget.TextView; CaptionedImages
Adapter.java

class CaptionedImagesAdapter extends RecyclerView.Adapter<CaptionedImagesAdapter.ViewHolder> {

private String[] captions;


private int[] imageIds;
Dodajemy obiekt Listener jako
private Listener listener; zmienną prywatną.

To jest interfejs.
public static interface Listener {
public void onClick(int position);
}

public static class ViewHolder extends RecyclerView.ViewHolder {


private CardView cardView;
public ViewHolder(CardView v) {
super(v);
cardView=v;
} Tych metod nie trzeba zmieniać.

public CaptionedImagesAdapter(String[] captions, int[] imageIds){


this.captions = captions;
this.imageIds = imageIds;
}

634 Rozdział 14.


Material Design

Kod adaptera CaptionedImagesAdapter (ciąg dalszy)


public void setListener(Listener listener) {
this.listener = listener; ać tej
Aktywności i fragmenty będą używ kty
obie
} metody by rejestrować się jako
nasłuchujące

@Override
public CaptionedImagesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
CardView cv = (CardView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_captioned_image, parent, false);
return new ViewHolder(cv);
}

@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
CardView cardView = holder.cardView;
ImageView imageView = (ImageView)cardView.findViewById(R.id.info_image);
Drawable drawable = cardView.getResources().getDrawable(imageIds[position]);
imageView.setImageDrawable(drawable);
imageView.setContentDescription(captions[position]);
TextView textView = (TextView)cardView.findViewById(R.id.info_text);
textView.setText(captions[position]);
cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) { WloskieCoNieco
listener.onClick(position);
} app/src/main
} Kliknięcie widoku CardView
}); spowoduje wywołanie metody java
onClick() obiektu implementująceg
} interfejs Listener. o
com.hfad.wloskieconieco

@Override
public int getItemCount() { CaptionedImages
Adapter.java
return captions.length;
}
}

Skoro już dodaliśmy do adaptera interfejs Listener, musimy go


zaimplementować w klasie PizzaMaterialFragment.

jesteś tutaj  635


Kod fragmentu PizzaMaterialFragment

Implementacja interfejsu Listener


we fragmencie PizzaMaterialFragment
Interfejs Listener zadeklarowany w klasie CaptionedImagesAdapter zaimplementujemy
w klasie fragmentu PizzaMaterialFragment, dzięki czemu kliknięcie którejś
z kart wyświetlanych w widoku RecyclerView spowoduje uruchomienie aktywności
PizzaDetailActivity. Oto zmodyfikowany kod fragmentu:

package com.hfad.wloskieconieco;

import android.app.Fragment; Używamy klasy Intent, by


import android.content.Intent; uruchomić aktywność, więc
musimy ją zaimportować.
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager; WloskieCoNieco
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; app/src/main
import android.view.View;
import android.view.ViewGroup; java

com.hfad.wloskieconieco
public class PizzaMaterialFragment extends Fragment {

@Override PizzaMaterial
Fragment.java
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RecyclerView pizzaRecycler = (RecyclerView)inflater.inflate(
R.layout.fragment_pizza_material, container, false);

String[] pizzaNames = new String[Pizza.pizzas.length];


for (int i = 0; i < pizzaNames.length; i++) {
pizzaNames[i] = Pizza.pizzas[i].getName(); W tym kodzie nic się
nie zmienia.
}

int[] pizzaImages = new int[Pizza.pizzas.length];


for (int i = 0; i < pizzaImages.length; i++) {
pizzaImages[i] = Pizza.pizzas[i].getImageResourceId();
}

636 Rozdział 14.


Material Design

Kod fragmentu PizzaMaterialFragment (ciąg dalszy)


CaptionedImagesAdapter adapter =
new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
pizzaRecycler.setLayoutManager(layoutManager);

adapter.setListener(new CaptionedImagesAdapter.Listener() {
public void onClick(int position) {
Intent intent = new Intent(getActivity(), PizzaDetailActivity.class);
intent.putExtra(PizzaDetailActivity.EXTRA_PIZZANO, position);
getActivity().startActivity(intent);
}
WloskieCoNieco
}); metody
Ten kod stanowi implementację onanie
onCl ick() inte rfejs u List ener . Wyk
return pizzaRecycler;
tej metody powoduje uruchomienie app/src/main
} aktywności PizzaDetailActivity a pizzy
} i przekazanie do niej identyfikator
wybranej przez użytkownika. java

To już cały kod konieczny, by sprawić, żeby widoki wyświetlone w widżecie


com.hfad.wloskieconieco
RecyclerView reagowały na kliknięcia. Dzięki zastosowaniu takiego rozwiązania
możemy użyć tego samego adaptera i widoku CardView zawierającego widok
obrazka i widok tekstowy do wyświetlania różnych rodzajów informacji PizzaMaterial
składających się ze zdjęcia i jego podpisu. Fragment.java

PizzaMaterialFragment.java

<Layout>
Każdy z tych
fragmentów
może używać </Layout>
tego samego
adaptera PastaMaterialFragment.java CaptionedImages card_captioned_
i widoku image.xml
CardView. Adapter.java

StoresMaterialFragment.java

A teraz zobaczmy, co się stanie po uruchomieniu aplikacji.


jesteś tutaj  637
Jazda próbna

Jazda próbna aplikacji


Uruchom aplikację, otwórz szufladę nawigacyjną i kliknij opcję Pizze.
Na ekranie zostanie wyświetlona lista kart, z których każda, tak jak
wcześniej, będzie prezentować jedną pizzę. Sprawdźmy teraz, co się
stanie, kiedy klikniesz jedną z pizz:

Kiedy klikniesz
pizzę, zostanie
uruchomiona aktywność
PizzaDetailActivity
ze szczegółowymi
informacjami
o wybranej pizzy.

Kiedy klikniesz opcję


Pizze, zostanie
wyświetlony fragment
PizzaMaterialFragment.

Widoki kart reagują na kliknięcia — uruchamiają aktywność


PizzaDetailActivity.

Jest jeszcze jedna rzecz, której musimy się przyjrzeć: treści


wyświetlane we fragmencie TopFragment.

638 Rozdział 14.


Material Design

Umieszczenie treści na samym początku


Kiedy po raz pierwszy przedstawialiśmy projekt naszej aplikacji dla Włoskiego Co
Nieco, fragment TopFragment zawierał listę opcji nawigacyjnych. Usunęliśmy je
z fragmentu dzięki zastosowaniu paska akcji oraz szuflady nawigacyjnej, przez co
fragment TopFragment został pozbawiony treści. Co zatem możemy w nim umieścić?

TopFragment jest naszym ekranem najwyższego poziomu — czyli pierwszym


ekranem naszej aplikacji, który użytkownik zobaczy po jej uruchomieniu. Powinien
on być przydatny i wartościowy zarówno dla nowych użytkowników, jak i osób,
które już wcześniej korzystały z aplikacji. Jednym ze sposobów by to zapewnić,
jest wyświetlenie na tym ekranie głównym wybranych treści.

Jeśli przyjrzysz się aplikacjom firmy Google dostępnym na Twoim urządzeniu


z Androidem, to zauważysz zapewne, że mają jedną cechę wspólną: zapewniają
możliwość błyskawicznego przejścia do najważniejszych treści aplikacji poprzez
zaprezentowanie niektórych z nich na ekranie głównym. Aplikacja Kalendarz
wyświetla nadchodzące wydarzenia. Inne aplikacje, takie jak Książki Play lub Muzyka
Play wyświetlają ostatnio wykonywane czynności oraz rekomendacje. To właśnie te
treści stanowią kluczowe elementu ekranów najwyższego poziomu tych aplikacji.

Aplikacja Książki
Play wyświetla Aplikacja
ostatnio pobrane kalendarza
książki i dodaje wyświetla
do nich sugestie nadchodzące
odnośnie innych wydarzenia,
książek, które dzięki czemu
mogłyby Ci się bez trudu można
spodobać. je zobaczyć.

W naszej aplikacji możemy postąpić podobnie — wystarczy, że wyświetlimy


we fragmencie TopFragment niektóre oferowane dania. Tak się szczęśliwie
składa, że bazując na informacjach zdobytych w tym rozdziale, możemy to zrobić
stosunkowo łatwo. jesteś tutaj  639
Ćwiczenie

Twoim zadaniem jest zmodyfikowanie fragmentu TopFragment w taki sposób,


by wyświetlał tekst stanowiący powitanie użytkownika oraz widok RecyclerView
prezentujący pizze. Zacznij od napisania kodu układu fragment_top.xml. Musisz sprawić,
Ćwiczenie by fragment TopFragment wyglądał tak jak na poniższym rysunku.

Oto, jak ma wyglądać


fragment TopFragment.

640 Rozdział 14.


Material Design

Zaostrz ołówek
Następnie dodaj do zaznaczonego poniżej obszaru pliku TopFragment.java kod,
dzięki któremu w widoku RecyclerView zostaną wyświetlone dwie pizze
w układzie siatki. Kiedy użytkownik kliknie którąś z wyświetlonych pizz, ma zostać
uruchomiona aktywność PizzaDetailActivity z informacjami o niej.

...
public class TopFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RelativeLayout layout = (RelativeLayout)
inflater.inflate(R.layout.fragment_top, container, false);

Tutaj
zapisz
swój
kod.

CaptionedImagesAdapter adapter =
new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
adapter.setListener(new CaptionedImagesAdapter.Listener() {
public void onClick(int position) {
Intent intent = new Intent(getActivity(), PizzaDetailActivity.class);
intent.putExtra(PizzaDetailActivity.EXTRA_PIZZANO, position);
getActivity().startActivity(intent);
}
}); Większość kodu będzie taka sam
a
w pliku PizzaMaterialFragment.jav jak
a.
return layout;
}
}

jesteś tutaj  641


Rozwiązanie ćwiczenia

Twoim zadaniem jest zmodyfikowanie fragmentu TopFragment w taki sposób,


by wyświetlał tekst stanowiący powitanie użytkownika oraz widok RecyclerView
prezentujący pizze. Zacznij od napisania kodu układu fragment_top.xml. Musisz sprawić,
Ćwiczenie by fragment TopFragment wyglądał tak jak na poniższym rysunku.
Rozwiązanie Nie przejmuj się, jeśli Twój kod będzie się trochę różnił od
naszego. Układ o podobnym wyglądzie można stworzyć na wiele
różnych sposobów.
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”

xmlns:tools=”http://schemas.android.com/tools”

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:paddingTop=”16dp”

android:paddingBottom=”16dp”

android:paddingRight=”16dp”

android:paddingLeft=”16dp”

tools:context=”.MainActivity”>

Ten element służy do


wyświetlania tekstu
<TextView powitania.

android:layout_width=”match_parent”

android:layout_height=”wrap_content”

android:text=”@string/welcome_text”

android:id=”@+id/welcome_text” /> A ten element


reprezentuje
widok
<android.support.v7.widget.RecyclerView RecyclerView.
android:id=”@+id/pizza_recycler”

android:scrollbars=”vertical”

android:layout_width=”match_parent”

android:layout_height=”wrap_content”

android:layout_below=”@+id/welcome_text”

android:layout_marginTop=”10dp”/>

</RelativeLayout>

642 Rozdział 14.


Material Design

Zaostrz ołówek
Rozwiązanie Następnie dodaj do zaznaczonego poniżej obszaru pliku TopFragment.java kod,
dzięki któremu w widoku RecyclerView zostaną wyświetlone dwie pizze
w układzie siatki. Kiedy użytkownik kliknie którąś z wyświetlonych pizz, ma zostać
uruchomiona aktywność PizzaDetailActivity z informacjami o niej.

...
public class TopFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RelativeLayout layout = (RelativeLayout)
inflater.inflate(R.layout.fragment_top, container, false);

RecyclerView pizzaRecycler = (RecyclerView)layout.findViewById(R.id.pizza_recycler);


String[] pizzaNames = new String[2];
for (int i = 0; i < 2; i++) {
pizzaNames[i] = Pizza.pizzas[i].getName();
Twój kod }
może Będziemy wyświetlać
wyglądać int[] pizzaImages = new int[2]; dwie pizze.
nieco
inaczej. for (int i = 0; i < 2; i++) {
Pizze wy
pizzaImages[i] = Pizza.pizzas[i].getImageResourceId(); składającświetlimy w układzie siatki
ym się z dwóch kolum
n.
}
GridLayoutManager layoutManager = new GridLayoutManager(getActivity(),2);
pizzaRecycler.setLayoutManager(layoutManager);

CaptionedImagesAdapter adapter =
new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
adapter.setListener(new CaptionedImagesAdapter.Listener() {
public void onClick(int position) {
Intent intent = new Intent(getActivity(), PizzaDetailActivity.class);
intent.putExtra(PizzaDetailActivity.EXTRA_PIZZANO, position);
getActivity().startActivity(intent);
}
}); Większość kodu jest taka sama jak
return layout; w pliku PizzaMaterialFragment.java.
}
}

jesteś tutaj  643


Plik układu fragment_top.xml

Kompletny kod pliku układu fragment_top.xml


Zmieniliśmy fragment TopFragment w taki sposób, by wyświetlał krótki tekst
stanowiący powitanie użytkownika i dwie pizze. Na kilku następnych stronach
pokażemy jego pełny kod.

Zaczniemy od dodania powitania do pliku strings.xml:

<string name=”welcome”>Oferujemy bogaty wybór świeżo wypiekanych pizz oraz dań z makaronu.
Przekonaj się, że warto spróbować naszych potraw!</string>

Następnie zaktualizuj plik fragment_top.xml, zapisując w nim następujący kod:

<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:paddingTop=”16dp”
android:paddingBottom=”16dp” WloskieCoNieco

android:paddingRight=”16dp”
app/src/main
android:paddingLeft=”16dp”
tools:context=”.MainActivity”>
res

<TextView layout
android:layout_width=”match_parent” <xml>
</xml>
android:layout_height=”wrap_content” fragment_top.xml
android:text=”@string/welcome_text”
android:id=”@+id/welcome_text” />
Do układu dodajemy widoki
<android.support.v7.widget.RecyclerView TextView i RecyclerView.
android:id=”@+id/pizza_recycler”
android:scrollbars=”vertical”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@+id/welcome_text”

android:layout_marginTop=”10dp”/>
</RelativeLayout>

Na następnej stronie przedstawimy kod pliku TopFragment.java.

644 Rozdział 14.


Material Design

Kompletny kod klasy TopFragment


package com.hfad.wloskieconieco;

WloskieCoNieco
import android.content.Intent;
import android.os.Bundle;
app/src/main
import android.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
java
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
com.hfad.wloskieconieco
import android.view.View; Używamy tych wszystkich klas.
import android.view.ViewGroup;
import android.widget.RelativeLayout; TopFragment.java

public class TopFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RelativeLayout layout = (RelativeLayout)
inflater.inflate(R.layout.fragment_top, container, false);
RecyclerView pizzaRecycler = (RecyclerView)layout.findViewById(R.id.pizza_recycler);
String[] pizzaNames = new String[2];
for (int i = 0; i < 2; i++) {
pizzaNames[i] = Pizza.pizzas[i].getName();
}
int[] pizzaImages = new int[2]; Tworzymy tablice z nazwami i zdję
ciami pizz.
for (int i = 0; i < 2; i++) {
pizzaImages[i] = Pizza.pizzas[i].getImageResourceId(); Wyświetlamy pizze w układzie siatki.
}
GridLayoutManager layoutManager = new GridLayoutManager(getActivity(),2);
pizzaRecycler.setLayoutManager(layoutManager);
CaptionedImagesAdapter adapter = new CaptionedImagesAdapter(pizzaNames, pizzaImages);
pizzaRecycler.setAdapter(adapter);
Do wyświetlenia pizz
adapter.setListener(new CaptionedImagesAdapter.Listener() { używamy adaptera.
public void onClick(int position) {
Intent intent = new Intent(getActivity(), PizzaDetailActivity.class);
intent.putExtra(PizzaDetailActivity.EXTRA_PIZZANO, position);
getActivity().startActivity(intent);
}
uruchamiamy
}); Kiedy użytkownik kliknie pizzę, kazując do niej
akty wnoś ć Pizz aDet ailA ctivi ty, prze
return layout; indeks klikn ięte j pizz y.
}
}
jesteś tutaj  645
Kolejna jazda próbna

Kolejna jazda próbna aplikacji


Zobaczmy, co się stanie po uruchomieniu naszej aplikacji.

Dwie pizze zostają wyświetlone


w układzie siatki. Po kliknięciu
pizzy zostają wyświetlone
szczegółowe informacje o niej.

Po uruchomieniu aplikacji zostaje wyświetlony fragment


TopFragment, prezentujący krótki tekst powitania i dwie
pizze. Po kliknięciu którejś z nich zostaje uruchomiona
aktywność PizzaDetailActivity prezentująca informacje
o wybranej pizzy.

646 Rozdział 14.


Material Design

Twój przybornik do Androida

Rozdział 14.
Opanowałeś już rozdział 14. i dodałeś
do swojego przybornika z narzędziami j
Pełny kod przykładowe
umiejętność korzystania z Material Design. aplikacji pre zen tow ane j
roz dzi ale mo żes z
w tym
pob rać z ser we ra FT P
wydawnictwa Helion:
klady/
ftp://ftp.helion.pl/przy
CELNE SPOSTRZEŻENIA andrrg.zi p

 Widoki CardView i RecyclerView zostały  Widok RecyclerView można dodać do układu,


umieszczone w swoich własnych bibliotekach używając elementu <android.support.
wsparcia. v7.widget.RecyclerView>. Atrybut
android:scrollbars pozwala dodawać do
 Aby dodać do układu widok CardView,
niego paski przewijania.
należy użyć elementu <android.support.
v7.widget.CardView>.  Do określania sposobu rozmieszczenia
elementów w widoku RecyclerView
 Aby w widoku CardView utworzyć zaokrąglone
służą menedżery układu. Menedżer
wierzchołki, wystarczy dodać do niego atrybut
LinearLayoutManager rozmieszcza
cardCornerRadius. Atrybut ten wymaga
elementy w formie liniowej listy, menedżer
także dodania przestrzeni nazw ”http://
GridLayoutManager w formie siatki,
schemas.android.com/apk/res-auto”.
a menedżer StaggeredGridLayoutManager
 Widok RecyclerView współpracuje w formie siatki, której poszczególne elementy
z adapterami, które są klasami pochodnymi klasy mogą mieć różne wielkości.
RecyclerView.Adapter.
 Tworząc własne klasy pochodne
RecyclerView.Adapter, trzeba zdefiniować
klasę ViewHolder i zaimplementować
jej metody onCreateViewHolder(),
onBindViewHolder() oraz
getItemsCount().

jesteś tutaj  647


Miłej zabawy!

Pożegnanie…

Miło było gościć Cię w Androidowie


Smutno nam, że odjeżdżasz, ale nie ma nic lepszego niż praktyczne zastosowanie zdobytej
wiedzy. W pozostałej części książki czeka na Ciebie jeszcze kilka małych perełek oraz indeks, który
także możesz przejrzeć. Później jednak nadejdzie najwyższy czas, by zebrać wszystkie nowe pomysły
i zastosować je w praktyce. Bon voyage!

648 Rozdział 14.


Dodatek A. ART
Środowisko uruchomieniowe
Androida

A więc to właśnie
to się dzieje pod
maską…

Aplikacje na Androida muszą działać na urządzeniach wyposażonych


w słabe procesory i bardzo małą pamięć.
Aplikacje pisane w Javie mogą potrzebować sporo pamięci, a ponieważ działają wewnątrz
własnej wirtualnej maszyny Javy (JVM), ich uruchomienie na komputerze o niewielkiej mocy
obliczeniowej może trwać dosyć długo. Android rozwiązuje ten problem, nie używając JVM do
uruchamiania aplikacji. Zamiast JVM używa całkowicie odmiennej wirtualnej maszyny, nazywanej
środowiskiem uruchomieniowym Androida (ang. Android Runtime, w skrócie ART). W tym
dodatku zobaczysz, jak to się dzieje, że ART umożliwia dobre działanie aplikacji napisanych
w Javie nawet na małych komputerach o niewielkiej mocy obliczeniowej.

to jest nowy dodatek  649


ART a JVM

Co to jest ART?
Środowisko uruchomieniowe Androida, określane skrótowo jako ART, to system, który wykonuje
skompilowane kody na urządzeniu działającym po kontrolą systemu operacyjnego Android.
Po raz pierwszy zostało ono wprowadzone w wersji KitKat Androida, a wraz z udostępnieniem
wersji Lollipop stało się standardowym sposobem wykonywania kodu.

ART zostało zaprojektowane dla zapewnienia szybkiego i wydajnego wykonywania


skompilowanych kodów aplikacji na Androida na urządzeniach o niewielkiej mocy obliczeniowej.

ART w ogromnym stopniu różni się od JVM


Język Java jest używany już od dawna, a skompilowane programy w nim pisane niemal zawsze działają
w wirtualnej maszynie Javy (Java Virtual Machine, w skrócie JVM) firmy Oracle. JVM symuluje układ
procesora i wczytuje skompilowane pliki klasowe (.class) zawierające kod maszynowy JVM, nazywany
także kodem bajtowym. Tradycyjnie pliki źródłowe w języku Java (.java) są kompilowane do plików
klasowych (.class). Te z kolei można uruchamiać, korzystając z interpretera JVM.

Kod źródłowy
napisany w Javie
jest kompilowany
do postaci plików
klasowych .class.
.java .class

ART działa zupełnie inaczej. Kiedy kompilujemy aplikację na Androida, wszystko rozpoczyna
się tak samo. Piszemy kod źródłowy, zapisując go w plikach .java, a te są kompilowane do
postaci plików klasowych (.class). Te pliki klasowe (.class, lub ich archiwum .jar) są następnie
konwertowane przez narzędzie o nazwie dx do postaci jednego pliku o nazwie classes.dex.

Proces budowy aplikacji


umieszcza wszystkie pliki
klasowe w jednym pliku
o nazwie classes.dex.

.java .class classes.dex

Plik classes.dex także zawiera kody bajtowe, różnią się one jednak od tych, które są zapisywane
w plikach .class. Kody bajtowe .dex są przeznaczone dla całkowicie innego procesora wirtualnego
o nazwie Dalvik. W rzeczywistości „dex” to skrót od słów Dalvik Executable1.

Procesor Dalvik nieco przypomina JVM. Zarówno JVM, jak i Dalvik są procesorami wirtualnymi
— to procesory teoretyczne. Jednak JVM firmy Oracle bazuje na stosie, a Dalvik na rejestrach.
Niektórzy wierzą, że kod przeznaczony na takie procesory bazujące na rejestrach możne
być mniejszy i działać szybciej. Dzięki konwertowaniu całego zestawu plików klasowych do
jednego pliku classes.dex program dx może sprawić, że skompilowana aplikacja będzie znacząco
mniejsza, gdyż jest w stanie usunąć wszystkie powtarzające się symbole, które normalnie mogłyby
występować w wielu różnych plikach .class.
1
Plik wykonywalny Dalvik — przyp. tłum.

650 Dodatek A
ART

Następnie plik classes.dex jest kompresowany wraz z grupą innych


zasobów i plików danych do postaci pliku ZIP, nazywanego pakietem
aplikacji lub plikiem APK. Plik .apk jest ostateczną, skompilowaną
aplikacją, którą można zainstalować na urządzeniu z Androidem.
To właśnie ten plik w końcu umieścimy w sklepie Google Play.

W ramach procesu Plik classes.dex


Pliki .java są jest następnie
Aplikacja konwertowane budowy wszystkie
jest tworzona te pliki .class są umieszczany
na wiele w archiwum ZIP
z wielu plików niezależnych umieszczane w jednym
źródłowych .java. pliku classes.dex. nazywanym pakietem
plików .class. aplikacji.

.java .class classes.dex .apk

Sposób wykonywania plików APK


Plik APK jest zwyczajnym archiwum ZIP. W ramach instalacji
na urządzeniu z Androidem plik ten jest zapisywany w katalogu
o nazwie /data/app/<nazwa_pakietu>, a następnie jest z niego
pobierany plik classes.dex.

Po pobraniu pliku classes.dex z pliku APK jest on konwertowany


do postaci natywnej biblioteki. Kody bajtowe Dalvik są
w trakcie tego procesu zmieniane na natywne instrukcje kodu
maszynowego, które mogą być wykonywane bezpośrednio
przez procesor urządzenia. Ta skompilowana biblioteka zostaje
zapisana w katalogu /data/dalvik-cache. Android musi wykonać tę
kompilację tylko jeden raz — podczas pierwszego uruchamiania
aplikacji. Później system ją jedynie wczytuje i uruchamia.

Android jest jedynie wersją systemu operacyjnego Linux, a Linux


Zygote
normalnie nie dysponuje możliwością wykonywania aplikacji
na Androida. To właśnie z tego względu na każdym urządzeniu
z Androidem działa proces o nazwie Zygote. Jest to jakby proces
Androida, który już jest przygotowany i działa. Kiedy każemy
Androidowi uruchomić jakąś aplikację, proces Zygote jest
rozwidlany. Ten rozwidlony proces jest zwyczajną kopią procesu
już istniejącego w pamięci. Linux potrafi rozwidlać procesy Chrome Kalendarz Coffeina
w bardzo wydajny sposób, dlatego dzięki rozwidlaniu procesu
Zygote i późniejszemu wczytaniu natywnej biblioteki aplikacje
na Androida mogą być wczytywane bardzo szybko.
Proces każdej aplikacji Dalvik jest
rozwidlonym procesem Zygote.
jesteś tutaj  651
Wydajność

Wydajność i wielkość
Urządzenia działające pod kontrolą systemu Android zazwyczaj mają
znacznie mniejszą moc obliczeniową i dysponują mniejszą pamięcią
niż komputery, które tradycyjnie były wykorzystywane do wykonywania
programów pisanych w języku Java. ART używa plików .dex, które
przeważnie są mniejsze od odpowiadających im plików klasowych
Javy .class. Wirtualna maszyna Javy (JVM) firmy Oracle potrafi
kompilować fragmenty interpretowanego kodu za pomocą techniki
kompilacji JIT (ang. Just-In-Time), co oznacza, że w trakcie
wykonywania aplikacji JVM konwertuje kody bajtowe Javy na kod
maszynowy. Takie rozwiązanie sprawdza się w przypadku aplikacji,
które mają działać przez długi okres czasu, takich jak aplikacje
serwerowe, jednak aplikacje na Androida mogą być często uruchamiane
i zatrzymywane. Dzięki wstępnemu skompilowaniu wszystkich kodów
bajtowych Dalvik do postaci natywnej biblioteki ART sprawia, że kod
aplikacji będzie musiał być kompilowany tylko jeden raz.

I jeszcze jedna uwaga: uruchomienie środowiska wykonawczego Javy


firmy Oracle na urządzeniach o niewielkiej mocy obliczeniowej może
zabierać sporo czasu. Jednak dzięki zastosowaniu procesu Zygote
Android jest w stanie uruchamiać aplikacje znacznie szybciej. Zygote
może także korzystać z pamięci współdzielonej, aby bezpiecznie
wykonywać kod wspólny dla większej liczby procesów Dalvik.

Bezpieczeństwo
Na urządzeniach z Androidem można wykonywać kod pochodzący
od wielu różnych programistów, dlatego bardzo duże znaczenie ma
całkowite odizolowanie od siebie poszczególnych aplikacji. Bez niego
jedna aplikacja mogłaby naruszyć bezpieczeństwo dowolnej innej
aplikacji zainstalowanej na tym samym urządzeniu. Aby zapewnić
odpowiednią izolację aplikacji, Android wykonuje każdą z nich
w ramach odrębnego procesu, używając do tego automatycznie
generowanych kont użytkowników. Dzięki temu aplikacje mogą być
izolowane za pomocą mechanizmów zabezpieczeń systemu operacyjnego
Linux. W przypadku użycia JVM firmy Oracle każdy proces musiałby
być wykonywany za pomocą odrębnego procesu Javy, co zazwyczaj
powodowałoby zwiększenie ilości pamięci potrzebnej do wykonywania
kilku aplikacji.

652 Dodatek A
Dodatek B. ADB
Android Debug Bridge
Czy dziewczynie,
która ma już wszystko,
można sprawić lepszy prezent
niż narzędzie obsługiwane Najdroższy,
z poziomu wiersza poleceń? jakiż to uroczy
wyraz troski!

W tej książce skoncentrowaliśmy się na zaspokajaniu wszystkich potrzeb


związanych z pisaniem aplikacji na Androida z wykorzystaniem IDE.
Zdarzają się jednak sytuacje, w których zastosowanie narzędzi obsługiwanych z poziomu wiersza
poleceń jest po prostu przydatne. Mamy tu na myśli na przykład przypadki, gdy Android Studio
nie jest w stanie zauważyć urządzenia z Androidem, choć my wiemy, że ono istnieje. W tym
rozdziale przedstawimy Android Debug Bridge (w skrócie ADB) obsługiwany z poziomu
wiersza poleceń program narzędziowy, którego można używać do komunikacji z emulatorem
lub z rzeczywistymi urządzeniami zaopatrzonymi w Androida.

to jest nowy dodatek  653


ADB

adb — Twój kumpel z wiersza poleceń


Za każdym razem, gdy komputer, którego używamy do pisania aplikacji, musi
porozumieć się z urządzeniem z Androidem, niezależnie od tego, czy jest to realne
urządzenie podłączone kablem USB, czy też urządzenie wirtualne działające
w emulatorze, robi to za pomocą Android Debug Bridge (ADB). ADB jest
procesem kontrolowanym za pomocą programu o tej samej nazwie — adb.

Program adb jest przechowywany w podkatalogu platform-tools, w katalogu, w którym


został zainstalowany Android SDK. W przypadku komputerów Mac zapewne będzie go
można znaleźć w katalogu /Users/<nazwa_użytkownika>/Library/Android/sdk/platform-
tools. Jeśli dodamy katalog platform-tools do zmiennej systemowej PATH, to będziemy
mogli uruchamiać program adb z poziomu wiersza poleceń.

W oknie terminala lub wiersza poleceń można tego programu używać w następujący sposób:

Polecenie adb devices oznacza: „Powiedz, z jakimi urządzeniami z Androidem jesteś


połączony”. Polecenie adb komunikuje się z procesem serwera adb, który działa w tle.
Serwer adb jest czasami nazywamy także demonem adb lub adbd. Wpisanie w oknie
terminala polecenia adb powoduje przesłanie żądania na komputer lokalny na port
5037. Proces adbd oczekuje na polecenia przesyłane na ten port. Kiedy Android
Studio chce uruchomić aplikację, sprawdzić wpisy w dzienniku bądź też wykonać
jakąkolwiek inną operację wymagającą komunikacji z urządzeniem z Androidem,
robi to, przesyłając polecenia na port 5037.

adb adbd Urządzenie

polecenie adb proces


demona adb

Urządzenie
Kiedy adbd odbierze polecenie, przekazuje je do odrębnego procesu adbd,
działającego na odpowiednim urządzeniu z Androidem. Dzięki temu możliwe
będzie wprowadzanie zmian na urządzeniu lub zwracanie żądanych informacji.

654
ADB

Czasami, gdy serwer adb nie będzie działał, wykonanie polecenia adb spowoduje jego
uruchomienie:

I analogicznie, jeśli kiedykolwiek zdarzy się, że podłączymy urządzenie z Androidem do


komputera, a Android Studio go nie zauważy, to będziemy mogli ręcznie zakończyć działanie
serwera adb i go ponownie uruchomić:

Kończąc, a następnie uruchamiając serwer, zmuszamy adb, by ponownie nawiązało połączenie


ze wszystkimi urządzeniami z Androidem podłączonymi do komputera.

jesteś tutaj  655


ADB

Uruchamianie powłoki
W przeważającej większości przypadków nie korzystamy jednak z ADB w sposób bezpośredni —
pozwalamy raczej, by przeważającą większość operacji wykonywało za nas jakieś zintegrowane
środowisko programistyczne (IDE), takie jak Android Studio. Zdarzają się jednak takie sytuacje,
w których przyda się nam możliwość przejścia do wiersza poleceń i bezpośredniego operowania
na podłączonym urządzeniu.

Jednym z takich przypadków jest korzystanie z powłoki systemowej na urządzeniu:

Polecenie adb shell uruchomi interaktywną powłokę systemową działającą bezpośrednio na


urządzeniu z Androidem. Polecenie to będzie działało wyłącznie w przypadku, gdy do komputera
będzie podłączone jedno urządzenie, gdyż w przeciwnym razie polecenie nie będzie wiedziało,
na którym urządzeniu chcemy operować.

Po uruchomieniu powłoki systemowej można w niej wykonywać wiele standardowych poleceń


systemu Linux:

656
ADB

Pobieranie treści z dziennika logcat


Wszystkie aplikacje działające w systemie Android wysyłają wyniki do centralnego
strumienia o nazwie logcat. Bieżące komunikaty trafiające do niego można
oglądać, wykonując polecenie adb logcat:

Treści z dziennika logcat będą wyświetlane tak długo, aż tego nie przerwiemy.
Zastosowanie polecenia adb logcat może być przydatne, jeśli na przykład
chcemy zapisywać komunikaty z dziennika w pliku. Polecenie to jest używane
przez Android Studio do pobierania komunikatów, które następnie są
wyświetlane na karcie logcat.

Kopiowanie plików do i z urządzenia


Do przekazywania plików pomiędzy urządzeniem i komputerem służą polecenia
adb pull i adb push. Na przykład poniżej pokazaliśmy, jak można skopiować
plik właściwości /default.prop i zapisać go na komputerze jako plik 1.txt:

jesteś tutaj  657


ADB

I wiele, wiele więcej…


Dostępnych jest bardzo wiele poleceń, które można wykonywać, używając adb:
można kopiować i odtwarzać bazy danych (co jest bardzo przydatne w razie
konieczności zdiagnozowania problemów występujących w aplikacji korzystającej
z bazy danych), uruchomić serwer adb na innym porcie, wymusić ponowne
uruchomienie urządzenia czy też uzyskiwać wiele informacji na temat urządzenia.
Aby poznać wszystkie dostępne polecenia, wystarczy wpisać w wierszu poleceń adb,
jak na poniższym rysunku:

658
Dodatek C. Emulator
Emulator Androida
Czy to
może działać
b ?!

Czy miałeś kiedyś wrażenie, że cały swój czas spędzasz, czekając


na emulator?
Nie ma najmniejszych wątpliwości co do tego, że emulator Androida jest bardzo przydatny.
Dzięki niemu możemy się przekonać, jak nasza aplikacja będzie działała na urządzeniach innych
niż te, do których mamy fizyczny dostęp. Niekiedy jednak można odnieść wrażenie, że emulator
działa… wolno. W tym dodatku wyjaśnimy, dlaczego tak się dzieje. Ale to nie wszystko, damy Ci
bowiem także kilka wskazówek, jak przyspieszyć jego działanie.

to jest nowy dodatek  659


Szybkość

Dlaczego emulator działa tak wolno?


Pisząc aplikacje na Androida, bardzo wiele czasu będziemy tracić na
oczekiwaniu na uruchomienie emulatora i na instalowaniu kodu na AVD.
A dlaczego? Dlaczego emulator Androida działa tak woooollllnooooooo?
Jeśli miałeś okazję pisać kod na iPhone’y, to wiesz, jak szybko działa symulator
tego telefonu. Skoro coś jest możliwe w odniesieniu do iPhone’a, to czemu nie
jest w przypadku Androida?

Wskazówką pozwalającą uzyskać odpowiedź na to pytanie jest nazwa: symulator


iPhone’a i emulator Androida.

Symulator iPhone’a (ang. iPhone Symulator) symuluje urządzenie działające


pod kontrolą systemu operacyjnego iOS. Cały kod pisanej aplikacji jest
kompilowany tak, by mógł być natywnie wykonywany na komputerze Mac,
a symulator iPhone’a działa z pełną szybkością tego komputera. To z kolei
oznacza, że możliwe jest zasymulowanie procesu uruchamiania iPhone’a
w ciągu zaledwie kilku sekund.

Natomiast emulator Androida (ang. Android Emulator) działa w całkowicie


inny sposób. Używa on aplikacji open source o nazwie QEMU (lub Quick
Emulator), by emulować kompletne, sprzętowe urządzenie z Androidem.
Innymi słowy: wykonywany jest kod, który interpretuje kod maszynowy, który
docelowo ma być wykonywany na procesorze urządzenia. Inny kod emuluje
system składowania danych, ekran oraz praktycznie wszystkie inne komponenty
sprzętowe urządzenia z Androidem.

AVD AVD AVD AVD AVD

Wszystkie wirtualne urządzenia


Emulator QEMU z Androidem (AVD) są wykonywane
przez emulator QEMU.

Emulatory, takie jak QEMU, tworzą znacznie bardziej realistyczną


reprezentację wirtualnego urządzenia niż programy takie jak iPhone
Symulator, choć ich wadą jest to, że nawet w przypadku prostych operacji,
takich jak odczyt z dysku lub wyświetlenie czegoś na ekranie, muszą
wykonać nieporównywalnie więcej pracy. I to właśnie dlatego uruchomienie
AVD w emulatorze zabiera tak dużo czasu. Musi udawać, że jest każdym
najmniejszym komponentem sprzętowym wchodzącym w skład urządzenia,
i interpretować każdą instrukcję programu.

660 Dodatek C
Emulator

Jak przyspieszyć pisanie aplikacji na Androida?


1. Używać rzeczywistych urządzeń
Najprostszym sposobem przyspieszenia procesu tworzenia aplikacji jest korzystanie
z rzeczywistych urządzeń. Takie urządzenia uruchamiają się znacznie szybciej niż wirtualne
urządzenia działające w emulatorze i najprawdopodobniej pozwolą także na szybsze
instalowanie i działanie aplikacji. Chcąc używać rzeczywistego urządzenia, warto wejść do
Opcji programistycznych i zaznaczyć w nich przełącznik Pozostaw ekran włączony. Dzięki temu
urządzenie nie będzie blokować ekranu, co na pewno będzie wygodne przy wielokrotnym
instalowaniu aplikacji.

2. Używać zrzutu stanu emulatora


W przypadku stosowania emulatora jego uruchamianie jest bez wątpienia jedną z rzeczy,
która zajmuje najwięcej czasu. Jeśli zapiszemy stan urządzenia podczas jego pracy, to emulator
będzie w stanie szybko do niego wrócić, bez przechodzenia całego, długotrwałego procesu
uruchamiania systemu. Aby skorzystać z tej możliwości, otwórz menedżer AVD, wybierając
w menu Android Studio opcję Tools/Android/AVD Manager, przejdź do edycji wybranego AVD,
klikając ikonę Edit, a następnie zaznacz pole wyboru Store snapshot for faster startup.

To sprawi, że zostanie zapisany stan pamięci działającego AVD. Emulator będzie mógł
odtworzyć ten stan pamięci bez konieczności uruchamiania systemu na wirtualnym urządzeniu.

3. Korzystać z akceleracji sprzętowej


Domyślnie emulator QEMU musi interpretować każdą instrukcję kodu maszynowego
wirtualnego urządzenia. Oznacza to, że jest on bardzo elastyczny, gdyż może udawać, iż jest
jednym z wielu różnych rodzajów procesorów, lecz jednocześnie jest to jeden z podstawowych
powodów jego wolnego działania. Na szczęście jest sposób, by nasz komputer roboczy
wykonywał instrukcje kodu maszynowego bezpośrednio. Istnieją dwa główne typy AVD:
urządzenia ARM i urządzenia x86. Jeśli stworzysz urządzenie x86 z Androidem i w swoim
komputerze używasz jednego z konkretnych typów procesora z rodziny x86 firmy Intel, to
będziesz mógł skonfigurować emulator w taki sposób, by instrukcje maszynowe Androida były
wykonywane bezpośrednio na procesorze Intela.

W tym celu konieczne będzie zainstalowanie narzędzia firmy Intel o nazwie Hardware
Accelerated Execution Manager (w skrócie HAXM). Gdy pisaliśmy tę książkę, można go było
pobrać ze strony: A jeśli go przeniesiono
w inne miejsce, to szybkie
https://software.intel.com/en-us/android/articles/intel-hardware-accelerated-execution-manager wyszukiwanie powinno
pozwolić Ci go odnaleźć.
HAXM jest hipernadzorcą. Oznacza to, że przełącza procesor komputera w specjalny tryb
działania, pozwalający na bezpośrednie wykonywanie instrukcji wirtualnego komputera.
HAXM działa wyłącznie na procesorach Intela, które obsługują technologię Intel
Virtualization Technology. Jeśli Twój komputer jest z nią zgodny, to zastosowanie HAXM
sprawi, że AVD będą działały znacznie szybciej.

jesteś tutaj  661


662 Dodatek C
Dodatek D. Pozostałości
Dziesięć najważniejszych zagadnień
(których nie opisaliśmy)
O rany! Spójrz na
te apetyczne wątki,
które zostały…

Nawet po tym wszystkim, co opisaliśmy w tej książce, wciąż pozostaje


wiele innych interesujących zagadnień.
Jest jeszcze kilka dodatkowych spraw, o których musisz się dowiedzieć. Czulibyśmy się nie
w porządku, gdybyśmy je pominęli, a jednocześnie chcieliśmy oddać w Twoje ręce książkę,
którą dasz radę podnieść bez intensywnego treningu na siłowni. Dlatego zanim odłożysz
tę książkę, przeczytaj kilka dodatkowych zagadnień opisanych w tym dodatku.

to jest nowy dodatek  663


Dystrybucja

1. Rozpowszechnianie aplikacji
Kiedy już napiszemy swoją aplikację, zapewne zechcemy udostępnić ją
innym użytkownikom. Zazwyczaj będziemy chcieli to zrobić, używając
jednego z internetowych sklepów, takich jak Google Play.

Takie rozpowszechnianie aplikacji wiąże się z wykonaniem dwóch


podstawowych czynności: przygotowania aplikacji do udostępnienia
i udostępnienia jej.

Przygotowanie aplikacji do udostępnienia


Zanim będziemy mogli udostępnić aplikację, konieczne będzie jej
skonfigurowanie, zbudowanie oraz przetestowanie gotowej wersji.
Obejmuje to takie czynności jak wybór ikony aplikacji i zmodyfikowanie
pliku manifestu AndroidManifest.xml w taki sposób, by aplikację można
było pobierać tylko na te urządzenia, na których będzie ona działać.

Przed udostępnieniem aplikacji należy się także upewnić, że została ona


przetestowana na przynajmniej jednym tablecie i telefonie, aby mieć
pewność, iż wygląda ona zgodnie z oczekiwaniami, a jej wydajność jest
zadowalająca.

Dodatkowe informacje na temat przygotowywania aplikacji do wydania


można znaleźć na stronie:

http://developer.android.com/tools/publishing/preparing.html

Udostępnianie aplikacji
Ten etap obejmuje reklamowanie aplikacji, sprzedawanie jej
i rozpowszechnianie.

Aby udostępnić swoją aplikację w sklepie Google Play, konieczne jest


zarejestrowanie się, utworzenia konta programisty i użycie serwisu Developer
Console do opublikowania aplikacji. Szczegółowe informacje na ten temat
można znaleźć na stronie:

http://developer.android.com/distribute/googleplay/start.html

Aby uzyskać więcej informacji o najlepszych sposobach kierowania aplikacji


do odpowiednich użytkowników i wzbudzania zainteresowania nią, warto
zajrzeć na stronę:

http://developer.android.com/distribute/index.html

664 Dodatek D
Pozostałości

2. Dostawcy treści
Z tej książki dowiedziałeś się, jak używać intencji do uruchamiania aktywności
należących do innych aplikacji. Na przykład możemy uruchomić aplikację Wiadomości,
aby wysłać przekazany fragment tekstu. Ale co zrobić, gdybyśmy chcieli używać we
własnej aplikacji danych pochodzących z innej aplikacji? Co zrobić, gdybyśmy chcieli
używać we własnej aplikacji danych z aplikacji Kontakty albo dodać jakieś nowe
wydarzenie do Kalendarza?

Dostępu do danych innej aplikacji nie można uzyskać, przeglądając jej bazę danych.
Rozwiązaniem jest skorzystanie z dostawców treści (ang. content providers). Dostawca
treści to interfejs pozwalający aplikacji na udostępnianie danych w kontrolowany
sposób. Umożliwia on wykonywanie zapytań w celu odczytu danych, dodawania
nowych rekordów oraz aktualizacji lub usuwania rekordów już istniejących.

Jeśli chcesz uzyskać


dostęp do bazy danych innej
aplikacji, musisz skorzystać
z moich usług.

Nasza aktywność Dostawca Baza danych


treści

Jeśli chcemy, by inne aplikacje mogły używać naszych danych, to możemy


w tym celu utworzyć własnego dostawcę treści.

Więcej informacji na temat dostawców treści można znaleźć na stronie:

http://developer.android.com/guide/topics/providers/content-providers.html

Na poniższej stronie można natomiast znaleźć poradnik dotyczący stosowania


we własnych aplikacjach dostawcy treści aplikacji Kontakty:

http://developer.android.com/guide/topics/providers/contacts-provider.html

Podobny poradnik dotyczący stosowania dostawcy treści aplikacji Kalendarz


jest dostępny na stronie:

http://developer.android.com/guide/topics/providers/calendar-provider.html

jesteś tutaj  665


WebView

3. Klasa WebView
Jeśli chcemy zapewnić użytkownikom dostęp do treści pochodzących z internetu,
możemy to zrobić na dwa sposoby. Pierwszym jest napisanie aplikacji internetowej,
której użytkownicy będą mogli używać za pomocą przeglądarki WWW. Drugą
możliwością jest natomiast zastosowanie klasy WebView.

Klasa WebView pozwala na wyświetlanie zawartości stron WWW w układzie


aktywności. Można jej używać, aby udostępniać całą aplikację internetową jako
aplikację kliencką albo aby wyświetlać jej konkretne strony. Takie rozwiązanie jest
wygodne w przypadkach, gdy nasza aplikacja zawiera treści, które od czasu do czasu
mogą być modyfikowane, takie jak licencja lub instrukcja.

Aby dodać do układu widok WebView, należy użyć poniższego elementu:

<WebView xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/webview”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />

Tak utworzony widok informujemy o tym, którą stronę należy wyświetlić, używając
w kodzie aktywności metody loadUrl():

WebView webView = (WebView) findViewById(R.id.webview);


webView.loadUrl(”http://helion.pl/”);

Oprócz tego musimy zaznaczyć, że aplikacja wymaga uprawnień do dostępu do


internetu. W tym celu musimy dodać do pliku AndroidManifest.xml uprawnienie
INTERNET:

<manifest ...>
<uses-permission android:name=”android.permission.INTERNET” />
....
</manifest>

Więcej informacji na temat stosowania w aplikacjach treści pochodzących z internetu


można znaleźć na stronie:

http://developer.android.com/guide/webapps/index.html

666 Dodatek D
Pozostałości

4. Animacje
Wraz z coraz większym wykorzystywaniem mocy graficznych komponentów sprzętowych
instalowanych na urządzeniach z Androidem w coraz większym stopniu wykorzystywane
są także animacje, które umożliwiają poprawę atrakcyjności tworzonych aplikacji.

W systemie Android można wykonywać kilka różnych rodzajów animacji.

Animowanie właściwości
Animacje właściwości bazują na fakcie, że bardzo wiele aspektów wyglądu wizualnych
komponentów aplikacji na Androida opisywanych jest przy użyciu właściwości liczbowych.
Jeśli zmienimy wartość takiej właściwości widoku jak wysokość lub szerokość, to zyskamy
możliwość animowania tego widoku. Na tym właśnie polega animowanie właściwości:
płynnym zmienianiu właściwości komponentu wizualnego wraz z upływem czasu.

Animowanie widoków
Wiele animacji można tworzyć w sposób deklaratywny, zapisując je w postaci zasobów
XML. Możemy zatem przygotować pliki XML zawierające standardowy zestaw animacji
(takich jak skalowanie, przejścia lub obroty), aby utworzyć efekty, które następnie
będziemy mogli wywoływać z poziomu kodu aktywności. Wspaniałą cechą deklaratywnych
animacji widoków jest to, że są one całkowicie oddzielone od kodu Javy, dzięki czemu
bardzo łatwo można je kopiować z jednej aplikacji do drugiej.

Przejścia aktywności
Załóżmy, że napisaliśmy aplikację wyświetlającą listę elementów składających się z nazwy
i obrazka. Po kliknięciu wybranego elementu tej listy zostaje wyświetlony widok ze
szczegółowymi informacjami na jego temat. W takim przypadku aktywność prezentująca
informacje szczegółowe będzie zapewne wyświetlać ten sam obrazek, który wcześniej był
widoczny na liście.

Przejścia aktywności (ang. activity transitions) pozwalają na


animowanie wybranego widoku z jednej aktywności, który będzie
także prezentowany w następnej aktywności. Dzięki nim możemy
sprawić, że obrazek z listy będzie płynnie animowany i znajdzie
się w odpowiednim miejscu kolejnej aktywności. Taka animacja
zapewni wrażenie płynnego działania aplikacji.

Więcej informacji na temat animacji w Androidzie można


znaleźć na stronie:

https://developer.android.com/guide/topics/graphics/index.html

Z kolei szczegółowe informacje na temat przejść aktywności


można znaleźć na stronie:

https://developer.android.com/training/material/animations.html
jesteś tutaj  667
Mapy

5. Mapy
Urządzenie z Androidem może podróżować razem z nami, dlatego określanie położenia
i wyświetlanie map są bardzo ważnymi możliwościami wielu aplikacji firmy Google.

Jeśli zainstalujemy bibliotekę Google Play Services, to będziemy mogli używać


map Google bezpośrednio w naszej aplikacji. Będą one dysponowały wszystkimi
możliwościami, jakie mają aplikacje firmy Google, dzięki czemu będziemy mogli
dostosowywać je na wiele różnych sposobów, tak by stały się integralną częścią naszej
aplikacji.

Poniższy kod XML pozwala wstawić fragment mapy do układu:

<fragment xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/map”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:name=”com.google.android.gms.maps.MapFragment”/>

Następnie, w kodzie aktywności, możemy się do niej odwołać i używać jej jako obiektu
GoogleMap:

GoogleMap map = getMap();

Do takiej mapy możemy na przykład dodawać własne elementy.


Poniższy przykład dodaje do mapy linię łamaną:

routeLine = map.addPolyline(new PolylineOptions()


.width(ROUTE_THICKNESS_PIXELS)
.color(Color.RED));

To jest aplikacja, w której zostały


wykorzystane mapy Google.

To jest linia łamana.

668 Dodatek D
Pozostałości

5. Mapy (ciąg dalszy)


Możemy także obsługiwać w naszej aplikacji zdarzenia generowane przez
mapy. Jeśli zaimplementujemy interfejs OnCameraChangeListener, możemy
się dowiadywać, kiedy użytkownik przesunął mapę w inne miejsce, a gdy
zaimplementujemy interfejs OnMapClickListener, jesteśmy w stanie określić
współrzędne geograficzne klikniętego miejsca mapy:

map.setOnCameraChangeListener(new OnCameraChangeListener() {
@Override
public void onCameraChange(CameraPosition cameraPosition) {
// Mapę przeciągnięto w inne miejsce
}
});

map.setOnMapClickListener(new OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
// Kliknięto w miejscu o współrzędnych określonych w obiekcie latLng
}
});

Więcej informacji o mapach Google i o możliwościach integrowania ich z własnymi


aplikacjami na Androida można znaleźć na stronie:

https://developer.android.com/training/maps/index.html

6. Klasa CursorLoader
Jeśli tworzona aplikacja w bardzo dużym stopniu używa bazy danych i dostawców
treści, to wcześniej czy później spotkamy się z obiektami CursorLoader.
Umożliwiają one asynchroniczne wykonywanie zapytań w tle i zwracają wyniki
do aktywności lub fragmentu, który je wywołał. Zarządzają one kursorami, dzięki
czemu nie musimy tego robić samodzielnie. Oprócz tego obiekty CursorLoader
mogą nas powiadamiać o modyfikacji danych, co pozwoli nam odpowiednio na
to zareagować w prezentowanych widokach.

Więcej informacji na temat klasy CursorLoader i jej zastosowań można znaleźć


na stronie:

https://developer.android.com/training/load-data-background/setup-loader.html

jesteś tutaj  669


Odbiorcy komunikatów

7. Odbiorcy komunikatów
Załóżmy, że chcielibyśmy, by nasza aplikacja w jakiś sposób reagowała na
wystąpienie konkretnego zdarzenia systemowego. Na przykład napisaliśmy
aplikację do odtwarzania muzyki i chcemy zatrzymywać odtwarzanie
w przypadku odłączenia słuchawek od urządzenia. W jaki sposób aplikacja
może wykryć takie zdarzenie?

Zdarzenia systemowe informują o takich sytuacjach jak osiągnięcie niskiego


poziomu naładowania baterii, nadchodzące połączenie telefoniczne czy też
ponowne uruchamianie systemu. Android rozgłasza te zdarzenia w momencie
ich wystąpienia, a nasza aplikacja może je odbierać i obsługiwać, korzystając
z tak zwanych odbiorców komunikatów. Odbiorcy komunikatów pozwalają
subskrybować i odbierać konkretne rodzaje komunikatów, a to oznacza, że
nasza aplikacja może reagować na zdarzenia systemowe.

Hej, OK, w porządku. Chwilowo


Bateria już aktywność! Android zatrzymam więc wszystkie
jest prawie właśnie powiedział, że operacje, które mogą
rozładowana… jeśli bateria już jest prawie powodować zwiększone
to kogoś interesuje. rozładowana. zużycie prądu.

Odbiorca Nasza aktywność


Android
komunikatów

Więcej informacji na ten temat można znaleźć na stronie:

http://developer.android.com/reference/android/content/BroadcastReceiver.html

670 Dodatek D
Pozostałości

8. Widżety aplikacji
Widżet aplikacji to niewielki widok aplikacji, który można dodać do innej
aplikacji lub umieścić na ekranie głównym. Zapewnia on bezpośredni dostęp
do podstawowych treści lub funkcjonalności aplikacji z poziomu ekranu
głównego bez konieczności uruchamiania tej aplikacji.

Oto przykład takiego widżetu:

To jest widżet aplikacji.


Zapewnia on bezpośredni
dostęp do jej podstawowych
możliwości funkcjonalnych.

Aby utworzyć widżet aplikacji, trzeba zaimplementować obiekty klasy


AppWidgetProviderInfo i AppWidgetProvider oraz przygotować
odpowiedni widok. Obiekt klasy AppWidgetProviderInfo opisuje metadane
widżetu, takie jak nazwa używanej przez widżet klasy AppWidgetProvider
i nazwa jego pliku układu. Informacje te są określane w formie kodu XML.
Natomiast implementacja klasy AppWidgetProvider zawiera metody
niezbędne do korzystania z widżetu. Z kolei plik układu to standardowy plik
XML określający wygląd widżetu.

Więcej informacji na temat tworzenia widżetów aplikacji można znaleźć


na stronie:

http://developer.android.com/guide/topics/appwidgets/index.html

jesteś tutaj  671


Elastycznie

9. Grafika 9-patch
Grafika 9-patch (lub NinePatch) to elastyczne mapy bitowe, których można
używać do określania tła widoków. Rozmiary tych obrazów są automatycznie
dostosowywane do wielkości widoku i ekranu. Najciekawsze w nich jest to,
że mamy możliwość określania, które obszary mają być rozciągane, a które nie.

Na przykład załóżmy, że chcielibyśmy użyć poniższego obrazka jako tła


przycisku:

Zależy nam przy tym, by wymiary obrazka mogły być elastycznie


dostosowywane do tekstów o różnej długości, lecz jednocześnie by nie
powodowało to zniekształceń krawędzi obrazka:

Chcemy, aby
wielkość tego Wielkość tego obszaru
obszaru była może być modyfikowana
elastycznie wyłącznie w pionie. Jego
modyfikowana. szerokość musi pozostać
taka sama.

modyfikowana
Wielkość tego obszaru może być aby był on
pozi omie . Nie chce my,
wyłącznie w ałoby jego
gdyż to spow odow
wyższy lub niższy,
zniekształcenie.

Jeśli zmienimy nasz obrazek w grafikę 9-patch, to jego rozmiary będzie można zmieniać
dokładnie w taki sposób, w jaki byśmy chcieli.

Android udostępnia narzędzie o nazwie Draw 9-patch tool, które pomaga w tworzeniu
grafiki 9-patch. Więcej informacji na temat tego programu i samej grafiki 9-patch można
znaleźć na stronie:

http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch

672 Dodatek D
Pozostałości

10. Testowanie
Nowoczesne sposoby tworzenia oprogramowania w dużym stopniu opierają się na
testowaniu, a Android udostępnia wiele wbudowanych mechanizmów wspomagających
testowanie. Ponieważ podstawowym językiem używanym do pisania aplikacji na Androida
jest Java, można używać standardowych frameworków do testowania programów pisanych
w Javie, ale Android idzie nawet dalej i udostępnia framework do testowania wbudowany
bezpośrednio w SDK. Co więcej, podczas tworzenia każdego nowego projektu Android
Studio automatycznie tworzy hierarchię plików przeznaczoną do tworzenia testów.

Testy aplikacji na Androida bazują na frameworku JUnit, rozszerzonym o dodatki


stworzone specjalnie pod kątem systemu Android. Podstawowe testy komponentów
można tworzyć w oparciu o klasę AndroidTestCase. Framework zawiera także atrapy
takich obiektów jak Intent i Context, które znacznie ułatwiają testowanie pojedynczych
komponentów.

Dostępna jest również specjalna klasa testowa, ApplicationTestCase, przydatna,


gdy konieczne jest sprawdzenie, czy podstawowa konfiguracja aplikacji podana w pliku
AndroidManifest.xml jest prawidłowa.

Najbardziej fascynującym elementem podstawowego frameworku testowego są


testy oprzyrządowania (ang. Instrumentation Testing). Aplikacje na Androida można
oprzyrządować, co daje możliwość monitorowania i modyfikowania interakcji pomiędzy
wybranym komponentem aplikacji i systemem operacyjnym. Oznacza to, że bezpośrednio na
urządzeniu można wykonywać testy, które będą wywoływać metody cyklu życia aktywności
i przekazywać intencje do systemu operacyjnego.

Więcej informacji na temat frameworku do testowania aplikacji na Androida można znaleźć


na stronie:

http://developer.android.com/tools/testing/testing_android.html

Jeśli kogoś interesuje bardziej zaawansowane testowanie w oparciu o scenariusze,


to powinien zainteresować się frameworkiem testowym Robotium. Bazuje on na
podstawowym frameworku do testowania aplikacji na Androida, lecz przenosi go na
całkowicie nowy poziom. Korzystając z niego, można pisać kod, który wygląda niemal
jak skrypty testowe wykonywane przez testerów.

Więcej informacji o Robotium można znaleźć na stronie:

https://github.com/robotiumtech/robotium

jesteś tutaj  673


Skorowidz

A Android Studio, 4, 5, 7 android:layout_alignBottom, 171


activity, Patrz: aktywność błąd, 345 android:layout_alignLeft, 171
adapter, 250, 632 instalacja, 6 android:layout_alignParentBottom,
ArrayAdapter, 251, 252, 291, 494 Android Virtual Device Manager, 24 169
CursorAdapter, 494, 495, 497, 523 Android wersja, 11 android:layout_alignParentLeft, 169
tworzenie, 498 animacja, 598, 667 android:layout_alignParentRight, 169
RecyclerView.Adapter, 605 widoków, 667 android:layout_alignParentTop, 169
separacja od interfejsu, 633 ANT, 7 android:layout_alignRight, 171, 183
ADB, 654, 656 API, 3 android:layout_alignTop, 171
akcja, 12, 96, 97, 113, 367 poziom, 10, 368, 369 android:layout_below, 171
dodawanie elementu, 379 aplikacja, 2, 3, 31, 95, 96, 116, 129, 228 android:layout_centerHorizontal, 169
dostawca, Patrz: dostawca akcji API, Patrz: API android:layout_centerInParent, 169
pasek, Patrz: pasek akcji bezpieczeństwo, 652 android:layout_centerVertical, 169
aktywność, 2, 12, 31, 74, 116 etykieta, Patrz: aplikacja nazwa android:layout_column, 193
bieżąca, 347 ikona, Patrz: ikona android:layout_columnSpan, 194
Blank Activity, 13, 42 katalog, Patrz: katalog android:layout_gravity, 193
blokowanie ponownego kompilowanie, 27 android:layout_height, 45, 166, 177
uruchomienia, 136 lokalizacja, 37 android:layout_marginBottom, 172
CreateMessageActivity, 75 nawigowanie, 233, 367, 400 android:layout_marginLeft, 172
cykl życia, 133, 134, 135, 142, 148, nazwa, 9, 385 android:layout_marginRight, 172
156, 327 nazwa firmowej domeny, 9 android:layout_marginTop, 172
na pierwszym planie, 151 pakiet, Patrz: plik APK android:layout_row, 193
widzialny, 143 rozpowszechnianie, 664 android:layout_toLeftOf, 171
deklarowanie, 81 spakowanie, 27 android:layout_toRightOf, 171
domyślna, 107, 108 stoper, Patrz: stoper android:layout_weight, 179, 180
interakcja z bazą danych, 472, 475 tworzenie, 8, 9, 10, 12, 13 android:layout_width, 45, 166, 177
interaktywna, 40 udostępnianie, 664 android:name, 548
kategoria, Patrz: kategoria uruchamianie android:onClick, 244, 347, 359
aktywność, kod, Patrz: plik na fizycznym urządzeniu, 105, 113 android:orderInCategory, 377
MainActivity.java w emulatorze, 23, 27, 113 android:parentActivityName, 392
listy, 247, 248, 257 wdrożenie, 27 android:rowCount, 189
tworzenie, 249 węzeł kluczowy, 398 android:text, 45, 50
nadrzędna, 392 widżet, Patrz: widżet aplikacji android:theme, 372
nazwa, 14 wydajność, 652 android:title, 377
poziomu głównego, 229, 230, 232, wykonanie, 27 choiceMode, 406
238, 293, 392, 507, 639 źródło danych, 438 divider, 406
pusta, Patrz: aktywność Blank Activity ART, 650 dividerHeight, 406
stan, 133, 282 atrybut layout_gravity, 406
wstrzymana, 150 android:columnCount, 189 layout_height, 406
szczegółów/edycji, 229, 230, 259, 295 android:entries, 250 layout_width, 406
tworzenie, 13, 14, 40, 42, 78, 79 android:exported, 548 padding, 167
uruchamianie, 82, 84 android:gravity, 181, 182, 183, 184, 193 parent, 373
wstrzymana, 150, 522 android:icon, 377 showAsAction, 378
zapisywanie stanu bieżącego, 137 android:id, 45 xmlns:android, 166
zastępcza, 151, 152 android:inputType, 206 AVD, 23, 30
Android Debug Bridge, Patrz: ADB android:label, 372 konfiguracja, 26
Android SDK, 5, 654 android:layout_above, 171 tworzenie, 24, 25

674 Skorowidz
Skorowidz

B poziomu głównego, 366 intencja, 82, 83, 87, 88, 117, 386, 388, 545
back stack, Patrz: stos cofnięć proporcje, 309 filtr, 101, 102, 113
baza danych, 438, 601 szczegółów/edycji, 366 jawna, 113, 559
aktualizacja, 449, 455, 457, 459, 460, wielkość, 309, 359 niejawna, 98, 113
461, 462, 465, 510 wygląd, 12 oczekująca, 559, 560
JDBC, Patrz: JDBC element tworzenie, 96, 97
kopiowanie, 658 Button, 48 uruchamianie przez powiadomienie,
modyfikacja struktury, 465 ContentValues, 449 559, 560
nazwa, 466 FragmentTransaction, 301 wyznaczanie, 101
numer wersji, 456, 459 FrameLayout, 300 intent filter, Patrz: intencja filtr
odtwarzanie, 658 HorizontalScrollView, 219 intent resolution, Patrz: intencja
rekord ImageView, 215, 216 wyznaczanie
aktualizacja, 448, 449, 465 ListView, Patrz: widok listy interfejs, 296, 298
usuwanie, 450, 465 meta-data, 392 DrawerListener, 417
schemat zmiana, 456 RelativeLayout, 47 Listener, 634, 636
SQLite, Patrz: SQLite ScrollView, 219 OnClickListener, 347, 349, 351, 359
tworzenie, 443, 445, 447 service, 548 OnItemClickListener, 408
wydajność, 525, 526, 539 style, 373 programowania aplikacji, Patrz: API
zamykanie, 489, 499 TextView, 48, 193, 205 użytkownika
biblioteka definiowanie, 205 graficzny, Patrz: GUI
AWT, 4 emulator, 4, 23, 660 wygląd, 12
Support Libraries, 368, 369, 370, zrzut stanu, 661 iPhone Symulator, Patrz: symulator
406, 602 event listener, Patrz: obiekt nasłuchujący iPhone’a
Swing, 4
wsparcia, Patrz: biblioteka F J
Support Libraries filtr intencji, Patrz: intencja filtr Java, 2, 4, 5, 16
błąd, 345 fragment, 271, 272, 273, 287, 359 instalacja, 6
cykl życia, 283, 327 klasa własna, Patrz: klasa własna
C dodawanie Javy
card view, Patrz: widok karty do projektu, 276, 401 Java Virtual Machine, Patrz: JVM
cień, 598, 599 do układu aktywności, 279 język
interfejs, 295, 296, 298 Groovy, Patrz: Groovy
listy, 288, 289, 297 Java, Patrz: Java
D zagnieżdżanie, 326, 337, 341, 342 SQL, Patrz: SQL
Dalvik, 650 znacznik, Patrz: znacznik XML, Patrz: XML
dostawca FrameLayout, Patrz: układ ramki journal file, Patrz: plik magazynu
akcji, 386, 387 JVM, 650, 652
treści, 665
dp, Patrz: piksel niezależny od gęstości G
DrawerLayout, 399, 405 Google Play płatności, 5 K
dziennik, 543, 545 GPS, 579, 582 katalog
logcat, 657 gradle, 7 aplikacji, 16
grafika 9-patch, 672 app, 17
graphical user interface, Patrz: GUI build, 17
E GridLayout, Patrz: układ siatki com.hfad., 17
Eclipse, 7 Groovy, 7 debug, 17
edytor GUI, 2, 164, 201, 545, 552 drawable, 215, 237
kodu, 18, 32 drawable-hdpi, 308, 379
projektu, 18, 32, 43, 44, 49 generated, 17
tekstów, 7 I java, 17
efekt 3D, 598 ikona
layout, 17, 308, 320
ekran, 270, 306, 307, 308 ic_action_new_event, 379
layout-large, 308, 320
gęstość, 215, 309 zestaw systemowy, 379
main, 17
kategorii, 366 Instrumentation Testing, Patrz: test
opcje, 309
orientacja, 309, Patrz też: oprzyrządowania
platform-tools, 654
urządzenie obracanie IntelliJ IDEA, 5
r, 17

jesteś tutaj  675


Skorowidz

katalog własna Javy, 66 delete, 450, 510


res, 17, 132 Workout, 275 distanceTo, 576
source, 17 klucz RSA, 105 doInBackground, 530, 532, 533, 535
src, 17 kod execSQL, 466
tworzenie, 215 aktywności, Patrz: plik MainActivity. execute, 536
values, 17, 373 java findFragmentById, 287
kategoria, 101, 113, 229, 230, 232, bajtowy, 650 findFragmentByTag, 427
247, 308 źródłowy, Patrz: plik źródłowy findViewById, 59, 60, 135, 203, 285,
ekran, Patrz: ekran kategorii komponent 287, 328
klasa Spinner, 49, 53, 54, 60 getApplicationContext, 555
abstrakcyjna, 135, 497 TextView, 45, 60 getChildFragmentManager, 341
ActionBar, 412 komunikat, 220 getFragmentManager, 339, 340
ActionBarActivity, 370, 385 na ekranie, 552 getHeight, 203
ActionBarDrawerToggle, 417, 420, odbiorca, 670 getId, 203
421 w dzienniku systemowym, 545, 551 getInt, 489
Activity, 57, 135 w górnej części ekranu, Patrz: getIntent, 88, 260
Android.util.Log, 546 powiadomienie getReadableDatabase, 444, 484, 485
ApplicationTestCase, 673 konsola, 28 getString, 489
AppWidgetProvider, 671 konstruktor bezargumentowy, 278 getStringExtra, 92
AppWidgetProviderInfo, 671 kursor, 475, 486, 488, 496 getSystemService, 561
AsyncTask, 530, 535, 539 pobieranie wartości, 489 getView, 284, 285, 287
Bundle, 137, 138, 141 tworzenie, 497, 509 getWidth, 203
Button, 244 zamykanie, 489, 499 getWritableDatabase, 444, 484, 485
CaptionedImagesAdapter, 632, 633, insert, 448, 510
635 invalidateOptionsMenu, 418
Context, 135
L isClickable, 203
layout, Patrz: układ
ContextThemeWrapper, 135 isFocused, 203
LinearLayout, Patrz: układ liniowy
ContextWrapper, 135 Log.d, 546
lista, 229, 288, 295, 599
Cursor, 440 Log.e, 546
opcji, 399
CursorAdapter, Patrz: adapter Log.i, 546
rozwijana, 214, 251
CursorAdapter Log.v, 546
widok, Patrz: widok listy
CursorLoader, 669 Log.w, 546
DrawerLayout, 369 Log.wtf, 546
EditText, 91 Ł moveToFirst, 488
Handler, 124 łańcuch znaków, Patrz: plik wartości moveToLast, 488
Handler, 552 łańcuchowych moveToNext, 488
implementacja, 66 moveToPrevious, 488
IntentService, 544, 545, 571 onActivityCreated, 283, 327
java.util.Map, 141
M onAttach, 283, 327
manifest, 17, 80, 81, 136, 249, 287, 372,
ListActivity, Patrz: aktywność listy onBackStackChanged, 426
548, 582
ListFragment, 404 onBind, 572, 573
mapy, 668, 669
ListView, Patrz: widok listy onClick, 56, 58, 59, 61, 67, 350
Material Design, 597, 599, 600
Location, 576 onCreate, 57, 129, 133, 134, 135, 137,
Maven, 7
OnItemClickListener, 631 141, 142, 152, 161, 283, 327, 424, 575
menu, 377
R, 59 onCreateOptionsMenu, 135, 380
metoda
ReceiveMessageActivity, 92 onCreateView, 278, 304, 327, 342,
add, 301
RecyclerView.Adapter, 605 356, 357
addToBackStack, 301
Service, 544, 571, 575 onDestroy, 133, 134, 135, 142, 152,
bindService, 588
SQLiteDatabase, 440, 448, 449, 450 161, 283, 327, 575
changeCursor, 523
SQLiteOpenHelper, 440, 443, 444 onDestroyView, 283, 327
chroniona, 327
testowa, 673 onDetach, 283, 327
closeDrawer, 413
testowanie, 66 onDowngrade, 444, 455, 456, 459, 461
commit, 301
Theme.Holo, 368 onHandleIntent, 544, 545, 552
createChooser, 108, 109, 111, 112, 113
View, 45, 202, 328 onItemClick, 241, 408, 631
cursor.close, 489
ViewGroup, 202 onListItemClick, 257
db.close, 489
WebView, 666 onLocationChanged, 576

676 Skorowidz
Skorowidz

onOpen, 444 NinePatch, Patrz: grafika 9-patch stylów, 372, 373


onOptionsItemSelected, 381 null, 138 źródłowy, 16, 18, 19
onPause, 135, 150, 151, 152, 153, 154, pole
156, 161, 283, 327 tekstowe, 204, 206
onPostExecute, 530, 534, 535
O wyboru, 210
obiekt
onPreExecute, 530, 531, 535 polecenie
Binder, 572, 573, 592
onPrepareOptionsMenu, 418 adb devices, 654
LocationListener, 576, 577, 578, 579
onProgressUpdate, 533, 535 adb logcat, 657
LocationManager, 579
onRestart, 135, 142, 143, 149, 151, adb pull, 657
nasłuchujący, 203, 241, 242, 244, 248,
161, 327, 523 adb push, 657
296, 408
onResume, 135, 150, 151, 152, 153, adb shell, 656
DrawerListener, 416
154, 161, 283, 327, 327 powiadomienie, 556
OnBackStackChangedListener, 426
onSaveInstanceState, 135, 142, 304, ikona, 556, 561
OnClickListener, 351
355, 424 szuflada, 556, 561
ServiceConnection, 572, 573, 587
onSendMessage, 93 tworzenie, 558
TaskStackBuilder, 559, 560
onServiceConnect, 587 wysyłanie za pomocą usługi
obraz, 215, 216
onServiceDisconnect, 587 systemowej, 561
obsługa zdarzeń, 203
onStart, 135, 142, 143, 144, 149, 152, powłoka systemowa, 656
obszar powiadomień, 543
154, 161, 283, 285, 327 proces, 116, 129
przesłanianie, 144 procesor, 270
onStartCommand, 553, 575 P wirtualny Dalvik, Patrz: Dalvik
onStop, 135, 142, 143, 144, 154, 156, pasek projekt, 75, 235
161, 283, 327, 588 akcji, 368, 375, 385 przeglądarka WWW, 666
przesłanianie, 144 dodawanie elementów, 376, 379 przełącznik, 209
onUpgrade, 444, 455, 456, 459, 460 dostawca akcji, Patrz: dostawca przycisk, 45, 55, 56, 58, 122, 204, 549
post, 124, 552 akcji ActionBarDrawerToggle, 417, 420,
postDelayed, 124 modyfikowanie elementów, 418 421
publiczna, 58, 327 przycisk W górę, 391 definiowanie, 207
publishProgress, 533 tytuł, 412, 416 etykieta, 120
put, 448 przewijania, 219 opcji, 212, 238, 239
putExtra, 88 piksel niezależny od gęstości, 166 przełącznika, 208
query, 477, 478, 479, 484, 486, 487 platforma SDK, Patrz: SDK platforma W górę, 391, 392, 393, 420
remove, 301 plik Wstecz, 391, 424, 426
replace, 301 .apk, 27, 30, 651 z obrazem, 216, 217
requestFocus, 203 .class, 5, 650 z tekstem, 216
requstLocationUpdates, 579 .dex, 650, 652
setActionBarTitle, 426 .jar, 5, 650
setContentView, 57, 133, 135, 278, activity_main.xml, 17, 19, 32, 33
R
RelativeLayout, Patrz: układ względny
281, 356 AndroidManifest.xml, Patrz: manifest
Robotium, 673
setListAdapter, 291 APK, 651
setOnClickListener, 351 bazy danych, 439
setShareIntent, 388 classes.dex, 650, 651 S
setTitle, 412 graficzny, 237 SDK, 5, 23
setVisibility, 203, 418 graficzny ikon, 16 platforma, 5
startActivity, 82, 99, 117, 135 klasowy, 650, 652 wersja minimalna, 10, 16, 25
toString, 251 konfiguracyjny, 16 service, Patrz: usługa
unbindService, 588 magazynu, 439 spinner, Patrz: komponent Spinner
update, 449, 450, 510 MainActivity.java, 17, 21, 31, 57 SQL, 447
motyw, 371, 373, 385 R.java, 17, 59, 65 klauzula
domyślny, 374 strings.xml, 17, 34, 35, 36, 40, 50, 53, GROUP BY, 482
Theme.Holo, 368, 371, 385 65, 77, 120 HAVING, 482
Theme.Material, 368, 374 styles.xml, 373 SELECT, 492
stylów, 16 w zapytaniach, 481
N wartości łańcuchowych, 16, 17, 34, 36 SQLite, 439, 446
navigation drawer, Patrz: szuflada zasobów pomocnik, 440, 442, 452
nawigacyjna menu, 377 tworzenie, 444

jesteś tutaj  677


Skorowidz

stoper, 118, 121, 123, 142, 326 nazwa, 14 grupa, 202


stos cofnięć, 299, 300, 301, 340, 424 ramki, 300, 342 hierarchia, 204
styl, 373 siatki, 165, 189, 197, 369 karty, 599, 609
Material Design, 597, 598 definiowanie, 190, 191 dodawanie danych, 610
symulator iPhone’a, 660 szerokość, 166 tworzenie, 603
szuflada nawigacyjna, 369, 398, 399, 507 waga, 178, 180 listy, 231, 241, 242, 244, 251, 289,
inicjalizacja listy, 407 wysokość, 166 400, 494, 495
otwieranie, 416, 417, 420 względny, 47, 164, 165, 166, 173, 204 źródło danych, 250
tworzenie, 400 zależny od wielkości ekranu, 308 obrazów, 215
zamykanie, 413, 416, 417, 420 zmiana, 51, 76 przewijany, 219
urządzenie RecyclerView, 599, 600, 605, 606,
ekran, Patrz: ekran 613
Ś konfiguracja, 132, 136 menedżer układu, Patrz: układ
środowisko
obracanie, 130, 131, 132, 152, 303, menedżer
programistyczne IDE, 4, 5
304, 354, 355, 424, 425, 522 obsługa kliknięć, 631, 632
uruchomieniowe, Patrz: ART
procesor, Patrz: procesor rozmieszczenie
tablet, Patrz: tablet względem innych widoków, 170
T telefon, Patrz: telefon względem układu nadrzędnego, 168
tablet, 42, 270, 271, 288, 306, 307, 314, 359 ustawienia lokalne, 132 tekstowy, 205
klawiatura, 206 wirtualne z Androidem, Patrz: AVD tło, Patrz: grafika 9-patch
wirtualny, 30 usługa, 542 zastępowanie, 300
tablica, 250, 251 deklarowanie, 548 widżet
telefon, 23, 24, 42, 270, 271, 288, 306, IntentService, 553 aplikacji, 671
307, 315, 316, 359 lokalizacyjna, 576 CardView, 369
klawiatura, 206 nazwa, 548 RecyclerView, 369
numer, 97, 206 powiadomień, 556, 557 wiersz poleceń, 7, 654, 656
połączenie, 117, 142 powiązana, 542, 569, 572 wirtualna maszyna Javy, Patrz: JVM
test oprzyrządowania, 673 systemowa, 561 właściwość, Patrz: atrybut
testowanie, 673 uruchamianie, 550 wyjątek, 546
tost, 220, 543, 556 uruchomiona, 542, 544 SQLiteException, 485, 492
transakcja, 299, 301, 340
zagnieżdżanie, 341
typ Runnable, 124
W X
wartość XML, 2, 16, 44, 48, 164
fill_parent, 166
U łańcuchowa, Patrz: plik wartości
Z
układ, 2, 12, 31, 32, 40, 48, 164, 201, 204 łańcuchowych
zadanie asynchroniczne, 536, 537, 539
activity_create_message.xml, 75 match_parent, 166
zapytanie, 476
DrawerLayout, Patrz: DrawerLayout null, Patrz: null
budowniczy, 477
liniowy, 165, 174, 175, 183, 186 void, 58
w tle, 669
definiowanie, 174 wątek, 526
warunki, 476, 478, 479
identyfikator widoku, 175 główny, 526, 533, 552
wyniki
modyfikowanie, 176 w tle, 526, 529, 533, 539, 552
ograniczanie, 476, 478, 479
marginesy, 172, 173 wyświetlania, 526
porządkowanie, 480
menedżer, 615 wiadomość, 94
zdarzenie, 203, 631, 670
GridLayoutManager, 615, 616 widok, 203
generowane przez widok, 632
LinearLayoutManager, 615, 616 aktualizowanie, 300
znacznik, 427, 546
StaggeredGridLayoutManager, 615 CardView, Patrz: widok karty

678 Skorowidz

You might also like