Helion - Android w Praktyce

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 730

Tytuł oryginału: Android in Practice

Tłumaczenie: Tomasz Walczak

Projekt okładki: Studio Gravite / Olsztyn


Obarek, Pokoński, Pazdrijowski, Zaprucki

ISBN: 978-83-246-6611-9

Original edition copyright © 2012 by Manning Publications Co.


All rights reserved.

Polish edition copyright © 2012 by HELION SA.


All rights reserved.

All rights reserved. No part of this book may be reproduced or transmitted in any
form or by any means, electronic or mechanical, including photocopying, recording
or by any information storage retrieval system, without permission from the Publisher.

Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości


lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte


w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe
z wykorzystania informacji zawartych w książce.

Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)

Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/androp_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.

Printed in Poland.

• Poleć książkę na Facebook.com • Księgarnia internetowa


• Kup w wersji papierowej • Lubię to! » Nasza społeczność
• Oceń książkę
Spis treści
Wstęp 11
Podziękowania 13
O książce 17
O ilustracji z okładki 23

CZĘŚĆ I TŁO HISTORYCZNE I PODSTAWY .................................. 25

1 Wprowadzenie do Androida 27
1.1. Android w pigułce 30
1.2. HelloAndroid 34
1.3. Java, ale nie do końca 45
1.4. Linux, ale nie do końca 51
1.5. Więcej możliwości dzięki bibliotekom natywnym 56
1.6. Potrzebne narzędzia 59
1.7. Podsumowanie 67

2 Podstawy tworzenia aplikacji na Android 69


2.1. Aplikacja DealDroid 70
2.2. Podstawowe cegiełki 72
2.3. Manifest aplikacji 74
2.4. Zasoby 76
2.5. Układ, widoki i kontrolki 80
2.6. Aktywności 82
2.7. Adaptery 91
2.8. Intencje i filtry intencji 96
2.9. Obiekty klasy Application 103
2.10. Podsumowanie 105

3 Zarządzanie cyklem życia i stanem


3.1.
107
Czym są aplikacje w Androidzie? 108
3.2. Cykl życia aktywności 113

3
4 Spis treści

3.3. Kontrolowanie stanu egzemplarza aktywności 125


3.4. Wykonywanie operacji za pomocą zadań 131
3.5. Podsumowanie 133

CZĘŚĆ II PRAKTYCZNE ROZWIĄZANIA ....................................... 135

4 Precyzja co do piksela 137


4.1. Aplikacja MyMovies 138
4.2. Hierarchie widoków i ich wyświetlanie 139
4.3. Porządkowanie widoków w układy 143
TECHNIKA 1. Dyrektywy scalania i dołączania 152
4.4. Rozwinięcie informacji o klasach ListView i Adapter 156
0 TECHNIKA 2. Zarządzanie listą z pamięcią stanu 156
0 TECHNIKA 3. Widoki nagłówka i stopki 161
4.5. Stosowanie motywów i stylów 165
0 TECHNIKA 4. Stosowanie i pisanie stylów 165
0 TECHNIKA 5. Stosowanie i pisanie motywów 167
0 TECHNIKA 6. Określanie stylu tła widoku ListView 170
4.6. Korzystanie z obiektów graficznych 174
0 TECHNIKA 7. Używanie obiektów graficznych w postaci
kształtów 175
0 TECHNIKA 8. Stosowanie selektorów
obiektów graficznych 179
0 TECHNIKA 9. Skalowanie widoków za pomocą
dziewięciopolowych obiektów graficznych 182
4.7. Tworzenie przenośnych interfejsów użytkownika 186
0 TECHNIKA 10. Automatyczne dostosowywanie aplikacji
do różnych ekranów 186
0 TECHNIKA 11. Wczytywanie zasobów zależnych
od konfiguracji 191
0 TECHNIKA 12. Uniezależnienie się od pikseli 194
4.8. Podsumowanie 196

5 Używanie usług do zarządzania zadaniami


wykonywanymi w tle 199
5.1. Wielozadaniowość jest najważniejsza 200
5.2. Do czego służą usługi i jak z nich korzystać? 201
0 TECHNIKA 13. Tworzenie usługi 202
0 TECHNIKA 14. Automatyczne uruchamianie usługi 206
Spis treści 5

0 TECHNIKA 15. Komunikowanie się z usługą 208


0 TECHNIKA 16. Wykorzystanie usługi do zapisywania
danych w pamięci podręcznej 214
0 TECHNIKA 17. Tworzenie powiadomień 217
5.3. Planowanie i usługi 222
0 TECHNIKA 18. Używanie klasy AlarmManager 222
0 TECHNIKA 19. Podtrzymywanie działania usługi 226
0 TECHNIKA 20. Używanie usługi
Cloud to Device Messaging 229
5.4. Podsumowanie 234

6 Wątki i współbieżność
6.1.
237
Współbieżność w Androidzie 238
0 TECHNIKA 21. Proste wątki 240
0 TECHNIKA 22. Przekazywanie informacji o zmianach
między wątkami 243
0 TECHNIKA 23. Zarządzanie wątkami w puli wątków 249
6.2. Korzystanie z klasy AsyncTask 255
0 TECHNIKA 24. Implementowanie prac za pomocą klasy
AsyncTask 256
0 TECHNIKA 25. Przygotowanie do zmian
w konfiguracji 261
6.3. Różne techniki 268
0 TECHNIKA 26. Wyświetlanie ekranów powitalnych
za pomocą zegarów 268
0 TECHNIKA 27. Implementowanie niestandardowych
pętli komunikatów 272
6.4. Podsumowanie 276

7 Lokalne zapisywanie danych


7.1. Odczyt i zapis plików 280
279

0 TECHNIKA 28. Korzystanie z pamięci wewnętrznej 282


0 TECHNIKA 29. Korzystanie z pamięci zewnętrznej 286
0 TECHNIKA 30. Używanie katalogów
na pamięć podręczną 292
0 TECHNIKA 31. Stosowanie synchronizacji przy zapisie
plików 293
7.2. Przechowywanie ustawień 294
0 TECHNIKA 32. Odczyt i zapis ustawień 295
6 Spis treści

0 TECHNIKA 33. Korzystanie z klasy PreferenceActivity 296


7.3. Korzystanie z bazy danych 299
0 TECHNIKA 34. Tworzenie bazy danych
i obiektów modelu 303
0 TECHNIKA 35. Tworzenie obiektów DAO
i menedżera danych 312
7.4. Badanie baz SQLite 323
7.5. Podsumowanie 325

8 Współużytkowanie danych między aplikacjami 327


8.1. Współużytkowanie danych między procesami 328
0 TECHNIKA 36. Stosowanie intencji 329
0 TECHNIKA 37. Zdalne wywołania procedur 335
0 TECHNIKA 38. Współużytkowanie danych (i innych
elementów) przez współdzielenie kontekstu 341
8.2. Dostęp do niestandardowych danych 347
0 TECHNIKA 39. Korzystanie ze standardowych dostawców
treści 347
0 TECHNIKA 40. Korzystanie z niestandardowego
dostawcy treści 352
8.3. Podsumowanie 356

9 Protokół HTTP i usługi sieciowe 357


9.1. Podstawy pracy z siecią z wykorzystaniem
protokołu HTTP 358
0 TECHNIKA 41. Protokół HTTP
i klasa HttpURLConnection 360
0 TECHNIKA 42. Praca z protokołem HTTP za pomocą klasy
HttpClient Apache’a 366
0 TECHNIKA 43. Konfigurowanie obiektu klasy HttpClient
bezpiecznego ze względu na wątki 370
9.2. Korzystanie z usług sieciowych generujących dane
w formatach XML i JSON 375
0 TECHNIKA 44. Przetwarzanie danych w XML-u
za pomocą interfejsu SAX 379
0 TECHNIKA 45. Przetwarzanie dokumentów XML
na podstawie specyfikacji XmlPull 385
0 TECHNIKA 46. Przetwarzanie danych
w formacie JSON 389
Spis treści 7

9.3. Elegancka obsługa awarii sieci 393


0 TECHNIKA 47. Ponawianie żądań
za pomocą komponentów obsługi 393
0 TECHNIKA 48. Obsługa zmian konfiguracji sieci 397
9.4. Podsumowanie 400

10 Najważniejsza jest lokalizacja 403


10.1. Krótkie wprowadzenie
do współrzędnych geograficznych 404
10.2. Menedżery, dostawcy i odbiorniki położenia 407
0 TECHNIKA 49. Sprawdzanie stanu dostawcy położenia 414
0 TECHNIKA 50. Określanie aktualnego położenia
za pomocą odbiornika LocationListener 416
10.3. Tworzenie aplikacji z wykorzystaniem map 422
0 TECHNIKA 51. Przekształcanie adresu
na współrzędne geograficzne 425
0 TECHNIKA 52. Tworzenie aktywności MapActivity
z powiązanym widokiem MapView 427
0 TECHNIKA 53. Wyświetlanie elementów OverlayItems
w widoku MapView 430
10.4. Podsumowanie 433

11 Uatrakcyjnianie aplikacji za pomocą multimediów 435


11.1. Funkcje zbyt zaawansowane
dla telefonu wielofunkcyjnego 436
0 TECHNIKA 54. Wykrywanie możliwości 437
11.2. Zarządzanie multimediami 440
0 TECHNIKA 55. Korzystanie z zasobów i plików 440
0 TECHNIKA 56. Korzystanie z dostawców
treści multimedialnych 447
0 TECHNIKA 57. Używanie intencji i aktywności 450
11.3. Odtwarzanie multimediów 453
0 TECHNIKA 58. Zdjęcia i proste animacje 454
0 TECHNIKA 59. Kontrolowanie dźwięku 458
0 TECHNIKA 60. Wyświetlanie filmów 462
11.4. Rejestrowanie multimediów 465
0 TECHNIKA 61. Robienie zdjęć 465
0 TECHNIKA 62. Rejestrowanie dźwięku i filmów 470
11.5. Podsumowanie 475
8 Spis treści

12 Grafika dwu- i trójwymiarowa 477


12.1. Rysowanie z wykorzystaniem bibliotek
do obsługi grafiki dwuwymiarowej 478
0 TECHNIKA 63. Przechodzenie
do trybu pełnoekranowego 480
0 TECHNIKA 64. Rysowanie prostych kształtów 481
0 TECHNIKA 65. Ciągłe wyświetlanie widoku
w wątku interfejsu użytkownika 484
0 TECHNIKA 66. Wyświetlanie tekstu na ekranie 485
0 TECHNIKA 67. Określanie czcionki
przy wyświetlaniu tekstu 487
0 TECHNIKA 68. Wyświetlanie bitmap 489
0 TECHNIKA 69. Stosowanie efektów dwuwymiarowych 490
12.2. Grafika trójwymiarowa i biblioteka OpenGL ES 493
0 TECHNIKA 70. Rysowanie pierwszego trójkąta 500
0 TECHNIKA 71. Tworzenie piramidy 504
0 TECHNIKA 72. Kolorowanie piramidy 510
0 TECHNIKA 73. Dodawanie tekstury do piramid 513
12.3. Podsumowanie 519

CZĘŚĆ III POZA STANDARDOWE ROZWIĄZANIA ......................... 521

13 Testowanie i instrumentacja 523


13.1 Testowanie aplikacji na Android 525
0 TECHNIKA 74. Prosty test jednostkowy aplikacji
na Android 533
13.2. Pociąganie za sznurki — instrumentacja w Androidzie 538
0 TECHNIKA 75. Testy jednostkowe aktywności 539
0 TECHNIKA 76. Scenariusz użytkownika jako testy
funkcjonalne 544
0 TECHNIKA 77. Eleganckie testy z wykorzystaniem
frameworku Robotium 549
13.3. Poza instrumentację — atrapy i testy losowe 554
0 TECHNIKA 78. Atrapy i sposoby ich stosowania 554
0 TECHNIKA 79. Przyspieszanie testów jednostkowych
z zastosowaniem Robolectrica 561
0 TECHNIKA 80. Przeprowadzanie testów obciążeniowych
za pomocą narzędzia Monkey 567
13.4. Podsumowanie 573
Spis treści 9

14 Zarządzanie budowaniem 575


14.1. Budowanie aplikacji na Android 577
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 583
14.2. Zarządzanie procesem budowania za pomocą Mavena 592
0 TECHNIKA 82. Budowanie za pomocą Mavena 595
0 TECHNIKA 83. Wtyczka Mavena
dla środowiska Eclipse 607
0 TECHNIKA 84. Narzędzie
maven-android-sdk-deployer 610
14.3. Serwery budowania i ciągłe budowanie 615
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem
Hudsona 617
0 TECHNIKA 86. Budowanie macierzowe 625
14.4. Podsumowanie 630

15 Pisanie aplikacji na tablety z Androidem 633


15.1. Przygotowania do tworzenia aplikacji na tablety 635
0 TECHNIKA 87. Wykorzystywanie istniejącego kodu
za pomocą projektów bibliotek 635
0 TECHNIKA 88. Tworzenie aplikacji przeznaczonej
na tablety 638
15.2. Podstawowe informacje o tabletach 641
0 TECHNIKA 89. Fragmenty 642
0 TECHNIKA 90. Pasek akcji 650
0 TECHNIKA 91. Przeciąganie 655
15.3. Podsumowanie 662

Dodatek A Narzędzia do debugowania 665


Dodatek B Niestandardowe techniki tworzenia aplikacji
na Android 677
Dodatek C ProGuard 687
Dodatek D Monkeyrunner 701

Skorowidz 713
10 Spis treści
Wstęp
W 2007 roku dużo się mówiło o wspieranym przez Google otwartym projekcie
z obszaru telefonów komórkowych, jednak nikt nie znał szczegółów. Od początku
byliśmy tym zainteresowani, ponieważ wszyscy w ten lub inny sposób uczestni-
czymy w projektach o otwartym dostępie do kodu źródłowego, a ponadto uży-
wamy Linuksa i znamy Javę. Nowa, wspierana przez Google „platforma dla tele-
fonów oparta na Javie i Linuksie” (jak była wówczas nazywana na niektórych
blogach i przez część specjalistów) była ekscytująca i wydawała się wprost stwo-
rzona dla nas.
Pojawiło się kilka oficjalnych komunikatów grupy Open Handset Alliance,
jednak w żadnym z nich nie padło słowo Java. Jednocześnie wiadomo było, że
oprogramowanie ma działać w „niestandardowej maszynie wirtualnej”. Nad jej
częściami miały pracować osoby, o których wiedzieliśmy, że programują w Javie.
Jak to więc było z tą Javą? To pierwszy aspekt Androida, który nas zainteresował,
zanim jeszcze dowiedzieliśmy się, czym jest nowa platforma.
Kiedy pojawiły się szczegółowe informacje na jej temat, stało się jasne, że jej
twórcy użyli Javy jako języka, jednak nie zamierzali (przynajmniej wtedy) korzy-
stać z maszyny wirtualnej firmy Sun. Ponadto nie zastosowano standardowego
dla Linuksa podejścia opartego na jądrze i dystrybucjach. Google i partnerzy
z grupy OHA wykorzystali wiele istniejących narzędzi i komponentów o otwar-
tym dostępie do kodu źródłowego, jednak połączyli je w nowy sposób i dodali
własne fragmenty kodu.
Uważaliśmy, że platforma jest dobrze zaprojektowana, pojawiła się w dosko-
nałym momencie i ma wielki potencjał. Kiedy ukazały się pierwsze wersje beta,
szybko dorwaliśmy się do pakietu SDK i narzędzi, po czym zaczęliśmy zabawę.
Kupiliśmy pierwsze dostępne urządzenia z Androidem, aby móc zainstalować
w swoich telefonach pierwsze napisane przez nas aplikacje. Od tego czasu kon-
tynuujemy naszą przygodę z tą platformą.
Obecnie Android to wyjątkowa, niezwykle popularna, otwarta platforma.
Współcześnie działa nie na jednym, ale na setkach urządzeń. Twórcy Androida
nie próżnowali od czasu jego udostępnienia. Pojawiło się wiele jego nowych wer-
sji i usprawnień. Został znacznie ulepszony i wszystko wskazuje na to, że dalej
będzie rozwijany.
Przy całej tej ekscytacji programiści wraz z rozwojem Androida zrozumieli
pewną rzecz. Choć niezwykle łatwo jest zacząć tworzyć aplikacje na tę platformę,

11
12 Wstęp

ponieważ jest oparta na Javie i znana tak wielu osobom, nietrudno też o pro-
blemy. Android jest potężną bronią rodem z przyszłości, jednak wielu z nas
nadal celuje nią prosto w swoje stopy. Oprócz typowych cech niektórych inter-
fejsów API i nowych mechanizmów, takich jak GPS, aparaty fotograficzne i czuj-
niki, trzeba też pamiętać o ograniczonym środowisku z niewielkimi zasobami.
Nie wystarczy opracować nowy interfejs użytkownika, sprawić, aby usługa sie-
ciowa zaczęła komunikować się z siecią, i zacząć używać GPS-u. Wszystko to
trzeba zrobić w cyklu, który ponownie uruchamia kod po zmianie orientacji urzą-
dzenia. Należy przy tym zapewnić obsługę ekranów o różnej wielkości, uniknąć
blokowania wątku interfejsu użytkownika, nie zużywać zbyt wielu zasobów sys-
temowych itd. Łatwo jest tworzyć aplikacje na Android, ale tworzenie dobrych
rozwiązań jest znacznie trudniejsze.
Dlatego powstała książka Android w praktyce. Napisaliśmy aplikacje na
Android pobrane przez miliony użytkowników i dużo się przy tym nauczyli-
śmy. Wyciągaliśmy wnioski z sukcesów i porażek oraz publikowaliśmy artykuły
i zamieszczaliśmy na blogach wpisy dotyczące Androida. Zbieraliśmy wskazówki
i porady oraz staraliśmy się nimi dzielić. Przeczytaliśmy nawet kilka dobrych
książek o Androidzie dla początkujących, a także zapoznaliśmy się z krótszymi
tekstami, w których dobrze omówiono niektóre zagadnienia, ale pominięto inne.
Zauważyliśmy, że na rynku czegoś brakuje. Nie ma książki, której autorzy zaczy-
nają od podstaw, a następnie przechodzą do niebanalnych przykładów i oma-
wiają wszystko, co uważamy za ważne — od informacji wstępnych i programo-
wania po kompilowanie, testowanie i inne zagadnienia. Połączyliśmy więc siły.
Zebraliśmy pomysły i artykuły, dzięki czemu powstał projekt nowej książki.
Pozycja, którą trzymasz w rękach, jest efektem próby podzielenia się na-
szymi doświadczeniami i wiedzą. Staraliśmy się napisać książkę, którą zarówno
początkujący, jak i zaawansowani programiści mogą wykorzystać do nauki oraz
jako źródło informacji. Mamy nadzieję, że opisane tu porady i techniki oka-
żą się naprawdę przydatne. Ponadto liczymy na to, że książka ta pomoże Ci zro-
zumieć, jak budować doskonałe aplikacje na Android, które przez lata będą odnosić
sukcesy na tej platformie.
Podziękowania
Napisanie książki wymaga współpracy całej grupy osób. Pozycja ta nigdy by nie
powstała, gdyby nie niestrudzone wysiłki zespołu z wydawnictwa Manning, zna-
jomych pomagających nam przy kilku podrozdziałach oraz redaktorów technicz-
nych i uczestników programu Early Access, którzy dzielili się z nami opiniami.
Michael Stephens z wydawnictwa Manning zainicjował cały projekt i prze-
kazał opiekę nad nami w ręce kompetentnego Troya Motta, który przeprowadził
nas przez dalszą część procesu. Redaktorem prowadzącym i głównym doradcą
w wielu obszarach była Cynthia Kane. Pomagała nam prawie we wszystkim —
od gramatyki i używania odpowiednich słów po styl, formatowanie i inne kwestie.
Mary Piergies dbała o organizację pracy i kierowała produkcją. Na dalszych
etapach Benjamin Berg wykonał fantastyczną robotę w obszarze formatowania
i redakcji, a Gordan Salinovic zajął się składem. Wszystko to było możliwe dzięki
wydawcy, Marjanowi Bace’owi.
Spoza wydawnictwa Manning udało nam się zachęcić do pomocy kilku naszych
znajomych i współpracowników. Tamas Jano i Robert Cooper przygotowali przy-
kładowy kod oraz tekst, co pomogło nam w napisaniu rozdziałów poświęconych
grafice dwu- i trójwymiarowej. Logan Johnson pracował nad kilkoma przykła-
dami związanymi z klasą ContentProvider, które stały się częścią rozdziału 8.,
„Wymiana danych między aplikacjami”. Bez cennego wkładu tych osób zabra-
kłoby w książce omówienia wspomnianych ważnych aspektów programowania
dla platformy Android.
Zewnętrzną pomoc otrzymaliśmy też od redaktorów technicznych. Jerome
Baton pobrał, skompilował i ocenił wszystkie przykładowe projekty; znalazł
w nich kilka błędów, które nam umknęły. Ponadto otrzymaliśmy wiele sugestii
i propozycji poprawek od innych recenzentów książki. Oto oni: Steve Prior, Nenad
Nikolic, Kevin McDonagh, Mark Ryall, Peter Johnson, Al Scherer, Norman Klein,
Tijs Rademakers, Michele Galli, Sivakumar Thyagarajan, Justin Tyler Wiley,
Cheryl Jerozal, Brian Ehmann, Robby O’Connor, Gabor Paller, Dave Nicolette,
Ian Stirk, Daniel Alford i David Strong. Cenne informacje zwrotne otrzymaliśmy
też od uczestników programu Early Access.
Wszystkie wymienione osoby sprawiły, że książka stała się dużo lepsza, niż
byłaby bez ich pomocy. Jesteśmy im głęboko wdzięczni za ich wkład.

13
14 Podziękowania

CHARLIE
Pisanie książki technicznej to długi i skomplikowany proces. Ostatecznie owo-
cuje jednak wielkim zadowoleniem, kiedy trzymasz w ręku gotowy produkt
i możesz być z niego dumny. Na początku chcę podziękować współautorom,
Michaelowi i Matthiasowi, za to, że mogę czuć wspomnianą dumę. Obaj nie tylko
świetnie znają się na swoim fachu, ale też nie poddawali się, kiedy praca szła
wolniej, niż to planowaliśmy. W tym momentach podejmowali się nadprogramo-
wych zadań. Współpraca z Michaelem i Matthiasem była wspaniałym doświad-
czeniem.
Dziękuję też zespołowi rozwijającemu Androida oraz społeczności skupionej
wokół tej platformy i innych rozwiązań o otwartym dostępie do kodu źródło-
wego. Wszystkie osoby, które bezpośrednio albo pośrednio — przez zgłaszanie
błędów lub poprawek, pomoc na forach i w serwisach z pytaniami oraz odpowie-
dziami, a także uczestnictwo w grupach użytkowników i konferencjach oraz two-
rzenie bibliotek i narzędzi — przyczyniają się do ulepszania Androida, są ważnym
czynnikiem sprawiającym, że platforma ta działa i rozkwita. Byłbym niewdzięczny,
gdybym obok podziękowań dla wszystkich osób wnoszących wkład w rozwój
Androida nie wspomniał o całej społeczności związanej z oprogramowaniem
o otwartym dostępie do kodu źródłowego. Osoby pracujące nad Linuksem,
bibliotekami takimi jak WebKit, SQLite, Apache HttpClient i innymi, a także
narzędziami w rodzaju Eclipse i Mavena również w dużym stopniu przyczyniły
się do sukcesu Androida oraz tego, w jaki sposób mogę używać tej platformy.
Na zakończenie chcę podziękować mojej rodzinie i przyjaciołom. Moja żona
Erin i córki Skylar oraz Delaney zawsze były pomocne i zachęcały mnie do
pracy — nawet kiedy „ta książka” sprawiała, że nie mogłem uczestniczyć w nie-
których rodzinnych wydarzeniach. Ponadto moi rodzice, Earl i Peg Farmerowie,
zawsze byli przy mnie i zachęcali mnie do tego, abym wszystko robił najlepiej,
jak potrafię.
MICHAEL
Przede wszystkim chcę podziękować mojej pięknej żonie Crystal. Pisanie książki
zajmuje dużo czasu, a jest on jedną z rzeczy, których brakuje programiście pra-
cującemu nad nowym projektem i ojcowi dwóch małych synów. Bez mojej
wspaniałej żony książka ta nigdy by nie powstała. Chcę też podziękować mojemu
nauczycielowi angielskiego ze szkoły średniej, doktorowi Edowi Deluzainowi.
To on nauczył mnie, jak pisać, a umiejętność ta otworzyła przede mną wiele
możliwości. Napisanie książki było dla mnie marzeniem, które wreszcie się ziściło.
Źródłem tego marzenia były zajęcia u doktora Deluzaina. Chciałbym też podzię-
kować Troyowi Mottowi, który przez wiele lat współpracował ze mną przy pisa-
niu różnych tekstów technicznych. Ponowna współpraca z nim nad tą książką
była prawdziwą przyjemnością.
Podziękowania 15

MATTHIAS
Przede wszystkim chcę z całego serca podziękować społeczności programistów
platformy Android, których szczerze podziwiam. Jestem zwolennikiem opro-
gramowania o otwartym dostępie do kodu źródłowego i wierzę w to, że należy
odwdzięczać się za wszystko, co otrzymujemy. Dużo zawdzięczam społeczności
związanej z oprogramowaniem o otwartym dostępie do kodu źródłowego. Sta-
ram się jej odwzajemnić przez moje odpowiedzi na forach, pomysły, kod i tę
książkę. Dziękuję zwłaszcza osobom, które w formie pomysłów i kodu wniosły
wkład w moje ulubione projekty — Signpost, Droid-Fu i Calculon.
Chcę też złożyć gorące podziękowania Kevinovi McDonaghowi i Carlowi-
-Gustafowi Harrochowi z firmy Novoda za starania na rzecz tego, aby Android
był nie tylko platformą, ale też społecznością. Specjalne podziękowania składam
też Manfredowi Moserowi, Hugonowi Josefsonowi i Renasowi Redzie, autorom
wtyczki umożliwiającej korzystanie z Mavena w Androidzie i biblioteki Robotium,
za redakcję rozdziałów poświęconych tym narzędziom. Wielkie podziękowania
należą się też Julianowi Hartyemu, Carlosowi Sessie, Nenadowi Nikolicowi,
Janowi Berkelowi, Thibautowi Rouffineau i wszystkim innym wspaniałym
ludziom, którzy recenzowali tę książkę, dyskutowali ze mną na temat Androida
lub pracowali nad znakomitym oprogramowaniem o otwartym dostępie do kodu
źródłowego. Jesteście cudowni!
Nie należy zapominać, że książka ta powstała w wyniku pracy zespołowej.
Dlatego dziękuję Charliemu i Michaelowi za nieustanne posuwanie się z projek-
tem do przodu oraz niezwykle przyjemną podróż!
Ostatnie — choć nie najmniej istotne — podziękowania należą się moim dro-
gim rodzicom, którzy wspierali mnie w czasie pisania tej książki i motywowali mnie
do wysiłku w trudnych momentach.
16 Podziękowania
O książce
Android to platforma mobilna dla urządzeń przenośnych, dostępna jako opro-
gramowanie o otwartym dostępie do kodu źródłowego. Jej producentami są
Google i Open Handset Alliance. Android działa na smartfonach, tabletach,
w przystawkach STB, telewizorach i innych urządzeniach. Książka Android
w praktyce ma pomóc programistom w tworzeniu aplikacji na tę platformę.
Książka ta obejmuje przeznaczone dla początkujących informacje wstępne
i podstawy rozwijania aplikacji na Android. Ponadto szczegółowo omawiamy
w niej wiele tematów ciekawych dla średnio zaawansowanych i zaawansowanych
programistów. Naszym ogólnym celem było zebranie i uporządkowanie rozma-
itych przydatnych technik programowania na Android oraz wyjaśnienie ich
w kontekście tej platformy. W książce odpowiedź na pytanie dlaczego jest równie
ważna, jak wyjaśnienie kwestii, jak coś działa. Znajdziesz tu 91 technik. Każdej
z nich towarzyszy opis problemu, rozwiązanie i omówienie.

Kto powinien przeczytać tę książkę?


Książka dotyczy programowania aplikacji na platformę Android. Opisujemy tu
różne zagadnienia: od najważniejszych komponentów i podstaw tworzenia apli-
kacji po zaawansowane techniki, testy, kompilowanie, zarządzanie projektem i inne
kwestie. Mamy nadzieję, że książka będzie ciekawa dla programistów o różnym
poziomie umiejętności (od początkujących do zaawansowanych), rozwijających
aplikacje na Android, dla testerów rozwiązań na Android, a także dla menedżerów
i kierowników zespołów, którzy chcą lepiej zrozumieć pisanie programów na
omawianą platformę.
Książka jest przeznaczona dla osób, które mają pewne doświadczenie w pro-
gramowaniu i choć trochę znają Javę. Dlatego zakładamy, że większość Czytelni-
ków jest zaznajomiona z Javą i powiązanymi z nią technologiami (potrafi korzystać
ze środowisk IDE, kompilować i pisać kod w Javie, zna język XML oraz pod-
stawy sieci komputerowych itd.).

Plan książki
Rozdział 1. to wprowadzenie do Androida. Opisujemy w nim platformę i jej
genezę, firmy stojące za jej opracowaniem i jej wyjątkowe cechy. Przedstawiamy
też podstawowe interfejsy API Androida i narzędzia, a także przykładowy program
HelloAndroid.

17
18 O książce

W rozdziale 2. opisujemy wszystkie kluczowe komponenty potrzebne w pod-


stawowej aplikacji na Android, w tym zasoby, układ, widoki, aktywności, adaptery
i intencje.
W rozdziale 3. omawiamy szczegóły cyklu życia aplikacji na Android i aktyw-
ności. Opisujemy zarówno stos aktywności z aplikacji, jak i grupowanie aktywności
w zadania.
Rozdział 4. w całości poświęcony jest interfejsowi użytkownika. Omawiamy
między innymi tworzenie i wyświetlanie widoków, porządkowanie ich w układy,
używanie adapterów do zarządzania widokami, wielokrotne wykorzystanie wido-
ków i nadawanie im stylów, korzystanie z obiektów graficznych i obsługę urzą-
dzeń o różnej wielkości ekranu.
W rozdziale 5. opisujemy szczegółowo wielozadaniowość opartą na usługach.
Rozdział rozpoczyna się od wyjaśnienia, czym są usługi i dlaczego są niezbędne.
Dalej wyjaśniamy, jak je tworzyć, jak uruchamiać je automatycznie lub według
planu za pomocą alarmów, jak przy użyciu usługi zapisywać dane w pamięci pod-
ręcznej i przesyłać powiadomienia, a także jak przesyłać komunikaty z chmury
do urządzeń.
W rozdziale 6. szczegółowo omawiamy, kiedy można wykorzystać wątki
i zadania asynchroniczne, aby zwiększyć szybkość reakcji w aplikacjach na Android
i poprawić ich wydajność. Znajdziesz tu informacje o komunikacji między wąt-
kami, zarządzaniu wątkami, korzystaniu z zegarów i metod obsługi zdarzeń,
pętlach komunikatów i innych zagadnieniach.
Rozdział 7. dotyczy korzystania z zewnętrznych i wewnętrznych magazynów
danych. Chodzi tu na przykład o system plików, pliki z preferencjami, bibliotekę
SQLite i bazy danych.
Rozdział 8. poświęcony jest wymianie danych między różnymi aplikacjami.
Opisujemy między innymi korzystanie na platformie z danych z innych aplikacji
i udostępnianie danych innym aplikacjom. Obie te operacje można wykonać za
pośrednictwem dostawcy treści.
W rozdziale 9. temat przechowywania i wymiany danych rozwijamy o zagad-
nienia sieciowe. Omawiamy tu obsługę połączeń HTTP z kilku różnych klientów,
korzystanie z usług sieciowych przy użyciu formatów XML i JSON, wykrywa-
nie różnych sieciowych źródeł danych i przełączanie się między nimi, a także
płynne przywracanie stanu w przypadku problemów z siecią.
Rozdział 10. dotyczy usług opartych na lokalizacji. Wyjaśniamy tu między
innymi, jacy dostawcy lokalizacji są dostępni, jakich zasobów wymaga każdy
z nich, jak pozyskiwać dane o lokalizacji z różnych źródeł i jak tworzyć aplikacje
oparte na mapach.
W rozdziale 11. omawiamy multimedia. Tematyka tego rozdziału obejmuje
możliwości multimedialne, korzystanie z zasobów i plików, używanie dostawców
treści multimedialnych, a także pracę z dźwiękiem i filmami wideo (opisujemy
O książce 19

na przykład używanie aparatu fotograficznego, wyświetlanie animacji i kontro-


lowanie odtwarzania dźwięku).
Rozdział 12. poświęcony jest grafice dwu- i trójwymiarowej. Z tego roz-
działu dowiesz się, jak rysować kształty i linie na płótnie, tworzyć efekty i nie-
standardowe widoki, a także programować obiekty trójwymiarowe za pomocą
narzędzia OpenGL ES.
W rozdziale 13. omawiamy automatyczne testowanie aplikacji na Android.
Znajdziesz tu informacje o rozmaitych rodzajach testów oraz kilka różnych podejść
i frameworków testowych.
Rozdział 14. poświęcono zarządzaniu projektami i automatyzacji budowania
oprogramowania. Obejmuje on przegląd wszystkich kroków potrzebnych do
budowania aplikacji na Android oraz omówienie korzystania z narzędzi do kom-
pilacji (Anta i Mavena). Opisujemy także ciągłą integrację budowania oprogramo-
wania na Android z użyciem narzędzia Hudson.
W rozdziale 15. opisujemy tworzenie aplikacji na tablety z Androidem. Oma-
wiamy tu korzystanie z istniejących bibliotek kodu, pisanie aplikacji na różne
urządzenia, pracę z fragmentami aktywności i różne komponenty interfejsu użyt-
kownika właściwe dla tabletów.
W dodatku A odpowiadamy na pewne pytania związane z debugowaniem
aplikacji na Android i zamieszczamy kilka przydatnych porad na temat efek-
tywnego korzystania z narzędzia Android Debug Bridge. Opisujemy też nowy
dodatek do Androida, StrictMode, umożliwiający wykrywanie w aplikacjach pro-
blemów z wydajnością.
W dodatku B tworzenie aplikacji na Android przedstawiamy z zupełnie nowej
perspektywy. Analizujemy tu dwa różne podejścia do tworzenia natywnych apli-
kacji na tę platformę — korzystanie z klasy WebView i programowanie w innych
językach, takich jak Scala.
Dodatek C dotyczy korzystania z narzędzia ProGuard. Służy ono do opty-
malizowania i zaciemniania kodu bajtowego. Warto pamiętać o tych zadaniach
przy tworzeniu produkcyjnych wersji aplikacji.
W dodatku D omawiamy monkeyrunner. Jest to skryptowe narzędzie do
instrumentacji aplikacji na Android. Ten dodatek to nasza próba rzucenia światła
na to przydatne, ale słabo udokumentowane narzędzie.

Konwencje używane w kodzie i pobieranie plików


Przedstawiamy wiele przykładowych projektów. Każdy z nich oparty jest na
licznych listingach. Staraliśmy się zamieszczać jak najpełniejsze listingi, a jed-
nocześnie zachować zwięzłość, co nie zawsze jest łatwe przy korzystaniu z Javy
i XML-a. Wielu listingom towarzyszą też komentarze do kodu, w których podkre-
ślamy ważne zagadnienia i wyjaśniamy fragmenty aplikacji. Komentarze oma-
wiamy też w tekście.
20 O książce

W niektórych listingach pomijamy długie i szablonowe fragmenty kodu (jeśli


jest to uzasadnione). Na przykład po wyjaśnieniu pewnego zagadnienia zwykle
nie powtarzamy tej samej techniki w dalszych listingach. Wiemy, że niekompletne
przykłady bywają frustrujące, jednak w książce nie da się w całości zamieścić
omawianego kodu i odpowiednio wyjaśnić wszystkich powiązanych z nim zagad-
nień. Staraliśmy się zachować równowagę i wskazywać na listingach miejsca,
w których z uwagi na zwięzłość pominięto fragmenty kodu. Cały kod rozwiązania
znajduje się w kompletnych projektach, które można pobrać w formie kodu źró-
dłowego lub działających aplikacji binarnych z towarzyszącej książce Android
w praktyce strony z witryny Google Code (http://code.google.com/p/android-in-
practice/). Kod jest dostępny także na stronie wydawnictwa, pod adresem
http://www.manning.com/AndroidinPractice; polską wersję znajdziesz na witry-
nie wydawnictwa Helion.

Author Online
Zakup książki Android w praktyce daje bezpłatny dostęp do forum interneto-
wego prowadzonego przez wydawnictwo Manning Publications. Na forum możesz
dodawać komentarze na temat książki, zadawać pytania techniczne i otrzymać
pomoc od autorów oraz innych użytkowników. Jeśli chcesz uzyskać dostęp do
forum i zasubskrybować pojawiające się na nim wiadomości, wejdź na stronę
http://manning.com/AndroidinPractice. Aby przejść do forum, kliknij odnośnik
Author Online.
Wydawnictwo Manning zobowiązuje się zapewnić miejsce, gdzie może być
prowadzony dialog między Czytelnikami oraz między nimi a autorami. Nie pociąga
to jednak za sobą żadnych zobowiązań ze strony autorów. Udzielają się oni na
forum w ramach „wolontariatu” i nie są za to opłacani. Zachęcamy do zadawania
autorom jak najciekawszych pytań, by nie tracili zainteresowania forum!
Forum Author Online i archiwa z wcześniejszymi dyskusjami będą dostępne
na witrynie wydawnictwa dopóty, dopóki książka będzie drukowana.

O autorach
CHARLIE COLLINS jest dyrektorem do spraw programowania w firmie MOVL,
gdzie pomaga tworzyć aplikacje pozwalające na interakcje między telewizorami
podłączonymi do internetu a urządzeniami przenośnymi. Charlie pracował nad
kilkoma projektami o otwartym dostępie do kodu źródłowego oraz ma duże
doświadczenie w tworzeniu aplikacji i usług sieciowych. Jest też współautorem
wydanych przez wydawnictwo Manning książek GWT in Practice i Unlocking
Android. Kiedy nie pisze aplikacji na Android lub kodu serwera, często gra
w tenisa lub jeździ na rowerze górskim. Mieszka w Atlancie w stanie Georgia
z żoną i dwiema córkami.
O książce 21

MICHAEL GALPIN jest programistą w firmie Bump Technologies, gdzie


pracuje nad Bumpem — jedną z najpopularniejszych aplikacji społecznościo-
wych dostępnych w sklepie Android Market. Wcześniej przez cztery lata pra-
cował w eBayu, gdzie zajmował się jedną z najpopularniejszych aplikacji do
obsługi zakupów, eBay Mobile for Android. Często pisze artykuły na temat tech-
nologii o otwartym dostępie do kodu źródłowego dla serwisu IBM developer-
Works. Mieszka w San Jose, w stanie Kalifornia, z żoną i dwoma synami.
MATTHIAS KÄPPLER jest programistą w Qype.com, największym w Euro-
pie portalu społecznościowym z lokalnymi recenzjami. Kieruje pracami pro-
gramistycznymi w dziale produktów mobilnych, nazywanym „A-Team” (odpo-
wiedzialnym za Android i interfejsy API). Jest zakochany w Androidzie od czasu
pojawienia się wersji alfa tej platformy. Jest twórcą lub współtwórcą kilku dobrze
przyjętych projektów o otwartym dostępie do kodu źródłowego, takich jak
Signpost Oauth, Droid-Fu, Calculon i wtyczka umożliwiająca używanie Gradle’a
w Androidzie. W wolnym czasie nałogowo słucha muzyki, ogląda filmy i pije
kawę. Kiedy nie jest zajęty odwiedzaniem nowych lokali i recenzowaniem ich
w serwisie Qype, zwykle ćwiczy taekkyon — koreańską sztukę walki. Mieszka
w Hamburgu, w Niemczech.
22 O książce
O ilustracji z okładki
Rysunek z okładki książki Android w praktyce jest podpisany jako „habit członka
osobistej straży magnata z 1700 roku” i pochodzi z czterotomowego dzieła Collec-
tion of the Dresses of Different Nations Thomasa Jefferysa, opublikowanego
w Londynie między 1757 a 1772 rokiem. Księga ta zawiera piękne ręcznie malo-
wane miedzioryty szat z całego świata i wywarła duży wpływ na projekty kostiu-
mów teatralnych.
Różnorodność rysunków z Collection of the Dresses of Different Nations uzmy-
sławia nam, jakie było bogactwo kostiumów prezentowanych na londyńskiej
scenie 200 lat temu. Kostiumy te — zarówno historyczne, jak i współczesne —
pozwalały londyńskiej publiczności teatralnej zobaczyć ubiory z różnych krajów
i okresów.
Ubiór bardzo zmienił się w ostatnim wieku. Zróżnicowanie między regio-
nami, w przeszłości tak duże, zanikło. Obecnie trudno jest rozróżnić mieszkań-
ców dwóch kontynentów. Być może trzeba spojrzeć na to optymistycznie i uznać,
że zamieniliśmy rozmaitość kulturową na bardziej zróżnicowane życie osobiste
lub bogatsze i ciekawsze życie intelektualne oraz techniczne.
W wydawnictwie Manning podkreślamy typowe cechy branży komputero-
wej — inwencję, inicjatywę i radość tworzenia — przez zamieszczanie na okład-
kach różnorodnych ubiorów regionalnych i historycznych, ocalonych od zapo-
mnienia dzięki rysunkom z dzieł podobnych do opisanego.

23
24 O ilustracji z okładki
Część I
Tło historyczne i podstawy

W pierwszej części książki Android w praktyce przybliżamy podsta-


wowe zagadnienia związane z platformą Android i opisujemy jej kluczowe kompo-
nenty. W rozdziale 1. wyjaśniamy, czym jest Android, kto go opracował i dlaczego.
Przedstawiamy tu też podstawy tworzenia aplikacji na Android. W rozdziale 2.
wykorzystasz te informacje i opracujesz pierwsze niebanalne rozwiązanie — pod-
stawę późniejszych przykładów. Będzie ono obejmować manifest aplikacji, aktyw-
ności, widoki, zasoby, układy i adaptery. Rozdział 3. jest oparty na tym podsta-
wowym rozwiązaniu i pomaga zrozumieć oraz wykorzystać dobrze zdefiniowany
cykl życia całych aplikacji na Android, a także ich aspektów (na przykład
aktywności).
Wprowadzenie
do Androida

W tym rozdziale
Q Android w pigułce
Q Tworzenie aplikacji HelloAndroid
Q Java i Linux — korzenie Androida
Q Biblioteki natywne i inne narzędzia

Rzeczywistość jest tym, co nie ginie, kiedy przestajemy w to wierzyć.


Philip K. Dick
Dziś telefony komórkowe są wszechobecne. Jest ich więcej niż komputerów.
Zastąpiły zegarki, kalkulatory, aparaty fotograficzne, odtwarzacze plików MP3
i często narzędzia zapewniające dostęp do internetu. Obejmują też rozwiązania
takie jak nawigacja GPS oraz interfejsy oparte na ruchu i gestach, a także oferują
dostęp do sieci społecznościowych i niezwykle bogaty zestaw aplikacji łączą-
cych różne funkcje. Uwzględniając to, łatwo jest zrozumieć, dlaczego urządzenia
przenośne są tak popularne.
Nastąpił szybki rozwój technologii, na których urządzenia te są oparte. Nie tak
dawno połączenia głosowe były realizowane za pośrednictwem sieci przewo-
dowych wymagających operatorów w centralach, a wszystkie aparaty telefoniczne
były połączone fizycznymi kablami. Standardowy system telefoniczny dojrzał,
a ręczne centrale telefoniczne zastąpiono sterowanymi przez komputery prze-
łącznikami. Następnie dodano funkcje poczty głosowej, identyfikacji rozmówcy
itp. Ostatecznie zrezygnowano z kabli. Początkowo telefony bezprzewodowe

27
28 ROZDZIAŁ 1. Wprowadzenie do Androida

wymagały stacji bazowych i dużych anten. Także te elementy stały się zbędne,
kiedy dostawcy utworzyli rozbudowane sieci bezprzewodowe. Później w telefo-
nach zaczęły pojawiać się proste aplikacje, a urządzenia przenośne i sieci zaczęły
oferować coraz więcej funkcji. Obecne osiągnięcia są bardzo duże, ale to jeszcze
nie koniec. Z uwagi na zdumiewające możliwości sprzętu i sieci powstały nie-
zwykle wydajne komputery bezprzewodowe, które można trzymać w dłoni.
Problemem jest efektywne wykorzystanie całej mocy obliczeniowej i szyb-
kości sieci. Do niedawna oprogramowanie w wielu popularnych urządzeniach
przenośnych było zastrzeżone przez firmy. Zwykle niosło to za sobą pewne
skutki utrudniające programistom pracę.
Q Nie był dostępny kod źródłowy, dlatego nie można było sprawdzić,
jak oprogramowanie działa.
Q Czasem trzeba było wnieść wysokie opłaty licencyjne i ponieść inne
koszty.
Q Nawet po zakupie licencji obowiązywały restrykcyjne warunki
korzystania z oprogramowania i niejasne polityki.
Q Nie istniały przystępne języki programowania lub pakiety SDK.
Q Nie było łatwych sposobów udostępniania aplikacji użytkownikom
i instalowania ich na urządzeniach.
Członkowie Open Handset Alliance, konsorcjum firm, wśród których czołową rolę
odgrywał Google, kilka lat temu przyjrzeli się sytuacji i zadali sobie pytanie, co
jest potrzebne do zbudowania lepszego telefonu komórkowego. Określenie lepszy
dotyczyło przezwyciężenia utrudnień w szeroko zakrojonej współpracy, inno-
wacyjności i współdziałaniu z innymi platformami. Odpowiedzią na to pytanie
był Android. Jest to potężna platforma o otwartym dostępie do kodu źródłowego.
Każdy może z niej korzystać i ją rozszerzać. Na rysunku 1.1 pokazano zestaw
zrzutów ekranu będących ilustracją niektórych możliwości tej platformy.
Możliwości Androida sprawiają, że jest on atrakcyjny dla użytkowników.
W połączeniu z otwartym dostępem do kodu źródłowego i znakomitym pro-
jektem powodują, iż platformą zainteresowani są także programiści. Android to
duży potencjał — droga do przyszłości. Obecnie potrzebni są tylko pomysłowi
programiści piszący aplikacje wysokiej jakości. Android Cię potrzebuje!
Ponieważ jesteśmy zarówno programistami, jak i użytkownikami Androida,
opisane wcześniej aspekty zainspirowały nas do przekazania innym praktycznej
wiedzy na temat platformy i pisania na nią aplikacji. Dlatego powstał Android
w praktyce. Książka ta dotyczy tworzenia aplikacji na Android i obejmuje prak-
tyczne wskazówki programistów.
WSTĘPNA LISTA KONTROLNA. Na wstępie warto powiedzieć, że
Android w praktyce w założeniu jest „książką z przepisami”, obejmującą
praktyczne przykłady dotyczące wielu różnych aspektów platformy (także
1.1. Android w pigułce 29

Rysunek 1.1. Kilka


zrzutów z Androida
pokazuje możliwości
platformy, w tym
modyfikowalny
interfejs,
obsługę połączeń
telefonicznych,
sklep z aplikacjami,
kompletną
przeglądarkę
oraz obsługę map
i nawigacji

zaawansowanych). Część I książki, w tym ten rozdział, to krótkie wprowa-


dzenie do podstaw. W dalszych częściach szybko poruszamy się naprzód.
Jeśli znasz już Android i pisałeś aplikacje na tę platformę, możesz od razu
rzucić się na głęboką wodę i przejść do części II oraz III. Każda z nich
dotyczy konkretnego obszaru platformy i jest znacznie bardziej szcze-
gółowa od niniejszego wprowadzenia. Jeśli chcesz, możesz też pozostać
z nami i przypomnieć sobie podstawy.
W pierwszym rozdziale zaczynamy od przedstawienia informacji wprowadzają-
cych i omówienia podstaw. Najpierw wyjaśniamy, czym jest Android i dlaczego
jest ważny. Następnie tworzymy prostą aplikację HelloAndroid, aby wkroczyć
w świat platformy. W ramach tego ćwiczenia poznasz pakiet Android Software
Development Kit (SDK) i główne części aplikacji na Android. Następnie przecho-
dzimy do kluczowych aspektów wyspecjalizowanego środowiska uruchomie-
niowego Javy używanego w Androidzie — Dalvika. Omawiamy też wybrane
szczegóły opartego na Linuksie systemu operacyjnego, w którym działają wszyst-
kie pozostałe elementy. Następnie opisujemy ogólną architekturę Androida, w tym
natywne biblioteki warstwy pośredniej, aplikacje, framework aplikacji oraz inne
narzędzia dla programistów, i przedstawiamy szczegółowe informacje związane
z pakietem SDK.
30 ROZDZIAŁ 1. Wprowadzenie do Androida

Rozdział ten powinien pomóc Ci zrozumieć platformę Android i proces pro-


gramowania aplikacji na nią. To pozwoli Ci przejść do bardziej szczegółowych
zagadnień z rozdziału 2. i dalszych.

1.1. Android w pigułce


Gdyby zapytano osoby wybrane spośród milionów użytkowników Androida o to,
czym jest ta platforma, otrzymano by różne odpowiedzi. Część ludzi może powie-
dzieć, że jest to rodzaj telefonu, miejsce do pobierania aplikacji na urządzenia
przenośne lub mały, słodki, zielony robocik. Ponieważ jednak jesteśmy pro-
gramistami, wiemy więcej — rozumiemy, że Android to rozbudowana platforma
do tworzenia i uruchamiania aplikacji.
Przed przejściem do pisania kodu warto ściślej określić, czym jest Android,
opisać wyróżniające go cechy i omówić kluczowe komponenty tej platformy.

1.1.1. Definicja Androida


W marketingu Android opisuje się jako „kompletny zestaw oprogramowania dla
urządzeń przenośnych, obejmujący system operacyjny, warstwę pośrednią i klu-
czowe aplikacje mobilne”. Android jest tym i czymś więcej, ponieważ wykracza
poza świat urządzeń przenośnych. Ponadto definicja ta nie obejmuje platformy
programowania i pakietu SDK, które także mają duże znaczenie.
Android jest kompletnym stosem elementów — od programu rozruchowego
i bibliotek po interfejsy API, wbudowane aplikacje i pakiet SDK. Android nie
jest konkretnym urządzeniem lub nawet klasą urządzeń; to platforma, którą
można wykorzystać i zaadaptować do różnych konfiguracji sprzętowych. Pod-
stawową grupą urządzeń, w których działa Android, są telefony komórkowe.
Jednak obecnie Androida używa się też w czytnikach książek, netbookach, table-
tach i przystawkach STB.

1.1.2. Co wyróżnia platformę Android?


Choć Android jest rozbudowaną platformą o otwartym dostępie do kodu źró-
dłowego, nie jest pozbawiony wad. Nie wszystko działa w nim idealnie, jednak
uważamy, że stanowi on duży krok w dobrym kierunku. Uniknięto w nim wielu
problemów związanych z zastrzeżonymi systemami, ponieważ udostępniono go
jako oprogramowanie o otwartym dostępie do kodu źródłowego i na bezpłatnej
licencji. Wraz z Androidem oferowany jest wygodny i łatwo dostępny, darmowy
pakiet SDK, a także inne narzędzia programistyczne. Ponadto użytkownicy mogą
pobierać programy przez wbudowaną aplikację do obsługi zakupów (Market),
która umożliwia łatwe pobieranie i instalowanie tych programów bezpośrednio
z poziomu telefonów.
MARKET I INSTALOWANIE APLIKACJI. Android Market to podsta-
wowe narzędzie do wyszukiwania i instalowania aplikacji na telefonach
1.1. Android w pigułce 31

przez użytkowników. Każdy, kto się zarejestruje i zaakceptuje warunki


korzystania z usługi, może umieszczać aplikacje w sklepie Android Market.
Przesłane do niego programy są natychmiast udostępniane odbiorcom
(nie ma żadnego procesu oceny). Następnie aplikacje mogą być oceniane
i komentowane przez użytkowników. Rozwiązanie to jest szczególne
z uwagi na niezwykłą wygodę i aspekt społecznościowy. Oceny użytkow-
ników są podstawą sztucznego (w odróżnieniu od doboru naturalnego
w ekosystemach biologicznych) procesu selekcji w ekosystemie aplikacji.
Najlepsze programy odnoszą sukcesy. Oprócz oficjalnego sklepu Android
Market użytkownicy mogą też korzystać (jeśli operator sieci to umożli-
wia) ze sklepów niezależnych firm oraz bezpośrednio pobierać i instalować
aplikacje.
Wspomnieliśmy już o użytkownikach i sklepie. Ważne jest też to, że Android
działa na wielu urządzeniach. Obecnie istnieje tak wiele różnych gadżetów, że
trudno jest pisać i testować aplikacje działające na każdym z nich. Jest to jeden
z argumentów podnoszonych przez krytyków Androida. Istnieje jednak wiele
sposobów na złagodzenie tego problemu, a Android zaprojektowano tak, aby to
ułatwiał. W dalszych rozdziałach książki dowiesz się więcej o tworzeniu aplikacji
działających na zróżnicowanych urządzeniach — także o różnej wielkości ekranu.
Android nie jest pierwszym mobilnym systemem operacyjnym o otwartym
dostępie do kodu źródłowego. Takie systemy istniały już wcześniej, a w przyszło-
ści z pewnością pojawią się nowe. Ponadto twórcy Androida nie wymyślili sklepu
zapewniającego użytkownikom łatwy i oparty na rekomendacjach społeczności
dostęp do aplikacji. Jednak w Androidzie wszystkie te elementy połączono w nowy
sposób, a sama platforma jest wspierana przez grupę odnoszących sukcesy komer-
cyjnych firm i została dobrze zaprojektowana. To sprawia, że jest jednym z naj-
popularniejszych i cieszących się największym powodzeniem mobilnych syste-
mów operacyjnych na świecie.
Po zapoznaniu się z opisem Androida i zrozumieniu celów przyświecających
jego twórcom pora przyjrzeć się kluczowym komponentom tej platformy.

1.1.3. Kluczowe komponenty platformy


Android podobnie jak każdy stos technologii można rozbić na zbiory funkcji, co
ułatwia zrozumienie tej platformy. Podstawowy podział Androida przedstawiono
na rysunku 1.2.
KODY QR I ADRESY URL. Tam, gdzie może to być przydatne, zamiast
samych tekstowych adresów URL do zasobów internetowych podajemy
też kody QR (dwuwymiarowe kody kreskowe). Kod taki można zeskanować
za pomocą skanera (na Android dostępnych jest kilka takich aplikacji),
a następnie przekształcić na adres URL, co umożliwia szybkie i łatwe przej-
ście do strony.
32 ROZDZIAŁ 1. Wprowadzenie do Androida

Rysunek 1.2. Przegląd podstawowych komponentów platformy Android


— system operacyjny, warstwa pośrednia, framework aplikacji,
aplikacje i narzędzia programistyczne

Widoczny tu kod QR prowadzi do oficjalnej dokumentacji wyja-


śniającej, czym jest Android (adres http://mng.bz/Z4Le). Znaj-
dziesz w niej także oficjalny schemat architektury w formie „war-
stwowego tortu”.

Schemat architektury widoczny na rysunku 1.2 pokazuje, że platformę Android


można podzielić na pięć części. Są to:
Q aplikacje;
Q framework aplikacji;
Q biblioteki warstwy pośredniej;
Q system operacyjny;
Q pakiet SDK i narzędzia programistyczne.
Aplikacje nie wymagają omawiania. Warto jednak wspomnieć, że w większości
urządzeń z Androidem działa kilka rodzajów aplikacji, między którymi wystę-
1.1. Android w pigułce 33

pują subtelne różnice. Podstawowe aplikacje o otwartym dostępie do kodu źró-


dłowego (na przykład Browser, Camera, Gallery, Music, Phone i inne) są ofero-
wane jako część samego Androida. Znajdują się na prawie każdym urządzeniu
z Androidem. Ponadto istnieją zastrzeżone aplikacje Google’a wbudowane
w większość oficjalnych kompilacji. Te aplikacje to na przykład Market, Gmail,
Maps i YouTube. W niektórych wersjach znajdują się też aplikacje właściwe dla
operatorów sieci lub producentów sprzętu, na przykład odtwarzacz muzyczny
firmy AT&T, Navigator firmy Verizon lub TV firmy Sprint. Ponadto w sklepie
Android Market dostępne są aplikacje (o otwartym dostępie do kodu źródłowego
lub zastrzeżone) niezależnych firm. Są to na przykład niezależne aplikacje
Google’a, takie jak Goggles i Listen, oficjalne aplikacje popularnych serwisów, na
przykład Twittera i Facebooka, oraz tysiące innych programów.
DLACZEGO NIE MOGĘ ODINSTALOWAĆ NIEKTÓRYCH APLIKA-
CJI? Wielu producentów sprzętu i dostawców usług (a nawet w pewnym
stopniu sam Google) umieszcza niektóre aplikacje w specjalnej, przezna-
czonej tylko do odczytu części systemu plików — w tak zwanej partycji
systemowej. Zainstalowanych tam aplikacji nie można łatwo odinstalować
(potrzebne są do tego uprawnienia administratora i (lub) zamontowanie
partycji jako przeznaczonej do odczytu i zapisu). Często bywa to irytujące,
ale jest też zrozumiałe. Jedną z zalet Androida jest to, że producenci
sprzętu i dostawcy usług mogą dostosować go w pożądany sposób do wła-
snych potrzeb. Jest to jeden z powodów, dla których firmy te w ogóle
zaczęły korzystać z omawianej platformy.
Aby była zapewniona obsługa aplikacji, na platformie Android działa framework
do ich uruchamiania. Framework aplikacji obejmuje ściśle zintegrowaną część
pakietu SDK i interfejsy API, co umożliwia aplikacjom wysokopoziomową inte-
rakcję z systemem. Kiedy program potrzebuje dostępu do czujników sprzętowych,
danych z sieci, stanu elementów interfejsu i wielu innych informacji, może pobrać
je poprzez framework aplikacji. Więcej o pakiecie SDK i frameworku aplikacji
dowiesz się z podrozdziału 1.6.
Na poziomie poniżej frameworka aplikacji działa oprogramowanie nazywane
ogólnie warstwą pośrednią. Jak wskazuje nazwa, warstwa pośrednia to kompo-
nenty oprogramowania znajdujące się pomiędzy innymi elementami, którymi tu
są system operacyjny oraz aplikacje lub framework aplikacji. Warstwa pośrednia
obejmuje biblioteki dla wielu funkcji (przechowywania danych, wyświetlania
grafiki, przeglądania sieci WWW itd.), a także specjalny moduł — środowisko
uruchomieniowe Dalvik. Jest to specjalna, niestandardowa maszyna wirtualna
Androida i jej podstawowe biblioteki aplikacji. Więcej o Dalviku dowiesz się
z podrozdziału 1.3.
Na dole stosu elementów Androida znajduje się system operacyjny. W Andro-
idzie jest on oparty na Linuksie i wykonuje podobne zadania jak system opera-
cyjny dowolnego, tradycyjnego komputera stacjonarnego. Zapewnia interfejs
34 ROZDZIAŁ 1. Wprowadzenie do Androida

sprzętowy za pomocą zbioru sterowników urządzeń (na przykład sterowników


audio i wideo), przetwarza dane wyjściowe od użytkowników, zarządza proce-
sami aplikacji, obsługuje plikowe i sieciowe operacje wejścia-wyjścia itd. Więcej
o linuksowym systemie operacyjnym Androida przeczytasz w podrozdziale 1.4.
W warstwowym projekcie Androida każdy poziom jest abstrakcyjną wersją
niższej warstwy. Nie martw się — programiści nie muszą bezpośrednio obsłu-
giwać niskopoziomowych szczegółów. Zamiast tego zawsze korzystają z pod-
systemów, używając prostych interfejsów dostępnych we frameworku aplikacji
Androida. Wyjątkiem jest tworzenie rozwiązań natywnych z wykorzystaniem
pakietu Native Development Kit (NDK), ale o tym później.
Android to rozbudowany system. Nie potrafimy ani nawet nie chcemy oma-
wiać go tu w całości. Zamiast tego w poszczególnych rozdziałach koncentrujemy
się na ważnych aspektach; na elementach, które naszym zdaniem powinieneś
znać i rozumieć. Szczegółowo omawiamy przy tym przedstawione tu warstwy
w kontekście tworzenia aplikacji i spojrzenia na platformę z perspektywy progra-
misty. Aby było to możliwe, zacznijmy od zrealizowania wymagań wstępnych
i napisania pierwszej aplikacji na Android — HelloAndroid.

1.2. HelloAndroid
Pierwsza aplikacja ma wyświetlać wiersz tekstu i grafikę na jednym ekranie.
Nie jest specjalnie efektowna, jednak celowo staramy się zachować prostotę.
Pozwala to skoncentrować się na komponentach aplikacji i procesie jej tworze-
nia. Na rysunku 1.3 pokazano kompletną aplikację HelloAndroid uruchomioną
w emulatorze.
Do tworzenia aplikacji HelloAndroid wykorzystujemy kilka narzędzi, które
trzeba najpierw pobrać. Są to: pakiet SDK Androida, zintegrowane środo-
wisko programowania (IDE) Eclipse i wtyczka Eclipse Android Development
Tools (ADT).

1.2.1. Pobieranie pakietu SDK i środowiska Eclipse


Jeśli nigdy nie pisałeś aplikacji na Android, musisz najpierw sprawdzić, czy Twój
komputer spełnia wymagania systemowe, a następnie pobrać i skonfigurować
pakiet dla programistów Javy (JDK), pakiet SDK Androida i środowisko IDE
Eclipse. Nie opisujemy szczegółowo procesu instalowania tych niezbędnych
narzędzi, ponieważ jest on dobrze udokumentowany w internecie. W tabeli 1.1
wymieniono potrzebne zasoby internetowe i podano odnośniki do ich lokalizacji.
Powiązana z Androidem wtyczka ADT współdziała z pakietem Java Deve-
lopment Tools (JDT) ze środowiska Eclipse. Nie jest przypadkiem, że kod źró-
dłowy aplikacji na Android można pisać w języku Java i że programowanie
rozwiązań na Android jest wspomagane przez środowisko Eclipse. Istnieje wiele
narzędzi do programowania w Javie (na przykład środowisko Eclipse), a także
1.2. HelloAndroid 35

Rysunek 1.3.
Aplikacja
HelloAndroid
uruchomiona
w emulatorze
i wyświetlająca
na ekranie
proste elementy
— tekst i grafikę

Tabela 1.1. Wymagane narzędzia i dokumentacja internetowa potrzebne


do tworzenia aplikacji na Android

Opis Adres URL


Wymagania systemowe http://developer.android.com/sdk/requirements.html
Java — JDK5 lub JDK6 http://www.oracle.com/technetwork/java/javase/downloads
Środowisko IDE Eclipse http://www.eclipse.org/downloads/
dla programistów Javy
Pakiet SDK Androida http://developer.android.com/sdk/index.html
Wtyczka Eclipse ADT http://developer.android.com/sdk/eclipse-adt.html

duża i aktywna społeczność programistów używających tego języka. Eclipse udo-


stępnia wygodne funkcje do programowania w Javie, na przykład kolorowanie
składni, uzupełnianie kodu, wykrywanie błędów, obsługę procesu budowania
oprogramowania i znakomity debuger. Ponadto Eclipse udostępnia kreatory do
tworzenia i uruchamiania aplikacji na Android oraz do zarządzania i manipulo-
wania wirtualnymi urządzeniami z Androidem (ang. Android Virtual Devices —
AVD), a także specjalne edytory do tworzenia interfejsów użytkownika i zarzą-
dzania metadanymi aplikacji.
CZY MUSZĘ UŻYWAĆ ŚRODOWISKA ECLIPSE? Nie, nie musisz.
Jeśli wolisz, możesz korzystać z wiersza poleceń i opartego na Javie narzę-
dzia Apache Ant, służącego do budowania oprogramowania. Możesz też
używać opartych na Ancie narzędzi dostępnych w innym środowisku IDE.
Zachęcamy do korzystania ze środowiska Eclipse. Zespół pracujący nad
Androidem wybrał Eclipse jako główne wspierane środowisko IDE.
Ponadto przydatna jest wtyczka Eclipse ADT.
36 ROZDZIAŁ 1. Wprowadzenie do Androida

PRZY OKAZJI — CZY MUSZĘ UŻYWAĆ JAVY? Twórcy Androida


(podobnie jak i my) nie zapomnieli o osobach, które nie lubią Javy. W jed-
nym z dodatków omawiamy korzystanie z innych języków, na przykład
ze Scali. Opisujemy też tworzenie aplikacji sieciowych na Android (na
przykład za pomocą JavaScriptu i CSS-u). Są to obszerne zagadnienia,
dlatego nie możemy opisać ich szczegółowo, chcemy jednak przedstawić
chociaż wprowadzenie do nich i upewnić się, że znasz dostępne możli-
wości. Java jest jednak podstawowym językiem programistów aplikacji na
Android i to głównie z niej będziemy korzystać w tej książce.
Choć nie zamierzamy dokładnie opisywać instalowania środowiska Eclipse,
pakietu SDK Androida i wtyczki ADT, przedstawiamy kilka wskazówek. Nawet
jeśli już używasz środowiska Eclipse i nie dodałeś narzędzi dla Androida, możesz
ponownie zainstalować środowisko wraz z wtyczką ADT w nowej lokalizacji. W ten
sposób utworzysz zupełnie nową, odpowiednią dla Androida instalację środo-
wiska Eclipse (ponadto możesz dodać wtyczkę Google’a dla narzędzi App Engine
i GWT, przez co powstanie instalacja Google Eclipse). Jest to pomocne pod
kilkoma względami. Po pierwsze, instalacja zbyt wielu wtyczek i dodatków może
sprawić, że środowisko Eclipse będzie działać powoli. Po drugie, nowa instala-
cja nie będzie powiązana z istniejącymi projektami i wtyczkami, co może ułatwić
rozwiązywanie ewentualnych problemów z wtyczkami i konfiguracją. Ponadto
choć prawdopodobnie będziesz często korzystał ze środowiska Eclipse, warto
dodać narzędzia dla Androida do zmiennej PATH, co pozwala wygodnie korzy-
stać z wiersza poleceń. Niektóre narzędzia działają tylko z poziomu wiersza
poleceń (nie są dostępne poprzez wtyczkę), dlatego warto znać te programy
i umieć z nich korzystać. Omawiamy je w podrozdziale 1.6 i w kontekście powią-
zanych zagadnień w dalszych przykładach oraz tematach.
Po przygotowaniu narzędzi pora uruchomić środowisko IDE Eclipse i utwo-
rzyć projekt dla Androida.

1.2.2. Tworzenie projektu dla Androida w środowisku Eclipse


Prawdopodobnie znasz już środowisko Eclipse, a przynajmniej wiesz, jak utwo-
rzyć nowy projekt w narzędziu z graficznym interfejsem użytkownika. Przy two-
rzeniu projektu HelloAndroid wykorzystamy opcję przedstawioną na rysunku 1.4 —
File/New/Android Project.

Rysunek 1.4.
Tworzenie
nowego
projektu
dla Androida
w środowisku
Eclipse
1.2. HelloAndroid 37

Okno dialogowe, które pojawi się w środowisku IDE, to początkowy ekran wła-
ściwości projektu. Należy wprowadzić tu podstawowe informacje o projekcie,
widoczne na rysunku 1.5. Właściwości potrzebne do utworzenia nowego pro-
jektu to Project Name (nazwa używana do identyfikowania projektu w środowisku
Eclipse) i szereg danych związanych z Androidem — target (Build Target), nazwa
aplikacji (Application Name), nazwa pakietu (Package Name) i nazwa aktywności
(Create Activity na rysunku 1.5).

Rysunek 1.5. Określ


w środowisku Eclipse
właściwości projektu
HelloAndroid,
używając
wtyczki ADT

Nazwy są proste, podobnie jak pakiet Javy. Ciekawsza jest właściwość Build Tar-
get. Określa ona platformę SDK Androida, którą trzeba dodać w czasie instalo-
wania pakietu SDK. Platforma obejmuje określone elementy zależne i narzędzia
dla konkretnej wersji interfejsu API Androida. Możesz zainstalować wiele plat-
form, co pozwala kompilować i testować rozwiązania dla różnych wersji interfejsu
38 ROZDZIAŁ 1. Wprowadzenie do Androida

API, jednak wymagana jest tylko jedna platforma. Tu wybraliśmy Android 1.6,
jednak w omawianym prostym projekcie nie ma to znaczenia. Możesz użyć
dowolnego targetu (lub dowolnej platformy). Warto wspomnieć także o właści-
wości Create Activity. Jeśli ją zaznaczysz, wtyczka ADT utworzy szablonową klasę
typu „Hello World” i ekran.
Przed przejściem do dalszych zagadnień przyjrzyjmy się strukturze plików
wygenerowanej po kliknięciu przycisku Finish i utworzeniu przez wtyczkę
Eclipse ADT początkowej wersji projektu dla Androida.

1.2.3. Struktura projektu


Projekty dla Androida mają zdefiniowaną strukturę, co umożliwia rozmieszcze-
nie różnych komponentów i wprowadza pewien porządek w konfiguracji. Kod
źródłowy w Javie, pliki układu, zasoby w postaci łańcuchów znaków, zasoby gra-
ficzne i inne elementy mają swoje miejsce w hierarchii. Na rysunku 1.6 przed-
stawiono kompletną strukturę projektu HelloAndroid, obejmującą kod źródłowy
(i wygenerowany kod źródłowy), zasoby oraz manifest.

Rysunek 1.6. Przegląd podstawowej struktury projektu aplikacji na Android

Jak widać na rysunku 1.6, kod źródłowy w Javie w projekcie dla Androida znaj-
duje się w katalogu najwyższego poziomu src. Na tym samym poziomie znajduje
się katalog gen, w którym narzędzia związane z Androidem zapisują automatycz-
nie wygenerowane pliki z kodem źródłowym, w tym plik R.java.
R to wewnętrzna klasa używana do wiązania zasobów. Zasoby to dołączone
do projektu elementy inne niż kod (na przykład zewnętrzne łańcuchy znaków). Są
1.2. HelloAndroid 39

one umieszczane w katalogu res. Obejmuje on kilka podkatalogów, określających


typ zasobu i to, kiedy należy z niego korzystać. W katalogu najwyższego poziomu
znajduje się plik konfiguracyjny projektu dla Androida, AndroidManifest.xml.
Po zapoznaniu się ze strukturą i lokalizacją różnych elementów w kilku następ-
nych punktach tworzymy aplikację HelloAndroid. W tym procesie koncentru-
jemy się na tym, czym są poszczególne komponenty, jak je tworzyć i jak z nich
korzystać. Zaczynamy od pliku Main.java z katalogu src. Jest to pierwszy kontakt
z typową dla Androida klasą aktywności.

1.2.4. Wprowadzenie do klas aktywności (Activity)


W kontekście Androida aktywność to klasa Javy tworząca domyślne okno na ekra-
nie i umożliwiająca rozmieszczanie elementów interfejsu użytkownika. Można
ująć to prosto — klasa aktywności ogólnie odpowiada ekranowi aplikacji na
Android (przynajmniej przeważnie; występują tu pewne niuanse, które poznasz
później). Ponieważ utworzyliśmy projekt za pomocą wtyczki ADT i zaznaczyli-
śmy opcję Create Activity, w programie znajduje się już pierwsza klasa aktyw-
ności — Main. Na listingu 1.1 przedstawiono kod tej klasy.

Listing 1.1. Klasa aktywności w pliku Main.java wygenerowana przez wtyczkę ADT

package com.manning.aip.helloandroid;

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

public class Main extends Activity {


/** Wywoływana przy tworzeniu aktywności. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}

Klasa Activity wygenerowana przez wtyczkę ADT jest prosta, dlatego stanowi
doskonały punkt wyjścia. Przede wszystkim zauważ, że nowa klasa rozszerza
klasę Activity . To ważne. Klasa Activity obejmuje wiele elementów, w tym
metody obsługi cyklu życia, takie jak onCreate . Jak wskazuje komentarz w kodzie
(także wygenerowany przez wtyczkę), metoda ta jest wywoływana przy pierw-
szym tworzeniu klasy Activity. Więcej o tej klasie dowiesz się z rozdziałów 2. i 3.
Activity to jedna z najważniejszych klas używanych w codziennym programowa-
niu. Związanych z nią jest wiele zagadnień, których nie opisujemy w tym miejscu.
Na razie możesz traktować tę klasę jak pierwszy ekran. Możesz w niej uzy-
skać dostęp do cyklu życia i za pomocą odrębnego zasobu układu poinformować
framework, jak ma skonfigurować elementy wizualne . Tu zasobem układu
jest obiekt R.layout.main, ustawiony jako widok zawartości. R to specjalna wyge-
nerowana klasa, która łączy nazwy z zasobami (więcej na ten temat już niedługo).
40 ROZDZIAŁ 1. Wprowadzenie do Androida

1.2.5. Ustawianie układu aktywności


Zasób układu to specjalny plik konfiguracyjny określający projekt i układ wizu-
alnych elementów na ekranie. Wygodnym aspektem programowania aplikacji na
Android jest to, że często interfejs użytkownika można zadeklarować w języku
XML, posługując się zasobem układu. Pozwala to oddzielić warstwę prezentacji
od kodu (w pewnym stopniu) i wielokrotnie wykorzystać wiele elementów inter-
fejsu użytkownika. Na listingu 1.2 pokazano pierwszy zasób układu dla ekranu
aktywności Main.

Listing 1.2. Zasób układu (plik Main.xml) z deklaracją elementów interfejsu


użytkownika ekranu aktywności Main

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


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
public class Main extends Activity {
/** Wywoływana przy tworzeniu aktywności. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#FFF"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:gravity="center_horizontal"
android:textColor="#000"
android:textSize="50dp"
android:text="@string/hello"
/>
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:src="@drawable/droid"
/>
</LinearLayout>

W aplikacji HelloAndroid użyto podstawowego układu, zmodyfikowanego jednak


względem układu domyślnie wygenerowanego przez wtyczkę ADT. Pierwszą
rzeczą, na którą warto zwrócić uwagę, jest przestrzeń nazw xmlns:android. Ten
zapis to skrót z języka XML. Zdefiniowano go w taki sposób, aby w dalszym
kodzie można było wskazywać elementy ze schematu Androida za pomocą samego
przedrostka android:. Dalej widać, że w kodzie użyto klasy LinearLayout . Jest
to używana w Androidzie klasa układu rozmieszczająca zawarte w niej elementy
podrzędne w linii (w poziomie lub pionie; zobacz atrybut orientation). Układ
w Androidzie ma specjalny typ View (a dokładniej ViewGroup, ale nie uprzedzajmy
1.2. HelloAndroid 41

faktów). W omawianej platformie dostępnych jest kilka różnych układów. W roz-


dziale 4. opisujemy każdy z nich. View to klasa bazowa elementów obsługujących
układ ekranu oraz przeznaczonych do wyświetlania użytkownikom lub wcho-
dzenia z nimi w interakcje. Android ma wiele różnych typów widoków, w tym
używany dalej w układzie widok TextView .
Widok TextView, jak się pewnie domyślasz, wyświetla tekst. Elementy widoku
często mają atrybuty pozwalające na manipulowanie właściwościami. W kodzie
ustawiono margines, pozycję na ekranie względem innych elementów (pole
gravity), kolor i rozmiar widoku TextView . Ponadto widać, że atrybut android:
´text (określa wyświetlany tekst) jest ustawiony na @string/hello . Zastoso-
wanie członu @string oznacza, że wskazywany jest zasób w postaci łańcucha
znaków. Można na stałe zapisać w tym miejscu tekst, jednak przechowywanie
zasobów na zewnątrz, tak jak w omawianym kodzie, pozwala oddzielić układ od
zawartości.
Po widoku TextView znajduje się widok ImageView . Tu atrybut src usta-
wiono na @drawable/droid. Jest to następna referencja do zasobu zewnętrznego,
którym tu jest obiekt graficzny (ang. drawable) o nazwie droid . Obiekty gra-
ficzne omawiamy w rozdziale 4. Na razie wystarczy wiedzieć, że plik droid.gif
umieszczono w katalogu res/drawable-mdpi projektu, a Android może go znaleźć
i wykorzystać (plik ten znajduje się w kodzie towarzyszącym książce; pobraliśmy
go ze strony z materiałami związanymi z Androidem — http://www.android.com/
media/goodies.html). Po omówieniu układu przyjrzyjmy się bliżej działaniu refe-
rencji do zasobów.

1.2.6. Wskazywanie zasobów


Wspomnieliśmy już, że symbol @ w pliku układu (który sam jest rodzajem zasobu)
to referencja do innego zasobu. Wyrażenie @string/hello wskazuje na plik
strings.xml. Zawsze warto przechowywać różnego rodzaju elementy projektu
niezależnie od kodu. Dotyczy to układów, łańcuchów znaków, rysunków, plików
XML-a i innych elementów wskazywanych w Androidzie jako zasoby.
Jeśli chodzi o łańcuchy znaków i rysunki, sprawa jest oczywista. Jeżeli chcesz
używać różnych zasobów w zależności od innych ustawień, takich jak język lub
lokalizacja, możesz to zrobić. Znak @ informuje, że podana wartość to zasób.
W Androidzie występują zasoby wielu typów, które poznasz w kilku kolejnych
rozdziałach. Na razie przyjrzyjmy się zawartości pliku strings.xml. Przedstawiono
ją na listingu 1.3.

Listing 1.3. Plik zasobów res/values/strings.xml

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


<resources>
<string name="hello">Witaj, Androidzie!</string>
<string name="app_name">HelloAndroid</string>
</resources>
42 ROZDZIAŁ 1. Wprowadzenie do Androida

Ten zewnętrzny plik łańcuchów znaków jest zapisany w formacie XML i obej-
muje dane w postaci par klucz-wartość. W układzie wymieniono zasób hello,
dlatego aplikacja ostatecznie wyświetla łańcuchy znaków Witaj, Androidzie!.
Łańcuchy znaków, podobnie jak bardziej złożone typy danych, takie jak kolory
i obiekty graficzne (jest to typ do opisu kształtów w Androidzie), można przedsta-
wiać w formacie XML i wykorzystywać jako zasoby.
PO CO UŻYWAĆ XML-A? XML nie jest wcale taki zły. W Androidzie
korzystanie z tego formatu jest uzasadnione. XML zapewnia narzędziom
sztywną strukturę i mocne typy danych, jednak często wymaga dużej ilości
kodu i jest wolny w przetwarzaniu. Nie martw się — zasoby są kompilo-
wane przez platformę na format binarny (nie są przetwarzane w formacie
XML w czasie wykonywania programu).
Android może też korzystać z zasobów w postaci innych komponentów, w for-
macie innym niż XML. Na przykład rysunek Androida to binarny plik graficzny.
Jeśli takie binarne pliki znajdują się w odpowiednim dla ich typu miejscu w hie-
rarchii projektu, automatycznie można z nich korzystać jak z zasobów. Dalej
pokrótce opisujemy nazwy zasobów i ich wyszukiwanie.
Wszystkie zasoby Androida są identyfikowane przez framework aplikacji jako
stałe w Javie. Umożliwia to automatycznie generowana klasa R. Klasa ta składa się
z wielu klas wewnętrznych, co pokazano na listingu 1.4.

Listing 1.4. Automatycznie wygenerowana klasa R.java obejmująca klasy


wewnętrzne i nazwy stałych

/* AUTO-GENERATED FILE. DO NOT MODIFY.


*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/

package com.manning.aip.helloandroid;

public final class R {


public static final class attr {
}
public static final class drawable {
public static final int droid=0x7f020000;
public static final int icon=0x7f020001;
}
public static final class layout {
public static final int main=0x7f030000;
}
public static final class string {
public static final int app_name=0x7f040001;
public static final int hello=0x7f040000;
}
}
1.2. HelloAndroid 43

Komentarz na początku pliku źródłowego R jest jasny — klasa jest generowana


automatycznie i nie należy jej ręcznie modyfikować .
ECLIPSE I R. Jeśli środowisko Eclipse zgłasza, że klasa R nie jest dostępna
lub nie można jej skompilować, nie wpadaj w panikę. Jeżeli istnieje kata-
log gen, klasę można odtworzyć. Należy wyczyścić projekt (opcja Pro-
ject/Clean) lub ponownie go skompilować albo zbudować. Przy ponownym
budowaniu projektu Eclipse uruchamia narzędzie Android Asset Processing
Tool (aapt), które odtwarza plik źródłowy R.
W pliku źródłowym R każdemu typowi zasobów używanych w projekcie odpo-
wiada odrębna podklasa. W projekcie HelloAndroid użyto obiektów graficznych
(rysunków) , układu i łańcuchów znaków . Kompilacja projektu na Android
obejmuje kilka specjalnych etapów, z których jeden polega na identyfikowaniu
i opisywaniu zasobów (i kompilowaniu ich, jeśli jest to możliwe). Stałe w klasie R
umożliwiają późniejsze wskazywanie zasobów za pomocą nazw, a nie liczb całko-
witych oznaczających lokalizację elementów (liczby te prowadzą do tabeli zaso-
bów używanej przez Android do ich wyszukiwania).
Więcej o zasobach dowiesz się z rozdziału 2. i dalszych fragmentów książki.
Teraz zapamiętaj, że elementy inne niż kod są zapisywane jako zasoby i wskazy-
wane w pliku R.java. Po zapoznaniu się z plikiem R wiesz już, że aplikacja obej-
muje kod źródłowy w Javie powiązany z zasobem układu, który sam prowadzi
do kilku innych zasobów. Dalej wyjaśniamy, w jaki sposób wszystkie te różne
elementy są łączone w aplikację. Służy do tego manifest aplikacji.

1.2.7. Łączenie elementów projektu — manifest


W każdej aplikacji na Android musi znajdować się plik manifestu AndroidMani-
fest.xml. Plik ten, jak pokazano na listingu 1.5, łączy różne komponenty aplika-
cji i obejmuje definicje właściwości — etykiet, wersji itd.

Listing 1.5. Plik AndroidManifest.xml używany do definiowania konfiguracji

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


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.helloandroid"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".Main"
android:label="@string/app_name">
<intent-filter>
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
44 ROZDZIAŁ 1. Wprowadzenie do Androida

</activity>
</application>
</manifest>

Plik manifestu z aplikacji HelloAndroid jest prosty. Przytoczono go tu w postaci


wygenerowanej przez wtyczkę ADT. Znajduje się w nim otwierający element
manifest obejmujący właściwości wersji, nazwę pakietu, przestrzeń nazw oraz
element application z ikoną i etykietą . W obu wymienionych elementach
można umieścić więcej atrybutów. Manifest dokładniej opisujemy w rozdziale 2.
W elemencie application znajduje się element activity z nazwą i etykietą .
Łatwo zgadnąć, że to tu podana jest jedyna klasa aktywności z aplikacji Hello-
Android. W manifeście trzeba zdefiniować wszystkie klasy aktywności z danej
aplikacji, aby platforma mogła je znaleźć i zastosować.
Następny element, intent-filter, związany jest z ważnym zagadnieniem .
Filtry intencji służą w Androidzie do zarządzania aktywnościami i innymi kom-
ponentami, takimi jak usługi i odbiorniki typu BroadcastReceiver, co opisujemy
w rozdziale 5. Określają też możliwości poszczególnych aktywności. Inne aktyw-
ności nie muszą znać dokładnej nazwy danej klasy aktywności, aby z niej korzy-
stać (choć mogą to robić także za pomocą nazwy). Zamiast tego wystarczy określić
w aktywności, jakie zadanie ma wykonać — czyli intencję — a system sprawdzi,
czy zarejestrowane są aktywności realizujące daną operację. Intencje są prze-
twarzane i wykorzystywane w czasie wykonywania programu.
Filtry intencji bywają skomplikowane. Na razie jednak nie zamierzamy zaj-
mować się szczegółami — przyjrzymy się im w rozdziale 2. W tym miejscu trzeba
podkreślić, że filtr intencji z atrybutem action o wartości Main i atrybutem category
o wartości Launcher sprawia, że dana klasa aktywności pojawia się na domyślnym
ekranie wyboru aplikacji w Androidzie (jest to ekran udostępnianej w platformie
aplikacji Launcher).
Projekt jest już gotowy i rozumiesz jego podstawową strukturę. Następny
krok to uruchamianie aplikacji w środowisku Eclipse i jej debugowanie.

1.2.8. Uruchamianie i debugowanie aplikacji HelloAndroid


Uruchamianie i debugowanie aplikacji na Android w środowisku Eclipse jest
proste. Odbywa się prawie tak samo jak w przypadku dowolnego innego pro-
jektu w środowisku Eclipse. Różnica polega na tym, że w konfiguracji możesz
wybrać, czy chcesz uruchamiać aplikację na emulowanym telefonie lub innym
urządzeniu. Aby utworzyć konfigurację uruchomieniową i włączyć aplikację
HelloAndroid lub rozpocząć jej debugowanie, wykonaj następujące operacje:
Q Uruchamianie — kliknij projekt prawym przyciskiem myszy i wybierz
opcję Run As/Android Application.
Q Debugowanie — kliknij projekt prawym przyciskiem myszy i wybierz
opcję Debug As/Android Application.
1.3. Java, ale nie do końca 45

Po pierwszym uruchomieniu aplikacji powstaje konfiguracja uruchomieniowa,


którą można edytować (opcja Run/Run Configurations w środowisku Eclipse).
W konfiguracyjnym oknie dialogowym można ustawić ręczny lub automatyczny
wybór targetu (urządzenia lub emulatora), a także zmodyfikować inne opcje emu-
latora. Emulator Androida, wirtualne urządzenia Androida (ang. Android Virtual
Devices — AVD), środowisko Eclipse, wtyczkę ADT i inne mechanizmy szcze-
gółowo omawiamy w podrozdziale 1.6.
Jednak przed przejściem do szczegółów zobaczmy w kontekście działającego
programu, w jaki sposób opisane platforma i architektura umożliwiają pracę
Androida. Na początku trzeba wyjaśnić, jak Android obsługuje Javę.

1.3. Java, ale nie do końca


Każde środowisko uruchomieniowe Javy (ang. Java Runtime Environment —
JRE) składa się z dwóch elementów. Pierwszym jest biblioteka podstawowa,
która obejmuje wszystkie klasy platformy Javy, w tym związane z językiem,
siecią, współbieżnością itd. Drugi to maszyna wirtualna Javy (ang. Java Virtual
Machine — JVM), która uruchamia napisane w Javie programy przez interpre-
towanie kodu bajtowego Javy umieszczonego w zbiorze plików klas wygenero-
wanych przez kompilator Javy.
Środowisko uruchomieniowe Androida też jest oparte na tym wzorcu, jed-
nak na tym kończą się podobieństwa ze środowiskiem JRE rozwijanym przez
firmy Sun i Oracle. Biblioteka podstawowa Javy w Androidzie obejmuje inne
pakiety (choć spora ich część się powtarza), a maszyna JVM nie potrafi wczy-
tywać plików .class ani interpretować kodu bajtowego Javy. Początkowo może
się wydawać, że to niekorzystne. Nie martw się jednak — w aplikacjach na
Android będziesz mógł wykorzystać wiele bibliotek Javy. Zwykle nawet nie
będziesz zauważać, że nie jest używany kod bajtowy Javy.
Skoro więc Java w Androidzie nie jest typową Javą, to czym jest? Zagadnie-
nie to omawiamy w kilku następnych punktach. Opisujemy implementację pod-
stawowej biblioteki Javy (opartą na projekcie Apache Harmony) oraz wyjaśniamy,
które ze standardowych pakietów się w niej znajdują, a które pominięto. Ponadto
omawiamy maszynę wirtualną o nazwie Dalvik, na której działają wszystkie napi-
sane w Javie aplikacje na Android.

1.3.1. Podstawą jest Harmony


Wspomnieliśmy już, że Android to projekt o otwartym dostępie do kodu źró-
dłowego. Dotyczy to także implementacji podstawowej biblioteki Javy. Możesz
sądzić, że firma Google opracowała własną implementację Javy o otwartym dostę-
pie do kodu źródłowego lub wykorzystała kod źródłowy z projektu OpenJDK
firmy Sun (która kilka lat temu zaczęła przekształcać Javę w platformę o otwar-
tym dostępie do kodu źródłowego). Nic z tych rzeczy. Android jest oparty na
46 ROZDZIAŁ 1. Wprowadzenie do Androida

projekcie Apache Harmony, który jest inną implementacją platformy Java 5 Stan-
dard Edition, rozwijaną przez organizację Apache Software Foundation.
Skoro już mówimy o projekcie Harmony — należy wiedzieć, że choć jest on
punktem wyjścia dla podstawowej biblioteki Javy z Androida, nie jest z nią toż-
samy. Implementacja podstawowej biblioteki w Androidzie została uproszczona
i obejmuje tylko pakiety przydatne w urządzeniach przenośnych oraz te, których
nie zastąpiono właściwymi dla Androida rozwiązaniami opartymi na Javie. Co
więc umieszczono w bibliotece, a z czego zrezygnowano?

1.3.2. Używane pakiety i biblioteki


Powtórzmy to jeszcze raz — implementacja z Androida nie obejmuje całej biblio-
teki uruchomieniowej Javy. Poznanie listy dostępnych komponentów pozwoli
Ci stwierdzić, ile wiedzy o programowaniu w Javie będziesz mógł wykorzystać
i w jakim stopniu będziesz mógł używać istniejących bibliotek Javy (przeważnie
opartych na podstawowej bibliotece uruchomieniowej). Na rysunku 1.7 pokazano,
które części standardowej biblioteki Javy są zaimplementowane w Androidzie.

Rysunek 1.7.
Graficzna
reprezentacja
pakietów
najwyższego
poziomu
ze standardowego
środowiska
uruchomieniowego
Javy i ich statusu
w środowisku
uruchomieniowym
Androida
1.3. Java, ale nie do końca 47

Jak widać na rysunku 1.7, w Androidzie zaimplementowano dużą część stan-


dardowej biblioteki uruchomieniowej Javy. W przypadku większości niezaim-
plementowanych pakietów ich pominięcie w środowisku uruchomieniowym
Androida jest zrozumiałe. Zrezygnowano na przykład z implementacji bibliotek
Javy AWT i Swing, które służą do obsługi interfejsu użytkownika na kompute-
rach stacjonarnych. Jest to uzasadnione. Android udostępnia własne komponenty
interfejsu użytkownika (oparte na klasie View, co opisano wcześniej), dlatego
biblioteki AWT lub Swing nie są potrzebne. Java obsługuje też niektóre starsze
technologie, takie jak CORBA i RMI, których stosowanie w Androidzie nie ma
sensu (zresztą ma to niewielki sens także w standardowej podstawowej bibliotece
Javy, ale nie odbiegajmy od tematu).
W Androidzie zaimplementowano natomiast większość elementów środowi-
ska uruchomieniowego Javy regularnie używanych przez programistów. Jest to
większość niezbędnego pakietu java.lang i większość pakietu java.util, obej-
mującego wszystkie kluczowe struktury danych, których możesz potrzebować,
w tym listy powiązane i tablice z haszowaniem. Są to także pakiety java.io
i java.net, przeznaczone do odczytu oraz zapisu danych z plików, sieci itd.
Zaimplementowano też cały pakiet java.nio, służący do asynchronicznego odczytu
i zapisu danych.
Dostępność niektórych pakietów w Androidzie może Cię zaskoczyć. Można
używać na przykład pakietów java.sql i javax.sql. To nie pomyłka — w Andro-
idzie dostępne są klasy do łączenia się z relacyjnymi bazami danych. Jednak
choć platforma udostępnia takie klasy, nie oznacza to, że warto łączyć się ze
zdalnymi bazami danych za pomocą telefonu. W Androidzie zaimplementowano
też większość dostępnych w Javie klas do obsługi XML-a. Możliwe jest na przy-
kład przetwarzanie dokumentów XML w modelu DOM (ang. Document Object
Model) i SAX (ang. Simple API for XML). Dostępne są wszystkie potrzebne do
tego podstawowe klasy Javy. Jednak nie wszystkie opcje XML-owe dostępne
w Javie są obsługiwane w Androidzie. W ogóle brakuje obsługi technologii JAXB
(ang. Java API for XML Binding). Pominięto też technologię StAX (ang. Streaming
API for XML), choć Android udostępnia bibliotekę o podobnym działaniu. Biblio-
teka ta, XML-owy parser typu pull org.xmlpull.vl, jest popularna na platformie
Java ME z uwagi na niskie wymagania pamięciowe.
Używana w Androidzie biblioteka XML-owego parsera typu pull to przy-
kładowa biblioteka o otwartym dostępie do kodu źródłowego niezależnego pro-
ducenta, którą włączono do środowiska uruchomieniowego platformy, dzięki
czemu jest dostępna dla każdej aplikacji na Android. Platforma obejmuje też kilka
innych podobnych ważnych bibliotek. Jedną z tych, z których prawdopodobnie
będziesz korzystał najczęściej, jest Apache HttpClient API. To popularna biblio-
teka o otwartym dostępie do kodu źródłowego dostępna od około 10 lat. Jak
wskazuje nazwa, można jej używać do znacznego uproszczenia komunikacji przez
protokół HTTP. Możesz bezpośrednio korzystać z pakietu java.net Javy. Jeśli
48 ROZDZIAŁ 1. Wprowadzenie do Androida

jednak chcesz zarządzać plikami cookies, przekierowaniami, uwierzytelnianiem


i podobnymi mechanizmami, warto pomyśleć o bibliotece HttpClient. Inną ważną
niezależną biblioteką dostępną w Androidzie jest JavaScript Object Notation
(JSON) API rozwijana przez twórców serwisu json.org. W Androidzie znajduje
się uproszczona wersja popularnej biblioteki JSON API, obejmująca tylko naj-
ważniejsze klasy, niezbędne do przetwarzania łańcuchów znaków w formacie
JSON i serializowania obiektów Javy na łańcuchy znaków w tym formacie.
Wszystkie możliwości związane z siecią oraz formatami XML i JSON opisujemy
szczegółowo w rozdziale 9.
Wiedza o tym, co jest dostępne w standardowych i niezależnych bibliote-
kach, zaoszczędzi Ci wiele czasu przy tworzeniu aplikacji na Android. Oprócz
podstawowych bibliotek Javy Android obejmuje bogaty zestaw interfejsów API
zapewniających dostęp do poszczególnych elementów Androida. Elementy te
to między innymi osprzęt urządzeń, multimedia, grafika, lokalizacja, przechowy-
wane lokalnie dane itd. Więcej o tego rodzaju interfejsach API dowiesz się z omó-
wienia pakietu SDL w podrozdziale 1.6. Innym ważnym aspektem Javy w Andro-
idzie jest dostępna w nim maszyna wirtualna Dalvik.

1.3.3. Maszyna wirtualna Dalvik


Dalvik to opracowana przez firmę Google maszyna wirtualna Javy. Odpowiada
ona za wykonywanie uruchamianych na Androidzie aplikacji w Javie. Zaprojek-
towano i opracowano ją od podstaw pod kątem optymalnego działania w syste-
mach osadzonych, na przykład w telefonach komórkowych. Dalvik nie jest powią-
zany z platformą Android. Może działać w dowolnym UNIX-owym systemie
operacyjnym, w tym w zwykłym Linuksie, a także w systemach BSD i Mac OS X.
Aplikacje uruchamiane na telefonach komórkowych działają w środowisku,
w którym zarówno zasoby, jak i moc obliczeniowa są ograniczone. Dlatego Dalvik
zaprojektowano z myślą o trzech podstawowych wymaganiach. Oto one:
Q Dalvik musi działać szybko nawet na słabych procesorach.
Q Dalvik musi działać w systemach o małej ilości pamięci.
Q Dalvik musi być energooszczędny.
Stwierdzenie, że Dalvik to maszyna wirtualna Javy, nie jest w pełni prawdziwe
(łatwiej jednak przyjąć taką perspektywę, kiedy myśli się o Dalviku jak o części
Androida wykonującej aplikacje napisane w Javie, co rzeczywiście robi). Wynika
to z tego, że — jak już wspomnieliśmy — Dalvik nie potrafi interpretować kodu
bajtowego w Javie, powstającego w efekcie kompilacji programu w Javie za
pomocą narzędzia javac. W Dalviku wykorzystano niestandardowy język baj-
towy, wydajny ze względu na pamięć, na który są przekształcane pliki .class
wygenerowane przez kompilator Javy.
Kod bajtowy używany w Dalviku różni się pod kilkoma ważnymi względami
od kodu bajtowego Javy używanego przez firmy Oracle i Sun. Po pierwsze, kod
1.3. Java, ale nie do końca 49

nie jest rozbity na kilka niezależnych plików .class, lecz znajduje się w jednym
pliku .dex (skrót od Dalvik executable — „wykonywalny plik Dalvika”). Pomaga
to ograniczyć powielanie wewnętrznych struktur danych i znacznie zmniejsza
rozmiar plików. Nieskompresowany plik DEX jest mniej więcej o połowę mniej-
szy od skompresowanego pliku JAR. Po drugie, Dalvik jest oparty na rejestrach,
natomiast maszyna JVM firm Oracle i Sun — na stosie. Dlatego zbiory instruk-
cji w Dalviku są nieco bardziej złożone (do reprezentowania i interpretowania
programów potrzeba większego słownika niż w maszynie wirtualnej opartej na
stosie), natomiast wykonanie tych samych zadań wymaga mniej kodu. Efekt to
mniejsza liczba instrukcji i mniejszy rozmiar programu. Mniej instrukcji oznacza
mniej cykli procesora, a tym samym mniejsze zużycie energii. Mniejszy program
przekłada się na zajmowanie mniejszej ilości pamięci w czasie jego wykonywania.
Choć pliki DEX nie zawierają kodu bajtowego Javy, warto wiedzieć, że narzę-
dzie javac i taki kod nadal odgrywają pewną rolę. Jest tak, ponieważ kod źró-
dłowy Javy aplikacji na Android jest najpierw kompilowany do plików .class Javy.
Wykorzystanie kompilatora Javy zamiast zastępowania go wynika z kilku istot-
nych powodów. Kompilator ten wprowadza wiele optymalizacji, a kod bajtowy
Javy jest językiem programowania znacznie prostszym w obsłudze za pomocą
narzędzi. Inną zaletą zastosowanego podejścia jest to, że można użyć dowolnego
kodu z plików .class (lub .jar). Aby wykorzystać bibliotekę w aplikacji na Android,
nie trzeba mieć dostępu do jej kodu źródłowego. Po skompilowaniu kodu źró-
dłowego do plików .class są one kompilowane do plików .dex przez narzędzie dx
z Androida. Więcej na temat różnych narzędzi, w tym programu dx, dowiesz się
z podrozdziału 1.6.
Oprócz zastosowania wydajniejszego formatu DEX wprowadzono w Dalviku
także wiele innych optymalizacji, takich jak korzystanie z pamięci współdzielo-
nej, co pozwala na używanie obiektów przez różne aplikacje. Prowadzi to do
zmniejszenia ilości zajmowanej pamięci i liczby cykli mechanizmu przywraca-
nia pamięci (co także przekłada się na zmniejszenie wymogów obliczeniowych,
a tym samym niższe zużycie energii). Aby było to możliwe, w Androidzie przy
ładowaniu systemu uruchamiany jest specjalny egzemplarz maszyny wirtualnej
Dalvik, tzw. Zygote, który wstępnie wczytuje do pamięci współdzielonej dane
potencjalnie potrzebne we wszystkich aplikacjach (na przykład podstawowe
biblioteki). Maszyna wirtualna Zygote tworzy następnie nowy egzemplarz Dalvika
(swoją kopię) dla każdej uruchamianej aplikacji. Dlatego każdy proces potomny
(który jest odrębnym procesem linuksowym, co omawiamy w następnym punkcie)
ma dostęp do współdzielonych danych. Maszynę wirtualną i proces tworzenia
aplikacji przez maszynę Zygote przedstawiono na rysunku 1.8.
Tak więc Dalvik celowo różni się od standardowej maszyny wirtualnej Javy.
Wprowadzono w nim optymalizacje zaprojektowane z myślą o wyższej wydajności
i lepszym wykorzystaniu zasobów w systemach osadzanych. Maszyna wirtualna
50 ROZDZIAŁ 1. Wprowadzenie do Androida

Rysunek 1.8.
Maszyna wirtualna
Javy Dalvik
z Androida i proces
inicjowania aplikacji
przez początkową
maszynę wirtualną
Zygote

Zygote Dalvik tworzy swoje kopie dla procesu każdej aplikacji. W urządzeniu
z Androidem ostatecznie działa zatem wiele maszyn wirtualnych — wiele odręb-
nych egzemplarzy Dalvika.
DALVIK I KOMPILACJA JIT. W Androidzie 2.2 Dalvik obejmuje też
kompilator JIT. Za pomocą takiego kompilatora maszyna wirtualna Dalvik
potrafi automatycznie rozpoznawać i optymalizować fragmenty kodu w cza-
sie wykonywania programu oraz kompilować je do kodu natywnego. To
dodatkowo poprawia wydajność kodu działającego na maszynie wirtualnej
Dalvik (kodu, który w innym modelu zawsze musiałby być interpretowany
i uruchamiany jako kod bajtowy).
Android udostępnia środowisko uruchomieniowe Javy, które jest rozbudowane
(prawie) tak jak w komputerach stacjonarnych, a co lepsze — niezwykle szybkie.
Dalej omawiamy następny element stosu — system operacyjny, w którym działa
maszyna wirtualna. W Androidzie tym systemem jest specjalna wersja Linuksa.
1.4. Linux, ale nie do końca 51

1.4. Linux, ale nie do końca


W warstwie pod kodem źródłowym Javy, kodem bajtowym, platformą aplikacji
i maszyną wirtualną Dalvik Android działa na podstawie systemu operacyjnego
opartego na Linuksie. Systemy operacyjne są skomplikowane, jednak nie masz się
czego obawiać. Nawet jeśli nie wiesz zbyt dużo na ich temat, jesteś w końcu pro-
gramistą, dlatego powinieneś zrozumieć związane z nimi podstawowe zagadnienia.

1.4.1. Czy Android to Linux?


Trwają spory dotyczące tego, czy system operacyjny Android należy nazywać
Linuksem (Linux to bezpłatny system operacyjny o otwartym dostępie do kodu
źródłowego, opracowany przez Linusa Torvaldsa w latach 90. ubiegłego wieku).
Odpowiedź zależy od tego, co rozumiesz przez określenie „Linux” i jak drobia-
zgowy jesteś. Tradycyjnie Linuksem nazywano jądro, czyli rdzeń systemu opera-
cyjnego pozbawiony wszelkich dodatkowych aplikacji. Jednak kiedy ktoś nazywa
system operacyjny Linuksem, często ma na myśli dystrybucje GNU/Linux. Dys-
trybucje tego typu obejmują jądro Linuksa, zestaw standardowych aplikacji systemu
operacyjnego z projektu GNU (nie są one przeznaczone tylko dla Linuksa), a także
dodatkowe aplikacje przeznaczone dla dystrybucji. Przykładowe dystrybucje
GNU/Linux to Ubuntu, Red Hat i openSUSE. Obejmują one jądro Linuksa
(często zmodyfikowane), aplikacje GNU i inne aplikacje danego producenta.
Android jest oparty na jądrze Linuksa. Jest odgałęzieniem od głównej wersji
2.6.x, jednak nie jest dystrybucją typu GNU/Linux, ponieważ nie obejmuje
licznych aplikacji dostępnych w takich dystrybucjach (w Androidzie brakuje
przede wszystkim systemu okienkowego X11). W Androidzie nie ma nawet biblio-
teki języka C (glibc), standardowej w GNU. Zamiast tego używana jest niestan-
dardowa, znacznie uproszczona implementacja Bionic, zoptymalizowana pod
kątem urządzeń przenośnych. Oznacza to, że programy napisane dla dystrybucji
GNU/Linux na procesory x86 domyślnie nie będą działać w Androidzie (czasem
w ogóle nie da się ich uruchomić na tej platformie). Najpierw trzeba je skom-
pilować za pomocą biblioteki C używanej w Androidzie (jak już wspomnieliśmy,
jest to biblioteka Bionic).
O ANDROIDACH I PINGWINACH. Kiedy rozpoczynano tworzenie
Androida, jądro systemu operacyjnego tej platformy było zwykłym odga-
łęzieniem linuksowego jądra 2.6.x. Społeczność skupiona wokół systemu
Linux wiązała duże nadzieje z jego rozwojem, skoro taka firma jak Google
aktywnie pracowała nad usprawnianiem kodu źródłowego i udostępniała
zmiany. Jednak z uwagi na poważne modyfikacje w architekturze sterow-
ników (co po części wymuszone było niestandardowym systemem zabez-
pieczeń z Androida) udostępnionego kodu z odgałęzienia z jądrem Androida
nie można było scalić z jądrem Linuksa z linii głównej. Zirytowało to
linuksową społeczność, ponieważ programiści rozwijający sterowniki dla
52 ROZDZIAŁ 1. Wprowadzenie do Androida

urządzeń z Androidem nie mieli możliwości udostępniania innym kodu


jądra Linuksa. W efekcie w lutym 2010 roku kod jądra Linuksa udostęp-
niony przez firmę Google całkowicie usunięto z serwisu kernel.org. Obec-
nie oba projekty są prowadzone niezależnie.
Mimo nieraz zaciętych dyskusji system operacyjny Android zawsze był —
i w dużej części nadal jest — Linuksem. Niech zabawny pingwinek Tux, który
jest symbolem Linuksa, Cię nie zwiedzie. Linux to ważny gracz na rynku syste-
mów operacyjnych. System ten działa na milionach komputerów na całym świecie.
Jego elastyczna architektura, bezpieczeństwo, szybkość i stabilność sprawiają,
że sprawdza się doskonale w wielu zastosowaniach.
Nie martw się, jeśli nie masz doświadczenia w korzystaniu z systemów ope-
racyjnych opartych na Linuksie. Rzadko będziesz potrzebował bezpośredniego
dostępu do systemu operacyjnego Android, ponieważ większość związanych z nim
zadań można wykonać za pomocą interfejsu frameworka (w kontekście tworzenia
aplikacji) lub wyspecjalizowanych narzędzi dostępnych w pakiecie SDK (w kon-
tekście interakcji z użytkownikiem, na przykład przez wiersz poleceń). Uważamy
jednak, że warto znać pewne aspekty typowego systemu linuksowego, ponieważ
niektóre z nich są niezwykle ważne, aby można było zrozumieć działanie apli-
kacji na Android i interakcję z nimi (a także to, dlaczego Open Handset Alliance,
konsorcjum firm stojące za powstaniem Androida, jako podstawową platformę
wybrało Linux). Zaczynamy od obsługi plików i urządzeń w Linuksie, dalej
omawiamy model zabezpieczeń, a na zakończenie pokrótce opisujemy model
działania procesów i jego wpływ na tworzenie aplikacji.

1.4.2. Urządzenia pamięci masowej i system plików


W Linuksie, inaczej niż w systemach Microsoft Windows, urządzenia pamięci
masowej (na przykład dyski twarde, karty pamięci itd.) nie mają przypisanych liter.
W Linuksie używane jest jedno drzewo katalogów o nazwie root lub /, w którym
dowolny katalog (w tym sam katalog główny) można odwzorować na urządzenie
pamięci masowej — a dokładniej na partycję z takiego urządzenia, jednak dla
uproszczenia pomijamy to rozróżnienie.
UWAGA NA TEMAT SEPARATORÓW. W Linuksie, inaczej niż w sys-
temie Windows, w ścieżkach do plików i katalogów używane są zwykłe
ukośniki. Na przykład do pliku readme.txt z katalogu help umieszczonego
w katalogu głównym prowadzi następująca ścieżka bezwzględna:
/help/readme.txt
Jeśli katalog główny jest już wybrany, plik można wskazać za pomocą
ścieżki względnej:
help/readme.txt lub ./help/readme.txt
Kropka (.) w ścieżkach w Linuksie zawsze prowadzi do bieżącego katalogu.
1.4. Linux, ale nie do końca 53

Katalog powiązany z urządzeniem pamięci masowej to tak zwany punkt monto-


wania. Dlatego mówi się, że urządzenie jest zamontowane w pewnym katalogu.
Możliwe, że zetknąłeś się już z pojęciem „montowanie”, kiedy podłączałeś tele-
fon z Androidem do komputera. Pojawia się wtedy pytanie, czy chcesz zamon-
tować kartę SD telefonu, czyli powiązać tę kartę z katalogiem, poprzez który
będzie można uzyskać dostęp do jej zawartości.
Katalog główny zawsze musi być punktem montowania. Zwykle prowadzi do
partycji rozruchowej. Inne katalogi mogą prowadzić do innych urządzeń, takich
jak karty pamięci lub napęd DVD. Urządzenia te można montować i odmonto-
wywać w czasie działania systemu, dlatego rozwiązanie pozwala na elastyczne
zarządzanie wieloma urządzeniami i ścieżkami dostępu. Przyjrzyjmy się drzewu
katalogów egzemplarza emulatora Androida, widocznemu na rysunku 1.9 (więcej
o emulatorze i narzędziu adb używanym do uruchamiania powłoki dowiesz się
z podrozdziału 1.6).
Symbol # w drugim wierszu na rysunku 1.9 to znak zachęty w wierszu pole-
ceń administratora. Jest to konto administracyjne w systemie Linux, domyślnie
uruchamiane w emulatorze Androida. Zwykli użytkownicy widzą symbol $. Człon
ls / to polecenie. ls jest aplikacją GNU wyświetlającą zawartość katalogu lub
ścieżki. Tu podano symbol /, czyli katalog główny. Cały tekst pod wierszem
z poleceniem ls (do następnego symbolu #) to efekt uruchomienia tej instrukcji.

Rysunek 1.9.
Struktura katalogu
głównego
wyświetlona
w egzemplarzu
powłoki emulatora
za pomocą
polecenia ls

Z większości plików i katalogów zwykle nie trzeba korzystać, jednak warto wie-
dzieć, gdzie znajdują się niektóre z nich i do czego służą. W tabeli 1.2 wymie-
niono wybrane z najważniejszych lokalizacji w systemie plików Androida.
W kontekście plików i katalogów nasuwa się pewne pytanie — co z bezpie-
czeństwem i prywatnością? W jaki sposób można uchronić prywatne dane przed
dostępem innych użytkowników? Okazuje się, że Linux udostępnia w tym celu
prosty, ale skuteczny system uprawnień.
54 ROZDZIAŁ 1. Wprowadzenie do Androida

Tabela 1.2. Ważne lokalizacje w systemie plików Androida

Lokalizacja Opis
/sdcard To punkt montowania kart Secure Digital (SD), które można umieszczać w wielu
urządzeniach z Androidem. Jeśli chcesz przejrzeć zawartość takiej karty
lub skopiować na nią (albo z niej) pliki, użyj tego właśnie katalogu.
/data/app Tu Android zapisuje wszystkie zainstalowane aplikacje w spakowanej postaci
(w plikach APK).
/data/data W tym miejscu Android zapisuje dane aplikacji. Jeśli aplikacja obejmuje plik
preferencji lub dołączone niestandardowe biblioteki, znajdziesz je w tym katalogu.

1.4.3. Konta użytkowników i uprawnienia do plików


Jedną z przyczyn popularności Linuksa, zwłaszcza w środowiskach wielodostęp-
nych, jest sposób zarządzania kontami użytkowników i ściśle powiązany z tym
model uprawnień oraz prywatności. W Linuksie uprawnienia działają na pozio-
mie plików. Może się to wydawać ograniczające, jest to jednak mylne wrażenie,
które może wynikać z tego, że nie wspomnieliśmy jeszcze o pewnym nieco zaska-
kującym aspekcie Linuksa (i innych UNIX-owych systemów operacyjnych) —
mianowicie w systemie tym wszystko jest plikiem. Dyski i procesy są repre-
zentowane oraz kontrolowane za pośrednictwem plików. Aplikacje i ich ustawie-
nia to pliki. Nawet katalogi są plikami. Dlatego można kontrolować dostęp do
niemal dowolnych elementów za pomocą jednego pliku (lub większej liczby pli-
ków). Jest to widoczne w modelu zabezpieczeń Linuksa. Uprawnienia są prze-
chowywane bezpośrednio w systemie plików. Każda maska uprawnień do pliku
określa zabezpieczenia z trzech obszarów — użytkownika, grupy i innych osób
(są one powiązane z właścicielem pliku, grupą użytkowników pliku i pozosta-
łymi użytkownikami). Dla każdego z tych obszarów można odrębnie ustawiać
uprawnienia do odczytu, zapisu i wykonywania plików. Można na przykład zezwo-
lić na zapis pliku przez właściciela, ale nie przez innych użytkowników. Wywo-
łanie polecenia ls –l na pliku lub katalogu powoduje wyświetlenie uprawnień
i kilku innych ciekawych informacji (rysunek 1.10).
Rysunek 1.10.
Opisany schemat
danych wyjściowych
wygenerowanych
z zastosowaniem
polecenia ls

W danych wyjściowych z rysunku 1.10 znajduje się kilka ważnych elementów.


Opisujemy tu każdy człon od lewej do prawej. Pierwsza od lewej litera w grupie
uprawnień określa typ pliku (tu jest to litera d oznaczająca katalog). Trzy zestawy
uprawnień do odczytu, zapisu i wykonania (rwx) dotyczą użytkownika, grupy
i innych osób. Myślnik oznacza brak uprawnienia. Dalej podany jest użytkownik.
Tu właścicielem zasobu jest użytkownik system. Następnie określona jest grupa,
którą tu jest cache. Za nią znajduje się czas ostatniej aktualizacji, po czym nastę-
1.4. Linux, ale nie do końca 55

puje nazwa zasobu (jest nim katalog o nazwie cache). Przedstawione dane wyj-
ściowe informują, że użytkownik i grupa mają pełny dostęp do katalogu, a pozo-
stali użytkownicy nie mają do niego żadnych uprawnień — nie mogą nawet
wyświetlić jego zawartości.
Ten model umożliwia precyzyjne kontrolowanie zasobów (plików, katalo-
gów i innych zasobów traktowanych jak pliki). Ma to ważne skutki w kontekście
Androida. Kiedy użytkownik instaluje aplikację na telefonie z Androidem, two-
rzone jest dla niej nowe konto użytkownika. Dostęp do plików aplikacji możliwy
jest tylko z poziomu tego konta. Dlatego aplikacja działa w bezpiecznym, zam-
kniętym środowisku. Nie ma dostępu do wrażliwych plików systemowych, plików
innych aplikacji ani prywatnych danych użytkownika. Może korzystać tylko
z własnych plików i danych. Nie oznacza to, że aplikacje na Androidzie nie mogą
ze sobą współdziałać, że nie mają dostępu do danych innych programów lub że
nie można bezpośrednio kontrolować użytkowników i uprawnień. Wszystko to
jest możliwe i dowiesz się, jak to robić. Jednak domyślne ustawienia powodują
utworzenie jednego użytkownika o ograniczonych uprawnieniach na aplikację.

1.4.4. Procesy i wielozadaniowość


Ścisły model zabezpieczeń Androida obowiązuje także w procesach systemu.
Każda aplikacja na Androidzie jest uruchamiana w osobnym procesie systemu
Linux, co powoduje oddzielenie jej stanu od innych działających w tym samym
czasie procesów, a zwłaszcza od innych aplikacji. Jest tak, ponieważ proces apli-
kacji w Linuksie (i w każdym współczesnym systemie operacyjnym) ma dostęp
tylko do przypisanej mu pamięci. Nie może korzystać z pamięci zarezerwowanej
przez system operacyjny lub inne aplikacje.
Warto pokrótce wspomnieć o jeszcze jednym aspekcie — wielozadaniowości.
Choć wszystkie współczesne systemy operacyjne potrafią równolegle wykonywać
wiele procesów, możesz być przyzwyczajony do tego, że na telefonie w danym
momencie działa tylko jedna aplikacja. W Androidzie to ograniczenie nie obo-
wiązuje. Możesz tu równolegle uruchomić dowolną liczbę aplikacji.
Wielozadaniowość daje duże korzyści, ponieważ nie trzeba wychodzić z apli-
kacji przy uruchamianiu innej, co poprawia komfort pracy użytkownika. Jest to
ważne na platformach, na których interakcja między aplikacjami jest częścią ogól-
nego projektu systemu (jedną z takich platform jest właśnie Android). W projek-
cie Androida zrównoważono potencjalnie wysokie koszty jednoczesnego działania
wielu aplikacji w środowisku o ograniczonych zasobach. Android priorytetowo
traktuje aplikacje, z których użytkownik korzysta lub których używał ostatnio,
a wszystkie programy są uruchamiane na stosie. Więcej o cyklu życia aplikacji na
Android, procesach i zadaniach dowiesz się z rozdziału 3. Na razie zapamiętaj,
że platforma zarządza zasobami systemowymi przez priorytetowe traktowanie
najważniejszych w danej chwili aplikacji.
56 ROZDZIAŁ 1. Wprowadzenie do Androida

To już wszystko, co powinieneś wiedzieć na temat linuksowego pochodzenia


Androida. Jeśli chcesz dowiedzieć się czegoś więcej o samym Linuksie, istnieje
wiele dobrych książek na ten temat. Teraz, kiedy znasz już podstawy zarządzania
plikami w Linuksie oraz model działania kont, zabezpieczeń i procesów, możesz
wkroczyć w świat natywnych bibliotek Androida działających w warstwie nad
systemem operacyjnym.

1.5. Więcej możliwości dzięki bibliotekom natywnym


Teraz przyjrzymy się bibliotekom systemowym wbudowanym w platformę
Android. Witaj w świecie języków C i C++ oraz bibliotek natywnych! Biblio-
teki te są udostępniane pakietowi Android SDK poprzez interfejs JNI, dlatego
nie musisz bezpośrednio używać kodu natywnego (chyba że chcesz to robić),
warto jednak zrozumieć zależności między poszczególnymi elementami.
Pokrótce omawiamy biblioteki natywne, aby opisać warstwę pośrednią Andro-
ida. Przez pokazanie wybranych technologii dostępnych w Androidzie chcemy
wyjaśnić, co można osiągnąć z zastosowaniem tej platformy. Zaczynamy od obsza-
rów, które pierwsze przykuwają uwagę — obsługi dźwięku i filmów przy użyciu
biblioteki OpenCORE. Następnie analizujemy bibliotekę SQLite, służącą do
obsługi baz danych. Dalej omawiamy bibliotekę WebKit, która jest silnikiem
przeglądarki internetowej. Podrozdział podsumowujemy opisem czujników sprzę-
towych i aparatu fotograficznego.

1.5.1. Przetwarzanie dźwięku i wideo


Android zapewnia bogatą obsługę multimediów, w tym zaawansowane wyświe-
tlanie grafiki dwu- i trójwymiarowej za pomocą bibliotek SGL i OpenGL ES
(opisanych w rozdziale 11.). Umożliwia też odtwarzanie i nagrywanie dźwięku oraz
wideo w różnych formatach. W tym ostatnim obszarze w Androidzie wykorzy-
stano system OpenCORE firmy PacketVideo. Jest to zaawansowany framework
multimedialny zoptymalizowany pod kątem urządzeń przenośnych. Obsługuje
wiele popularnych formatów plików oraz kodeków, w tym MP3, MIDI, Ogg
Vorbis, PCM i ACC w obszarze dźwięku oraz H.263, H.264 i MPEG-4 w obsza-
rze odtwarzania wideo. Obsługiwany jest też format kontenerowy opracowany
przez grupę 3GPP.
Dzięki tego rodzaju bibliotekom audio i wideo w aplikacjach na Android
dostępne są zaawansowane funkcje multimedialne. Oprócz mechanizmów do
rekodowania filmów wideo i grania w gry trójwymiarowe Android udostępnia też
inną ważną bibliotekę — silnik bazodanowy SQLite.

1.5.2. Silnik bazodanowy


Jeśli potrzebujesz zapisać dane z aplikacji w urządzeniu, Android Ci to umoż-
liwia. Jednym z jego elementów jest SQLite — transakcyjny silnik bazodanowy
oparty na standardzie SQL-91. SQLite jest relacyjnym silnikiem bazodanowym.
1.5. Więcej możliwości dzięki bibliotekom natywnym 57

Przechowuje dane w tabelach (w teorii baz danych nazywanych relacjami), podob-


nie jak robią to MySQL, Oracle i DB2. Jednak architektura silnika SQLite
znacznie różni się od budowy konwencjonalnych systemów zarządzania bazami
danych (systemów DBMS), takich jak wymienione wcześniej.
Po pierwsze, SQLite nie wymaga architektury klient-serwer. W systemach
DBMS o architekturze klient-serwer proces serwera oczekuje na żądania przy-
chodzące od jednego procesu lub większej liczby procesów klienckich oraz
przekazuje dane tam i z powrotem w ramach komunikacji międzyprocesowej
(ang. interprocess communication — IPC; zwykle odbywa się ona poprzez
gniazda). Silnik SQLite można osadzić bezpośrednio w korzystającej z niego
aplikacji, ponieważ komunikacja odbywa się przez proste wywołania funkcji,
a nie z wykorzystaniem skomplikowanych mechanizmów IPC.
Po drugie, silnik SQLite w prawie każdym aspekcie jest mniej skomplikowany
od innych rozwiązań. Wykorzystano w nim znacznie prostszy sposób przecho-
wywania danych. Schemat, indeksy i tabele bazy danych znajdują się w jednym,
międzyplatformowym i przenośnym pliku. Dlatego tworzenie kopii bazy danych
jest niezwykle proste — wystarczy skopiować jeden plik z miejsca A do B. Sil-
nik SQLite jest ponadto niezależny i niezwykle mały. Instaluje się go jako po-
jedynczy plik biblioteczny o wielkości 200 – 300 kilobajtów (zależy to od konfi-
guracji używanej w czasie kompilacji), w którym w tylko niewielkim stopniu
wykorzystywana jest biblioteka języka C. Dodatkowo SQLite w ogóle nie wymaga
konfigurowania. Nie są potrzebne pliki konfiguracyjne lub procedury instalacji —
wystarczy umieścić bibliotekę w wybranym miejscu i zacząć jej używać. Dlatego
SQLite doskonale nadaje się do stosowania w systemach osadzonych, na przy-
kład w telefonach komórkowych.
Mimo tych uproszczeń SQLite daje duże możliwości. Ten silnik bazodanowy
obsługuje transakcje w modelu ACID (atomowe, spójne, izolowane i trwałe),
a także przeprowadza indeksowanie oparte na drzewach zrównoważonych, co
zapewnia szybki dostęp do danych. SQLite ma też jednak pewne ograniczenia.
Zapis do tabeli powoduje zablokowanie całej bazy danych, co zmniejsza szyb-
kość pracy przy wysokiej współbieżności. Zwykle w aplikacjach mobilnych taka
sytuacja nie występuje, dlatego wspomniana wada nie jest poważna. Znacznie
gorsza jest natomiast ograniczona obsługa instrukcji ALTER TABLE, co utrudnia
migracje schematów. Może to okazać się poważnym problemem przy aktuali-
zowaniu aplikacji. Utrwalanie danych za pomocą silnika SQLite opisujemy
w rozdziale 6.
Oprócz mechanizmów obsługi danych Android obejmuje też inną bibliotekę,
niezwykle ważną we współczesnym świecie z powszechnym dostępem do inter-
netu. Jest to WebKit — kompletny silnik przeglądarki internetowej.
58 ROZDZIAŁ 1. Wprowadzenie do Androida

1.5.3. Integracja z internetem


W Androidzie dostępny jest WebKit — kompletny silnik przeglądarki inter-
netowej z obsługą HTML-u, używany też w przeglądarkach Safari i Chrome.
WebKit obsługuje arkusze stylów CSS3 (osiąga imponujący wynik 100 na 100
w Acid3; jest to ważny test zgodności ze standardami internetowymi), a także
obejmuje sprawnie działający silnik do obsługi JavaScriptu (jest to silnik V8
Google’a; silnik ten w wielu bezpośrednich porównaniach okazuje się znacznie
szybszy od innych maszyn wirtualnych JavaScriptu). Aplikacja Browser preinsta-
lowana w każdym urządzeniu z Androidem jest równie rozbudowana jak dowolna
przeglądarka na komputery stacjonarne. To bardzo ważne. Silnik przeglądarki
dostępny w Androidzie nie jest uproszczony. Przeglądarka nie działa wprawdzie
tak samo jak w komputerach stacjonarnych, ale w bardzo zbliżony sposób.
Warto też wiedzieć, że bibliotekę WebKit można wykorzystać nie tylko
w aplikacji Browser. Możesz osadzać obsługiwany przez bibliotekę WebKit kod
w HTML-u bezpośrednio w aplikacjach, używając komponentu interfejsu użyt-
kownika o nazwie WebView (wykorzystano go w kilku przykładach w książce).
Pozwala to płynnie integrować aplikacje z materiałami z internetu.
Następnym obszarem integracji z bibliotekami natywnymi, z którym należy
się zapoznać, jest imponujący zestaw sterowników sprzętowych oraz obsługa czuj-
ników, aparatów fotograficznych itd.

1.5.4. Czujniki, aparat fotograficzny i inne


Oprócz obsługi multimediów, baz danych i przeglądarki Android zapewnia też
obsługę licznych czujników (co pozwala badać środowisko telefonu) oraz wbudo-
wanych aparatów fotograficznych. W najnowszej wersji Android obsługuje nastę-
pujące rodzaje czujników:
Q Moduł GPS, pozwalający dokładnie określić pozycję urządzenia (pozycję
można też ustalić na podstawie informacji z sieci komórkowej, używając
triangulacji; opisujemy to w rozdziale 9.).
Q Żyroskopy i akcelerometry, pozwalające wykryć orientację urządzenia
oraz ruch.
Q Czujniki pola magnetycznego.
Q Czujniki sztucznego światła i zbliżeniowe.
Q Czujniki temperatury.
Q Czujniki ciśnienia.
Warto zauważyć, że nie wszystkie urządzenia obejmują każdy z tych czujników.
Pierwszy telefon Google’a z Androidem (G1, nazywany też HTC Dream) miał
tylko moduł GPS, akcelerometr oraz czujniki pola magnetycznego i orientacji.
Nowsze telefony z Androidem, na przykład Droid Motoroli (w Europie nazywany
Milestone), mają też czujniki światła i zbliżeniowe. Wszystkie telefony z Andro-
idem dostępne w czasie powstawania tej książki miały wbudowany aparat foto-
1.6. Potrzebne narzędzia 59

graficzny. Pozostawiamy Twojej wyobraźni wymyślenie sposobów na wykorzysta-


nie tych technologii do budowania naprawdę innowacyjnych aplikacji. W tabeli 1.3
wymieniono aplikacje, w których już zastosowano różne czujniki.

Tabela 1.3. Lista ciekawych aplikacji, w których w innowacyjny sposób


wykorzystano czujniki z platformy Android

Nazwa aplikacji Opis


Hoccer Wykorzystuje lokalizację oraz gesty rzucania i łapania do wymiany
elementów w rodzaju kontaktów, obrazów lub plików między dwoma
telefonami. Wymiana danych nigdy nie była przyjemniejsza!
Locale Zarządza ustawieniami telefonu (na przykład głośnością dzwonka)
na podstawie lokalizacji i czasu. Możesz automatycznie wyciszyć telefon,
kiedy jesteś w domu!
Coin Flip Podaje rezultat rzutu monetą na podstawie gestu rzutu i danych
z żyroskopu. Pora zacząć przyjmować zakłady!
Bubble Wykorzystuje czujnik orientacji do symulacji działania poziomicy.
Powiedz „nie” przekrzywionym obrazom na ścianie!
Aplikacja do rozmów Wykorzystuje czujnik zbliżeniowy do ustalania, czy trzymasz telefon
telefonicznych przy uchu. Powoduje to automatyczne wyłączenie wyświetlacza w trakcie
w Androidzie rozmów w celu zmniejszenia zużycia energii!
Compass Wykorzystuje dane o polu magnetycznym do wyświetlania wirtualnego
kompasu. Już nigdy się nie zgubisz!
Barcode Scanner Wykorzystuje aparat fotograficzny do wczytywania jedno-
i dwuwymiarowych kodów kreskowych. Dzięki temu nie musisz
już wpisywać długich kodów produktów!

Istnieją też inne aplikacje, jednak tabela 1.3 powinna pomóc Ci zrozumieć, jakie
możliwości dają czujniki obsługiwane w Androidzie. Podsumowując, można
zauważyć, jak wyjątkowe połączenie sprzętu i oprogramowania przekłada się
na wyjątkowe i ekscytujące doświadczenia użytkowników.
Po omówieniu podstawowych aspektów samego Androida — czym jest,
dlaczego go stworzono, a także aplikacji, kluczowych komponentów platformy
i bibliotek natywnych — pora dokładniej przyjrzeć się standardowym narzę-
dziom programistycznym. Zaczynamy od pakietu SDK i wtyczki Eclipse ADT.

1.6. Potrzebne narzędzia


Wiemy, że nie możesz się doczekać, aby poznać więcej szczegółów na temat
aplikacji na Android. Jednak tworzenie oprogramowania to rzemiosło. Dobry
stolarz musi znać się na gwoździach i drewnie (na materiałach), a także na świ-
drach i młotkach (na narzędziach). Kiedy poznałeś już podstawy pisania aplikacji
i dowiedziałeś się co nieco o materiałach, pora bliżej przyjrzeć się narzędziom.
Android udostępnia wiele różnych narzędzi do tworzenia aplikacji, ich pie-
lęgnowania, debugowania, profilowania itd. Jedno z nich, pakiet SDK, obejmuje
biblioteki zapewniające dostęp do wszystkich funkcji urządzenia (od wysyłania
SMS-ów po określanie współrzędnych geograficznych), a także bogaty framework
aplikacji opracowany w taki sposób, aby tworzenie programów było proste, a ilość
60 ROZDZIAŁ 1. Wprowadzenie do Androida

szablonowego kodu — minimalna. Obok interfejsów API pakiet SDK obejmuje


też duży zestaw niezwykle przydatnych programów uruchamianych z wiersza
poleceń. Dla obu wymienionych części pakietu SDK istnieje też pomocna
nakładka z graficznym interfejsem użytkownika. Ma ona postać środowiska IDE
Eclipse i wtyczki ADT dla tego środowiska.

1.6.1. Interfejsy API przeznaczone dla Androida


Pakiet SDK Androida udostępnia prawie wszystkie potrzebne podstawowe funk-
cje Javy. Można z nich korzystać poprzez oparte na Apache Harmony podsta-
wowe biblioteki JVM opisane w podrozdziale 1.3. Dostępne są główne pakiety
java i javax, a także niezależne biblioteki ogólnego użytku do obsługi sieci,
przetwarzania danych w formacie XML i wykonywania podobnych zadań. Co
jednak z bibliotekami do korzystania z mechanizmów właściwych dla Androida?
Co z interakcją ze sprzętem, pracą z dźwiękiem i wideo, używaniem sieci lokal-
nych itd.? Obsługę tych elementów zapewnia następna warstwa pakietu SDK
Android — kod z przestrzeni nazw android.
W świecie Javy obok podstawowych bibliotek dostępne są mechanizmy
przeznaczone dla Androida, umieszczone w pakiecie android. Chcesz odtwa-
rzać pliki MP3? Zajrzyj do pakietu android.media. Potrzebujesz ustalić lokaliza-
cję geograficzną użytkownika? Użyj pakietu android.location. A może chcesz
połączyć się z innym urządzeniem z Androidem za pośrednictwem Bluetooth?
Przyjrzyj się pakietowi android.bluetooth. Większość telefonów ma aparat fotogra-
ficzny. Możesz uzyskać dostęp do niego z wykorzystaniem klasy Camera z pakietu
android.hardware (znajdziesz tam także inne sprzętowe interfejsy API). Skoro
już jesteśmy przy funkcjach telefonu — co z wykonywaniem połączeń telefonicz-
nych i wysyłaniem wiadomości tekstowych? Te tradycyjne funkcje telefonów
komórkowych dostępne są w pakiecie android.telephony.
Obok obsługi multimediów i sprzętu inną ciekawą funkcją Androida jest fan-
tastyczna grafika. Jest to oczywiście ważne dla twórców gier, jednak atrakcyjna
grafika jest pożądaną cechą każdej aplikacji. Pakiet android.graphics obejmuje
wiele łatwych w użyciu interfejsów API do obsługi prostych elementów graficz-
nych, takich jak rysunki, kolory i wielokąty. Do tworzenia bardziej złożonej grafiki
trójwymiarowej służy pakiet android.opengl. Znajdziesz w nim przeznaczoną
dla Androida implementację biblioteki OpenGL ES do obsługi grafiki trójwy-
miarowej.
A CO Z PROGRAMOWANIEM NATYWNYM? Biblioteki podstawowe
z pakietu SDK i framework aplikacji (podobnie jak zdecydowana więk-
szość kodu aplikacji na Android) są napisane w czystej Javie. Istnieje
jednak odpowiednik pakietu SDK oparty na językach C i C++ — pakiet
NDK. Jest on dodatkiem do pakietu SDK i współdziała z nim. Za pomocą
pakietu NDK można pisać kod bezpośrednio w językach C i C++, cał-
1.6. Potrzebne narzędzia 61

kowicie pomijając Javę oraz maszynę wirtualną Dalvik. Jak może odgadłeś,
ma to służyć zwiększeniu wydajności. Pakiet NDK obejmuje wszystkie
nagłówki potrzebne do dowiązywania kodu natywnego, a także narzędzia do
tworzenia bibliotek natywnych i osadzania ich w aplikacjach na Android.
Powiązanie pakietu SDK Androida z Javą staje się w pełni widoczne po połącze-
niu interfejsów API z pakietu android z podstawowymi bibliotekami Javy i klu-
czowymi niezależnymi komponentami. Suma tych elementów to rozbudowana
platforma do tworzenia aplikacji. Oprócz interfejsów API pakiet SDK Androida
udostępnia też kilka ważnych narzędzi uruchamianych z wiersza poleceń.

1.6.2. Narzędzia i komponenty pakietu SDK


Skoro już mówimy o narzędziach — pakiet SDK udostępnia ich wiele. Obej-
muje między innymi narzędzia do kompilowania kodu źródłowego aplikacji do
zrozumiałych dla maszyny wirtualnej Dalvik plików klas .dex, do umieszczania
kodu w plikach APK używanych przez urządzenia z Androidem, do uruchamia-
nia emulatora Androida, rejestrowania zdarzeń, debugowania w trybie „na żywo”,
profilowania wydajności itd.
Niektóre z tych narzędzi wykorzystaliśmy przy tworzeniu aplikacji Hello-
Android za pomocą wtyczki Eclipse ADT w podrozdziale 1.2. Tu omawiamy je
dokładniej. Wymieniona wtyczka opakowuje wiele narzędzi i automatycznie je
uruchamia. To wygodna funkcja. Możesz samodzielnie korzystać z omawianych
tu narzędzi (często są one niezwykle przydatne), jednak nie jest to konieczne.
Zachęcamy Cię do tego, abyś poznał narzędzia i wiedział, do czego służą, ponie-
waż pozwala to lepiej zrozumieć Android. Wiedza ta ułatwia identyfikowanie
i rozwiązywanie problemów, jednak jeśli chcesz, możesz ograniczyć się do uży-
wania wtyczki ze środowiska Eclipse.
Zanim przejdziemy dalej, trzeba wyjaśnić, że narzędzia Androida należą do
dwóch kategorii. Są to: narzędzia podstawowe i narzędzia przeznaczone dla
platformy. Jednym ze skomplikowanych aspektów tworzenia aplikacji na Android
jest to, że trzeba uwzględnić wiele dostępnych wersji interfejsów API lub plat-
form Androida. Twórcy pakietu SDK o tym wiedzą. Możesz wraz z nim zain-
stalować komponenty z wielu platform. Jest to zdecydowanie wygodniejsze niż
konieczność instalowania wielu pakietów SDK!
Po zainstalowaniu pakietu SDK i wybranych platform narzędzia znajdziesz
w kilku lokalizacjach. Podstawowe narzędzia pakietu SDK są umieszczone
w katalogu <sdk>/tools (warto go dodać do zmiennej PATH, co pozwala wygodnie
korzystać z narzędzi w dowolnym miejscu). Narzędzia przeznaczone dla plat-
formy znajdują się w katalogu <sdk>/platform-tools. W tabeli 1.4 wymieniono
wybrane ważne narzędzia i opisano ich przeznaczenie.
Przegląd zaprezentowany w tabeli 1.4 nie jest wyczerpujący. Możesz szybko
zapoznać się z opisem każdego dostępnego narzędzia i instrukcjami dotyczącymi
62 ROZDZIAŁ 1. Wprowadzenie do Androida

Tabela 1.4. Wybrane ważne narzędzia Androida uruchamiane z wiersza poleceń

Narzędzie Lokalizacja Opis

aapt <sdk>/platform-tools Android Asset Packaging Tool. Służy


do kompilowania zasobów do jednostek
binarnych i tworzenia archiwów (plików APK).
aidl <sdk>/platform-tools Android Interface Definition Language.
Kompiluje używane do definiowania interfejsów
pliki .aidl na potrzeby komunikacji
międzyprocesowej w Androidzie.
dx <sdk>/platform-tools Wczytuje kod bajtowy z pliku .class
i przekształca go na kod bajtowy Androida
(przechowywany w plikach .dex).
adb <sdk>/platform-tools Android Debug Bridge. Aplikacja typu
klient-serwer służąca do interakcji
z urządzeniami i emulatorami oraz zarządzania
nimi. Udostępnia wiele poleceń pomocniczych.
android <sdk>/tools Służy do tworzenia i usuwania urządzeń
wirtualnych Androida (egzemplarzy emulatora).
Pozwala także tworzyć i aktualizować projekty
z poziomu wiersza poleceń. Służy też do
zarządzania komponentami platformy SDK.
ddms <sdk>/tools Dalvik Debug Monitor Service. Służy
do debugowania i sprawdzania uruchomionych
aplikacji z Androida. Udostępnia interfejs
do rejestrowania zdarzeń, statystyki na temat
wątków, informacje o stanie itd. Pozwala też
kierować do urządzenia lub egzemplarza
emulatora testowe połączenia, SMS-y i dane
o lokalizacji.
draw9patch <sdk>/tools Służy do rysowania obrazów typu NinePatch.

emulator <sdk>/tools Oparty na QEMU emulator urządzeń


przenośnych.
hierarchyviewer <sdk>/tools Służy do wyświetlania i optymalizowania
hierarchii układu interfejsu użytkownika.
layoutopt <sdk>/tools Służy do szybkiego analizowania układu
i rekomendowania optymalizacji.
mksdcard <sdk>/tools Służy do tworzenia obrazów pamięci
zewnętrznej (kart SD) dla egzemplarzy
emulatora.
sqlite3 <sdk>/tools Służy do eksplorowania baz danych SQLite
i używania ich.
traceview <sdk>/tools Służy do analizowania plików śladów
(migawek używanych do profilowania aplikacji
z Androida).

jego użytkowania, wywołując je z poziomu wiersza poleceń bez argumentów


(a czasem przez podanie jako jedynego argumentu członu --help). Szczegółowe
omówienie wszystkich narzędzi znajdziesz w internetowej dokumentacji pakietu
SDK. Tu opisujemy wybrane podstawowe narzędzia, co pomoże Ci zrozumieć,
które z nich służą do wykonywania poszczególnych zadań. Inne ważne narzędzia
omawiamy dalej. Zaczynamy od kompilowania kodu za pomocą kompilatora dx.
Jak już wyjaśniono, Android zwykle korzysta z języka programowania Java,
jednak pliki binarne umieszczane w urządzeniu nie są plikami .class Javy uru-
1.6. Potrzebne narzędzia 63

chamianymi w maszynie wirtualnej tego języka. Zamiast tego, co opisujemy


w podrozdziale 1.3, używane są pliki .dax, działające w maszynie wirtualnej
Dalvik. Programiści Javy są przyzwyczajeni do korzystania z kompilatora Javy
javac i kompilowania plików źródłowych Javy do plików .class tego języka. Choć
maszyna wirtualna Dalvik nie korzysta z takich plików, to kompilator Javy jest
potrzebny, co wyjaśniamy na rysunku 1.11.

Rysunek 1.11. Proces kompilowania i tworzenia pakietów w Androidzie obejmuje


etapy kompilacji oraz ostateczne tworzenie pliku APK

Narzędzie dx jest przeznaczone dla platformy, co nie jest zaskoczeniem. Pobiera


ono wszystkie pliki .class z aplikacji i tworzy jeden plik .dex. Plik ten stanowi dane
wejściowe w ostatnim etapie większego procesu tworzenia pakietu z aplikacją.
Służy do tego narzędzie aapt, wspomniane w punkcie 1.2.6, w omówieniu pliku R.
Narzędzie aapt odpowiada za budowanie i kompilowanie aplikacji. Co jednak
z jej uruchamianiem i debugowaniem? W podrozdziale 1.2 wspomniano, że przed
uruchomieniem aplikacji trzeba utworzyć urządzenie AVD, w którym aplikacja
może działać. Takie urządzenie to obraz emulatora z konkretną wersją systemu
operacyjnego Android i określonym sprzętem (ważny jest przede wszystkim
wyświetlacz). Do tworzenia obrazów można zastosować inne narzędzie —
Android SDK and AVD Manager.
Narzędzie android służy do zarządzania urządzeniami AVD, aktualizowania
i instalowania platform oraz aktualizowania samego pakietu SDK (pamiętaj, że
pakiet ten ma budowę modułową). Aby utworzyć nowe urządzenie AVD, należy
użyć następującego polecenia:
android create avd –t <PLATFORMA> -n <NAZWA>

Przykładowa instrukcja android create avd -t android-7 -n avd21 powoduje


utworzenie urządzenia AVD o nazwie avd21 przeznaczonego na platformę
android-7. Łańcuch znaków android-7 określa platformę Androida (nazywaną też
64 ROZDZIAŁ 1. Wprowadzenie do Androida

poziomem interfejsu API). Aby wyświetlić listę dostępnych platform, użyj pole-
cenia android list target. Instrukcja android –help powoduje wyświetlenie
wszystkich opcji narzędzia android. Jeśli nie chcesz uczyć się ich wszystkich na
pamięć, możesz wywołać narzędzie android bez argumentów — pojawi się gra-
ficzny interfejs, w którym można uruchomić dowolne polecenie; przedstawiono
go na rysunku 1.12.

Rysunek 1.12. Interfejs narzędzia android z widocznym narzędziem Android SDK


and AVD Manager

Na rysunku 1.12 widać, że interfejs graficzny narzędzia android służy też do


uruchamiania urządzeń AVD (czyli do włączania emulatora Androida). Aby wyko-
nać tę operację w interfejsie graficznym, należy zaznaczyć urządzenie AVD i klik-
nąć przycisk Start w prawej kolumnie. Emulator można też uruchomić z wiersza
poleceń, używając narzędzia emulator:
emulator –avd <AVD NAME>

Przykładowe polecenie emulator –avd avd21 powoduje uruchomienie urządzenia


AVD o nazwie avd21. W narzędziu android i do utworzonych przy jego użyciu
emulatorów można stosować także wiele innych opcji. Szczegółowe informacje
znajdziesz systemie pomocy i w dokumentacji. Na rysunku 1.13 pokazano emu-
lator z uruchomionym obrazem platformy Android 2.1.
Po zapoznaniu się z narzędziami do tworzenia i uruchamiania urządzeń AVD
pora przejść do wywoływania zapytań o dostępne urządzenia i instalowania
aplikacji na emulatorze. Służy do tego narzędzie, które dobrze poznasz — Android
Debug Bridge (adb). Narzędzie adb to główny punkt dostępu do działającego
urządzenia AVD. Ma wiele funkcji. Zachęcamy do zapoznania się z nimi. Podob-
nie jak w innych narzędziach, listę opcji możesz zobaczyć po wprowadzeniu
1.6. Potrzebne narzędzia 65

Rysunek 1.13.
Emulator Androida
z uruchomionym
obrazem AVD
skonfigurowanym
dla wersji 2.1
platformy Android

instrukcji adb –help. Aby wyświetlić podłączone lub działające (utworzone i uru-
chomione) urządzenia, użyj polecenia adb devices. Do instalowania aplikacji
(po upewnieniu się, że emulator działa) służy następująca instrukcja:
adb install <aplikacja>

Przykładowa instrukcja adb install MyApp.apk powoduje zainstalowanie aplikacji


MyApp.apk na działającym emulatorze (polecenie to działa tylko wtedy, kiedy
uruchomiony jest jeden emulator; jeśli jest ich więcej, należy wskazać jeden
z nich). Tego samego polecenia adb możesz użyć do zainstalowania aplikacji na
fizycznym urządzeniu. Wygodnym sposobem na przekierowywanie poleceń adb
tam i z powrotem między pojedynczym emulatorem a pojedynczym urządze-
niem fizycznym jest stosowanie opcji –e i –d (adb –e install i adb –d install).
Narzędzia adb można też użyć do połączenia się z urządzeniem (polecenie
adb shell) i zbadania zawartości wirtualnego systemu. W podrozdziale 1.4 wyja-
śniono, że jądro Androida jest oparte na Linuksie, a powłoka daje dostęp do
wiersza poleceń. Sama powłoka udostępnia wiele przydatnych poleceń pomoc-
niczych. Zachęcamy do zapoznania się z nimi. Po uruchomieniu emulatora i zain-
stalowaniu aplikacji narzędzie adb można wykorzystać do śledzenia plików dzien-
nika lub wykonywania zrzutów informacji przydatnych do debugowania.
Bardziej szczegółowa analiza urządzenia wymaga innego niezwykle przydatnego
narzędzia pakietu SDK — programu Dalvik Debug Monitor (ddms). Aplikacja ta
ma interfejs graficzny i wyświetla różnego rodzaju informacje diagnostyczne
z emulatora lub urządzenia. Na rysunku 1.14 pokazano działanie tego narzędzia.
Rysunek 1.14 pozwala zrozumieć możliwości ddms. Narzędzie to można podłą-
czyć do maszyny wirtualnej Dalvik z daną aplikacją i wyświetlić szczegółowe
informacje z dziennika, a także dane na temat alokacji i przywracania pamięci.
Są one cenne, ponieważ aplikacje mobilne często mają dostęp do ograniczonej
ilości pamięci, dlatego wiedza na temat sposobu jej wykorzystania jest bardzo
66 ROZDZIAŁ 1. Wprowadzenie do Androida

Rysunek 1.14. Korzystanie z narzędzia Dalvik Debug Monitor (DDMS)


do sprawdzania sterty działającej aplikacji. Narzędzie to ma też wiele innych funkcji

istotna. Narzędzie ddms można też wykorzystać do dopracowania działania emu-


latora. Możesz na przykład zmienić szybkość połączenia sieciowego emulatora.
Komputer używany do programowania prawdopodobnie jest podłączony do sieci
LAN lub łącza szerokopasmowego, które są znacznie szybsze niż typowe połą-
czenia do transferu danych w telefonach komórkowych. Możesz też zasymulować
przychodzące połączenia telefoniczne lub wiadomości tekstowe, a nawet przeka-
zać do urządzenia sfabrykowane współrzędne GPS. W zależności od tego, jakie
operacje aplikacja wykonuje, może to zwiększyć realizm testów emulatora, a tym
samym ich wartość.
Więcej informacji o tych narzędziach znajdziesz w dalszych rozdziałach. Oma-
wiamy w nich także inne narzędzia, pominięte w tym miejscu. W rozdziale 4.
opisujemy na przykład draw9patch, layoutopt i hierarchyviewer, a w kilku dalszych
przykładach wykorzystano narzędzie adb. Najważniejsze jest, aby wiedzieć, gdzie
są one dostępne i jakie możliwości oferują. Choć korzystanie z narzędzi urucha-
mianych z wiersza poleceń nie jest konieczne, są one pomocne przy diagnozo-
waniu i rozwiązywaniu problemów, a także oferują pewne zaawansowane opcje
niedostępne we wtyczkach środowiska IDE.
1.7. Podsumowanie 67

1.7. Podsumowanie
Witaj w świecie Androida. Mamy nadzieję, że to krótkie wprowadzenie rozbu-
dziło Twój apetyt oraz zwiększyło chęci do nauki i tworzenia. W końcu eko-
system Androida ma umożliwiać programistom pisanie wysokiej jakości aplikacji,
co z kolei pozwala na rozwój całej platformy.
Na tym etapie podróży powinieneś dobrze rozumieć, czym jest Android i dla-
czego go opracowano. W kontekście Androida powtarza się pewien motyw —
jest to oprogramowanie o otwartym dostępie do kodu źródłowego. Platforma jest
otwarta i każdy może z niej korzystać. Kod jest otwarty i można go dostosować
do różnych potrzeb. Nawet narzędzia są otwarte, dlatego programiści mogą
wybierać, w jaki sposób będą rozwijać aplikacje. Nie można pominąć znaczenia
otwartego charakteru platformy — to on odróżnia Android od innych rozwiązań.
Oprócz ogólnego obrazu Androida powinieneś też znać architekturę tej
platformy. Dowiedziałeś się, że Android jest oparty na Javie i Linuksie, ale nie
na ich standardowych wersjach. Zobaczyłeś też, że Android udostępnia bogatą
w funkcje warstwę pośrednią, znajdującą się między specjalną maszyną wirtualną
Dalvik (i podstawowymi bibliotekami Javy oraz frameworkiem aplikacji) a war-
stwą systemu operacyjnego. Architektura ta ma służyć optymalizacji środowiska
wykonywania aplikacji w urządzeniach przenośnych.
Przejdźmy do samych aplikacji. Zobaczyłeś, co jest potrzebne do utworzenia
podstawowej aplikacji — kod źródłowy, układy, zasoby, manifesty itd. Obok
składników aplikacji poznałeś narzędzia i komponenty pakietu SDK Androida,
a także środowisko IDE Eclipse i wtyczkę ADT. Są to najważniejsze elementy
potrzebne przy tworzeniu aplikacji na Android. Teraz pora zrobić następny krok
i przejść do szczegółów związanych z podstawami rozwijania aplikacji na Android.
68 ROZDZIAŁ 1. Wprowadzenie do Androida
Podstawy tworzenia
aplikacji na Android

W tym rozdziale
Q Podstawowe cegiełki
Q Manifest aplikacji
Q Korzystanie z zasobów, układów, widoków i kontrolek
Q Adaptery, intencje i filtry intencji

Zawsze wolę podziw wynikający ze zrozumienia od podziwu wynika-


jącego z ignorancji.
Douglas Adams
Aby opracować solidną aplikację na Android, trzeba zacząć od podstaw. Ta sama
zasada obowiązuje, gdy chcemy zyskać jakąkolwiek nową umiejętność czy osią-
gnąć sukces sportowy. Na tym etapie trener koszykówki wygłasza mowę o tym,
że najpierw trzeba się nauczyć kozłować i podawać, a dopiero potem doskonalić
wsady. Aby poznać zaawansowane techniki, trzeba dobrze opanować podstawy.
Dlatego w tym rozdziale koncentrujemy się na podstawowych cegiełkach
będących budulcem aplikacji na Android. Oznacza to, że wracamy do bazowych
zagadnień z rozdziału 1., rozszerzamy je i uzupełniamy o dodatkowe szczegóły.
Przyglądamy się bliżej wszystkim elementom aplikacji. Zaczynamy od manifestu
i zasobów, następnie analizujemy układ i widoki, a potem — aktywności i inten-
cje. Używamy też klasy Adapter do wiązania danych z kontrolkami. W końcowej
części poruszamy kwestię przekazywania danych między aktywnościami, do

69
70 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

czego służy obiekt Application. Wszystkie omawiane zagadnienia dotyczą stan-


dardowych sposobów wykonywania prostych zadań na platformie Android. Są
to podstawy, które trzeba poznać przed przejściem do zaawansowanych zagadnień
w dalszych częściach książki.
Utworzymy nową przykładową aplikację, w której będą uwzględnione wszyst-
kie wymienione kwestie. Aplikacja ta nie będzie prosta, ale nie będzie też spe-
cjalnie skomplikowana. Wynika to z tego, że chcemy omówić szeroki zakres naj-
ważniejszych technik programowania używanych w wielu aplikacjach na Android,
a jednocześnie zachować względną prostotę. Aplikacja ta nie będzie wyrafino-
wana ani piękna, ale umożliwi wykonanie postawionych jej zadań. Jeśli chcesz,
aby Twoje aplikacje wyglądały elegancko, poczekaj do rozdziału 4. Jeżeli chcesz
dodać więcej funkcji, znajdziesz je w dalszych rozdziałach. Na razie musisz zado-
wolić się skromną aplikacją, którą docenić może tylko programista. Wystarczy już
tego wprowadzenia — poznaj aplikację DealDroid.

2.1. Aplikacja DealDroid


DealDroid to ciekawa aplikacja wyświetlająca
oferty dnia (Daily Deals) z eBaya. Co ważniejsze,
pokazano w niej wiele podstawowych kompo-
nentów i standardowych technik stosowanych
w licznych aplikacjach na Android. Czym jest
DealDroid? Zacznijmy od ofert dnia z eBaya.
Oferty dnia to popularny mechanizm z wi-
tryny eBay wyświetlający ograniczone czasowo
oferty dla osób zainteresowanych zakupami
w internecie. Oferty te możesz zobaczyć na
stronie http://deals.ebay.com. Jednak kto chciałby
oglądać je na nudnej stronie, skoro mogą być
wyświetlone w wygodnej aplikacji na Android?
Widzisz, do czego zmierzamy? Na rysunku 2.1 Rysunek 2.1. Ekran
pokazano ekran początkowy aplikacji DealDroid początkowy aplikacji
z wyświetlonymi ofertami dnia z eBaya. DealDroid wyświetlający
wyróżnione oferty dnia
w widoku ListView Androida

POBIERZ PROJEKT DEALDROID. Kod


źródłowy projektu i pakiet APK z aplikacją do uruchomienia znajdziesz
w witrynie z kodem do książki Android w praktyce. Ponieważ niektóre
listingi skrócono, abyś mógł skoncentrować się na konkretnych zagad-
nieniach, zachęcamy do pobrania kompletnego kodu źródłowego i śle-
dzenia go w środowisku Eclipse (albo innym środowisku IDE lub edyto-
rze tekstu).
Kod źródłowy: http://mng.bz/r560, plik APK: http://mng.bz/ARip.
2.1. Aplikacja DealDroid 71

Początkowy ekran aplikacji DealDroid, trafnie


nazwany DealList, wyświetla listę wyróżnionych
ofert z danego dnia. Lista ta jest oparta na danych
z eBaya, które — jak wskazuje nazwa — zmie-
niają się każdego dnia. Czasem modyfikacje są
wprowadzane częściej niż codziennie, ponieważ
oferty są wyprzedawane i zastępowane przez
nowe propozycje. Aplikacja nie tylko wyświetla
bieżące oferty dnia, ale też inne propozycje
z różnych kategorii, takich jak gadżety i moda.
Kiedy znajdziesz interesującą ofertę, możesz wy-
świetlić szczegółowe informacje przez jej klik-
nięcie i przejście do ekranu DealDetails, przed-
stawionego na rysunku 2.2.
To już w zasadzie wszystko na temat pod-
stawowego interfejsu użytkownika aplikacji Deal-
Droid. Jak wspomnieliśmy, jest on dość prosty Rysunek 2.2. Ekran DealDetails
i niezbyt elegancki. Wyjdźmy jednak poza inter- aplikacji DealDroid wyświetla
informacje na temat konkretnej
fejs użytkownika. Co możesz zrobić oprócz wy- oferty. Można do niego przejść
świetlenia oferty? DealDroid umożliwia prze- przez kliknięcie oferty
na ekranie DealList
słanie jej e-mailem do znajomego za pomocą
wbudowanego klienta pocztowego z Androida.
Ponadto DealDroid pozwala podzielić się ofertą inną drogą, na przykład za po-
średnictwem Facebooka lub Twittera (lub innej powiązanej aplikacji do wymiany
informacji). Jeśli oferta spodobała Ci się tak bardzo, że chcesz kupić dany pro-
dukt, DealDroid uruchamia doskonałą przeglądarkę z Androida i otwiera mo-
bilną wersję serwisu eBay. Na rysunku 2.3 przedstawiono menu do dzielenia się
informacjami i działanie wybranych opcji.

Inne funkcje aplikacji DealDroid


DealDroid może uruchomić w tle usługę (obiekt Service), aby wykrywać nowe
oferty zaraz po ich pojawieniu się (możliwe, że oferta się skończyła i została zastą-
piona lub aplikacja działała w momencie wprowadzenia nowych ofert z danego
dnia) i przesyłać wtedy powiadomienie (obiekt Notification). Nie uwzględniamy
tych funkcji w tym rozdziale, ponieważ usługi omówiono w rozdziale 5. Dlatego
pakiet z kodem źródłowym dołączony do książki obejmuje dwie wersje aplikacji
DealDroid — podstawową, opisaną w tym rozdziale, i wersję DealDroidWithService,
obejmującą kilka odbiorników typu BroadcastReceiver i działającą w tle usługę.

Teraz, kiedy już wiesz, jakie możliwości ma aplikacja, pora rozłożyć ją na części
i zobaczyć, jak działa. To moment, w którym możesz wybrać czerwoną lub niebie-
ską pigułkę. Jeśli nie chcesz się dowiedzieć, co kryje się „pod maską” Androida,
72 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

zakończ lekturę w tym miejscu!


W przeciwnym razie czytaj dalej
i przygotuj się na przerażające
szczegóły funkcjonowania aplikacji
na Android.

2.2. Podstawowe cegiełki


Jednym z najwartościowszych as-
pektów platform takich jak Android
jest framework aplikacji. Zapewnia
on nie tylko dostęp do czujnika GPS
urządzenia i obsługę żądań HTTP,
ale też strukturę, w którą można
wpasować aplikację. Korzystanie
z niego jest jak przyjęcie umowy.
Jeśli umieścisz dane pliki w odpo-
wiednich miejscach, framework wy-
korzysta te pliki w znany, określony
sposób. Otrzymujesz w ten sposób
schemat, którym możesz się posłu-
giwać. Wiele szablonowych zadań
jest wykonywanych za programistę,
który dzięki temu może skoncentro-
wać się na aplikacji. Masz mniej Rysunek 2.3. DealDroid dobrze obsługuje
powodów do zmartwień, ponieważ dzielenie się informacjami poprzez inne
aplikacje. Wybranie w telefonie przycisku
platforma wiele operacji wykonuje menu na stronie ze szczegółami powoduje
za Ciebie. To kluczowa, choć często wyświetlenie opcji dzielenia się informacjami
pomijana podstawa sukcesów (więk-
szych w porównaniu z mobilnymi aplikacjami sieciowymi) natywnych platform
dla aplikacji mobilnych (na przykład platformy Android). Programista mobilnych
aplikacji sieciowych pod wieloma względami ma większą swobodę, jednak ma
też więcej żmudnych prac do wykonania i więcej zmartwień. Pod różnymi
względami łatwiej jest tworzyć rozwiązania na platformy natywne, na przykład
na Android. Na rysunku 2.4 przedstawiono główne komponenty udostępniane
w Androidzie na potrzeby rozwijania aplikacji.
W rozdziale 1. przedstawiliśmy krótki przegląd podstawowej aplikacji na Android.
Teraz nadeszła pora na zdefiniowanie jej komponentów, a w dalszych punktach
dowiesz się więcej na temat każdego z nich. Aplikacje na Android obejmują przede
wszystkim kilka podstawowych elementów. Oto one:
Q Aktywność (obiekt klasy Activity). Działa na pierwszym planie. Zarządza
interfejsem użytkownika oraz obsługuje zdarzenia i interakcje.
2.2. Podstawowe cegiełki 73

Q Usługa (obiekt klasy


Service). Działa w tle.
Może obsługiwać
długotrwałe zadania
lub monitorowanie.
Q Odbiornik typu
BroadcastReceiver.
Obejmuje metody
obsługi zdarzeń
uruchamiane przez
rozpowszechniane
zdarzenia (intencje)
i reagujące na nie.
Q Dostawca treści (obiekt
klasy ContentProvider).
Umożliwia udostępnianie
innym aplikacjom
interfejsu API do
wymiany danych.
Elementów tych będziesz używał
do tworzenia ekranów interfejsu
użytkownika i procesów działają-
cych w tle, a także do reagowania
Rysunek 2.4. Najważniejsze komponenty
na zdarzenia określonego rodzaju. aplikacji dostępne na platformie Android
Do tworzenia tych elementów
i ich łączenia służą dodatkowe
komponenty.
Q Widoki (obiekty klasy View). Są to elementy interfejsu użytkownika
wyświetlane na ekranie.
Q Układy (obiekty klasy Layout). Są to hierarchie widoków kontrolujące format
i wygląd ekranu.
Q Intencje (obiekty klasy Intent). Są to komunikaty łączące komponenty
ze sobą.
Q Zasoby (obiekty klasy Resource). Są to elementy zewnętrzne, na przykład
łańcuchy znaków i obiekty graficzne (obrazki).
Q Manifest (obiekt klasy Manifest). Obejmuje konfigurację aplikacji.
Więcej o tych elementach dowiesz się z dalszych podrozdziałów, w których krok
po kroku przedstawiono przykładową aplikacją. Zaczynamy od dolnej warstwy —
od pliku manifestu, w którym zdefiniowane są relacje, możliwości, uprawnienia
i konfiguracja każdej aplikacji na Android.
74 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

2.3. Manifest aplikacji


Jak pokazano w rozdziale 1., manifest aplikacji jest punktem wyjścia do two-
rzenia każdej aplikacji na Android. To nie żadna przesada, lecz czysta prawda.
Kiedy użytkownik uruchamia aplikację na Android, pierwszą rzeczą, jaką wyko-
nuje system operacyjny Androida, jest wczytanie manifestu aplikacji. Dzieje się
to jeszcze przed rozpoczęciem wykonywania kodu, ponieważ to manifest określa,
które fragmenty kodu należy uruchomić. Jest to zgodne z tradycyjnym modelem
wykonywalnych aplikacji Javy. Polega on na tym, że aplikacja jest pakowana do
pliku .jar wraz z plikiem manifestu, informującym maszynę wirtualną Javy, która
klasa (w pliku .jar) jest punktem wejścia do aplikacji. W świecie Androida jed-
nostkami pracy są aktywności. W pliku manifestu aplikacji trzeba wskazać, która
klasa aktywności jest punktem wejścia do aplikacji. Przyjrzyjmy się konkretnemu
przykładowi. Na listingu 2.1 przedstawiono plik manifestu aplikacji DealDroid.

Listing 2.1. Plik manifestu AndroidManifest.xml aplikacji DealDroid

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


<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.dealdroid"
android:versionCode="1"
android:versionName="1.0">

<application
android:icon="@drawable/ddicon"
android:label="@string/app_name"
android:name=".DealDroidApp">

<activity
android:name=".DealList"
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
android:name=".DealDetails"
android:label="@string/deal_details" />
</application>

<uses-permission android:name="android.permission.INTERNET" />


<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-sdk android:minSdkVersion="4" />

</manifest>

Jeśli od dawna programujesz w Javie, plik manifestu prawdopodobnie nie jest


dla Ciebie niczym nowym. W aplikacjach na Android plik ten nosi nazwę
AndroidManifest.xml i udostępnia środowisku uruchomieniowemu Androida
2.3. Manifest aplikacji 75

wewnętrzne dane konfiguracyjne oraz metadane na temat aplikacji. W elemencie


manifest podane są nazwa pakietu i identyfikatory wersji aplikacji. Element
application określa środowisko uruchomieniowe o danej nazwie, ikonie i etykie-
cie aplikacji . Elementy potomne elementu application obejmują definicje
operacji, które aplikacja potrafi wykonywać.
W elementach tych podano klasę aktywności o nazwie .DealList, będącą
punktem wejścia do aplikacji, i obiekt klasy IntentFilter z zadeklarowanymi akcją
MAIN i kategorią LAUNCHER . Nazwa klasy aktywności jest podawana względem
pakietu aplikacji. Obiekt IntentFilter informuje środowisko uruchomieniowe,
że należy zarejestrować daną aplikację i udostępnić ją na ekranie głównym tele-
fonu (w aplikacji Launcher). Ogólnie obiekt IntentFilter przedstawia możliwo-
ści aplikacji. W innych komponentach należy użyć obiektów Intent do zadekla-
rowania akcji do wykonania. Mechanizmy te są ważne w Androidzie, ponieważ
umożliwiają łączenie różnych komponentów i wspólne używanie ich w środowi-
sku uruchomieniowym (technika późnego wiązania). Więcej na ten temat znaj-
dziesz w podrozdziale 2.9.
W manifeście obok punktu wejścia trzeba zadeklarować także wszystkie pozo-
stałe aktywności . To samo dotyczy też innych komponentów, takich jak Broad
´castReceiver, Service i ContentProvider (które w tym przykładzie nie wystę-
pują). BroadcastReceiver to specjalny filtr odbierający intencje rozpowszechniane
w systemie, a Service to proces działający w tle. Komponenty te opisano szcze-
gółowo w rozdziale 5. Obiekt klasy ContentProvider umożliwia udostępnianie
innym aplikacjom interfejsu API do wymiany danych. Zagadnienie to omówiono
w rozdziale 8. Wróćmy do manifestu — po deklaracji głównych komponentów
znajdują się uprawnienia , które są ostatnim elementem konfiguracji aplikacji
DealDroid.

2.3.1. Uprawnienia
W aplikacji DealDroid określono, że należy jej umożliwić korzystanie z internetu
(aplikacja przetwarza kanał RSS z eBaya, aby pobrać informacje o ofercie) i pozwo-
lić na sprawdzanie stanu sieci. System uprawnień Androida opisuje wszystkie
zadeklarowane w ten sposób zastrzeżone operacje, a następnie wyświetla je użyt-
kownikowi, gdy jest instalowana aplikacja. To ważna kwestia. W czasie wykony-
wania programu nie są przeprowadzane żadne testy. Użytkownik w momencie
instalowania aplikacji widzi, jakie operacje będzie ona wykonywać. Zezwolenie
na to prowadzi do trwałego przyznania uprawnień. Jeżeli aplikacja próbuje wyko-
nać operację, do której nie jest uprawniona, zgłaszany jest wyjątek Security
´Exception.
Oprócz zdarzeń związanych z internetem i systemem można też zadeklaro-
wać takie operacje, jak odczyt lub zapis danych w systemie plików, odczyt lub
zapis danych kontaktowych użytkownika, możliwość wzbudzenia telefonu itd.
76 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

W dostępnej w pakiecie SDK klasie Manifest.permission ze stałymi można łatwo


podejrzeć wszystkie wbudowane uprawnienia.
Rzadziej potrzebne jest deklarowanie i wymuszanie własnych uprawnień,
wykraczających poza deklaracje systemowe. Jeśli potrzebujesz takich uprawnień,
możesz zadeklarować niestandardowe uprawnienia w manifeście i wymuszać je
w komponentach (aktywnościach, usługach, odbiornikach rozpowszechnianych
informacji itd.).
Wyjdźmy teraz poza manifest. Następnym omawianym aspektem aplikacji
DealDroid są używane w niej elementy różne od kodu, a konkretnie — zasoby.

2.4. Zasoby
Zasób to ogólne określenie. W rozdziale 1. wyjaśniono, że nazwa ta może ozna-
czać obrazki używane w aplikacji, różne wersje językowe tekstu lub statyczną
wartość dowolnego typu zapisaną poza kodem aplikacji. Zasoby są definiowane
przez umieszczanie plików w katalogu /res projektu. Następnie dostęp do zasobów
można uzyskać albo w kodzie, albo przez wskazywanie ich w plikach XML.

2.4.1. Definiowanie zasobów


Wszystkie elementy zadeklarowane w katalogu /res nie tylko są pakowane wraz
z aplikacją, ale są też programowo dostępne w kodzie aplikacji. Zasoby mają
kilka kluczowych cech, o których warto pamiętać:
Q każdy zasób ma identyfikator;
Q każdy zasób ma określony typ;
Q zasoby mają określoną lokalizację i plik, w którym są zdefiniowane.
Programiści zwykle definiują zasoby kilku różnych typów (na przykład łańcuchy
znaków lub układy) za pomocą XML-a. Jest to zdecydowanie najczęściej stosowany
sposób korzystania z zasobów. To jednak nie wszystko. W postaci XML-owych
zasobów można definiować kształty, kolory, obiekty graficzne, style, motywy,
menu, statyczne tablice danych itd. Niezdefiniowane w XML-u zasoby można
umieszczać w określonych lokalizacjach (na przykład obrazki w katalogu
res/drawable) albo w katalogu /res/raw. Zasoby z tego ostatniego miejsca są
używane jak strumienie (dotyczy to na przykład plików audio i wideo). Po zde-
finiowaniu zasobu przez umieszczenie elementu w katalogu /res platforma auto-
matycznie przetwarza go (chyba że zasób znajduje się w katalogu raw) i urucha-
mia narzędzie aapt, aby powiązać identyfikator z zasobem w klasie R, opisanej
w rozdziale 1. W klasie R wszystkie identyfikatory są odwzorowane na lokalizację
lub skompilowaną zawartość zasobów.
DLACZEGO ZASOBY? Wbudowanie w Android definicji typów zasobów
i obsługi zasobów dostępnych poprzez interfejs API wymagało wiele
zachodu. Możliwe, że zastanawiasz się, po co to wszystko. Powodów jest
2.4. Zasoby 77

kilka. Po pierwsze, zasoby pozwalają oddzielić kod od zewnętrznych


elementów, takich jak obrazki i łańcuchy znaków. Taki podział jest poży-
teczny, ponieważ kod jest wtedy konkretny i przejrzysty. Po drugie, zasoby
są wydajne i szybkie. Zasoby w formacie XML są kompilowane na format
binarny. Dzięki temu są wygodne na etapie programowania i szybkie
w czasie wykonywania programu. Po trzecie, zasoby umożliwiają obsługę
dynamicznego ładowania w trakcie wykonywania programu na podstawie
różnych właściwości środowiskowych, takich jak język, konfiguracja ekranu
i możliwości sprzętowe. Dlatego możliwe jest umiędzynaradawianie apli-
kacji i tworzenie ich wersji językowych (co opisano dalej), a także wpro-
wadzanie innych zmian na podstawie środowiska.
Zasoby używane w aplikacji DealDroid są proste. To łańcuchy znaków, nazwy
w liczbie mnogiej (ang. plurals) i układy. Nazwy w liczbie mnogiej to specjalny
rodzaj zasobów, umożliwiający systemowi automatyczną obsługę liczby mnogiej
dla łańcuchów znaków. Do tego tematu wrócimy nieco dalej. Najpierw przyj-
rzyjmy się łańcuchom znaków używanym w aplikacji DealDroid. Przedstawiono
je na listingu 2.2.

Listing 2.2. Plik zasobów res/values/strings.xml z wartościami nazwanych


łańcuchów znaków

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


<resources>
<string name="app_name">DealDroid</string>
string name="deal_list_missing_data">
Brak danych - spróbuj później</string>
<string name="deal_list_retrieving_data">Pobieranie ofert . . . </string>
<string name="deal_list_network_unavailable">
Brak połączenia - nie można pobrać danych</string>
<string name="deal_list_reparse_menu">Ponowne przetwarzanie kanału RSS</string>
<string name="deal_details">Szczegóły oferty</string>
<string name="deal_details_price_prefix">$</string>
<string name="deal_details_mail_menu">Poczta</string>
<string name="deal_details_browser_menu">Przeglądarka</string>
<string name="deal_details_share_menu">Podziel się</string>
<string name="deal_details_msrp_label">Sugerowana cena:</string>
<string name="deal_details_quantity_label">Liczba:</string>
<string name="deal_details_quantity_sold_label">Sprzedano:</string>
<string name="deal_details_location_label">Lokalizacja:</string>
</resources>

Jak wspomniano, łańcuchy znaków używane w aplikacji DealDroid są zapisane


poza kodem i zdefiniowane w pliku XML. Plik ten ma nazwę strings.xml i znaj-
duje się w katalogu /res/values. Rozpoczyna się od typowego dla XML-a wstępu,
po którym następuje element główny resources . Dalej każdy łańcuch znaków
jest zdefiniowany w odrębnym elemencie z określającym nazwę atrybutem name .
Nazwą jest identyfikator zasobu, a także identyfikator w postaci stałej z wyge-
nerowanej klasy R.
78 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Wszystkie łańcuchy znaków zdefiniowane w pliku strings.xml są zapisane


jako stałe w klasie R, gdzie mają postać wartości szesnastkowych. Wartości te
wskazują miejsca w wewnętrznej tabeli zasobów, w których zostały skompilo-
wane i zapisane wartości pierwotne. O ile nie wykonujesz zaawansowanych ope-
racji w Androidzie, nie musisz korzystać z tabeli zasobów. Warto jednak wie-
dzieć, jak działają skompilowane zasoby.
ZASOBY, ALE NIE DO KOŃCA — KATALOG ASSETS. Jeśli potrze-
bujesz dostępu do normalnych plików, które nie są wstępnie przetwarzane
jako zasoby i nie mają przypisanego identyfikatora, możesz użyć katalogu
/assets. Każdy plik umieszczony w tym katalogu jest dostępny w aplika-
cji. Przykładowym elementem tego rodzaju jest plik źródłowy z kodem
w HTML-u przeznaczony do użytku w widoku WebView.
Innym ważnym zasobem wykorzystywanym w aplikacji DealDroid są układy
ekranu (zapisane w XML-u) i wspomniane wcześniej tajemnicze pliki z łańcu-
chami w liczbie mnogiej. Pliki z łańcuchami w liczbie mnogiej to skomplikowany,
ale przydatny typ zasobów Androida, umożliwiający łatwą (i z uwzględnieniem
wielu języków) obsługę liczby mnogiej. Na listingu 2.3 pokazano, w jaki sposób
zdefiniować w XML-u łańcuchy w liczbie mnogiej.

Listing 2.3. Plik zasobów res/values/plurals.xml w formacie XLIFF

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="deal_service_new_deal">
<item quantity="one">1 nowa oferta!</item>
<item quantity="other">
Liczba nowych ofert:
<xliff:g id="count">%d</xliff:g>
!
</item>
</plurals>
</resources>

Łańcuchy z liczbą mnogą różnią się od większości zasobów XML-owych z uwagi


na zastosowanie specjalnego formatu, OASIS XLIFF . Nie musisz dużo o nim
wiedzieć, aby móc go używać. Należy zdefiniować przynajmniej dwie etykiety
tekstowe — jedną dla pojedynczych elementów (one) i drugą dla elementów
w większej liczbie (other) . Framework zwraca następnie odpowiednią wartość.
W Androidzie w zasobach w postaci łańcuchów znaków można używać także
argumentów typu String.format. W kodzie takim argumentem jest %d , zastę-
powany cyfrą podawaną przy późniejszym pobieraniu zasobu.
Dlaczego łańcuchy z liczbą mnogą są ważne? Dlaczego nie wystarczy napisać
„Nowych ofert: 10”? No cóż, można zastosować takie podejście, jednak zdaniem
niektórych nie jest ono eleganckie i z pewnością nie jest międzynarodowe. Sytu-
acja może się szybko skomplikować, jeśli używasz wielu języków i liczby mnogiej.
Na przykład w języku japońskim nie ma liczby mnogiej, a w języku słowackim
2.4. Zasoby 79

ma ona kilka postaci (dla wartości 1, 2, 3, 4, 5 i większych). Łańcuchy znaków


w liczbie mnogiej pozwalają rozwiązać problem. Dalej omawiamy, jak uzyskać
dostęp do zasobów.

2.4.2. Dostęp do zasobów


Zdefiniowane zasoby można wskazywać w kodzie lub w XML-u za pomocą iden-
tyfikatorów. Więcej na ten temat dowiesz się przy analizowaniu klas aktywności
aplikacji DealDroid w dalszej części rozdziału, a także przy korzystaniu ze stylów
i motywów w rozdziale 4. Tu przedstawiamy podstawowe informacje na temat
dostępu do zasobów.
Aby uzyskać dostęp do zasobu w kodzie, należy użyć zdefiniowanego w kla-
sie R identyfikatora, na przykład R.string.deal_details. Na podstawie tego iden-
tyfikatora można ustalić, że używany jest lokalny (a nie systemowy) zasób w postaci
łańcucha znaków. Zasoby systemowe różnią się od lokalnych przedrostkiem
przestrzeni nazw android (na przykład android.R.string.yes). Identyfikatora
można używać w różnych metodach. Zwykle stosuje się go w metodach klasy
Resources w następujący sposób:

Q Dla standardowych łańcuchów znaków wykorzystywana jest składnia


Resources.getString(R.string.deal_details).
Q Dla liczby mnogiej używa się składni
Resources.getQuantityString(R.plurals.deal_service_new_deal, 1);.

Stosowanie referencji do zasobów w XML-u jest jeszcze łatwiejsze. Wystarczy


poprzedzić identyfikator potrzebnego zasobu przedrostkiem @. Łańcuch zna-
ków deal_details można wskazać tak: @string/deal_details. Aby podać prze-
strzeń nazw android, należy użyć dwukropka, na przykład @android:string/yes.
TYPY ZASOBÓW. W Androidzie istnieje wiele różnych typów danych,
które można zapisać poza kodem jako zasoby. Na razie przedstawiliśmy
łańcuchy znaków, łańcuchy w liczbie mnogiej i kilka układów (dalej zoba-
czysz też inne zasoby). Warto też wiedzieć, że jako zasoby można definio-
wać także menu, style, animacje, kształty, tablice danych i inne elementy.
Więcej zasobów i różne ich typy poznasz w dalszych fragmentach książki.
Kompletny i aktualny przegląd wszystkich obsługiwanych typów zasobów
znajdziesz w dokumentacji zasobów Androida pod adresem http://mng.
bz/aLRy.
Trzeba wiedzieć o kilku niuansach dostępu do zasobów w formacie XML, na
przykład o definiowaniu nowych identyfikatorów w XML-u i stosowaniu ukła-
dów. To prowadzi do projektowania ekranu dla aktywności i korzystania z wido-
ków, ich hierarchii i kontrolek.
80 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

2.5. Układ, widoki i kontrolki


Układ to specjalny zasób, który służy do projektowania ekranów, tworzenia
pozycji na listach i innych elementów interfejsu użytkownika w Androidzie.
Układy przedstawiono w rozdziale 1. Tu dokładniej opisano, jak deklarować je
w XML-u. Omówiono też komponenty układów — widoki i kontrolki.
Ten krótki przegląd nie jest kompletnym omówieniem układu, widoków i kon-
trolek. Dodatkowe szczegóły stopniowo poznasz w kontekście uczenia się pod-
staw programowania aplikacji na Android. Dalej, w rozdziale 4., wracamy do tych
zagadnień i omawiamy je dokładnie.

2.5.1. Deklarowanie układów


W Androidzie w obszarze tworzenia głównych elementów interfejsu użytkow-
nika aplikacji warstwa prezentacji jest zapisywana w zasobach układu, co przy-
pomina podejście używane w HTML-u, natomiast różni się od funkcjonowania
typowych frameworków interfejsu użytkownika z Javy, takich jak Swing. Podsta-
wowy pomysł polega na statycznym deklarowaniu interfejsu użytkownika dla
danego widoku jako zasobu XML-owego i późniejszym używaniu identyfikato-
rów do wskazywania elementów interfejsu użytkownika w kodzie. Przyjrzyjmy się
przykładowi. Na listingu 2.4 pokazano kod XML układu początkowego ekranu
aplikacji DealDroid.

Listing 2.4. Plik res/layout/deallist.xml zasobu układu obejmujący widoki i kontrolki

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


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_weight="1" />
<Spinner android:id="@+id/section_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp" />
</LinearLayout>

Plik z układem to plik XML. Obejmuje on inny zbiór elementów niż pliki HTML
używane do tworzenia stron internetowych, ale jego działanie jest podobne.
Elementy HTML-owe są zwykle niskopoziomowe, natomiast elementy układu
w Androidzie są bardziej złożone. Element główny pliku układu to kontener
LinearLayout obejmujący klasy View . Kontener LinearLayout rozmieszcza wszyst-
kie widoki potomne w jednym wierszu lub kolumnie. Dostępne są też inne typy
układów, na przykład FrameLayout, RelativeLayout i TableLayout. Ponadto możesz
tworzyć własne typy, jednak na razie ograniczamy się do układów LinearLayout.
Inne typy poznasz w rozdziale 4.
2.5. Układ, widoki i kontrolki 81

W przedstawionym przykładzie układ LinearLayout ma orientację pionową


(zdefiniowano ją w atrybucie elementu). Oznacza to, że wszystkie widoki potomne
są rozmieszczane w jednej kolumnie od góry do dołu, w kolejności ich wystę-
powania w pliku układu. W układzie umieszczono dwa elementy potomne —
element ListView o specjalnym, zarezerwowanym identyfikatorze list i element
Spinner . Widać też, że zdefiniowano atrybuty tych elementów, na przykład
layout_width, layout_height i layout_weight. Określają one wielkość i pozycję
elementów widoku. Te i inne atrybuty układu dokładniej opisano w rozdziale 4.
Na zrzucie z rysunku 2.5 (uzyskanym na pod-
stawie układu deallist) widać, że ListView to kon-
trolka pokazująca listę elementów, a Spinner jest
kontrolką wyświetlającą elementy do wyboru,
przy czym w danym momencie pokazywany jest
jeden element. Więcej o obu kontrolkach dowiesz
się przy analizie kodu układu, przedstawionej
w kilku dalszych punktach. Warto wrócić do lis-
tingu 2.4 i zauważyć, że obie kontrolki mają iden-
tyfikatory zasobu. Podobne identyfikatory można
przypisać do elementów w HTML-u.
Może zauważyłeś, że identyfikator zasobu
kontrolki Spinner, @+id/section_spinner, jest po-
przedzony znakiem +. Ta specjalna notacja ozna-
Rysunek 2.5. Ekran DealList
cza, że jeśli identyfikator zasobu nie istnieje, aplikacji DealDroid wyświetla
system ma go utworzyć w tabeli zasobów i pliku dwa komponenty (ListView
i Spinner) zdefiniowane
R.java. Jeżeli chcesz powtórnie wykorzystać iden- w układzie deallist.xml
tyfikator zasobu lub wskazać istniejący już identy-
fikator, nie musisz używać znaku plus.
XML NIE JEST JEDYNĄ MOŻLIWOŚCIĄ. Układy oparte na XML-u są
wygodnym i ogólnie dobrym wyborem projektowym, pozwalającym
oddzielić warstwę prezentacji od logiki aplikacji. Warto jednak wiedzieć,
że można całkowicie zrezygnować z XML-a. Układy, inne zasoby XML-owe,
a także wszystkie widoki i kontrolki można definiować w kodzie Javy.
Wszystkie XML-owe układy używane w Androidzie reprezentują klasy
Javy. Układy te są parsowane i przekształcane na obiekty Javy. Więcej
o pisaniu nieprzetworzonych widoków i przekształcaniu układów na klasy
dowiesz się dalej. Na razie zapamiętaj, że XML nie jest jedynym sposobem
na definiowanie komponentów interfejsu użytkownika w aplikacjach na
Android.
Identyfikatory w układach w formacie XML umożliwiają wskazywanie kontrolek
w kodzie, co pozwala na zapełnianie kontrolek danymi, dołączanie odbiorników
82 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

zdarzeń itd. Warto też zauważyć, że elementy w układach w formacie XML


to dużo bardziej rozbudowane komponenty niż niskopoziomowe elementy
w HTML-u.
To jeszcze nie koniec omawiania układów. Do utworzenia pozostał jeszcze
jeden ekran aplikacji DealDroid (ze szczegółami oferty), a głównym tematem
rozdziału 4. jest interfejs użytkownika. Jednak obecnie wiesz już, czym są układy.
Teraz należy przybliżyć inne wykorzystywane w książce pojęcia — widoki
i kontrolki.

2.5.2. Widoki i kontrolki


W rozdziale 1. wspomniano, że w Androidzie klasą bazową dla obiektów interfejsu
użytkownika jest klasa android.view.View. Jest ona podstawą każdego elementu
wyświetlanego w dowolnej aplikacji na Android. Istnieją trzy główne typy widoków:
Q SurfaceView,
Q ViewGroup,
Q Widget.

Pierwszy i najważniejszy typ to SurfaceView. Zapewnia on obszar do rysowania


elementów. Nie omawiamy go w tym rozdziale — więcej na jego temat dowiesz
się w kontekście rysowania i grafiki w dalszych rozdziałach. Następny typ widoku
to ViewGroup. Jest to abstrakcja układów i innych kontenerów widoków (czyli
widoków obejmujących inne widoki). Zobaczyłeś już kilka prostych układów.
Dalej dowiesz się więcej na ich temat, a także o ich powiązaniach z hierarchiami i
grupami widoków. Ostatnim typem widoków jest Widget. Ten typ jest podstawą
klasycznych komponentów interfejsu użytkownika i to z niego najczęściej będziesz
korzystał.
Kontrolki (są one częścią pakietu android.widget) to widoki, które często służą
do wchodzenia w interakcje z użytkownikami i mogą zostać powiązane ze źródłem
danych. Kontrolkami są proste elementy formularza, na przykład pola tekstowe
i przyciski, a także bardziej złożone komponenty, takie jak ListView oraz Spinner.
Po zadeklarowaniu widoków i kontrolek w układach oraz wyjaśnieniu zna-
czenia tych pojęć (przy czym dalej omawiamy je dokładniej) pora wykorzystać
te elementy w kodzie i „ożywić” je w aktywnościach.

2.6. Aktywności
Aktywność (klasa typu Activity) odpowiada jednej konkretnej rzeczy, jaką użyt-
kownik może zrobić. Zwykle każdy ekran aplikacji jest definiowany za pomocą
układu i składa się z widoków oraz kontrolek obsługiwanych przez powiązaną
aktywność. Każda aktywność tworzy okno interfejsu użytkownika, zarządza cyklem
życia i stanem, stanowi punkt docelowy intencji (więcej o intencjach dowiesz
się w podrozdziale 2.8), obsługuje zdarzenia interfejsu, kontroluje menu itd.
2.6. Aktywności 83

JEDNA KONKRETNA RZECZ. Aktywność zwykle odpowiada ekranowi


aplikacji, jednak warto zwrócić uwagę na staranny dobór słów w definicji
z dokumentacji. Jedna konkretna rzecz to nie zawsze ekran. Posługiwanie
się w omówieniu ekranem to ogólnie właściwe i wygodne podejście, warto
jednak pamiętać, że aktywność może odpowiadać pływającemu oknu
widocznemu nad oknem innej aktywności.
Aby utworzyć w aplikacji ekran dla Androida, należy rozszerzyć klasę Activity
lub jedną z jej wyspecjalizowanych podklas (jak pokazano wcześniej, interfejs
użytkownika ekranu jest zwykle definiowany za pomocą zasobu układu). Pewne
skomplikowane aspekty korzystania z aktywności, w tym niuanse dotyczące cyklu
życia i powiązań aktywności z zadaniami, omawiamy w rozdziale 3. Tu opisu-
jemy podstawy używania aktywności, a także pokazujemy, jak używać pierwszej
wyspecjalizowanej podklasy do obsługi list — ListActivity. Zaczynamy od naj-
ważniejszych aspektów aktywności, czyli od często implementowanych metod.

2.6.1. Podstawowe informacje o aktywnościach


Platforma Android przeprowadza zawiłe manipulacje w zakresie zarządzania
zasobami. Z uwagi na ograniczoną moc obliczeniową procesora i ilość dostępnej
pamięci w Androidzie używany jest kontrolowany przez system stos aktywności.
System dąży do tego, aby możliwe było realizowanie najważniejszych dla użyt-
kownika zadań i wykonywanie pozostałych operacji w tle.
Które operacje są najważniejsze i jak system manipuluje zasobami? Najważ-
niejsza jest aplikacja, z której użytkownik w danym momencie korzysta. Aplikacja
zwykle obejmuje zestaw komponentów, w tym aktywności, usługi i odbiorniki
rozpowszechnianych informacji uruchomione na platformie z wykorzystaniem
tego samego identyfikatora użytkownika i procesu (opisano to już w rozdziale 1.).
Kiedy użytkownik klika przycisk lub reaguje na powiadomienie w celu urucho-
mienia nowych aktywności, system rozpoczyna wykonywanie bieżących aktyw-
ności w tle. W tym celu zmienia stan aktywności za pomocą metod obsługi cyklu
życia. Oto najczęściej używane z tych metod:
Q Metoda onCreate. Wywoływana przy tworzeniu obiektu klasy Activity.
Q Metoda onPause. Wywoływana przy odsuwaniu obiektu klasy Activity w tło.
Q Metoda onResume. Wywoływana przy wznawianiu pracy obiektu klasy
Activity działającego w tle.

Istnieje więcej metod obsługi cyklu życia (wszystkie je opisano w następnym


rozdziale), jednak to w metodzie onCreate inicjowane jest działanie aktywności,
w metodzie onPause należy uporządkować lub utrwalić stan, a w metodzie on
´Resume — ponownie wczytać lub zresetować aktywność. Metoda onCreate jest
przesłaniana w każdej rozwijanej klasie aktywności. W większości takich klas
(choć nie we wszystkich) przesłaniane są też metody onPause i onResume.
84 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Oprócz możliwości przesłonięcia metod obsługi cyklu życia klasa Activity


(z uwagi na dziedziczenie po klasie Context) oferuje też liczne metody obsługi
zdarzeń, stanu, menu, a także inne metody pomocnicze. Koniecznie trzeba dobrze
zrozumieć metody obsługi cyklu życia klasy Activity i odpowiednio je stosować.
Właściwe korzystanie z tych metod pozwala tworzyć szybko reagujące i pozba-
wione błędów aplikacje. Ponieważ metody te i dotyczące ich zagadnienia są tak
ważne, w następnym rozdziale koncentrujemy się na tym temacie i powiąza-
nych z nim kwestiach, na przykład na używaniu wybranych innych metod klasy
Activity. Przed przejściem do tych szczegółów warto najpierw przyjrzeć się
implementacji klasy Activity tworzącej ekran z listą ofert w aplikacji DealDroid.

2.6.2. Aktywności oparte na listach


Listy w Androidzie doskonale nadają się do rozpoczęcia szczegółowego oma-
wiania widoków i aktywności. Są też dobrym przykładem zastosowania wzorca
projektowego model-widok-kontroler (ang. Model-View-Controller — MVC).
Ważne jest, aby wiedzieć, w jaki sposób dane i ich reprezentacja są oddzielone
od siebie oraz jak odzwierciedlone jest to w interfejsach frameworku.
Na rysunku 2.5 pokazano, że DealDroid wyświetla listę ofert za pomocą klasy
ListView — to kontener z funkcją przewijania, który może obejmować dowolną
liczbę widoków potomnych (elementów listy). Elementem listy może być widok
dowolnego rodzaju, ponadto nie wszystkie elementy muszą być tego samego
typu, co pozwala tworzyć listy o różnym poziomie złożoności. Klasa ListView
wykonuje za programistę wszystkie skomplikowane operacje. Odpowiada za przy-
wracanie i ponowne wyświetlanie wszystkich widocznych elementów listy po
zmianie danych, obsługuje zdarzenia związane z dotknięciem itd.
Choć bezpośrednie korzystanie z klasy ListView jest w pełni akceptowalnym
rozwiązaniem (a czasem wprost niezbędnym), zwykle używa się jej pośrednio,
poprzez androidową klasę ListActivity. Klasa ListActivity zarządza klasą
ListView za programistę, a tym samym pozwala uniknąć pisania szablonowego
kodu potrzebnego do konfigurowania widoku listy, reagowania na zdarzenia itd.
Tu do dodania do aplikacji DealDroid klasy ListView wykorzystano klasę
ListActivity. Klasa DealList to pierwszy skomplikowany fragment kodu w tej
książce. Na potrzeby omówienia podzielono ją na kilka fragmentów. Zaczynamy
od największej części — deklaracji klasy i metody onCreate, przedstawionych
na listingu 2.5.

Listing 2.5. Początek klasy aktywności z pliku DealList.java — od deklaracji


do metody onCreate

public class DealList extends ListActivity {

private static final int MENU_REPARSE = 0;

private DealDroidApp app;


private List<Item> items;
2.6. Aktywności 85

private DealsAdapter dealsAdapter;


private Spinner sectionSpinner;
private ArrayAdapter<Section> spinnerAdapter;
private int currentSelectedSection;
private ProgressDialog progressDialog;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.deallist);

progressDialog = new ProgressDialog(this);


progressDialog.setCancelable(false);
progressDialog.setMessage(
getString(R.string.deal_list_retrieving_data) );

app = (DealDroidApp) getApplication();

items = new ArrayList<Item>();


dealsAdapter = new DealsAdapter(items);

setListAdapter(dealsAdapter);

if (app.getSectionList().isEmpty()) {
if (app.connectionPresent()) {
new ParseFeedTask().execute();
} else {
Toast.makeText(this, getString(
R.string.deal_list_network_unavailable),
Toast.LENGTH_LONG).show();
}
} else {
resetListItems(app.getSectionList().get(0).getItems());
}

sectionSpinner = (Spinner) findViewById(R.id.section_spinner);


spinnerAdapter =
new ArrayAdapter<Section>(DealList.this,
android.R.layout.simple_spinner_item, app.getSectionList());
spinnerAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
sectionSpinner.setAdapter(spinnerAdapter);

sectionSpinner.setOnItemSelectedListener(
new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parentView,
View selectedItemView, int position, long id) {
if (currentSelectedSection != position) {
currentSelectedSection = position;
resetListItems(
app.getSectionList().get(position).getItems() );
}
}

@Override
public void onNothingSelected(AdapterView<?> parentView) {
// Nic nie robi.
86 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

}
});
}
// ... ciąg dalszy na kolejnych listingach.

Pierwszą rzeczą, jaką warto zauważyć na listingu 2.5, jest to, że — zgodnie
z zapowiedzią — w kodzie rozszerzono klasę ListActivity . Następnie, jak
w prawie każdej aktywności, przesłaniana jest metoda onCreate . Uczestniczy
ona w niezwykle ważnym cyklu życia aktywności, który dokładnie opisano w roz-
dziale 3. Na razie należy zapamiętać, że gdy tworzony jest obiekt klasy aktyw-
ności, w metodzie onCreate konfigurowane są różne elementy. W metodzie tej
plik układu z listingu 2.4 jest za pomocą metody setContentView ustawiany jako
widok zawartości. Więcej o działaniu tej ostatniej metody dowiesz się w omó-
wieniu przekształcania układów na klasy (ang. inflating) w rozdziale 4. Na razie
zapamiętaj, że za pomocą wspomnianej metody można powiązać z aktywnością
układ w formacie XML.
Po zakończeniu początkowego konfigurowania kod tworzy obiekt klasy Appli
´cation używany później do zapisywania stanu globalnego i definiowania
metod narzędziowych. Kod klasy Application i ogólne omówienie obiektów tej
klasy znajdziesz w podrozdziale 2.9. Teraz przechodzimy do istoty klasy List
´View i używamy obiektu klasy Adapter do udostępnienia danych dla listy.
Tu wykorzystujemy standardową kolekcję Javy (klasy List) i przekazujemy
ją do klasy DealsAdapter . DealsAdapter to niestandardowa klasa będąca roz-
szerzeniem klasy Adapter. Służy ona do udostępniania ofert widocznych na liście.
Ogólnie taka jest właśnie funkcja adapterów — udostępniają dane. Adaptery
przyjmują różną postać. Mogą być oparte na tablicach, kolekcjach, a nawet plikach
lub kursorach bazodanowych. Mogą też być proste lub złożone. W podrozdziale
2.7 znajdziesz kod klasy DealsAdapter i dowiesz się więcej o adapterach. Na razie
uwierz nam, że wspomniany adapter udostępnia oferty widoczne na liście ListView.
Do wiązania obiektów klas Adapter i ListView służy metoda setListAdapter .
Należy zwrócić uwagę na to, że nigdzie nie wymieniono bezpośrednio obiektu
klasy ListView. To jedno z udogodnień zapewnianych przez klasę ListActivity.
Możliwe, że w tym momencie marszczysz brwi. Jak rozwiązanie działa, skoro nie
zostało skonfigurowane? Skonfigurowano je, ale w wyrafinowany sposób. Pamię-
tasz, jak w układzie z listingu 2.4 przekazano zarezerwowany identyfikator do
elementu <ListView>? Sztuczka polega na tym, że po ustawieniu klasy ListActivity
jako klasy bazowej aplikacja szuka deklaracji elementu <ListView> w układzie
aktywności o identyfikatorze zasobu android:id/list. Następnie automatycznie
łączy kontrolkę z operacjami podanymi w metodzie setListAdapter (lub innej
metodzie pomocniczej, takiej jak getListView). Nie ma w tym nic skomplikowanego.
ZAREZERWOWANE IDENTYFIKATORY ZASOBÓW. W Androidzie
predefiniowane zarezerwowane identyfikatory są używane nie tylko dla list,
ale też w innych kontekstach. Na przykład klasa TabActivity wyszukuje
2.6. Aktywności 87

w układzie identyfikatory tabhost, tabcontent i tabs. Identyfikatory te można


też wykorzystać, aby uzyskać dostęp do widoków zdefiniowanych w nie-
których predefiniowanych układach Androida. Android obejmuje na przy-
kład domyślne układy elementów list, takie jak simple_list_item_1 i sim
´ple_list_item_2 dla jedno- i dwuwierszowych tekstowych elementów list.
Po skonfigurowaniu adaptera dla obiektu klasy ListView przechodzimy do wywo-
łania metody, która sprawdza, czy fragment listy z aktualnymi ofertami jest już
zapełniony. Jeśli nie, należy sprawdzić, czy aplikacja ma dostęp do sieci. Dalej
następuje tajemnicze wywołanie metody ParseFeedTask.execute . Jest to wywoła-
nie implementacji klasy AsyncTask. Klasa AsyncTask umożliwia wykonywanie
długich operacji w wątku odrębnym od wątku interfejsu użytkownika (tu apli-
kacja pobiera dane z sieci i przetwarza kanał RSS z ofertami z eBaya). W tym
miejscu nie omawiamy kodu tej klasy, ponieważ wymagałoby to odejścia od
podstaw, jednak nie martw się — w rozdziale 6. opisujemy wątki i szczegółowo
przedstawiamy klasę AsyncTask (jeśli chcesz przejść do tego zagadnienia, możesz
zajrzeć do kodu w pobranym projekcie DealDroid). Tu warto zapamiętać, że
w metodzie onCreate niepożądany jest kod, który może długo się wykonywać
lub blokować działanie metody (takiego kodu warto unikać w całym głównym
wątku interfejsu użytkownika). Ponadto jeśli nie można uruchomić klasy AsyncTask
z uwagi na brak połączenia z siecią, należy wyświetlić użytkownikowi komunikat
na ekranie za pomocą klasy Toast .
Po pobraniu danych przechodzimy do kontrolki Spinner . Jak pokazano na
rysunku 2.6, kontrolka ta udostępnia listę opcji (podobnie jak znacznik select
w HTML-u). Także w kontrolce Spinner źródłem danych jest obiekt klasy Adapter.
Tym razem jest to standardowa klasa ArrayAdapter, pobierająca dane z obiektu klasy
Application (klasę adaptera szczegółowo opisujemy w dalszym podrozdziale).
Po skonfigurowaniu danych za pomocą adaptera do kontrolki Spinner dołą-
czany jest odbiornik OnItemSelectedListener . Umożliwia to odbieranie infor-
macji o zdarzeniu zaznaczenia elementu w kontrolce Spinner. Tu aplikacja pobiera
kliknięty element, sprawdza, czy jest różny od aktualnie wybranego, a jeśli tak,
wywołuje dla klikniętego elementu metodę resetListItems . Działanie tej
metody przedstawiamy na następnym listingu. Najpierw jednak dokładniej oma-
wiamy, w jaki sposób komponent View z Androida reaguje na zdarzenia. We
frameworku Androida istnieje wiele odbiorników dla różnego rodzaju zdarzeń —
kliknięcia elementu, długiego kliknięcia elementu, przewijania, przesunięcia
po ekranie, zmiany aktywnego elementu itd. Odbiorniki pełnią funkcję interfej-
sów. Tu bezpośrednio w metodzie zaimplementowano interfejs OnItemSelected
´Listener za pomocą anonimowej klasy wewnętrznej.
ANONIMOWE KLASY WEWNĘTRZNE. Klasę można zdefiniować
w odrębnym pliku z implementacją interfejsu odbiornika, a następnie two-
rzyć egzemplarze tej klasy i używać ich jako odbiorników w adapterach.
88 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Inna możliwość to zadeklarowanie, że dana klasa obejmuje implementację


interfejsu, i napisanie potrzebnych metod w tej klasie (jeśli wykorzystu-
jesz kilka odbiorników, możesz użyć jednej metody i zastosować w niej
filtr do wyboru odpowiednich komponentów). Dostępnych jest kilka
podejść. To, które warto wybrać, zależy w pewnym stopniu od sytuacji
i osobistych preferencji. Naszym zdaniem anonimowe klasy wewnętrzne to
wygodne i dobre rozwiązanie, dlatego wybraliśmy właśnie je, choć począt-
kowo niełatwo je zrozumieć. Jedną z zalet anonimowych klas wewnętrz-
nych jest to, że pozwalają na różny poziom dostępu do zewnętrznych
metod i zmiennych klasy poprzez mechanizm domknięcia leksykalnego
(ang. lexical closure).
To już wszystko na temat metody onCreate klasy DealList. Metoda ta nie jest
prosta, dlatego nie martw się, jeśli nie w pełni ją rozumiesz. Gdy zapoznasz się
ze szczegółami dotyczącymi adapterów i z dalszymi listingami, wszystko stanie
się jasne. Teraz zastanówmy się nad tym, co się dzieje, kiedy w widoku ListView
trzeba wyświetlić nową listę elementów, na przykład po wybraniu opcji w kon-
trolce Spinner. To prowadzi do wspomnianej już metody resetListItems, przed-
stawionej na listingu 2.6.

Listing 2.6. Ponowne ustawianie adaptera widoku ListView w klasie aktywności


z pliku DealList.java

private void resetListItems(List<Item> newItems) {


items.clear();
items.addAll(newItems);
dealsAdapter.notifyDataSetChanged();
}

Metoda resetListItems jest krótka i prosta. Pobiera nowy obiekt klasy List
z obiektami typu Item i używa go do ponownego zapełnienia zmiennej items .
Warto przypomnieć, że ten sam obiekt items przekazano do obiektu klasy Deals
´Adapter w momencie tworzenia tego ostatniego. Po zmodyfikowaniu obiektu
items wywoływana jest metoda notifyDataSetChanged klasy DealsAdapter. Potem
następuje aktualizacja listy i ponowne wyświetlenie widoków. Dalej w tym roz-
dziale znajduje się kod niestandardowego adaptera i dodatkowe ogólne infor-
macje na temat adapterów.
Wiesz już, w jaki sposób obiekt klasy ListView jest aktualizowany przy wią-
zaniu go z nowymi danymi. Następny krok to obsługa kliknięcia określonego
elementu z listy przez użytkownika. Służy do tego metoda onListItemClick przed-
stawiona na listingu 2.7.

Listing 2.7. Obsługa kliknięcia elementu z kontrolki ListView w klasie aktywności


z pliku DealList.java

@Override
protected void onListItemClick(ListView listView,
2.6. Aktywności 89

View view, int position, long id) {


view.setBackgroundColor(android.R.color.background_light);
app.setCurrentItem(app.getSectionList().
get(currentSelectedSection).getItems().get(position));
Intent dealDetails = new Intent(DealList.this, DealDetails.class);
startActivity(dealDetails);
}

Metoda onListItemClick znajduje się w klasie ListActivity i jest wywoływana


zwrotnie w celu obsługi zdarzenia. Metoda ta jest uruchamiana w reakcji na
wybranie przez użytkownika elementu w kontrolce ListView. Przesłonięto ją ,
aby wykonywała pożądane operacje . W metodzie tej ustawiany jest globalny
stan wspomnianego wcześniej obiektu klasy Application, a następnie za pomocą
obiektu klasy Intent metoda uruchamia aktywność DealDetails. Jak już wyja-
śniliśmy, intencje łączą ze sobą różne elementy aplikacji na Android. Więcej na
ten temat dowiesz się z podrozdziału 2.8.
MOŻLIWOŚCI KONTROLKI LISTVIEW. Kontrolka ListView to przewi-
jana lista elementów do wyboru. Jest to jedna z najprzydatniejszych i dają-
cych najwięcej możliwości kontrolek dostępnych w Androidzie. Choć nie
stosujemy tu jej zaawansowanych funkcji, warto wiedzieć, że obsługuje
ona też filtrowanie, sortowanie, złożone widoki elementów, niestandar-
dowe układy, nagłówki, stopki itd. Kontrolka ListView używana jest też
w wielu dalszych przykładach w książce, gdzie opisano ją dokładniej. Kom-
pletny przegląd możliwości tej kontrolki znajdziesz w dokumentacji pod
adresem http://mng.bz/2LZM.
Po omówieniu kontrolek ListView i Spinner należy wspomnieć o jeszcze jednym
aspekcie ekranu z klasy DealList — o menu opcji. Menu to jest wyświetlane
w odpowiedzi na kliknięcie przycisku menu przez użytkownika. W tej wersji
aplikacji DealDroid menu obejmuje tylko jedną opcję — służącą do ponownego
przetwarzania kanału z danymi (ponieważ operacja ta nie jest wykonywana auto-
matycznie przez obiekt klasy Service, potrzebna jest opcja w menu). Kod konfi-
gurujący menu i reagujący na związane z nim zdarzenia pokazano na listingu 2.8.

Listing 2.8. Konfigurowanie menu w klasie aktywności z pliku DealList.java

@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, DealList.MENU_REPARSE, 0,
R.string.deal_list_reparse_menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_REPARSE:
if (app.connectionPresent()) {
new ParseFeedTask().execute();
} else {
90 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Toast.makeText(this,
getString(R.string.deal_list_network_unavailable),
Toast.LENGTH_LONG).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}

Menu opcji można dołączyć do każdej klasy aktywności. Aby je utworzyć, należy
przesłonić metodę onCreateOptionsMenu , a następnie — tak jak w kodzie —
dodać elementy MenuItem do obiektu klasy Menu. Metoda Menu.add umożliwia
określenie identyfikatora grupy, identyfikatora elementu, kolejności i wyświe-
tlanego obiektu typu String . To tylko część możliwości (tu nie użyto jednak
żadnych innych). Menu opcji może obejmować dowolną liczbę elementów, choć
w tak zwanym menu ikon wyświetlanych jest tylko pierwszych sześć. Pozostałe
znajdują się w menu rozszerzonym, które można otworzyć przez wybranie opcji
More z menu ikon. Ponieważ w przykładzie występuje tylko jeden element,
identyfikatory grupy i elementu nie mają większego znaczenia (są one przydatne,
jeśli używanych jest więcej elementów). Metoda onCreateOptionsMenu zwraca
wartość true, ponieważ menu ma być wyświetlane (jeśli nie chcesz wyświetlać
menu, zwróć wartość false).
Do reagowania na wybranie przez użytkownika elementu z menu opcji służy
nowa wersja przesłanianej metody onOptionsItemSelected . Tu do metody prze-
kazywany jest wybrany element typu MenuItem, a identyfikator pozwala ustalić,
jaki jest to element. Następnie można wykonać dowolne potrzebne operacje (tu
taką operacją jest ponowne przetworzenie kanału z ofertami dnia, do czego
ponownie wykorzystano klasę AsyncTask).
MENU OPCJI JAKO ZASÓB W FORMACIE XML. Menu opcji można
zdefiniować w kodzie (tę technikę zastosowano w klasie DealList) lub jako
zasób menu w formacie XML (/res/menu). Dostępnych jest wiele możli-
wości i opcji. Kompletny opis menu opcji znajdziesz w aktualnej doku-
mentacji pod adresem http://mng.bz/h8c0.
Po omówieniu menu ostatnim fragmentem podstawowego kodu klasy aktywności,
który należy opisać w kontekście klasy DealList, jest niezwykle ważna metoda
onPause, przedstawiona na listingu 2.9.

Listing 2.9. Metoda onPause z klasy aktywności z pliku DealList.java

@Override
public void onPause() {
if (progressDialog.isShowing()) {
progressDialog.dismiss();
}
super.onPause();
}
2.7. Adaptery 91

Cyklem życia aktywności (wspomniano już o nim; ponadto szczegółowe omó-


wienie tego zagadnienia znajduje się w rozdziale 3.) można zarządzać przez
przesłonięcie jego metod, takich jak onCreate i onPause. W metodzie onCreate
utworzono potrzebne w aktywności komponenty, a w metodzie onPause należy
wykonać dodatkowe operacje porządkowe. W aplikacji DealDroid użyto klasy
ProgressDialog do informowania użytkowników o różnych zdarzeniach (na przy-
kład o połączeniu z siecią w celu pobrania danych o ofertach). ProgressDialog to
interaktywne wyskakujące okno dialogowe, które wyświetla informacje o postę-
pie, na przykład zapełniający się poziomy pasek lub kręcące się kółko. Jeśli takie
okno dialogowe jest widoczne po zatrzymaniu aktywności, oznacza to wycieka-
nie zasobów, co może prowadzić do błędów wymuszonego zamknięcia. Dlatego
w metodzie onPause trzeba zamknąć takie okno, jeżeli jest widoczne.
Po opisaniu korzystania z metod zarządzania cyklem życia aktywności
(w ramach wprowadzenia do rozdziału 3.) i pokazaniu działania klasy ListActivity
następny krok to dokończenie przeglądu przez omówienie implementacji współ-
działających z widokami adapterów.

2.7. Adaptery
Przekazywanie danych ze źródła do widoku odbywa się za pośrednictwem obiektu
klasy Adapter. Jak wskazuje nazwa, klasa ta adaptuje określone źródło danych,
a tym samym pozwala podłączać różne rodzaje źródeł danych do widoku (obiektu
klasy AdapterView), który następnie wyświetla dane na ekranie. Kontrolki ListView
i Spinner to widoki typu AdapterView. Android udostępnia kilka predefiniowa-
nych adapterów, w tym klasy ArrayAdapter (do przesyłania danych z tablic lub
obiektów klasy Collection Javy) i CursorAdapter (do pobierania danych z baz
SQLite; więcej o bazach danych i kursorach dowiesz się z rozdziału 7.). Nie jesteś
jednak ograniczony do używania wbudowanych adapterów. Możesz na przykład
zaimplementować adapter, który jest nakładką na usługę sieciową i pobiera dane
z internetu bezpośrednio do widoków. Wszystko jest możliwe!

2.7.1. Podstawowe informacje o adapterach


Najważniejszym sposobem używania adaptera jest wykorzystanie jednej z imple-
mentacji dostępnych w Androidzie, na przykład klasy ArrayAdapter (który mimo
nazwy współdziała także z kolekcjami). Aby zobaczyć działanie tej techniki,
warto się cofnąć i przyjrzeć sposobowi udostępniania danych dla kontrolki
Spinner z listingu 2.5.
spinnerAdapter =
new ArrayAdapter<Section>(DealList.this,
android.R.layout.simple_spinner_item, sectionList);

Do tworzenia egzemplarza klasy ArrayAdapter używane są obiekt klasy Context


(zwracany przez wywołanie instrukcji DealList.this), zasób układu (informujący
92 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

obiekt klasy Adapter, jak należy wyświetlać każdy element) i same dane w formie
obiektu klasy List z obiektami klasy Section. Section to prosta klasa podobna do
ziaren (klas JavaBean), obejmująca metody do pobierania i ustawiania wartości.
Zawiera tytuł i kolekcję elementów typu Item, które pochodzą z używanego
w aplikacji modelu. Item to następne proste ziarno, reprezentujące konkretną
ofertę. Obejmuje identyfikator, tytuł, cenę, lokalizację itd. Kompletny kod źró-
dłowy tych klas znajdziesz w pobranym pakiecie z projektem. Układ używany
dla elementu Spinner jest ustawiany za pomocą zarezerwowanego identyfikatora
android.R.layout.simple_spinner_item. Klasa ArrayAdapter domyślnie oczekuje
układu, który reprezentuje pojedynczy widok TextView. Ze względu na przed-
rostek android w nazwie łatwo się domyślić, że używany jest do tego układ udo-
stępniany przez framework. Obiekt klasy Spinner jest prosty, dlatego można zasto-
sować taki wbudowany układ. W razie potrzeby można zamiast tego układu
zdefiniować własny. Domyślne działanie klasy ArrayAdapter polega na wywoła-
niu metody toString dla wszystkich dostępnych danych i wyświetleniu ich za
pomocą określonego układu. Jeśli chcesz wykonać inne operacje, możesz przesło-
nić metodę getView klasy ArrayAdapter. Technikę tę przedstawiono w następ-
nym punkcie.
ANDROID I KLASA CONTEXT. Przeglądając różne interfejsy API
Androida, zauważysz, że wiele z nich jako parametr przyjmuje obiekt klasy
android.content.Context. Zobaczysz też, że jako obiekty klasy Context
często używane są obiekty klas Activity lub Service. Jest to możliwe,
ponieważ są to klasy pochodne od klasy Context. Czym dokładnie jest
klasa Context? Według dokumentacji Androida reprezentuje ona różne
dane środowiskowe. Zapewnia dostęp do lokalnych plików, baz danych,
powiązanych ze środowiskiem programów ładujących klasy, usług (w tym
usług systemowych) itd. Podczas lektury tej książki i w codziennym pisa-
niu aplikacji na Android będziesz często stykał się z przekazywanymi
obiektami klasy Context.
Prosty adapter umożliwia szybkie przesłanie danych do widoku. Co jednak
zrobić, jeśli trzeba dostosować widok do potrzeb programisty lub odzwiercie-
dlić zmiany danych w widoku albo na odwrót? Aby poradzić sobie z takimi sytu-
acjami, często trzeba użyć niestandardowego adaptera.

2.7.2. Niestandardowe adaptery


Utworzenie własnego adaptera wymaga dodania klasy z implementacją interfejsu
Adapter. Istnieje kilka klas pomocniczych, na przykład ArrayAdapter i BaseAdapter,
po których można dziedziczyć. Trzeba w nich przesłonić lub dodać odpowied-
nie fragmenty. Metoda getView jest wywoływana za każdym razem, kiedy trzeba
wyświetlić element listy. Dzieje się to często, na przykład w trakcie przewijania
listy. Modyfikacja danych na liście wymaga poinformowania widoku listy (przez
2.7. Adaptery 93

wywołanie metody Adapter.notifyDataSetChanged) o tym, że powinien ponownie


wyświetlić elementy potomne. Podejście to zastosowano na listingu 2.6.
Klasa DealsAdapter użyta na listingu 2.5 to niestandardowy adapter będący
rozszerzeniem klasy ArrayAdapter. Na listingu 2.5 utworzono egzemplarz klasy
DealsAdapter i za pomocą wywołania setListAdapter(dealsAdapter) ustawiono
go jako podstawę całej klasy ListAdapter. Na listingu 2.10 pokazano kod klasy
DealsAdapter.

Listing 2.10. Niestandardowa klasa Adapter z pliku DealsAdapter.java służąca


do udostępniania widoków klasie DealList

private class DealsAdapter extends ArrayAdapter<Item> {

public DealsAdapter() {
super(DealList.this,
R.layout.list_item, new ArrayList<Item>());
}

@Override
public View getView(int position,
View convertView, ViewGroup parent) {

if (convertView == null) {
LayoutInflater inflater = (LayoutInflater)
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.list_item,
parent, false);
}

TextView text =
(TextView) convertView.findViewById(R.id.deal_title);
ImageView image =
(ImageView) convertView.findViewById(R.id.deal_img);
image.setImageBitmap(
BitmapFactory.decodeResource(getResources(), R.drawable.ddicon));

Item item = getItem(position);

if (item != null) {
text.setText(item.getTitle());
Bitmap bitmap = app.getImageCache().get(item.getItemId());
if (bitmap != null) {
image.setImageBitmap(bitmap);
} else {
image.setTag(item.getItemId());
new RetrieveImageTask(image)
.execute(item.getSmallPicUrl());
}
}

return convertView;
}
}
94 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

W klasie DealsAdapter wykonywanych jest wiele operacji. Jest to pierwsza (choć


nie ostatnia) opisywana tu niestandardowa klasa Adapter. Takie klasy są ważne,
jeśli chcesz wyjść poza domyślne funkcje kontrolek. W przykładzie jest to
konieczne, ponieważ kontrolka ListView, jak pokazano na rysunku 2.5, ma nie-
standardowy układ z małym obrazkiem i tytułem oferty dla każdego obiektu
klasy Item z listy. Warto przypomnieć, że domyślne działanie klasy ArrayAdapter,
zastosowane dla kontrolki Spinner, polega na wyświetlaniu wartości toString
każdego elementu z danych. Teraz potrzeba czegoś więcej.
Klasa DealsAdapter rozpoczyna się od podania klasy ArrayAdapter jako klasy
bazowej . To ważne, ponieważ Adapter to interfejs o dużej liczbie metod, a tu
należy przesłonić tylko wyświetlanie obiektu klasy View. Pozostałe mechanizmy
nie wymagają zmian. Możesz zaimplementować od podstaw własną klasę Adapter
lub rozszerzyć klasę BaseAdapter, aby zacząć od niższego poziomu. Chcemy jed-
nak w możliwie dużym stopniu ponownie wykorzystać framework, dlatego roz-
szerzamy klasę ArrayAdapter.
Pierwszy fragment klasy DealsAdapter to definicja konstruktora, który przeka-
zuje potrzebne elementy do klasy ArrayAdapter . Aby użyć klasy ArrayAdapter,
potrzebny jest obiekt klasy Context, identyfikator zasobu układu i tablica lub
obiekt klasy Collection z danymi. Po konstruktorze znajduje się przesłonięta
klasa getView . W tym miejscu przydaje się framework Androida, który w pomy-
słowy sposób (za pomocą obiektu convertView) pomaga w szybszym wyświetla-
niu ekranów z kontrolką ListView. Obiekt convertView to istniejący obiekt klasy
View. Jeśli jest dostępny i ma odpowiedni typ, można go ponownie wykorzystać.
Ponieważ kontrolka ListView umożliwia przewijanie na ekranie wielu elemen-
tów, które często można przedstawiać za pomocą tych samych widoków o róż-
nej zawartości (tu należy użyć nowej nazwy i obrazka), ponowne wykorzystanie
widoków zamiast odtwarzania ich dla każdej pozycji jest istotną optymalizacją.
Jeśli lista obejmuje 1000 elementów, nie wszystkie znajdują się na ekranie w tym
samym czasie. Tu za pomocą obiektu convertView klasy View zastosowano stroni-
cowanie danych, a także stronicowanie elementów interfejsu użytkownika i zmie-
nianie ich zawartości. Jeśli przekazany obiekt convertView to null, należy go utwo-
rzyć przez przekształcenie pożądanego układu, R.layout.list_item, na klasę .
Wspomniany układ jest statyczny i zadeklarowany w formacie XML. Do
przekształcania go na klasę służy systemowa usługa LAYOUT_INFLATER_SERVICE.
Układ ten, umieszczony w katalogu /res/layout projektu, to prosty układ typu
RelativeLayout.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView android:id="@+id/deal_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="5dp" />
2.7. Adaptery 95

<TextView android:id="@+id/deal_title"
android:layout_toRightOf="@id/deal_img"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp" />
</RelativeLayout>

Układ RelativeLayout działa inaczej niż używany wcześniej układ LinearLayout.


Zamiast rozmieszczać elementy w poziomie lub pionie, porządkuje je względem
siebie w określony przez programistę sposób. Więcej o tym i innych układach
dowiesz się z rozdziału 4.
Po utworzeniu układu wywoływana jest metoda findViewById w celu pobra-
nia uchwytu do elementów typu View o podanym identyfikatorze . Po uzyska-
niu uchwytów można pobrać aktualny element danych i umieścić wartości
danych w widokach . Tytuł znajduje się w klasie Item z danymi, a obrazek jest
najpierw ustawiany na domyślny za pośrednictwem zasobu graficznego o nazwie
ddicon (plik tego rysunku to res/drawable-mdpi/ddicon.gif). Po ustawieniu domyśl-
nego rysunku następuje próba pobrania obrazka dla oferty z internetu na podsta-
wie adresu URL elementu (jeśli rysunek nie jest jeszcze zapisany w obiekcie
klasy Application). Pobieranie odbywa się za pomocą innej klasy AsyncTask
(RetrieveImageTask), która zgłasza wywołanie HTTP przez sieć. Kod tej klasy
pominięto, aby nie odbiegać od tematu; zainteresowani mogą jednak znaleźć go
w pobranym projekcie. W końcowej części listingu metoda getView zwraca utwo-
rzony obiekt klasy View , a widok AdapterView go wyświetla.
Podsumowując, możemy powiedzieć, że niestandardowy adapter wyświetla
niestandardowe widoki i odzwierciedla w nich zmiany w modelu danych. We
wcześniejszej metodzie resetListItems (z listingu 2.6) wywoływana jest metoda
notifyDataSetChanged adaptera, co pozwala przygotować adapter (metoda jest
wywoływana także po wybraniu przez użytkownika sekcji z ofertami innego typu
w kontrolce Spinner). Powoduje to ponowne wyświetlenie widoków w celu
odzwierciedlenia bieżącego stanu danych.
Dopełnieniem rozwiązania mogłoby być aktualizowanie modelu danych na
podstawie operacji wykonywanych w interfejsie użytkownika. Te operacje to
na przykład zaznaczenie przez użytkownika elementów na liście (co wymaga
śledzenia, które elementy są wybrane) lub korzystanie z bardziej złożonych
elementów interfejsu użytkownika, umożliwiających uzupełnianie pól formularza
bądź innego typu interakcje z danymi (każdy element może być dowolnie skom-
plikowany). W aplikacji DealDroid nie jest to potrzebne, ale należy zauważyć,
że za pomocą niestandardowych adapterów można zapewnić tego typu dwu-
stronne wiązanie danych. W dalszych rozdziałach zobaczysz przykłady wykorzy-
stania tej techniki.
POJAWIA SIĘ WZORZEC MODEL-WIDOK-KONTROLER. Może zwró-
ciłeś uwagę na standardowy wzorzec. Używane są widok wyświetlający
dane ze źródła (modelu) i obiekt klasy Activity (kontroler), który kieruje
96 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

dane wejściowe od użytkowników do widoku i powiadamia o zmianach


w danych, co pozwala na ponowne wyświetlenie widoku. To klasyczny
wzorzec MVC! Jeśli dokładnie się temu przyjrzeć, we frameworku często
występują zgodne z wzorcem MVC interakcje między obiektami. Warto
o tym pamiętać. Jest to elastyczny i rozbudowany wzorzec projektowy,
często stosowany (podobnie jak adapter) we frameworkach kontrolek.
Pomyśl o tym, jak elastyczne jest rozwiązanie oparte na adapterze. Jeśli trzeba
zapisać dane w bazie lub pobrać je strona po stronie z usługi sieciowej, można
zastąpić pokazany adapter innym, który przechodzi po elementach z potrzeb-
nego źródła danych. Nie trzeba nic modyfikować w widoku listy. To znakomity
przykład luźnego powiązania i podejścia obiektowego.
Przykładem omawianego podejścia jest korzystanie przez aktywności z wido-
ków, które są luźno powiązane ze źródłami danych poprzez adaptery. Android
udostępnia też inny mechanizm luźnego wiązania aktywności z innymi kompo-
nentami — intencje.

2.8. Intencje i filtry intencji


Jedną z wyjątkowych zalet Androida jest elastyczność w zakresie komunikacji
między komponentami i wymiany danych między nimi. Android umożliwia to
na podstawie zdarzeń opartych na klasach Intent i IntentFilter. Jak już wspo-
mniano, klasa Intent to opis pożądanych akcji, a klasa IntentFilter umożliwia
komponentowi zadeklarowanie, że potrafi obsługiwać akcje opisane w konkretnej
klasie Intent. Same intencje nie wykonują żadnych operacji. Są jedynie opisem
tego, co trzeba zrobić.
Jeśli komponent ma wykonywać pewną akcję, należy zadeklarować w nim
intencję za pomocą klasy Intent i przekazać obiekt tej klasy do systemu. System
następnie przetwarza ten obiekt i określa, jakie inne komponenty (klasy Activity,
Service lub BroadcastReceiver) mogą obsłużyć zadanie.
Ponadto jeśli klasa Activity, BroadcastReceiver lub Service ma udostępniać
pewne akcje innym komponentom, należy zadeklarować w niej obiekt klasy
IntentFilter. Platforma Androida śledzi wszystkie deklaracje IntentFilter
dostępne w działającym systemie, a następnie dynamicznie (w czasie wykonywa-
nia programu) łączy zgłaszane intencje z najodpowiedniejszymi komponentami.
Na rysunku 2.6 proces ten przedstawiono w inny sposób, ilustrując relację między
klasami Intent i IntentFilter pasującymi do siebie kształtami.
Aby pokazać, w jaki sposób działają klasy Intent, zaimplementujmy klasę Deal
´Details z aplikacji DealDroid. Klasa ta wymaga zadeklarowania intencji kilku
różnych typów. Dalej dokładniej omawiamy filtry intencji.
2.8. Intencje i filtry intencji 97

Rysunek 2.6. Klasy Intent i IntentFilter są łączone ze sobą, co pozwala filtrować


zdarzenia i reagować na nie przez kierowanie ich do odpowiednich
zarejestrowanych komponentów

2.8.1. Stosowanie intencji


Aby przedstawić zawartość klasy Intent, tworzymy ostatnią klasę aktywności
w aplikacji DealDroid — klasę DealDetails. W podrozdziale 2.1 stwierdziliśmy,
że ekran z klasy DealDetails wyświetla szczegółowe informacje o ofercie po jej
kliknięciu przez użytkownika na ekranie z klasy DealList. Oprócz wyświetlania
informacji inną ważną funkcją klasy DealDetails jest umożliwianie użytkowni-
kowi dzielenia się informacjami o ofercie za pośrednictwem intencji i opcji menu.
Służący do tego kod pokazano na listingu 2.11.

Listing 2.11. Pierwsza część klasy aktywności z pliku DealDetails.java

public class DealDetails extends Activity {

private static final int MENU_MAIL = 1;


private static final int MENU_BROWSE = 2;
private static final int MENU_SHARE = 3;

private DealDroidApp app;


private ProgressBar progressBar;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dealdetails);

app = (DealDroidApp) getApplication();

progressBar = (ProgressBar) findViewById(R.id.progress);


progressBar.setIndeterminate(true);

Item item = app.getCurrentItem();

if (item != null) {
// W celu skrócenia listingu pominięto dodawanie elementów do widoku.
// Kod znajdziesz w pobranym pakiecie.
}
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(DealDetails.NONE, DealDetails.MAIL,
DealDetails.NONE, R.string.deal_details_mail_menu);
menu.add(DealDetails.NONE, DealDetails.BROWSE,
98 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

DealDetails.NONE, R.string.deal_details_browser_menu);
menu.add(DealDetails.NONE, DealDetails.SHARE,
DealDetails.NONE, R.string.deal_details_share_menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MAIL:
shareDealUsingChooser("text/html");
return true;
case BROWSE:
openDealInBrowser();
return true;
case SHARE:
shareDealUsingChooser("text/*");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
...

W klasie DealDetails zastosowano standardowy wzorzec rozszerzania klasy


Activity , po czym przesłaniana jest metoda obsługi cyklu życia onCreate .
Dalej za pomocą metody setContentView ustawiany jest układ (kod układu dla
klasy DealDetails pominięto, ponieważ jest prosty i nie wzbogaciłby niniejszych
wyjaśnień). Po tych znanych już fragmentach znajduje się metoda onCreate
´OptionsMenu , w której konfigurowane są elementy menu opcji do wykony-
wania operacji na określonej ofercie. Metoda ta zwraca wartość true, co powo-
duje wyświetlenie menu (dla innych wartości menu nie jest pokazywane).
Po definicjach elementów menu opcji znajduje się nowa wersja przesłania-
nej metody onOptionItemSelected, wywoływana w odpowiedzi na zaznaczenie
opcji . Metoda ta reaguje na różne opcje — pozwala podzielić się informacjami
o ofercie za pośrednictwem programu wyboru z typem MIME text/html ,
otwiera ofertę w aplikacji Browser lub umożliwia podzielenie się informacjami
o ofercie przy wykorzystaniu programu wyboru z typem MIME text/* , który
zapewnia więcej możliwości niż typ text/html. W każdej z bezpośrednio obsłu-
giwanych sytuacji zwracana jest wartość true, co oznacza, że należy zakończyć
używanie menu. Po przekazaniu nieobsługiwanego elementu menu przypadek
default jest przetwarzany przez implementację z klasy nadrzędnej.
Program wyboru (ang. chooser) to okno dialogowe z opcjami obsługi obiektu
klasy Intent. Kod tego okna znajduje się na listingu 2.12. Szczegółowo pokazano
tam wykonywanie potrzebnych operacji za pomocą intencji. Służą do tego metody
shareDealUsingChooser i openDealInBrowser.
2.8. Intencje i filtry intencji 99

Listing 2.12. Obsługiwane za pomocą obiektów klasy Intent akcje dzielenia się
informacjami w klasie aktywności DealDetails

private void shareDealUsingChooser(String type) {


Intent i = new Intent(Intent.ACTION_SEND);
i.setType(type);
i.putExtra(Intent.EXTRA_SUBJECT, "Temat:");
i.putExtra(Intent.EXTRA_TEXT, createDealMessage());
try {
startActivity(Intent.createChooser(i, "Podziel się ofertą..."));
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(DealDetails.this,
"Brak opcji programu wyboru dla typu "
+ type + ".",
Toast.LENGTH_SHORT).show();
}
}

private void openDealInBrowser() {


Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(
app.getCurrentItem().getDealUrl()));
startActivity(i);
}

private String createDealMessage() {


Item item = app.getCurrentItem();
StringBuffer sb = new StringBuffer();
sb.append("Sprawdź tę ofertę:\n");
sb.append("\nTytuł:" + item.getTitle());
sb.append("\nCena:" + item.getConvertedCurrentPrice());
sb.append("\nLokalizacja:" + item.getLocation());
sb.append("\nLiczba:" + item.getQuantity());
sb.append("\nURL:" + item.getDealUrl());
return sb.toString();
}
// Klasę wewnętrzną AsyncTask i końcówkę klasy DealDetails
// pominięto w celu skrócenia listingu.

W metodzie shareDealUsingChooser, w której wykonywanych jest wiele operacji


powiązanych z klasą Intent, należy najpierw skonfigurować klasę Intent przy
wykorzystaniu akcji ACTION_SEND . To prosty, ale kluczowy krok. Akcja to jeden
z najważniejszych aspektów klasy Intent obok danych (opisanych dalej) i typu
MIME . Można też dołączyć obiekt klasy Bundle z dodatkami. Dodatkowe
dane mogą być typu prostego (łańcuchy znaków, typy podstawowe itd.) lub typu
niestandardowego z obsługą interfejsu Parcelable (co pozwala na ich serializo-
wanie między procesami). Tu dodano nagłówek z tematem i szczegóły oferty
jako obiekt klasy String , utworzony za pomocą metody createDealMessage .
Kiedy obiekt klasy Intent jest już gotowy, w celu jego włączenia należy użyć
metody startActivity z argumentem w postaci metody createChooser . Program
wyboru widoczny po wybraniu opcji Share deal w aplikacji DealDroid (w urzą-
dzeniu, a nie w emulatorze, który ma mniej możliwości) pokazano na rysunku 2.7.
100 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Program wyboru widoczny na rysunku 2.7 poka-


zuje, że obiekty klasy Intent typu SEND text/* są
obsługiwane przez wiele zarejestrowanych kom-
ponentów (wyświetlanych w wyniku kliknięcia
przycisku Share deal aplikacji DealDetails). Pro-
gram wyboru to informacja dla użytkownika, że
za każdym razem powinien wybrać opcję.
Gdyby nie zastosowano programu wyboru,
użytkownik nadal mógłby wskazać opcję, ale
możliwe byłoby także zastosowanie akcji do-
myślnej. Wystarczy zmienić jeden wiersz klasy
DealDetails, aby zobaczyć, jak działa to rozwiąza-
nie. Zmodyfikuj metodę shareDealUsingChooser
przez zmianę wiersza startActivity z:
startActivity(Intent.CreateChooser(i, "Share
´deal..."));

na: Rysunek 2.7. Program wyboru


dla opcji Share deal (z menu
startActivity(i);
ekranu szczegółów z aplikacji
DealDroid) pokazuje wiele
Następnie ponownie wybierz przycisk Share deal. możliwości obsługi
Zobaczysz opcje widoczne na rysunku 2.8. konkretnego typu Intent

Ręczna kontrola programu wyboru (rysunek 2.7)


pozwala ustawić tytuł, a także wymusić na
użytkowniku dokonywanie wyboru każdorazowo
przy wykonywaniu operacji (jeśli może ją obsłu-
żyć więcej niż jeden komponent). Jeśli jednak
zezwolisz na prezentowanie opcji systemowi
(rysunek 2.8), nie da się zmienić tytułu, a użyt-
kownik może ustawić domyślny sposób obsługi
obiektu klasy Intent. Im bardziej ogólny jest
obiekt klasy Intent, tym więcej opcji jest do-
stępnych — dotyczy to obu podejść. Jak widać,
typ SEND text/* jest ogólny, co powoduje poja-
wienie się długiej listy opcji (jest ich więcej niż
na zrzutach; aby je wyświetlić, należy przewinąć
stronę).
Wróćmy do listingu 2.12. W metodzie open
´DealInBrowser zastosowano inny sposób two-
rzenia obiektów klasy Intent. Tu ustawiono akcję Rysunek 2.8. Ten sam zbiór
opcji bez zastosowania
na VIEW, a dane — na Uri (podano adres HTTP programu wyboru. Użytkownik
URL oferty). Użyty obiekt klasy Intent jest może ustawić opcję domyślną
2.8. Intencje i filtry intencji 101

szczegółowy, ponieważ służy do wyświetlania elementu, a identyfikator URI


określa konkretny typ danych (podany jest adres URL, protokół HTTP, nazwa
hosta i ścieżka). Jeśli w ten sposób zdecydowano, że należy wyświetlić adres
HTTP URL, zareagować powinien komponent tylko jednego typu — przeglą-
darka internetowa. Jeżeli w systemie działa kilka przeglądarek (jest to możliwe,
jeśli użytkownik zainstalował dodatkowe przeglądarki), pojawią się opcje wyboru,
jednak jest to znacznie mniej prawdopodobne niż w przypadku dzielenia się
informacjami za pomocą akcji SEND.
Aby zobaczyć, jak tworzyć jeszcze bardziej specyficzne intencje i w jaki spo-
sób elementy klasy Intent wpływają na listę obsługujących ją komponentów, trzeba
omówić intencje innego typu.

2.8.2. Typy intencji


Wróćmy do listingu 2.7, aby przejść od klasy aktywności DealList do klasy aktyw-
ności DealDetails użytej w następującym obiekcie klasy Intent:
Intent dealDetails = new Intent(DealList.this, DealDetails.class);
startActivity(dealDetails);

Dla tego obiektu klasy Intent nie określono akcji, typu ani danych, co zrobiono
dla intencji we wcześniejszych podrozdziałach. Ten obiekt klasy Intent wska-
zuje bezpośrednio na konkretną klasę, DealDetails.class, i informuje: „to ona
służy do obsługi”. Jest to bezpośredni obiekt klasy Intent. Intencje bezpośrednie
dość często występują w niezależnych aplikacjach, w których dokładnie wia-
domo, co każdy komponent robi i jaka jest nazwa klasy (intencje tego rodzaju są
proste).
Jeśli jednak chcesz wyjść poza granice aplikacji i (lub) korzystać z funkcji
opisanych w bardziej abstrakcyjny sposób (takich jak wyświetlanie danej strony,
wybieranie danego numeru telefonu, pokazywanie danej mapy itd.), powinieneś
użyć pośredniego obiektu klasy Intent. Intencje pośrednie są wiązane z kompo-
nentami, które potrafią je obsłużyć, na podstawie kombinacji podanych opcjonal-
nych atrybutów. Są to między innymi akcja, dane i typ. W tabeli 2.1 znajdziesz
definicje różnych atrybutów.
Akcja, dane, typ i kategoria służą do odwzorowywania pośrednich obiektów
klasy Intent na komponent deklarujący, że potrafi ją obsłużyć. W intencjach
bezpośrednich w kodzie na stałe zapisywany jest komponent wywoływany do
ich obsługi. Bezpośrednie podejście łatwo jest zrozumieć. Model pośredni jest
bardziej skomplikowany i obejmuje określanie obiektu klasy Intent.

2.8.3. Określanie intencji


W obiektach klasy Intent deklarowane są pożądane operacje. Intencje służą do
wywoływania innych komponentów. Następnym fragmentem układanki jest zade-
klarowanie, jakiego rodzaju akcje, typy i kategorie komponenty obsługują, aby
102 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

Tabela 2.1. Atrybuty, które można definiować i stosować przy deklarowaniu oraz
wiązaniu obiektów klasy Intent

Nazwa atrybutu
Opis Przykłady
intencji

action Wykonywana akcja. ACTION_VIEW, ACTION_DIAL,


ACTION_SHARE, ACTION_EDIT
data Dane, na których akcja jest content://contacts/people/1,
wykonywana. http://www.reddit.com
type Typ MIME danych intencji. text/*, text/plain, text/html,
Opcjonalnie można go wywnioskować image/png
z danych.
category Dodatkowe wskazówki na temat CATEGORY_LAUNCHER,
akcji do wykonania. CATEGORY_ALTERNATIVE
extras Obiekt klasy Bundle z dodatkowymi putExtra("KEY", "VALUE")
informacjami.
component Klasa komponentu do zastosowania. MyActivity.class
Pomijane jest wtedy sprawdzanie
innych atrybutów intencji.

można było je wykorzystać do realizowania intencji z innych jednostek. Intencje


można realizować przez deklarowanie i stosowanie obiektów klasy IntentFilter.
Przykładowa deklaracja obiektu klasy IntentFilter znajduje się w manife-
ście aplikacji DealDroid z listingu 2.1. Filtr ten obejmuje akcję MAIN i kategorię
LAUNCHER. Ta deklaracja oznacza, że aktywność DealList można umieścić na ekra-
nie głównym (czyli na ekranie aplikacji Launcher platformy). Inny przykładowy
obiekt klasy IntentFilter to jeden z wielu obiektów zadeklarowanych we wbudo-
wanej aplikacji Messaging z platformy. Oto on:
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>

Obiekt klasy IntentFilter zadeklarowany w aplikacji Messaging informuje, że


należy go stosować, jeśli jakaś jednostka chce użyć akcji SEND dla kategorii
DEFAULT i typu danych MIME text/plain. Metoda shareDealUsingChooser z lis-
tingu 2.12 tworzy obiekt klasy Intent o podobnych parametrach. Nie zadekla-
rowano w niej kategorii, jednak nie ma to znaczenia. Kategorie muszą pasować
tylko wtedy, gdy są zadeklarowane w obiekcie klasy Intent. Ujmijmy to dokład-
niej — jeśli w obiekcie klasy Intent określona jest kategoria, obiekt klasy Intent
´Filter musi ją obejmować, aby elementy te do siebie pasowały. Jeżeli w obiek-
cie klasy Intent kategorii nie zdefiniowano, w filtrze mogą być podane dowolne
kategorie.
Z kategoriami związane jest pewne zastrzeżenie. Przy każdym wywołaniu
metody Context.startActivity z pośrednią intencją (bez wskazanego kompo-
nentu) system automatycznie ustawia dla obiektu klasy Intent kategorię DEFAULT.
2.9. Obiekty klasy Application 103

Oznacza to, że w każdym obiekcie klasy IntentFilter, który ma obsługiwać


pośrednie intencje, należy zadeklarować obsługę kategorii DEFAULT (jak zrobiono
to w odpowiednim obiekcie z aplikacji Messaging).
Ponieważ obiekt klasy IntentFilter z aplikacji Messaging pasuje do obiektu
klasy Intent z metody shareDealUsingChooser, na liście opcji z rysunku 2.8 znaj-
duje się aktywność ComposeMessageActivity z tej aplikacji.
Dalej w książce napotkasz wiele innych przykładowych intencji i filtrów
intencji. Teraz zapamiętaj, że Android śledzi wszystkie dostępne filtry intencji
i w czasie wykonywania programu dopasowuje zgłoszone intencje do kompo-
nentów, które potrafią je obsłużyć. Android śledzi zarejestrowane deklaracje
obiektów klasy IntentFilter za pomocą klasy PackageManager (do której można
też kierować zapytania; pozwala to ustalić, jakie komponenty są dostępne w danej
chwili). W momencie instalowania danej aplikacji dodawane są zapisane w niej
deklaracje. System usuwa je przy odinstalowywaniu aplikacji.
Utworzyliśmy już dużą część aplikacji DealDroid. Opracowaliśmy układ
i aktywność DealList, a także aktywność DealDetails. Dowiedziałeś się też, jak
konfigurować manifesty, jak deklarować zasoby, jak ich używać, jak deklarować
i wywoływać intencje, jak korzystać z widoków i kontrolek, a także jak stosować
adaptery. Ostatnim krokiem prac nad aplikacją DealDroid jest jej zbudowanie.
Warto też poznać obiekt klasy Application, który zastosowano na kilku wcze-
śniejszych listingach.

2.9. Obiekty klasy Application


W tym rozdziale zobaczyłeś już dużą ilość kodu. W kilku miejscach wystąpiła
referencja do klasy aplikacji. Może sobie przypominasz, że jest to referencja do
klasy DealDroidApp. DealDroidApp to klasa pochodna od klasy Application Andro-
ida. Klasa Application ma ściśle zdefiniowany cykl życia i można jej używać jako
struktury danych do przechowywania globalnego stanu aplikacji. Więcej o cyklu
życia dowiesz się z rozdziału 3. Na razie warto zapamiętać, że obiekt klasy
Application jest tworzony w momencie tworzenia procesu aplikacji. Obiekt ten
nie jest powiązany z żadnym konkretnym obiektem typu Activity lub Service.
Oznacza to, że zapewnia doskonały i niezwykle prosty sposób na przechowy-
wanie i współużytkowanie skomplikowanych nietrwałych danych różnych aktyw-
ności i usług z aplikacji. Skomplikowane i nietrwałe dane to potrzebne aplikacji
informacje, które niewygodnie jest przekazywać jako dodatkowe elementy inten-
cji, a poza tym nie nadają się one do zapisania w pliku lub bazie danych.
INNY SPOSÓB NA WSPÓŁDZIELENIE DANYCH W APLIKACJI.
Innym dobrym narzędziem do przechowywania skomplikowanych i nie-
trwałych danych jest statyczny obiekt singleton. Jednak przy korzystaniu
z obiektów statycznych należy zachować ostrożność. Nie mają dobrze
zdefiniowanego cyklu życia, dlatego łatwo pozostawić po nich „wiszącą”
104 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

referencję, co może prowadzić do wyciekania pamięci. Jeśli wolisz korzy-


stać z obiektów statycznych zamiast z obiektu klasy Application, oczywiście
możesz tak zrobić, rozważ jednak konfigurowanie i usuwanie klas statycz-
nych w obiekcie klasy Application, która ma ściśle zdefiniowany cykl życia.
Pozwala to wykorzystać zalety obu podejść.
Na listingu 2.13 wreszcie zobaczysz klasę DealDroidApp używaną w kilku wcze-
śniejszych aktywnościach.

Listing 2.13. W pliku DealDroidApp.java znajduje się współdzielona klasa


Application z aplikacji DealDroid

public class DealDroidApp extends Application {

private ConnectivityManager cMgr;


private DailyDealsFeedParser parser;
private List<Section> sectionList;
private Map<Long, Bitmap> imageCache;
private Item currentItem;

public DailyDealsFeedParser getParser() {


return this.parser;
}

public List<Section> getSectionList() {


return this.sectionList;
}

public Map<Long, Bitmap> getImageCache() {


return this.imageCache;
}

public Item getCurrentItem() {


return this.currentItem;
}

public void setCurrentItem(Item currentItem) {


this.currentItem = currentItem;
}

@Override
public void onCreate() {
super.onCreate();
this.cMgr = (ConnectivityManager)
this.getSystemService(Context.CONNECTIVITY_SERVICE);
this.parser = new DailyDealsXmlPullFeedParser();
this.sectionList = new ArrayList<Section>(6);
this.imageCache = new HashMap<Long, Bitmap>();
}

// Metody pomocnicze retrieveBitmap i connectionPresent


// pominięto w celu skrócenia listingu.
}
2.10. Podsumowanie 105

Klasa DealDroidApp, podobnie jak większość klas pochodnych od klasy Applica


´tion , obejmuje kilka składowych używanych w różnych miejscach aplika-
cji, a także kilka metod narzędziowych (które tu pominięto). Składowe z klasy
DealDroidApp to :

Q ConnectivityManager,
Q implementacja DailyDealsFeedParser klasy XMLPullParser służąca
do przetwarzania kanału RSS,
Q lista sekcji (jeśli istnieją),
Q obiekt klasy Map do zapisywania małych rysunków w pamięci podręcznej,
Q bieżący zaznaczony element (jeśli taki istnieje).
Po deklaracjach zmiennych składowych znajduje się kod przesłaniający metodę
obsługi cyklu życia, onCreate , potrzebny do skonfigurowania rozwijanej wer-
sji klasy Application. Metoda onCreate tworzy kilka ciekawych obiektów. Pierw-
szym z nich jest obiekt klasy ConnectivityManager. Klasa ta to usługa systemowa
pozwalająca sprawdzać stan sieci (więcej na ten temat dowiesz się z dalszych
przykładów). Drugim jest obiekt klasy DailyDealsFeedParser, używany w aktyw-
ności DealList do przetwarzania kanału RSS z ofertami dnia (przetwarzanie danych
w formacie XML omówiono w rozdziale 6.). Dalszy kod tworzy kilka standar-
dowych obiektów klasy Collection Javy, przeznaczonych do przechowywania
danych.
Ostatni krok to upewnienie się, że aplikacja będzie używać niestandardo-
wego obiektu klasy Application. Zapewnia to opisany już kod z manifestu. Na
listingu 2.1 użyto odpowiedniego atrybutu name w elemencie application (gdyby
nie to, zostałby wykorzystany domyślny obiekt klasy Application).
<application android:icon="@drawable/ddicon"
android:label="@string/app_name"
android:name=".DealDroidApp">

To już wszystko! Po dodaniu globalnego stanu i udostępnieniu metod narzę-


dziowych w nowej klasie Application aplikacja DealDroid będzie kompletna.
Ten ostatni fragment aplikacji DealDroid jest jednocześnie przystankiem koń-
cowym w podróży po podstawach aplikacji na Android.

2.10. Podsumowanie
Przedstawiając tworzenie aplikacji DealDroid, omówiliśmy wiele najistotniejszych
zagadnień związanych z rozwijaniem programów na Android. Wyruszyliśmy
w tę podróż, aby przybliżyć Ci podstawy, upewnić się, że znasz główne kompo-
nenty aplikacji na Android, a także że wiesz, jak korzystać z nich w bardziej
skomplikowanych aplikacjach. Staraliśmy się przy tym zachować ogólny charak-
ter omówienia.
Dowiedziałeś się, że podstawowymi cegiełkami aplikacji na Android są mani-
fest aplikacji, zasoby, układy, widoki, aktywności i intencje. Manifest obejmuje
106 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android

konfigurację aplikacji, a zasoby to przechowywane zewnętrznie elementy (na


przykład łańcuchy znaków i rysunki). Kod pojawia się w aktywnościach, które
pobierają zasoby i układy. Układy to zbiory widoków porządkujące interfejs użyt-
kownika na ekranie lub w komponencie. Układy często są zapisywane w forma-
cie XML i przekształcane na klasy w czasie wykonywania programu, co dodat-
kowo pomaga rozdzielać elementy aplikacji. Aktywności na podstawie widoków
i kontrolek tworzą wyświetlane użytkownikowi elementy, z którymi można wcho-
dzić w interakcje. Intencje łączą komponenty, a nawet różne aplikacje.
Zaprezentowaliśmy aplikację DealDroid i podstawowe kwestie związane
z używanymi w niej komponentami. Teraz skoncentrujemy się na ogólnym cyklu
życia aplikacji i jej komponentów.
Zarządzanie
cyklem życia i stanem

W tym rozdziale
Q Wprowadzenie do procesów aplikacji i identyfikatorów
użytkowników
Q Cykl życia aktywności
Q Obsługa stanu egzemplarzy klasy aktywności
Q Wprowadzenie do zadań i pokrewieństwo zadań

Wszystko jest podobne od zarania wieków i powtarza się w cyklu rzeczy.


Marek Aureliusz
Wszystkie aplikacje na Android są sobie równe. Nie chodzi tu o jakieś filozo-
ficzne ideały — to fakt wynikający z konieczności. Wiele urządzeń z Androidem
(o czym już kilkukrotnie wspomnieliśmy, ale będziemy powtarzać to do znudze-
nia) ma ograniczenia w zakresie pamięci, mocy obliczeniowej i innych zasobów.
Dlatego w projekcie platformy trzeba było uwzględnić sposób na zapewnienie
najważniejszym procesom niezbędnych zasobów i jednoczesne ograniczenie
lub zamknięcie innych procesów, które mogą w tym przeszkadzać.
W Androidzie problem rozwiązano przez zarządzanie procesami aplikacji
w hierarchii, na której szczycie znajdują się bieżące i niedawno używane kompo-
nenty. Kiedy zaczyna brakować zasobów, platforma zamyka najmniej istotne pro-
cesy. Ponadto komponenty Androida udostępniają szereg metod do zarządzania
cyklem życia działających jak wywołania zwrotne. Platforma wykorzystuje te
metody do tworzenia i usuwania komponentów (oraz zmiany ich stanu).

107
108 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Wszystko wygląda pięknie, ale jest pewien problem — użytkowników w ogóle


to nie obchodzi. Chcą, aby aplikacje działały szybko i wydajnie bez utraty danych
o stanie lub zawieszania się przy każdym obróceniu urządzenia. To chyba nie-
wygórowane oczekiwania, prawda? Rzeczywiście, jednak zdziwiłbyś się, gdybyś
wiedział, ile aplikacji na Android — nawet tych rozwijanych przez firmy pro-
gramistyczne — nie funkcjonuje zbyt dobrze w środowisku Androida. Mamy
nadzieję, że przekażemy Ci wiedzę potrzebną do uniknięcia pułapek.
Dowiesz się, czym są aplikacje na Android i że dla każdej z nich używane są
odrębne identyfikatory użytkownika i procesy (przynajmniej zazwyczaj). Dalej
omawiamy, w jaki sposób Android ustala, które procesy można zamknąć. Następ-
nie schodzimy na niższy poziom i opisujemy komponenty aplikacji, a przede
wszystkim klasę aktywności. Aktywności (oraz inne komponenty, na przykład
omówione w dalszych rozdziałach klasy Service i BroadcastReceiver) mają szereg
metod cyklu życia, takich jak onCreate i onPause, umożliwiających kontrolowanie
tworzenia, usuwania oraz odtwarzania obiektów tego rodzaju.
Oprócz cyklu życia trzeba też poznać sposoby obsługi stanu egzemplarzy
klasy Activity i zarządzania nim. Stan egzemplarza to nietrwałe dane, które
trzeba przekazywać między obiektami klasy Activity (przy zmianie orientacji
ekranu odtwarzany jest nowy obiekt aktywności), aby opcje zaznaczone przez
użytkownika i inne elementy nie zostały utracone. Możesz przechowywać takie
dane w ramach cyklu życia, jeśli wiesz, gdzie należy je przekazywać.
Po omówieniu procesów, aplikacji i komponentów przechodzimy do zadań
w Androidzie. Zadanie to grupa powiązanych aktywności dotyczących akcji
biznesowej, którą użytkownik próbuje wykonać. Takie aktywności mogą pocho-
dzić z różnych aplikacji na Android, jednak dla użytkownika wyglądają jak jedna
całość. Wiedza na temat aktywności, stosu aktywności i ich powiązań z zada-
niami pomoże Ci zrozumieć platformę i rozwijać aplikacje.

3.1. Czym są aplikacje w Androidzie?


Jedną z cech Androida jest to, że w aplikacjach na tej platformie można korzystać
z komponentów z innych programów. Jest to łatwe i odbywa się w sposób nie-
zauważalny dla użytkownika. W ten sposób zacierają się granice między aplika-
cjami. Dla użytkownika aplikacja składa się z aktywności z różnych miejsc
(map, przeglądarki, klienta pocztowego, kontaktów i aparatu fotograficznego —
to tylko kilka wbudowanych możliwości). Ważny jest cel użytkownika — nieza-
leżnie od tego, jak wiele komponentów służy do jego realizacji. W Androidzie
takie operacje z udziałem wielu aplikacji to zadania.
Więcej informacji o zadaniach znajdziesz w podrozdziale 3.3. Wspominamy
o nich w tym miejscu, aby jednoznacznie opisać to pojęcie. Tu koncentrujemy
się jednak na technicznej definicji pojedynczej aplikacji. Aplikacja na Android
(w technicznym ujęciu) obejmuje wszystkie komponenty uruchomione z tym
samym identyfikatorem użytkownika i w jednym procesie oraz powiązane z nad-
3.1. Czym są aplikacje w Androidzie? 109

rzędnym obiektem klasy Application. Obiekt ten pełni funkcję centralnego


kontekstu dla wszystkich komponentów (aktywności, usług, odbiorników typu
BroadcastReceiver i dostawców treści). Wszystkie te elementy są łączone w plik
APK, który można umieścić w sklepie jako aplikację.
Dalej korzystamy z podanej technicznej definicji aplikacji. Omawiamy cykl
życia obiektu klasy Application, a także powiązania aplikacji z procesem i identy-
fikatorem użytkownika. Opisujemy też priorytety przyznawane przez Android
różnym procesom w momencie odzyskiwania zasobów.

3.1.1. Cykl życia aplikacji


Każda aplikacja na Android jest „zapisywana” w obiekcie klasy Application.
W rozdziale 2. używaliśmy takiego obiektu do współdzielenia stanu i przechowy-
wania metod narzędziowych, jednak nawet jeśli tworzysz własną klasę (zamiast
rozszerzać istniejącą), dostępne są pewne domyślne elementy. Obiekt klasy
Application ma charakterystyczny cykl życia, który na szczęście jest łatwy do
zrozumienia.
1. W momencie uruchamiania aplikacji wywoływana jest metoda onCreate.
2. Kiedy system żąda od aplikacji przywrócenia możliwych zasobów,
wywoływana jest metoda onLowMemory.
3. Czasem w momencie zatrzymywania aplikacji wywoływana jest metoda
onTerminate.
4. Kiedy w trakcie działania aplikacji zmienia się konfiguracja urządzenia,
wywoływana jest metoda onConfigurationChanged (zobacz stronę
http://mng.bz/LJGK).
Jak widać, istnieją cztery proste metody. Spośród nich najczęściej używa się
metod onCreate i onLowMemory. Z pozostałych metod też można korzystać, jednak
nie ma gwarancji, że metoda onTerminate zostanie wywołana, a metoda onConfigu
´rationChanged jest na poziomie aplikacji potrzebna tylko w zaawansowanych
rozwiązaniach.
Metoda onCreate, jak pokazano w rozdziale 2., służy do konfigurowania począt-
kowego wewnętrznego lub globalnego stanu egzemplarza klasy Application.
Framework Androida automatycznie tworzy obiekt klasy Application przy pierw-
szym uruchomieniu aplikacji i wywołuje wtedy metodę onCreate. Należy jednak
pamiętać o pewnej kwestii — metoda ta powinna działać szybko. Nie należy
umieszczać w niej długich operacji, ponieważ wpływa to na czas uruchamiania
aplikacji. Metodę onLowMemory można wykorzystać do opróżniania pamięci pod-
ręcznej lub zwalniania możliwej pamięci, jeśli system tego zażąda. Korzyść z tego
taka, że jeśli metoda ta jest zaimplementowana w odpowiednio wielu aplika-
cjach, system może odzyskać na tyle dużo pamięci, że nie będzie musiał wyłączać
nieużywanych aplikacji (lub co gorsza, zamykać procesów).
110 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Ważną kwestią związaną z obiektami klasy Application jest to, że są one


tworzone w momencie uruchamiania całej aplikacji, ale nie są usuwane do czasu
jej zamknięcia. Są trwalsze od aktywności, usług, odbiorników typu Broadcast
´Receiver i innych komponentów.
Jeśli chodzi o same procesy, w rozdziale 1. wspomniano, że Android dla
każdej aplikacji stosuje odrębny proces i identyfikator użytkownika. Może się
zastanawiasz, jak jest to związane z obiektami klasy Application i jak kontrolo-
wać poszczególne elementy, jeśli zajdzie taka potrzeba.

3.1.2. Identyfikator użytkownika, proces i wątki aplikacji


Kiedy użytkownik po raz pierwszy żąda komponentu (klasy Activity, Service,
BroadcastReceiver lub ContentProvider) aplikacji na Android, jest ona uruchamiana
z unikatowym identyfikatorem użytkownika i umieszczana w nowym procesie
systemowym powiązanym z tym identyfikatorem. Linuksowy system operacyjny
Androida opisano w rozdziale 1. Wyjaśniono tam także, że stosowanie odręb-
nych procesów dla poszczególnych aplikacji zapewnia izolację pamięci i stanu,
co zwiększa bezpieczeństwo oraz umożliwia wprowadzenie prawdziwej wielo-
zadaniowości.
Należy także wiedzieć, że każdy proces domyślnie działa w jednym wątku
głównym (klasa Thread). Wątek główny jest często nazywany wątkiem interfejsu
użytkownika, jest to jednak mylące określenie, ponieważ oprócz aktywności
z wątku korzystają także odbiorniki typu BroadcastReceiver, dostawcy treści
i usługi. W każdym komponencie w razie potrzeby można uruchomić wątek
odrębny od wątku głównego, aby wykonywać operacje równoległe lub działające
w tle (więcej o wątkach dowiesz się z rozdziału 6.). Opisaną hierarchię — proces,
aplikacja, wątek główny i komponenty — przedstawiono na rysunku 3.1.
Zwykle (i domyślnie) struktura proces-aplikacja-wątek-komponent jest powie-
lana dla każdej aplikacji. Używany jest tylko inny identyfikator użytkownika
i odrębny proces. System operacyjny zarządza wieloma procesami, a do prze-
kazywania danych między nimi służy mechanizm komunikacji międzyproce-
sowej Androida. Aby wyświetlić działające procesy, uruchom polecenie ps w po-
włoce narzędzia ADB lub użyj opcji Device/Show Process Status w narzędziu
DDMS (które korzysta z programu ps). Efekt pokazano na rysunku 3.2.
Na rysunku 3.2 widać, że polecenie ps generuje dużo informacji na temat dzia-
łających procesów. Informacje o opcjach do kontrolowania danych i o znaczeniu
wszystkich kolumn znajdziesz w dokumentacji polecenia. Warto zwrócić uwagę
na identyfikator użytkownika (kolumna 1.), identyfikator procesu (kolumna 2.)
i nazwę (kolumna 8.). Identyfikatory użytkownika to zwykle app_n, gdzie war-
tość n jest zwiększana dla każdej aplikacji. Niektóre wbudowane aplikacje mają
specjalne identyfikatory użytkownika, na przykład radio (telefon) lub system
(ustawienia). Nazwy procesów to nazwy pakietów aplikacji.
3.1. Czym są aplikacje w Androidzie? 111

Rysunek 3.1. Domyślnie każda aplikacja działa w odrębnym procesie z unikatowym


identyfikatorem użytkownika i własnym wątkiem głównym

WYBIERANIE PROCESÓW DO ZAMKNIĘCIA


Platforma Androida stara się jak najdłużej nie zamykać procesów aplikacji. Jed-
nak z uwagi na ograniczone zasoby nie może ich podtrzymywać w nieskończoność.
Kiedy więc przychodzi czas na zamknięcie procesów i w jaki sposób platforma
określa, które zachować, a które usunąć? Wykorzystywana jest do tego pięciopo-
ziomowa hierarchia przedstawiona w tabeli 3.1.
Android stara się podtrzymywać działanie komponentów o najwyższym prio-
rytecie (według hierarchii z tabeli 3.1) i umożliwia zamykanie pozostałych pro-
cesów w celu odzyskania zasobów systemowych. Ciekawym aspektem hierarchii
jest to, że proces obejmujący usługę ma wyższy priorytet niż proces obejmujący
aktywności działające w tle. Oznacza to, że długotrwałe zadania działające w tle
lepiej uruchamiać jako usługi (więcej o usługach dowiesz się z rozdziału 5.).

Cykl życia innych komponentów


Komponenty odbiorników typu BroadcastReceiver, usług i dostawców treści (podob-
nie jak aktywności) są domyślnie wiązane z głównym procesem aplikacji. Choć
korzystają z tego samego procesu, mają inny cykl życia (z innymi metodami).
Odbiornik typu BroadcastReceiver jest prosty. Istnieje w czasie działania zdefinio-
wanej w nim metody onReceive i nie dłużej. Cykl życia usług jest bardziej skom-
plikowany, o czym przekonasz się w rozdziale 5.; dostawców treści omówiono
w rozdziale 8.
112 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Rysunek 3.2. Dane wygenerowane przez polecenie ps w powłoce ADB pozwalają


zobaczyć działające procesy

DOPRACOWYWANIE USTAWIEŃ PROCESU


Opisano tu typową strukturę procesu, która nie zawsze jest odpowiednia.
W zaawansowanych rozwiązaniach można w razie potrzeby zyskać kontrolę nad
konfiguracją i zmodyfikować ustawienia. Możesz określić proces, w którym ma
działać aplikacja, a także wskazywać procesy dla poszczególnych komponentów.
Są to zaawansowane ustawienia, a w tym miejscu nie chcemy zanadto odcho-
dzić od tematu. Pamiętaj jednak, że możesz uruchomić kilka aplikacji w tym
samym procesie lub z wykorzystaniem tego samego identyfikatora użytkownika.
Możesz też uruchomić jedną aplikację w kilku procesach. Android udostępnia
sensowne i łatwe w użyciu ustawienia domyślne, ale jednocześnie zapewnia pełną
kontrolę. Aby zmienić proces, ustaw atrybut android:process w manifeście
(atrybut ten można zastosować do aplikacji i poszczególnych komponentów).
3.2. Cykl życia aktywności 113

Tabela 3.1. Pięć poziomów używanych przez platformę Android do określania


priorytetów procesów

Stan procesu Opis Priorytet


Działający na Proces z aktywnością, z której korzysta użytkownik, z usługą 1
pierwszym planie powiązaną z używaną aktywnością, z usługą wykonującą jedną
z metod obsługi cyklu życia lub z działającym odbiornikiem
typu BroadcastReceiver.
Widoczny Proces, który nie jest używany na pierwszym planie, ale 2
obejmuje aktywność potencjalnie wpływającą na wygląd
ekranu lub usługę powiązaną z tego rodzaju aktywnością.
Usługa Proces obejmujący usługę uruchomioną za pomocą metody 3
startService (i który nie spełnia kryteriów dla procesów
z pierwszego planu i widocznych).
Działający w tle Proces obejmujący zatrzymaną aktywność. Istnieć może wiele 4
procesów tego rodzaju. Są one przechowywane na liście
typu LRU.
Pusty Proces, który nie obejmuje aktualnych komponentów aplikacji. 5

Dlaczego miałbyś zmieniać wspomniane ustawienia? Jeśli chcesz uruchomić kilka


aplikacji i zachować łatwy dostęp do tych samych plików (lub innych zasobów,
na przykład bazy danych), a jednocześnie zabezpieczyć dane przed innymi
programami, możesz uruchomić odpowiednie aplikacje w tym samym procesie.
Ponadto jeśli potrzebujesz wysokiej wielozadaniowości i chcesz ręcznie w po-
prawny sposób ją kontrolować, możesz uruchomić każdą aktywność w odrębnym
procesie (więcej o usługach dowiesz się z rozdziału 5.).
Warto wiedzieć, w jaki sposób elementy systemowe, na przykład identyfi-
katory użytkownika i procesy, wpływają na aplikacje na Android, jednak także
same komponenty mają określony cykl życia. Jednym z najważniejszych — i nie-
stety potencjalnie najbardziej kłopotliwych — aspektów pisania aplikacji na
Android jest cykl życia aktywności.

3.2. Cykl życia aktywności


Aktywności, podobnie jak procesy, nie są podtrzymywane w nieskończoność,
ponieważ zajmują pamięć i wykorzystują cykle procesora. Jeśli z jedną aplikacją
powiązanych jest kilka aplikacji, to nawet w ramach procesu niektóre aktywno-
ści działają na pierwszym planie, natomiast inne — nie. Priorytetowo traktowane
są aktywności z pierwszego planu, z których korzysta użytkownik. Inne aktyw-
ności mogą zostać zatrzymane, kiedy platforma musi odzyskać zasoby (lub są
zamykane, jeśli dzieje się to z obejmującym je procesem; zamykanie odbywa
się na podstawie hierarchii opisanej w poprzednim podrozdziale).
Dla użytkowników przełączanie procesów i aktywności przez platformę
powinno być niezauważalne. Dla nich cały proces wykonywania zadania musi
być płynny. Tworzenie nowych aktywności i przywracanie innych ze stanu uśpie-
nia powinno być niedostrzegalne.
114 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Dla programistów sytuacja jest bardziej skomplikowana. Wiemy, kiedy należy


tworzyć i usuwać zasoby oraz jak zachować stan w obliczu nieustannego tworze-
nia, usuwania i odtwarzania aktywności. Nasze zadanie polega na tym, aby z wyko-
rzystaniem platformy sprawić, że operacje będą wykonywane płynnie. Aktyw-
ności powinny przy tym reagować szybko i działać poprawnie. Realizacja tego
celu wymaga znajomości etapów i metod cyklu życia aktywności.

3.2.1. Etapy i metody cyklu życia


Aby zmierzyć się z problemem, zacznijmy od ogólnego obrazu — etapów cyklu
życia. Dalej omawiamy najważniejsze metody cyklu życia. Aktywności mają trzy
hierarchiczne etapy cyklu życia:
Q cały cykl życia (od utworzenia do usunięcia),
Q cykl życia w stanie widoczności (od ponownego uruchomienia
do zatrzymania),
Q cykl życia w stanie działania na pierwszym planie (od wznowienia
do wstrzymania).
Te etapy cyklu życia określają względne znaczenie aktywności w systemie i zapew-
niają logiczne punkty, w których można podłączać, tworzyć, stosować i usuwać
zasoby (widoki, usługi systemowe, kursory bazodanowe, żądania sieciowe i inne).
Pierwszy etap, cały cykl życia, to najogólniejsza kategoria. Obejmuje wszystko —
od momentu utworzenia aktywności do czasu jej usunięcia. Następny etap, stan
widoczności, obejmuje okres, w którym aktywność działa na ekranie i jest widoczna,
choć niekoniecznie jest wykonywana na pierwszym planie (może znajdować się
w stanie przejściowym lub za inną, „pływającą” aktywnością). Ostatni etap,
działania na pierwszym planie, jest najważniejszy. To tu aktywność wchodzi
w interakcje z użytkownikiem.
Do kontrolowania przechodzenia między etapami oraz obsługiwania konfi-
gurowania i usuwania zasobów służą metody cyklu życia. Z kilku z nich (na przy-
kład onCreate i onPause) już korzystaliśmy. Bez wątpienia znasz je przynajmniej
ogólnie. Tu opisano je dokładniej, ponieważ właściwe korzystanie z tych metod jest
niezbędne do tworzenia solidnych aplikacji na Android. Najważniejsze z tych
metod przedstawiono w tabeli 3.2 (jest ona oparta bezpośrednio na dokumen-
tacji Androida).
Choć ogólnie staramy się unikać powielania informacji z dokumentacji Andro-
ida, to w przypadku tabeli 3.2 celowo zrobiliśmy wyjątek. Metody cyklu życia
aktywności mogą rodzić wiele wątpliwości, a dokumentacja jest niezwykle waż-
nym źródłem wiedzy. W tabeli 3.2 pokazano, kiedy kończą się i zaczynają etapy
cyklu życia, a także umieszczono krótki opis każdej metody cyklu życia, infor-
macje o tym, czy można wymusić zakończenie pracy poszczególnych metod,
i kolejność ich wykonywania.
3.2. Cykl życia aktywności 115

Tabela 3.2. Metody cyklu życia

Można
Metoda Opis Następna
zamknąć?

onCreate Wywoływana przy początkowym tworzeniu Nie onStart


aktywności. To tu należy przeprowadzić
standardową statyczną konfigurację — utworzyć
widoki, powiązać dane z listami itd. Udostępnia
też obiekt klasy Bundle obejmujący wcześniej
zamrożony stan aktywności (jeśli taki obiekt
istnieje).
Następną metodą zawsze jest onStart.
onRestart Wywoływana po zatrzymaniu aktywności, Nie onStart
ale przed jej ponownym uruchomieniem.
Następną metodą zawsze jest onStart.
onStart Wywoływana, kiedy aktywność staje się widoczna Nie onResume
dla użytkownika. lub onStop
Następną metodą jest onResume, jeśli aktywność
zaczyna działać na pierwszym planie, lub onStop,
jeżeli jest ukrywana.
onResume Wywoływana, kiedy aktywność wchodzi Nie onPause
w interakcje z użytkownikiem. Na tym etapie
aktywność znajduje się na szczycie stosu
aktywności i trafiają do niej dane wejściowe
od użytkownika.
Następną metodą zawsze jest onStart.
onPause Wywoływana, kiedy system ma zacząć W wersjach onResume
wznawianie wcześniejszej aktywności. Zwykle starszych lub onStop
zatwierdzane są wtedy niezapisane zmiany niż 3.0
w trwałych danych, zatrzymywane są animacje
oraz inne operacje obciążające procesor itd.
Metoda musi działać szybko, ponieważ następna
aktywność nie zostanie wznowiona do czasu
zwrócenia sterowania przez tę metodę.
Następną metodą jest onResume, jeśli aktywność
zaczyna działać na pierwszym planie, lub onStop,
jeżeli jest ukrywana.
onStop Wywoływana, kiedy aktywność nie jest widoczna Tak onRestart
dla użytkownika, ponieważ inna aktywność lub onDestroy
została wznowiona i zakrywa daną. Może to być
wynik uruchomienia nowej aktywności,
przeniesienia innej istniejącej aktywności przed
daną lub usuwania danej aktywności.
Następną metodą jest onRestart , jeśli dana
aktywność ponownie wchodzi w interakcje
z użytkownikiem, lub onDestroy, jeżeli aktywność
jest usuwana.
onDestroy Jest to ostatnie wywołanie zgłaszane przed Tak Nic
usunięciem aktywności. Może to mieć miejsce
z powodu kończenia pracy aktywności (wywołania
dla niej metody finish ) lub z uwagi
na tymczasowe usunięcie danego egzemplarza
aktywności przez system w celu odzyskania
zasobów. Do rozróżniania tych sytuacji służy
metoda isFinishing.
116 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

W tabeli 3.3 dokładnie opisano najczęściej przesłaniane metody cyklu życia


i dodano pewne uwagi.
Tabela 3.3. Najczęściej przesłaniane metody cyklu życia aktywności,
czas ich wywoływania i typowe zastosowania

Metoda Czas wywoływania Kiedy przesłaniane? Opis i uwagi

onCreate Wywoływana, kiedy Zawsze, przy czym Tu należy umieścić kod


aktywność jeszcze nie należy wywołać metodę inicjujący. Jeśli aktywność
istnieje i trzeba super, co zresztą dotyczy jest uruchamiana po raz
ją utworzyć. wszystkich przesłanianych pierwszy, nie ma
metod cyklu życia. zapisanego stanu
(przekazany obiekt Bundle
to null). Jeżeli aktywność
wcześniej usunięto i jest
ponownie uruchamiana,
może mieć stan (obiekt
Bundle to wartość zapisana
w ostatnim wywołaniu
metody
onSaveInstanceState).
onResume Wywoływana, kiedy Metodę onResume często Wywołanie tej metody
aktywność zaczyna przesłania się w celu oznacza, że aktywność
działać na pierwszym aktualizowania widoków, jest wyświetlana i obsługuje
planie i użytkownik jednak nie należy w niej zdarzenia generowane
wchodzi z nią odtwarzać komponentów. przez użytkowników. Jest
w interakcje. W metodzie tej można to ostatnia metoda cyklu
odświeżać widoki życia, której nie można
na podstawie danych zamknąć.
pobranych przez usługę
sieciową, jeśli dane
te zmieniły się między
zatrzymaniem
a wznowieniem pracy
aktywności.
onPause Wywoływana, kiedy Metoda onPause często Tu zapisywany jest
aktywność zaczyna jest przesłaniana. globalny stan trwały lub
działać w tle, ale nie To tu należy wykonać stan powiązany z zadaniem
została jeszcze operacje porządkujące lub aplikacją zachowywany
zamknięta. dla wszystkich elementów po usunięciu aktywności
utworzonych przez (obejmuje on dane, które
aktywność. trzeba zapisać w plikach,
bazach itd.). Tu też
zwalniane są zasoby. Często
wyrejestrowuje się
tu odbiorniki intencji
i wiązania usług, usuwa
odbiorniki lokalizacji oraz
czujników, zatrzymuje
wątki tła itd.

W tabeli 3.2 przedstawiono trzy najczęściej używane metody — onCreate, onResume


i onPause. Jak widać na rysunku 3.3, nie są to jedyne metody cyklu życia aktyw-
ności. Jeśli potrzebujesz większej kontroli, przydatne mogę być także niektóre
inne metody, na przykład onStart, onStop i onDestroy.
Teraz, kiedy już znasz metody cyklu życia i wiesz, do czego służą, można przejść
do praktycznego przykładu, który pozwoli utrwalić te informacje.
3.2. Cykl życia aktywności 117

Rysunek 3.3. Trzy aktywności aplikacji LifecycleExplorer wyświetlają zdarzenia


związane z metodami cyklu życia. Przedstawiono tu powiadomienia, aktywności
ze stosu i pracę ze stanem egzemplarza

3.2.2. Cykl życia w praktyce


Aby pokazać, co konkretnie powoduje, że dla aktywności wywoływane są kolejne
metody cyklu życia (kiedy jest wstrzymywana i wznawiana oraz kiedy jest zamy-
kana), omawiamy tu przykład obejmujący rejestrowanie informacji i wyświetlanie
powiadomień na każdym etapie. Następnie wypróbowujemy aplikację i spraw-
dzamy, co się dzieje. Pozwala to pokazać, w jaki sposób aktywności są umiesz-
czane na stosie, a także bezpośrednio stwierdzić, co dzieje się po początkowym
uruchomieniu aplikacji oraz kliknięciu przycisków Back lub Home. Opisujemy
też zamykanie procesu obejmującego aktywność (system wykonuje tę samą ope-
rację, kiedy odzyskuje zasoby).
Omawiana aplikacja obejmuje abstrakcyjną klasę bazową Activity, która
rejestruje informacje i opcjonalnie wyświetla powiadomienie (komunikat typu
Notification) dla każdej wywołanej metody cyklu życia. Na podstawie bazowej
klasy Activity utworzono trzy inne, co pozwala przyjrzeć się metodom cyklu życia,
zobaczyć, jak funkcjonuje stos, a także wykorzystać stan egzemplarza. Kom-
pletną aplikację LifecycleExplorer przedstawiono na rysunku 3.3.
Ekrany aktywności aplikacji LifecycleExplorer można trafnie opisać słowami
„ubogie” i „brzydkie”, jednak nie ma w tym nic złego. Tu przedkładamy treść
nad formę. Pierwszy ekran, Main, obejmuje kilka prostych elementów interfejsu
użytkownika i przycisków pozwalających przejść do następnej aktywności lub
zakończyć bieżącą. Drugi ekran, Activity2, określa miejsce na stosie aktywności.
Trzeci ekran, Activity3, wykorzystano dalej do pracy ze stanem egzemplarza.
118 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

POBIERZ PROJEKT LIFECYCLEEXPLORER. Kod źródło-


wy projektu i spakowany plik APK do uruchomienia znaj-
dziesz na witrynie poświęconej książce Android in Practice.
Ponieważ niektóre listingi skrócono, co pozwala skupić się
na konkretnych zagadnieniach, zachęcamy do pobrania kom-
pletnego kodu źródłowego i śledzenia go w Eclipse (lub in-
nym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/Hbuq, plik APK: http://mng.bz/vUQO.
Na ekranie Main aplikacji LifecycleExplorer ważne jest nie tyle to, co się wyświe-
tla, ile to, w jaki sposób można zwizualizować działanie metod cyklu życia za
pomocą generowanych powiadomień, co przedstawiono na rysunku 3.4.

Rysunek 3.4. Ekran


aktywności Main
aplikacji
LifecycleExplorer
generuje
powiadomienia

Powiadomienia generowane przez aplikację LifecycleExplorer, jak widać na


rysunku 3.4, obejmują nazwę klasy, nazwę metody i czas. Powiadomienia pozwa-
lają stwierdzić, że przy pierwszym uruchomieniu aplikacji, kiedy to wywoływana
jest aktywność Main, używane są metody onCreate, onStart i onResume. Na listingu 3.1
przedstawiono kod związany z omawianym ekranem.

Listing 3.1. Aktywność z pliku Main.java aplikacji LifecycleExplorer z przesłoniętymi


metodami cyklu życia

public class Main extends LifecycleActivity {

private Button finish;


private Button activity2;
private Chronometer chrono;

@Override
3.2. Cykl życia aktywności 119

public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.main);
finish = (Button) findViewById(R.id.finishButton);
finish.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
finish();
}
});
activity2 = (Button) findViewById(R.id.activity2Button);
activity2.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
startActivity(new Intent(Main.this,
Activity2.class));
}
});
chrono = (Chronometer) findViewById(R.id.chronometer);
}

@Override
protected void onResume() {
super.onResume();
chrono.setBase(SystemClock.elapsedRealtime());
chrono.start();
}

@Override
protected void onPause() {
chrono.stop();
super.onPause();
}
}

Na początku kodu ekranu Main znajduje się deklaracja rozszerzania klasy Life
´cycleActivity . Kod tej wysyłającej powiadomienia klasy przedstawiono
dalej. Po deklaracji zastosowano podstawowy wzorzec przesłaniania klas, z którego
zawsze korzystamy przy tworzeniu klas aktywności. W metodzie onCreate konfi-
gurowane są widoki, w metodzie onResume resetowane są wszystkie potrzebne
elementy, a w metodzie onPause wykonywane są operacje porządkujące. Do
demonstrowania zatrzymywania pracy w czasie wstrzymywania i resetowania
w czasie wznawiania służy kontrolka Chronometer. Jest to wymyślna kontrolka
TextView odliczająca sekundy. Kiedy aktywność jest wstrzymana, aplikacja zawie-
sza odliczanie. Przyznajemy, że jest to bardzo sztuczny przykład, ale chcieliśmy
zachować prostotę. Bardziej realistyczne byłyby na przykład aktualizacje danych
w metodzie onResume lub zapisywanie danych i zwalnianie zasobów w rodzaju
odbiorników w metodzie onPause.
Ostatnią ciekawą rzeczą jest to, że dodano przycisk (kontrolka Button) do
wywoływania intencji (obiekt klasy Intent) w celu przejścia do drugiego ekranu
aplikacji, Activity2 . Klasa Activity2 nie obejmuje żadnego specjalnego kodu,
120 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

a tylko przedstawione na rysunku 3.5 kontrolki TextView i Button, dlatego pomi-


nięto ją w tekście (jak zawsze, możesz pobrać i przejrzeć kompletną aplikację).
Klasę Activity2 zastosowano po to, aby można było umieścić kilka aktywności
na stosie i zobaczyć, jak działa przycisk Back.

Rysunek 3.5. Metody


cyklu życia zgłaszane
po wciśnięciu
przycisku Home
i Back

Następny fragment kodu aplikacji LifecycleExplorer, który omawiamy, to rozsze-


rzana klasa LifecycleActivity (można ją rozszerzać w dowolnej aktywności). Kod
z listingu 3.2 rejestruje informacje i generuje powiadomienia związane z meto-
dami cyklu życia.

Listing 3.2. Klasa z pliku LifecycleActivity.java wysyła powiadomienia dotyczące


każdej metody cyklu życia

public abstract class LifecycleActivity extends Activity {

private static final String LOG_TAG = "LifecycleExplorer";

private NotificationManager notifyMgr;


private boolean enableNotifications;
private final String className;

public LifecycleActivity() {
super();
this.className = this.getClass().getName();
}

public LifecycleActivity(final boolean enableNotifications) {


this();
this.enableNotifications = enableNotifications;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
notifyMgr = (NotificationManager)
3.2. Cykl życia aktywności 121

getSystemService(Context.NOTIFICATION_SERVICE);
debugEvent("onCreate");
}

@Override
protected void onStart() {
debugEvent("onStart");
super.onStart();
}

@Override
protected void onResume() {
debugEvent("onResume");
super.onResume();
}

@Override
protected void onPause() {
debugEvent("onPause");
super.onPause();
}

// Pozostałe metody cyklu życia, takie jak onStop,


// onDestroy i inne, ze względu na zwięzłość
// pominięto. Działają podobnie jak inne metody:
// uruchamiają debugowanie i wywołują metodę z klasy super.

private void debugEvent(final String method) {


long ts = System.currentTimeMillis();
Log.d(LOG_TAG, " *** " + method + " " + className + " " + ts);
if (enableNotifications) {
Notification notification =
new Notification(android.R.drawable.star_big_on,
"Zdarzenia cyklu życia: " + method, 0L);
RemoteViews notificationContentView =
new RemoteViews(getPackageName(),
R.layout.custom_notification_layout);
notification.contentView = notificationContentView;
notification.contentIntent =
PendingIntent.getActivity(this, 0, null, 0);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notificationContentView.setImageViewResource(
R.id.image, android.R.drawable.btn_star);
notificationContentView.setTextViewText(
R.id.lifecycle_class, getClass().getName());
notificationContentView.setTextViewText(
R.id.lifecycle_method, method);
notificationContentView.setTextColor(
R.id.lifecycle_method, R.color.black);
notificationContentView.setTextViewText(
R.id.lifecycle_timestamp, Long.toString(ts));
notifyMgr.notify((int) System.currentTimeMillis(), notification);
}
}
}
122 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Klasa LifecycleActivity obejmuje nowe wersje każdej z przesłanianych metod


cyklu życia aktywności. W wersjach tych wywoływana jest lokalna metoda debug
´Event z argumentem w postaci nazwy metody . Sama metoda debugEvent
rejestruje w dzienniku nazwę metody, nazwę klasy i czas, a także opcjonalnie
wysyła powiadomienie z tymi samymi informacjami (aby zobaczyć dane wyjściowe,
można użyć narzędzia logcat, co jest szybszym rozwiązaniem, lub wyświetlić
powiadomienia w interfejsie użytkownika) . Dla zachowania porządku zamiesz-
czono szczegóły powiadomień, natomiast objaśnianie tego zagadnienia wykra-
cza poza zakres omawianego przykładu. Więcej o powiadomieniach dowiesz się
w kontekście usług w rozdziale 5.
Teraz, kiedy już wiesz, jak prosty jest kod i jak działa, pora wypróbować
aktywność Main. Na rysunku 3.4 pokazano, że pierwsze uruchomienie tej aktyw-
ności powoduje wywołanie metod onCreate, onStart i onResume (w tej właśnie
kolejności). Co się dzieje po wciśnięciu przycisku Home lub Back? Pojawiają się
wtedy powiadomienia widoczne na rysunku 3.5.
Co ciekawe, ścieżki cyklu życia po wciśnięciu przycisków Home i Back (po
usunięciu wcześniejszych powiadomień i ponownym uruchomieniu maszyny
wirtualnej) są inne, co pokazano na rysunku 3.6. Ścieżka po wciśnięciu przycisku
Home obejmuje metody onSaveInstanceState, onPause i onStop. Kiedy aktywność
jest usuwana przez system, a następnie wznawiana, do metody onCreate prze-
kazywany jest w obiekcie klasy Bundle stan egzemplarza aktywności. Metoda
onSaveInstanceState służy do zapisywania tego stanu. Tak naprawdę nie jest to
metoda cyklu życia, ale jest z nim związana i jest ważna, dlatego w tym miejscu
powiadomienia są zgłaszane także dla niej. Ścieżka dla przycisku Back obejmuje
wywołania metod onPause, onStop i onDestroy.

Rysunek 3.6.
Przejście z ekranu
Main do Activity2
w aplikacji
LifecycleExplorer
pozwala zobaczyć
zdarzenia cyklu
życia wywoływane
dla każdej
aktywności

Dlaczego wciśnięcie przycisku Back nie prowadzi do zapisania stanu i powo-


duje usunięcie aktywności, a przycisk Home pozwala zapisać stan egzemplarza
3.2. Cykl życia aktywności 123

aktywności i zapobiega jego usunięciu? Domyślne działanie przycisku Back


polega na zdjęciu bieżącej aktywności ze stosu przez wywołanie metody finish,
która usuwa aktywność bez zapisywania stanu. Jeśli aktywność zostaje zamknięta,
nie trzeba zachowywać jej stanu (ponieważ zakończyła działanie). Przycisk
Home nie kończy działania aktywności, a jedynie przenosi ją na szczyt stosu
aktywności i powoduje, że zaczyna ona działać w tle.

Stos aktywności
Kiedy użytkownicy przechodzą między aktywnościami, każda z nich jest umiesz-
czana na liniowym stosie (na tak zwanym stosie aktywności). Użytkownicy za
pomocą przycisku Back mogą cofnąć się do wcześniejszych aktywności, co powo-
duje zdjęcie bieżącej aktywności ze stosu i wznowienie poprzedniej. Końcem stosu
jest ekran główny (aplikacja Launcher). Więcej o stosie aktywności dowiesz się
z podrozdziału 3.4, gdzie omówiono zadania.

Stos aktywności może wydawać się skomplikowany, jednak pozwala platformie


w łatwy sposób śledzić aktywności wywoływane przez użytkownika i umożli-
wiać powrót do wcześniejszych aktywności. Mechanizm ten jest bardzo atrak-
cyjny dla użytkowników. Jego działaniu możesz się lepiej przyjrzeć, wybierając
przycisk Go to Activity2 na ekranie aktywności Main. Powoduje to przejście do
aktywności Activity2, co pokazano na rysunku 3.6.
Wciśnięcie przycisku Go to Activity2 nie powoduje usunięcia aktywności Main.
Dla aktywności tej wywoływane są metody onSaveInstanceState, onPause i onStop
(tak jak przy wciśnięciu przycisku Home), po
czym aktywność zaczyna działać w tle. Jedno-
cześnie system tworzy aktywność Activity2 (uru-
chamiając metody onCreate, onStart i onResume)
oraz ją wyświetla. Wciśnięcie przycisku Back na
ekranie Activity2 powoduje usunięcie tej aktyw-
ności. Na wierzchu stosu znajduje się wtedy
aktywność Main, dlatego jest ona wznawiana. Na
rysunku 3.7 widać związane z tym powiadomie-
nia o zdarzeniach z cyklu życia.
Opisane scenariusze dotyczą poprawnego funk-
cjonowania aplikacji. Pokazano tu, jak działa stos,
co dzieje się w czasie początkowego tworzenia
aktywności, a także w trakcie jej wznawiania
i zamykania za pomocą metody finish. A jak wy-
gląda sytuacja, kiedy występują problemy? Co Rysunek 3.7. Wciśnięcie
się dzieje, kiedy brakuje pamięci i proces z daną przycisku Back powoduje
wyświetlenie zdarzeń
aktywnością zostaje zamknięty? Można się tego dotyczących cyklu życia
dowiedzieć na kilka sposobów. Jeden z nich to przy przechodzeniu z ekranu
Activity2 z powrotem
do ekranu Main
124 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

zalogowanie się do powłoki adb i zamknięcie procesu aplikacji. Inny sposób to


użycie narzędzia ddms do zatrzymania docelowej maszyny wirtualnej.
W obu sytuacjach aktywność musi dojść do metody onResume, aby można było
zamknąć daną aktywność (w przeciwnym razie i tak się ona nie pojawi). Następ-
nie proces można zamknąć w dowolnym momencie. Można opisać to w słynnym
stylu Yogiego Berry’ego — dojdziesz tylko tam, gdzie dojdziesz. Połączenie tej
informacji z wiedzą o tym, które elementy Android zamyka jako pierwsze w trak-
cie odzyskiwania zasobów (zobacz tabelę 3.1), pomoże Ci ustalić aktywności
zamykane w typowych sytuacjach. Jako pierwsze zamykane są aktywności dzia-
łające w tle. Z definicji są to te, które doszły do metody onStop. Potem zamykane
są widoczne aktywności, które jednak nie działają na pierwszym planie. To te,
które doszły do metody onPause.
Skoro już jesteśmy przy problemach — na zakończenie omówienia aktyw-
ności i ich cyklu życia warto wspomnieć o tym, co się dzieje przy zmianie kon-
figuracji.

3.2.3. Zmiany konfiguracji


W klasie Configuration zdefiniowane są wszystkie informacje o konfiguracji urzą-
dzenia udostępniane aplikacji w formie zasobów. Są to między innymi informa-
cje o konfiguracji sprzętowej, orientacji urządzenia, wielkości ekranu, ustawienia
językowe itd. Niektóre aspekty konfiguracji (na przykład ustawienia językowe)
rzadko zmieniają się w czasie wykonywania programu. Inne, takie jak orientacja,
zmieniają się często.
Pułapką, na którą trzeba uważać w Androidzie, jest to, że przy zmianie konfi-
guracji system domyślnie usuwa i odtwarza bieżącą aktywność. Ponieważ zmiana
orientacji (z pionowej na poziomą lub na odwrót) także jest rodzajem zmiany kon-
figuracji, prowadzi to do wielu operacji usuwania i odtwarzania aktywności. Ma
to miejsce za każdym razem, kiedy użytkownik obraca telefon lub wysuwa kla-
wiaturę.
Aby się o tym przekonać, obróć telefon, kiedy uruchomiona jest przykładowa
aplikacja LifecycleExplorer. W celu obrócenia ekranu w emulatorze użyj kom-
binacji Ctrl+F11 na klawiaturze. Następnie za pomocą narzędzia logcat możesz
łatwo sprawdzić ścieżkę cyklu życia na podstawie danych z dziennika (wpisz
polecenie adb logcat w wierszu poleceń lub wybierz opcję Eclipse Window/Show
View/Android/Logcat). Efekt pokazano na rysunku 3.8.
Przy zmianie konfiguracji aktywność przechodzi od metody onPause do onDestroy,
a następnie od metody onCreate do onResume. Ponadto stan egzemplarza jest zapi-
sywany i odtwarzany (metody onSaveInstanceState i onRestoreInstanceState). To
ważne. Aktywności nie są wstrzymywane i wznawiane, ponieważ jest to niemoż-
liwe. Konfiguracja się zmienia. Trzeba ponownie uruchomić aktywność, aby
uwzględnić potencjalne różnice w konfiguracji. Należy jednak zachować stan
3.3. Kontrolowanie stanu egzemplarza aktywności 125

Rysunek 3.8. Dane wyjściowe narzędzia logcat pozwalają zobaczyć, jakie metody
cyklu życia są wywoływane dla aktywności przy zmianie orientacji

egzemplarza, a ponowne uruchamianie powinno być szybkie (niezauważalne dla


użytkowników). Więcej o stanie egzemplarza i szczegółowych danych obiektu
niezwiązanych z konfiguracją dowiesz się z następnego podrozdziału. Na razie
zapamiętaj, że aktywności są często tworzone, usuwane i odtwarzane.
Zrozumienie cyklu życia aktywności i stosu aktywności to klucz do tworze-
nia szybko reagujących oraz niezawodnych aplikacji na Android. Możesz tworzyć
dobrze działające aktywności dzięki wiedzy o etapach i metodach cyklu życia
aktywności oraz o tym, gdzie należy tworzyć i usuwać zasoby (pamiętaj też o tym,
aby nie pozostawiać „wiszących” zasobów, takich jak statyczne referencje). Przy
korzystaniu z aktywności i obsłudze cyklu życia trzeba też wiedzieć o tym,
jak zarządzać stanem egzemplarzy i wznawiać działanie aktywności na jego
podstawie.

Kontrolowanie ustawień związanych ze zmianą konfiguracji


Jeśli nie chcesz, aby aktywność była w całości usuwana i odtwarzana przy zmianie
konfiguracji, odpowiednio ustaw wartość atrybutu android:configChanges w mani-
feście. Następnie możesz wymienić typy zmian konfiguracji samodzielnie obsłu-
giwane w aktywności. Dobrze jest wiedzieć o tym zaawansowanym ustawieniu,
jednak zwykle nie należy stosować go jako alternatywy dla poprawnej obsługi zmian
konfiguracji i prawidłowego przekazywania stanu egzemplarza (nie warto walczyć
z frameworkiem).

3.3. Kontrolowanie stanu egzemplarza aktywności


Jeśli kiedykolwiek wypełniłeś formularz internetowy, a następnie próbowałeś
go przesłać, po czym z powodu jednego błędu system opróżnił wszystkie pola,
wiesz, jak frustrujące jest korzystanie z aplikacji, która nie zarządza stanem i nie
potrafi go odtworzyć. To niezwykle irytujące. Teraz wyobraź sobie ten sam sce-
nariusz, ale w wielokrotnie bardziej frustrującym środowisku. Korzystasz z plat-
formy mobilnej, wprowadzasz dużą ilość danych za pomocą malutkiej klawiatury
wirtualnej, a następnie przypadkowo zmieniasz orientację urządzenia. Gdzie się
podziały moje dane?
126 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

A co się dzieje, kiedy aplikacja na Android traci dane z dużej kontrolki ListView
i zaczyna ponownie pobierać je przez sieć? To kosztowne i energochłonne
rozwiązanie. Oto subtelniejszy przykład — aplikacja zachowuje dane z kontrolki
ListView, jednak lista obejmuje 1000 elementów, a aplikacja gubi pozycję i prze-
nosi użytkownika na początek. Aj.
Na szczęście można zapobiec tego typu problemom oraz zapewnić płynne
i poprawne działanie aplikacji. Wymaga to wiedzy o zarządzaniu stanem egzem-
plarzy w aktywnościach Androida.

3.3.1. Zapisywanie i odtwarzanie stanu egzemplarza


Zagadnienia związane ze stanem egzemplarza mogą być trudne do zrozumienia,
dlatego zaczynamy od wyjaśnienia pojęć. Stan egzemplarza (ang. instance state)
to stan, który aktywności muszą same przywrócić do postaci, w jakiej użytkow-
nik go pozostawił. Obejmuje takie elementy, jak nieprzesłane wartości z formula-
rza, zaznaczone pola, indeks kontrolki ListView itd. Stanem egzemplarza nie są
natomiast utrwalane informacje, na przykład lista opcji formularza, kontakty
lub ustawienia aplikacji. Te elementy tworzą stan trwały. Występują więc dwa
rodzaje stanu:
Q Stan egzemplarza. Dostępny tak długo, jak długo istnieje egzemplarz
klasy Activity.
Q Stan trwały. Trwalszy od aktywności (związany z plikami, ustawieniami,
bazą danych czy siecią).
Mylącym aspektem jest tu to, że w Androidzie egzemplarz nie oznacza tego
samego co w Javie. W Androidzie chodzi o nowy egzemplarz obiektu tego samego
typu i o tej samej zawartości, wyglądający identycznie jak dawny egzemplarz.
Najważniejsze jest to, że stan egzemplarza jest zapisywany wtedy, kiedy to sys-
tem (a nie programista) usuwa aktywność.
Q Stan egzemplarza jest zapisywany, kiedy system usuwa aktywność
(na przykład w wyniku zmiany konfiguracji).
Q Stan egzemplarza NIE jest zapisywany po wywołaniu metody finish
(jest ona wywoływana domyślnie po wciśnięciu przycisku Back).
Po zapoznaniu się z tymi informacjami działanie aktywności, z którymi się zetkną-
łeś lub które debugowałeś, może nabrać sensu. Kwestię stanu poruszono w omó-
wieniu przycisków Back i Home w podrozdziale 3.2. System przy próbie zapi-
sania stanu egzemplarza wywołuje metodę onSaveInstanceState. Uruchamia ją
w momencie wprowadzenia zmian w konfiguracji lub w innych sytuacjach, kiedy
trzeba usunąć aktywność (jeśli wywołanie metody jest możliwe; jeżeli problemy
z pamięcią są poważne, czasem nie da się zapisać stanu egzemplarza). Stan egzem-
plarza jest zachowywany w obiekcie klasy Bundle. Jest to pakiet danych typu
Parcelable (można je przekazywać między procesami). Może obejmować dane
3.3. Kontrolowanie stanu egzemplarza aktywności 127

typów prostych, łańcuchy znaków i tablice elementów tych typów. Możliwe jest
też przekazywanie innych typów Parcelable, jednak omawianie ich wykracza
poza zakres rozdziału.
System zapisuje sensowne informacje domyślne na temat stanu egzemplarza,
można jednak przesłonić metodę onSaveInstanceState i zastąpić ją lub wzbogacić.
Załóżmy, że dla każdego obiektu klasy View system wywołuje metodę View.onSave
´InstanceState. Oznacza to, że zachowywana i automatycznie odtwarzana jest
na przykład zawartość elementów klasy EditText.
Elementy są odtwarzane albo w metodzie onCreate, która przyjmuje jako
dane wejściowe obiekt klasy Bundle, albo w metodzie onRestoreInstanceState.
Do odtwarzania wartości najczęściej stosuje się metodę onCreate, jednak można
też użyć metody onRestoreInstanceState, co pozwala oddzielić odtwarzanie od
inicjowania innych komponentów.
Aby pokazać, jak przebiega tworzenie, zapisywanie, usuwanie i odtwarzanie
stanu, wróćmy do przykładowej aplikacji i wypróbujmy kilka rzeczy. Najpierw
omawiamy kod klasy Activity3, w której używany jest stan egzemplarza. Kod ten
znajduje się na listingu 3.3.

Listing 3.3. Klasa z pliku Activity3.java zapisuje i odtwarza stan

public class Activity3 extends LifecycleActivity {


private static final String COUNT_KEY = "cKey";
private TextView numResumes;
private int count;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity3);
numResumes = (TextView) findViewById(R.id.numResumes);
}

@Override
protected void onResume() {
super.onResume();
numResumes.setText(String.valueOf(count));
count++;
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if ((savedInstanceState != null) &&
savedInstanceState.containsKey(COUNT_KEY)) {
count = savedInstanceState.getInt(COUNT_KEY);
}
super.onRestoreInstanceState(savedInstanceState);
}

@Override
128 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

protected void onSaveInstanceState(Bundle outState) {


outState.putInt(COUNT_KEY, count);
super.onSaveInstanceState(outState);
}
}

Pierwszą ciekawą rzeczą w przedstawionej na listingu 3.3 klasie Activity3 jest


zmienna egzemplarza count . W metodzie onResume służy ona do ustawienia
wartości kontrolki TextView na wartość licznika . Ponieważ cała aplikacja śledzi
cykl życia, ten przykładowy kod pozwala ustalić, ile razy wznowiono daną
aktywność.
Aby zachować zmienną egzemplarza także przy usuwaniu i ponownym two-
rzeniu egzemplarza klasy Javy, trzeba zaimplementować metody obsługi stanu
egzemplarza. Metoda onSaveInstanceState służy do zapisywania wartości liczni-
ka w tworzonym obiekcie klasy Bundle , a metoda onRestoreInstanceState —
do pobierania tego obiektu i przywracania dawnej wartości licznika. Jest to
oczywiście prosty przykład, jednak bez opisanego kodu licznik zawsze pokazy-
wałby 0 (system by go zerował przy usuwaniu aktywności i nie aktualizowałby go
w czasie wstrzymywania lub wznawiania pracy aktywności). Aby zademonstrować
działanie kodu, należy uruchomić aktywność Activity3, a następnie obrócić
ekran (powoduje to zmianę konfiguracji, co prowadzi do usunięcia i odtworzenia
aktywności). Efekt pokazano na rysunku 3.9.

Rysunek 3.9. Na ekranie aktywności Activity3 widać, że wartość licznika


egzemplarza została zachowana. Na liście powiadomień widoczne są wywołania
metod cyklu życia i związanych ze stanem

Na rysunku 3.9 widać, że aktywność została usunięta, a następnie odtworzona


po zmianie orientacji. System zachowuje jednak poprzednią wartość tekstową
ustawioną automatycznie w kontrolce EditText, a także śledzi wcześniejszy stan
3.3. Kontrolowanie stanu egzemplarza aktywności 129

wewnętrznego licznika. Dzięki zastosowaniu metod onSaveInstanceState i onRe


´storeInstanceState zmiana przebiegła płynnie, a użytkownik nie utracił żad-
nych informacji ani nie musiał ponownie wprowadzać żadnych wartości.
Przed zakończeniem omawiania stanu egzemplarza warto wspomnieć o jesz-
cze jednym specjalnym typie tego stanu, który może okazać się przydatny i daje
jeszcze większe możliwości. Jest to stan egzemplarza niezwiązany z konfiguracją
(nazwa ta jest nieco myląca).

3.3.2. Korzystanie ze stanu egzemplarza


niezwiązanego z konfiguracją
Stan egzemplarza niezwiązany z konfiguracją to dowolne rozbudowane dane
o stanie, które trzeba przekazać z bieżącego egzemplarza aktywności do jego
przyszłego odpowiednika, tworzonego w wyniku wprowadzenia zmian w konfi-
guracji. Ta specjalna optymalizacja Androida bywa niezwykle przydatna. Trzeba
jednak pamiętać o pewnym zastrzeżeniu — technika ta dotyczy tylko bieżącego
egzemplarza oraz egzemplarza odtwarzanego natychmiast po usunięciu pier-
wotnego.
Jak działa ten mechanizm i jakie dane pozwala przekazywać? Aby odpowie-
dzieć na te pytania, na listingu 3.4 do klasy Activity3 z aplikacji LifecycleExplorer
dodano potrzebny kod.

Listing 3.4. Dodatkowy kod klasy aktywności służący do obsługi stanu egzemplarza
niezwiązanego z konfiguracją

. . .
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity3);
numResumes = (TextView) findViewById(R.id.numResumes);

Date date = (Date) this.getLastNonConfigurationInstance();


if (date != null) {
Toast.makeText(this, "Obiekt \"LastNonConfiguration\": "
+ date, Toast.LENGTH_LONG).show();
}
}

. . .
@Override
public Object onRetainNonConfigurationInstance() {
return new Date();
}
}

W zmodyfikowanej metodzie onCreate z klasy Activity3 pobierany jest obiekt


klasy Object z metody getLastNonConfigurationInstance . Tu obiekt jest rzuto-
wany na typ Date, a następnie wyświetlany za pomocą klasy Toast , przy czym
130 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

warto wiedzieć, że obiekt można wykorzystać w dowolny sposób. Na tym etapie


nie jest obiektem typu prostego ani specjalnego typu Parcelable z Androida —
to zwykły obiekt klasy Object. Można zrzutować go na rysunek, typ Thread, typ
Map lub własny typ ziarna, który obejmuje obiekty innych typów. Masz pod tym
względem dużą dowolność.
Wspomniana dowolność jest jednak ograniczona do typów bezpośrednio
dostępnych w metodzie getLastNonConfigurationInstance przez zapisanie ich
w metodzie onRetainNonConfigurationInstance. Tu przy przesłanianiu tej ostatniej
metody zwrócono obiekt klasy Date . Android odwzorowuje dane z bieżącego
egzemplarza aktywności na następny, natychmiast odtwarzany egzemplarz tej
samej klasy aktywności.

Poważne ostrzeżenie
Choć w stanie egzemplarza niezwiązanym z konfiguracją możesz umieścić obiekt
dowolnego typu, musisz uważać, aby nie zapisywać obiektów (na przykład widoku,
całego adaptera itd.) z mocnymi referencjami do usuwanej aktywności. Wtedy
nie można w całości usunąć aktywności i następuje wyciekanie pamięci. W takiej
sytuacji aplikacja tworzy egzemplarze, których nie da się usunąć.

Po dodaniu kodu z listingu 3.4 można ponownie uruchomić aplikację Lifecycle-


Explorer, przejść do aktywności Activity3 i obrócić ekran. Wywoła to zmianę
konfiguracji. Zobaczysz, że obiekt klasy Date jest przekazywany jako dane niezwią-
zane z konfiguracją, co pokazano na rysunku 3.10.

Rysunek 3.10. Przekazywanie


stanu niezwiązanego
z konfiguracją z bieżącej
aktywności do natychmiast
utworzonego egzemplarza
tej samej klasy. Zapewnia
to optymalizację stanu

Bardzo wartościowymi cechami stanu egzemplarza niezwiązanego z konfigura-


cją są elastyczność i szybkość. Trzeba jednak pamiętać o kilku zastrzeżeniach.
Otóż technika ta działa tylko dla bieżącego i natychmiast odtwarzanego egzem-
plarza, dlatego nie można jej używać bez zastanowienia. Jest to optymalizacja,
ale nie można w pełni polegać na jej działaniu. Ponadto trzeba uważać, aby nie
przekazywać danych w rodzaju łańcuchów znaków, elementów graficznych lub
innych zasobów, które mogą ulec zmianie w wyniku modyfikacji konfiguracji.
W końcu omawiana technika dotyczy stanu niezwiązanego z konfiguracją.
3.4. Wykonywanie operacji za pomocą zadań 131

Stan niezwiązany z konfiguracją oraz zwykły stan egzemplarza to ważne narzę-


dzia do zapewniania płynnego i prawie natychmiastowego działania androido-
wych aktywności. Teraz w ramach uzupełnienia przeglądu cyklu życia omawiamy
zadania, czyli grupy aktywności pochodzących z jednej lub kilku aplikacji.

3.4. Wykonywanie operacji za pomocą zadań


W Androidzie z procesami, aplikacjami i aktywnościami związany jest jeszcze
jeden mechanizm — zadania. Zadania nie są tworzone jako obiekty Javy i nie
definiuje się ich w manifeście. Jest to mechanizm frameworku służący do gru-
powania aktywności. Grupy są ważne, ponieważ są ściśle powiązane ze stosem
aktywności i wpływają na to, w jaki sposób użytkownicy poruszają się po grupach
komponentów.

3.4.1. Definiowanie zadań


Omówiono już techniczną definicję grupy komponentów z jednego podstawo-
wego pakietu spakowanego do pliku APK. Jest to aplikacja na Android. Jednak,
jak wcześniej wspomniano, użytkownik nie postrzega aplikacji w ten sposób.
Dla użytkownika aplikacja składa się ze wszystkich aktywności potrzebnych do
wykonania operacji. W Androidzie takie grupy aktywności to zadania.
Zadanie jest zawsze uruchamiane przez jedną aktywność — tak zwaną aktyw-
ność główną. Aktywność główna jest zwykle uruchamiana z poziomu ekranu głów-
nego (aplikacji Launcher). Następnie każda aktywność związana z zadaniem jest
dodawana do stosu aktywności zadania, a całe zadanie system traktuje jak odrębną
jednostkę, co pokazano na rysunku 3.11.

Rysunek 3.11. Schemat


zadania i stosu aktywności
w jego ramach
132 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

Inny sposób na zobaczenie, w jaki sposób aktywności są grupowane w zadania


(i podejrzenie zadań uruchomionych w urządzeniu), polega na długim przyci-
śnięciu przycisku Home. Powoduje to wyświetlenie ekranu niedawnych zadań,
na którym widnieje odpowiadająca aktywności głównej ikona i nazwa każdego
zadania (rysunek 3.12).

Rysunek 3.12. Ekran


niedawnych zadań
wyświetla zadania,
które użytkownik może
ponownie uruchomić,
wybierając ikonę
aktywności głównej

Oprócz aktywności głównej na stosie aktywności danego zadania domyślnie


umieszczane są wszelkie inne aktywności związane z aplikacją, wywołane za
pomocą intencji. Stos aktywności opisano w podrozdziale 3.2, gdzie pokazano,
w jaki sposób aktywności można umieszczać na stosie i zdejmować je z niego.
Tu wracamy do tego zagadnienia, ponieważ w systemie znajduje się wiele stosów
aktywności (po jednym na każde zadanie).

3.4.2. Tworzenie stosu aktywności w zadaniu


Zadanie to grupa powiązanych aktywności. W ramach tej grupy aktywności są
umieszczane na stosie, po którym użytkownik może się poruszać. Użytkownik
może umieszczać aktywności na stosie przez ich uruchomienie (użycie aplikacji),
a także zdejmować je, wybierając przycisk Back. Kiedy użytkownik wybiera
jedno zadanie, stos wyświetla tylko aktywności związane z tym zadaniem, a nie
duży zestaw wszystkich możliwych aktywności. W czasie cofania się nie można
przeskakiwać między różnymi zadaniami. Byłoby to mylące. Nawigacja odbywa
się w ramach zadań.
Mechanizm współdziałania części wielu aplikacji daje niezwykle duże moż-
liwości. Grupowanie potrzebnych do realizacji celu różnych części wielu apli-
kacji w zadanie pozwala łatwo zarządzać tymi częściami, a także kontrolować
poruszanie się po stosie aktywności.
3.5. Podsumowanie 133

3.4.3. Pokrewieństwo na stosie aktywności


Zadania są mechanizmem ważnym dla użytkowników, a ponadto są wygodne
dla programistów. Nie trzeba ponownie pisać aktywności do wysyłania e-maili
lub robienia zdjęć. Można zastosować intencje i wykorzystać wbudowane apli-
kacje. Potrzebne aktywności są wtedy dołączane do wywołujących je aplikacji jako
części zadań. Aktywności wywoływane przez aplikację łączy pokrewieństwo
z zadaniem.
Zwykle nie trzeba przejmować się kontrolowaniem takiego pokrewieństwa.
Jeśli aplikacja jest uruchamiana z wykorzystaniem programu Launcher, pod-
stawowa aktywność rozpoczyna nowe zadanie (oraz jest wykonywana w nowym
procesie o określonym identyfikatorze użytkownika) i pełni funkcję aktywności
głównej. Większość pozostałych aktywności używanych w aplikacji jest wiązana
z zadaniem i automatycznie cechuje się pokrewieństwem z nim.
Pokrewieństwo ma znaczenie, kiedy potrzebujesz precyzyjniejszej kontroli.
Android także wtedy wiąże zadania z aktywnościami, jednak umożliwia też
ręczną zmianę ustawień. Możesz bezpośrednio określić pokrewieństwo dla zada-
nia, zmienić tryb uruchamiania aktywności (sposób ich powiązania z zadaniem,
kiedy są uruchamiane za pomocą intencji), kontrolować, jak i kiedy stos aktyw-
ności w zadaniu jest zachowywany oraz opróżniany itd. Dokładne omówienie
tych zaawansowanych ustawień znajdziesz w aktualnej dokumentacji.
Zadania są ważne, ponieważ stanowią ostatni krok w łączeniu intencji, apli-
kacji i aktywności, a także w logiczny sposób grupują elementy na potrzeby
nawigacji. Zadania są też ostatnim punktem omówienia cyklu życia i stanu apli-
kacji na Android.

3.5. Podsumowanie
Gratulacje! Przeczytałeś pierwszą część książki Android w praktyce i masz już
solidne podstawy do programowania aplikacji na Android.
W tym rozdziale skoncentrowano się na tym, czym są aplikacje na Android
i jak wygląda cykl życia aktywności. Aktywności są podstawowym komponentem
każdej aplikacji na Android, a tworzenie i usuwanie elementów w ramach cyklu
życia aktywności ma bardzo duże znaczenie. Oprócz znajomości cyklu życia nie-
zwykle istotna jest też wiedza o tym, jak zapisywać i odtwarzać stan egzemplarzy
aktywności. Od tego może zależeć komfort pracy użytkowników. Zarządzanie
cyklem życia w środowisku, które nie gwarantuje działania aplikacji do czasu jej
zamknięcia, a zamiast tego usuwa i tworzy komponenty na żądanie, bywa skom-
plikowane.
Inną ważną kwestią opisaną w rozdziale jest grupowanie przez Android aktyw-
ności według celów użytkownika (niezależnie od aplikacji, z których te aktywno-
ści pochodzą). System traktuje takie grupy jak odrębne zadania. Są one istotne,
134 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem

ponieważ stanowią logiczne punkty nawigacyjne dla użytkowników i dzięki sto-


sowi aktywności umożliwiają powrót do punktu wyjścia.
Opisane zagadnienia — aplikacje, aktywności, zadania, procesy i zachowywa-
nie stanu — to ostatni etap wprowadzenia do Androida. Rozdział ten kończy
część pierwszą książki i jest ostatnim fragmentem podstawowych informacji
potrzebnych do przejścia do bardziej zaawansowanych, praktycznych przykładów
z części drugiej i trzeciej.
Część II
Praktyczne rozwiązania

W drugiej części książki Android w praktyce wychodzimy poza pod-


stawy i tworzymy wiele kompletnych przykładowych aplikacji, w których szcze-
gółowo przedstawiamy najczęściej stosowane mechanizmy i wymagania. Zaczy-
namy od omówienia interfejsu użytkownika w rozdziale 4. Opisujemy tam zasoby
i widoki, a także dodatkowe zagadnienia, takie jak stosowanie stylów i moty-
wów oraz obsługa ekranów o różnej wielkości. W rozdziale 5. pokazujemy, jak
w skuteczny sposób realizować wielozadaniowość w Androidzie z wykorzysta-
niem usług działających w tle. Rozdział 6. to kontynuacja tego tematu. Znaj-
dziesz tu omówienie wątków i współbieżności, w tym pracy z wątkami, kompo-
nentami obsługi zdarzeń, zadaniami asynchronicznymi itd. W rozdziale 7.
następuje zmiana tematu na lokalne przechowywanie danych. Tu dowiesz się,
jak używać systemu plików, pamięci wewnętrznej i zewnętrznej, współużytkowa-
nych ustawień i bazy danych. Rozdział 8. dotyczy współdzielenia danych mię-
dzy aplikacjami za pomocą dostawców treści. Nauczysz się korzystać z danych
z innych aplikacji, a także tworzyć własnych dostawców i udostępniać dane innym
jednostkom. W rozdziale 9. wykraczamy z danymi poza lokalne urządzenie i zaj-
mujemy się siecią. Dowiesz się, jak radzić sobie z charakterystyczną dla połą-
czeń mobilnych niestabilnością, a także jak korzystać z protokołu HTTP i usług
sieciowych z wykorzystaniem formatów JSON oraz XML. W rozdziale 10. prze-
chodzimy do usług opartych na miejscu pobytu i korzystamy z dostawców określa-
jących takie miejsce. Tu nauczysz się, jak wykrywać dostępnych dostawców i jak
przełączać się między nimi. Zobaczysz też, jak pracować z danymi i aktywno-
ściami opartymi na mapach. W rozdziale 11. przedstawiamy multimedia. Zoba-
czysz, jak pracować z plikami audio i wideo, a także dowiesz się czegoś o plikach,
zasobach i animacjach. W rozdziale 12. rozwijamy omówienie animacji i elemen-
tów wizualnych, aby przedstawić rysowanie w dwóch i trzech wymiarach (w tym
pracę z płótnem) oraz korzystanie z biblioteki OpenGL.
Precyzja co do piksela

W tym rozdziale
Q Wyświetlanie widoków
Q Tworzenie układów
Q Praca z motywami i stylami
Q Tworzenie interfejsów dla aplikacji mobilnych

Nie znam odpowiedzi. Zajmuję się tylko oczami. Jesteś Nexusem,


prawda? Zaprojektowałem twoje oczy.
Blade Runner
W tym rozdziale omawiamy różne komponenty wizualne. Pokazujemy, jak
powstaje hierarchia widoków i jak w kilku etapach wyświetlać je na ekranie.
Dokładniej opisujemy też dostępne w Androidzie menedżery układów i stoso-
wanie parametrów układów. Następnie wyjaśniamy, jak używać motywów i sty-
lów do dostosowania aplikacji do potrzeb programisty, jak wyświetlać niestan-
dardowe przyciski i inne elementy okna, a także jak zapewnić skalowanie
interfejsów użytkownika do różnych urządzeń. W ostatniej części dowiesz się naj-
ważniejszego — jak radzić sobie z typowymi problemami, które pojawiają się
we wszystkich wymienionych obszarach. Jest to jeden z najdłuższych rozdziałów
książki, jednak nie martw się! Obejmuje on jedne z najbardziej podstawowych
i najczęściej stosowanych rozwiązań, dlatego znajdziesz tu wiele informacji, które
ułatwią Ci pisanie aplikacji na Android.

137
138 ROZDZIAŁ 4. Precyzja co do piksela

4.1. Aplikacja MyMovies


Na potrzeby tego rozdziału tworzymy nową przykładową aplikację — MyMovies.
Aplikacja DealDroid z rozdziału 2. dobrze nadaje się do przedstawienia wielu
podstawowych elementów Androida, jednak trzeba szczerze przyznać, że nie
jest szczególnie atrakcyjna. Użytkownicy smartfonów to ludzie, a nie androidy;
ludzie są wzrokowcami i uwielbiają eleganckie aplikacje! Dlatego tym razem
w większym stopniu koncentrujemy się na prezentacji, a w mniejszym — na
funkcjach.
POBIERZ PROJEKT MYMOVIES. Kod źródłowy projektu
i spakowany plik APK do uruchomienia aplikacji znajdziesz na
witrynie książki Android w praktyce. Ponieważ niektóre listingi
skrócono, abyś mógł skoncentrować się na szczegółowych
zagadnieniach, zachęcamy do pobrania kompletnego kodu
źródłowego i śledzenia go w Eclipse (albo innym środowisku
IDE lub edytorze tekstu).
Kod źródłowy: http://mng.bz/7JxQ, plik APK: http://mng.bz/26DZ.
Zadanie polega na napisaniu prostej aplikacji do zarządzania osobistą kolekcją
filmów. Aplikacja ma wyświetlać użytkownikowi listę tytułów filmów. Użytkow-
nik przez kliknięcie tytułu może określić, czy ma, czy nie ma go w kolekcji. Jak
już wspomnieliśmy, ta wersja aplikacji jest prosta. W dalszych rozdziałach zwięk-
szamy przydatność aplikacji przez wzbogacenie opisanego tu zbioru funkcji. Na
podstawie tego przykładowego programu dowiesz się, jak tworzyć niestandardowe
interfejsy użytkownika, które nie tylko poprawnie działają i dobrze się skalują,
ale też atrakcyjnie wyglądają. Aby zaostrzyć Twój apetyt, na rysunku 4.1 pokazano
zrzut aplikacji, którą tworzymy w tym rozdziale.
Jak widać, większość ekranu zajmuje lista filmów znanych aplikacji. Efekt ten
można uzyskać za pomocą kontrolki ListView (przedstawionej w rozdziale 2.),
którą zmodyfikowaliśmy przez zastosowanie półprzezroczystego tła oraz selektora
elementów listy z gradientem i zaokrąglonymi rogami. Selektor ten zmienia kolor
w momencie kliknięcia. Dodaliśmy też rysunek tła i grafikę z tytułem, która
automatycznie zmienia wielkość na podstawie szerokości i orientacji ekranu.
Wymienione zmiany zachodzą nie tylko w tej konkretnej aplikacji i nie są ogra-
niczone tylko do niej. Wszystko, czego nauczysz się w tym rozdziale, możesz
zastosować we własnych aplikacjach. Należy jednak zacząć od podstaw i odpo-
wiedzieć na pytanie, co dzieje się „pod maską”, kiedy Android wyświetla interfejs
użytkownika. Dlatego przed przedstawieniem kodu aplikacji MyMovies szcze-
gółowo omawiamy wyświetlanie widoków, układy i menedżery układów.
4.2. Hierarchie widoków i ich wyświetlanie 139

4.2. Hierarchie widoków i ich wyświetlanie


Wyświetlanie widoków to ważny aspekt każdej
aplikacji z interfejsem użytkownika. Wszyscy
lubią eleganckie aplikacje, jednak wyświetlanie
różnych elementów interfejsu zajmuje progra-
mom dużo czasu. Dlatego aby uniknąć proble-
mów z wydajnością, należy zrozumieć, co dzieje
się „pod maską”. Nawet piękne aplikacje
powinny działać wydajnie. Choć już wcześniej
przedstawiliśmy widoki i ich używaliśmy, teraz
omawiamy je dokładniej. Wyjaśniamy, jak są upo-
rządkowane, jak system je wyświetla i na jakie
rzeczy należy zwrócić uwagę, aby interfejs użyt-
kownika działał sprawnie.

4.2.1. Hierarchie widoków


Rysunek 4.1. Ekran tytułowy
Widoki w Androidzie są zwykle definiowane aplikacji MyMovies. Zwróć
deklaratywnie w formacie XML. W formacie tym uwagę na to, że interfejs
użytkownika jest
informacje mają strukturę drzewiastą. Wszystkie niestandardowy i obejmuje
węzły rozwijają się i rozgałęziają z jednego węzła mechanizmy w rodzaju
półprzezroczystego selektora
głównego (korzenia). Popularność XML-a to nie- elementów listy
jedyny powód zastosowania takiej reprezentacji
w Androidzie. Wewnętrznie interfejs użyt-
kownika każdej aplikacji na Android jest reprezentowany jako drzewo obiektów
klasy View. Jest to tak zwana hierarchia widoków lub drzewo widoków. Korzeń
każdego drzewa widoków (i interfejsu użytkownika każdej aplikacji) to jeden
obiekt klasy DecorView. Jest to wewnętrzna klasa frameworku, której nie można
bezpośrednio używać. Reprezentuje ona bieżące widoczne okno urządzenia.
Węzeł DecorView obejmuje jeden układ LinearView, który rozgałęzia się na dwa
układy FrameLayout — jeden na obszar tytułowy aktualnie widocznej aktywno-
ści i drugi na jej zawartość (układy FrameLayout zajmują określony obszar ekra-
nu, aby wyświetlić jeden element). Zawartość oznacza tu dowolne elementy
zdefiniowane w XML-owym układzie właśnie używanej aktywności. Aby zro-
zumieć działanie tego mechanizmu, warto przyjrzeć się XML-owemu układowi
głównego ekranu aplikacji MyMovies (res/layout/main.xml).
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
140 ROZDZIAŁ 4. Precyzja co do piksela

android:layout_height="fill_parent"
/>

</LinearLayout>

Aby zrozumieć, w jaki sposób hierarchia elementów widoku funkcjonuje dla


przedstawionego ekranu, warto użyć dostępnego w pakiecie SDK narzędzia
hierarchyviewer. Można je uruchomić albo z wiersza poleceń, albo (jeśli uży-
wasz najnowszej wersji wtyczki ADT) za pomocą perspektywy Hierarchy View
z Eclipse. Każda z tych technik powoduje połączenie się z egzemplarzem dzia-
łającego emulatora lub podłączonym urządzeniem. Następnie zobaczysz liczne
opcje związane z układami. Na rysunku 4.2 przedstawiono hierarchię widoków
dla pokazanego wcześniej układu głównego.

Rysunek 4.2. Hierarchia widoków dla głównego układu aplikacji MyMovies


wyświetlona w narzędziu hierarchyviewer. Lewa gałąź reprezentuje pasek
tytułowy okna, a prawa — zawartość właśnie używanej aktywności

Ciemne pole widoczne pośrodku rysunku 4.2 to układ LinearLayout, który wystę-
puje na początku pliku XML. W hierarchii widać, że w układzie LinearLayout
znajduje się nadrzędny układ FrameLayout (o identyfikatorze zasobu android.R.id.
´content), przeznaczony na węzeł z zawartością. W ten sposób Android repre-
zentuje obszar ekranu obejmujący zawartość. Obszar ten zajmuje większą część
interfejsu użytkownika aplikacji. Zazwyczaj dla programisty ważna jest tylko ta
gałąź — elementy związane z węzłem zawartości. Widoczny (po lewej) jest także
siostrzany układ FrameLayout, przeznaczony na węzeł z tytułem. Układ ten tworzy
pasek tytułowy okna. Pod głównym węzłem LinearLayout aplikacji MyMovies
4.2. Hierarchie widoków i ich wyświetlanie 141

znajduje się kontrolka ListView z listingu 4.1. Dla każdego elementu tej listy
istnieje potomny układ LinearLayout.
Przy każdym uruchomieniu aktywności drzewo widoków układu jest wstawiane
do drzewa widoków aplikacji przez wywołanie metody setContentView(int lay
´outId) aktywności. Prowadzi to do zastąpienia elementów znajdujących się
poniżej obecnego węzła zawartości drzewem widoków wskazanym za pomocą
identyfikatora layoutId, któremu odpowiada układ zdefiniowany w pliku XML.
Proces wczytywania układu i scalania go z obecnym drzewem widoków to rozwi-
janie układu do klasy (ang. layout inflation). Operację tę wykonuje klasa Layout
´Inflater, której działanie przypomina rozrastanie się drzewa. Od czasu do
czasu wyrasta nowa gałąź, z niej następna itd. Układy nie są bezpośrednio roz-
wijane na podstawie plików XML, ponieważ Android przed kompilacją prze-
kształca dane z tych plików na wydajny format binarny (więcej o procesie budo-
wania aplikacji w Androidzie dowiesz się z rozdziału 14.).
Widok po rozwinięciu do klasy jest uwzględniany przy wyświetlaniu okien.
Oznacza to, że może zostać pokazany na ekranie, o ile nie zasłania go inny widok.
Android wyświetla widoki z zastosowaniem dwuprzebiegowego algorytmu, który
pokrótce omawiamy w następnym punkcie.

4.2.2. Wyświetlanie widoków


Po umieszczeniu drzewa widoków w pamięci Android musi je wyświetlić. Każdy
widok odpowiada za wyświetlanie samego siebie, jednak rozkład elementów
i pozycję widoku na ekranie można ustalić tylko w kontekście całego drzewa.
Jest tak, ponieważ pozycja każdego widoku wpływa na położenie następnego.
Aby ustalić, gdzie należy wyświetlić widok i jak duży powinien on być, Android
musi wyświetlać elementy w dwóch odrębnych przebiegach — przebiegu pomia-
rowym i przebiegu rozmieszczania.
PRZEBIEG POMIAROWY
W przebiegu pomiarowym każdy widok nadrzędny musi określić, jak duże
powinny być jego widoki potomne. W tym celu należy wywołać metodę measure
tych ostatnich. Proces ten obejmuje przekazywanie w dół drzewa obiektu ze spe-
cyfikacją pomiarów. Obiekt ten obejmuje informacje o ograniczeniu wielkości
narzucane przez widok nadrzędny potomnemu. Każdy element potomny musi
następnie określić, ile miejsca zajmuje (z uwzględnieniem ograniczeń).
PRZEBIEG ROZMIESZCZANIA
Po dokonaniu pomiaru wszystkich widoków następuje przebieg rozmieszczania.
Tym razem każdy widok nadrzędny musi rozmieścić na ekranie wszystkie widoki
podrzędne (na podstawie wartości uzyskanych na etapie pomiarów). Odbywa się
to przez wywołanie metody layout. Proces ten przedstawiono na rysunku 4.3.
142 ROZDZIAŁ 4. Precyzja co do piksela

Rysunek 4.3. Widoki są wyświetlane w ramach dwuprzebiegowego przejścia


po drzewie widoków. Pierwszy przebieg (po lewej) polega na zbieraniu wymiarów;
drugi (po prawej) powoduje rozmieszczenie elementów na ekranie

Rozmieszczanie i pomiary widoków odbywają się w sposób niezauważalny dla


programisty, chyba że implementuje on własną klasę View. Wtedy powinien prze-
słonić metody onMeasure i onLayout, co pozwala aktywnie zarządzać przebiegami.
Omawiany proces jest złożony, co oznacza, że wyświetlanie widoków to kosz-
towna operacja, zwłaszcza jeśli jest ich wiele, a drzewo widoków rozrosło się do
dużych rozmiarów. Niestety, w Androidzie aplikacja przeznacza na wyświetlanie
elementów dużo czasu. Widoki nieustannie stają się nieaktualne i są ponownie
wyświetlane — albo z uwagi na przesłonięcie ich przez inne widoki, albo w wyniku
zmiany stanu. Nie można zbyt wiele z tym zrobić. Można jednak pamiętać o kosz-
tach przy pisaniu własnego kodu i starać się ograniczać niepotrzebne operacje
wyświetlania. W tabeli 4.1 wymieniono najlepsze praktyki z obszaru optymali-
zacji wydajności pracy z widokami.
W czasie korzystania z widoków i układów zawsze należy pamiętać o wydajno-
ści widoku. Jeśli masz już pewne doświadczenie w korzystaniu z Androida, nie-
które kwestie powinny być dla Ciebie oczywiste. Nie wspominalibyśmy o nich
jednak, gdybyśmy regularnie nie natrafiali na aplikacje, w których naruszono
podane reguły. Zawsze warto dokładnie sprawdzić układ pod kątem słabości
strukturalnych. Służy do tego narzędzie layoutopt z pakietu SDK. Nie powi-
nieneś, oczywiście, polegać tylko na nim, jednak layoutopt dobrze radzi sobie
z wykrywaniem problemów w układach.
W aplikacjach mobilnych najważniejsza jest interakcja z użytkownikami.
Aplikacja przeważnie przez większość czasu wyświetla różne elementy interfejsu
i reaguje na dane wejściowe wprowadzone przez użytkownika. Dlatego ważne
jest, aby dobrze wiedzieć, co steruje interfejsem użytkownika. Teraz, kiedy już
wiesz, w jaki sposób widoki i układy są porządkowane w pamięci oraz jakich
algorytmów Android używa do pomiaru i rozmieszczania widoków przed ich
wyświetleniem na ekranie, pora przejść do szczegółowego omówienia samych
układów.
4.3. Porządkowanie widoków w układy 143

Tabela 4.1. Najlepsze praktyki korzystania z widoków

Rada Uzasadnienie
Pamiętaj, Jeśli obiekt klasy View domyślnie jest ukryty i pojawia się tylko
że „najtańszy” widok w odpowiedzi na zdarzenia w interfejsie użytkownika (na przykład
to ten, który nigdy dotknięcie lub kliknięcie), można w zamian zastosować klasę ViewStub
nie jest wyświetlany. (widok wypełniacz). Można też dynamicznie dodawać i usuwać widok
z łańcucha wyświetlanych elementów, ustawiając jego widoczność
na View.GONE.
Unikaj stosowania Z uwagi na treść poprzedniej rady dobrze zastanów się nad używaniem
zbyt wielu widoków. dodatkowych widoków i w miarę możliwości upraszczaj interfejs
użytkownika. Pozwala to zachować przejrzystość układów ekranu
i poprawić wydajność.
Staraj się wielokrotnie Często można uniknąć dodatkowego rozwijania i wyświetlania
używać widoków. elementów przez zapisywanie widoków w pamięci podręcznej oraz ich
ponowne wykorzystywanie. Jest to ważne przy wyświetlaniu list, kiedy
to jednocześnie pokazywanych jest wiele elementów, a zmiany stanu
są częste (na przykład przy przewijaniu listy). Pomóc może wzorzec
oparty na elementach convertView i ViewHolder, opisany w technice
1. w tym rozdziale.
Unikaj nadmiernego Niektórzy programiści używają zagnieżdżonych układów LinearLayout
zagnieżdżania do rozmieszczania elementów względem siebie. Nie rób tak. Ten sam
układów. efekt można zwykle osiągnąć, stosując pojedynczy układ RelativeLayout
lub TableLayout.
Unikaj powielania. Jeśli zauważysz, że kopiujesz definicje widoków w celu kilkukrotnego
użycia ich w jednym układzie, rozważ zastosowanie w zamian znacznika
<include>. Także zagnieżdżanie układów tego samego typu jest zwykle
zbędnym powielaniem kodu. Można tego uniknąć, stosując znacznik
<merge>.

4.3. Porządkowanie widoków w układy


Kiedy implementujesz interfejs użytkownika dla aktywności, używasz jej układu.
Jak już wspomnieliśmy, układy służą do porządkowania widoków na ekranie.
Układ jest jak schemat ekranu. Określa, jakie elementy znajdują się na ekranie,
jak są uporządkowane, jak wyglądają itd. Dlatego przy implementowaniu ekranu
aplikacji najpierw należy pomyśleć o układzie. Jeśli współpracujesz z projek-
tantami, niezbędna jest znajomość menedżerów układu. Prawdopodobnie otrzy-
masz szablony lub schematy każdego ekranu i powinieneś wiedzieć, jak odwzo-
rować projekt na androidowe menedżery układu.

Układ a menedżer układu


Kiedy mówimy o układzie, mamy na myśli zestaw wszystkich widoków jednej
aktywności uporządkowanych za pomocą pliku XML układu. Plik ten znajduje się
w katalogu res/layout. Nie należy mylić takich układów z konkretnymi klasami
układu, tak zwanymi menedżerami układu. Układ aktywności (w zależności od
stopnia skomplikowania) może obejmować wiele menedżerów układu. Jak już
wspomnieliśmy, menedżer układu to następny obiekt klasy View (a dokładniej —
klasy ViewGroup). Pełni funkcję kontenera i porządkuje widoki w określony sposób.
144 ROZDZIAŁ 4. Precyzja co do piksela

W następnym punkcie szczegółowo przedstawiamy ogólną strukturę układu


i kompletne omówienie dostępnych w Androidzie menedżerów układów.

4.3.1. Struktura układu


Do tej pory zetknąłeś się już z kilkoma układami, na przykład układem aplika-
cji HelloAndroid z rozdziału 1. i układem DealList z rozdziału 2. Omówiliśmy
podstawy tych układów, ale nie określiliśmy dokładnie, które elementy można
umieszczać w plikach układów i jaka jest ogólna budowa tych komponentów. Nie
opisaliśmy też parametrów ani atrybutów obsługiwanych w układach. Zagadnie-
nia te omawiamy w niniejszym punkcie.
BUDOWA PLIKÓW UKŁADU
Każdy plik układu zaczyna się od XML-owego nagłówka, w którym można
zdefiniować kodowanie pliku (zwykle jest to UTF-8). Układ, podobnie jak każdy
inny dokument XML, składa się z jednego węzła głównego, który ma zero lub
więcej dzieci (elementów podrzędnych). Liczba dzieci zależy od tego, czy węzeł
główny to ViewGroup, tak jak we wszystkich menedżerach układów, czy prosty
obiekt klasy View, który musi być jedynym widokiem zdefiniowanym w ukła-
dzie. Nazwy węzłów odpowiadają nazwom klas, a w układzie możesz umieścić
dowolną klasę konkretną pochodną od klasy android.view.View. Domyślnie nazwy
klas są wyszukiwane w pakietach android.view (klasy SurfaceView, ViewStub itd.)
oraz android.widget (TextView, ListView, Button itd.). Dla innych widoków, niebę-
dących częścią frameworku (na przykład samodzielnie zdefiniowanych), trzeba
podać w pełni kwalifikowaną nazwę klasy, taką jak com.myapp.MyShinyView. Jest
to ważne zwłaszcza wtedy, kiedy chcesz osadzić w aplikacji widok MapView z mapą
Google’a (więcej o miejscach i widoku MapView dowiesz się z rozdziału 10.). Klasa
MapView obejmuje zastrzeżony kod firmy Google i nie jest rozpowszechniana wraz
z podstawowym frameworkiem. Dlatego klasę tę trzeba wskazać za pomocą
w pełni kwalifikowanej nazwy.
<com.google.android.maps.MapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="Klucz do interfejsu API map"
/>

ATRYBUTY I PARAMETRY UKŁADU


Każdy układ w widoku może mieć atrybuty dwóch rodzajów — właściwe dla
klasy widoku i klas bazowych oraz właściwe dla menedżera układu, w którym
dany widok się znajduje. Atrybuty dostępne dla widoku można znaleźć w jego
dokumentacji (lub z wykorzystaniem mechanizmu autouzupełniania ze środowiska
Eclipse). Dla widoku TextView dostępny jest na przykład atrybut android:text,
4.3. Porządkowanie widoków w układy 145

który umożliwia zdefiniowanie domyślnej wartości tekstu. We wspomnianym


widoku można też zastosować atrybut android:padding, odziedziczony po klasie
bazowej View Androida.
ATRYBUTY DOSTĘPNE DLA WIDOKU. Wszystkie dostępne w Andro-
idzie atrybuty widoków można znaleźć w jednym miejscu — w doku-
mentacji klasy android.R.attr.
Parametry układu wyglądają inaczej. Można odróżnić je od zwykłych z uwagi na
przedrostek layout_. Parametry określają, w jaki sposób widok należy wyświe-
tlić w układzie. W odróżnieniu od zwykłych atrybutów, które są bezpośrednio
stosowane do widoku, parametry układu to wskazówki dla widoku nadrzędnego
(zwykle jest nim menedżer układu) z układu. Nie pomyl jednak widoku nadrzęd-
nego z układu z klasą nadrzędną. To pierwsze określenie dotyczy odrębnego
widoku, w którym dany widok jest osadzony w układzie. Druga nazwa związana
jest z hierarchią typów widoków. Wszystkie menedżery układu, a także niektóre
widoki (na przykład Gallery), mają odpowiednie parametry układu zdefiniowane
w klasie wewnętrznej LayoutParams. Wszystkie klasy LayoutParams obsługują
atrybuty android:layout_width i android:layout_height, a we wszystkich mene-
dżerach układu dostępny jest atrybut android:layout_margin.
MARGINESY ZEWNĘTRZNE I WEWNĘTRZNE. Dla każdego boku
można niezależnie zdefiniować marginesy zewnętrzne (ang. margin)
i wewnętrzne (ang. padding). Dla widoku można na przykład zdefiniować
dowolne z poniższych atrybutów:
Q android:layout_marginLeft,
Q android:layout_marginTop,
Q android:layout_marginRight,
Q android:layout_marginBottom.

To samo dotyczy atrybutu android:padding.


Pozostałe parametry są związane z różnymi implementacjami klasy LayoutParams
z frameworku. Parametry określające szerokość i wysokość są szczególne z dwóch
przyczyn — muszą występować w każdym widoku (jeśli jest inaczej, Android
zgłasza wyjątek). Ponadto przyjmują nie tylko wartości liczbowe (w pikselach),
ale też dwie zarezerwowane wartości. Oto one:
Q fill_parent. Określa, że widok zajmuje maksymalny dostępny obszar
w widoku nadrzędnym. Próbuje wypełnić obszar widoku nadrzędnego
(z pominięciem marginesów wewnętrznych i zewnętrznych) niezależnie
od miejsca zajmowanego przez własne elementy podrzędne. Jeśli element
nadrzędny to kwadrat o boku 100 pikseli bez podanych marginesów
zewnętrznych lub wewnętrznych, element podrzędny to także kwadrat
o boku 100 pikseli. Warto zauważyć, że atrybut fill_parent uznano
146 ROZDZIAŁ 4. Precyzja co do piksela

za przestarzały. Jego nowy odpowiednik to match_parent. Prawdopodobnie


chcesz zapewnić obsługę starszych wersji Androida, dlatego do czasu
zniknięcia z rynku dawnych wersji platformy warto używać atrybutu
fill_parent.
Q wrap_content. Określa, że widok zajmuje w widoku nadrzędnym tyle
miejsca, ile potrzeba na wyświetlenie całej zawartości. Jeśli widok
nadrzędny to kwadrat o boku 100 pikseli, a elementy podrzędne danego
widoku zajmuje tylko kwadrat o boku 50 pikseli, sam widok to kwadrat
o boku 50 pikseli.
Teraz, po przedstawieniu działania kilku plików układu, opisaniu ich struktury,
a także obsługiwanych atrybutów i parametrów, w następnym kroku jeszcze
dokładniej przyglądamy się układom i szczegółowo omawiamy dostępne mene-
dżery układów.

4.3.2. Menedżery układów


W Androidzie obecnie zdefiniowane są cztery różne menedżery układu, które
można stosować do porządkowania widoków na ekranie. Jeśli potrzebujesz bar-
dziej złożonego rozwiązania, możesz zaimplementować własny menedżer, jednak
nie omawiamy tego zagadnienia. Menedżery można podzielić na ustrukturyzo-
wane i nieustrukturyzowane lub według złożoności. Klasyfikację menedżerów
przedstawiono w tabeli 4.2.
Tabela 4.2. Wbudowane menedżery układów z Androida

Złożoność Nieustrukturyzowane Ustrukturyzowane


Mniejsza FrameLayout LinearLayout
Większa RelativeLayout TableLayout

Istnieje też piąty menedżer układów, AbsoluteLayout, który jednak został uznany
za przestarzały. Nie należy z niego korzystać, ponieważ nie dopasowuje się do
różnych konfiguracji ekranu (w podrozdziale 4.7 zobaczysz, że ma to znaczenie).
W dalszych podpunktach opisujemy pokrótce wszystkie menedżery (z wyjątkiem
menedżera AbsoluteLayout). Zaczynamy od najprostszego, FrameLayout, i stop-
niowo przechodzimy do najbardziej złożonego (RelativeLayout).
FRAMELAYOUT
Jest to najprostszy z wszystkich menedżerów układu. FrameLayout nie wykonuje
żadnych operacji na układzie i pełni tylko funkcję kontenera (ramki; ang. frame).
Menedżer FrameLayout wyświetla jeden element podrzędny naraz. Obsługuje
wiele elementów podrzędnych, umieszczanych jeden na drugim. Elementy pod-
rzędne są wyrównywane do lewego górnego rogu i wyświetlane jeden na drugim
zgodnie z kolejnością ich deklaracji. Wydaje Ci się to bezużyteczne? Szczerze
przyznajemy, że menedżer FrameLayout rzadko jest przydatny jako coś więcej
4.3. Porządkowanie widoków w układy 147

niż zwykły kontener lub element w układzie pudełkowym. Przydaje się jednak
do nakładania okien pływających na układ ekranu (w bibliotece Ignition, która
jest przydatnym zestawem narzędzi i wzbogaconych komponentów Androida;
technika ta służy do wyświetlania „karteczek samoprzylepnych” dołączanych
do dowolnej kontrolki). Na listingu 4.1 pokazano, jak zdefiniować menedżer
FrameLayout obejmujący dwa widoki TextView.

Listing 4.1. Przykładowy menedżer FrameLayout z dwoma widokami TextView

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


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="150px"
android:layout_height="150px"
android:background="@android:color/darker_gray"
/>
<TextView
android:layout_width="75px"
android:layout_height="75px"
android:background="@android:color/white"
/>
</FrameLayout>

Możesz się zastanawiać, jak dwa zdefiniowane


widoki tekstowe wyświetlane są w ramach
układu. Przyjrzyj się rysunkowi 4.4. Widać na
nim, że widoki znajdują się jeden na drugim,
przy czym widok znajdujący się na wierzchu jest
wyświetlany jako ostatni.
Nie trzeba wyjaśniać, że FrameLayout to nie
tylko najprostszy, ale też najszybciej działający
menedżer układu. Dlatego zawsze dobrze prze-
myśl decyzję o zastosowaniu bardziej skompli-
kowanego menedżera! Przejdźmy teraz do przy-
datniejszego menedżera, LinearLayout, który już
kilkukrotnie wykorzystaliśmy. Prawdopodobnie
Rysunek 4.4. Dwa widoki
będziesz często go używał. uporządkowane za pomocą
menedżera FrameLayout.
LINEARLAYOUT Zauważ, że jeden widok
znajduje się na drugim,
LinearLayout to najczęściej stosowany (czasem a oba są wyrównane
nadużywany) menedżer układu. Jest prosty, łatwy do lewego górnego rogu
w użyciu i pełni wiele funkcji. Jak już stwier-
dziliśmy, w menedżerze LinearLayout wszystkie widoki są wyświetlane w wier-
szach lub kolumnach (zależy to od wartości atrybutu android:orientation). Jeśli
nie określono bezpośrednio orientacji, elementy domyślnie rozmieszczane
148 ROZDZIAŁ 4. Precyzja co do piksela

są w poziomie. Menedżer LinearLayout ma dwa dodatkowe parametry układu,


które można stosować do jego elementów podrzędnych. Parametry te opisano
w tabeli 4.3.
Tabela 4.3. Parametry układu menedżera LinearLayout

Atrybut Działanie

android:layout_weight Informuje menedżer układu o tym, ile miejsca powinien zajmować


widok względem siostrzanych widoków. Wielkość widoku jest
określana na podstawie stosunków wag widoków. Jeśli na przykład
wszystkie widoki mają tę samą wagę, dostępny obszar jest dzielony
między nie po równo. To, którego wymiaru mają dotyczyć wagi
(szerokości czy wysokości), można kontrolować przez ustawienie
odpowiedniej wartości dla danego wymiaru na 0px.
Warto zauważyć, że wagi nie muszą sumować się do 1, choć
standardowo są rozdzielane między wszystkie elementy podrzędne
jako ułamki liczby 1 (semantyka oparta na procentach). Istotne
są stosunki wartości przypisanych poszczególnym widokom.
android:layout_gravity Informuje menedżer układu, w którym kierunku widoki powinny
być rozmieszczane w kontenerze. Atrybut ten ma znaczenie tylko
wtedy, kiedy wielkość widoku w danym wymiarze jest albo ściśle
określona, albo ma wartość wrap_content.

Na listingu 4.2 znajduje się definicja układu podobnego do układu FrameLayout


z listingu 4.1, jednak tym razem używamy menedżera LinearLayout. Ponadto
stosujemy atrybut weight do rozdzielenia dostępnego miejsca po równo między
dwa widoki tekstowe.

Listing 4.2. Przykładowy menedżer LinearLayout z elementami podrzędnymi


z wagami

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="0px"
android:layout_height="100px"
android:layout_weight="0.5"
android:background="@android:color/darker_gray"
/>
<TextView
android:layout_width="0px"
android:layout_height="100px"
android:layout_weight="0.5"
android:background="@android:color/white"
/>
</LinearLayout>

Na rysunku 4.5 pokazano, jak wygląda ten układ po wyświetleniu. Warto zauwa-
żyć, że każdy widok zajmuje dokładnie tyle samo miejsca w poziomie. Przypo-
minamy, że liczy się tylko stosunek między wagami. Ustawienie obu wag na 1
dałoby ten sam efekt, ponieważ 0,5/0,5 = 1/1 = 1.
4.3. Porządkowanie widoków w układy 149

Menedżer LinearLayout jest prosty, ale sku-


teczny. Dobrze nadaje się do rozwiązywania typo-
wych problemów z układami, na przykład do
porządkowania przycisków jeden obok drugiego.
Można go też wykorzystać do tworzenia siatek
i tabel, jednak do tego wygodniejszy jest mene-
dżer TableLayout.
TABLELAYOUT
Klasa TableLayout jest klasą typu LinearLayout
(dziedziczy po niej) wzbogaconą o mechanizmy
przydatne do wyświetlania tabel lub siatek.
Zastosowano tu specjalną klasę widoku, TableRow,
pełniącą funkcję kontenera na komórki tabeli.
Każda komórka może obejmować tylko jeden
widok. Widok ten może być oparty na mene- Rysunek 4.5. Dwa widoki
dżerze układu lub dowolnym innym obiekcie rozmieszczone za pomocą
menedżera LinearLayout.
klasy ViewGroup. Na listingu 4.3 pokazano prosty Oba widoki zajmują tyle samo
menedżer TableLayout z pojedynczym wierszem. miejsca i są ułożone
w poziomie

Listing 4.3. Przykładowy menedżer TableLayout


z pojedynczym wierszem

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


<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TableRow>
<TextView
android:layout_width="150px"
android:layout_height="100px"
android:background="@android:color/darker_gray"
/>
<TextView
android:layout_width="150px"
android:layout_height="100px"
android:background="@android:color/white"
/>
</TableRow>
</TableLayout>

Na rysunku 4.6 pokazano, jak zdefiniowana tabela wygląda na ekranie. Z uwagi


na podane przyczyny różnica w porównaniu z przykładem opartym na menedżerze
LinearLayout jest symboliczna.
Na rysunku 4.6 widać, że każdemu widokowi podrzędnemu z widoku TableRow
odpowiada kolumna tabeli. Uzyskanie tego samego efektu za pomocą menedżera
LinearLayout wymaga znacznie więcej kodu, dlatego warto korzystać z omawianego
menedżera układu, kiedy potrzebne są komórki tabel lub siatek.
150 ROZDZIAŁ 4. Precyzja co do piksela

Rysunek 4.6. Dwa widoki rozmieszczone przy


użyciu menedżera Tab\leLayout. Różni się
on od menedżera LinearLayout tylko sposobem
konfigurowania układu, ponieważ TableLayout
to wyspecjalizowana wersja menedżera
LinearLayout

Poznałeś już trzy pierwsze menedżery układu udostępniane przez Android.


Nadal jednak nie wiesz, jak tworzyć naprawdę skomplikowane układy. Opisane
do tej pory menedżery układu wykonują proste zadania — w najlepszym razie
rozmieszczają widoki jeden obok drugiego. Jeśli potrzebujesz więcej kontroli
nad tym, jak widoki powinny być uporządkowane na ekranie, musisz zastosować
prawdopodobnie najprzydatniejszy menedżer układu z Androida — Relative
´Layout.
RELATIVELAYOUT
RelativeLayout to najbardziej zaawansowany spośród czterech menedżerów układu.
Umożliwia niemal dowolne rozmieszczanie widoków przez uporządkowanie
ich względem siebie. Menedżer RelativeLayout udostępnia parametry pozwa-
lające elementom podrzędnym wskazywać siebie nawzajem za pośrednictwem
identyfikatorów (notacja @id). Aby przejść do konkretów, przedstawiamy następny
przykład (zobacz listing 4.4). Pokazujemy w nim działanie tego menedżera.

Listing 4.4. Atrybuty do określania względnej pozycji w przykładowym menedżerze


RelativeLayout

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


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView android:id="@+id/text_view_1"
android:layout_width="150px"
android:layout_height="100px"
android:background="@android:color/darker_gray"
/>
<TextView android:id="@+id/text_view_2"
android:layout_width="150px"
android:layout_height="100px"
android:layout_toRightOf="@id/text_view_1"
4.3. Porządkowanie widoków w układy 151

android:layout_centerVertical="true"
android:background="@android:color/white"
/>
</RelativeLayout>

Na listingu 4.4 w widoku text_view_2 za pomocą atrybutu layout_toRightOf dekla-


rujemy, że widok ten należy wyświetlić na prawo od (toRightOf) widoku
text_view_1 . W ten sposób w widokach w menedżerze RelativeLayout określana
jest pozycja względem innych widoków. Jest to skuteczny i skalowalny sposób
podawania pozycji widoków. Technika ta ma jednak pewien subtelny efekt
uboczny. Ponieważ referencje mają tu postać @id/view_id, obejmującą identyfi-
kator już zdefiniowanego widoku, czasem trzeba przestawić definicje widoków,
aby móc wskazywać te elementy. Jeśli układ jest złożony, bywa to skomplikowane.
Aby rozwiązać problem, można zastosować specjalną notację dla identyfikatorów
i nakazać frameworkowi utworzenie tych, które jeszcze nie istnieją.
ZARZĄDZANIE IDENTYFIKATORAMI W UKŁADACH
Wróćmy do listingu 4.4. Gdybyś chciał wskazać widok text_view_2 w widoku
text_view_1, musiałbyś przestawić definicje tych widoków. W przeciwnym razie
wystąpiłby błąd dotyczący braku definicji widoku text_view_2. Aby uniknąć pro-
blemu, można przy deklarowaniu identyfikatora zastosować specjalną notację
@+id. Jeśli dodasz znak +, Android utworzy nowy identyfikator dla każdego widoku,
który jeszcze nie istnieje.
UWAGA. Może się to wydawać dziwne, ale identyfikatory — choć służą do
identyfikowania zasobów — same są zasobami. Oznacza to, że podobnie
jak dla innych zasobów, takich jak łańcuchy znaków, można utworzyć plik
ids.xml w katalogu res/values i zapisać w nim puste identyfikatory (mające
nazwę, ale niewskazujące na żadne zasoby). Notacja @+id nie jest potrzebna
do korzystania z takich identyfikatorów, ponieważ już istnieją (jednak
dozwolone jest stosowanie tej notacji). Aby zdefiniować identyfikator
w pliku zasobów ids.xml, należy użyć znacznika item:
<item type="id" name="my_id" />

Zapis @+id powoduje utworzenie nowego identyfikatora w wewnętrznej tabeli


identyfikatorów Androida, jednak dzieje się to tylko wtedy, gdy dany identyfi-
kator jeszcze nie istnieje. Jeżeli już go utworzono, nadal wskazuje na istniejący
egzemplarz (nie prowadzi to do błędu). Możesz użyć symbolu + przy danym
identyfikatorze dowolną liczbę razy, jednak identyfikator powstaje tylko raz
(jest to operacja idempotentna). Dlatego omawianą notację można stosować do
wskazywania widoku zdefiniowanego dalej w pliku XML z układem. Android two-
rzy wtedy odpowiedni identyfikator w momencie zastosowania go do wskazania
widoku, a następnie ponownie wykorzystuje ten identyfikator w definicji widoku.
152 ROZDZIAŁ 4. Precyzja co do piksela

Na zakończenie omówienia menedżerów układu na rysunku 4.7 pokazano


dwa widoki TextView z przedstawionego przykładu uporządkowane za pomocą
menedżera RelativeLayout.

Rysunek 4.7. Dwa widoki uporządkowane przy


użyciu menedżera RelativeLayout. Warto
zauważyć, że przez określenie atrybutów
pozycji z zastosowaniem samych wartości
względnych można uporządkować elementy
w niemal dowolny sposób

Z wykorzystaniem czterech wbudowanych menedżerów układu z Androida można


utworzyć niemal dowolny potrzebny układ — nawet stosunkowo złożony. Kiedy
zaczniesz budować bardziej zaawansowane układy, używaj narzędzia layoutopt
do wykrywania potencjalnych problemów (wspomnieliśmy już o tym, jednak
narzędzie jest łatwe w użyciu, ale często pomijane, dlatego warto przypomnieć
o jego istnieniu).
To już koniec abc widoków i układów. Uważamy, że masz wystarczającą wie-
dzę, aby zrozumieć, czym jest aktywność w Androidzie. Poznałeś układ obej-
mujący elementy wizualne aktywności, a także wiesz, jak jest wyświetlany na
ekranie. Pora przejść do technik. Przedstawiamy tu zaawansowane rozwiązania,
które prawdopodobnie okażą się przydatnymi narzędziami w codziennym pro-
gramowaniu interfejsu użytkownika na Android. Omówienie układów kończymy
prezentacją pierwszej techniki, polegającej na scalaniu i dołączaniu układów.

0 TECHNIKA 1. Dyrektywy scalania i dołączania

Unikanie powtórzeń to optymalizacja ważna ze względu na komfort pracy pro-


gramisty i działanie układów. Wzrost złożoności układów prowadzi do coraz
częstszego powielania kodu, ponieważ określone fragmenty są potrzebne także
w innych miejscach. Dobrym przykładem jest pasek z przyciskami OK i Anuluj.
Na listingu 4.5 przedstawiono kod układu z takim paskiem.
0 TECHNIKA 1. Dyrektywy scalania i dołączania 153

Listing 4.5. Układ z paskiem przycisków dla akcji OK i Anuluj

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView android:text="Aktywność z paskiem przycisków"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<!-- Pasek przycisków -->
<LinearLayout android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:text="@string/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:text="@string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</LinearLayout>

Wszystko jest dobrze, jeśli potrzebujesz przycisków w tylko jednej aktywności.


Co się jednak dzieje, kiedy chcesz użyć ich w kilku miejscach? Dla każdej aktyw-
ności definiowany jest odrębny plik układu. Możesz skopiować potrzebny kod
układu do wielu plików, jednak każdy dobry programista wie, że to kiepski
pomysł. Powielanie kodu powoduje powstawanie programów podatnych na błędy
i trudnych w konserwacji.
PROBLEM
Chcemy wykorzystać wybrane fragmenty układu także w innych plikach, aby
zminimalizować powtórzenia wynikające z kopiowania tego samego kodu w róż-
nych miejscach.
ROZWIĄZANIE
Kiedy natrafisz na powtarzające się fragmenty w różnych plikach układu, takich
jak pokazany, warto wypróbować specjalne elementy układu — <merge> i <include>.
Elementy te pozwalają wyodrębnić często używane fragmenty widoków do
osobnych plików układu (możesz traktować je jak komponenty widoków lub
układów). Następnie fragmenty te można wykorzystać w innych układach.
W celu pokazania działania tej techniki kod paska przycisków z listingu 4.5
wyodrębniamy do osobnego pliku o nazwie button_bar.xml (zapisanego w katalogu
res/layout). Plik przedstawiono na listingu 4.6.
154 ROZDZIAŁ 4. Precyzja co do piksela

Listing 4.6. Przeznaczony do wielokrotnego użytku komponent paska przycisków


zdefiniowany w odrębnym pliku układu

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:text="@string/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:text="@string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>

Aby następnie dołączyć fragment lub komponent układu do innego pliku, wystar-
czy użyć elementu <include>.
<include layout="@layout/button_bar" />

Gotowe! Element include nie przyjmuje żadnych parametrów oprócz nazwy


układu. Jeśli jednak chcesz, możesz przekazać niestandardowe parametry układu
lub nowy identyfikator, aby przesłonić atrybuty zdefiniowane dla widoku głów-
nego z dołączanego układu.
PUŁAPKA ZWIĄZANA Z DOŁĄCZANIEM UKŁADÓW. Kiedy prze-
słaniasz atrybut layout_width lub layout_height, zawsze powinieneś
przesłonić także drugi atrybut z tej pary. Jeśli przesłonisz na przykład
tylko atrybut layout_width, ale nie layout_height, Android bez powiada-
miania zignoruje nowe ustawienia layout_*. Jest to słabo udokumento-
wane i nieco kontrowersyjne rozwiązanie, jednak jeżeli wiesz, jak działa,
możesz łatwo uporać się z problemem.
Pozostaje jedno pytanie — co zrobić, aby dołączyć widok o innej bazowej klasie
View lub układ w widoku nadrzędnym o niezgodnym typie? Może się wydawać,
że wystarczy pominąć nadrzędny element LinearLayout i zmodyfikować plik
button_bar.xml z listingu 4.6 w następujący sposób:
<?xml version="1.0" encoding="utf-8"?>
<Button android:text="@android:string/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:text="@android:string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

Niestety, nie można tak postąpić, ponieważ nie jest to prawidłowy dokument
XML. Pamiętaj, że dokumenty XML to drzewa, a drzewo zawsze ma korzeń.
Kod z listingu nie spełnia tego warunku, dlatego nie zadziała. Android zapewnia
0 TECHNIKA 1. Dyrektywy scalania i dołączania 155

jednak pewne rozwiązanie. Element <merge> to element zastępczy na dowolną


nadrzędną klasę View, do której widoki (części) układu będą dołączane. Oznacza
to, że w celu zapewnienia działania poprzedniego fragmentu należy przekształ-
cić go tak:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button android:text="@android:string/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:text="@android:string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</merge>

Możesz traktować znacznik <merge> jak symbol wieloznaczny. Pełni on funkcję


spoiwa. Każdy otoczony nim układ jest wstawiany do menedżera (LinearLayout,
RelativeLayout itd.), a węzeł <merge> w czasie wykonywania programu jest zastę-
powany przez węzeł menedżera. Technikę tę można stosować wszędzie tam,
gdzie bez niej konieczne byłoby dołączenie widoku do innego widoku tego samego
rodzaju (co prowadziłoby do niepożądanych powtórzeń).
OMÓWIENIE
Gorąco zachęcamy do zapamiętania tej techniki i stosowania jej w kodzie ukła-
dów zawsze, kiedy jest to możliwe. Dzięki niej złożone pliki układu są przejrzyste
i czytelne. Technika ta znacznie zmniejsza też liczbę powtórzeń w kodzie, a tym
samym ilość pracy przy jego konserwacji. Zgodnie z podstawową zasadą inży-
nierii w danej aplikacji nigdy nie należy kodować tych samych informacji więcej
niż raz (jest to zasada nie powtarzaj się; ang. don’t repeat yourself — DRY). Znacz-
niki <merge> i <include> pomogą Ci zapewnić zgodność układów z zasadą DRY.
Wadą podziału układów w opisany sposób jest to, że w środowisku Eclipse
wizualne narzędzia do tworzenia układów na Android czasem błędnie wyświe-
tlają podgląd takich rozwiązań. Z drugiej strony jest tylko kwestią czasu, kiedy
androidowe narzędzia poprawią się na tyle, aby prawidłowo wyświetlać pod-
gląd także układów scalanych w skomplikowany sposób.
Po tej pierwszej krótkiej technice wracamy do omawiania układów w Andro-
idzie. Pamiętasz, że obiecaliśmy napisać całą aplikację, która zarządza tytułami
filmów i dobrze wygląda? Tu spełniamy tę obietnicę. Ponieważ aplikacja
MyMovies (podobnie jak DealDroid) jest oparta na widoku listy, ponownie
wracamy do tego widoku i adapterów. Zagadnienia te są bardziej złożone, niż
można by podejrzewać.
156 ROZDZIAŁ 4. Precyzja co do piksela

4.4. Rozwinięcie informacji o klasach ListView i Adapter


W rozdziale 2. zobaczyłeś już, jak używać widoku listy z adapterem. Tamten
przykład był prosty. Aplikacja wyświetlała listę ofert, jednak dane te były sta-
tyczne. Nie zmieniały się po umieszczeniu ich na liście. W aplikacji MyMovies
możliwe ma być zaznaczanie filmów z kolekcji użytkownika. Niezbędne jest do
tego lepsze poznanie widoku listy i adapterów. Potrzebna jest lista z pamięcią
stanu, obejmująca elementy przyjmujące dwa stany — zaznaczony lub nieza-
znaczony. Ponadto uatrakcyjniamy aplikację i pokazujemy, jak dodać do listy
nagłówek i stopkę. Przy okazji przedstawiamy też kilka optymalizacji, na przykład
wzorzec ViewHolder, który znacznie przyspiesza wyświetlanie listy i zwiększa
płynność jej przewijania.
Zacznijmy jednak od początku. Aby zachować stan zaznaczenia potrzebny
w aplikacji MyMovies, po wybraniu filmu z listy trzeba zapisać, że użytkownik
ma go w kolekcji. Usunięcie zaznaczenia ma prowadzić do usunięcia filmu. Nie
zajmujemy się tu utrwalaniem informacji w bazie danych lub pliku, ponieważ
nie opisaliśmy jeszcze potrzebnych do tego mechanizmów. Oznacza to, że lista
filmów dodanych do kolekcji przez ich dotknięcie jest tracona w momencie
ponownego uruchomienia aplikacji. Jest to celowe rozwiązanie, pozwalające
skoncentrować się na używaniu adapterów do zachowywania stanu. Nie martw
się — w rozdziale 7. nauczysz się zapisywać dane w plikach i bazach oraz poznasz
inne techniki.
Do przedstawienia wiązania widoków ListView ze źródłem danych służy tu
statyczny plik z filmami używany poprzez adapter. Na razie musimy wrócić do
klasy ListActivity i przyjrzeć się kodowi aplikacji MyMovies.

0 TECHNIKA 2. Zarządzanie listą z pamięcią stanu

Na listingu 4.1 główny ekran aplikacji MyMovies odpowiada aktywności List


´Activity, która na całym wyświetlaczu wyświetla jeden widok ListView. Ponadto
obok każdego filmu z listy znajduje się pole wyboru, które można przełączyć.
Pole to jest prostym mechanizmem zachowywania stanu w modelu i widokach.
Pytanie tylko, gdzie zapisać ten stan? Ponadto jak w widoku ListView odzwiercie-
dlić aktualizację stanu?
PROBLEM
Lista jest oparta na danych pochodzących z adaptera. Aplikacja ma albo zapi-
sywać zmiany wprowadzone (na przykład przez kliknięcie elementu listy)
w widoku z powrotem w źródle danych, albo odzwierciedlać modyfikacje danych
w widoku.
0 TECHNIKA 2. Zarządzanie listą z pamięcią stanu 157

ROZWIĄZANIE
Kiedy trzeba przechowywać dane dynamicznie zmieniające się w reakcji na
zdarzenia w widoku (lub dowolne inne zdarzenia), należy utworzyć własną imple-
mentację adaptera, która wykonuje opisane niżej operacje.
Q Kiedy dane, dla których adapter jest nakładką, się zmieniają, należy
przekazać informacje o modyfikacjach do widoku, aby ten mógł się
ponownie wyświetlić.
Q Kiedy użytkownik wchodzi z widokiem w interakcje związane z aktualizacją
danych, należy poinformować o tym adapter i wprowadzić odpowiednie
zmiany w źródle danych.
Aby pokazać działanie tej techniki, zaczynamy od prostego kodu głównego ekranu
aplikacji MyMovies (listing 4.7). Następnie przechodzimy do niestandardowego
adaptera.

Listing 4.7. Plik MyMovies.java z klasą pochodną od ListActivity

public class MyMovies extends ListActivity {

private MovieAdapter adapter;

public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);

setContentView(R.layout.main);

ListView listView = getListView();

this.adapter = new MovieAdapter(this);


listView.setAdapter(this.adapter);
listView.setItemsCanFocus(false);
}

@Override
protected void onListItemClick(ListView l, View v,
int position, long id) {
this.adapter.toggleMovie(position);
this.adapter.notifyDataSetChanged();
}
}

Ekran główny aplikacji MyMovies działa podobnie jak inne opisane wcześniej
aktywności (tak jak one jest rozszerzeniem klasy ListActivity , która po raz
pierwszy pojawiła się w rozdziale 2.). Omawiana aktywność (także podobnie jak
w rozdziale 2.) obejmuje adapter . Zanim przejdziemy dalej, warto przypo-
mnieć, że używamy klasy ListActivity, ponieważ odpowiada za wiele aspektów
zarządzania widokiem ListView. Między innymi zapewnia łatwy dostęp do widoku
ListView (poprzez metodę getListView) i umożliwia wygodną obsługę kliknięć
(za pomocą metody onListItemClick). Także tu adapter jest ustawiany w widoku
ListView i używany do udostępniania źródła danych dla elementów listy .
158 ROZDZIAŁ 4. Precyzja co do piksela

Następnie należy przyjrzeć się źródłu danych i niestandardowemu adapte-


rowi MovieAdapter używanemu w widoku ListView. Przed skonfigurowaniem
adaptera warto zobaczyć, jak wygląda źródło danych z filmami. Dane można
pobierać z internetu (niektóre usługi sieciowe, na przykład DealDroid, działają
w ten sposób; technikę tę szczegółowo opisujemy w rozdziale 9.), jednak tu
upraszczamy rozwiązanie. W aplikacji zapisujemy listę 100 najpopularniejszych
filmów z bazy IMDB jako zasób w postaci tablicy. Wymaga to utworzenia w kata-
logu res/values nowego pliku o nazwie movies.xml i następującej strukturze:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="movies">
<item>The Shawshank Redemption (1994)</item>
<item>The Godfather (1972)</item>
<item>The Godfather, Part II (1974)</item>
<item>The Good, the Bad, and the Ugly (1966) (It.)</item>
...
</string-array>
</resources>

Ponieważ zasoby poznałeś już w rozdziale 2., wiesz, że tablicę można wskazy-
wać w aplikacji za pomocą wyrażenia R.array.movies. Warto zauważyć, że można
na stałe zapisać listę jako zwykłą tablicę Javy w jednej z klas aplikacji. Jednak
wykorzystanie androidowego mechanizmu zasobów ma tę zaletę, że można uzy-
skać pełną kontrolę nad momentem wczytywania danych do pamięci, a przy
tym zachować przejrzystość kodu aplikacji. W końcu kod aplikacji powinien
obejmować logikę jej działania, a nie dane.
Następny krok to wyświetlanie danych w widoku ListView. Ponieważ uży-
wamy zwykłej tablicy, do zaimplementowania klasy MovieAdapter (przypisanej
do widoku ListView na listingu 4.7) doskonale nadaje się klasa ArrayAdapter
z Androida. Adapter MovieAdapter ma służyć do śledzenia filmów dodawanych
przez użytkownika i udostępniać ich stan w celu aktualizacji pola wyboru. Imple-
mentację adaptera przedstawiono na listingu 4.8.

Listing 4.8. Adapter MovieAdapter śledzi zaznaczone filmy

public class MovieAdapter extends ArrayAdapter<String> {

private HashMap<Integer, Boolean> movieCollection =


new HashMap<Integer, Boolean>();

public MovieAdapter(Context context) {


super(context, R.layout.movie_item,
android.R.id.text1, context
.getResources().getStringArray(R.array.movies));
}

public void toggleMovie(int position) {


if (!isInCollection(position)) {
movieCollection.put(position, true);
} else {
0 TECHNIKA 2. Zarządzanie listą z pamięcią stanu 159

movieCollection.put(position, false);
}
}

public boolean isInCollection(int position) {


return movieCollection.get(position) == Boolean.TRUE;
}

@Override
public View getView(int position, View convertView,
ViewGroup parent) {

View listItem = super.getView(position, convertView, parent);

CheckedTextView checkMark = null;


ViewHolder holder = (ViewHolder) listItem.getTag();
if (holder != null) {
checkMark = holder.checkMark;
} else {
checkMark = (CheckedTextView)
listItem.findViewById(android.R.id.text1);
holder = new ViewHolder(checkMark);
listItem.setTag(holder);
}
checkMark.setChecked(isInCollection(position));

return listItem;
}

private class ViewHolder {


protected final CheckedTextView checkMark;

public ViewHolder(CheckedTextView checkMark) {


this.checkMark = checkMark;
}
}
}

Na początku warto zauważyć, że klasa MovieAdapter jest rozszerzeniem klasy


ArrayAdapter . Pozwala to nie tylko zaimplementować potrzebne metody, ale
też uniknąć ponownego wymyślania koła. Klasa MovieAdapter obejmuje lokalny
obiekt HashMap do przechowywania danych o stanie filmów . Kolekcja filmów
użytkownika jest zapisywana jako odwzorowanie z pozycji listy filmów na warto-
ści logiczne (wartość true oznacza, że użytkownik posiada dany film). Przypo-
minamy, że stan ten jest nietrwały, dlatego zamknięcie aplikacji powoduje jego
utratę. Jednak w podobny sposób można używać także bazy danych, systemu
plików lub innego mechanizmu przechowywania danych.
Dalej w konstruktorze wywoływany jest konstruktor klasy bazowej Array
´Adapter (tak działa każda dobra klasa pochodna w Javie), do którego przeka-
zujemy kontekst, układ używany dla każdego elementu, identyfikator widoku
TextView dla elementów i początkową kolekcję danych . Pierwszą metodą
w klasie jest toggleMovie, służąca do aktualizowania stanu modelu . Dalej
160 ROZDZIAŁ 4. Precyzja co do piksela

znajduje się niezwykle istotna metoda getView, po raz pierwszy przedstawiona


w rozdziale 2. Metoda ta zwraca widok potrzebny dla każdego elementu i jest
wywoływana za każdym razem, kiedy trzeba (ponownie) wyświetlić element listy .
Ponowne wyświetlanie elementów listy odbywa się często, na przykład przy
jej przewijaniu. Ponieważ operacja ta bywa kosztowna, używamy wzorca View-
Holder do zoptymalizowania pobierania elementów listy w widoku ListView
i przypisywania do nich zawartości. Technika ta polega na pominięciu dodatko-
wych wywołań metody findViewById, ponieważ jest ona stosunkowo kosztowna.
Aby zrealizować cel, zapisujemy efekt działania metody findViewById w obiekcie
klasy ViewHolder. ViewHolder to wewnętrzna klasa utworzona do przechowywania
potrzebnego widoku CheckedTextView .
W jaki sposób powiązać zapisany w pamięci podręcznej widok z obecnym
elementem listy? Do tego służy wygodna metoda o nazwie getTag. Metoda ta
umożliwia powiązanie z widokiem dowolnych danych. Można wykorzystać ją
do zapisania w pamięci podręcznej samego obiektu klasy ViewHolder, który z kolei
zapisuje referencje do widoku. Metodę getTag należy wywołać dla aktualnego
elementu listItem (pozwalamy klasie bazowej ustalić, czy trzeba utworzyć nowy
element, czy można wykorzystać istniejący za pomocą metody convertView), aby
sprawdzić, czy obiekt klasy ViewHolder jest dostępny . Jeśli taki obiekt istnieje,
można bezpośrednio pobrać z niego element CheckedTextItem . Brak obiektu
klasy ViewHolder oznacza, że aplikacja nie używa zapisanego widoku, dlatego
trzeba wywołać metodę findViewById w celu uzyskania referencji do elementu
CheckedTextItem , utworzyć obiekt klasy ViewHolder i użyć metody setTag do
powiązania obiektu z elementem listItem . Wzorzec ViewHolder pozwala
przyspieszyć działanie widoku ListView i zwiększyć jego wydajność. Zawsze
warto zastanowić się nad zastosowaniem tego wzorca, jeśli na liście może znaleźć
się większa liczba elementów.
PISANIE KODU Z MYŚLĄ O WIELOKROTNYM WYKORZYSTANIU
WIDOKU. Ponieważ nie utrwalamy tu żadnych informacji, możesz uznać,
że męczenie się z tworzeniem niestandardowego adaptera, który tylko
zapisuje w pamięci dane usuwane przy każdorazowym zamknięciu apli-
kacji, to przesada. Możesz też uważać, że wystarczy przełączać widok dla
pola wyboru przy każdym dotknięciu, a efekt będzie taki sam. Nieprawda!
Adaptery odpowiadają za tworzenie widoku, który reprezentuje element
z określonej pozycji z używanego zbioru danych. Wszystkie standardowe
klasy adapterów, na przykład ArrayAdapter, jak i dobrze zaimplementowane
adaptery niestandardowe zapisują w pamięci podręcznej oraz ponownie
wykorzystują widoki elementów. Służy do tego metoda convertView; tech-
nika ta pozwala poprawić wydajność. Dlatego na liście ponownie pojawiają
się zaznaczone elementy, choć użytkownik nie kliknął ich po raz wtóry.
Nawet gdyby taki efekt nie był potrzebny, pamiętaj, że stan należy aktu-
alizować w modelu, a nie w widoku.
0 TECHNIKA 3. Widoki nagłówka i stopki 161

Warto zauważyć, że zastosowanie niestandardowego adaptera nie jest jedynym


rozwiązaniem. To tylko jedno proste rozwiązanie. Innym całkowicie poprawnym
podejściem jest utworzenie klasy modelu Movie i zapamiętywanie w obiektach
tej klasy, czy użytkownik ma dany film. Pozwala to zrezygnować z klasy HashMap,
a zamiast niej pobierać obiekt klasy Movie z danej pozycji i sprawdzać w nim,
czy użytkownik posiada film. Rozwiązanie to stosujemy w rozdziale 9., gdzie
wzbogacamy aplikację MyMovies, tak aby komunikowała się z internetową bazą
filmów. Niezależnie od podejścia trzeba aktualizować zarządzany przez nowy
adapter model w reakcji na każde kliknięcie filmu przez użytkownika. Do aktu-
alizacji służy metoda obsługi zdarzeń ListActivity.onListItemClick przedsta-
wiona na listingu 4.7.
OMÓWIENIE
Stosowanie adapterów w opisany tu sposób to wartościowa metoda zarządzania
dynamicznie zmieniającymi się danymi, pozwalająca całkowicie oddzielić je od
widoków, a tym samym od sposobu ich wyświetlania. Choć nie napisaliśmy tu
dużej ilości kodu, czasem nawet taki poziom złożoności jest zbyt wysoki. Jeśli
chcesz tylko poprosić użytkownika o wybór opcji na liście (na przykład w oknie
dialogowym), istnieje technika dużo prostsza od implementowania własnego
adaptera — wystarczy użyć atrybutu choiceMode widoku ListView. Jeśli tryb wyboru
jest ustawiony na multipleChoice, można za pomocą metody ListView.getChecked
´ItemIds() pobrać elementy listy zaznaczone przez użytkownika. Odpowiada
to mniej więcej przechowywaniu odwzorowania z wartościami logicznymi w opi-
sanej wersji aplikacji. Tryb wyboru można też ustawić programowo. Służy do
tego metoda ListView.setChoiceMode. W tym alternatywnym podejściu stan jest
przechowywany tylko w widokach (bez używania modelu), technika ta ma jednak
ograniczone zastosowania.
Istnieje wiele rodzajów adapterów. Z niektórymi z nich zetkniesz się w dal-
szych rozdziałach. Na razie wystarczy wiedzieć, jak korzystać z adapterów i jak
używać ich do tworzenia bardziej elastycznych projektów przez oddzielenie
danych od ich reprezentacji widocznej na ekranie. Ponadto prawdopodobnie
zauważyłeś, że widoki list — choć przydatne — bywają dość skomplikowane.
To dlatego dalej omawiamy pewne ogólne wskazówki, jednak najpierw pokrótce
pokazujemy, jak dodać do listy nagłówek i stopkę.

0 TECHNIKA 3. Widoki nagłówka i stopki

Widoki list doskonale nadają się do obsługi dużych zbiorów danych, jednak
sama możliwość przewijania nie zawsze zapewnia odpowiednią wygodę. Zanim
użytkownik dotrze do końca listy, może rozboleć go kciuk. Wygodniejszy jest
na przykład przycisk „wróć do początku” na końcu listy. Dzięki niemu użytkow-
nik nie musi przewijać setek tytułów, aby przejść na początek zbioru. Lista obej-
muje jednak tytuły filmów, którym odpowiadają widoki tekstowe, a nie przyciski.
162 ROZDZIAŁ 4. Precyzja co do piksela

Potrzebny jest element listy, który wygląda i działa inaczej niż zwykłe pozycje.
Adapter potrafi jednak tworzyć tylko jeden rodzaj elementów, reprezentujący
filmy. Wygląda na to, że mamy kłopot.
PROBLEM
Na początku lub końcu listy potrzebne są specjalne elementy, przewijane wraz
z zawartością listy jak normalne pozycje, jednak o zupełnie innym układzie i dzia-
łaniu.
ROZWIĄZANIE
Rozwiązaniem problemu są dostępne w Androidzie widoki nagłówka i stopki.
Możesz ustawić je za pomocą metod ListView.addHeaderView i ListView.addFooter
´View. Aby pokazać działanie rozwiązania, tworzymy przycisk Wróć na począ-
tek w aplikacji MyMovies. Najpierw należy zdefiniować dla widoku stopki układ
(list_footer.xml) obejmujący pojedynczy przycisk.
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="@android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:text="Wróć na początek"
android:onClick="backToTop"
/>

Zauważ, że w układzie widoku stopki zastosowano atrybut onClick. Jest to infor-


macja dla Androida, że ma znaleźć w aktywności publiczną metodę backToTop,
przyjmującą jako parametr jeden obiekt klasy View. Jest to wygodny sposób na
powiązanie metody obsługi kliknięcia z komponentem w pliku XML (nie trzeba
pisać szablonowego kodu w celu bezpośredniego określenia metody obsługi).
Wspomniana metoda to wywołanie zwrotne przewijające listę. Układ trzeba roz-
winąć do klasy, aby umieścić w nim obiekt klasy Button. Układ należy też ustawić
jako widok stopki dla listy (odbywa się to w metodzie onCreate aktywności).
public void onCreate(Bundle savedInstanceState) {
...
Button backToTop =
(Button) getLayoutInflater().inflate(R.layout.list_footer, null);
backToTop.setCompoundDrawablesWithIntrinsicBounds(getResources()
.getDrawable(android.R.drawable.ic_menu_upload), null, null,
null);
listView.addFooterView(backToTop, null, true);
...
}

W kodzie tym nie ma nic niezwykłego. Nowy kod znajduje się w środkowej części
metody onCreate z listingu 4.7, bezpośrednio po deklaracji ListView listView =
getListView(). Dzięki temu układ jest dołączany w momencie tworzenia aktywno-
0 TECHNIKA 3. Widoki nagłówka i stopki 163

ści ListActivity oraz ustawiany przed zadeklarowaniem i skonfigurowaniem


adaptera. Aby zobaczyć, jak wygląda rozwiązanie, przyjrzyj się rysunkowi 4.8, na
którym widać listę z przyciskiem na dole.

Rysunek 4.8. Z wykorzystaniem widoku stopki


dodaliśmy przycisk, który pozwala wrócić
na początek listy. Ikonę przycisku można
ustawić przy użyciu metody
setCompoundDrawableWithIntrinsicBounds

Do przycisku dodaliśmy też ikonę (wykorzystaliśmy ikonę strzałki skierowanej


w górę z frameworku). Możliwe, że zastanawiasz się, ile widoków nagłówków
i stopek możesz dodać — nie obowiązują tu żadne ograniczenia. Elementy te są
umieszczane jeden pod drugim i pojawiają się przed elementami listy (widoki
nagłówków) lub po nich (widoki stopek).
OMÓWIENIE
Widoki nagłówków i stopek są przydatne do wyświetlania informacji, które nie
są danymi z listy. Ze względu na to, że elementy te trzeba traktować inaczej niż
zwykłe pozycje listy, należy zwrócić uwagę na pewne niuanse. Wszystkie widoki
nagłówków i stopek musisz dołączyć przed dodaniem (za pomocą metody List
´View.setAdapter) danych do listy. W przeciwnym razie zostanie zgłoszony
wyjątek. Tak więc widoki nagłówków i stopek są mało elastyczne, ponieważ nie
można dodać ich lub usunąć w dowolnym momencie.
Ponadto warto pamiętać, że choć widoki nagłówków i stopek wyglądają jak
normalne elementy listy, nie są reprezentacją danych z adaptera, dlatego nie
odpowiadają im zapisane w nim obiekty. Oznacza to także, że jeśli chcesz zliczyć
elementy widoczne dla użytkownika, nie możesz zakładać, że metoda Adapter.
´getCount zwróci prawidłowe dane. Metoda ta nie uwzględnia widoków nagłów-
ków lub stopek. Kiedy korzystasz z nagłówków i stopek, pamiętaj o asymetrii
między widokiem listy a powiązanym z nim adapterem.
164 ROZDZIAŁ 4. Precyzja co do piksela

Dochodzimy do pierwszego ważnego punktu procesu rozwijania aplikacji


MyMovies. Utworzyliśmy aplikację, która wyświetla 100 pierwszych filmów
z bazy IMDB, a użytkownik może zaznaczyć, które tytuły posiada w swojej kolek-
cji. Widok listy daje dużo możliwości i wiesz już, jak z niego korzystać — jak się
z tym czujesz? Przyznajemy, że widok ListView jest skomplikowany i używanie
go związane jest z wieloma pułapkami. Kiedy będziesz tworzył bardziej złożone
układy elementów list, prawdopodobnie natrafisz na pewne problemy. Dlatego
w tabeli 4.4 przedstawiono wybrane pułapki związane z widokiem ListView
i odpowiednie rozwiązania. Pewnego dnia mogą Ci się przydać!
Tabela 4.4. Ogólne wskazówki związane z widokiem ListView

Pułapka związana
Rozwiązanie
z widokiem ListView
Nie używaj opcji Nigdy nie używaj opcji wrap_content dla atrybutu height widoku listy.
wrap_content. Lista jest kontenerem przeznaczonym do przewijania i z definicji
ma nieograniczoną długość, dlatego zawsze należy zezwalać
obiektom klasy ListView na wypełnianie elementu nadrzędnego
(opcja fill_parent) lub na rozwijanie się w inny sposób (na przykład
opcja layout_weight).
Uważaj na elementy list Ogólnie kliknięcie pozycji listy powoduje zgłoszenie zdarzenia
możliwe do kliknięcia. kliknięcia dla tego elementu (a konkretnie dla widoku lub kontenera,
który jest elementem głównym układu elementu listy). Umieszczenie
przycisku w układzie elementu powoduje, że zdarzenia są zgłaszane
dla przycisku, a nie dla elementu. Oznacza to, że można kliknąć
przycisk, natomiast samego elementu listy nie można ani aktywować,
ani kliknąć. Problem można częściowo rozwiązać przez wywołanie
metody ListView.setItemsCanFocus(false) . Pozwala
to przynajmniej aktywować cały element listy i wyróżnić
go po zaznaczeniu. Jednak kliknięcia trzeba obsługiwać dla
poszczególnych elementów w ich układzie.
Zwracaj uwagę na Widoki list mogą znacznie pogarszać wydajność aplikacji. Metoda
wydajność metody getView adaptera służy do wyświetlania elementów list i jest często
getView. wywoływana. Powinieneś unikać kosztownych zadań, takich
jak rozwijanie widoków do klas, a przynajmniej zapisywać wyniki
takich operacji w pamięci podręcznej. Wielokrotnie wykorzystuj
widoki i rozważ stosowanie przedstawionego wcześniej wzorca
ViewHolder.

Pora przejść do tematu, który często budzi obawy programistów, a jednocześnie


jest niezwykle istotny ze względu na sukces aplikacji mobilnych. Chodzi o wygląd
i styl. Nie omawiamy tu projektu graficznego. Jeśli czytasz książkę o programo-
waniu, prawdopodobnie nie płacą Ci za jego przygotowanie. Mimo to powinieneś
wiedzieć, jak wykorzystać projekt w aplikacji. Implementowanie niestandardo-
wych projektów w Androidzie wymaga wiele pracy programistycznej. Dlatego
w kilku następnych podrozdziałach dowiesz się, jak pisać wysoce niestandar-
dowe interfejsy użytkownika na Android. Pora „stuningować” platformę!
4.5. Stosowanie motywów i stylów 165

4.5. Stosowanie motywów i stylów


Nie ma co ukrywać — typowe aplikacje na Android nie są specjalnie atrakcyjne.
W wersji Android 3.0 (Honeycomb) i na tabletach sytuacja wygląda znacznie
lepiej, jednak zwykłe programy na starszych wersjach platformy wizualnie pozo-
stawiają sporo do życzenia. Na szczęście firma Google zapewniła programistom
narzędzia niezbędne do uatrakcyjnienia interfejsów aplikacji. Są to bardzo liczne
atrybuty widoków, które można definiować lub zmieniać. Jak zapewne już podej-
rzewasz, korzystanie z tych atrybutów może być żmudne i powtarzalne, dlatego
firma Google dodała do Androida silnik motywów. Omawiamy go tu na podsta-
wie dwóch technik.

4.5.1. Nadawanie stylu aplikacjom


Zacznijmy od sprostowania pewnych błędnych założeń dotyczących motywów
i stylów Androida. To prawda, można tworzyć niestandardowe motywy aplikacji
do pobrania przez użytkowników, jednak motywy Androida są przede wszystkim
narzędziem dla programistów, a nie dla odbiorców końcowych. Autorzy wielu
aplikacji, na przykład przeglądarki Firefox, umożliwiają użytkownikom tworze-
nie własnych motywów i udostępnianie ich innym osobom. Jest tak, ponieważ
silnik motywów Firefoksa mogą stosować także użytkownicy końcowi, a nie tylko
programiści przeglądarki. Z motywami Androida zwykle jest inaczej — rozwi-
janie ich ściśle wiąże się z pisaniem i wdrażaniem samej aplikacji. Nie istnieje
mechanizm, który pozwala zmienić motyw aplikacji (chyba że programista wbu-
dował taką możliwość w program).
Czym są motywy i style w Androidzie? Zacznijmy od odpowiedzi na pierwszą
część pytania.

0 TECHNIKA 4. Stosowanie i pisanie stylów

Warto wspomnieć, że w zasadzie można utworzyć bardzo elegancką aplikację


bez napisania choćby jednej definicji stylu. Jest to możliwe. Czy jednak chcesz
to robić? Aby zrozumieć problem, przyjrzyj się poniższej definicji widoku.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:text="Witaj, widoku tekstowy!"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#CCC"
android:background="@android:color/transparent"
android:padding="5dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

Panuje tu chaos. W tej definicji widoku zapisano wiele informacji wpływających


na jego wygląd. Czy należy umieszczać je w pliku układu? W układach liczy się
166 ROZDZIAŁ 4. Precyzja co do piksela

struktura, a nie wygląd. Ponadto co zrobić, jeśli we wszystkich widokach teksto-


wych aplikacja ma używać czcionki o tej samej wielkości? Czy chcesz określać
tę wielkość w każdym widoku tekstowym? Jeśli zechcesz zmienić czcionkę,
będziesz musiał zmodyfikować kod wszystkich widoków tekstowych w aplikacji.
PROBLEM
Modyfikowanie atrybutów widoków w układach (a zwłaszcza atrybutów doty-
czących wyglądu) prowadzi do zaśmiecania i powielania kodu oraz ogólnie spra-
wia, że nie można go ponownie wykorzystać.
ROZWIĄZANIE
Kiedy zauważysz, że stosujesz kilka powiązanych atrybutów bezpośrednio do
widoku, zastanów się nad użyciem stylów. Style są zaskakująco prostym mecha-
nizmem.
DEFINICJA. Styl w Androidzie to zestaw atrybutów widoku połączonych
w odrębny zasób. Style standardowo definiuje się w pliku res/values/
styles.xml.
Po utworzeniu niestandardowego stylu widoku tekstowego można zdefiniować
modyfikowane atrybuty raz (w pliku styles.xml) w następujący sposób:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MyCustomTextView" parent="@android:style/Widget.TextView">
<item name="android:textSize">14sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">#CCC</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:padding">5dip</item>
</style>
</resources>

Jak widać, wszystkie atrybuty związane ze stylami pobraliśmy z definicji widoku


i umieściliśmy je w elemencie <style>. Styl definiuje się za pomocą elementów
stylu. Każdy z nich wskazuje na atrybut widoku, do którego dany styl jest stoso-
wany. Style można też tworzyć przez dziedziczenie. Tu nowy styl dziedziczy po
stylu domyślnym Widget.TextView, co jest odpowiednikiem skopiowania wszystkich
atrybutów ze stylu bazowego do nowego stylu. Wszystkie atrybuty ponownie
zdefiniowane w nowym stylu są używane zamiast atrybutów o tych samych
nazwach ze stylu bazowego. Styl można zastosować do widoku w następujący
sposób:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:text="Witaj, widoku tekstowy!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/MyCustomTextView"
/>
0 TECHNIKA 5. Stosowanie i pisanie motywów 167

Warto zauważyć, że zastosowano tu atrybut style bez przedrostka android:.


Jest to celowe. Atrybut style jest globalny. Nie zdefiniowano go w XML-owej
przestrzeni nazw android. Należy pamiętać, że style to zestawy połączonych razem
atrybutów widoków. Style nie są traktowane jak typy. Jeśli zdefiniujesz w stylu
atrybuty, a następnie zastosujesz go do widoku, który nie ma tych atrybutów, to
styl ten nie zadziała, jednak nie pojawi się błąd. To programista odpowiada za
prawidłowe opracowanie i zastosowanie stylów.
OMÓWIENIE
Style mają rozwiązywać dwa często występujące problemy projektowe. Otóż
umieszczanie w jednym pliku kodu określającego wygląd i kodu definicji struktury
nie pozwala odpowiednio rozdzielić tych części aplikacji. Choć to programista
tworzy układ ekranu, stylami zwykle zajmuje się osoba, która nie pisze kodu pro-
gramu. Nieustanne modyfikowanie tych samych plików z kodem źródłowym to
proszenie się o konflikty w czasie ich przesyłania do systemu kontroli kodu źró-
dłowego (korzystasz z takiego systemu, prawda?). Nawet jeśli jedna osoba odpo-
wiada za oba wspomniane aspekty, dobry podział zadań pomaga zachować przej-
rzystość i czytelność kodu oraz ułatwia jego konserwację.
Skoro już jesteśmy przy konserwacji kodu — z tym zagadnieniem związany
jest drugi problem. Definiowanie atrybutów wyglądu bezpośrednio w pliku XML
widoku (lub jeszcze gorzej — w kodzie aplikacji) sprawia, że nie można ich
ponownie wykorzystać. Oznacza to, że te same atrybuty stylu trzeba kopiować
do innych widoków tego samego rodzaju. Prowadzi to do powielania i nadmier-
nego rozrastania się kodu. Już wcześniej wspomnieliśmy, że jest to niekorzystne,
ponieważ narusza zasadę DRY. Style w Androidzie, podobnie jak elementy <merge>
i <include>, pomagają zachować zgodność z tą zasadą. Wspólne atrybuty widoków
można umieścić w zasobie stylu, a następnie zastosować go do wielu widoków
i przy tym konserwować kod w jednym miejscu.
Są też inne pytania. Dla jakich elementów można określić styl i po jakich
gotowych stylach można dziedziczyć? W stylach można używać wszystkich
elementów zdefiniowanych w klasach android.R.styleable i android.R.attr.
Elementów tych można używać jako wartości atrybutu name elementów stylów.
Istniejące style są zdefiniowane w klasie android.R.style, dlatego każdy określony
w niej styl można podać jako wartość atrybutu parent stylu, a nawet zastosować
go bezpośrednio do widoku. Podkreślenia w nazwach atrybutów w klasie R odpo-
wiadają jak zawsze kropkom w kodzie widoków. Tak więc android.R.style.
´Widget_TextView_SpinnerItem to android:style/Widget.TextView.SpinnerItem.
Wiesz już, czym są style Androida. Pora przejść do motywów.

0 TECHNIKA 5. Stosowanie i pisanie motywów

Potrafisz już wyodrębnić wspólne atrybuty widoku do stylów. Nadal jednak


występują powtórzenia, co narusza zasadę DRY. Po zdefiniowaniu stylu dla wido-
ków tekstowych nadal trzeba ręcznie stosować styl do każdego widoku TextView.
168 ROZDZIAŁ 4. Precyzja co do piksela

Jest to niezgodne z zasadą DRY. Jej naruszenie nie jest może poważne, ale jed-
nak występuje. Pojawia się też nowe pytanie. Co się stanie, jeśli nie zastosujesz
stylu do jednego z widoków?
PROBLEM
Łączenie atrybutów widoków w style jest przydatne, ale to tylko połowa roz-
wiązania. Nadal trzeba zastosować style do wszystkich docelowych widoków.
Powinno się to odbywać automatycznie.
ROZWIĄZANIE
Kompletne rozwiązanie obejmuje, jak pewnie zgadłeś, motywy. Na szczęście łatwo
jest je opisać. Motywy to style. Naprawdę — to tak proste. Jedyna różnica między
motywem a stylem (takim jak przedstawiony w poprzedniej technice) polega na
tym, że motywy dotyczą aktywności lub całej aplikacji (czyli wszystkich aktywności
z programu), a nie pojedynczych widoków. Różnica związana jest więc z zakresem,
a nie ze sposobem działania lub nawet strukturą rozwiązania.
DEFINICJA. Motyw w Androidzie to styl stosowany albo do jednej aktyw-
ności, albo do wszystkich aktywności (wtedy jest globalnym motywem
aplikacji).
Motywy to style, zatem działają i są definiowane w dokładnie taki sam sposób —
za pomocą znacznika <style>. Ponieważ stosuje się je do aktywności, przyjmują
inne atrybuty niż style kontrolek. Atrybuty stylu przeznaczone do użytku w defi-
nicjach motywów można rozpoznać po przedrostku Theme_ w pliku android.R.
´styleable.
Przejdźmy dalej i zastosujmy motyw do aplikacji MyMovies. Style opisaliśmy
na podstawie widoku TextView. To dobry wybór, ponieważ widok ten przyjmuje
wiele różnych atrybutów (dalej pokazujemy inny, jeszcze lepszy sposób na ograni-
czenie liczby atrybutów widoku TextView). Jednak w przykładowej aplikacji nie
znajduje się zbyt wiele elementów TextView, dlatego tworzenie motywów dla
nich to przesada. Zamiast tego spróbujmy zwiększyć atrakcyjność wizualną listy
filmów. Dodajmy do aplikacji rysunek tła. Odpowiednia będzie grafika związana
z filmami. Powinna stapiać się z listą, przez którą może przebijać się tło. Ponadto
warto wprowadzić także kilka mniejszych zmian, na przykład wyświetlać suwaki
szybkiego przewijania. Na listingu 4.9 pokazano, jak uzyskać pożądane efekty za
pomocą motywów i stylów.

Listing 4.9. Plik z definicjami stylu i motywu aplikacji MyMovies

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


<resources>
<style name="MyMoviesTheme"
parent="@android:style/Theme.Black">
<item name="android:listViewStyle">@style/MyMoviesListView</item>
<item name="android:windowBackground">@drawable/film_bg</item>
</style>
0 TECHNIKA 5. Stosowanie i pisanie motywów 169

<style name=" MyMoviesListView"


parent="@android:style/Widget.ListView">
<item name="android:background">#A000</item>
<item name="android:fastScrollEnabled">true</item>
<item name="android:footerDividersEnabled">false</item>
</style>
</resources>

Definicja motywu przedstawiona na listingu 4.9 (w której wykorzystano także


element style) powoduje zastosowanie niestandardowych stylów do wszystkich
egzemplarzy klasy ListView w aplikacji i ustawienie określonego tła okna .
W niestandardowym stylu widoku ListView zdefiniowane są atrybuty wspólne dla
wszystkich widoków list w danej aplikacji .
Motyw aplikacji jest już zdefiniowany, jednak jeszcze go nie zastosowaliśmy.
Warto przypomnieć, że motywy można stosować do pojedynczych aktywności
lub całych aplikacji (wszystkich aktywności). Dlatego trzeba poinformować
Android, do czego ma przypisać styl. Motywy stosuje się w pliku manifestu za
pomocą atrybutu android:theme. Jeśli chcesz zastosować motyw do jednej aktyw-
ności, powinieneś podać w atrybucie jej element. W przeciwnym razie należy
przypisać do atrybutu element aplikacji.
<application android:theme="@style/MyMoviesTheme" ...>
...
</application>

Na rysunku 4.9 pokazano wygląd aplikacji MyMovies po dodaniu do niej


stylów.
Tło okna jest widoczne przez półprzezroczyste
tło widoku listy. Ponadto po prawej stronie listy
znajduje się suwak szybkiego przewijania. Można
go złapać, aby szybko przewinąć elementy; po
zakończeniu tej operacji suwak przestaje być
widoczny.
OMÓWIENIE
Podobnie jak można zdefiniować atrybuty widoku
w pliku XML i kodzie programu, można też
programowo stosować motywy do aktywności.
Służy do tego metoda Activity.setTheme. Jednak
technika ta nie jest zalecana. Ogólnie jeśli coś
można zrobić w XML-u, dobrą praktyką jest
korzystanie z tego podejścia zamiast z kodu
aplikacji. Ponadto wywołanie metody setTheme Rysunek 4.9. Ekran tytułowy
działa tylko przed rozwinięciem układu aktyw- aplikacji MyMovies
po zastosowaniu stylów.
ności do klasy. Jeśli wywołasz ją później, style Zauważ, że dodano rysunek
z motywu nie zostaną użyte. Oznacza to też, że tła i suwak szybkiego
przewijania
170 ROZDZIAŁ 4. Precyzja co do piksela

nie można zmieniać motywów „na żywo” (na przykład w reakcji na kliknięcie przy-
cisku). Konieczne jest ponowne uruchomienie aktywności.
To kończy omówienie podstaw definiowania i używania stylów oraz motywów.
Warto jednak napisać także o kilku innych kwestiach. Pamiętasz, jak wspo-
mnieliśmy o tym, że widoki ListView są skomplikowane i że wrócimy do nich?
Robimy to w tym miejscu. Z określaniem stylu widoków ListView związana jest
nieprzyjemna pułapka, w którą wpadają prawie wszyscy programiści rozpoczy-
nający przygodę z Androidem. Warto raz na zawsze rozbroić tę pułapkę.

0 TECHNIKA 6. Określanie stylu tła widoku ListView

ListView to skomplikowana kontrolka. Jej złożoność czasem przeszkadza przy


modyfikowaniu wyglądu listy. Nie byliśmy do końca uczciwi, kiedy przedsta-
wiliśmy kod stylu widoku listy na listingu 4.9. Brakuje na nim ustawienia, które
umożliwia prawidłowe wyświetlanie stylu. Zastosowanie niestandardowego tła
lub (tak jak w przykładzie) prześwitującego tła okna albo innego widoku może
prowadzić do wizualnych anomalii, takich jak migotanie kolorów w czasie prze-
wijania lub po kliknięciu listy. Powiązane jest z tym inne zagadnienie — jeśli
ustawiasz tło na przezroczysty kolor i oczekujesz, że pod listą zobaczysz inną
kontrolkę, rozczarujesz się, ponieważ Android i tak wyświetli domyślne czarne tło.
PROBLEM
Stosujemy niestandardowe tło (kolor lub rysunek) dla widoku ListView, jednak
po wyświetleniu listy nie otrzymujemy pożądanych efektów lub pojawiają się
wizualne artefakty.
ROZWIĄZANIE
Winą za problemy można obarczyć mechanizm optymalizacji wyświetlania widoku
ListView stosowany w czasie wykonywania programu. Ponieważ wskazówką infor-
mującą użytkowników o możliwości przewijania są zanikające krawędzie (przezro-
czystość), a kolor dla prześwitujących obszarów trudno jest obliczyć, w widokach
listy używana jest sugerowana barwa (domyślnie jest to kolor tła okna podany
w motywie). Na jej podstawie system przygotowuje gradient imitujący pożądany
efekt. Zwykle widok listy jest wyświetlany za pomocą domyślnego schematu
kolorów. Wtedy opisana optymalizacja jest skuteczna. Jednak przy stosowaniu
niestandardowych kolorów tła mogą wystąpić wspomniane wcześniej anomalie.
Aby rozwiązać problem, trzeba poinformować Android o tym, który kolor
powinien zastosować jako sugerowaną barwę. Służy do tego atrybut android:cache
´ColorHint. Używa się go tak:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<style name="MyMoviesListView" parent="@android:style/Widget.ListView">
<item name="android:background">#A000</item>
<item name="android:cacheColorHint">#0000</item>
0 TECHNIKA 6. Określanie stylu tła widoku ListView 171

...
</style>
</resources>

Prawidłowe ustawienie atrybutu cacheColorHint (lub wyłączenie sugerowanego


koloru przez ustawienie go na przezroczystość) rozwiązuje wszelkie dziwne pro-
blemy, na które można natrafić przy korzystaniu z listy o niestandardowym
kolorze tła.
OMÓWIENIE
Jaką wartość przypisać do sugerowanego koloru? Jeśli tło listy ma jednolity
kolor, użyj tej barwy jako sugerowanej. Jeżeli chcesz, aby tło okna prześwitywało,
lub zamierzasz zastosować niestandardową grafikę, musisz ustawić sugerowany
kolor na przezroczystość (#0000 lub android:color/transparent). To już wszystko.
Jest to stosunkowo mało znany problem, jednak należy o nim pamiętać, kiedy
korzysta się z widoków ListView z niestandardowym tłem.
Wyjaśnienie działania atrybutu cacheColorHint to już prawie ostatni punkt
omówienia stylów i motywów. Jednak przed przejściem do dalszych zagadnień
chcemy się jeszcze podzielić ciekawostkami na temat stylów w Androidzie.

4.5.2. Przydatne ciekawostki na temat stylów


Gotów na jeszcze bardziej zaawansowane informacje o stylach aplikacji? Wiesz
już, jak tworzyć motywy i style oraz jak je stosować. Jednak przygotowaliśmy też
dodatkowe sztuczki i kruczki, które uzupełniają omówienie. W dalszych akapi-
tach opisujemy zagadnienia, które do tej pory pomijaliśmy. Nie przedstawiamy
ich w żadnej szczególnej kolejności. Każda z opisanych sztuczek ułatwi Ci pracę
ze stylami. Przede wszystkim rozwiewamy wątpliwości związane z wartościami
reprezentującymi kolory, pokazujemy, jak traktować wygląd tekstu, a także przed-
stawiamy rzadko spotykane, ale przydatne notacje związane ze stylami.
WARTOŚCI REPREZENTUJĄCE KOLORY
Przy pracy ze stylami często określa się wartości reprezentujące kolory. Można
podać je albo bezpośrednio (w notacji szesnastkowej), albo przez wskazanie zasobu
koloru. Tak, także kolory mogą być zasobami! Każda wartość koloru w XML-u
jest definiowana w notacji szesnastkowej i identyfikowana na podstawie przed-
rostka #. Warto pokrótce omówić takie wartości. W Androidzie definiuje się je
w przestrzeni kolorów ARGB (ang. alpha, red, green, blue), w której każdemu
kolorowi odpowiada 32-bitowa liczba. Pierwszych osiem bitów określa wartość
kanału alfa (przezroczystość koloru), a pozostałe 24 bity reprezentują trzy kom-
ponenty koloru z ośmioma bitami na składowe czerwoną, zieloną i niebieską.
Ponieważ dla poszczególnych składowych można wykorzystać po osiem bitów,
wartości tych składowych należą do przedziału od 0 do 255 (od 00 do FF w notacji
szesnastkowej). Wartość #800000FF oznacza więc kolor niebieski o przezroczy-
stości 50%.
172 ROZDZIAŁ 4. Precyzja co do piksela

SKRÓCONE WARTOŚCI REPREZENTUJĄCE KOLORY. Kiedy każdy


kanał koloru jest reprezentowany przez dwie identyczne cyfry szesnast-
kowe, można zastosować skrócony łańcuch szesnastkowy, w którym każdej
parze cyfr odpowiada ich jedno wystąpienie. Kolory #FFFFFFFF i #AABBCCDD
można na przykład skrócić do wersji #FFFF i #ABCD. Ponadto zawsze można
pominąć kanał alfa — wtedy kolor jest zupełnie nieprzezroczysty. War-
tość #FFFF można więc skrócić do #FFF.
Wprowadzanie wartości reprezentujących kolory bywa żmudne. Korzystanie
z takich wartości prowadzi do powtórzeń, co jest niekorzystne. Co gorsza,
podane wartości mogą być nieintuicyjne, chyba że dobrze radzisz sobie z odwzo-
rowywaniem wartości szesnastkowych na przestrzeń kolorów. Dlatego wartości
zwykle definiuje się tylko raz — jako zasób koloru, który można wskazać za
pomocą czytelnego identyfikatora. Aby uzyskać taki efekt, utwórz w katalogu
res/values plik o nazwie colors.xml i dodaj do niego następujący kod.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="translucent_blue">#800000FF</color>
</resources>

Teraz możesz wskazywać ten kolor w widokach i stylach jako @color/translucent_


´blue. Warto zauważyć, że w Androidzie pewne kolory są zdefiniowane w ten
sposób. Często używanym kolorem tego rodzaju jest android:color/transparent,
odpowiadający wartości #00000000. W kodzie aplikacji można też wykorzystać
definicje z klasy Color, jednak nie można podawać ich w XML-u.
Ostatnia rzecz na temat kolorów. Kolory zdefiniowane w pokazany tu spo-
sób można w Androidzie wykorzystać jako obiekty graficzne. Nie omówiliśmy
jeszcze takich obiektów (wspomnieliśmy tylko o nich w rozdziale 2.). Warto
wiedzieć, że wartości reprezentujące kolory można też przypisywać do atrybu-
tów, których wartościami są obiekty graficzne (dotyczy to na przykład atrybutów
tła lub selektora listy). Kolory stosowane jako obiekty graficzne dają dużo moż-
liwości. Opisujemy to w podrozdziale 4.6.
WYGLĄD TEKSTU
Wcześniej wspomniano, że przy tworzeniu stylów tekstowych istnieje lepsza tech-
nika niż rozwijanie ich od podstaw. Można wykorzystać wygląd tekstu z Androida.
Wygląd tekstu (ang. text appearance) to styl obejmujący elementy stosowane
dla widoków TextView z aplikacji. Ponadto przesłonięcie domyślnych stylów ma
natychmiastowy wpływ na wszystkie style tekstowe z aplikacji. Jeśli chcesz w całej
aplikacji zmienić domyślny styl tekstu w taki sposób, aby czcionka była czer-
wona i pogrubiona, możesz zastosować następujący kod:
<style name="MyTheme">
<item name="android:textAppearance">@style/MyTextAppearance</item>
</style>
0 TECHNIKA 6. Określanie stylu tła widoku ListView 173

<style name="MyTextAppearance">
<item name="android:textColor">#F00</item>
<item name="android:textStyle">bold</item>
</style>

W motywach jest dostępnych wiele atrybutów wyglądu tekstu, na przykład


textAppearance (dla domyślnego tekstu), textAppearanceButton (dla napisów na
przyciskach) i textAppearanceInverse (dla wyróżnionego tekstu). Prawdopodob-
nie zastanawiasz się, jaka jest różnica między tym podejściem a zdefiniowa-
niem — tak jak wcześniej — domyślnego stylu dla widoku TextView. Różnice są
subtelne, ale istotne. Po pierwsze, style zdefiniowane jako wygląd tekstu są sto-
sowane do wszystkich widoków TextView, w tym podklas, natomiast atrybut
textViewStyle nie wpływa na widoki tekstowe takie jak Button lub EditText (są
to podklasy klasy TextView). Po drugie, atrybut textAppearance można zastoso-
wać do motywu i pojedynczego widoku TextView (a tym samym i do definicji
stylu dla takiego widoku). Pozwala to połączyć wspólne style tekstowe i zastoso-
wać je do widoków tekstowych różnego rodzaju. Jest to następny poziom zapew-
niania zgodności stylów z zasadą DRY.
W każdym razie jeśli chcesz określać styl tekstu w aplikacji, warto zrobić to
za pomocą wyglądu tekstu. Możesz zastosować dziedziczenie po domyślnym
wyglądzie tekstu z Androida i zmienić tylko odpowiednie ustawienia.
SPECJALNE WARTOŚCI STYLU
Wiesz już, jak ponownie wykorzystać atrybuty dotyczące stylu przez łączenie
ich w style i z zastosowaniem mechanizmu dziedziczenia. Poznałeś też notację @
służącą do wskazywania istniejących zasobów. Technika ta działa dobrze, jeśli
chcesz wskazać cały zasób. Co jednak zrobić, aby użyć jednej wartości? Zastanów
się nad stylem tekstu. Załóżmy, że chcesz zmienić kolor odnośników na barwę
używaną w Androidzie dla zwykłego tekstu, jednak nie znasz jej wartości. Ponadto
podstawowy kolor tekstu nie jest dostępny jako zasób koloru, ponieważ zmienia
się w zależności od motywu.
Rozwiązaniem jest notacja ?, która — podobnie jak zapis @ — działa tylko
w XML-u. Możesz wykorzystać ją do wskazywania elementów stylu z właśnie
zastosowanego motywu z wykorzystaniem ich nazw. Jeśli chcesz ustawić kolor
odnośników w aplikacji na domyślny kolor tekstu, możesz zrobić to tak:
<style name="MyTheme">
<item name="android:textColorLink">?android:attr/textColorPrimary</item>
</style>

Ostatnią rzeczą, o jakiej warto wspomnieć, jest wartość @null. Możesz ją zasto-
sować, kiedy chcesz usunąć domyślnie ustawioną wartość ze stylu bazowego.
Technika ta rzadko jest potrzebna, ale przydaje się między innymi do usuwania
domyślnie ustawionych obiektów graficznych. Android ustawia na przykład atry-
but windowBackground na wartość domyślną, jeśli jednak tło okna jest zawsze zasło-
nięte widokami aplikacji, można je usunąć przez przypisanie do atrybutu
174 ROZDZIAŁ 4. Precyzja co do piksela

wartości @null. Prowadzi to do niewielkiego wzrostu wydajności, ponieważ obecnie


Android nie potrafi optymalizować całkowicie zasłoniętych widoków przez pomi-
nięcie ich wyświetlania (w przyszłych wersjach systemu może się to zmienić).
Style to złożone zagadnienie. Ponadto łatwo jest pomylić motywy ze stylami.
Mamy nadzieję, że udało nam się rozwiać większość Twoich wątpliwości. Zaczę-
liśmy od pokazania definicji stylów i sposobów stosowania ich do widoków.
Dalej opisaliśmy globalne przypisywanie stylów do aplikacji za pomocą motywów,
a nawet wyjaśniliśmy pewne problemy dotyczące stylów tła w widokach list.
Zachęcamy do samodzielnych eksperymentów ze stylami widoków. To najlepszy
sposób na zrozumienie tego zagadnienia. Teraz pora przejść dalej i poznać
następny ważny aspekt frameworku do tworzenia interfejsu użytkownika
z Androida — obiekty graficzne.

4.6. Korzystanie z obiektów graficznych


Szczerze przyznajemy, że do tego miejsca pomijaliśmy zagadnienie obiektów
graficznych i bezwstydnie staraliśmy się zamiatać je pod dywan. Trudno jest
omówić wszystkie kwestie związane z interfejsem użytkownika bez opisania
obiektów graficznych. Jednak nie udało nam się Ciebie zwieść, prawda? Kilku-
krotnie zetknąłeś się już z obiektami graficznymi — rysunkami (bitmapami)
i kolorami. Czym dokładnie są takie obiekty?
DEFINICJA. Obiekt graficzny (ang. drawable) w Androidzie jest definio-
wany z wykorzystaniem klasy Drawable i reprezentuje element graficzny
wyświetlany na ekranie, który to element jednak w odróżnieniu od kon-
trolek zwykle nie umożliwia interakcji (wyjątkiem są obiekty graficzne
w postaci obrazów, umożliwiające zapisywanie grafiki rysowanej na
powierzchni ekranu).
Istnieją różne rodzaje obiektów graficznych — dla rysunków i kolorów, dla nie-
standardowych kształtów i linii, zmieniające się w zależności od stanu, a także
animowane.
Obiekty graficzne zasługują na odrębne omówienie, ponieważ dają dużo
możliwości i są wszechobecne w Androidzie. Są potrzebne niemal wszędzie —
jako tła, panele kontrolek, niestandardowe powierzchnie i ogólnie wszystkie
elementy związane z grafiką dwuwymiarową. W tym miejscu opisujemy tylko
najczęściej używane obiekty graficzne. Do obiektów graficznych wracamy w roz-
dziale 12. w kontekście wyświetlania grafiki dwu- i trójwymiarowej. Tu koncen-
trujemy się na obiektach ważnych ze względu na styl aplikacji.

4.6.1. Struktura obiektów graficznych


Obiekty graficzne zawsze znajdują się w katalogu res/drawables i jego odmianach
właściwych dla konfiguracji. Obiekty te mają dwa różne formaty — binarnych
plików graficznych i plików XML. Jeśli chcesz zastosować w aplikacji niestan-
0 TECHNIKA 7. Używanie obiektów graficznych w postaci kształtów 175

dardowy plik graficzny, wystarczy umieścić go we wspomnianym katalogu. Narzę-


dzie ADT wykrywa obiekty graficzne i generuje dla nich identyfikatory, które
można wskazywać w Javie za pomocą składni R.drawable.nazwa_pliku (podobnie
jak inne zasoby w Androidzie). W taki sam sposób można używać obiektów gra-
ficznych w formacie XML, jednak najpierw trzeba napisać dla nich odpowiedni
plik, co wyjaśniamy dalej.
Obiekt graficzny umieszczony w odpowiednim katalogu można wykorzystać
w aktywności przez wywołanie metody getResources().getDrawable(id). Jednak
częściej takie obiekty stosuje się w definicjach stylów i widoków, gdzie używa się
ich jako tła lub innych graficznych części kontrolki (choć na podstawie kodu często
trudno się tego domyślić). Przypomnijmy definicję stylu dla listy z listingu 4.9.
<style name="MyMoviesListView" parent="@android:style/Widget.ListView">
<item name="android:background">#A000</item>
<item name="android:fastScrollEnabled">true</item>
<item name="android:footerDividersEnabled">false</item>
</style>

W kodzie zastosowano obiekt graficzny. Potrafisz stwierdzić, w którym miejscu?


Przyznajemy, że trudno go zauważyć. To kolor tła listy. W niektórych atrybu-
tach można stosować reprezentujące kolory wartości tam, gdzie oczekiwany jest
zwykły obiekt graficzny (na przykład bitmapa). Podany kolor jest wtedy prze-
kształcany wewnętrznie na obiekt klasy ColorDrawable. Warto zauważyć, że
z pewnych przyczyn nie wszędzie można stosować tę technikę. Koloru nie można
na przykład przypisać do atrybutu android:windowBackground.
Ponadto w XML-u można definiować całe specjalne obiekty graficzne.
Wtedy elementem głównym jest zwykle nazwa klasy obiektu graficznego z pomi-
niętym członem Drawable. Aby zdefiniować na przykład obiekt ColorDrawable,
należy użyć znacznika <color>, tak jak we wcześniejszym podrozdziale (dalej
poznasz dwa wyjątki od tej reguły, które nieco komplikują pracę). Warto zauwa-
żyć, że nie wszystkie rodzaje obiektów graficznych można stosować w dowol-
nym miejscu. W selektorach listy nie można na przykład używać zwykłych
wartości reprezentujących kolor, ponieważ selektor ma kilka stanów, a jeden
obiekt graficzny określający kolor nie wystarcza do ich odzwierciedlenia. Co
ciekawe, można w tym kontekście zastosować zwykły rysunek.
Tworzenie niestandardowych obiektów graficznych niestety nie jest dobrze
udokumentowanym aspektem pakietu SDK. Tu omawiamy trzy rodzaje tych
obiektów (kształty, selektory i obiekty dziewięciopolowe) przydatne do określe-
nia stylów aplikacji.

0 TECHNIKA 7. Używanie obiektów graficznych w postaci kształtów

Czasem obrazy statyczne w formacie PNG lub JPEG nie dają wystarczająco
bogatych możliwości. Nie skalują się dobrze, jeśli wielkość obszaru ich wyświe-
tlania się zmienia. Na myśl od razu przychodzą dwa przykłady. Pierwszy z nich
176 ROZDZIAŁ 4. Precyzja co do piksela

dotyczy gradientów. Zwykle mają one jeden kolor na jednym końcu i przecho-
dzą w inną barwę. Zdefiniowanie gradientu jako statycznego rysunku prowadzi
do problemów po jego rozciągnięciu lub zmniejszeniu. Inny dobry przykład
związany jest z przerywanymi ramkami. Jeśli przerywana ramka jest zdefinio-
wana za pomocą rysunku tła z nałożonym na nie obramowaniem, długość przerw
dopasowuje się do zmian wielkości widoku. Co jednak zrobić, jeśli długość kre-
sek i przerw między nimi ma pozostać stała nawet przy dowolnej zmianie roz-
miaru widoku? W obu opisanych sytuacjach korzystne jest generowanie grafiki
w czasie wykonywania programu.
PROBLEM
Chcemy wyświetlać trudno skalowalną grafikę (na przykład gradienty bądź
wzorce) lub elementy, którymi łatwiej jest manipulować w czasie wykonywania
programu.
ROZWIĄZANIE
Jeśli natrafisz na jeden ze wspomnianych problemów, możesz wykorzystać
obiekty graficzne w postaci kształtu. Takie obiekty definiuje się deklaratywnie
i wyświetla dynamicznie. Można je zastosować w dowolnym miejscu, gdzie
dopuszczalne są obiekty graficzne (na przykład jako tło widoku).
Obiekty graficzne w postaci kształtu są wewnętrznie reprezentowane jako
klasy GradientDrawable i ShapeDrawable. W XML-u do ich definiowania zawsze
służy element <shape>. Pokazaliśmy już, jak w widoku listy zmienić rysunek selek-
tora na predefiniowany obiekt graficzny. Tym razem utwórzmy własny obiekt
graficzny bez posługiwania się aplikacjami graficznymi. Selektor z gradientem
dla aplikacji MyMovies wydaje się dobrym pomysłem. Przygotujmy taki selektor.
W katalogu res/drawable utwórz nowy plik list_selector.xml. Należy umieścić
w nim obiekt graficzny w postaci kształtu, odpowiadający nowemu selektorowi.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#AFFF"
android:endColor="#FFFF"
android:angle="0"/>
<stroke android:color="#CCC" android:width="1px" />
<corners android:radius="5px" />
</shape>

Kształt w obiekcie graficznym ustawiamy na rectangle (czyli prostokąt), ponie-


waż właśnie taki obiekt jest potrzebny. Tu można pominąć deklarację kształtu,
gdyż domyślnie stosowany jest prostokąt. W elementach podrzędnych zdefinio-
wane są cechy kształtu. Ustawiamy tu niestandardowy gradient tła (<gradient>),
wygląd linii ramki (<stroke>) i promień ramki (<corners>). Teraz zastosujmy
obiekt w stylu widoku listy.
0 TECHNIKA 7. Używanie obiektów graficznych w postaci kształtów 177

<style name="MyMoviesListView" parent="@android:style/Widget.ListView">


...
<item name="android:listSelector">@drawable/list_selector</item>
</style>

Jak widać, kształt list_selector można stosować


jak każdy inny obiekt graficzny. Wcześniejszą
wartość obiektu graficznego dla selektora listy
zastąpiliśmy referencją do nowego kształtu (gra-
dientu). Na rysunku 4.10 pokazano, jak po wpro-
wadzeniu zmian wygląda zaznaczony element
listy w aplikacji MyMovies.
Podoba Ci się to, co widzisz? Dopiero zaczy-
namy. Wkrótce się przekonasz, że istnieje wiele
innych opcji tworzenia obiektów graficznych
w postaci kształtów.
OMÓWIENIE
Obiekty graficzne w postaci kształtów to dosko-
nałe narzędzie do tworzenia eleganckich ele-
Rysunek 4.10. Selektor
mentów wizualnych bez konieczności używa- w widoku listy oparty
nia programu graficznego. Omawiane obiekty na niestandardowym elemencie
można modyfikować w kodzie źródłowym, a pro- graficznym w postaci kształtu.
Warto zauważyć, że użyliśmy
gramista ma nad tym pełną kontrolę. Czy to, co niestandardowego poziomego
słyszymy, to głosy oburzenia zespołu projektan- gradientu

tów? Tak, to oni odpowiadają za tworzenie ele-


gancko wyglądającej grafiki. Jeśli jednak chcesz
dodać obramowanie kontrolki lub potrzebujesz
dodatkowych możliwości, wykorzystaj paletę
kolorów opracowaną przez projektantów. Na tym
polega praca zespołowa!

Pułapki związane z selektorami list


W widokach list atrybut android:listSelector służy do określania koloru lub
rysunku używanego jako selektor listy. Android pozwala na wyświetlanie selek-
tora na dwa sposoby — za układem elementu listy lub przed nim. Zależy to od
wartości opcji android:drawListSelectorOnTop. Każde z tych podejść ma okre-
ślone wady i zalety, o których warto pamiętać. Domyślnie Android wyświetla selek-
tory za elementem listy. Dlatego wszystkie widoki w układzie elementu listy muszą
mieć przezroczyste tło (warunek ten spełnia większość niezmodyfikowanych wido-
ków z Androida). Jeśli jest inaczej, selektor zostaje zasłonięty widokiem. Jeżeli
w elemencie listy wyświetlasz grafikę (na przykład zdjęcie), nie masz wyboru.
Zdjęcie zasłania selektor, ponieważ zawsze jest nieprzezroczyste. Oznacza to, że
przy stosowaniu rysunków lub nieprzezroczystego tła warto wyświetlać selektor
na wierzchu. Dlatego selektor musi być przezroczysty. W przeciwnym razie zasła-
nia wszystkie widoki elementu listy. Warto o tym pamiętać w czasie projektowania
niestandardowych selektorów list.
178 ROZDZIAŁ 4. Precyzja co do piksela

Wspomnieliśmy już, że możesz tworzyć coś więcej niż pola i obramowania.


Obiekty graficzne w postaci kształtów mogą przybierać różne rozmiary i formy —
od prostokątów i owali po linie i pierścienie. W tabeli 4.5 znajduje się lista więk-
szości elementów używanych do definiowania obiektów graficznych w postaci
kształtów. Z uwagi na zwięzłość pomijamy niektóre rzadko stosowane atrybuty.
Tabela 4.5. Elementy do definiowania obiektów graficznych w postaci kształtów

Nazwa
Opis Atrybuty
elementu

<shape> Element główny. android:shape — rodzaj kształtu (rectangle,


oval, line, ring)
<gradient> Definiuje gradientowe android:type — rodzaj gradientu (linear, radial,
tło kształtu. sweep)
android:startColor — początkowy kolor gradientu
android:centerColor — opcjonalny trzeci
(środkowy) kolor gradientu
android:endColor — końcowy kolor gradientu
android:angle — kąt gradientu, jeśli typ to linear
android:centerX i android:centerY — lokalizacja
środkowego koloru, jeśli go określono
android:gradientRadius — promień gradientu,
jeśli typ to radial lub sweep
<solid> Przypisuje do kształtu android:color — kolor tła
nieprzezroczyste tło.
<stroke> Określa obramowanie android:color — kolor obramowania
kształtu.
android:width — grubość obramowania
android:dashGap — przerwy w przerywanej linii
android:dashWidth — grubość kresek w przerywanej
linii
<corners> Określa promień android:radius — promień dla wszystkich czterech
narożników kształtu. narożników
android:topLeftRadius,
android:topRightRadius,
android:bottomLeftRadius,
android:bottomRightRadius — promień każdego
z czterech narożników
<padding> Określa margines android:top, android:bottom, android:left,
wewnętrzny kształtu. android:right — margines wewnętrzny dla
każdej krawędzi kształtu
<size> Określa wielkość android:width, android:height — szerokość
kształtu. i wysokość kształtu
Pełną listę atrybutów znajdziesz na stronie http://mng.bz/vORg.

Na tym etapie gotowa jest atrakcyjna grafika selektora listy, jednak związany jest
z nią pewien problem — zawsze wygląda tak samo. Spodziewamy się, że klik-
nięcie lub zaznaczenie kontrolki zostanie wizualnie odzwierciedlone. Jak dodać
taki mechanizm? Przecież jest tylko jeden atrybut listSelector przyjmujący
0 TECHNIKA 8. Stosowanie selektorów obiektów graficznych 179

dokładnie jedną wartość, a podanie innej grafiki wymaga zastosowania przy-


najmniej drugiej wartości. Okazuje się, że to nieprawda — druga wartość nie
jest konieczna. Wystarczy użyć selektora obiektów graficznych.

0 TECHNIKA 8. Stosowanie selektorów obiektów graficznych

Czasem trzeba wyświetlić obiekty graficzne, które zmieniają się wraz ze sta-
nem widoku. Dobrym przykładem jest tu działanie przycisków w Androidzie.
Zaznaczenie przycisku za pomocą D-pada lub trackballa powoduje aktywowanie
elementu i nałożenie na niego jasnopomarańczowego koloru. Naciśnięcie przyci-
sku prowadzi do zmiany koloru na ciemniejszy odcień pomarańczowego, a długie
naciśnięcie ma jeszcze inny efekt. Ponieważ jako tło lub wyróżnienie można usta-
wić tylko jeden obiekt graficzny, potrzebny jest taki obiekt z pamięcią stanu.
PROBLEM
Widok udostępnia atrybut przyjmujący obiekt graficzny, jednak obiekt ten ma
się zmieniać wraz ze stanem widoku.
ROZWIĄZANIE
W Androidzie obiekty graficzne z pamięcią stanu to selektory obiektów graficz-
nych, deklarowane z zastosowaniem elementu <selector>. Specjalne obiekty
graficzne tego rodzaju można traktować jak przełączniki. W zależności od stanu
widoku (zaznaczony, wciśnięty, aktywowany itd.) omawiany obiekt ustawia jeden
z obiektów graficznych, którymi zarządza. Mechanizm ten pozwala wybierać
kształty.
Wróćmy do aplikacji i zastosujmy opisaną technikę do selektora listy. Zamiast
zawsze wyświetlać ten sam gradient, należy zmieniać jego kolor początkowy
z szarego na jasnoniebieski po wciśnięciu elementu listy. Ponieważ wymaga to
zastosowania dwóch różnych selektorów listy (dla stanu domyślnego i wciśnięcia),
trzeba zapisać je w dwóch odrębnych plikach. Nazwijmy je list_item_default.xml
i list_item_pressed.xml. Oto fragment kodu nowego obiektu graficznego list_
´item_pressed.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:startColor="#AA66CCFF"
android:endColor="#FFFF"
android:angle="0"/>
<stroke android:color="#CCC" android:width="1px" />
<corners android:radius="5px" />
</shape>

Nie ma tu nic nowego. Zastąpiliśmy początkowy kolor gradientu innym. Teraz


istnieją dwa obiekty graficzne. Trzeba umieścić je w selektorze obiektów gra-
ficznych. W tym celu należy zmodyfikować plik list_selector.xml z poprzedniej
techniki w następujący sposób:
180 ROZDZIAŁ 4. Precyzja co do piksela

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


<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/list_item_default" />
<item android:state_pressed="false"
android:drawable="@drawable/list_item_pressed" />
</selector>

W kodzie tym zmieniono element główny selektora listy z kształtu na przełącz-


nik kształtów (selektor obiektów graficznych). Selektor obiektów graficznych
zdefiniowano za pomocą elementów <item>, z których każdy przyjmuje dwa argu-
menty — stan i obiekt graficzny wyświetlany, kiedy widok powiązany z selektorem
przyjmuje dany stan (dalej dowiesz się, że można też zastosować różne wartości
kolorów). Na rysunku 4.11 pokazano selektor w obu stanach.

Rysunek 4.11. Nowy selektor listy w stanie domyślnym (po lewej) i po wciśnięciu
(po prawej). Zwróć uwagę na zmianę kolorów po wybraniu pozycji

Z wykorzystaniem znacznika <selector> można przełączać obiekty graficzne


dowolnego rodzaju, nie tylko kształty. Najczęściej używane są w tym kontekście
opisane dalej dziewięciopolowe obrazy. W Androidzie selektory i dziewięciopo-
lowe obrazy są powszechnie stosowane do wyświetlania systemowego interfejsu
użytkownika.
OMÓWIENIE
Przedstawiony przykład jest prosty, ponieważ używamy tu tylko dwóch różnych
stanów — domyślnego i po naciśnięciu elementu. Istnieje jednak także wiele
innych stanów. Każdy z nich reprezentuje warunek logiczny. Przegląd stanów
znajduje się w tabeli 4.6 (podajemy w niej tylko najczęściej używane stany).
0 TECHNIKA 8. Stosowanie selektorów obiektów graficznych 181

Tabela 4.6. Stany często używane w selektorze obiektów graficznych

Stan Opis

state_focused Aktywny widok.

state_window_focused Okno widoku stało się aktywne.

state_enabled Widok jest włączony.

state_pressed Widok został naciśnięty lub kliknięty.

state_checkable Widok można zaznaczyć (nie wszystkie widoki przyjmują ten stan).

state_checked Widok został zaznaczony (nie wszystkie widoki przyjmują ten stan).

state_selected Widok został wybrany (nie wszystkie widoki przyjmują ten stan).

Kompletną listę stanów znajdziesz na stronach http://mng.bz/Math i http://mng.bz/qzXz.

Kwestią, na którą należy zwrócić uwagę, jest kolejność elementów stanu w selek-
torze. Aby znaleźć w selektorze obiekt graficzny pasujący do bieżącego stanu
widoku, Android przechodzi po liście od góry do dołu (w kolejności zgodnej
z deklaracjami) i wybiera pierwszy pasujący obiekt. Dlaczego ma to znaczenie?
Wyobraź sobie aktywny i zaznaczony widok CheckBox powiązany z poniższym
selektorem:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
android:drawable="@drawable/my_checkbox_unchecked" />
<item android:state_focused="true" android:state_checked="true"
android:drawable="@drawable/my_checkbox_checked" />
</selector>

Możesz sądzić, że Android wybierze drugi obiekt graficzny, ponieważ wyraźnie


zadeklarowano, że to z niego należy korzystać, jeśli kontrolka CheckBox jest zazna-
czona. Błąd! Dzieje się inaczej, ponieważ pierwszy obiekt jest mniej ścisły i także
pasuje do stanu widoku. Wystarczy, że kontrolka jest aktywna (warunek ten jest
spełniony) — nie musi być zaznaczona. Ponieważ Android najpierw dopasowuje
pierwszy obiekt, używa go dla kontrolki CheckBox zawsze, kiedy jest ona aktywna.
To, czy ją zaznaczono, nie ma znaczenia. Dlatego drugi obiekt nigdy nie jest dopa-
sowywany. Co to oznacza? Że zawsze należy umieszczać ogólniejsze stany na
końcu listy. W przeciwnym razie uniemożliwią wybranie dokładniej określonego
stanu.
Oto następna przydatna wskazówka. Wspomnieliśmy wcześniej, że za pomocą
atrybutu android:color można podawać w selektorach kolory. Jest to wygodne
zwłaszcza w sytuacji, kiedy wygląd tekstu zależy od stanu (kiedy zmienia się
kolor lub rozmiar czcionki). Kiedy na przykład widok TextView staje się aktywny,
można zastosować selektor, który w zależności od stanu przełącza kolory na war-
tość atrybutu android:color widoku! To tego rodzaju szczegóły stanowią o elastycz-
ności i niezwykłych możliwościach systemu widoków w Androidzie.
Selektory obiektów graficznych (podobnie jak obiekty graficzne w postaci
kształtów) są odwzorowywane na klasy frameworku Javy. Przy stosowaniu
182 ROZDZIAŁ 4. Precyzja co do piksela

zwykłych obiektów graficznych selektorowi odpowiada klasa StateListDrawable.


Przy korzystaniu z kolorów jest to klasa ColorStateList, która — co ciekawe —
nie jest obiektem graficznym. Dlatego przy stosowaniu selektorów przełączają-
cych kolory pamiętaj, że selektorów tych nie można używać w miejscach, w któ-
rych oczekiwane są obiekty graficzne. Takie selektory można stosować tylko
zamiast wartości reprezentujących kolory (jednak jak wcześniej wspomnieliśmy,
kolory można łatwo przekształcić na obiekty graficzne, dlatego opisany problem
jest prosty do rozwiązania).
Zbliżamy się do końca. Pozostał nam do omówienia jeszcze jeden rodzaj
obiektów graficznych. Wspomnieliśmy już o dziewięciopolowych obiektach tego
rodzaju. Jest to prawdopodobnie najprzydatniejszy i najczęściej używany typ
obiektów graficznych, dlatego czytaj uważnie!

0 TECHNIKA 9. Skalowanie widoków za pomocą dziewięciopolowych


obiektów graficznych

Dziewięciopolowe obiekty graficzne najlepiej jest opisać na przykładzie. Wyobraź


sobie taką sytuację — nad widokiem listy z filmami chcemy umieścić rysunek
tytułowy z napisem „IMDB Top 100”. Można to zrobić, zmieniając ekran główny
aplikacji MyMovies w sposób pokazany na listingu 4.10.

Listing 4.10. Wzbogacanie układu aplikacji MyMovies o rysunek tytułowy

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<ImageView android:src="@drawable/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY" />

<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>

Na listingu 4.10 dodaliśmy widok ImageView wyświetlający rysunek tytułowy


(zapisany w pliku /res/drawable/title.9.png) . Rysunek tytułowy to plik gra-
ficzny w formacie PNG, a nie obiekt graficzny w XML-u. Warto zwrócić uwagę
na ustawienie fill_parent. Powoduje ono wypełnienie przez rysunek elementu
nadrzędnego w poziomie. Jeśli urządzenie jest trzymane w pionie, ustawienie
nie powoduje żadnych efektów (przynajmniej na ekranie o standardowej wiel-
kości, a w przykładzie zakładamy, że taki właśnie jest używany), ponieważ rysunek
ma dokładnie 320 pikseli szerokości. Jednak po obróceniu urządzenia widok
0 TECHNIKA 9. Skalowanie widoków za pomocą dziewięciopolowych obiektów graficznych 183

ImageView ma rozciągać się na całą szerokość ekranu. Ponieważ parametr układu


wpływa tylko na obiekt klasy ImageView (kontrolkę), a nie na sam rysunek (bit-
mapę), należy ustawić atrybut scaleType na fitXY. Oznacza to, że system ma
zmienić wielkość bitmapy, tak aby wypełniała kontener ImageView na szerokość
i wysokość . Efekt pokazano na rysunku 4.12 (zarówno dla orientacji piono-
wej, jak i poziomej).
Rysunek 4.12. Rysunek tytułowy
aplikacji MyMovies widoczny
w orientacji pionowej (po lewej)
i poziomej (po prawej). W układzie
pionowym rysunek nie jest
rozciągnięty. W orientacji
poziomej rozciąga się, aby wypełnić
widok w poziomie, co prowadzi
do zaburzenia proporcji

Warto zauważyć, że w orientacji poziomej tekst na rysunku tytułowym staje się


nieostry i ma zaburzone proporcje. Białe kółka wyglądają jak jajka! Dzieje się
tak, ponieważ Android rozciąga rysunek, aby zapełnić ekran. Proces ten obejmuje
interpolowanie pikseli do momentu, w którym rysunek przyjmuje ten sam roz-
miar, co kontener. Efekt jest bardzo nieatrakcyjny. Co zrobić, aby uniknąć pro-
blemu?
PROBLEM
Chcemy wyświetlać statyczny obraz w widoku o zmiennej szerokości lub wyso-
kości, jednak rozciągnięcie rysunku prowadzi do utraty jego jakości.
ROZWIĄZANIE
Spróbuj zgadnąć. Świetnie, rozwiązaniem są dziewięciopolowe obiekty graficzne!
Taki obiekt to plik PNG, w którym w danych obrazka (w bitmapie) zdefiniowano
obszary możliwe do rozciągnięcia. Obrazy w tym formacie można wyświetlać
w widokach o różnych rozmiarach i nie prowadzi to do zauważalnej utraty jako-
ści. Nazwy dziewięciopolowych plików PNG mają format *.9.png. Tylko wtedy
Android rozpoznaje plik PNG jako dziewięciopolowy obraz. Przekształcanie
standardowych rysunków na dziewięciopolowe obrazy jest proste.
Q Najpierw do obrazu, którego chcesz użyć, dodaj jednopikselowe
przezroczyste obramowanie (w praktyce możesz użyć dowolnego koloru
oprócz czarnego).
Q Następnie zdefiniuj możliwe do rozciągania obszary rysunku, oznaczając
czarnym kolorem odpowiednie fragmenty górnej i lewej krawędzi
obramowania. Rozszerzalny obszar to pole odpowiadające przecięciu
zaznaczonych na obramowaniu fragmentów.
Q Inna możliwość to zaznaczenie fragmentów prawej i dolnej krawędzi
w celu określenia marginesów wewnętrznych obrazu. Wtedy obszary
nieoznaczone czarnym kolorem są używane jako margines wewnętrzny.
184 ROZDZIAŁ 4. Precyzja co do piksela

Na rysunku 4.13 przedstawiono proces określania rozszerzalnych obszarów


i dodawania marginesów wewnętrznych.
Rysunek 4.13.
Definiowanie
rozszerzalnych
obszarów (u góry)
i pól marginesów
wewnętrznych
(u dołu)
w dziewięciopolo-
wych obiektach
graficznych.
Do definiowania
rozszerzalnych
obszarów służą
kreski na górnej
i lewej krawędzi,
natomiast obszar
marginesów
wewnętrznych
można zdefiniować
za pomocą kresek
na dolnej i prawej
krawędzi (źródło:
http://developer.
android.com)

Na górnym obrazku z rysunku 4.13 pokazano, jak definiowane są rozszerzalne


obszary pliku PNG. Środkowe pole to obszar powielany w celu zmiany wielkości
rysunku (można utworzyć kilka takich pól). Na dolnym obrazku środkowe pole
to obszar z treścią grafiki. Pozostała część to margines wewnętrzny. Jeśli treść gra-
fiki wyjdzie na obszar marginesów wewnętrznych, wielkość rysunku jest odpo-
wiednio modyfikowana przez powielenie pikseli z pola z treścią.
PUŁAPKA ZWIĄZANA Z POLEM MARGINESÓW WEWNĘTRZNYCH.
Zachowaj ostrożność, jeśli nie definiujesz bezpośrednio marginesów
wewnętrznych. Są one opcjonalne, jednak jeśli je pominiesz, Android
przyjmie, że pole z treścią odpowiada obszarowi rozszerzalnemu. Może
to prowadzić do dziwnych i nieoczekiwanych efektów ubocznych w czasie
wyświetlania rysunku.
W użytym rysunku tytułowym rozciągane mają być tylko małe obszary między
zaokrąglonymi narożnikami a tekstem. Mają one jednolity kolor i można je powie-
lać bez utraty jakości grafiki. Na rysunku 4.14 przedstawiono rysunek tytułowy
przekształcony na dziewięciopolowy obrazek i wyświetlony w narzędziu draw9patch
z pakietu SDK. Warto zauważyć, że grube linie biegnące wzdłuż krawędzi
to tylko wskazówki wizualne dodane przez narzędzie. Linie te nie są częścią
obrazu.
0 TECHNIKA 9. Skalowanie widoków za pomocą dziewięciopolowych obiektów graficznych 185

Rysunek 4.14. Rysunek tytułowy


aplikacji MyMovies zdefiniowany
jako dziewięciopolowy obiekt
graficzny. Narożne fragmenty
wyznaczone przez czarne kreski
służą do skalowania (nie
zdefiniowano tu marginesów
wewnętrznych)

Zapisaliśmy ten plik pod nazwą res/drawable/title.9.png i ponownie uruchomili-


śmy aplikację. Teraz spojrzenie na rysunek tytułowy, widoczny na rysunku 4.15,
w orientacji pionowej i poziomej pozwala przekonać się, że problem został roz-
wiązany. Rysunek się skaluje!

Rysunek 4.15. Teraz rysunek


tytułowy poprawnie się skaluje
w orientacji poziomej. Efekt
osiągnięto przez rozciąganie
tylko odpowiednich fragmentów
obrazka (które nie powodują
zniekształceń), zdefiniowanych
w dziewięciopolowym formacie

Nieźle radzimy sobie z rozwiązywaniem problemów, co? Chwileczkę, coś jest


nie tak? Zauważyłeś zniekształcony rysunek tła, prawda? Nie poprawiliśmy go,
jednak Ty możesz to zrobić, ponieważ wiesz już, jak działają dziewięciopolowe
obiekty graficzne.
OMÓWIENIE
Dziewięciopolowe obiekty graficzne są niezwykle przydatne i powszechnie wystę-
pują w samym Androidzie. Wszystkie standardowe kontrolki platformy są zbu-
dowane przy ich użyciu. Omawiane obiekty są użyteczne przede wszystkim dla
kontrolek w rodzaju przycisków i pól tekstowych, które często trzeba dopasować
do obejmującego je układu. Jeśli chcesz zmienić styl wszystkich standardowych
kontrolek aplikacji, dobrym rozwiązaniem jest wykorzystanie istniejących dzie-
więciopolowych obiektów graficznych z projektu Androida (jest on oprogramo-
waniem o otwartym dostępie do kodu źródłowego) i wprowadzenie modyfikacji
właśnie w nich. Programiści często zmieniają na przykład kolor wyróżnienia, aby
dopasować go do palety barw aplikacji. Standardowy pomarańczowy kolor czasem
nie jest odpowiedni.
Dziewięciopolowe obiekty graficzne można tworzyć za pomocą dowolnych
aplikacji graficznych, jednak na szczęście dla osób, które nie posiadają wymyśl-
nych komercyjnych programów tego rodzaju, Google dodał do pakietu SDK
wspomniane już narzędzie draw9patch. Pomaga ono programistom przez auto-
matyczne dodawanie jednopikselowego obramowania do istniejącego rysunku,
wyświetlanie wizualnych wskazówek (na przykład wyróżnianie pól używanych
przy skalowaniu) i pokazywanie podglądu zmodyfikowanego rysunku po prze-
skalowaniu go we wszystkich kierunkach.
186 ROZDZIAŁ 4. Precyzja co do piksela

Ten podrozdział w całości poświęciliśmy grafice. Zobaczyłeś, jak uporząd-


kować atrybuty widoków aplikacji z zastosowaniem stylów i motywów. Dowie-
działeś się też, czym są obiekty graficzne i jak używać ich do tworzenia nie-
standardowych, eleganckich interfejsów użytkownika — oczywiście jeśli Twoje
zdolności projektowe dorównują umiejętnościom programistycznym. Omówiliśmy
skalowanie rysunków (na małą skalę). Co jednak ze skalowaniem całego inter-
fejsu użytkownika? Urządzenia z Androidem mają ekrany o różnej wielkości,
a nawet najpiękniejszy interfejs użytkownika nie wygląda dobrze, jeśli nie jest
prawidłowo wyświetlany na wszystkich urządzeniach. Dlatego z następnego
podrozdziału dowiesz się o dopasowywaniu interfejsu użytkownika do różnych
wyświetlaczy i konfiguracji — zarówno tych dostępnych, jak i tych, które dopiero
powstaną!

4.7. Tworzenie przenośnych interfejsów użytkownika


Przenośność może oznaczać różne rzeczy — zarówno związane z oprogramo-
waniem (nie wszystkie funkcje pakietu SDK są dostępne w każdym urządzeniu),
jak i sprzętem (nie każde urządzenie z Androidem jest wyposażone w czujnik
światła lub fizyczną klawiaturę). W tym podrozdziale omawiamy przenośność
i skalowalność pod kątem interfejsów użytkownika oraz rozmiarów ekranu.
Do programowania aplikacji na Android przystąpiliśmy zaraz po udostęp-
nieniu wersji alfa tej platformy, kiedy nie działała ona jeszcze na żadnym urzą-
dzeniu — chyba że uwzględnimy tablety Nokia Internet, na których garstka
odważnych programistów instalowała przedprodukcyjne wersje Androida. Następ-
nie pojawił się telefon G1 Google’a (czyli HTC Dream) i życie stało się piękne.
Wystarczyło wtedy uwzględniać jedną konfigurację sprzętową. Obecnie istnieje
tak wiele różnych urządzeń, że przestaliśmy je liczyć, a popularność Androida
wciąż rośnie, ponieważ z platformy tej korzysta coraz więcej producentów.
Konieczność obsługi tak dużej liczby urządzeń, o czym wspomnieliśmy już
w rozdziale 1., to kwestia poruszana przez krytyków Androida. Na szczęście
platforma udostępnia wygodne sposoby obsługi różnych konfiguracji sprzęto-
wych, dzięki czemu zapewnienie działania aplikacji na ekranach urządzeń, które
nie istniały w momencie rozwijania danego programu, jest niemal banalne.
W trzech następnych technikach pokazujemy, jak umożliwić eleganckie dosto-
sowanie aplikacji do różnych konfiguracji ekranu. Poznasz zarówno proste, doraźne
rozwiązanie przeznaczone dla starszych aplikacji, jak i bardziej zaawansowane
podejścia oparte na mechanizmach systemowych.

0 TECHNIKA 10. Automatyczne dostosowywanie aplikacji


do różnych ekranów

Wszystkie dotychczasowe zrzuty z przykładowych aplikacji wykonano na emu-


latorze z domyślną konfiguracją ekranu (standardową dla wersji starszych niż 1.6).
0 TECHNIKA 10. Automatyczne dostosowywanie aplikacji do różnych ekranów 187

Rysunek 4.16.
Poszczególne
urządzenia
z Androidem mogą
mieć różne
konfiguracje
wyświetlacza. HTC
Magic (po lewej) ma
ekran o przekątnej 3,4
cala i rozdzielczości
320×480 pikseli (160
dpi), natomiast HTC
Tattoo ma mniejszy
wyświetlacz (2,8 cala)
o niższej
rozdzielczości
(240×320 pikseli
— 120 dpi)

W wersji 1.6 Androida wprowadzono obsługę nowych konfiguracji. Pojawiły


się też urządzenia oparte na tej konfiguracji, na przykład HTC Tattoo z ekranem
QVGA, który był krótszy od wcześniejszych (rysunek 4.16).
Warto zadać pewne pytanie. Jeśli opracowałeś aplikację na Android 1.5 lub dla
starszej wersji platformy, a program został już wprowadzony na rynek, to jak
możesz zapewnić prawidłowe wyświetlanie interfejsu użytkownika na wszyst-
kich urządzeniach? Nowe urządzenia nie tylko mogą mieć niższą lub wyższą
rozdzielczość (mniej lub więcej fizycznych pikseli), ale też ich wyświetlacze
mogą mieć inną gęstość pikseli (mniej lub więcej pikseli na określoną jednostkę).
Ten drugi problem może prowadzić do kłopotów przy wyświetlaniu aplikacji.
Jeśli element interfejsu użytkownika ma mieć 100 pikseli szerokości, po wyświe-
tleniu na ekranie o wysokiej gęstości może wyglądać na zmniejszony, ponieważ
zajmuje mniejszy obszar. Problem ten zilustrowano na rysunku 4.17 dla dwóch
potencjalnych gęstości, równych trzem i sześciu punktom na cal.
W Androidzie od wersji 1.6 wbudowane są algorytmy, które automatycznie
rozwiązują tego typu problemy. Dalej dowiesz się, jak aktywnie poradzić sobie
z takimi trudnościami, jednak na razie załóżmy, że jesteśmy leniwi i nie mamy
ochoty samodzielnie poprawiać aplikacji. Zamiast tego pozwalamy rozwiązać pro-
blem środowisku uruchomieniowemu Androida.
PROBLEM
Aplikację opracowano pod kątem wyświetlaczy określonego rodzaju (o konkret-
nej gęstości pikseli i wymiarach). Teraz chcesz udostępnić ją dla innych konfi-
guracji ekranu bez konieczności modyfikowania kodu widoków.
188 ROZDZIAŁ 4. Precyzja co do piksela

Rysunek 4.17.
Kreska o długości
ośmiu pikseli
i wysokości jednego
piksela na ekranie
o gęstości 6 dpi
zajmuje tylko
połowę szerokości
i wysokości tej samej
kreski z ekranu
o gęstości 3 dpi.
Zauważ, że w tym
przykładzie
dla uproszczenia
przyjęto, że punkty
odpowiadają
pikselom
— nie zawsze
jest to prawdą

ROZWIĄZANIE
Za pomocą elementu <supports-screens> można określić wielkości ekranu i gęsto-
ści pikseli obsługiwane przez aplikację. Element ten wprowadzono w Andro-
idzie 1.6. Ponieważ aplikacji MyMovies nie zbudowaliśmy z myślą o konfigura-
cjach ekranu różnych od domyślnej, należy poinformować o tym Android.
W manifeście aplikacji należy podać obsługiwane konfiguracje.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

<uses-sdk android:minSdkVersion="3" />

<supports-screens
android:smallScreens="false"
android:normalScreens="true"
android:largeScreens="false"
android:xlargeScreens="false"
android:anyDensity="false"
/>

...
</manifest>

Jest to informacja dla Androida, że obsługiwane są tylko urządzenia o standar-


dowym ekranie. Warto zauważyć, że nie oznacza to, iż aplikacji nie można zain-
stalować na innych urządzeniach. Nie oznacza to nawet tego, że programy nie będą
na nich działać. Podane ustawienia mają jednak pewne skutki, o czym za chwilę.
OSTRZEŻENIE. Konfiguracja z listingu 4.21 jest generowana automatycz-
nie przy budowaniu aplikacji na Android 1.5 i starsze wersje platformy
(wersję określa element <uses-sdk>). Dlatego jeśli aplikacja działa na plat-
formie Android 1.6 lub nowszej, ale zbudowano ją za pomocą starszego
pakietu SDK, domyślnie przyjmuje się, że program opracowano z myślą
o normalnej wielkości ekranu i gęstości pikseli, ponieważ takie były
0 TECHNIKA 10. Automatyczne dostosowywanie aplikacji do różnych ekranów 189

dawne standardy. Są to sensowne domyślne ustawienia dla takich aplikacji.


Jest jednak inaczej, jeśli aplikacja jest przeznaczona dla interfejsu API
w wersji 4. lub wyższej (dla platformy Android 1.6 lub jej nowszej wersji).
Przy ustawieniu atrybutu minSdkVersion na wartość równą przynajmniej 4
wszystkie atrybuty <supports-screens> domyślnie mają wartość true. Jeśli
zatem chcesz pozostawić tryb dla starszych wersji (z obsługą jednego ekranu
lub wielu ekranów), musisz zmienić odpowiednie wartości na false.
Co oznacza określenie „normalny ekran”? Nie wspomnieliśmy na razie o konkret-
nych rozmiarach w pikselach ani gęstości w dpi. Wynika to z tego, że Android
odwzorowuje wszystkie dostępne wyświetlacze na konfiguracje z tabelki o wymia-
rach 4×4, co opisano w tabeli 4.7. Tabelka ta jest oparta na konfiguracji podsta-
wowej, która była jedyną dostępną przed pojawieniem się platformy 1.6 i z której
korzystały wszystkie urządzenia z Androidem 1.5, na przykład telefon G1.
Tabela 4.7. Tabelka konfiguracji ekranu z przykładami

Średnia Wysoka Bardzo wysoka


Niska gęstość
gęstość gęstość gęstość*
Mały ekran Sony Xperia Mini
(QVGA 240×320,
2,55")
Normalny Podstawowa Google Nexus
ekran konfiguracja One (WVGA
z Google G1 i HTC 480×800, 3,7")
Magic (HVGA
320×480, 3,2")
Duży ekran HTC Desire HD2
(qHD 540×960,
4,3")
Bardzo Tablet Motorola
duży Xoom (WXGA
ekran* 1280×800, 10,1")
Dokładniejsze omówienie znajdziesz na stronie http://mng.bz/InPC.
* Konfiguracje xlargeScreens (dla bardzo dużych ekranów) i hxdpi (dla bardzo wysokiej gęstości) dodano
w wersjach Android 2.3 i Android 2.2.

Jeśli aplikację opracowano pod kątem konfiguracji podstawowej (320×480


i 160 dpi), Android przez przejście w tryb awaryjny gwarantuje, że program
będzie działał na urządzeniach z wyświetlaczem WVGA. Technika ta jest jednak
dostępna tylko dla niektórych konfiguracji. Warto wiedzieć, które tryby awaryjne
Android stosuje dla poszczególnych konfiguracji.
OMÓWIENIE
Jeśli dla jednego z wcześniej wspomnianych atrybutów ustawiona jest wartość
false, Android uruchamia tryb awaryjny dla odpowiednich konfiguracji ekranu.
Działanie tego trybu jest różne dla poszczególnych atrybutów. Jeśli wartość false
ustawiono dla atrybutu smallScreens, użytkownicy z urządzeniami o małych
190 ROZDZIAŁ 4. Precyzja co do piksela

ekranach nie zobaczą aplikacji w sklepie Android Market (choć mogą ją zain-
stalować ręcznie). Wynika to z tego, że interfejs użytkownika nie może działać
poprawnie, jeśli nie ma miejsca na jego wyświetlenie. Warto o tym pamiętać.
W przeciwnym razie możesz utracić dużą grupę potencjalnych użytkowników,
ponieważ nie mogą oni nawet znaleźć danej aplikacji w sklepie Android Market!
Zupełnie inaczej jest z urządzeniami o dużym ekranie, na przykład z table-
tami, ponieważ ich wyświetlacze są wystarczająco duże, aby pokazać na nich
całą aplikację. Można ująć to inaczej — jeśli wartość false ustawiono dla atrybutu
largeScreens, Android wyświetla aplikację w trybie letterbox. Oznacza to, że wyko-
rzystywana jest podstawowa wielkość i gęstość, a pozostałą wolną przestrzeń
zajmują czarne paski. Nie jest to piękne rozwiązanie, ale przynajmniej działa.
Do omówienia pozostaje opcja anyDensity. Tu sprawy się komplikują. Jeśli
jej wartość to false, Android uruchamia tryb zgodności, w którym wszystkie
wartości podane w pikselach (miara px) są skalowane względem podstawowej
gęstości równej 160 dpi w celu dopasowania ich do gęstości ekranu danego urzą-
dzenia. Jeśli gęstość jest wyższa, wartości są skalowane w górę. Jeżeli jest mniej-
sza, zachodzi skalowanie w dół. Technika ta gwarantuje, że współrzędnym lub
wymiarom określonym w pikselach będą odpowiadać mniej więcej te same
fizyczne pozycje i rozmiary niezależnie od gęstości pikseli. Przypominamy, że —
jak pokazano na rysunku 4.17 — wymiary elementów zdefiniowanych w pik-
selach standardowo wyglądałyby inaczej na wyświetlaczach o różnej gęstości.

Przykład — tryb automatycznego skalowania w Androidzie


Załóżmy, że chcesz wyświetlić rysunek o szerokości 100 pikseli. W konfiguracji
podstawowej (320×480 i 160 dpi) jeden fizyczny piksel ma 0,00625 (1/160) cala.
Rysunek na urządzeniu o tej konfiguracji ma więc szerokość 0,0625 cala. Ten sam
rysunek wyświetlony na urządzeniu z ekranem o wysokiej rozdzielczości 480×800 i
gęstości 240 dpi zajmuje tylko 0,416 cala, ponieważ z uwagi na wyższą gęstość
jeden piksel zajmuje mniej miejsca na wyświetlaczu. Aby przeciwdziałać temu efek-
towi, Android mnoży pierwotną, podaną w pikselach wartość przez 1,5 (240/160).
Wynik to 0,625. Gotowe — rysunek o szerokości ustawionej na 100 pikseli zaj-
muje tyle samo miejsca na obu ekranach!
Ponadto z uwagi na to, że domyślnie przyjmowana jest gęstość 160 dpi, a wyświe-
tlacze o wysokiej rozdzielczości mają zdecydowanie więcej pikseli i wyższą gęstość,
Android musi informować aplikację, że ekran jest mniejszy niż w rzeczywistości.
W przeciwnym razie ekran będzie wydawał się większy (z uwagi na większą liczbę
pikseli rozrzuconych na większym obszarze). Dlatego Android skaluje wielkość
ekranu w dół, mnożąc ją przez 0,75 (160/240), i informuje aplikację, że wyświetlacz
ma wymiary 320×533 piksele. Powoduje to, że ekran zaliczany jest do klasy nor-
malnych wyświetlaczy.
Kto powiedział, że małe kłamstwo nie może się czasem przydać?

Oprócz wykonywania podanych przekształceń Android automatycznie skaluje


wszystkie obiekty graficzne wczytywane ze standardowego katalogu z takimi
obiektami (ponieważ domyślnie są one dopasowane do konfiguracji podstawowej).
Na przykład rysunek w formacie PNG o szerokości 100 pikseli zawsze zajmuje
0 TECHNIKA 11. Wczytywanie zasobów zależnych od konfiguracji 191

na ekranie tyle samo miejsca, ponieważ jest skalowany w górę lub w dół w zależ-
ności od wymiarów i gęstości wyświetlacza (odbywa się to w opisany wcześniej
sposób). Jest to tak zwane wstępne skalowanie (ang. prescaling), które odbywa
się w trakcie wczytywania danego zasobu. Skalowanie bitmap jest jednak zaso-
bochłonne. Dalej wyjaśniamy, jak uniknąć tych kosztownych obliczeń.
W tabeli 4.8 pokazujemy, co się dzieje, jeśli w pliku manifestu określasz, że
aplikacja nie obsługuje konfiguracji innych niż podstawowa.
Tabela 4.8. Przegląd ustawień atrybutu supports-screens i ich efektów

Atrybut ustawiony
Efekt
na false

smallScreens Na urządzeniach z małym ekranem aplikacja jest odfiltrowywana


ze sklepu Android Market. Nadal można zainstalować ją ręcznie.
Platforma przeprowadza wtedy opisane wcześniej skalowanie.
normalScreens Na urządzeniach z normalnymi ekranami działa tryb automatycznego
skalowania z Androida. Jeśli nie tworzysz rozwiązań przeznaczonych
konkretnie na urządzenia z małym lub dużym ekranem, używanie tego
ustawienia nie ma sensu.
largeScreens Na urządzeniach z dużym ekranem aplikacja jest wyświetlana
w konfiguracji podstawowej po odpowiednim przeskalowaniu. Jeśli
po przeskalowaniu program nie zajmuje całego ekranu,
niewykorzystany obszar zajmują czarne paski (tryb letterbox).
xlargeScreens Działa tak samo jak largeScreens.
anyDensity Na urządzeniach o gęstości pikseli różnej od podstawowej Android
automatycznie skaluje wszystkie rysunki (o ile nie są specjalnie
przygotowane) i wartości podane w pikselach, aby dostosować
je do danej konfiguracji.

Odpowiednie ustawienia supports-screens mogą być przydatne i ułatwiają dosto-


sowanie starszych aplikacji do nowszych wersji platformy, jednak występują przy
tym pewne efekty uboczne. Poprawne rozwiązanie wymaga wykorzystania frame-
worku zasobów zastępczych Androida.

0 TECHNIKA 11. Wczytywanie zasobów zależnych od konfiguracji

Mechanizmy opisane w poprzedniej technice doskonale nadają się do łatwego


dostosowywania starszych aplikacji do prawie wszystkich konfiguracji ekranu
bez konieczności pisania w tym celu specjalnego kodu. Stanowią one jednak tylko
udogodnienie, a korzystanie z nich nie należy do dobrych praktyk przy rozwi-
janiu nowych aplikacji.
Wady rozwiązania są oczywiste. Są to: brak aplikacji w sklepie Android Market
na małych urządzeniach, brak gwarancji wyświetlania aplikacji w trybie pełno-
ekranowym na dużych wyświetlaczach i często zauważalna utrata jakości wstępnie
skalowanych rysunków (a także pewne wydłużenie czasu wczytywania). Co
można zrobić, aby zapewnić lepszą obsługę różnych konfiguracji wyświetlacza?
192 ROZDZIAŁ 4. Precyzja co do piksela

PROBLEM
Zamiast polegać na wstępnym skalowaniu rysunków w Androidzie, chcemy
utworzyć zasoby (na przykład układy lub rysunki) dostosowane do konkretnych
wielkości ekranu lub gęstości. Ma to pozwolić uniknąć utraty jakości grafiki, która
wynika ze wspomnianego procesu wstępnego skalowania.
ROZWIĄZANIE
Rozwiązanie polega na wykorzystaniu frameworku zasobów zastępczych z Andro-
ida. O mechanizmie tym wspomnieliśmy już w rozdziale 2., gdzie napisaliśmy,
że dla poszczególnych języków można zastosować kilka różnych plików z zasobami
w postaci łańcuchów znaków. Wymaga to użycia odrębnego katalogu zasobów
dla każdej obsługiwanej wersji językowej (na przykład /res/values-en dla angiel-
skich łańcuchów znaków i /res/values-de dla niemieckich zwrotów). Ten sam
mechanizm można wykorzystać do udostępnienia w Androidzie zasobów zwią-
zanych z daną konfiguracją, na przykład obiektów graficznych lub układów.
Android przyjmuje, że takie zasoby opracowano specjalnie dla konkretnych
konfiguracji, i nie próbuje ich wstępnie skalować.
Załóżmy, że chcesz dodać do aplikacji MyMovies niestandardową ikonę
z napisem „MyMovies”. Problem polega na tym, że na ekranach o średniej i niskiej
rozdzielczości tekst ikony jest trudny do odczytania (lub w ogóle nieczytelny).
Dlatego cały tekst ma być wyświetlany tylko na urządzeniach HDPI (ang. high
dots per inch — „duża liczba punktów na cal”), a na urządzeniach LDPI (ang.
low dots per inch — „mała liczba punktów na cal”) aplikacja ma pokazywać tylko
skrót „MM”. W normalnej konfiguracji nie są wprowadzane żadne zmiany. Na
potrzeby rozwiązania trzeba utworzyć dwie wersje standardowej ikony i umieścić
je w katalogach drawable-hdpi oraz drawable-ldpi, co pokazano na rysunku 4.18.

Rysunek 4.18. Udostępnianie różnych plików graficznych dla różnych konfiguracji


ekranu odbywa się przez zapisanie ich w odpowiednich katalogach zasobów dla
danej konfiguracji. Wybór katalogu zasobów wczytywanego w czasie wykonywania
programu odbywa się przez dopasowanie konfiguracji urządzenia do nazwy katalogu
0 TECHNIKA 11. Wczytywanie zasobów zależnych od konfiguracji 193

Tu tworzymy wersje przeznaczone bezpośrednio na urządzenia o niskiej i wyso-


kiej gęstości pikseli. W tym celu udostępniamy dwa nowe pliki ikon przygoto-
wane specjalnie dla ekranów o docelowej gęstości pikseli. Nie trzeba robić nic
więcej. Android automatycznie wyszukuje i wczytuje odpowiednie pliki, nawet
jeśli działa w trybie awaryjnym! Na rysunku 4.19 porównano wygląd nowych ikon
na dużym ekranie o wysokiej gęstości pikseli i na małym ekranie o niskiej gęsto-
ści pikseli.

Rysunek 4.19. Dwa różne pliki ikon wyświetlane w docelowych konfiguracjach


— ikona z pełnym tekstem na ekrany HDPI (duży rysunek) i wersja z akronimem
na ekrany LDPI (mały rysunek)

Technikę tę można wykorzystać na wiele sposobów. Można nawet wczytywać


różne łańcuchy znaków (i zasoby dowolnego typu) w zależności od wielkości
ekranu, jednak — jak łatwo się domyślić — rozwiązanie to jest przydatne przede
wszystkim w kontekście rysunków i układów.
OMÓWIENIE
Można tworzyć zasoby przeznaczone nie tylko dla ekranów o określonej gęstości
pikseli. W nazwach katalogów zasobów jest kodowanych znacznie więcej infor-
macji. Możliwe jest wczytywanie różnych zasobów na podstawie języka, kraju
karty SIM, rodzaju ekranu dotykowego, typu klawiatury, orientacji ekranu, wersji
interfejsu API itd. Można nawet łączyć różne ustawienia. W tabeli 4.9 przedsta-
wiono kwalifikatory związane z konfiguracją ekranu.
Warto pamiętać, że tworzenie katalogów zasobów o kwalifikowanych nazwach
jest opcjonalne. Jeśli w katalogu /res/drawables/hdpi nie ma żadnych wstępnie
przeskalowanych rysunków (lub folder ten w ogóle nie istnieje), Android wpraw-
dzie najpierw szuka obrazków w tym katalogu, ale jeśli ich nie znajdzie, wybierze
domyślny katalog z obiektami graficznymi. Oznacza to, że umieszczanie wszyst-
kich plików w katalogach zasobów bez kwalifikowanej nazwy zawsze jest bez-
piecznym rozwiązaniem. Dzięki temu Android znajdzie zasoby.
194 ROZDZIAŁ 4. Precyzja co do piksela

Tabela 4.9. Kwalifikatory zasobów związane z obsługą ekranów

Docelowy
Kwalifikatory Przykłady
atrybut
Klasa small — małe ekrany (2 – 3 cale) /res/drawables-small
wielkości
normal — normalne ekrany (rozmiar podstawowy, /res/drawables-small-ldpi
ekranu
3 – 4 cale)
/res/layouts-normal-land
large — duże ekrany (4 – 7 cali)
xlarge — bardzo duże ekrany (ponad 7 cali)
Zwiększona long — dłuższe ekrany (na przykład WQVGA, /res/drawables-long
wysokość WVGA i FWVGA)
/res/drawables-large-long
ekranu
notlong — normalne proporcje (na przykład
/res/layouts-notlong-port
QVGA, HVGA i VGA)
Gęstość ldpi — niska gęstość (około 120 dpi) /res/drawables-ldpi
pikseli (dpi)
mdpi — średnia gęstość (około 160 dpi) /res/drawables-large-mdpi
hdpi — wysoka gęstość (około 240 dpi) /res/layouts-port-hdpi
xhdpi — bardzo wysoka gęstość (około 320 dpi)
nodpi — wyłączenie skalowania dla danych
zasobów
Pełną listę i reguły określania kolejności kwalifikatorów znajdziesz na stronie http://mng.bz/d0M9.

W JAKI SPOSÓB ANDROID WYBIERA KATALOGI ZASOBÓW?


Jeśli odpowiednich jest kilka katalogów, Android wczytuje zasoby z tego,
którego nazwa najlepiej odpowiada bieżącej konfiguracji. Algorytm wyboru
folderu jest dość skomplikowany. Znajdziesz go na stronie http://mng.
bz/7NiH.
Opisana technika wprawdzie zwiększa rozmiar aplikacji, ale jeśli pakiet obej-
muje kilka wersji danego zasobu, warto stosować tę metodę dla rysunków (na
przykład ikon lub tła okna), jeżeli skalowanie może prowadzić do utraty jakości
grafiki. Pomyśl o zmianie wielkości ikony ze 100 do 150 pikseli na urządzeniu
HDPI. Oznacza to pięćdziesięcioprocentowy wzrost liczby pikseli w jednym
wymiarze. Po przeskalowaniu rysunek prawdopodobnie będzie wyglądał nie-
atrakcyjnie. Natomiast dziewięciopolowe obrazki z natury dobrze się skalują
i powodują mniejsze problemy nawet wtedy, kiedy Android działa w trybie auto-
matycznego skalowania.
Wiesz już, jak umożliwić Androidowi modyfikowanie elementów aplikacji i jak
udostępnić zasoby związane z daną konfiguracją. Do omówienia pozostała jeszcze
jedna kwestia. Zostawiliśmy ją na koniec, ale w żadnym razie nie jest ona najmniej
istotna. Chodzi o pisanie aplikacji z myślą o różnych konfiguracjach ekranu.

0 TECHNIKA 12. Uniezależnienie się od pikseli

To ostatnia z technik, które omawiamy w tym rozdziale. Jest krótka, ale ważna.
Musimy odpowiedzieć na pytanie, czy jeśli za pomocą elementu <supports-
´screens> włączymy obsługę wszystkich gęstości pikseli, to androidowy mecha-
0 TECHNIKA 12. Uniezależnienie się od pikseli 195

nizm automatycznego skalowania przestanie działać i ponownie wystąpi pro-


blem z tym, że wartości podane bezpośrednio w pikselach będą odzwierciedlane
na różnych urządzeniach w odmienny sposób.
PROBLEM
Bezpośrednie zadeklarowanie obsługi ekranów o gęstości pikseli różnej od nor-
malnej powoduje, że w Androidzie tryb automatycznego skalowania przestaje
działać. Oznacza to, że wartości podane w pikselach nie są prawidłowo skalowane
na niestandardowych ekranach.
ROZWIĄZANIE
Rozwiązanie jest zaskakująco proste — wystarczy nie podawać bezpośrednio
wartości w pikselach. Stosowanie jednostek px jest niebezpieczne. Każda wartość
podana z wykorzystaniem takich jednostek jest dostosowana do urządzenia uży-
wanego w czasie programowania. W jaki sposób określać więc pozycje i wymiary?
Android udostępnia w tym celu zestaw jednostek niezależnych od gęstości. Na
urządzeniach z podstawową konfiguracją działają one tak, jakby podano je bez-
pośrednio w pikselach. Jeśli urządzenie ma inny ekran, następuje skalowanie,
które przebiega w opisany wcześniej sposób.
Pamiętasz, jak zdefiniowaliśmy, że promień narożnika selektora listy ma mieć
pięć pikseli? Przyjrzyj się rysunkowi 4.20. Po lewej stronie widać, jak selektor
listy powinien wyglądać, natomiast na obrazku po prawej stronie wydaje się, że
promień narożnika ma mniej niż ustawione pięć pikseli. Oba zrzuty wykonano
na emulatorze z ustawioną wysoką gęstością, jednak na lewym obrazku promień
narożnika określiliśmy za pomocą pikseli niezależnych od gęstości (ang. density-
-independent pixels — dip lub dp), a na prawym użyliśmy zwykłej jednostki px.

Rysunek 4.20. Problem z promieniem narożników na ekranie o wysokiej gęstości


pikseli. Po lewej stronie promień podano w pikselach niezależnych od gęstości
(dip), natomiast po prawej stronie użyto bezpośrednio podanych pikseli (px),
co przełożyło się na fizycznie mniejszy promień
196 ROZDZIAŁ 4. Precyzja co do piksela

Jeśli określasz wartości w jednostkach dip, w urządzeniach o podstawowej konfi-


guracji są one traktowane jak wartości w pikselach, kiedy system porządkuje
elementy na ekranie. Oznacza to, że przy korzystaniu z takich urządzeń nie
dostrzeżesz żadnej różnicy między jednostkami px a dip. Jednak jeśli gęstość
pikseli jest inna, Android zadba o to, aby aplikacja wyglądała odpowiednio!
OMÓWIENIE
Wszędzie tam, gdzie to możliwe, do określania pozycji lub wymiarów należy
używać jednostek niezależnych od gęstości, zamiast bezpośrednio podawać pik-
sele. W Androidzie można używać dwóch jednostek, aby umożliwić automa-
tyczne skalowanie wartości. Oto te jednostki:
Q dip (inaczej dp). Są to piksele niezależne od gęstości, przydatne
do podawania pozycji i wymiarów w skalowalny sposób.
Q sip (inaczej sp). Są to piksele niezależne od skali, przydatne do podawania
wielkości czcionki w skalowalny sposób.
Można (i należy) używać tych jednostek w układach i stylach. Jeśli w kodzie
programu podajesz wartości w pikselach, jednak chcesz uzyskać efekt podobny
jak przy stosowaniu opisanych jednostek, musisz sam zadbać o skalowanie (chyba
że stosujesz tryb awaryjny), ponieważ funkcje z pakietu SDK przyjmują bezpo-
średnio podane piksele. Oto funkcja pomocnicza, która za programistę przekształca
jednostki dip na piksele:
public static int dipToPx(Activity context, int dip) {
DisplayMetrics displayMetrics =
context.getResources().getDisplayMetrics();

return (int) (dip * displayMetrics.density + 0.5f);


}

Za pomocą tej funkcji pomocniczej możesz pisać aplikacje, które dostosowują


się do wszystkich wyświetlaczy. Nie musisz przy tym posługiwać się trybem awa-
ryjnym z Androida.
W ostatnim podrozdziale pokazaliśmy trzy techniki. Dowiedziałeś się, jak
z wykorzystaniem trybu awaryjnego z Androida przygotować starsze aplikacje
do działania na urządzeniach o różnych konfiguracjach ekranu. Co ważniejsze,
pokazaliśmy, jak zapewnić eleganckie dostosowywanie nowych aplikacji do
ekranów o różnych wymiarach i gęstościach pikseli. Efekt ten można uzyskać
z użyciem niestandardowych zasobów i pikseli niezależnych od gęstości lub skali.
To już koniec tego rozdziału.

4.8. Podsumowanie
W tym rozdziale skoncentrowaliśmy się na interfejsie użytkownika. Pokazaliśmy,
jak konfigurować widoki w układach oraz w jaki sposób hierarchie widoków są
wyświetlane na ekranie. Dowiedziałeś się też, jak działają menedżery układów
4.8. Podsumowanie 197

dostępne w Androidzie, a także jak korzystać z atrybutów do tworzenia struktury


układu. Od tego rozpoczyna się budowanie interfejsu użytkownika.
Dalej dokładnie opisaliśmy używanie widoku ListView. Przedstawiliśmy kilka
ciekawych zagadnień, na przykład widoki nagłówka i stopki, a także dopasowy-
wanie do siebie stanu widoku i stanu modelu danych udostępnianego przez adap-
ter. Dzięki temu mogliśmy skoncentrować się na omówieniu korzystania z popu-
larnego i przydatnego widoku ListView. Ponadto na podstawie tego widoku
pokazaliśmy, jak wielokrotnie wykorzystać style, zamiast powielać w każdym
widoku ustawienia związane z wyglądem. Wyjaśniliśmy też, jak pójść o krok
dalej i tworzyć oraz stosować motywy.
W omówieniu motywów przedstawiliśmy, jak przenieść interfejs użytkow-
nika na wyższy poziom, używając obiektów graficznych i definiując je. Zobaczy-
łeś też, jak udostępniać niezależne od urządzenia zasoby dla sprzętu o różnej
konfiguracji. Pozwala to tworzyć perfekcyjne układy i rysunki, dzięki czemu
aplikacja wygląda w oczekiwany sposób na ekranach o różnych wymiarach.
Dowiedziałeś się już dużo o podstawach tworzenia aplikacji na Android
i doskonaleniu formy (interfejsu użytkownika) takich programów. Pora przejść
do funkcji. Robimy to w następnym rozdziale, gdzie odchodzimy od interfejsu
użytkownika i zajmujemy się nowym tematem — usługami działającymi w tle.
198 ROZDZIAŁ 4. Precyzja co do piksela
Używanie usług
do zarządzania zadaniami
wykonywanymi w tle

W tym rozdziale
Q Usługi i wielozadaniowość
Q Tworzenie zadań wykonywanych w tle
Q Odtwarzanie usuniętych zadań

Jestem najlepszy. Mówiłem tak, zanim jeszcze o tym wiedziałem. Nie


mów mi, że nie mogę czegoś osiągnąć. Nie mów mi, że to niemożliwe.
Nie mów mi, że nie jestem najlepszy. Jestem najlepszy po dwakroć.
Muhammad Ali
Usługi to „zabójcza” cecha Androida. To odważne, ale prawdziwe stwierdzenie.
Precyzyjniej można powiedzieć, że „zabójczą” cechą Androida jest wielozada-
niowość, a stosować ją można właśnie dzięki usługom. Nie musisz wierzyć nam
na słowo — wystarczy, że włączysz telewizor. W jednej z najbardziej udanych
reklam popularnego smartfonu Motorola Droid firma podkreśla wielozadaniowość
i wyśmiewa inne telefony, które nie potrafią „jednocześnie chodzić i żuć gumy”.
Niestety, wielozadaniowość to jedna z cech, które najczęściej są błędnie rozu-
miane (także z technicznego punktu widzenia). Od lat używamy laptopów i kom-
puterów stacjonarnych. To tego rodzaju urządzenia ukształtowały nasze myślenie
o wielozadaniowości. Kiedy rozpoczynamy otwieranie strony w przeglądarce,

199
200 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

a następnie zmieniamy okno i piszemy tekst w edytorze, oczekujemy, że strona


nadal będzie się wczytywać. Programiści często uruchamiają budowanie kodu,
a następnie w trakcie trwania tego procesu przełączają się do innego programu.
Jak byś zareagował, gdyby system wstrzymał budowanie po tym, jak otworzyłeś
inne okno? A właśnie tak wygląda wielozadaniowość w świecie aplikacji mobil-
nych. Z tego rozdziału dowiesz się, że usługi w Androidzie umożliwiają realizowa-
nie wielozadaniowości, kiedy nie można zastosować jej w tradycyjnym modelu.
Najpierw należy zrozumieć, jak działa wielozadaniowość na urządzeniach
z Androidem.

5.1. Wielozadaniowość jest najważniejsza


Łatwy sposób na zrozumienie znaczenia wielozadaniowości w urządzeniach
komputerowych to chwilowa rezygnacja z tej funkcji. W niektórych aplikacjach
wielozadaniowość nie ma istotnego znaczenia, ponieważ program działa tylko
w ramach danego urządzenia. Przykładową aplikacją tego typu jest program do
zapisywania notatek. Jeśli aplikacja służy tylko do zapamiętywania notatek na
urządzeniu, pewnie nie ma znaczenia, czy działa ona w trybie wielozadaniowości.
Jeżeli jednak aplikacja zapisuje notatki na zdalnym serwerze, co pozwala na dostęp
do nich (w trybie do odczytu i zapisu) z dowolnego komputera i urządzenia,
wielozadaniowość staje się przydatna. Dlaczego? Możliwość działania aplikacji
w tle pozwala synchronizować notatki przechowywane na urządzeniu i na ser-
werze. Bez tej funkcji konieczne jest synchronizowanie danych z serwerem po
każdym uruchomieniu programu. Może się wydawać, że ma to niewielkie zna-
czenie, jednak operacje sieciowe bywają długie. Użytkownik odczuje to przy
każdym włączeniu aplikacji. W praktyce może to być pierwsza rzecz, jaką zauważy
po uruchomieniu programu. Czy nie uważasz, że mogą towarzyszyć temu nie-
przyjemne odczucia?
Oczywiste rozwiązanie polega na umożliwieniu aplikacji działania w tle w nie-
skończoność (tak pracują aplikacje na komputerach stacjonarnych). Jednak to,
co funkcjonuje dobrze na komputerach stacjonarnych, nie zawsze sprawdza się
w urządzeniach przenośnych. Głównym problemem jest ilość pamięci. Na kom-
puterach stacjonarnych jest jej dużo. Kiedy ilość dostępnej pamięci spada, sys-
tem wykorzystuje pamięć wirtualną lub stronicowanie, aby zwiększyć tę ilość
przez udostępnienie dysku twardego lub podobnego nośnika. Kiedy aplikacja
nie działa na pierwszym planie, przeznaczona dla niej pamięć jest często prze-
noszona do pamięci wirtualnej. Kiedy program ponownie zaczyna pracować na
pierwszym planie, dane trzeba umieścić z powrotem w zwykłej pamięci. Proces
ten bywa długi i spowalnia pracę komputera.
W urządzeniach przenośnych ilość dostępnej zwykłej pamięci jest niska. Łatwo
sobie wyobrazić, że przy standardowym podejściu konieczne byłoby przenoszenie
wielu aplikacji do pamięci wirtualnej i z niej. Każde przełączenie programu pro-
wadziłoby do zaprzestania reagowania urządzenia. Nikt nie chciałby go używać.
5.2. Do czego służą usługi i jak z nich korzystać? 201

Na urządzeniach z Androidem aplikacja przenoszona w tło kontynuuje dzia-


łanie w podobny sposób, jak robią to programy na komputerach stacjonarnych.
Aplikacja może działać tak przez długi czas, jednak nie ma na to gwarancji. Jeśli
(lub kiedy) ilość wolnej pamięci spada, system operacyjny Androida kończy pracę
aplikacji. Może się to wydawać „bezwzględne”, ale to tylko pozory. Dzięki temu
rozwiązaniu nie trzeba stosować pamięci wirtualnej i plików wymiany, o czym
wspomnieliśmy w rozdziale 3. Ponadto system operacyjny zgłasza zdarzenia, aby
poinformować aplikację o jej planowanym zamknięciu. Pozwala to zachować
stan programu.
Gdyby na tym kończyła się wielozadaniowość w Androidzie, musielibyśmy
przyznać, że nie da się uznać go za wielozadaniowy system operacyjny. Jeśli użyt-
kownik ma szczęście, przez pewien czas może korzystać z wielozadaniowości,
jednak trudno jest projektować aplikacje pod kątem opisanego modelu. Co gorsza,
gdyby rozwiązanie działało w przedstawiony sposób, nie mielibyśmy o czym
pisać w tym rozdziale! Na szczęście dla nas wszystkich Android oferuje więcej
możliwości. Są one oparte na usługach. Pierwszy raz wspomnieliśmy o nich
w rozdziale 2. Teraz pora dokładniej przyjrzeć się temu mechanizmowi. W tym
rozdziale omawiamy liczne aspekty usług.
Zaczynamy od podstaw — tworzenia usług i ich automatycznego urucha-
miania w momencie rozruchu urządzenia. Poznasz dwa najczęściej stosowane
wzorce projektowe dotyczące usług, związane ze scentralizowanym dostępem
do danych i zapisywaniem ich w pamięci podręcznej oraz okresowym urucha-
mianiem usług w celu sprawdzenia zdalnie zgłoszonych zdarzeń (i opcjonalnie
publikowania powiadomień na ich temat). Te zagadnienia prowadzą do plano-
wania wykonania usług i upewnienia się, że plany są realizowane nawet w sytu-
acji, kiedy urządzenie jest uśpione lub dysponuje małą ilością pamięci. W koń-
cowej części rozdziału poznasz wprowadzoną w wersji Android 2.2 usługę
Cloud to Device Messaging i zobaczysz, jak używać zdalnych serwerów do pla-
nowania wykonywania usług i interakcji z nimi. Zaczynamy od wyjaśnienia, dla-
czego warto stosować usługi.

5.2. Do czego służą usługi i jak z nich korzystać?


Już o tym wspominaliśmy, ale warto to powtórzyć — usługi umożliwiają pełną
realizację wielozadaniowości w Androidzie. Potrzebne są też inne technologie
(które również omawiamy), jednak to usługi są podstawowym mechanizmem
służącym do realizowania w Androidzie wielozadaniowości wszelkiego rodzaju.
Jeśli chcesz uruchomić zadanie niezależnie od głównej aplikacji, pomyśl o użyciu
usługi. Załóżmy, że musisz przesłać duży plik na zdalny serwer. Może to zająć
sporo czasu. Możliwe, że przed zakończeniem przesyłania pliku użytkownik
przejdzie do innej aplikacji. Jeśli przesyłanie jest powiązane z programem, może
zostać ukończone, o ile program ten działa w tle. Jeżeli jednak system zamknie
aplikację, aby zwolnić pamięć, może to prowadzić do przerwania transferu.
202 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

Następny przykład dotyczy budowania złożonej struktury danych, powiedzmy,


obiektu klasy ContentProvider, na potrzeby wyszukiwania systemowego w Andro-
idzie. Proces budowania tego obiektu może obejmować pobieranie danych, prze-
twarzanie ich, a następnie zapisywanie na urządzeniu (prawdopodobnie w bazie
SQLite). Jest to jednorazowe zadanie, którego wykonanie zajmuje dużo czasu.
Nie należy wiązać tego zadania z cyklem życia aplikacji, ponieważ może to spo-
wodować, że zadanie to nie zostanie poprawnie ukończone. Usługi doskonale
nadają się do wykonywania tego rodzaju jednorazowych zadań (a także powta-
rzalnych operacji).
POBIERZ PROJEKT STOCKPORTFOLIO. Kod źródłowy
projektu i spakowany plik APK do uruchamiania go znaj-
dziesz w witrynie poświęconej książce Android w praktyce.
Ponieważ niektóre listingi w tekście skróciliśmy, abyś mógł
skoncentrować się na konkretnych zagadnieniach, zachęcamy
do pobrania kompletnego kodu źródłowego i śledzenia go
w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/APOO, plik APK: http://mng.bz/4iDX.
Na razie wszystko brzmi pięknie, ale przejdźmy do konkretnego przykładu.
W tym rozdziale rozwijamy aplikację o nazwie StockPortfolio. Umożliwia ona
śledzenie portfela akcji. Użytkownik może zobaczyć, ile akcji poszczególnych
spółek posiada i ile za nie zapłacił. Ponadto może ustawiać alarmy, żeby otrzy-
mywać powiadomienia o spadku lub wzroście ceny do określonego poziomu
i móc zareagować na to sprzedażą albo zakupem akcji. To prosta aplikacja, jednak
oparta na usługach wielozadaniowość przydaje się w niej w dwóch obszarach.
Po pierwsze, program wczytuje w tle najnowsze dane na temat akcji i zapisuje je
lokalnie w pamięci podręcznej. Dlatego kiedy użytkownik uruchamia program,
natychmiast widzi dokładne dane. Po drugie, aplikacja, działając w tle, może
sprawdzać, czy bieżące ceny akcji odpowiadają poziomowi, przy którym użytkow-
nik chce otrzymać powiadomienie. W ten sposób użytkownik może dostawać
powiadomienia bez konieczności uruchamiania aplikacji. Wszystko to wydaje się
proste, jednak na niektórych urządzeniach taki program nie będzie działał. Nawet
przy korzystaniu z Androida trzeba pamiętać o pewnych pułapkach. Z tego
rozdziału dowiesz się nie tylko tego, jak tworzyć potrzebne usługi, ale też jak
uruchamiać je okresowo w tle (nawet przy małej ilości dostępnej pamięci, kiedy
to system operacyjny musi zamknąć usługę).

0 TECHNIKA 13. Tworzenie usługi

Ten rozdział dotyczy usług. Opisujemy je tu bardzo szczegółowo. Zaczynamy


jednak od omówienia podstaw. Usługi mają pewne wyjątkowe cechy, ponieważ
zaprojektowano je na potrzeby przetwarzania w tle w modelu wielozadaniowości
0 TECHNIKA 13. Tworzenie usługi 203

z systemu operacyjnego Androida. Nie jest zaskoczeniem, że aby utworzyć i uru-


chomić usługę, nie wystarczy zaimplementować interfejs i wywołać metody.
PROBLEM
Aplikacja musi śledzić ceny akcji przez cały czas — nie tylko wtedy, kiedy pro-
gram działa na pierwszym planie.
ROZWIĄZANIE
W Androidzie do wykonywania operacji w tle służą usługi. Jeśli chcesz pobierać
dane w tle tylko wtedy, kiedy użytkownik korzysta z otwartej aplikacji, wystarczy
utworzyć wątek w głównej aktywności. Jeżeli chcesz, aby wątki działały rów-
nolegle, użyj klasy java.util.Timer. Wygodnym mechanizmem do zarządzania
utworzonymi wątkami i obsługi ich interakcji z interfejsem użytkownika jest
klasa AsyncTask Androida (znacznie więcej informacji o wątkach i klasie AsyncTask
znajdziesz w rozdziale 6.). Problem z tym podejściem polega na tym, że kiedy
aplikacja przestanie działać na pierwszym planie, system operacyjny może ją
zamknąć w dowolnym momencie.
Może się wydawać, że w praktyce system nigdy nie zakończy pracy programu.
Łatwo można utworzyć aplikację, która uruchamia obiekt klasy Timer, działający
po tym, jak program przestaje pracować na pierwszym planie. Taka aplikacja może
działać na testowym urządzeniu przez długi czas i wydaje się, że system nigdy
jej nie zamyka. Jest to jednak mylące, ponieważ urządzeń zwykle nie używa się
w opisany sposób. Standardowo użytkownicy korzystają z wielu różnych apli-
kacji, rozmawiają przez telefon, wysyłają e-maile itd. Wszystkie te operacje
wymagają pamięci, co zwiększa prawdopodobieństwo, że system zamknie apli-
kację. Nie daj się więc zmylić — jeśli program ma działać w tle, potrzebujesz
usługi. Aby ją utworzyć, musisz ją zadeklarować w pliku manifestu. Na listingu 5.1
pokazano deklarację usługi PortfolioManagerService z aplikacji do śledzenia
portfela akcji.

Listing 5.1. Deklaracja usługi PortfolioManagerService

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


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.portfolio"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".ViewStocks"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:process=":stocks_background"
android:name="PortfolioManagerService"
204 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

android:icon="@drawable/icon"
android:label="@string/service_name" />
</application>
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

Manifest jest prosty, więc możemy skoncentrować się na deklaracji usługi.


W Androidzie usługi są tak ważne, że utworzono dla nich specjalny znacznik!
Istotna jest pierwsza część deklaracji. Pierwszy zadeklarowany atrybut doty-
czy procesu usługi. Określono tu proces systemu operacyjnego, w którym to
procesie usługa ma działać. Atrybut procesu jest opcjonalny. Jeśli go pominiesz,
system uruchomi usługę w procesie aplikacji.
Uruchomienie usługi w procesie aplikacji powoduje, że system operacyjny
zmienia kategorię tego procesu. Ogólnie jest to korzystne, ponieważ zmniejsza
prawdopodobieństwo zamknięcia procesu w celu zwolnienia pamięci. Jednak
aplikacja i usługa współdzielą wtedy pamięć procesu, w którym działają. Zwięk-
sza to ryzyko wyczerpania pamięci i częstotliwość jej przywracania. Może to
prowadzić do niestabilnej pracy aplikacji, ponieważ czasem interfejs użytkow-
nika „zawiesza się” w czasie przywracania pamięci. Umieszczenie usługi w odręb-
nym nazwanym procesie pozwala uniknąć tego problemu.
Aby uruchomić usługę w nowym procesie, wystarczy podać atrybut process.
W kodzie wartość tego atrybutu to :stocks_background. Przedrostek w postaci
dwukropka jest ważny. Określa, że proces jest prywatny i może go używać tylko
dana aplikacja. Jedyny program, który może uruchamiać usługę lub wchodzić
z nią w interakcje, to właśnie ta aplikacja. Jeśli usuniesz dwukropek, usługa
będzie działać w odrębnym, ale globalnym procesie. Jeżeli do usługi dostęp
mają mieć inne aplikacje, możesz zastosować to rozwiązanie. Usługi globalne opi-
sujemy w dalszej części rozdziału.
Wróćmy do listingu 5.1. Dalej deklarujemy atrybut name usługi . Jest to
jedyny atrybut wymagany w deklaracji usługi. Służy do określania klasy usługi
(ustalanej — podobnie jak dla aktywności — w kontekście pakietu aplikacji).
Dalej znajdują się deklaracje dwóch innych opcjonalnych atrybutów usługi —
icon i label . System operacyjny Androida umożliwia użytkownikom wyświe-
tlenie wszystkich usług działających na urządzeniu i ich zatrzymanie. System
używa atrybutów icon i label, kiedy użytkownik wyświetla listę działających
usług. Listę tego rodzaju pokazano na rysunku 5.1.
Po zadeklarowaniu usługi trzeba ją zaimplementować. Można to łatwo zro-
bić, rozszerzając klasę android.app.Service. Nie trzeba pisać dużej ilości nowego
kodu, jednak często warto przesłonić metody cyklu życia usługi. Na listingu 5.2
przedstawiono podstawową strukturę klasy PortfolioManagerService.
0 TECHNIKA 13. Tworzenie usługi 205

Listing 5.2. Deklaracja klasy


PortfolioManagerService

public class PortfolioManagerService extends


´Service {
@Override
public void onCreate() {
// ...
}
@Override
public IBinder onBind(Intent intent) {
// ...
}
@Override
public void onDestroy() {
// ...
}
}

Rysunek 5.1. Wyświetlanie


uruchomionych usług

Kod na listingu 5.2 to zarys usługi (szczegółowy opis jej metod znajdziesz dalej).
Tu trzeba zaimplementować tylko jedną metodę — onBind . Metoda ta umoż-
liwia innym komponentom (zwykle aktywnościom, czasem innym usługom)
komunikowanie się z daną usługą. Pamiętaj, że usługa zwykle działa we własnym
procesie, dlatego do skomunikowania się z nią nie wystarczy wywołać jej metodę.
Niezbędna jest komunikacja międzyprocesowa. Kanał do takiej komunikacji two-
rzony jest w metodzie onBind.
Inne metody, które zdecydowaliśmy się przesłonić na listingu 5.2, to onCreate
i onDestroy. Przesłanianie ich jest opcjonalne. Jeśli usługa wykonuje wszystkie
operacje w metodzie onBind (na przykład przesyła dane na zdalny serwer), nie
trzeba przesłaniać metody onCreate. Obliczenia wykonywane poza metodą onBind
zwykle umieszcza się w metodzie onCreate. Metoda onDestroy, jak wskazuje jej
nazwa, jest wywoływana w momencie zamykania usługi. W metodzie tej należy
zwolnić wszelkie zasoby używane przez usługę.
OMÓWIENIE
Poznałeś już podstawy deklarowania i tworzenia usług. Warto zapamiętać kilka
istotnych zagadnień. Po pierwsze, usługa działa w odrębnym procesie. Pozwala
to oddzielić ją od procesu aplikacji, dlatego usługa nie jest zamykana w momen-
cie kończenia pracy programu. Po drugie, ponieważ usługa działa w odrębnym
procesie, w interakcje z nią można wchodzić tylko przez mechanizm komuni-
kacji międzyprocesowej. Działanie tego mechanizmu w Androidzie omawiamy
w dalszej części rozdziału. Wcześniej jednak warto wspomnieć o innej metodzie
cyklu życia. W wielu aplikacjach warto zaimplementować metodę onStartCommand
(lub przestarzałą metodę onStart, jeśli piszesz kod na urządzenia z wersjami
206 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

Androida starszymi niż 2.0). Za pomocą tej metody można przekazać do urucha-
mianej po raz pierwszy usługi dodatkowe parametry. Jeśli chcesz udostępnić
usłudze parametry konfiguracyjne, standardowym sposobem jest właśnie użycie
tej metody. Możesz na przykład pozwolić użytkownikowi określić, jak często
aplikacja ma sprawdzać nowe dane na temat akcji. Nieraz pożądane jest automa-
tyczne uruchamianie usługi bez interwencji użytkownika. W następnej technice
pokazujemy, jak to zrobić.

0 TECHNIKA 14. Automatyczne uruchamianie usługi

Częstym zastosowaniem usługi jest okresowe pobieranie informacji i — jeśli


spełniony jest określony warunek — przesyłanie powiadomienia. Usługi dobrze
nadają się do wykonywania tego zadania. Kiedy jednak należy je uruchamiać?
PROBLEM
Chcemy wyświetlać użytkownikowi powiadomienia, jeśli cena akcji osiąga okre-
ślony poziom. Użytkownik nie powinien jednak musieć uruchamiać aplikacji
tylko po to, aby otrzymywać powiadomienia. System powinien automatycznie uru-
chamiać usługę zaraz po rozruchu urządzenia.
ROZWIĄZANIE
Rozwiązanie polega na użyciu odbiornika zgłoszeń zdarzenia BOOT_COMPLETED
Androida. System operacyjny zgłasza to zdarzenie bezpośrednio po zakończeniu
rozruchu urządzenia. Omawiane zdarzenie umożliwia łatwe wykonywanie ope-
racji po uruchomieniu urządzenia. Aby było to możliwe, trzeba zadeklarować
odbiornik w manifeście, co pokazano na listingu 5.3.

Listing 5.3. Deklaracja odbiornika dla zdarzenia zakończenia rozruchu

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


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flexware.stocks"
android:versionCode="1"
android:versionName="1.0">
...
<receiver android:name="PortfolioStartupReceiver"
android:process=":stocks_background">
<intent-filter>
<action android:name=
"android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
...
</manifest>

Listing 5.3 rozpoczyna się od deklaracji odbiornika. Wygląda ona podobnie do


deklaracji usługi (ma wiele takich samych atrybutów). Także tu deklarujemy klasę
odbiornika, posługując się atrybutem name . Dalej deklarujemy, że odbiornik
0 TECHNIKA 14. Automatyczne uruchamianie usługi 207

ma działać w procesie odrębnym od procesu głównej aplikacji . Jeśli porów-


nasz ten kod z kodem z listingu 5.1, zobaczysz, że odbiornik ma działać w tym
samym procesie co usługa.
Wróćmy do listingu 5.3. Ostatnią ważną rzeczą w deklaracji odbiornika jest
określenie, jakiego rodzaju zdarzenia należy odbierać . Wykorzystujemy tu znany
już (mamy nadzieję) model intencji i filtrów. W Androidzie BOOT_COMPLETED to
predefiniowane zdarzenie (inaczej akcja). Jego zgłoszenia może odbierać także
wiele innych odbiorników. Każdy z nich może wykonać odpowiednie operacje
w reakcji na uruchomienie urządzenia. Po zadeklarowaniu odbiornika trzeba go
zaimplementować. Kod odbiornika znajduje się na listingu 5.4.

Listing 5.4. Uruchamianie usługi za pomocą odbiornika

public class PortfolioStartupReceiver extends BroadcastReceiver {


@Override
public void onReceive(Context context, Intent intent) {
Intent stockService =
new Intent(context, PortfolioManagerService.class);
context.startService(stockService);
}
}

Przedstawiony odbiornik jest tak prosty, jak to możliwe. Tworzy nową intencję
i używa jej do uruchomienia usługi . Prowadzi to do uruchomienia metod
onCreate i onStartCommand usługi oraz późniejszego zwrócenia sterowania do
odbiornika. Ponieważ odbiornik powinien szybko zwracać sterowanie, obie
wymienione metody usługi powinny działać szybko. Dlatego jeśli musisz wyko-
nać w tych metodach czasochłonne operacje, lepiej zrobić to w odrębnym wątku.

Instalowanie aplikacji na kartach SD


Jedną z najbardziej wyczekiwanych funkcji Androida 2.2 była możliwość instalo-
wania aplikacji na kartach SD zamiast w pamięci wewnętrznej. Wydaje się, że to
bardzo atrakcyjne rozwiązanie dla użytkowników, ponieważ na takich kartach
jest znacznie więcej miejsca niż w pamięci wewnętrznej. Jeśli jednak udostępnisz
tę możliwość, uważaj przy korzystaniu ze zdarzenia rozruchu urządzenia.
Zdarzenie BOOT_COMPLETED jest zgłaszane przed zamontowaniem karty SD. Apli-
kacja nie jest wtedy jeszcze dostępna. Istnieje jednak inne, podobne zdarzenie,
ACTION_EXTERNAL_APPLICATIONS_AVAILABLE, którego zgłoszenie też można odbie-
rać. System zgłasza to zdarzenie po zamontowaniu karty SD. Jeśli aplikacja jest
zainstalowana na takiej karcie, można odbierać zgłoszenia wspomnianego zdarze-
nia i w reakcji na nie uruchamiać usługi.
W czasie, kiedy powstawała ta książka, w Androidzie występował znany błąd 8485,
który mógł uniemożliwiać aplikacjom z karty SD odbieranie zgłoszeń podanego
zdarzenia.

OMÓWIENIE
Możesz się zastanawiać, dlaczego odbiornik trzeba uruchomić w odrębnym pro-
cesie. Wynika to z tego, że często warto współużytkować obiekty w usłudze
208 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

i odbiorniku, który ją uruchomił lub wywołał. Chcemy, aby odbiornik i usługa


działały w jednym procesie. Pozwala to uniknąć komunikacji międzyprocesowej.
Technikę tę stosujemy także dalej w rozdziale, kiedy opisujemy najlepsze prak-
tyki pozwalające na stałe podtrzymywanie działania usługi. W tym przykładzie
opisane rozwiązanie nie jest konieczne. Dalej zobaczysz inne sytuacje, w których
odbiornik jest wywoływany przez systemowy obiekt klasy AlarmManager lub
w wyniku powiadomień typu push kierowanych z chmury Google’a do usługi
Device Messaging; następnie wywołany odbiornik uruchamia usługę za pomocą
opisanej tu techniki.
Na zakończenie warto zauważyć, że uruchamianie usługi przy rozruchu urzą-
dzenia jest przydatne nie tylko w kontekście usług generujących powiadomie-
nia. Technika ta jest użyteczna także przy wstępnym pobieraniu danych i zapisy-
waniu ich w pamięci podręcznej przez usługę. Kiedy użytkownik otwiera aplikację
po raz pierwszy, wszystkie dane są już pobrane i gotowe do użycia. Zwiększa to
komfort korzystania z programu.

0 TECHNIKA 15. Komunikowanie się z usługą

Usługę można wykorzystać do wykonywania przydatnych zadań w tle. W roz-


dziale 2. przedstawiliśmy prosty przykład, w którym usługa publikowała powia-
domienia dla użytkowników. Zwykle jednak dane są przekazywane z usługi i do
niej. Tak działa usługa w aplikacji StockPortfolio.
PROBLEM
Trzeba poinformować usługę, które akcje ma obserwować. Usługa potrzebuje
dwóch informacji na temat akcji — symbolu akcji i poziomu ceny, który ma
prowadzić do wygenerowania powiadomienia dla użytkownika. Ponieważ usługa
ma działać w odrębnym procesie, przekazywanie do niej danych jest bardziej
skomplikowane niż wywołanie metody obiektu. Konieczna jest komunikacja mię-
dzyprocesowa. Na szczęście system operacyjny Androida ją obsługuje.
ROZWIĄZANIE
Do przesyłania danych do usługi potrzebujemy mechanizmu komunikacji mię-
dzyprocesowej z Androida. Mechanizm ten umożliwia udostępnianie usług innym
procesom i przesyłanie zserializowanych danych między różnymi procesami.
Przypomina to rozbudowane mechanizmy komunikacji międzyprocesowej, na
przykład z technologii CORBA i Windows COM. Technologie te obsługują język
IDL (ang. interface definition language), służący do opisywania interfejsu udo-
stępnianego elementu i klasy pośredniczącej używanej przez klienty interfejsu.
W Androidzie obowiązuje podobny schemat. Dostępny jest nawet specjalny język
IDL — Android IDL (AIDL). Na listingu 5.5 przedstawiono napisany w tym języku
opis interfejsu usługi.
0 TECHNIKA 15. Komunikowanie się z usługą 209

Listing 5.5. Plik IStockService.aidl. Zewnętrzny interfejs usługi aplikacji


StockPortfolio

package com.flexware.stocks.service;

import com.flexware.stocks.Stock;

interface IStockService{
void addToPortfolio(in Stock stock);
List<Stock> getPortfolio();
}

Na listingu 5.5 widać, że AIDL w dużym stopniu przypomina Javę. Podobnie


jak w Javie używane są tu pakiety i instrukcje importu. Główna różnica polega na
tym, że importować można tylko inne definicje w AIDL-u. Tu importujemy obiekt
klasy Stock . Jest to ta sama klasa Stock, której używamy w interfejsie użytkow-
nika aplikacji (wkrótce zobaczysz, w jaki sposób). Interfejs jest prosty. Udo-
stępnia zewnętrznym jednostkom tylko dwie metody . Warto zauważyć, że
w pierwszej metodzie używamy typu Stock, a parametr wejściowy ma modyfika-
tor in. Oznacza to, że parametr jest przekazywany do metody, ale nie jest zwracany
do jednostki wywołującej tę metodę. Modyfikator trzeba zastosować, ponieważ
Stock to typ złożony. Gdyby był to typ prosty Javy, modyfikator moglibyśmy
pominąć.

Typy i parametry w AIDL-u


Dodanie do parametru wejściowego modyfikatora in przypomina użycie modyfi-
katora final w Javie. Zmienić można wartość dowolnego parametru wejściowego,
jeśli jednak taki parametr ma modyfikator in, nowa wartość nie jest przekazywana
do jednostki wywołującej metodę. Modyfikator in to znacznik kierunkowy (ang.
directional tag). Można podać także dwie inne wartości — out i inout. Modyfika-
tor out oznacza, że przekazane dane są ignorowane. Usługa tworzy wtedy pusty
parametr lub przypisuje do niego wartość domyślną, a z powrotem przekazywana
jest ostateczna wartość parametru. Modyfikator inout określa, że do metody należy
przekazać wartość, którą można potem zmienić i przekazać z powrotem. Ważne
jest, aby wiedzieć, co jest potrzebne. Dane przesyłane w ramach komunikacji
międzyprocesowej są szeregowane i rozszeregowywane. Proces ten bywa kosz-
towny. Parametr z modyfikatorem inout jest szeregowany i rozszeregowywany
dwukrotnie. Jak już wspomnieliśmy, dla wartości typów prostych nie trzeba poda-
wać znacznika kierunkowego. Wartości te działają tylko w trybie in — nie można
ich zmieniać.

Na podstawie przedstawionej krótkiej definicji można wygenerować dużo kodu.


Jeśli korzystasz z wiersza poleceń, użyj narzędzia AIDL. Jeśli pracujesz w środo-
wisku Eclipse, automatycznie wygeneruje ono kod na podstawie znalezionych
w projekcie plików .aidl. Wygenerowane klasy Javy zapisywane są w katalogu
/gen (tam też umieszczany jest wygenerowany plik R.java). Środowisko w celu
wygenerowania kodu musi rozwikłać instrukcje importu. Potrzebny jest do tego
następny plik .aidl. Oto on:
210 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

package com.flexware.stocks;
parcelable Stock;

Plik ten (Stock.aidl) obejmuje deklarację referencji do klasy Stock z listingu 5.5.
Zadeklarowano tu pakiet klasy (podobnie jak w kodzie w AIDL-u z listingu 5.5),
jednak jedyną operacją w pliku jest podanie referencji do typu Parcelable. Podaną
klasę Javy (Stock) można wykorzystać w aplikacji, jednak trzeba ją przekształcić
na typ android.os.Parcel. Jest to typ serializowany, pozwalający na przesyłanie
egzemplarzy klasy między procesami. Klasę Stock przedstawiono na listingu 5.6.

Listing 5.6. Klasa Stock z implementacją interfejsu Parcelable, którą można


przesyłać w ramach komunikacji międzyprocesowej

public class Stock implements Parcelable{


// Określane przez użytkownika.
private String symbol;
private double maxPrice;
private double minPrice;
private double pricePaid;
private int quantity;
// Dynamicznie pobierane.
private String name;
private double currentPrice;
// Związane z klasą StocksDb.
private int id;
private Stock(Parcel parcel){
this.readFromParcel(parcel);
}
public static final Parcelable.Creator<Stock> CREATOR =
new Parcelable.Creator<Stock>() {

public Stock createFromParcel(Parcel source) {


return new Stock(source);
}

public Stock[] newArray(int size) {


return new Stock[size];
}
};
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(symbol);
parcel.writeDouble(maxPrice);
parcel.writeDouble(minPrice);
parcel.writeDouble(pricePaid);
parcel.writeInt(quantity);
}

public void readFromParcel(Parcel parcel){


symbol = parcel.readString();
maxPrice = parcel.readDouble();
inPrice = parcel.readDouble();
pricePaid = parcel.readDouble();
0 TECHNIKA 15. Komunikowanie się z usługą 211

quantity = parcel.readInt();
}
}

Listing ten obrazuje wszystkie podstawowe informacje o tworzeniu klasy z imple-


mentacją interfejsu Parcelable. Interfejs ten wymaga tylko zaimplementowa-
nia metody writeToParcel . Jak wskazuje nazwa („zapisz w paczce”), metoda
ta służy do serializowania egzemplarza klasy do obiektu typu Parcel . Klasa
Parcel obejmuje przydatne metody do serializowania typów prostych i łańcuchów
znaków. Wystarczy zaimplementować wspomnianą metodę, aby móc przesłać
egzemplarz klasy do innego procesu. Następnie trzeba jednak zdeserializować
obiekt typu Parcel, aby uzyskać obiekt klasy Stock. Środowisko uruchomieniowe
Androida używa do obsługi tych operacji statycznego pola o nazwie CREATOR .
Pole to ma typ Parcelable.Creator, w którym zdefiniowana jest metoda fabryczna
createFromParcel. Na listingu 5.6 w klasie typu Parcelable znajduje się metoda
readFromParcel , której obiekt typu Creator „zleca” wykonanie zadania. Klasa
Parcel ma kilka metod pomocnych przy pobieraniu zserializowanych danych
z obiektu tej klasy. Ważną rzeczą, na którą warto zwrócić uwagę, jest to, że
wartości z obiektu klasy Parcel trzeba wczytywać w tej samej kolejności, w jakiej
je zapisano. Na przykład: pole symbol to pierwsza wartość zapisywana do obiektu
klasy Parcel w metodzie writeToParcel, dlatego też jako pierwsze jest wczyty-
wane w metodzie readFromParcel.
Mamy już strukturę danych, którą można przesyłać tam i z powrotem między
procesem z główną aplikacją a procesem z usługami tła. Na listingu 5.5 zdefi-
niowane są operacje, które działająca w tle usługa udostępnia głównej aplikacji.
Na podstawie interfejsu z pliku .aidl można wygenerować interfejs w Javie. Można
zrobić to ręcznie (za pomocą narzędzia AIDL) lub automatycznie (jeśli używasz
środowiska Eclipse i wtyczki ADT). Na listingu 5.7 znajduje się wygenerowany kod.

Listing 5.7. Interfejs w Javie wygenerowany na podstawie interfejsu w AIDL-u

package com.flexware.stocks.service;
public interface IStockService extends android.os.IInterface
{
/** Lokalna klasa Stub z implementacją komunikacji międzyprocesowej. */
public static abstract class Stub extends android.os.Binder
implements com.flexware.stocks.service.IStockService
{
// Wygenerowany kod.
}
public void addToPortfolio(com.flexware.stocks.Stock stock)
throws android.os.RemoteException;
public java.util.List<com.flexware.stocks.Stock> getPortfolio()
throws android.os.RemoteException;
}

Tego można było oczekiwać na podstawie kodu w AIDL-u z listingu 5.5. Inter-
fejs i jego dwie operacje zostały bezpośrednio przekształcone. Jedynym ciekawym
212 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

aspektem jest klasa abstrakcyjna Stub . Jak wskazuje nazwa, jest to klasyczna
namiastka klasy z implementacją interfejsu (choć bez operacji, które także i tu
są abstrakcyjne). W klasie znajduje się duża ilość wygenerowanego szablono-
wego kodu. Należy rozszerzyć tę abstrakcyjną klasę przez zaimplementowanie
metod interfejsu IStockService. Można w ten sposób wykorzystać wygenerowany
szablonowy kod. Metoda onBind klasy usługi powinna zwracać obiekt nowej klasy
z implementacją. Na listingu 5.8 pokazano potrzebny kod.

Listing 5.8. Klasa PortfolioManagerService

public class PortfolioManagerService extends Service {


private final StocksDb db = new StocksDb(this);
// Inne metody pominięto.
@Override
public IBinder onBind(Intent intent) {
return new IStockService.Stub() {
public void addToPortfolio(Stock stock)
throws RemoteException {
db.addStock(stock);
}

public List<Stock> getPortfolio()


throws RemoteException {
return db.getStocks();
}
};
}
}

Klasa PortfolioManagerService to typowa usługa umożliwiająca zdalną komuni-


kację. W rozdziale 2. przedstawiliśmy usługę, która nie umożliwiała takiej komu-
nikacji, dlatego metoda onBind zwracała tam wartość null. Tu komunikacja
międzyprocesowa jest obsługiwana, dlatego trzeba zwrócić klasę pochodną od
wygenerowanej klasy Stub z listingu 5.7. W przykładzie implementacja jest
prosta (zadania są delegowane do klasy pomocniczej StocksDb), dlatego używamy
anonimowej wewnętrznej klasy pochodnej od klasy Stub. W klasie StocksDb uży-
wamy androidowej bazy SQLite do zapisywania akcji, o których informacje apli-
kacja ma na żądanie pobierać. Metoda addToPortfolio wstawia dane, a metoda
getPortfolio wykonuje proste zapytanie. Na zakończenie chcemy pokazać, jak
korzystać z usługi w głównej aplikacji. Na listingu 5.9 pokazujemy główną aktyw-
ność aplikacji. Zwróć uwagę na to, jak aktywność jest powiązana z usługą i jak
ją wywołuje.

Listing 5.9. Wiązanie głównej aktywności z usługą

public class ViewStocks extends ListActivity {

private ArrayList<Stock> stocks;


private IStockService stockService;
0 TECHNIKA 15. Komunikowanie się z usługą 213

private ServiceConnection connection = new ServiceConnection(){

public void onServiceConnected(ComponentName className,


IBinder service) {
stockService = IStockService.Stub.asInterface(service);
try {
stocks = (ArrayList<Stock>)
stockService.getPortfolio();
if (stocks == null){
stocks = new ArrayList<Stock>(0);
}
refresh();
} catch (RemoteException e) {
Log.e(LOGGING_TAG, "Wystąpił wyjątek przy pobieraniu
danych z usługi",e);
}
}
public void onServiceDisconnected(ComponentName className) {
stockService = null;
}
};
@Override
public void onStart(Bundle savedInstanceState) {
super.onStart();
bindService(new Intent(IStockService.class.getName()), connection,
Context.BIND_AUTO_CREATE);
... // Kod interfejsu użytkownika pominięto.
}
}

Na listingu 5.9 znajduje się fragment kodu aktywności ListActivity. Na początku


listingu 5.9 definiowany jest obiekt klasy ServiceConnection. Obiekt ten to delegat
informujący o cyklu życia połączenia ze zdalną usługą. Wygenerowanej namiastki
klasy używamy do pobrania interfejsu zdalnej usługi (reprezentowanego za pomocą
interfejsu android.os.IBinder) i uzyskania implementacji lokalnego interfejsu.
Dalej, w metodzie onStart aktywności, używamy metody bindService . Jest
ona dostępna w każdym obiekcie typu Context (na przykład w obiektach aktyw-
ności i usług), a służy do wiązania aplikacji ze zdalnymi usługami. Należy przeka-
zać nazwę klasy wiązanej usługi, delegata zarządzającego połączeniem i opcję
informującą, że w razie potrzeby system ma automatycznie utworzyć usługę.
Wywołanie usługi działającej w innym procesie odbywa się dużo szybciej niż
zgłaszanie wywołań przez sieć, jednak i tak jest to długa operacja, której nie należy
wykonywać w głównym wątku interfejsu użytkownika (metoda bindService asyn-
chronicznie wiąże usługę z aplikacją). Metoda onServiceConnected klasy Service
´Connection pełni funkcję wywołania zwrotnego w toku asynchronicznego wią-
zania usługi. Wywołanie tej metody oznacza, że usługa została powiązana z apli-
kacją. Można wtedy pobrać dane i odświeżyć interfejs użytkownika .
214 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

Widoczne procesy i powiązane usługi


W przykładzie aplikacja i usługa działają w odrębnych procesach, jednak dostępna
jest dla nich tylko określona ilość pamięci. W urządzeniach pierwszej generacji
z zainstalowanym Androidem jest to zwykle po 16 megabajtów pamięci na proces.
W urządzeniach drugiej generacji są to 24 megabajty na proces. Po rozdzieleniu
wszystkich 16- lub 24-megabajtowych porcji pamięci system operacyjny musi
zamknąć wybrane procesy. Poszczególnym procesom system przypisuje różne prio-
rytety, co opisano w rozdziale 3.

OMÓWIENIE
Komunikowanie się ze zdalną usługą to jedna z najbardziej skomplikowanych
technik w tej książce. Zadanie to obejmuje kilka stosunkowo prostych etapów.
Nie dziwią nas jednak wątpliwości, czy stosowanie tej techniki jest warte zachodu.
Złożoność wynika tu z komunikowania się między procesami. Oznacza to, że
trzeba utworzyć kanał komunikacji, a także szeregować i rozszeregowywać dane
przesyłane między procesami. Z pewnością warto stosować tę technikę, jeśli chcesz
oddzielić pracę aplikacji od interakcji z użytkownikiem. Jest to jedna z możli-
wości, które odróżniają Android od konkurencyjnych platform. Standardowym
zastosowaniem omawianej techniki jest używanie usług do zarządzania danymi
ze zdalnych serwerów i zapisywania takich informacji w pamięci podręcznej.

0 TECHNIKA 16. Wykorzystanie usługi do zapisywania danych


w pamięci podręcznej

Usługa często potrzebuje tych samych danych co główna aplikacja. Oba kompo-
nenty mogą pobierać dane i zarządzać nimi. Jednak, co pokazano we wcześniej-
szych punktach, główna aplikacja może komunikować się z usługą. Dlatego usługa
może zarządzać wszystkimi danymi, a jeśli są one pobierane z sieci WWW, usługa
może zapisywać informacje z serwera w pamięci podręcznej.
PROBLEM
Utworzyliśmy aplikację i powiązaną usługę działającą w tle. Zarówno główna
aplikacja, jak i usługa potrzebują danych ze zdalnego serwera. Chcemy zarządzać
dostępem do danych w jednym miejscu i zapisywać je w pamięci podręcznej,
ponieważ pobieranie ich przez sieć jest powolne i kosztowne. Odpowiadać ma
za to działająca w tle usługa, co pozwala pobierać dane nawet wtedy, kiedy główna
aplikacja nie pracuje. Dane można następnie udostępnić głównej aplikacji za
pomocą komunikacji międzyprocesowej z usługą.
ROZWIĄZANIE
Jest to standardowy wzorzec budowania aplikacji na Android. Popularność tego
rozwiązania wynika po części z jego prostoty. Jest ono oparte na opisanych wcze-
śniej technikach. Działającą w tle usługę można uruchamiać w momencie roz-
ruchu urządzenia. Następnie usługa ma pobierać dane przez sieć. Może to robić
0 TECHNIKA 16. Wykorzystanie usługi do zapisywania danych w pamięci podręcznej 215

okresowo. Po uruchomieniu aplikacji jedna z jego aktywności może nawiązywać


połączenie z usługą i wywoływać jedną z jej metod, aby otrzymać dane pobrane
przez usługę z sieci.
Ten prosty wzorzec wykorzystano w wielu popularnych aplikacjach na
Android. Jak można zastosować go w aplikacji do zarządzania portfelem akcji?
W przykładowym programie lista akcji śledzonych przez użytkownika jest prze-
chowywana lokalnie, w bazie SQLite. W celu śledzenia bieżącej ceny akcji dane
są pobierane z sieci. Utworzenie takiego rozwiązania wymaga jedynie zmodyfi-
kowania usługi. Na listingu 5.10 przedstawiono nową wersję usługi.

Listing 5.10. Usługa do zarządzania akcjami — wersja z pamięcią podręczną

public class PortfolioManagerService extends Service {


private final StocksDb db = new StocksDb(this);
private long timestamp = 0L;
private static final int MAX_CACHE_AGE = 15*60*1000;
// 15 minut.
@Override
public IBinder onBind(Intent intent) {
return new IStockService.Stub() {
public Stock addToPortfolio(Stock stock)
throws RemoteException {
Stock s = db.addStock(stock);
updateStockData();
return s;
}

public List<Stock> getPortfolio() throws RemoteException {


ArrayList<Stock> stocks = db.getStocks();
long currTime = System.currentTimeMillis();
if (currTime - timestamp <= MAX_CACHE_AGE){
return stocks;
}
Stock[] currStocks = new Stock[stocks.size()];
stocks.toArray(currStocks);
try {
ArrayList<Stock> newStocks =
fetchStockData(currStocks);
updateStockData(newStocks);
return newStocks;
} catch (Exception e) {
Log.e("PortfolioManagerService",
"Wystąpił wyjątek przy pobieraniu danych",e);
throw new RemoteException();
}
}
};
}
... // Kod do pobierania danych o akcjach pominięto.

Kod z listingu 5.10 to rozwinięcie usługi przedstawionej po raz pierwszy na lis-


tingu 5.2. Aby umożliwić zapis w pamięci podręcznej, trzeba zrobić kilka rzeczy.
Należy ustawić limit czasu , po którym zawartość pamięci jest uznawana za
216 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

przestarzałą i usługa pobiera dane z serwera. Aby ustalić świeżość zawartości


pamięci podręcznej, trzeba sprawdzić czas ostatniego wczytania danych z ser-
wera . Następnie do dwóch udostępnianych metod — addToPortfolio i get
´Portfolio — należy dodać kod do zarządzania pamięcią podręczną. W meto-
dzie addToPortfolio do lokalnej bazy danych dodawany jest obiekt klasy Stock,
po czym następuje wywołanie metody updateStockData . Metoda ta pobiera
dane z sieci, a następnie aktualizuje informacje o akcjach w lokalnej bazie danych.
Kod tej metody przedstawiamy dalej. Po dodaniu akcji nowej firmy należy pobrać
informacje na ich temat z sieci, dlatego przy okazji warto pobrać dane na temat
wszystkich akcji i zaktualizować pamięć podręczną.
Metoda getPortfolio najpierw pobiera zapisane dane z lokalnej bazy i spraw-
dza, czy są one wystarczająco świeże. W kodzie z listingu użyto prostej reguły —
dane z pamięci podręcznej są akceptowalne, jeśli mają mniej niż 15 minut. Łatwo
można wymyślić bardziej zaawansowane zasady i zastosować agresywniejszy
limit w godzinach pracy giełdy oraz mniej agresywny w innym okresie. Zastoso-
wane tu reguły są odpowiednie dla rozwijanej aplikacji, dlatego kod sprawdza,
czy od ostatniego zapisu danych minęło mniej niż 15 minut . Jeśli tak, należy
zwrócić dane z pamięci podręcznej. W przeciwnym razie trzeba pobrać dane
z sieci , a następnie zaktualizować pamięć podręczną świeżymi danymi.
W tym celu należy wywołać nową wersję metody updateStockData, przedstawioną
na listingu 5.11.

Listing 5.11. Aktualizowanie pamięci podręcznej z danymi o akcjach

private void updateStockData() throws IOException{


ArrayList<Stock> stocks = db.getStocks();
Stock[] currStocks = new Stock[stocks.size()];
currStocks = stocks.toArray(currStocks);
stocks = fetchStockData(currStocks);
updateStockData(stocks);
}

private void updateStockData(ArrayList<Stock> stocks){


timestamp = System.currentTimeMillis();
Stock[] currStocks = new Stock[stocks.size()];
currStocks = stocks.toArray(currStocks);
for (Stock stock : currStocks){
db.updateStockPrice(stock);
}
checkForAlerts(stocks);
}

Dwóch podanych metod usługa używa do odświeżania danych w pamięci pod-


ręcznej. Pierwsza metoda nie przyjmuje argumentów i jest wykorzystywana, kiedy
użytkownik dodaje nowe akcje. Metoda ta pobiera z lokalnej bazy pełną listę
akcji śledzonych przez użytkownika. Następnie wywołuje metodę fetchStock
´Data , aby pobrać najnowsze informacje o akcjach z sieci. W ostatnim kroku
0 TECHNIKA 17. Tworzenie powiadomień 217

deleguje zadanie do drugiej metody , która przyjmuje listę obiektów klasy


Stock i aktualizuje ceny akcji w bazie. Ta ostatnia metoda przechodzi następnie
po liście akcji i aktualizuje cenę każdej z nich .
OMÓWIENIE
Zapisywanie danych w pamięci podręcznej pozwala znacznie poprawić wydajność
każdej aplikacji. Im kosztowniejsze jest pobieranie danych, tym większe korzyści
daje używanie pamięci podręcznej. Ma to duże znaczenie w aplikacjach mobil-
nych, które często do działania wymagają danych ze zdalnych serwerów. Szyb-
kość połączenia sieciowego w sieciach mobilnych prawie nigdy nie jest bardzo
wysoka (a często jest wręcz niska). Zapisywanie danych w lokalnej bazie to dosko-
nały sposób na przechowywanie informacji w pamięci podręcznej. Umieszczenie
całego kodu do zarządzania danymi w działającej w tle usłudze umożliwia pobie-
ranie i aktualizowanie informacji w tle, niezależnie od poczynań użytkownika
aplikacji. Zarządzanie danymi w usłudze działającej w tle pozwala wykonywać na
nich dodatkowe operacje. Na podstawie danych pobranych z serwera aplikacje
często generują powiadomienia.

0 TECHNIKA 17. Tworzenie powiadomień

Wysyłanie powiadomień to jedna z najważniejszych funkcji aplikacji mobilnych.


Powiadomienia umożliwiają aplikacji asynchroniczną interakcję z użytkowni-
kami. Użytkownicy ci nie muszą bezpośrednio korzystać z aplikacji (program
nie musi być otwarty w urządzeniu), aby otrzymywać od niej ważne i pilne infor-
macje. Nie powinno zaskakiwać, że bardzo ważnym mechanizmem generowania
powiadomień są działające w tle usługi (w końcu są one najistotniejszym narzę-
dziem Androida umożliwiającym aplikacji asynchroniczne działanie).
PROBLEM
Chcemy powiadamiać użytkownika o ważnych zdarzeniach, nawet jeśli dana osoba
w czasie ich wystąpienia nie korzysta z aplikacji. Chcemy udostępniać szczegó-
łowe informacje o zdarzeniu i umożliwiać natychmiastowe użycie aplikacji do
odpowiedniego zareagowania na jego wystąpienie. Zdarzenie może zachodzić
w zdalnym systemie lub lokalnie, na urządzeniu. W obu przypadkach warto zasto-
sować różnorodne możliwości Androida do informowania użytkowników, tak aby
mogli reagować na zdarzenia.
ROZWIĄZANIE
Android udostępnia elastyczny i rozszerzalny system powiadomień. Najprostszy
typ powiadomień w Androidzie to powiadomienia typu toast. Powiadomień takich
często używa się w aktywnościach do informowania o zdarzeniu. Jednak powia-
domienia typu toast można też stosować w usługach. Powiadomienia te służą do
wyświetlania informacji użytkownikom (nie są interaktywne). Aby uzyskać pożą-
218 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

daną interaktywność, trzeba użyć klasy android.app.Notification. Klasa Notifi


´cation (po umieszczeniu jej w intencji) umożliwia użytkownikom komuniko-
wanie się z aplikacją. Powiadomienie można wyświetlać na pasku stanu, wygene-
rować po jego zgłoszeniu dźwięk, uruchomić wibracje telefonu, a nawet włączać
lampki LED.
W aplikacji do zarządzania portfelem akcji użytkownicy mogą wpisać mini-
malny i maksymalny poziom cen akcji każdej firmy. Za każdym razem po pobra-
niu z sieci najnowszych danych należy sprawdzić, czy ceny akcji którejś z firm nie
przekroczyły maksymalnego progu lub nie spadły poniżej minimalnego poziomu.
Na listingu 5.12 pokazano potrzebny do tego kod, który należy dodać do usługi.

Listing 5.12. Sprawdzanie, czy ceny nie osiągnęły maksymalnego lub minimalnego
poziomu

private void updateStockData(List<Stock> stocks){


// Dawny kod pominięto.
checkForAlerts(stocks);
}

private void checkForAlerts(Iterable<Stock> stocks){


for (Stock stock : stocks){
double current = stock.getCurrentPrice();
if (current > stock.getMaxPrice()){
createHighPriceNotification(stock);
continue;
}
if (current < stock.getMinPrice()){
createLowPriceNotification(stock);
}
}
}

Kod do sprawdzania cen najlepiej jest wywoływać po aktualizacji przechowy-


wanych lokalnie danych nowymi informacjami z sieci. Należy przejść po cenach
akcji wszystkich spółek i utworzyć powiadomienie, jeśli bieżąca cena jest wyż-
sza od ustawionego maksimum lub niższa od minimum. Warto zauważyć, że
do tworzenia poszczególnych powiadomień służą odrębne metody. Na listingu 5.13
przedstawiono metodę do generowania powiadomień o przekroczeniu maksy-
malnej ceny.

Listing 5.13. Tworzenie powiadomień o przekroczeniu maksymalnej ceny

private static final int HIGH_PRICE_NOTIFICATION = 1;


private void createHighPriceNotification(Stock stock) {
NotificationManager mgr = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
int dollarBill = R.drawable.dollar_icon;
String shortMsg = "Powiadomienie o przekroczeniu maksimum: " + stock.getSymbol();
long time = System.currentTimeMillis();
Notification n = new Notification(dollarBill, shortMsg, time);
String title = stock.getName();
0 TECHNIKA 17. Tworzenie powiadomień 219

String msg = "Obecna cena " + stock.getCurrentPrice() +


" jest wysoka";
Intent i = new Intent(this, NotificationDetails.class);
i.putExtra("stock", stock);
PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);

n.setLatestEventInfo(this, title, msg, pi);


n.defaults |= Notification.DEFAULT_SOUND;
long[] steps = {0, 500, 100, 200, 100, 200};
n.vibrate = steps;
n.ledARGB = 0x80009500;
n.ledOnMS = 250;
n.ledOffMS = 500;
n.flags |= Notification.FLAG_SHOW_LIGHTS;
mgr.notify(HIGH_PRICE_NOTIFICATION, n);
}

W metodzie 5.12 wykorzystujemy wiele opcji tworzenia powiadomień. Najprostsze


rozwiązanie to wyświetlenie informacji (kursu akcji) na pasku stanu. Służące do
tego powiadomienie obejmuje ikonę (rysunek) , krótką wiadomość, a także czas
wyświetlenia informacji. Na tym moglibyśmy poprzestać, jednak chcemy, aby
powiadomienie umożliwiało podjęcie działań. Dlatego wybranie powiadomienia
przez użytkownika prowadzi do uruchomienia aktywności. Wymaga to użycia
intencji . Zauważ, że obiekt klasy Stock powiązany z powiadomieniem jest
dodawany do intencji jako dodatkowy element. Można tak postąpić, ponieważ
Stock to klasa typu Parcelable, dlatego system operacyjny może łatwo seriali-
zować i deserializować obiekty tej klasy. Intencja jest następnie umieszczana
w obiekcie klasy PendingIntent (reprezentuje ona intencje, które zostaną urucho-
mione w przyszłości).
W dalszym kodzie znajdują się wybrane inne opcje, które można wykorzy-
stać, aby ułatwić użytkownikowi zauważenie powiadomienia. Urządzenie może
na przykład odtwarzać dźwięk . Tu używamy domyślnego dźwięku ustawionego
przez użytkownika dla powiadomień. Można też dołączyć do aplikacji własny
plik dźwiękowy. Dalej dla powiadomienia włączane są wibracje . W tym celu
należy przekazać tablicę wartości typu long. Pierwsza z nich określa czas do
włączenia wibracji. Dalej następuje seria wartości oznaczających na przemian
czas generowania wibracji i długość przerw między nimi. Po dojściu do końca
tablicy telefon wyłącza wibracje. Ponadto można też włączać lampki LED tele-
fonu . Rodzaj (i dostępność) takich światełek zależy od urządzenia. Jeśli jednak
zastosujesz mechanizm, którego urządzenie nie udostępnia, system operacyjny
pominie dane ustawienia. Tu w notacji szesnastkowej ustawiliśmy dla lampek
LED kolor zielony z przestrzeni barw ARGB, a dalej podaliśmy schemat włącza-
nia i wyłączania światła. Wzorzec jest powtarzany w nieskończoność.
Jeśli (lub kiedy) użytkownik rozwinie pasek stanu, aby zobaczyć więcej infor-
macji z powiadomienia, ujrzy wartości zmiennych contentTitle i contentText. Na
listingu 5.12 wartości te określono za pomocą metody setLatestEventInfo. Metoda
220 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

ta przyjmuje obiekt klasy PendingIntent, dlatego jeśli użytkownik otworzy powia-


domienie, intencja zapisana w tym obiekcie posłuży do uruchomienia powiązanej
aktywności. Wspomniana metoda to metoda pomocnicza, która umożliwia poda-
nie potrzebnych wartości i połączenie ich z wbudowanym widokiem. Można też
użyć własnego, niestandardowego widoku. Na listingu 5.14 znajduje się kod nie-
standardowego widoku używanego do tworzenia powiadomień o spadku kursu
poniżej minimum.

Listing 5.14. Tworzenie powiadomień o spadku ceny poniżej minimum

private static final int LOW_PRICE_NOTIFICATION = 0;


private void createLowPriceNotification(Stock stock){
NotificationManager mgr = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
int dollarBill = R.drawable.dollar_icon;
String shortMsg = "Powiadomienie o spadku poniżej minimum: " + stock.getSymbol();
long time = System.currentTimeMillis();
Notification n = new Notification(dollarBill, shortMsg, time);
String pkg = getPackageName();
RemoteViews view =
new RemoteViews(pkg, R.layout.notification_layout);
String msg = "Obecna cena " + stock.getCurrentPrice() +
" jest niska";
view.setTextViewText(R.id.notification_message, msg);
n.contentView = view;
Intent i = new Intent(this, NotificationDetails.class);
i.putExtra("stock", stock);
PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);
n.contentIntent = pi;
n.defaults |= Notification.DEFAULT_SOUND;
long[] steps = {0, 500, 100, 500, 100, 500, 100, 500};
n.vibrate = steps;
n.ledARGB = 0x80A80000;
n.ledOnMS = 1;
n.ledOffMS = 0;
n.flags |= Notification.FLAG_SHOW_LIGHTS;
mgr.notify(LOW_PRICE_NOTIFICATION, n);
}

Metoda createLowPriceNotification z listingu 5.13 przypomina metodę create


´HighPriceNotification. Komunikaty, ikony, wzorzec wibracji i światełka są nieco
inne, ale wykorzystano te same interfejsy API co na listingu 5.12. Istotną różnicą
jest to, że nie używamy metody setLastEventInfo obiektu klasy Notification.
W zamian stosujemy niestandardowy widok. Tworzenie widoku jest tu skompli-
kowane, ponieważ należy zrobić to w działającej w tle usłudze, wykonywanej
w procesie odrębnym od procesu aplikacji. Ponieważ widok jest tworzony w usłu-
dze, nie można nawet wykorzystać usługi systemowej do przekształcania układów
na klasy, gdyż przekształcenie widoku wymaga użycia aktywności. Na szczęście
Android udostępnia klasę RemoteViews, która pozwala rozwiązać ten problem.
Trzeba tylko podać nazwę pakietu z aplikacją i widok w formacie XML , aby
przekształcić ten drugi na klasę. Przekształcany widok pokazano na listingu 5.15.
0 TECHNIKA 17. Tworzenie powiadomień 221

Listing 5.15. Niestandardowy układ w formacie XML używany w powiadomieniu

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notification_layout_root"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="5dp">
<ImageView android:id="@+id/notification_icon_left"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="5dp"
android:src="@drawable/radioactive_icon"
/>
<TextView android:id="@+id/notification_message"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:textColor="#000"
/>
<ImageView android:id="@+id/notification_icon_right"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginLeft="5dp"
android:src="@drawable/radioactive_icon"
/>
</LinearLayout>

Widok dla powiadomienia jest oparty na poziomym układzie LinearLayout.


Widoczny jest komunikat tekstowy , po którego lewej i prawej stronie znaj-
dują się ikony. Komunikat tekstowy umieszczony jest w obiekcie klasy TextView
o określonym identyfikatorze, co pozwala pobrać ten obiekt i ustawić jego
tekst. Należy to zrobić w metodzie setLowPriceNotification, która jest częścią
działającej w tle usługi. Znana już metoda findViewById jest dostępna tylko
w aktywności, a nie w usłudze. Na szczęście klasa RemoteViews udostępnia wiele
pomocnych metod. Wróćmy do listingu 5.13. Aby ustawić tekst pojawiający się
w powiadomieniu, używamy metody setTextViewText. Klasa RemoteViews obej-
muje kilka innych zbliżonych metod przydatnych w podobnych sytuacjach.
Kiedy widok jest już gotowy, należy ustawić go w powiadomieniu jako widok
contentView. Warto też zauważyć, że w powiadomieniu trzeba również ustawić
intencję contentIntent. W metodzie setHighPriceNotification nie było to konieczne,
ponieważ użyliśmy metody setLastEventInfo, która sama wprowadza odpowied-
nie ustawienia.
OMÓWIENIE
Android zapewnia programistom aplikacji bogaty zestaw interfejsów API do two-
rzenia powiadomień i zarządzania nimi. W przedstawionej technice omówili-
śmy wiele możliwości z tego obszaru. Jednak czy naprawdę chcesz, aby telefon
odtwarzał dźwięki, uruchamiał wibracje na kilka sekund i świecił lampkami LED
przy każdym przesłaniu powiadomienia? To pytanie retoryczne. Poważne jest
222 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

natomiast pytanie, jaki jest sens stosowania tych wszystkich „bajerów”. Android
udostępnia znacznie więcej możliwości niż inne popularne platformy mobilne,
ale czy daje to jakieś korzyści? Można łatwo przesadzić ze stosowaniem dostęp-
nych funkcji (podobnie zresztą jak przy używaniu innych mechanizmów). Jednak
bogate możliwości pozwalają tworzyć charakterystyczne powiadomienia, co jest
korzystne.
Warto pamiętać, że powiadomienia zwykle zgłaszane są, kiedy użytkownik
korzysta z innej aplikacji lub — co jeszcze częstsze — w ogóle nie używa telefonu.
Możliwe, że urządzenie znajduje się w kieszeni lub leży na biurku. Jeśli powia-
domienie jest charakterystyczne, użytkownik bez spoglądania na wyświetlacz
rozpozna, że informacje pochodzą z danej aplikacji. Zwiększa to prawdopodobień-
stwo zareagowania na powiadomienie i włączenia aplikacji, co jest pożądane.
Połączenie działających w tle usług z powiadomieniami jest wygodne i daje
dużo możliwości. Jednak aby móc skutecznie wykorzystać te techniki, trzeba
poznać mechanizm planowania i jego współdziałanie z cyklem życia usług.

5.3. Planowanie i usługi


Uruchamianie programu w tle na tradycyjnych komputerach lub serwerach
stacjonarnych jest proste. W mobilnych systemach operacyjnych, na przykład
w Androidzie, jest to dużo trudniejsze z uwagi na małą ilość pamięci. Programy
działające w tle mogą zostać zamknięte przez system operacyjny w celu zwolnie-
nia pamięci, którą trzeba przydzielić używanej właśnie aplikacji. Dla użytkownika
takie działanie systemu operacyjnego jest bardzo dogodne, ponieważ gwarantuje,
że aplikacje będą reagować na jego dane wejściowe. Dla programistów sytuacja
jest mniej korzystna. Nie wystarczy uruchomić usługę, aby kod działał w tle w nie-
skończoność. Trzeba założyć, że system operacyjny zamknie usługę i konieczne
będzie jej ponowne uruchomienie. Niezbędne są do tego odpowiednie „haczyki”
w systemie operacyjnym. Na szczęście Android je udostępnia. Wcześniej progra-
miści z wykorzystaniem klasy android.app.AlarmManager musieli uzyskać dostęp
do systemowych usług alarmowych. Od czasu wprowadzenia w Androidzie 2.2
usługi Cloud to Device Messaging istnieje też inny sposób na przesyłanie z ser-
werów wywołań, które wzbudzają usługi na danym urządzeniu. W tym podroz-
dziale poznasz różne techniki, dzięki którym dowiesz się, jak za pomocą wymie-
nionych mechanizmów Androida zwiększyć niezawodność usług działających w tle.

0 TECHNIKA 18. Używanie klasy AlarmManager

Eksperci od Linuksa z pewnością znają alarmy systemowe i zegary z tego systemu.


Mechanizmy te są dostępne także w procesach Androida. Nie musisz jednak
zaglądać do podręcznika, aby móc ich użyć. Android udostępnia prosty interfejs
API Javy do ustawiania alarmów systemowych (zarówno jednorazowych, jak
i powtarzalnych). Jest to najważniejszy w Androidzie interfejs API do urucha-
0 TECHNIKA 18. Używanie klasy AlarmManager 223

miania programu w określonym późniejszym momencie i upewniania się, że kod


zostanie wykonany także wtedy, gdy aplikacja lub usługa w określonej chwili nie
będzie pracować.
PROBLEM
Usługa ma wykonać kod w pewnym momencie w przyszłości. Jednak nawet jeśli
usługa działa, nie można zagwarantować, że będzie tak w odpowiedniej chwili.
Gdyby można było zapewnić działanie usługi (lub gdyby akceptowalne było
niewykonanie kodu, jeśli usługa nie pracuje), można by użyć klas Timer i TimerTask
Javy w połączeniu z klasą Handler Androida. Taką naiwną implementację przed-
stawiamy na listingu 5.16.

Listing 5.16. Używanie klas Timer i Handler do planowania wykonania usług


(NIE STOSUJ TEGO PODEJŚCIA!)

Calendar when = Calendar.getInstance();


when.add(Calendar.MINUTE, 2);
final Handler handler = new Handler();
TimerTask task = new TimerTask(){
@Override
public void run() {
handler.post(new Runnable(){
public void run() {
updateStockData();
}
});
}
};
Timer timer = new Timer();
timer.scheduleAtFixedRate(task, when.getTime(), 15*60*1000);

Jeśli dopuszczasz możliwość, że usługa i zaplanowane operacje zostaną zam-


knięte przez system operacyjny, możesz użyć kodu z listingu 5.15. W kodzie
wywoływana jest metoda updateStockData z listingu 5.11. Jej pierwsze wywołanie
ma miejsce dwie minuty od chwili uruchomienia kodu. Następnie metoda jest
wywoływana co 15 minut dopóty, dopóki usługa działa. Właściwe rozwiązanie
powinno funkcjonować w podobny sposób, jednak z wyłączeniem zastrzeżenia:
„dopóki usługa działa”. Chcemy zastąpić ten fragment stwierdzeniem: „dopóki
urządzenie jest włączone”.
ROZWIĄZANIE
Jeśli chcesz zagwarantować wykonanie kodu w określonym czasie, nie możesz
polegać na usługach, ponieważ system operacyjny zamyka je w celu zwolnienia
pamięci. Trzeba wykorzystać system operacyjny do planowania wykonania kodu,
a to wymaga użycia klasy android.app.AlarmManager. Usługi systemowe to na przy-
kład usługa przekształcająca układy na klasy lub menedżer powiadomień. W apli-
kacji do zarządzania portfelem akcji działa odbiornik uruchamiany po rozruchu
urządzenia. Dawna wersja odbiornika uruchamia usługę. Na listingu 5.17 poka-
zano nową wersję, która planuje wykonywanie usługi.
224 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

Listing 5.17. Używanie odbiornika informacji o rozruchu urządzenia do planowania


wykonywania usługi

public class PortfolioStartupReceiver extends BroadcastReceiver {


private static final int FIFTEEN_MINUTES = 15*60*1000;
@Override
public void onReceive(Context context, Intent intent) {
AlarmManager mgr = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);

Intent i = new Intent(context, AlarmReceiver.class);


PendingIntent sender = PendingIntent.getBroadcast(context, 0,
i, PendingIntent.FLAG_CANCEL_CURRENT);
Calendar now = Calendar.getInstance();
now.add(Calendar.MINUTE, 2);
mgr.setRepeating(AlarmManager.RTC_WAKEUP,
now.getTimeInMillis(),FIFTEEN_MINUTES, sender);
}
}

Jeśli porównasz listingi 5.16 i 5.4, zauważysz, że zmieniliśmy implementację


metody onReceive. Nowa wersja zamiast uruchamiać usługę, planuje jej wyko-
nanie. Tworzy intencję dla odbiornika, który odbiera alarm od obiektu klasy
AlarmManager. Warto zauważyć, że intencja znów umieszczona jest w obiekcie klasy
PendingIntent (podobną technikę zastosowaliśmy przy tworzeniu powiadomień).
Wynika to z tego, że intencję należy uruchomić w przyszłości, a nie w danym
momencie. Następnie używamy obiektu klasy AlarmManager do zaplanowania
uruchomienia obiektu klasy PendingIntent. Ustawienie typu alarmu na RTC_WAKEUP
to informacja dla systemu operacyjnego, że ma uruchomić alarm, nawet jeśli
urządzenie jest w trybie uśpienia (określa to przedrostek wakeup; człon RTC
oznacza, że czas wykonania należy mierzyć według czasu systemowego). Alarm
po raz pierwszy jest uruchamiany dwie minuty od momentu wykonania kodu,
a następnie co 15 minut. Warto zauważyć, że intencja nie dotyczy bezpośrednio
usługi, ale klasy AlarmReceiver. Klasę tę przedstawiono na listingu 5.18.

Listing 5.18. Klasa AlarmReceiver — odbiornik do obsługi alarmów systemowych

public class AlarmReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
Intent stockService =
new Intent(context, PortfolioManagerService.class);
context.startService(stockService);
}
}

Klasa ta powinna wyglądać znajomo. Jest to odpowiednik pierwotnej klasy


PortfolioStartupReceiver z listingu 5.4. Owa pierwotna klasa tworzy intencję
dla usługi PortfolioManagerService, a następnie natychmiast uruchamia tę usługę.
0 TECHNIKA 18. Używanie klasy AlarmManager 225

Jednak tu chcemy, aby usługa aktualizowała dane o akcjach i sprawdzała, czy


trzeba przesłać powiadomienie użytkownikowi. Na listingu 5.19 pokazano, w jaki
sposób należy zmodyfikować usługę.
CO OBEJMUJE INTENCJA? Może zauważyłeś, że metoda onReceive
odbiornika AlarmReceiver przyjmuje intencję (możesz to stwierdzić na
podstawie specyfikacji tej metody w klasie odbiornika). Jest to ta sama
intencja, którą utworzono w odbiorniku PortfolioStartupReceiver, umiesz-
czona w obiekcie klasy PendingIntent. Nie jest to jednak ten sam egzem-
plarz intencji, ponieważ system serializuje ją i deserializuje. Jednak
wszelkie dane dołączone (przez wywołanie metod putExtra) do intencji
utworzonej na listingu 5.16 są obecne w intencji z listingu 5.18; można je
pobrać za pomocą metod getExtra.

Listing 5.19. Zmodyfikowana usługa współdziałająca z alarmami systemowymi

public class PortfolioManagerService extends Service {


// Pozostały kod pominięto.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
updateStockData();
return Service.START_NOT_STICKY;
}
}

Aby usługa prawidłowo współdziałała z alarmami systemowymi, trzeba prze-


słonić pewną metodę cyklu życia klasy android.app.Service — onStartCommand.
Aplikacja wywołuje ją za każdym razem, kiedy w kontekście klienta wywoływana
jest metoda startService (na przykład w kodzie z listingu 5.18) — także wtedy,
gdy usługa już działa. W metodzie onStartCommand wystarczy wywołać metodę
updateStockData, ponieważ odpowiada ona za pobranie świeżych danych z sieci,
aktualizuje dane zapisane w lokalnej bazie, sprawdza, czy trzeba wysłać powia-
domienia, a jeśli tak jest — wysyła je.
Zauważ, że metoda updateStockData musi zwracać liczbę całkowitą. Wartość
tej liczby to informacja dla systemu operacyjnego, co ma zrobić z usługą, jeśli
trzeba ją zamknąć. Opcja START_NOT_STICKY oznacza, że system operacyjny nie
musi „przejmować się” usługą po jej zamknięciu. W omawianym przykładzie ma
to sens, ponieważ wiadomo, że alarm powoduje późniejsze ponowne uruchomie-
nie usługi. Inna opcja, którą można zwrócić, to START_STICKY. Wtedy system ope-
racyjny powinien sam ponownie uruchomić usługę.
ZDARZENIA ONSTART I ONSTARTCOMMAND USŁUGI. Jeśli poszu-
kasz w internecie przykładów okresowego uruchamiania usługi, możesz
natrafić na kod, w którym przesłonięta jest metoda onStart, a nie — jak
na listingu 5.18 — metoda onStartCommand. Metoda onStart to starsza
metoda cyklu życia, uznana za przestarzałą od wersji Android 2.0. W odróż-
nieniu od metody onStartCommand nie zwraca wartości, dlatego nie
226 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

udostępnia systemowi operacyjnemu żadnych informacji na temat tego,


co należy zrobić po zamknięciu usługi. Zawsze używaj metody onStartCom-
mand, chyba że piszesz kod przeznaczony konkretnie dla urządzeń z wer-
sjami Androida starszymi niż 2.0.
OMÓWIENIE
Korzystanie z klasy AlarmManager wydaje się łatwe. W końcu jest to tylko następny
zbiór interfejsów API będących częścią Androida. Klasa ta daje jednak duże
możliwości. Pozwala oddzielić wykonywanie kodu działającego w tle od procesu,
w którym kod ten jest uruchamiany. Przyjrzyj się obecnej wersji usługi. Rozpo-
czyna działanie dwie minuty po zakończeniu rozruchu urządzenia, a następnie co
15 minut pobiera dane z internetu do momentu wyłączenia sprzętu. Urządzenie
może nawet znajdować się w stanie uśpienia, a alarm i tak zostanie uruchomiony.
Aby uzyskać ten efekt, wystarczy określić typ alarmu (RTC_WAKEUP) w miejscu pla-
nowania jego wykonywania.
Na zapleczu klasa AlarmManager musi zająć blokadę wzbudzającą (ang. wake
lock), aby zapobiec uśpieniu urządzenia. Blokada ta jest utrzymywana w czasie
działania metody onReceive odbiornika, który otrzymuje zgłoszenie alarmu. Tu
odbiornikiem jest obiekt klasy AlarmReceiver przedstawionej na listingu 5.17.
Po zwróceniu sterowania przez metodę onReceive urządzenie znów może przejść
w stan uśpienia, co prowadzi do zakończenia pracy usługi. Następna technika
pozwoli Ci zapobiec zamknięciu usługi.

0 TECHNIKA 19. Podtrzymywanie działania usługi

W poprzedniej technice poznałeś klasę AlarmManager, a przede wszystkim dowie-


działeś się, jak przy jej użyciu wznowić działanie zamkniętej usługi. Jednak
wznowienie to może okazać się krótkotrwałe. Sam alarm to za mało. Ponadto
należy się upewnić, że usługa zakończyła pracę, czyli pobieranie świeżych danych
o akcjach z internetu i wysyłanie (w razie potrzeby) powiadomień. Aby osią-
gnąć ten cel, trzeba użyć androidowych interfejsów API do zarządzania zasila-
niem, a także dobrze zastanowić się nad działaniem procesów w Androidzie.
PROBLEM
Usługa powinna działać także po przejściu urządzenia w stan uśpienia. Chcemy,
aby urządzenie znajdowało się w stanie wzbudzenia do czasu utworzenia powia-
domień dla użytkownika. Nie powinno być tak, że użytkownik nie otrzyma powia-
domienia, ponieważ urządzenie znajduje się uśpione w jego kieszeni.
ROZWIĄZANIE
Do rozwiązania tego problemu potrzebujemy androidowego interfejsu API
PowerManager. Jest to usługa systemowa Androida umożliwiająca kontrolowa-
nie stanu urządzenia. Za pomocą wspomnianego interfejsu API można zająć
blokadę wzbudzającą. Zajęcie takiej blokady (odpowiada jej klasa WakeLock) pozwala
0 TECHNIKA 19. Podtrzymywanie działania usługi 227

aplikacji zapobiegać przejściu urządzenia w stan uśpienia (co związane jest


z wyłączeniem procesora). Jest to ważna funkcja, którą system operacyjny udo-
stępnia programistom. Musisz wymienić ją w elemencie <uses-permission>
w pliku AndroidManifest.xml. Jeśli nadużyjesz tego mechanizmu, będzie to
oczywiście miało poważny negatywny wpływ na zużycie energii. Dlatego istnieje
kilka typów blokad wzbudzających. Najczęściej używa się blokad PARTIAL_WAKE_
´LOCK. Powoduje ona włączenie procesora, ale ekran (i podświetlenie klawiatury
fizycznej, jeśli urządzenie ją posiada) pozostaje wyłączony. Ponieważ to ekran
zwykle powoduje największe zużycie energii, najlepiej jest zawsze, kiedy to moż-
liwe, używać blokad typu PARTIAL_WAKE_LOCK. Ich zaletą jest też to, że na takie
blokady nie ma wpływu wciśnięcie przycisku zasilania w urządzeniu. Inne typy
blokad (SCREEN_DIM_WAKE_LOCK, SCREEN_BRIGHT_WAKE_LOCK i FULL_WAKE_LOCK) powo-
dują włączenie ekranu, jednak z tego powodu użytkownik może wyłączyć blokadę
przyciskiem zasilania. Nie powinno więc nikogo dziwić, że dla działającej w tle
usługi używamy blokad PARTIAL_WAKE_LOCK.
Po wcześniejszym wstępie rozwiązanie problemu może wydawać się oczy-
wiste. Należy dodać do usługi kod, który zajmuje blokadę WakeLock w metodzie
onStartCommand, a następnie zwalnia ją po zakończeniu sprawdzania, czy należy
przesłać powiadomienia. Z podejściem tym związany jest jednak poważny pro-
blem. Jeśli urządzenie znajduje się w stanie uśpienia, blokada WakeLock zajęta przez
obiekt klasy AlarmManager jest zwalniana po zakończeniu pracy przez metodę
onReceive odbiornika AlarmReceiver. Może to stać się przed wywołaniem metody
onStartCommand usługi. Urządzenie może ponownie przejść w stan uśpienia, zanim
aplikacja zdąży zająć blokadę WakeLock. Dlatego blokadę tę trzeba zająć w meto-
dzie onReceive odbiornika AlarmReceiver, ponieważ jest to jedyne miejsce, w któ-
rym wykonywanie kodu z pewnością nie zostanie wstrzymane. Na listingu 5.20
znajduje się nowa, zmodyfikowana wersja odbiornika.

Listing 5.20. Zmodyfikowany odbiornik AlarmReceiver z funkcją zarządzania


zasilaniem

public class AlarmReceiver extends BroadcastReceiver {


private static PowerManager.WakeLock wakeLock = null;
private static final String LOCK_TAG = "com.flexware.stocks";
public static synchronized void acquireLock(Context ctx){
if (wakeLock == null){
PowerManager mgr = (PowerManager)
ctx.getSystemService(Context.POWER_SERVICE);
wakeLock =
mgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
LOCK_TAG);
wakeLock.setReferenceCounted(true);
}
wakeLock.acquire();
}
public static synchronized void releaseLock(){
if (wakeLock != null){
wakeLock.release();
228 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

}
}
@Override
public void onReceive(Context context, Intent intent) {
acquireLock(context);
Intent stockService =
new Intent(context, PortfolioManagerService.class);
context.startService(stockService);
}
}

W odbiorniku AlarmReceiver wprowadziliśmy poważne zmiany. Teraz znajduje


się w nim zmienna statyczna w postaci egzemplarza klasy WakeLock. Ponadto
dostępne są dwie metody do zajmowania i zwalniania blokady WakeLock. Uży-
wamy tu statycznego obiektu klasy WakeLock i statycznych metod do zajmowania
oraz zwalniania blokady, tak aby wspomniany obiekt mógł być współużytkowany
przez egzemplarz klasy AlarmReceiver i działającą w tle usługę. Standardowo w celu
współużytkowania elementów z uruchamianą usługą przekazuje się do niej owe
elementy w intencji (zwykle jako elementy dodatkowe), jednak obiekty przekazy-
wane w intencji muszą być typu Parcelable. Obiekt klasy WakeLock reprezentuje
ustawienia systemowe, dlatego nie jest typu Parcelable. Aby obejść problem,
wykorzystujemy statyczne zmienne i metody.
Warto pamiętać, że aby pokazana technika funkcjonowała poprawnie, odbior-
nik AlarmReceiver i usługa muszą działać w tym samym procesie (w przeciwnym
razie wystąpi trudny do wykrycia błąd). Jeśli oba te komponenty działają w jed-
nym procesie, wczytuje je ta sama ładowarka klas, dlatego komponenty współ-
użytkują statyczny obiekt klasy WakeLock. Jeżeli komponenty działają w różnych
procesach, używane są różne ładowarki klas i odrębne kopie obiektu klasy Wake
´Lock. Oto deklaracja odbiornika AlarmReceiver z pliku AndroidManifest.xml:
<receiver android:name="AlarmReceiver"
android:process=":stocks_background" />

Porównaj to z listingiem 5.1, a przede wszystkim z deklaracją klasy Portfolio


´ManagerService. W obu komponentach znajduje się ustawienie android:process=
´":stocks_background". Oba działają w tym samym procesie, odrębnym jednak
od procesu głównej aplikacji. Przy takiej konfiguracji rozwiązanie działa popraw-
nie. Teraz trzeba dodać do usługi PortfolioManagerService kod do zwalniania
obiektu klasy WakeLock, tak aby urządzenie mogło ponownie przejść w stan uśpie-
nia. Na listingu 5.21 pokazano zmodyfikowaną wersję metody checkForAlerts,
wzbogaconą o kod do zarządzania zasilaniem.

Listing 5.21. Zwalnianie obiektu klasy WakeLock po sprawdzeniu alarmów

private void checkForAlerts(Iterable<Stock> stocks){


try{
for (Stock stock : stocks){
double current = stock.getCurrentPrice();
if (current > stock.getMaxPrice()){
0 TECHNIKA 20. Używanie usługi Cloud to Device Messaging 229

createHighPriceNotification(stock);
continue;
}
if (current < stock.getMinPrice()){
createLowPriceNotification(stock);
}
}
} finally {
AlarmReceiver.releaseLock();
stopSelf();
}
}

Ważnym aspektem tej metody jest to, że jej kod znajduje się w strukturze try-
´finally. W bloku finally wywoływana jest statyczna metoda releaseLock
odbiornika AlarmReceiver, co powoduje zwolnienie blokady WakeLock zajętej
w metodzie onReceive tego odbiornika.
OMÓWIENIE
Ważne jest, aby zastanowić się nad wpływem nowego kodu na czas pracy urzą-
dzenia na baterii. Procesor jest wzbudzany w celu nawiązania połączenia z siecią,
zaktualizowania lokalnej bazy danych i czasem także utworzenia powiadomień.
Kod do zarządzania zasilaniem jest niezbędny, jeśli operacje te mają być wyko-
nywane, kiedy urządzenie znajduje się w stanie uśpienia. Cały proces zajmuje
kilka sekund, ponieważ wymaga nawiązania połączenia z siecią. Aplikacja nie
włącza jednak ekranu, co pozwala zminimalizować zużycie energii.
Inną kwestią, o której warto pamiętać, jest to, że w blokadach WakeLocks
można ustawić także kilka innych opcji. Określają one, czy zajęcie blokady ma
prowadzić do włączenia ekranu. Standardowo blokady WakeLock nie powodują
włączenia wyświetlacza, jednak przy użyciu dodatkowych opcji można go uru-
chamiać. Opcje te nie działają jednak w trybie PARTIAL_WAKE_LOCK. Ten tryb opra-
cowano dla zadań, które wymagają wzbudzenia urządzenia, ale są wykonywane
w tle. Tak właśnie działa usługa w omawianej aplikacji. Ważne jest jednak to,
że w ramach powiadomień usługa nie ogranicza się do wyświetlania kursu akcji.
Ekran może być wyłączony, jednak nie można go włączyć, dlatego użytkownik
nie zobaczy tekstowych powiadomień. W rozwijanej aplikacji nie stanowi to pro-
blemu, ponieważ powiadomienia generują dźwięk, wywołują wibracje telefonu
i powodują włączenie się lampek LED. Nie trzeba stosować wszystkich trzech
mechanizmów, jednak warto użyć przynajmniej jednego z nich.

0 TECHNIKA 20. Używanie usługi Cloud to Device Messaging

Do tej pory w tym podrozdziale koncentrowaliśmy się na tym, jak używać systemu
operacyjnego Androida do planowania wykonywania usługi. Było to potrzebne
do tego, aby usługa mogła kierować do serwera internetowego zapytania o świeże
dane na temat akcji. Jednak kierowanie zapytań to niewydajne rozwiązanie. Więk-
szość otrzymywanych danych nie wymaga wygenerowania powiadomienia przez
230 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

usługę. Oznacza to, że zapytań jest zbyt wiele. Jednak często mija pewien czas
od wystąpienia zdarzenia (o którym aplikacja powinna powiadomić użytkownika)
do momentu wykrycia tego przez usługę. Oznacza to, że zapytania są zgłaszane
zbyt rzadko. W omawianej aplikacji wysyłane są co 15 minut. Jednak z uwagi
na zmienność kursów akcji przedział ten może być niesatysfakcjonujący dla
użytkownika. Zapytania można zgłaszać częściej, jednak skutkuje to większym
zużyciem energii. Usługa Cloud to Device Messaging w Androidzie pozwala
zastosować eleganckie alternatywne rozwiązanie.
PROBLEM
Chcemy natychmiast powiadamiać użytkowników o ważnych zdarzeniach. Im
mniej czasu upłynęło od zdarzenia do pojawienia się powiadomienia, tym warto-
ściowsza jest aplikacja dla użytkowników. Jednak zbyt częste zgłaszanie zapytań
negatywnie odbija się na zużyciu energii i może prowadzić do przeciążenia ser-
werów, z którymi usługa się komunikuje. Ponadto, jak pokazaliśmy, kod zapew-
niający niezawodność zapytań zgłaszanych w tle jest skomplikowany.
ROZWIĄZANIE
Gdybyś wśród programistów aplikacji na Android przeprowadził ankietę i zapy-
tał ich o najważniejszą nową funkcję systemu Android 2.2 (Froyo), wielu z nich
natychmiast odpowiedziałoby, że jest nią usługa Cloud to Device Messaging
(C2DM). Ta usługa to reakcja twórców Androida na usługę Apple Push Notifi-
cation Service (APNS), mająca jednak wiele zalet w porównaniu z apple’owym
odpowiednikiem. Usługa C2DM pozwala zdalnym serwerom WWW przesyłać
intencje do konkretnych aplikacji na określonych urządzeniach z Androidem.
W przykładowej aplikacji można wykorzystać takie usługi, aby umożliwić serwe-
rowi informowanie działającej w tle usługi o tym, że powinna odświeżyć zawar-
tość pamięci podręcznej i sprawdzić, czy należy przesłać powiadomienia. Używa-
nie usług C2DM wymaga skonfigurowania rozwiązania i uzyskania pewnych
uprawnień. Na listingu 5.22 przedstawiono potrzebne nowe elementy pliku
AndroidManifest.xml.

Listing 5.22. Nowa wersja manifestu z uprawnieniami dla usługi C2DM

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flexware.stocks"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<!-- Kod pominięto. -->
<receiver android:name=".PushReceiver"
android:permission=
"com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name=
"com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.flexware.stocks" />
0 TECHNIKA 20. Używanie usługi Cloud to Device Messaging 231

</intent-filter>
<intent-filter>
<action android:name=
"com.google.android.c2dm.intent.REGISTRATION"/>
<category android:name="com.flexware.stocks" />
</intent-filter>
</receiver> </application>
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.INTERNET"/>
<permission android:name="com.example.myapp.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name=
"com.example.myapp.permission.C2D_MESSAGE"/>
<uses-permission android:name=
"com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name=
"android.permission.MANAGE_ACCOUNTS"/>
<uses-permission
android:name="android.permission.WAKE_LOCK"/>
</manifest>

W manifeście zadeklarowany jest nowy odbiornik o nazwie PushReceiver. Dalej


szczegółowo omawiamy klasę tego odbiornika. Obsługuje ona zarówno służące
do rejestracji komunikaty od serwerów usługi C2DM, jak i właściwe dla apli-
kacji komunikaty od serwerów aplikacji, przechodzące przez serwery usługi
C2DM. Korzystanie z usług C2DM wymaga też podania kilku nowych upraw-
nień . W ostatnim kroku trzeba uzyskać dostęp do informacji o koncie . Przy
korzystaniu z usługi C2DM informacje te nie są wprawdzie konieczne, jednak —
jak się wkrótce przekonasz — bywają przydatne. Po przyjrzeniu się potrzebnym
uprawnieniom i deklaracjom pora zobaczyć, jak wygląda proces rejestrowania
usługi C2DM (listing 5.23).

Listing 5.23. Żądanie rejestracji kierowane do usługi C2DM

public class PortfolioStartupReceiver extends BroadcastReceiver {


private static final String DEVELOPER_EMAIL_ADDRESS = "...";

@Override
public void onReceive(Context context, Intent intent) {
Intent registrationIntent =
new Intent("com.google.android.c2dm.intent.REGISTER");
registrationIntent.putExtra("app",
PendingIntent.getBroadcast(context, 0,
new Intent(), 0));
registrationIntent.putExtra("sender", DEVELOPER_EMAIL_ADDRESS);
context.startService(registrationIntent);
}
}

Na listingu 5.22 znajduje się następna wersja klasy PortfolioStartupReceiver


wywoływanej po rozruchu urządzenia. Tym razem do planowania wykonywa-
nia usługi nie używamy obiektu klasy AlarmManager, ale usługi C2DM. Najpierw
232 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

trzeba jednak zarejestrować komunikaty od tej usługi. Proces ten polega na


poinformowaniu serwerów usługi C2DM, że aplikacja chce otrzymywać od niej
komunikaty. Serwery C2DM reagują na tę informację podaniem identyfikatora
rejestracyjnego. Kod z listingu 5.22 rozpoczyna ten proces przez zażądanie takiego
identyfikatora. Większość kodu jest tu stosunkowo szablonowa. Jedyną rzeczą,
jaką trzeba podać, jest adres e-mail używany w aplikacjach na Android. Po
zakończeniu rozruchu urządzenia odbiornik wysyła wspomniane żądanie reje-
stracji. Potrzebny jest też drugi odbiornik do obsługi odpowiedzi od serwerów
usług C2DM (deklaracja tego odbiornika znajduje się na listingu 5.21). Na lis-
tingu 5.24 przedstawiamy implementację tego odbiornika.

Listing 5.24. Odbiornik do obsługi rejestracji i komunikatów

public class PushReceiver extends BroadcastReceiver {


@Override
public void onReceive(Context context, Intent intent) {
AlarmReceiver.acquireLock(context);
if (intent.getAction().equals(
"com.google.android.c2dm.intent.REGISTRATION")) {
onRegistration(context, intent);
} else if (intent.getAction().equals(
"com.google.android.c2dm.intent.RECEIVE")) {
onMessage(context, intent);
}
}
// Kod pominięto.
}

PushReceiver to klasa odbiornika, dlatego trzeba w niej zaimplementować metodę


onReceive. Warto zauważyć, że w momencie otrzymania komunikatu statyczna
blokada WakeLock jest zajmowana w podobny sposób jak w poprzedniej technice.
Odbiornik otrzymuje komunikaty dwóch rodzajów — dotyczące zdarzeń reje-
stracji i związane ze zdarzeniami z serwera aplikacji. Rozróżnianie rodzajów
komunikatów odbywa się na podstawie intencji przesłanej z serwera usługi
C2DM (a konkretnie — właściwości action z tej intencji). Jeśli zgłaszane jest
zdarzenie dotyczące rejestracji, należy wywołać pokazaną na listingu 5.25
metodę onRegistration.

Listing 5.25. Obsługa związanych z rejestracją zdarzeń usługi C2DM


(w klasie PushReceiver)

private void onRegistration(Context context, Intent intent) {


String regId = intent.getStringExtra("registration_id");
if (regId != null) {
Intent i =
new Intent(context, SendC2dmRegistrationService.class);
i.putExtra("regId", regId);
context.startService(i);
}
}
0 TECHNIKA 20. Używanie usługi Cloud to Device Messaging 233

W celu obsługi zdarzeń związanych z rejestracją należy pobrać identyfikator


rejestracyjny z serwera usługi C2DM i przesłać ten identyfikator do serwe-
rów aplikacji. Identyfikator jest potrzebny do tego, aby serwery aplikacji mogły
informować o zdarzeniach serwery usługi C2DM. Serwery usługi C2DM korzy-
stają z udostępnionych przez serwery aplikacji identyfikatorów rejestracyjnych
do przekazywania komunikatów do odpowiedniego odbiornika na konkretnym
urządzeniu. Identyfikator rejestracyjny można wysyłać na serwery aplikacji
w odbiorniku, jednak powinien on działać szybko, dlatego w przykładowym kodzie
za zadanie odpowiada usługa typu IntentService (listing 5.26).

Listing 5.26. Usługa typu IntentService przesyłająca dane rejestracyjne na serwery

public class SendC2dmRegistrationService extends IntentService {

private static final String WORKER_NAME = "SendC2DMReg";


public SendC2dmRegistrationService() {
super(WORKER_NAME);
}

@Override
protected void onHandleIntent(Intent intent) {
try{
String regId = intent.getStringExtra("regId");
// DO ZROBIENIA: wysyłanie identyfikatora regId na serwer.
} finally {
AlarmReceiver.releaseLock();
}
}
}

Usługa pobiera identyfikator rejestracyjny przekazany w kodzie z listingu 5.24.


Następnie wysyła identyfikator na serwer, po czym zwalnia blokadę WakeLock .
Serwer wykorzystuje identyfikator za każdym razem, kiedy przesyła komunikat
do aplikacji. Oprócz identyfikatora rejestracyjnego z urządzenia potrzebny jest
znacznik uwierzytelniający ClientLogin. Tak działa standardowy google’owy
mechanizm uwierzytelniania i autoryzacji. Znacznik ClientLogin umożliwia danej
aplikacji dostęp do google’owej aplikacji lub usługi z poziomu konkretnego konta
Google’a. Przy korzystaniu z usług C2DM niezbędna jest autoryzacja w usłudze
ac2dm, a kontem Google’a jest konto programisty używającego usługi C2DM.
Serwer w żądaniu znacznika musi podać adres e-mail i hasło programisty. Możesz
utworzyć konto Google’a specjalnie na potrzeby aplikacji. Jeśli używasz prywat-
nego konta, zmiana hasła spowoduje, że serwer nie będzie mógł wysyłać komu-
nikatów C2DM na google’owe serwery takich usług.
Po uzyskaniu przez serwer identyfikatora rejestracyjnego użytkownika i znacz-
nika uwierzytelniającego ClientLogin z danymi konta można zacząć wysyłanie
komunikatów do aplikacji. Jak pokazano to na listingu 5.23, komunikaty z usługi
C2DM przetwarza metoda onMessage:
234 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle

private void onMessage(Context context, Intent intent){


Intent stockService =
new Intent(context, PortfolioManagerService.class);
stockService.putExtras(intent);
context.startService(stockService);
}

Ten kod uruchamia usługę PortfolioManagerService. Na tym etapie nadal zajęta


jest statyczna blokada WakeLock. Jednak, jak pokazaliśmy w poprzedniej technice,
usługa PortfolioManagerService zwalnia tę blokadę po zakończeniu pracy.
OMÓWIENIE
W tym przykładzie używamy przesyłanych z serwera komunikatów do informo-
wania działającej w tle usługi o tym, że powinna zaktualizować zawartość pamięci
podręcznej i w razie potrzeby wygenerować powiadomienia. Jednak dane prze-
syłane z serwera mogą być dużo bardziej rozbudowane. Kiedy aplikacja wysyła
dane na serwery C2DM, może przekazać dowolne pary nazwa-wartość. Pary te
można pobrać w odbiorniku za pomocą metod Intent.getXXXExtra. W omawianej
aplikacji można śledzić na serwerze zdarzenia związane z osiągnięciem mini-
malnej lub maksymalnej ceny i przekazywać te informacje w ramach intencji.
Pozwala to uniknąć oczekiwania na dane z sieci w działającej w tle usłudze, co
przekłada się na szybsze zgłaszanie powiadomień.
Warto ponadto zauważyć, że przedstawiony kod nie obsługuje błędów, które
mogą wystąpić przy korzystaniu z usług C2DM. Firma Google opracowała małą
bibliotekę o otwartym dostępie do kodu źródłowego przeznaczoną do używania
takich usług. Nie jest ona częścią Androida, jednak można ją łatwo pobrać. Biblio-
teka ta udostępnia dużą część przedstawionego tu kodu, co pozwala w znacznym
stopniu uniknąć pisania szablonowych instrukcji.

Czy usługi C2DM są odpowiednie dla Ciebie?


C2DM to ważna nowa funkcja dodana w Androidzie 2.2. Omówiliśmy ją tylko w zary-
sie, przy czym mamy nadzieję, że dostrzegasz jej potencjał. Czy oznacza to, że
powinieneś z niej korzystać? Pamiętaj, że usługi C2DM działają tylko na urządze-
niach z Androidem 2.2 lub nowszym. W czasie, kiedy powstawała ta książka, takie
wersje działały na 83% urządzeń, a w przyszłości odsetek ten jeszcze wzrośnie.
Warto jednak starannie przeanalizować, z jakich wersji Androida korzystają użyt-
kownicy i czy wybór określonego poziomu interfejsu API nie wpłynie na powo-
dzenie aplikacji. Pamiętaj, że aplikacje ze sklepu Android Market nie pojawiają się
na urządzeniach, które nie obsługują danych programów.

5.4. Podsumowanie
W tym rozdziale dokładnie opisaliśmy, czym jest wielozadaniowość, a także przed-
stawiliśmy różne narzędzia z Androida, które pozwalają ją zastosować. Zapew-
nienie prawdziwej wielozadaniowości to jeden z aspektów, które wyróżniają
Android w świecie mobilnym. Jednak zaawansowane funkcje tego rodzaju wią-
żą się z pewnymi efektami ubocznymi. W Androidzie udało się zachować rów-
5.4. Podsumowanie 235

nowagę między bogactwem możliwości dostępnych w aplikacjach a komfortem


pracy użytkowników. Sprawia to jednak, że programiści muszą radzić sobie z pew-
nymi komplikacjami. Mamy nadzieję, że efekty są tego warte. Dzięki wieloza-
daniowości można zapewnić synchronizację aplikacji z danymi z serwerów.
Powoduje to, że aplikacje są bogatsze i szybciej reagują.
Dawniej programiści aplikacji na Android stali przed poważnym wyzwa-
niem, kiedy chcieli, aby działające w tle usługi były niezawodne i potrafiły pra-
widłowo pobierać dane z sieci. Niektóre aplikacje nawiązywały nawet trwałe
połączenia z serwerami, podtrzymywane przez działające w tle usługi. Prowa-
dziło to do szeregu problemów. Jednak od czasu pojawienia się usług Cloud to
Device Messaging stały dostęp do połączenia można wykorzystać we wszystkich
aplikacjach. Jedną z często pomijanych cech usług C2DM jest to, że można ich
używać nie tylko do wysyłania powiadomień. Na podstawie komunikatów wysy-
łanych przez serwery do aplikacji można wykonać określony kod, a następnie
stwierdzić, czy należy wyświetlić powiadomienie. Możesz zsynchronizować dane
z serwerem, uruchomić następną usługę itd. Przetwarzanie komunikatów w tle
daje programistom wiele możliwości.
236 ROZDZIAŁ 5. Używanie usług do zarządzania zadaniami wykonywanymi w tle
Wątki i współbieżność

W tym rozdziale
Q Tworzenie wątków i zarządzanie nimi
Q Komunikacja między wątkami
Q Zegary i pętle komunikatów

Ta sieć czasu — nici, które od stuleci zbliżają się do siebie, rozdzielają,


przecinają lub biegną niezależnie — obejmuje wszystkie możliwości.
„The Garden of Forking Paths”
Z poprzedniego rozdziału dowiedziałeś się, jak uruchamiać części aplikacji jako
usługi. Jest to doskonały sposób na wykonywanie zadań, które nie wymagają
interakcji z użytkownikiem. Zwykle zadaniami tymi są wykonywane (stale lub
okresowo) procedury, dlatego sensowne jest uruchamianie ich w tle. Działanie
w tle oznacza, że praca procedur jest niewidoczna dla użytkownika. Nie wynika
z tego jednak, że procedury te działają współbieżnie z aktywnościami aplikacji.
Dlaczego? W poprzednim rozdziale pokazaliśmy, że usługi można uruchamiać
w odrębnych procesach, jednak nie jest to konieczne. Jeśli nie podasz bezpośred-
nio identyfikatora procesu, usługa będzie działać w procesie aplikacji.
Co się wtedy dzieje? Przypominamy, że zbiór aktywności aplikacji tworzy
interfejs użytkownika. A zgodnie z podstawową regułą takie interfejsy zawsze
powinny reagować na poczynania użytkownika. Jeśli wszystkie aktywności i usługi
działają w jednym wątku, a w jednej z nich znajduje się operacja, która może
zablokować pracę aplikacji (na przykład sieciowa operacja wejścia-wyjścia),
interfejs użytkownika będzie się zawieszał. Przywitaj się z niesławnym oknem
dialogowym Activity Not Responding (ANR). Nawet jeśli nigdy nie tworzyłeś

237
238 ROZDZIAŁ 6. Wątki i współbieżność

aplikacji na Android, możliwe, że natrafiłeś na ten wyjątek w źle napisanych apli-


kacjach ze sklepu Android Market. Na rysunku 6.1 wspomniane okno dialogowe
pokazano w całej okazałości.

Rysunek 6.1. Jeśli aplikacja przestaje reagować


(na przykład z uwagi na wykonywanie kosztownych
operacji w głównym wątku programu), Android
po kilku sekundach zamyka ją i informuje
użytkownika o wystąpieniu wyjątku

Co za wstyd! W tle działają obiekty usługi, jednak domyślnie są one wykonywane


w głównym wątku aplikacji. Jeśli zatem zamierzasz uruchamiać kod równolegle,
a nie chcesz tworzyć odrębnego procesu linuksowego (co wiąże się z pewnymi
kosztami — na przykład koniecznością używania języka AIDL i komunikacji
międzyprocesowej), potrzebujesz sposobu na tworzenie nowych wątków. Na
szczęście Android obejmuje wszystkie podstawowe mechanizmy do obsługi wąt-
ków i synchronizacji dostępne w bibliotece klas Javy. Ponadto udostępnia dodat-
kowe niestandardowe klasy pomocnicze, ułatwiające równoległe wykonywanie
kodu.
W dalszych podrozdziałach omawiamy androidowy framework do obsługi
wątków, tworzenie przy jego użyciu współbieżnych aplikacji i problemy, na
które warto zwrócić uwagę. Najpierw pokazujemy, jak w aplikacjach na Android
wykorzystać zwykłe wątki Javy. Dalej przedstawiamy bardziej skomplikowane
techniki, na przykład informowanie interfejsu użytkownika o zmianach przez
niestandardowe wątki, implementowanie wątków roboczych za pomocą klasy
AsyncTask z Androida, wykonywanie zadań o określonym czasie trwania (na
przykład wyświetlanie ekranu powitalnego) i implementowanie niestandardowych
kolejek komunikatów do współbieżnego przetwarzania zdarzeń.

6.1. Współbieżność w Androidzie


Aby lepiej zrozumieć znaczenie współbieżnego kodu w aplikacji, wyobraź sobie,
że chcesz pobrać z sieci plik (pliki). W Androidzie najłatwiej zrobić to przez
uruchomienie aktywności lub usługi i wykonanie bezpośrednio w niej kodu do
komunikacji z siecią. Tak działa poniższa kiepsko napisana aktywność:
6.1. Współbieżność w Androidzie 239

public class PoorlyImplementedActivity extends Activity {

private HttpClient httpClient = new DefaultHttpClient();

public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);

HttpGet request = new HttpGet("http://www.example.com/file");


HttpResponse response = httpClient.execute(request);
...
}
}

W czym tkwi problem? Chodzi o wywołanie metody HttpClient.execute. Ope-


racja ta blokuje działanie programu, a jej wykonanie może trwać dość długo,
ponieważ metoda musi nawiązać połączenie sieciowe z serwerem WWW przez
protokół HTTP i pobrać dane z serwera na urządzenie. W momencie uruchamia-
nia aplikacji Android tworzy jeden proces, który działa w jednym wątku. Domyśl-
nie cały kod programu wykonywany jest właśnie w tym wątku. W rozdziale 3.
wyjaśniliśmy, że jest to główny wątek aplikacji, główny wątek interfejsu użytkow-
nika lub wątek interfejsu użytkownika, ponieważ Android wyświetla przy jego
użyciu także elementy interfejsu.
Tego rodzaju kod może prowadzić do zawieszania się aplikacji. Do czasu
zakończenia pobierania Android nie może kontynuować wyświetlania interfejsu
użytkownika aplikacji, ponieważ kod do pobierania pliku i kod do wyświetlania
interfejsu użytkownika działają w tym samym wątku. Jest to podstawowy problem
występujący we wszystkich programach z interfejsem użytkownika. Dotyczy to
nie tylko Androida. Na rysunku 6.2 pokazano, jak wygląda problem związany
z przykładowym fragmentem kodu.

Rysunek 6.2. Domyślnie system uruchamia dla aplikacji tylko jeden wątek (prawy
dolny fragment rysunku). Jeśli w wątku tym wykonywane są operacje blokujące
pracę, nie można zaktualizować interfejsu użytkownika (górne i dolne pole)
240 ROZDZIAŁ 6. Wątki i współbieżność

Jakie można wyciągnąć z tego wnioski? W głównym wątku aplikacji można wyko-
nywać dowolne nieblokujące lub szybkie operacje. Wszystkie pozostałe zadania
należy wykonywać w odrębnym wątku. W kilku następnych technikach pokazu-
jemy, jak to zrobić. Zacznijmy od czegoś prostego.

0 TECHNIKA 21. Proste wątki

Chcemy pobrać plik graficzny z sieci i przekształcić go na androidowy obiekt


typu Bitmap. Pobieranie uruchamiane jest za pomocą przycisku, a aplikacja ma
aktualizować pole tekstowe, aby informować o postępie pobierania. Na rysunku 6.3
pokazano wygląd programu do pobierania plików graficznych.

Rysunek 6.3. Prosta aplikacja do pobierania obrazków. Kliknięcie przycisku


powoduje rozpoczęcie pobierania i aktualizację tekstu o stanie operacji. Poprawna
aktualizacja tekstu wymaga, aby pobieranie odbywało się w wątku odrębnym
od głównego wątku interfejsu użytkownika

POBIERZ PROJEKT SIMPLEIMAGEDOWNLOAD. Kod


źródłowy projektu i pakiet APK do uruchamiania aplikacji
znajdziesz w witrynie z kodem do książki Android w prakty-
ce. Ponieważ niektóre listingi skrócono, abyś mógł skoncen-
trować się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/1897, plik APK: http://mng.bz/b134.
Jak już wiesz, nie można pobierać danych w głównym wątku aplikacji. W prze-
ciwnym razie w czasie pobierania cały interfejs użytkownika zostanie zabloko-
0 TECHNIKA 21. Proste wątki 241

wany. Po kliknięciu przez użytkownika przycisku, który początkuje pobieranie,


Android daje aplikacji nie więcej niż kilka sekund na zareagowanie na to zdarze-
nie. Jeśli aplikacja nie zareaguje, system zamknie ją i zgłosi opisany wcześniej
wyjątek ANR. Dla odbiorników Android jest „łagodniejszy” i dłużej wstrzymuje
się z zamknięciem programu, jednak śledzi czas ich wykonania. W obu przy-
padkach sytuacja nie wygląda jednak dobrze. Zobaczmy, jak wykorzystać wątki
Javy, aby zapobiec zamknięciu aplikacji.
PROBLEM
Potrzebne są długo działające operacje. Jeśli są wykonywane w głównym wątku
interfejsu użytkownika, aplikacja może przestać reagować. System może ją nawet
zamknąć i wyświetlić komunikat „Activity Not Responding”.
ROZWIĄZANIE
Aby uniknąć problemu, należy wyodrębnić blokujący kod i uruchamiać go
w nowym wątku, który działa równolegle z głównym wątkiem aplikacji. Najpro-
ściej zrobić to przez wykorzystanie klasy java.lang.Thread. Z obiektem tej klasy
można powiązać obiekt klasy Runnable, w którym należy umieścić uruchamiany kod
(pracę; ang. job). Wywołanie metody Thread.start powoduje wtedy wykonanie
kodu w nowym wątku w procesie aplikacji. Przyjrzyj się listingowi 6.1. Zaimple-
mentowano tu prostą aplikację do pobierania plików graficznych, w której nie
występuje problem zawieszania się interfejsu użytkownika.

Listing 6.1. W pliku SimpleImageDownload.java użyto klasy java.lang.Thread


do pobrania pliku graficznego

public class SimpleImageDownload extends Activity {

private Runnable imageDownloader = new Runnable() {


public void run() {
try {
URL url = new URL("http://www.android.com/images/froyo.png");
Bitmap image = BitmapFactory.decodeStream(url.openStream());
if (image != null) {
Log.i("DL", "Pobrano plik!");
} else {
Log.i("DL", "Błąd odczytu pliku ze strumienia!");
}
} catch (Exception e) {
Log.i("DL", "Nieudane pobieranie pliku!");
e.printStackTrace();
}
}
};

public void startDownload(View source) {


new Thread(imageDownloader, "Wątek do pobierania plików.").start();
TextView statusText = (TextView) findViewById(R.id.status);
statusText.setText("Rozpoczęto pobieranie...");
}
242 ROZDZIAŁ 6. Wątki i współbieżność

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

Kod układu aktywności aplikacji znajdziesz na listingu 6.2.

Listing 6.2. Plik układu main.xml z definicjami przycisku i widoku tekstowego


dla stanu

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">

<Button android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Pobierz plik"
android:onClick="startDownload"
/>
<TextView android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Kliknij, aby rozpocząć"
/>
</LinearLayout>

Kod ten jest zaskakująco prosty i skuteczny. Wystarczyło dodać kilka wierszy,
które tworzą obiekt do wykonywania pracy (obejmujący implementację interfejsu
Runnable) i przekazują go do nowego wątku, gdzie uruchamiany jest potrzebny kod.
Na rysunku 6.4 pokazano, jak wątek i procesy wyglądają w narzędziu DDMS
(przedstawiliśmy je w rozdziale 1.) w czasie pobierania pliku.
Na rysunku 6.4 widać też, że Android tworzy inne, wewnętrznie używane wątki,
które odpowiadają za przywracanie pamięci i obsługę sygnałów. Programista
jednak nie komunikuje się bezpośrednio z tymi wątkami, dlatego nie musisz się
nimi przejmować.
OMÓWIENIE
Warto zauważyć, że uruchomienie wątku pobierającego plik nie prowadzi do
zablokowania interfejsu użytkownika. Można się o tym przekonać, obserwując,
jak tekst z informacją o stanie zmienia się bezpośrednio po utworzeniu wątku
pobierającego dane.
W kontekście używania wątków Javy w Androidzie często zadaje się pytanie,
jak długo żyje taki wątek. Czy czas jego życia jest ograniczony do czasu życia
0 TECHNIKA 22. Przekazywanie informacji o zmianach między wątkami 243

Rysunek 6.4. Po lewej stronie widać wykryte urządzenia i działające na nich procesy.
Proces aplikacji do pobierania plików jest wyróżniony. Po prawej stronie pokazano
wątki tego procesu, w tym główny wątek interfejsu użytkownika i niestandardowy
wątek pobierający plik

komponentu (aktywności lub usługi), w którym go uruchomiono? Co się dzieje,


jeśli system zamknie taki komponent przed zakończeniem pracy wątku? To dobre
pytania. Okazuje się, że wątek żyje tak długo, jak długo działa jego metoda run.
Oznacza to, że może działać dłużej niż komponent, który go uruchomił. Ma to
ciekawe skutki — trzeba zachować bardzo dużą ostrożność przy tworzeniu
w wątku referencji do aktywności i podobnych komponentów, ponieważ aktyw-
ność może zakończyć pracę wcześniej niż wątek. Środowisko uruchomieniowe
zachowuje wtedy obiekt aktywności, ponieważ przechowywana jest do niego
silna referencja, jednak z perspektywy frameworku cykl życia tego obiektu się
zakończył! Takie rozwiązanie to często popełniany błąd. Zajmujemy się nim
w technice 25.
Wykorzystanie wątków Javy do wykonywania kosztownych zadań to dobre
rozwiązanie, jednak często warto aktualizować interfejs użytkownika przez
wyświetlanie informacji o postępie. W przeciwnym razie użytkownik nie wie,
co dzieje się w tle. Możesz się zastanawiać, dlaczego aplikacja rejestruje komuni-
katy o efekcie pobierania, zamiast aktualizować tekst informujący o stanie pro-
gramu. W następnej technice wyjaśniamy, dlaczego jest to niemożliwe bez dokła-
dniejszego zapoznania się z androidowym frameworkiem do obsługi wątków.

0 TECHNIKA 22. Przekazywanie informacji o zmianach między wątkami

Jednym z najczęściej stosowanych wzorców w programowaniu interfejsu użytkow-


nika jest wyświetlanie wizualnych wskaźników postępu, kiedy aplikacja wyko-
nuje kosztowne, długie zadania lub jest zajęta wykonywaniem innych operacji.
Ludzie wprost nie mogą oderwać oczu od wskaźników postępu, prawda? Takie
wskaźniki dają użytkownikom poczucie, że aplikacja informuje ich o tym, co się
dzieje. Interfejs użytkownika nadal przy tym reaguje, a prawdopodobnie nawet
umożliwia użytkownikom anulowanie zadania, jeśli trwa ono zbyt długo.
244 ROZDZIAŁ 6. Wątki i współbieżność

Opisane podejście wymaga użycia przynajmniej dwóch wątków — wątku


interfejsu użytkownika, który aktualizuje wskaźnik postępu, i jednego lub kilku
wątków wykonujących zadanie. Wątki wymieniają informacje o postępie, przeka-
zując powiadomienia o konieczności aktualizacji. Przedstawiamy to na rysunku 6.5.

Rysunek 6.5. Wątki robocze (jeden lub kilka) informują wątek interfejsu
użytkownika o postępach prac, przekazując komunikaty. Wątek interfejsu
użytkownika odbiera te komunikaty i odpowiednio modyfikuje wygląd interfejsu
przez aktualizowanie wskaźnika postępu

Możesz stwierdzić, że jeśli podzielisz operację na wiele małych porcji i będziesz


wykonywał kod roboczy oraz kod do aktualizowania interfejsu użytkownika w tym
samym wątku, interfejs zachowa zdolność do reakcji (pod warunkiem że porcje
kodu roboczego są na tyle małe, że można je szybko wykonywać). Niestety, roz-
wiązanie to nie zadziała, ponieważ z definicji nie da się aktualizować elementów
interfejsu użytkownika spoza głównego wątku tego interfejsu. Próba wykonania
takiej operacji powoduje zgłoszenie wyjątku przez Android. Jest to uzasadnione.
Jeśli dwa wątki — lub większa ich liczba — współużytkują stan (a tak dzieje się
przy aktualizowaniu widoków na podstawie danych o postępie z wątków robo-
czych), wspólne dane trzeba synchronizować przy użyciu prostych mechanizmów,
takich jak słowa kluczowe synchronize i volatile lub obiekt Lock Javy. Zapewnia-
nie bezpieczeństwa ze względu na wątki w każdej procedurze interfejsu użyt-
kownika dodatkowo zwiększa złożoność kodu, co prowadzi do spadku wydajności.
Dlatego w wielu frameworkach kontrolek, w tym w Androidzie, stosuje się pewne
uproszczenie. Polega ono na tym, że elementy interfejsu użytkownika zawsze są
aktualizowane w wątku tego interfejsu. Nie ma od tego odwołania.
WIĘCEJ O WSPÓŁBIEŻNOŚCI. Współbieżność w programach kompu-
terowych i synchronizacja wątków to obszerne oraz złożone zagadnienia.
Należą one do najtrudniejszych i najbardziej skomplikowanych obszarów
w dziedzinie programowania. Na ich temat powstały całe książki (w kon-
0 TECHNIKA 22. Przekazywanie informacji o zmianach między wątkami 245

tekście Javy gorąco polecamy pozycję Java Concurrency in Practice Briana


Goetza; jest ona dostępna jako e-book); szczegółowe omawianie tych zagad-
nień wykracza poza zakres niniejszej publikacji.
Jeśli zastosujemy rozwiązanie z poprzedniej techniki, mamy problem. Nie można
aktualizować interfejsu użytkownika bezpośrednio z poziomu wątku roboczego,
dlatego nie da się w ten sposób aktualizować informacji o postępie. Najwyraźniej
potrzebny jest sposób na komunikowanie się z wątkiem interfejsu użytkownika
z poziomu innego wątku, co pozwoli na przesyłanie komunikatów aktualizacyjnych
i reagowanie na nie. Wygląda na to, że natrafiliśmy na następne kłopoty.
PROBLEM
Aplikacja wykonuje długie zadania w odrębnych wątkach. Chcemy, aby program
w czasie wykonywania tych zadań aktualizował informacje o postępie w interfej-
sie użytkownika.
ROZWIĄZANIE
Można przechowywać informacje o postępie we współużytkowanych zmien-
nych i korzystać z nich w obu wątkach. Wątek roboczy może zapisywać dane
w tych zmiennych, a wątek interfejsu użytkownika — okresowo je wczytywać.
To jednak wymaga synchronizacji dostępu do zmiennych, co zawsze jest kłopo-
tliwe. Okazuje się, że w Androidzie potrzebny efekt można uzyskać w prostszy
sposób. Należy wykorzystać androidowe mechanizmy przekazywania komuni-
katów. W tym podejściu trzeba użyć kolejek komunikatów, aby umożliwić komu-
nikowanie się wątków w kontrolowany, bezpieczny ze względu na wątki sposób.
Informacje o postępie można wtedy przekazywać z wątku roboczego do wątku
interfejsu użytkownika przez umieszczanie komunikatów aktualizacyjnych
w kolejce komunikatów wątku tego interfejsu. Umożliwiają to klasy Handler
i Message Androida.
Komponent obsługi (ang. handler) to obiekt, który można powiązać z dowolnym
wątkiem (wątkiem obsługi). Inne wątki mogą korzystać z komponentu obsługi
do wysyłania komunikatów, a nawet wykonywania kodu w wątku obsługi. Wią-
zanie komponentów jest proste. Komponent obsługi zawsze jest powiązany
z tworzącym go wątkiem. Jeśli komponent obsługi jest powiązany z wątkiem
interfejsu użytkownika, śledzi kolejkę komunikatów tego wątku. Drugi wątek
(roboczy) może używać komponentu obsługi do przesyłania komunikatów do
wątku interfejsu użytkownika, wywołując metodę sendMessage(Message). Może
nawet zażądać wykonania metody wątku interfejsu użytkownika; w tym celu
należy wywołać metodę post(Runnable). Nie jest potrzebna żadna dodatkowa
synchronizacja — takie rozwiązanie działa automatycznie! Wróćmy teraz do
rysunku 6.5 i nadajmy powiadomieniom o aktualizacji konkretny kształt w postaci
komunikatów (rysunek 6.6).
246 ROZDZIAŁ 6. Wątki i współbieżność

Rysunek 6.6. Komunikaty (klasa Message) i komponent obsługi (klasa Handler)


pozwalają powiązać egzemplarz klasy Handler z wątkiem interfejsu użytkownika
i kierować do tego wątku komunikaty z innych wątków. W ten sposób można
przekazywać dowolne dane, przy czym ręczna synchronizacja nie jest konieczna

KOLEJKI KOMUNIKATÓW. Kilkukrotnie wspomnieliśmy, że komunikaty


trafiają do kolejki komunikatów. Nie musisz przejmować się szczegółami
jej działania — omawiamy je w technice 27. Na razie wystarczy wiedzieć,
że główny wątek interfejsu użytkownika zarządza pętlą komunikatów, z któ-
rej komunikaty można kierować do komponentu obsługi.
W wątku otrzymującym komunikaty należy zaimplementować metodę handle
´Message(Message) interfejsu Handler.Callback. Standardowym podejściem jest
zaimplementowanie interfejsu Handler.Callback w aktywności i skonfigurowanie
komponentu obsługi jako obiektu odpowiedzialnego za przetwarzanie komuni-
katów.
Wygląda na to, że dokładnie takiego rozwiązania potrzebujemy. W aplikacji
działają dwa wątki (wątek do pobierania danych i główny wątek interfejsu
użytkownika). Ponadto chcemy informować wątek interfejsu użytkownika, że
ma aktualizować widok tekstowy z informacjami o stanie przy każdej zmianie
stanu wątku roboczego. Trzeba więc wykonać następujące operacje:
1. Utworzyć komponent obsługi (obiekt klasy Handler) i powiązać go z wątkiem
interfejsu użytkownika.
2. Zaimplementować interfejs Handler.Callback (na przykład w aktywności).
3. W wątku do pobierania danych użyć komponentu obsługi do przesyłania
do wątku interfejsu użytkownika komunikatów obejmujących nowy tekst
z informacjami o stanie.
4. W wywoływanej zwrotnie metodzie wczytywać tekst z informacjami
o stanie z komunikatu i aktualizować widok tekstowy.
Zmodyfikujmy aplikację do pobierania danych przez użycie w niej obiektów klas
Handler i Message. Cały kod źródłowy aktywności znajdziesz na listingu 6.3.
0 TECHNIKA 22. Przekazywanie informacji o zmianach między wątkami 247

POBIERZ PROJEKT IMAGEDOWNLOADWITHMESSAGE


´PASSING. Kod źródłowy projektu i pakiet APK do urucha-
miania aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skon-
centrować się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub in-
nym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/PnPD, plik APK: http://mng.bz/vRQ1.

Listing 6.3. Do przesyłania informacji o stanie między wątkami można wykorzystać


mechanizm przekazywania komunikatów

public class ImageDownloadWithMessagePassing extends Activity


implements Handler.Callback {

private Handler handler = new Handler(this);

private Runnable imageDownloader = new Runnable() {

private void sendMessage(String what) {


Bundle bundle = new Bundle();
bundle.putString("status", what);
Message message = new Message();
message.setData(bundle);
handler.sendMessage(message);
}

public void run() {


sendMessage("Rozpoczęto pobieranie");

try {
URL url = new URL("http://www.android.com/images/froyo.png");
Bitmap image = BitmapFactory.decodeStream(url.openStream());
if (image != null) {
sendMessage("Pobieranie zakończone powodzeniem!");
} else {
sendMessage("Nieudany odczyt pliku ze strumienia.");
}
} catch (Exception e) {
sendMessage("Nieudane pobieranie plików!");
e.printStackTrace();
}
}
};

public void startDownload(View source) {


new Thread(imageDownloader, "Wątek do pobierania danych").start();
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
248 ROZDZIAŁ 6. Wątki i współbieżność

public boolean handleMessage(Message msg) {


String text = msg.getData().getString("status");
TextView statusText = (TextView) findViewById(R.id.status);
statusText.setText(text);
return true;
}
}

Pierwszy krok polega na zaimplementowaniu interfejsu wywołań zwrotnych —


i . Kod wywołań zwrotnych wczytuje łańcuch znaków o kluczu status z otrzy-
manego komunikatu i aktualizuje przy użyciu łańcucha widok tekstowy z informa-
cjami o aktualizacji. Przed wysłaniem komunikatów trzeba utworzyć i powiązać
z wątkiem komponent obsługi (Handler) . Należy przekazać do niego referencję
do aktualnej aktywności, ponieważ to w niej zaimplementowana jest wywoływana
zwrotnie metoda dla komponentu obsługi. Zarówno kod tworzący komponent
obsługi, jak i wywoływana zwrotnie metoda są wykonywane w wątku interfejsu
użytkownika. W pracy odpowiedzialnej za pobieranie pliku należy utworzyć
metodę pomocniczą , która z wykorzystaniem obiektu klasy Bundle przygoto-
wuje komunikat z informacjami o stanie, a następnie za pomocą komponentu
obsługi wysyła ten komunikat (klasę Bundle możesz traktować jak odpowiednik
klasy Map z Javy, pozwalający jednak przekazywać pary klucz-wartość nawet
między różnymi wątkami lub procesami). Następnie w metodzie run używamy
metody pomocniczej do przesyłania aktualizacji stanu . Podane etapy są wyko-
nywane w wątku do pobierania plików.
OMÓWIENIE
Przekazywanie komunikatów daje duże możliwości w zakresie wymiany danych
między kilkoma wątkami i jest łatwe w realizacji. Nie trzeba przy tym przejmo-
wać się prostymi mechanizmami synchronizacyjnymi. Przekazywać można też
dane bardziej skomplikowane niż łańcuchy znaków. Ponieważ dane są zapisy-
wane w obiekcie klasy Bundle, można przekazać cokolwiek — od prostych liczb
po złożone obiekty z możliwością serializowania lub z implementacją interfejsu
Parcelable (w Androidzie zalecanym sposobem szeregowania danych jest uży-
wanie klasy Parcel).
Ponieważ wywołanie zwrotne jest uruchamiane w wątku interfejsu użytkow-
nika, a klasa Bitmap obejmuje implementację interfejsu Parcelable, w obiekcie
klasy Bundle można umieścić także bitmapę i przekazać ją do wywołania zwrot-
nego! Pozwala to natychmiast zaktualizować widok ImageView za pomocą pobra-
nego rysunku.
Możesz się zastanawiać, dlaczego to wygląda tak, jakby wątek przyjmujący
komunikaty (tu jest nim wątek interfejsu użytkownika) odbierał je natychmiast
po ich przesłaniu. Pamiętaj, że przy przekazywaniu komunikatów wywołania
zwrotne nie są uruchamiane od razu. Komunikaty trafiają do kolejki, co oznacza,
0 TECHNIKA 23. Zarządzanie wątkami w puli wątków 249

że odbierający je wątek musi okresowo sprawdzać, czy w kolejce znajdują się


nowe informacje. Okazuje się, że Android wykonuje tę operację automatycznie
przez utworzenie pętli komunikatów dla wątku interfejsu użytkownika. Jednak
przy przekazywaniu komunikatów między dwoma niestandardowymi wątkami
trzeba samodzielnie zadbać o obsługę takiej pętli (w technice 27. zobaczysz, jak
to zrobić).
Rozwiązaliśmy więc problem przekazywania informacji między wątkami,
jednak aplikacja nadal działa w niepożądany sposób. Kliknięcie przycisku zawsze
powoduje uruchomienie nowego wątku do pobierania plików. Programista nie ma
żadnej kontroli nad liczbą jednocześnie działających wątków. Jeśli użytkownik
kliknie przycisk pobierania 100 razy, aplikacja uruchomi 100 wątków. Negatyw-
nie wpływa to na aplikację, ponieważ tworzenie wątków i zarządzanie nimi jest
bardzo kosztowne. Dobrze byłoby mieć większą kontrolę nad wątkami.

0 TECHNIKA 23. Zarządzanie wątkami w puli wątków

Aplikacja do pobierania rysunków pozwoliła nam pokazać działanie wątków,


bądźmy jednak szczerzy — zaczyna robić się nudna, prawda? Przejdźmy ponow-
nie do prawdziwych aplikacji. Pamiętasz program MyMovies z rozdziału 4.?
Warto go rozwinąć o wyświetlanie miniatur ze scenami z filmu. Miniatury powinny
znajdować się obok tytułów filmów w widoku listy. Na rysunku 6.7 pokazano
wygląd nowej wersji aplikacji w porównaniu z implementacją z rozdziału 4.

Rysunek 6.7. Wcześniejsza, pozbawiona rysunków wersja aplikacji MyMovies


(po lewej), i nowa, ulepszona wersja z eleganckimi obrazkami wczytywanymi
na bieżąco (po prawej)
250 ROZDZIAŁ 6. Wątki i współbieżność

POBIERZ PROJEKT MYMOVIESWITHIMAGES. Kod źródło-


wy projektu i pakiet APK do uruchamiania aplikacji znajdziesz
w witrynie z kodem do książki Android w praktyce. Ponieważ
niektóre listingi skrócono, abyś mógł skoncentrować się na kon-
kretnych zagadnieniach, zalecamy pobranie kompletnego kodu
źródłowego i śledzenie go w Eclipse (lub innym środowisku IDE
albo edytorze tekstu).
Źródło: http://mng.bz/31J4, plik APK: http://mng.bz/54sf.
Ponieważ pobieranie 100 miniatur ze scenami z filmów i dołączanie ich do apli-
kacji byłoby kłopotliwe oraz zasobochłonne, zamiast tego zapisujemy w danych
adres URL rysunku dla każdego filmu. Aplikacja pobiera te obrazki na bieżąco,
kiedy jest to konieczne. W technikach 21. i 22. dowiedziałeś się, że operacja ta
musi przebiegać asynchronicznie. Jaki wpływ ma to na wydajność aplikacji
MyMovies?
Ze wcześniejszych rozdziałów dowiedziałeś się, że każdy element listy jest
tworzony w metodzie getView adaptera listy, a metoda ta jest wywoływana przy
przewijaniu listy w celu wyświetlenia dalszych pozycji. Metodę tę można wyko-
rzystać do tworzenia wątku pobierającego miniaturę rysunku, ponieważ pobie-
ranie grafiki bezpośrednio w metodzie getView blokowałoby działanie aplikacji.
Widok listy działałby wtedy powoli i mógłby zakończyć pracę z powodu wystą-
pienia wyjątku ANR.
Jednak nie tak prędko! Szybkie przewijanie widoku listy ze 100 filmami
spowoduje utworzenie kilkudziesięciu wątków do pobierania rysunków, ponie-
waż aplikacja wywołuje metodę getView dla każdego wyświetlonego elementu!
Niedobrze. Najwyraźniej potrzebny jest sposób na ograniczenie liczby równole-
głych wątków i w miarę możliwości ponowne wykorzystanie ich po zakończeniu
wykonywania przez nie zadań.
PROBLEM
Kod trzeba wykonywać w odrębnym wątku, jednak nie możemy kontrolować
tego, jak często tworzone są nowe wątki, dlatego aplikacji grozi wyczerpanie
zasobów systemowych.
ROZWIĄZANIE
Rozwiązaniem jest wykorzystanie puli wątków. Pula wątków to zbiór wątków
zarządzanych w kontrolowanym środowisku. Można na przykład ustawić górny
limit liczby wątków i wymusić na aplikacji ponowne wykorzystanie wątków oraz
rozdzielanie pracy między nie.
W Javie i w Androidzie pulami wątków można zarządzać za pomocą wyko-
nawcy (obiektu klasy ThreadPoolExecutor). Wykonawca służy do planowania wyko-
nywania zadań i zarządzania nimi. Zadaniom odpowiadają obiekty typu Runnable
wykonywane w wątkach pochodzących z puli. Wydaje się to skomplikowane, jed-
0 TECHNIKA 23. Zarządzanie wątkami w puli wątków 251

nak cały proces odbywa się automatycznie. Wystarczy użyć wykonawcy do uru-
chomienia zadania i pozwolić temu obiektowi na przeprowadzenie wszystkich
trudnych operacji (rysunek 6.8).

Rysunek 6.8. Aplikacja przekazuje obiekt typu Runnable do wykonawcy, który


planuje wykonanie kodu tego obiektu. Kiedy któryś z wątków jest wolny,
wykonawca pobiera go z puli i używa do uruchomienia obiektu typu Runnable

Pule wątków udostępniają wiele opcji konfiguracyjnych. Można na przykład


określić dolny i górny limit liczby wątków lub reguły planowania używane do
rozdzielania zadań między wszystkie wątki. Standardowa pula wątków obejmuje
ich stałą liczbę. Wątki te wykonują zadania umieszczone we współużytkowanej
kolejce. Jeśli zadań jest więcej niż wątków, muszą oczekiwać na to, aż któryś
wątek zakończy działanie i stanie się wolny.
Zastosujmy opisaną technikę w aplikacji MyMovies. Wzbogaćmy ją o pobie-
ranie i wyświetlanie zdjęć z filmów dla każdego elementu listy. Zmiany trzeba
wprowadzić tylko w dwóch miejscach. Po pierwsze, trzeba zmodyfikować układ
movie_item.xml, ponieważ obok widoku tekstowego z tytułem filmu trzeba
umieścić widok ImageView. Po drugie, trzeba zmienić kod adaptera, tak aby rozpo-
czynał pobieranie zdjęcia przy każdym wywołaniu metody getView. Kod nowego
układu elementu z filmem przedstawiamy na listingu 6.4.

Listing 6.4. Nowy układ elementu z filmem obejmuje miniaturę obok tytułu

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
>

<ImageView android:id="@+id/movie_icon"
android:layout_width="50dip"
android:layout_height="50dip"
252 ROZDZIAŁ 6. Wątki i współbieżność

android:scaleType="centerCrop"
/>

<CheckedTextView android:id="@android:id/text1"
android:layout_width="0px"
android:layout_height="fill_parent"
android:layout_weight="0.9"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
/>

</LinearLayout>

W kodzie tym nie ma nic niezwykłego. Warto zauważyć, że użyliśmy atrybutu


scaleType do automatycznego dostosowania wielkości zdjęć do elementów listy.
Ciekawszy jest nowy kod adaptera. Przedstawiamy go na listingu 6.5.

Listing 6.5. Plik MoviesAdapter.java zmodyfikowany pod kątem obsługi pobierania


rysunków

public class MovieAdapter extends ArrayAdapter<String> {

private HashMap<Integer, Boolean> movieCollection =


new HashMap<Integer, Boolean>();

private String[] movieIconUrls;

private ThreadPoolExecutor executor;

public MovieAdapter(Context context) {


super(context, R.layout.movie_item, android.R.id.text1, context
.getResources().getStringArray(R.array.movies));

movieIconUrls =
context.getResources().getStringArray(R.array.movie_thumbs);
executor =
(ThreadPoolExecutor) Executors.newFixedThreadPool(5);
}
...

@Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);

CheckedTextView checkMark =
(CheckedTextView) listItem.findViewById(android.R.id.text1);
checkMark.setChecked(isInCollection(position));
ImageView imageView = (ImageView)
listItem.findViewById(R.id.movie_icon);
imageView.setTag(position);
downloadImage(position, imageView);

return listItem;
}
0 TECHNIKA 23. Zarządzanie wątkami w puli wątków 253

private void downloadImage(int position, ImageView imageView) {


final Handler handler = new ImageHandler(position, imageView);
final String imageUrl = movieIconUrls[position];
executor.execute(new Runnable() {
public void run() {
try {
URL url = new URL(imageUrl);
Bitmap image = BitmapFactory.decodeStream(url.openStream());
Bundle data = new Bundle();
data.putParcelable("image", image);
Message message = new Message();
message.setData(data);
handler.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}

Najpierw aplikacja tworzy tablicę na adresy URL rysunków do pobrania i wyko-


nawcę do zarządzania pulą wątków . Konstruktor zapełnia tablicę adresami
URL obrazów . Adresy te, podobnie jak tytuły filmów w rozdziale 4., przecho-
wywane są w pliku zasobów w formacie XML (/res/values/movie_thumbs.xml).
Ponadto używamy klasy narzędziowej Executors do zainicjowania wykonawcy,
który ma zarządzać stałą pulą pięciu wątków .
W tym miejscu zaczyna się robić ciekawie. W metodzie getView należy naj-
pierw pobrać referencję do widoku rysunku. W widoku tym ma znaleźć się pobie-
rane zdjęcie. To jednak jeszcze nie wszystko. Pobieranie odbywa się asynchro-
nicznie, ponieważ zachodzi w odrębnym wątku, a jak wcześniej wyjaśniliśmy,
warto wielokrotnie używać widoków elementów listy. Tu pożądany efekt uzyska-
liśmy przez pobranie widoków z wywołania metody obiektu super, przy czym
w wywołaniu tym widok zapisywany jest w pamięci podręcznej. Dlatego może
zdarzyć się sytuacja, że aplikacja uruchomi pobieranie widoku rysunku, a widok
ten zostanie ponownie użyty dla innego filmu przed zakończeniem pobierania
danych dla pierwszego filmu. W środowisku współbieżnym taką okoliczność
nazywa się sytuacją wyścigu. Oznacza to, że aplikacja może ustawić dla widoku
złe zdjęcie! Dlatego używamy metody setTag , która umożliwia powiązanie
z widokiem dowolnych metadanych (tu określają one pozycję, dla której pobie-
rany jest rysunek). Wkrótce się przekonasz, że metoda ta przydaje się do roz-
wiązania opisanego problemu. Na końcu aplikacja uruchamia pobieranie zdję-
cia — podobnie jak we wcześniejszych technikach, jednak tym razem używany
jest do tego wykonawca .
Może zauważyłeś, że na listingu 6.5 do ustawiania rysunku w widoku użyli-
śmy niestandardowej implementacji komponentu obsługi (klasy ImageHandler).
Ma to uzasadnienie — takie podejście pozwala uniknąć opisanej wcześniej sytu-
acji wyścigu. Kod rozwiązania pokazano na listingu 6.6.
254 ROZDZIAŁ 6. Wątki i współbieżność

Listing 6.6. W pliku ImageHandler.java znajduje się kod do aktualizowania widoków


ImageView

public class ImageHandler extends Handler {

private int position;

private ImageView imageView;

public ImageHandler(int position, ImageView imageView) {


this.position = position;
this.imageView = imageView;
}

@Override
public void handleMessage(Message msg) {
int forPosition = (Integer) imageView.getTag();
if (forPosition != this.position) {
return;
}
Bitmap image = msg.getData().getParcelable("image");
imageView.setImageBitmap(image);
}
}

W czasie tworzenia egzemplarza komponentu obsługi aplikacja zapamiętuje


element listy, dla którego należy pobrać grafikę. Po rozpoczęciu pobierania zapi-
suje też referencję do widoku rysunku, co pozwala zamienić obiekt graficzny .
Jeśli pobieranie zakończyło się powodzeniem, aplikacja przesyła komunikat
(jak pokazano na rysunku 6.5) i uruchamia metodę handleMessage danego kom-
ponentu obsługi. Przed wczytaniem pobranego rysunku z paczki i zaktualizo-
waniem widoku należy przeprowadzić prosty test, aby się upewnić, że widoku
rysunku nie użyto dla nieodpowiedniego elementu (innego od tego, dla którego
pobrano dane). W tym celu należy wczytać bieżącą pozycję widoku ImageView ze
znacznika i porównać ją z pozycją ustawioną w momencie rozpoczęcia pobie-
rania . Aplikacja dodaje rysunek tylko wtedy, jeśli obie te pozycje są takie same.
UWAGA. Choć pracujemy w środowisku współbieżnym, nie trzeba syn-
chronizować wywołań metod setTag i getTag. Zastanów się przez chwilę,
dlaczego tak jest. Jeśli nadal tego nie rozumiesz, wróć do opisu wcze-
śniejszych technik. Zarówno metoda getView, w której aplikacja ustawia
znacznik, jak i odczytująca go metoda handleMessage działają w jednym
wątku — w wątku interfejsu użytkownika. Aplikacja z pewnością nie
wczyta więc nieaktualnego współużytkowanego stanu. To dlatego kompo-
nent obsługi wiązany jest z wątkiem interfejsu użytkownika — kod wyko-
nywany w takim komponencie działa w tym samym wątku co interfejs,
dlatego nie trzeba przeprowadzać synchronizacji!
6.2. Korzystanie z klasy AsyncTask 255

Przedstawiliśmy dużo kodu. Czy prześledziłeś go w całości i napisałeś własną


aplikację? Jeśli nie, dlaczego nie zrobisz tego teraz? Możesz też pobrać cały kod
źródłowy przykładowych projektów, uruchomić go i zobaczyć, jak rozwiązanie
działa w telefonie.
OMÓWIENIE
Zaletą opisanego rozwiązania jest to, że aplikacja pobiera z sieci rysunki tylko
dla tych filmów, które są widoczne. Tylko dla nich wywołuje metodę getView.
Kiedy użytkownik przewija listę, aplikacja rozpoczyna pobieranie nowych rysun-
ków. Ponownie wykorzystuje przy tym wątki do pobierania plików (jeśli to
możliwe) lub czeka na zakończenie przez nie pracy. Dzieje się to bez blokowania
procedur interfejsu użytkownika. Jest to łatwe i skalowalne rozwiązanie.
Przedstawione podejście działa zgodnie z opisem, jeśli jednak uruchomisz
aplikację, zauważysz, że nie pracuje ona płynnie. Rysunki szybko się zmieniają
i jeden jest zastępowany drugim w momencie rozpoczynania pobierania. Ponadto
kliknięcie pola wyboru przy elemencie powoduje ponowne wyświetlenie całej
listy. Oznacza to ponowne pobieranie rysunków dla wszystkich widocznych pozy-
cji. Nie jest to konieczne, ponieważ zmiana stanu wpływa tylko na pole wyboru,
a nie na resztę elementu.
Problem można ograniczyć przez zapisywanie rysunków w pamięci pod-
ręcznej po ich pobraniu. Po wywołaniu metody getView warto najpierw spraw-
dzić zawartość pamięci podręcznej z obrazkami, zamiast ponownie uruchamiać
pobieranie. Jeśli potrzebny rysunek się w niej znajduje, można natychmiast go
ustawić i zwrócić sterowanie. W tym miejscu nie omawiamy technik korzystania
z pamięci podręcznej. Można uzyskać dobre efekty, stosując proste rozwiązania.
Możesz na przykład użyć obiektu klasy LinkedHashMap z miękką referencją (obiek-
tem klasy SoftReference) do rysunku. Inne podejście to wykorzystanie metody
removeEldestEntry, która usuwa wpisy z pamięci podręcznej po wyczerpaniu jej
pojemności.
Pokazaliśmy kilka podejść i technik związanych z wątkami — od prostego
tworzenia wątków po pule wątków i komunikację międzywątkową z wykorzysta-
niem komponentów obsługi. Jednak choć zapewnia to znaczną swobodę, wymaga
pisania dużej ilości szablonowego kodu. Często wykonywane są zadania w rodzaju
tworzenia okien dialogowych z informacjami o postępie lub inne operacje asyn-
chronicznego aktualizowania widoków w czasie działania wątku. Chcielibyśmy,
aby framework zapewniał w tym zakresie większą pomoc. Okazuje się, że jest
ona dostępna w postaci klasy AsyncTask.

6.2. Korzystanie z klasy AsyncTask


Wcześniejsze techniki powinny pomóc Ci zrozumieć, jak wykonywać współ-
bieżne zadania w Androidzie. Rozwiązania przedstawialiśmy na ogólnym po-
ziomie, dzięki czemu techniki można wykorzystać w różnorodnych aplikacjach
256 ROZDZIAŁ 6. Wątki i współbieżność

wymagających użycia wątków. Bezpośrednie stosowanie komponentów obsługi,


wątków i pul wątków zapewnia wysoki poziom kontroli oraz znaczną elastyczność.
Jest to bardzo cenne, jeśli tego potrzebujesz, jednak kiedy cechy te nie są nie-
zbędne, bezpośrednie korzystanie z wymienionych elementów bywa irytujące.
W typowych scenariuszach, takich jak opisane we wcześniejszych techni-
kach, zauważalne są pewne powtarzające się wzorce. Oto one:
Q równolegle z działaniem interfejsu użytkownika trzeba wykonywać jedną
lub kilka prac;
Q przed zakończeniem wykonywania pracy lub po tym zdarzeniu należy
zaktualizować interfejs użytkownika;
Q w interfejsie użytkownika trzeba wyświetlać informacje o postępie
wykonywania pracy.
Programiści z firmy Google zdali sobie sprawę z istnienia takich wzorców i opra-
cowali rozwiązanie, aby uprościć wykonywanie potrzebnych operacji. Tym roz-
wiązaniem są zadania asynchroniczne (klasa AsyncTask). Zobaczmy, jak działa
ten mechanizm.

0 TECHNIKA 24. Implementowanie prac za pomocą klasy AsyncTask

Według dokumentacji klasa AsyncTask „umożliwia prawidłowe i łatwe używanie


wątku interfejsu użytkownika […] bez konieczności manipulowania wątkami
i (lub) komponentami obsługi”. To dobre podsumowanie możliwości tej klasy.
Można też ująć je tak: jeśli chcesz tylko uruchomić zadanie i aktualizować inter-
fejs użytkownika z wykorzystaniem wyników lub informacji o postępie, klasa
AsyncTask jest łatwym w użyciu (choć nieco ograniczonym) abstrakcyjnym odpo-
wiednikiem wcześniej przedstawionych mechanizmów.
Klasę AsyncTask można traktować jak opis pracy lub zadania. Sama praca jest
wykonywana w odrębnym wątku, istnieje jednak kilka dobrze zdefiniowanych
punktów zaczepienia, które umożliwiają programistom łączenie się z interfejsem
użytkownika i aktualizowanie go. Te punkty zaczepienia pozwalają na aktuali-
zowanie widoków interfejsu użytkownika przed uruchomieniem pracy, w trakcie
jej wykonywania i po jej zakończeniu. Pozwala to na łatwe wyświetlanie okien
dialogowych z informacjami o postępie i manipulowanie interfejsem użytkownika.
Klasa AsyncTask wykorzystuje też pulę wątków, tak więc nawet ten aspekt jest
obsługiwany za programistę.
PROBLEM
Chcemy wykonywać asynchroniczną pracę z operacjami działającymi przed jej
uruchomieniem, w trakcie jej działania i po zakończeniu jej wykonywania. Potrze-
bujemy szablonowego kodu, który na każdym etapie umożliwia informowanie
użytkownika o postępie lub aktualizowanie interfejsu użytkownika w inny sposób.
0 TECHNIKA 24. Implementowanie prac za pomocą klasy AsyncTask 257

ROZWIĄZANIE
Klasę AsyncTask, podobnie jak większość innych komponentów, najlepiej omówić
na przykładzie. W poprzedniej technice miniatury ze scenami z filmów pobie-
raliśmy za pomocą prostej metody pomocniczej, która tworzyła nowy wątek do
pobierania pliku. Aplikacja przekazywała następnie rysunek do niestandardowego
komponentu obsługi, który aktualizował grafikę w widoku listy. Ponadto samo-
dzielnie zarządzaliśmy pulą wątków, aby określić górne ograniczenie liczby
wątków działających jednocześnie. Zmodyfikujmy ten kod przez zastosowanie
klasy AsyncTask. Pozwala to pominąć niestandardową pulę wątków i klasę
ImageHandler.

POBIERZ PROJEKT MYMOVIESWITHIMAGESASYN-


CTASK. Kod źródłowy projektu i pakiet APK do uruchamiania
aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł
skoncentrować się na konkretnych zagadnieniach, zalecamy
pobranie kompletnego kodu źródłowego i śledzenie go
w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/VAhI, plik APK: http://mng.bz/CAq3.
Przed przejściem do kodu źródłowego warto napisać kilka słów wprowadzenia.
AsyncTask to klasa generyczna. Przy tworzeniu jej egzemplarzy należy użyć argu-
mentów określających typy. Oto te argumenty:
1. Argument określający typ dla metody roboczej, która wykonuje zadanie.
Tu jest to typ String, ponieważ praca wymaga adresu URL rysunku.
2. Typ potrzebny przy informowaniu o postępach. Tu jest to typ Void,
ponieważ aplikacja nie wyświetla informacji o postępie.
3. Typ danych zwracanych przez metodę roboczą. Tu jest to typ Bitmap,
ponieważ po zdekodowaniu strumienia z serwera otrzymujemy obiekt
tego typu. Tego typu dane chcemy też przekazywać do wątku interfejsu
użytkownika.
Przy okazji warto wspomnieć, że jeśli któryś z wymienionych parametrów nie
ma znaczenia, możesz podać typ Void. Oprócz zmodyfikowania kodu przez użycie
klasy AsyncTask chcemy też nieco wzbogacić warstwę graficzną przez wyświe-
tlanie rysunku zastępczego przed rozpoczęciem pobierania. Rysunkiem tym jest
standardowy obrazek miniatury z galerii Androida (android.R.drawable.gallery_
´thumb) — mała, biała ramka widoczna na rysunku 6.9.
Przejdźmy do kodu. Na listingu 6.7 pokazujemy, jak za pomocą klasy AsyncTask
napisać kod do pobierania plików.
258 ROZDZIAŁ 6. Wątki i współbieżność

Rysunek 6.9. Upraszczamy kod z wcześniejszej


techniki przez zastosowanie wbudowanych
w klasę AsyncTask punktów zaczepienia
do interfejsu użytkownika. Dodajemy też
rysunek zastępczy dla obrazków, których
aplikacja jeszcze nie pobrała

Listing 6.7. Plik DownloadTask.java zawiera kod do pobierania rysunków.


W tej wersji wykorzystujemy klasę AsyncTask

public class DownloadTask


extends AsyncTask<String, Void, Bitmap> {

private int position;


private ImageView imageView;
private Drawable placeholder;

public DownloadTask(int position, ImageView imageView) {


this.position = position;
this.imageView = imageView;
Resources resources = imageView.getContext().getResources();
this.placeholder = resources.getDrawable(
android.R.drawable.gallery_thumb);
}

@Override
protected void onPreExecute() {
imageView.setImageDrawable(placeholder);
}

@Override
protected Bitmap doInBackground(String... inputUrls) {
try {
URL url = new URL(inputUrls[0]);
return BitmapFactory.decodeStream(url.openStream());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

@Override
protected void onPostExecute(Bitmap result) {
0 TECHNIKA 24. Implementowanie prac za pomocą klasy AsyncTask 259

int forPosition = (Integer) imageView.getTag();


if (forPosition == this.position) {
this.imageView.setImageBitmap(result);
}
}
}

Zaczynamy od utworzenia klasy pochodnej od klasy AsyncTask. W deklaracji


należy podać parametry w postaci typów argumentu metody roboczej (String),
informacji o postępie (Void) i wyniku zwracanego przez metodę roboczą (Bitmap) .
Ponieważ rysunek zastępczy chcemy ustawić przed uruchomieniem pracy pobie-
rającej plik, trzeba podać go w metodzie onPreExecute . Jest ona wykonywana
w wątku interfejsu użytkownika. Sama praca jest umieszczona w metodzie
doInBackground . Metoda ta przyjmuje dowolnie długą listę argumentów okre-
ślonego wcześniej typu i zwraca jedną wartość także podanego wcześniej typu.
W ostatnim fragmencie aplikacja w metodzie onPostExecute ustawia nowy pobrany
rysunek . Metoda ta przyjmuje jako jedyny argument wartość zwróconą przez
metodę doInBackground. Warto zauważyć, że w kodzie produkcyjnym nie należy
używać metody URL.openStream bez wcześniejszego ustawienia odpowiednich
limitów czasu. Tu upraszczamy wywołanie, abyś mógł skoncentrować się na oma-
wianych zagadnieniach.

Klasa AsyncTask i pule wątków


Niestety, w poszczególnych wersjach Androida klasa AsyncTask zarządza wątkami
w zupełnie odmienny sposób. W wersji Android 1.6 (Donut) klasa nie uruchamia
pojedynczego wątku roboczego, ale zarządza pulą wątków. Tak działa opisany
przykład (testowany i uruchamiany w Androidzie 2.2). Wraz z pojawieniem się
tabletów i wersji 3.0 (Honeycomb) twórcy platformy wrócili do dawnego rozwią-
zania. Od tej wersji klasa AsyncTask tworzy tylko jeden wątek. Jeśli chcesz zarzą-
dzać pulą wątków w wersji Honeycomb i nowszych, do uruchamiania zadań używaj
metody executeOnExecutor (zobacz http://mng.bz/PGxJ).

Pozostaje nam odpowiedzieć na pytanie, gdzie i jak należy uruchamiać zadanie.


W tym samym miejscu co wcześniej, czyli w metodzie Adapter.getView. Jest ona
wywoływana, kiedy trzeba wyświetlić element listy.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);

ImageView imageView = (ImageView)
listItem.findViewById(R.id.movie_icon);
imageView.setImageDrawable(null);
imageView.setTag(position);
String imageUrl = this.movieIconUrls[position];

new DownloadTask(position, imageView).execute(imageUrl);

return listItem;
}
260 ROZDZIAŁ 6. Wątki i współbieżność

Ogólnie kod z listingu 6.7 nie różni się znacznie od metody do pobierania plików
i komponentu ImageHandler z wcześniejszej techniki. Zastosowaliśmy jednak
kilka znaczących usprawnień. Po pierwsze, nie występuje tu żadna bezpośrednia
komunikacja między wątkami oparta na komponentach obsługi i komunikatach.
Po drugie, nowy kod jest wyższej jakości, ponieważ cały kod związany z pobiera-
niem plików znajduje się w jednej klasie.
OMÓWIENIE
Klasa AsyncTask ma też kilka innych przydatnych cech. Nie wyświetlamy żadnych
wiadomości o postępie pobierania pliku, jednak można użyć metod publishPro
´gress i onProgressUpdate klasy AsyncTask do przekazywania między wątkiem
roboczym a wątkiem interfejsu użytkownika informacji o postępach (o procencie
wykonanego zadania).
Ponadto klasa AsyncTask śledzi stan i, co ważniejsze, umożliwia anulowania
zadania. Do śledzenia służą metody onCancelled i isCancelled, a do anulowania —
metoda cancel. Możesz na przykład użyć metody cancel do zakończenia zada-
nia, jeśli jeden z warunków wstępnych sprawdzanych w metodzie onPreExecute
nie jest spełniony.
Omawiana technika ma oczywiste zalety, ale nie istnieją rzeczy doskonałe.
Dotyczy to także klasy AsyncTask. Pozwala ona uprościć wykonywanie współbież-
nych prac, które aktualizują interfejs użytkownika, jednak ma pewne ograniczenia.
Pierwsze z nich dotyczy wielkości puli wątków. To prawda, klasa AsyncTask
wewnętrznie robi to, co w poprzedniej technice musieliśmy wykonać samodziel-
nie — zarządza pulą wątków, w których uruchamia się nowe zadania. Niestety,
niemożliwe jest tu skonfigurowanie wielkości puli wątków (jak pewnie pamiętasz,
w tej technice zamieniliśmy elastyczność na wygodę). Ponadto wielkość puli
zmienia się w zależności od wersji Androida. Przyznajemy, że choć przykładowa
aplikacja dobrze nadaje się do przedstawienia przeznaczenia klasy AsyncTask,
w praktyce klasa ta nie sprawdza się zbyt dobrze w omawianym programie. Jeśli
użytkownik na urządzeniu z Androidem 2.2 szybko przewija listę, możesz zaob-
serwować, że Android tworzy ponad 30 współbieżnych wątków do pobierania
plików. Przynajmniej 22 z tych wątków pobierają niepotrzebne pliki, ponieważ
do czasu pobrania rysunku aplikacja ponownie wykorzystuje docelowy widok
ImageView dla innego elementu (jednocześnie wyświetlanych jest tylko osiem
elementów listy). Przyjrzyj się rysunkowi 6.10. Widać na nim listę wątków uru-
chamianych, kiedy użytkownik szybko przewija listę. Dlatego choć interfejs klasy
AsyncTask kusi prostotą, zastanów się, czy klasa ta nadaje się do wykonywanego
zadania!
Innym ograniczeniem jest obsługa błędów. Metoda doInBackground domyślnie
nie umożliwia zgłaszania wyjątków, ponieważ w Javie należą one do sygnatury
metody, a w klasie AsyncTask w sygnaturze wspomnianej metody nie wymie-
niono żadnych wyjątków. Prostym obejściem jest przechwytywanie wyjątków
0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji 261

Rysunek 6.10. W wersjach


Androida starszych niż 3.0
klasa AsyncTask ma wysokie
górne ograniczenie liczby
wątków w wewnętrznej puli.
Jeśli zadanie jest uruchamiane
często, korzystne może być
użycie niestandardowej puli
wątków

w metodzie doInBackground i przekazywanie ich do metody onPostExecute. Pozwala


to na ich obsługę w wątku interfejsu użytkownika przez wyświetlanie komuni-
katów o błędach.
Jest też inna, mniej oczywista pułapka związana z klasą AsyncTask. Programi-
ści często nie zdają sobie z niej sprawy — a nawet jeśli o niej wiedzą, nie obsłu-
gują jej poprawnie. Problemu tego nie można łatwo rozwiązać. Niezbędne jest
dokładne poznanie cyklu życia aktywności, dlatego wspomnianemu problemowi
poświęcamy następną technikę.

0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji

Wszyscy uwielbiają piękne aplikacje. Niedawno pobraliśmy ze sklepu Android


Market aplikację na Android związaną z popularnym serwisem społecznościowym.
Po jej uruchomieniu pomyśleliśmy, że wreszcie ktoś pomyślał nie tylko o funk-
cji, ale też o formie! Po kilku minutach używania zachwyt przerodził się w fru-
strację. Aplikacja była eleganckim androidowym frontonem do usługi sieciowej
z witryny, dlatego wyświetlenie większości ekranów wymagało skierowania
wywołania do takiej usługi. Aplikacja nie tylko wolno działała, ale i nie zapisywała
stanu przy zmianie orientacji ekranu. Okno dialogowe z informacjami o postępie
niknęło, choć wywołanie nie zwróciło sterowania. Ponadto po zniknięciu okna
dialogowego często następowała awaria aplikacji — prawdopodobnie z uwagi
na niewłaściwą synchronizację między operacjami wykonywanymi w tle a wido-
kiem wyświetlanym użytkownikom. Warto dbać o formę. Zawsze powinieneś
o niej pamiętać. Z rozdziału 4. dowiedziałeś się, jak to robić. Jednak forma to
nie wszystko. Dobra aplikacja powinna też być stabilna, a oba wymienione aspekty
się nie wykluczają.
262 ROZDZIAŁ 6. Wątki i współbieżność

W tym rozdziale omawiamy techniki zapewniania współbieżności, a we współ-


bieżnych aplikacjach z natury występują problematyczne sytuacje, nieznane
z sekwencyjnych programów, w których wszystko działa po kolei. Przykładowe
trudności to nieoczekiwane zamknięcie wątku lub usunięcie obiektu, w którym
zapisana jest część stanu wątku. Jeśli wątki potrzebują do działania danych
z innych wątków lub ich stanu, a stan ten nagle niknie lub staje się nieaktualny
(czasem używa się określenia przestarzały), program może zacząć działać w nie-
oczekiwany sposób.
Okazuje się, że tego rodzaju problemy są „wbudowane” we wszystkie apli-
kacje na Android. Z rozdziału 3. dowiedziałeś się, że cykl życia aktywności może
zostać zakłócony lub nawet przerwany w dowolnym momencie. Standardowym
zakłóceniem jest zmiana orientacji ekranu. Jeśli zmienia się ona na przykład
z pionowej na poziomą, Android zamyka bieżącą aktywność i ponownie ją uru-
chamia z wykorzystaniem nowego, poziomego układu.
ZMIANY ORIENTACJI SĄ ZMIANAMI KONFIGURACJI. Warto
zauważyć, że zmiana orientacji nie jest jedyną możliwą zmianą konfigura-
cji. Zmianę konfiguracji powoduje też umieszczenie telefonu z Androidem
w stacji dokującej (na przykład w samochodzie). Każda zmiana konfigu-
racji prowadzi do zamknięcia widocznej aktywności i ponownego urucho-
mienia jej z wykorzystaniem nowej konfiguracji systemu. Dlatego zawsze
należy być przygotowanym na zakłócenia.
Po tym wstępie zastanówmy się ponownie nad źle zaimplementowaną aplika-
cją, o której wcześniej wspomnieliśmy. Jej autorzy najwyraźniej chcieli wczy-
tywać dane z usługi sieciowej w wątku roboczym (prawdopodobnie za pomocą
klasy AsyncTask) i aktualizować widok otrzymanymi danymi. Co się jednak sta-
nie, jeśli aplikacja uruchomi zadanie z klasy AsyncTask w aktywności (przy czym
zadanie to ma też aktualizować interfejs użytkownika w komponencie obsługi
wywoływanym po zakończeniu pracy), jednak aktywność zostanie zamknięta —
na przykład z powodu zmiany orientacji ekranu — przed zakończeniem wyko-
nywania zadania? Jeśli programiści nie obsłużą poprawnie tej sytuacji, nastąpi
utrata wyników pracy wątku roboczego lub — co gorsza — awaria aplikacji.
Możliwe, że sam zetknąłeś się już z taką sytuacją. Zadanie pobierające pliki
opisane w poprzedniej technice przechowuje referencję do widoku ImageView.
Widok ten obejmuje referencję do nadrzędnej aktywności (tak działają wszystkie
widoki), czyli tej, w której dany widok utworzono. Jeśli zadanie jest wykony-
wane po zamknięciu aktywności, próba manipulowania widokiem prowadzi do
awarii aplikacji, ponieważ aktywność jest nieaktualna — powiązane z nią okno
zostało zamknięte! Rysunek 6.11 pomaga zrozumieć ten problem.
Wyobraź sobie narciarza wodnego. Motorówka (wątek roboczy) ciągnie narcia-
rza (aktywność) uczepionego liny. Zwykle narciarz musi trzymać się liny, dlatego
jego połączenie z motorówką jest słabe. Załóżmy jednak (bez żadnych masochi-
0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji 263

Rysunek 6.11. Aktywność tworzy wątek roboczy i zostaje usunięta w trakcie


wykonywania zadania. Wątek roboczy nie ma informacji na ten temat, dlatego
zachowuje referencję do aktywności uznawanej przez środowisko uruchomieniowe
Androida za zakończoną

stycznych skojarzeń!), że narciarz jest mocno powiązany z motorówką i nie może


uciec. Połączenie jest wtedy trwałe, co odpowiada mocnej referencji do obiektu
aktywności. Co się dzieje, kiedy narciarz się przewraca? Nie może puścić liny.
Jak widać na rysunku 6.11, każdy ręcznie utworzony wątek (niezależnie od
tego, czy użyto do tego obiektu klasy AsyncTask, czy nie) może przetrwać dłużej
niż aktywność, w której powstał. Wątek może działać nawet tak długo, jak cała
aplikacja. Jeśli taki wątek bezpośrednio lub pośrednio (na przykład poprzez widok)
obejmuje mocną referencję do aktywności, w której powstał, występuje ryzyko
wykorzystania przestarzałego obiektu. Gdyby w wątku nie znajdowała się refe-
rencja do takiego obiektu, obiekt zostałby usunięty przez mechanizm przywra-
cania pamięci. W ten sposób nie tylko powstają referencje do bezużytecznych
obiektów, ale może też wystąpić wyciekanie pamięci, ponieważ mocne referencje
do dawnego egzemplarza aktywności uniemożliwiają usunięcie go przez mecha-
nizm przywracania pamięci.
Jest to wada projektowa Androida, ponieważ występuje tu sprzeczność —
w zadaniu nie można przechowywać referencji do aktywności, jednak taka refe-
rencja jest potrzebna do wykonywania przydatnych operacji w komponencie
obsługi używanym po zakończeniu pracy wątku. To trochę tak, jakby poprosić
malarza o pomalowanie ścian, jednak zabronić mu używania pędzli i wałków.
Spróbujmy teraz zwięźle opisać problem.
264 ROZDZIAŁ 6. Wątki i współbieżność

PROBLEM
Chcemy wykonywać zadania asynchronicznie, a przy tym mieć pewność, że
wątek roboczy zawsze ma dostęp do prawidłowego egzemplarza aktywności,
w której powstał (nawet jeśli aktywność ta została usunięta).
ROZWIĄZANIE
Chcielibyśmy móc napisać, że Android udostępnia rozwiązanie tego problemu,
jednak to nieprawda. Nawet klasa AsyncTask, która ma upraszczać implemento-
wanie wątków roboczych, nie stanowi rozwiązania. Choć gwarantuje, że wywoła-
nie zwrotne onPostExecute jest uruchamiane po zakończeniu zadania dla odpo-
wiedniego egzemplarza aktywności, nie pozwala uzyskać referencji do tego
egzemplarza, dlatego trzeba samemu o to zadbać. Musisz to zrobić za każdym
razem, kiedy chcesz zaktualizować interfejs użytkownika po zakończeniu zadania,
ponieważ każda operacja wykonywana na takim interfejsie przechodzi pośrednio
lub bezpośrednio przez aktywny egzemplarz aktywności. Jak więc można rozwią-
zać problem? Podsumujmy krótko wnioski:
1. Chcemy przechowywać referencję do aktywności w klasie roboczej,
co ma zapewnić metodzie onPostExecute pełny dostęp do operacji
na interfejsie użytkownika.
2. Wiemy, że wspomniana referencja może stać się przestarzała, dlatego
potrzebny jest sposób na zerwanie powiązania referencji z obiektem
aktywności i ponowne jego nawiązanie w momencie usuwania aktywności
oraz jej odtwarzania.
3. Jeśli aktywność jest odtwarzana w czasie działania zadania, nowy
egzemplarz aktywności nie ma informacji o obiekcie zadania powstałym
w dawnym egzemplarzu. Dlatego potrzebny jest sposób na przekazanie
obiektu roboczego z jednego egzemplarza aktywności do drugiego.
Oto nasz pomysł: chcemy przechowywać referencję do aktywności w klasie robo-
czej, jednak zamierzamy resetować ją przy każdej zmianie egzemplarza aktyw-
ności (wynikającej ze zmiany konfiguracji). Ponadto planujemy użycie metody
udostępnianej w Androidzie specjalnie na potrzeby optymalizacji. Metoda ta
pozwala szybko przekazywać dane w cyklu życia jednej aktywności i ma dziwną
nazwę — onRetainNonConfigurationInstance. Może pamiętasz, jak korzystaliśmy
z tej metody w rozdziale 3. do przekazywania stanu egzemplarza. Zarys planu
przedstawiamy na rysunku 6.12.
Rozwiązanie wydaje Ci się skomplikowane? Nie jest tak źle, jak na to wygląda.
Przyjrzyj się prostej aplikacji, która tworzy wątek roboczy w sposób pozwalający
na sprawną obsługę zmian konfiguracji. Wątek roboczy rozpoczyna pracę po
uruchomieniu aplikacji, następnie działa przez kilka sekund i przesyła informacje
o stanie do aktywności. Przetestuj aplikację przez kilkukrotną zmianę orientacji
urządzenia. Zauważ, że nie wpływa to na pracę wątku.
0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji 265

Rysunek 6.12. Aby uniknąć przechowywania przestarzałej referencji do aktywności,


ustawiamy referencję (connect) w momencie tworzenia aktywności i usuwamy
ją (disconnect) w momencie zamykania tej ostatniej

POBIERZ PROJEKT HANDLINGACTIVITYINTERRUP-


TIONS. Kod źródłowy projektu i pakiet APK do urucha-
miania aplikacji znajdziesz w witrynie z kodem do książki
Android w praktyce. Ponieważ niektóre listingi skrócono, abyś
mógł skoncentrować się na konkretnych zagadnieniach, zale-
camy pobranie kompletnego kodu źródłowego i śledzenie go
w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/61PJ, plik APK: http://mng.bz/71bN.
Przyjrzyjmy się najpierw implementacji aktywności i zarządzaniu przez nią
egzemplarzem wątku roboczego. Kod aktywności przedstawiono na listingu 6.8.

Listing 6.8. Sprawne zarządzanie wątkami roboczymi w trakcie zmian konfiguracji

public class WorkerActivity extends Activity {

private Worker worker;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

worker = (Worker) getLastNonConfigurationInstance();


if (worker == null) {
worker = new Worker();
worker.execute();
}
worker.connectContext(this);
}
266 ROZDZIAŁ 6. Wątki i współbieżność

@Override
protected void onDestroy() {
super.onDestroy();
worker.disconnectContext();
}

@Override
public Object onRetainNonConfigurationInstance() {
return worker;
}
}

Aplikacja najpierw przez wywołanie metody getLastNonConfigurationInstance


sprawdza, czy dostępny jest obiekt roboczy z wcześniejszego egzemplarza danej
klasy aktywności . Metoda ta działa w prosty sposób — pobiera wartość zwró-
coną przez metodę onRetainNonConfigurationInstance . W aplikacji ta wartość
to obiekt roboczy. W rozdziale 3. wartość zwrócona przez tę ostatnią metodę
jest przekazywana bez zmian do nowego egzemplarza aktywności.
Jeśli wartość zwrócona przez metodę getLastNonConfigurationInstance to
null, wiadomo, że obiekt roboczy nie został wcześniej zapisany. Oznacza to, że
uruchamiana jest „zwykła” aktywność. Należy wtedy utworzyć nowy obiekt robo-
czego i uruchomić zadanie . Niezależnie od tego, czy obiekt roboczy został
odtworzony, czy utworzono go po raz pierwszy, należy wywołać metodę connect
´Context (przedstawiamy ją dalej). Informuje ona obiekt roboczy, że dany
egzemplarz aktywności jest aktualny.
W momencie zamykania aktywności aplikacja wywołuje metodę disconnect
´Context obiektu roboczego, aby poinformować go, że dany egzemplarz aktyw-
ności wkrótce zostanie usunięty, dlatego nie należy się z nim więcej komunikować.
Do napisania pozostaje kod niestandardowej klasy roboczej (listing 6.9). Aby
zachować zwięzłość, w klasie roboczej wykonujemy proste operacje. Obiekt tej
klasy usypia na kilka sekund, a następnie przekazuje łańcuch znaków do nad-
rzędnej aktywności.

Listing 6.9. Implementacja klasy roboczej, która zrywa i tworzy powiązania


z nadrzędną aktywnością

public class Worker extends AsyncTask<Void, Void, String> {

private Activity context;

public void connectContext(Activity context) {


this.context = context;
}

public void disconnectContext() {


this.context = null;
}

@Override
0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji 267

protected String doInBackground(Void... params) {


try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
return "Zadanie wykonane!";
}

@Override
protected void onPostExecute(String result) {
if (context != null) {
Toast.makeText(context, result, Toast.LENGTH_LONG).show();
}
}
}

Niestandardowa klasa robocza przechowuje referencję do aktywności, przy czym


nadrzędną aktywność można powiązać z obiektem klasy roboczej i odłączyć
od niego . Dzięki temu aplikacji nie grozi przechowywanie referencji do usu-
niętej aktywności. Ponadto przed próbą interakcji z interfejsem użytkownika (na
przykład wyświetleniem komunikatu typu toast) kod ustala, czy egzemplarz aktyw-
ności jest prawidłowy. W tym celu sprawdza, czy referencja do egzemplarza ma
wartość różną od null . Jest to niezbędna ochrona przed sytuacją, w której
aktywność została usunięta, ale aplikacja później jej nie odtwarza. Dzieje się tak
na przykład po wciśnięciu przycisku Back lub po wyczerpaniu się pamięci.
OMÓWIENIE
Przyznajemy, że jest to dość zawiły problem, jednak rozwiązanie okazało się pro-
ste. Zachęcamy do stosowania wzorca obejmującego tworzenie i zrywanie powią-
zania (lub odpowiednika tej techniki), ponieważ zwiększa to płynność pracy
aplikacji.
Jeśli dokładnie śledziłeś tę technikę, możesz mieć pewne wątpliwości. Zapew-
niliśmy prawidłową obsługę referencji do aktywności i wszystko wygląda dobrze
po utworzeniu oraz zerwaniu powiązania. Co się jednak stanie, jeśli zadanie
skończy pracę między tymi operacjami w trakcie przetwarzania zmian w konfi-
guracji? Referencja do aktywności będzie wtedy miała wartość null. Czy więc
nie oznacza to, że wynik wykonania zadania zostanie utracony, ponieważ jest
pomijany w metodzie onPostExecute, jeśli referencja do aktywności to null?
Nie! Teraz możemy napisać, że Android zapewnia rozwiązanie tego problemu.
Gwarantuje, że między wywołaniem metody onRetainNonConfigurationInstance
wcześniejszego egzemplarza a wywołaniem metody onCreate nowego nie są prze-
twarzane żadne komunikaty. Oznacza to, że mogą wystąpić tylko dwie sytuacje:
1. Zadanie kończy pracę przed wywołaniem metody
onRetainNonConfigurationInstance. Wtedy można bezpiecznie
kontynuować pracę, ponieważ aktywność wciąż działa.
268 ROZDZIAŁ 6. Wątki i współbieżność

2. Zadanie kończy pracę po wywołaniu metody


onRetainNonConfigurationInstance. Wtedy aktywność jest przeznaczona
do usunięcia i wywołanie onPostExecute jest odraczane do czasu utworzenia
nowego egzemplarza aktywności, który może przetworzyć zdarzenie.
Ograniczeniem tego rozwiązania jest to, że działa tylko dla aktywności. W klasie
usługi nie ma zdefiniowanej metody onRetainNonConfigurationInstance, dlatego
nie można śledzić przechowywanych obiektów zadań (a przynajmniej nie w opi-
sany tu sposób).
Inne ograniczenie polega na tym, że nie można użyć egzemplarza aktywności
w metodzie doInBackground, ponieważ metoda ta działa nie w wątku interfejsu
użytkownika, lecz w wątku zadania. Dlatego nie ma gwarancji, że metoda zawsze
użyje odpowiedniego egzemplarza aktywności.
Jeśli te ograniczenia są dla kogoś nieakceptowalne, mamy dobrą wiadomość.
W bibliotece ignition (http://github.com/kaeppler/ignition) do tworzenia aplika-
cji na Android zdefiniowano klasę IgnitedAsyncTask (jest to implementacja klasy
AsyncTask) umożliwiającą uruchamianie prac niezależnie od typu nadrzędnego
obiektu klasy Context. Autorzy zagwarantowali, że wszystkie trzy wywoływane
zwrotnie metody zawsze używają odpowiedniego egzemplarza kontekstu. Wspo-
mniana biblioteka uwalnia też programistów od konieczności pisania większości
szablonowego kodu przedstawionego w tej technice.
To nie koniec dobrych wiadomości. Wiesz już wszystko na temat klasy
AsyncTask! Pora wrócić do bardziej praktycznych kwestii.

6.3. Różne techniki


Zaczynasz się nudzić? Ostrzegaliśmy, że jest to rozdział poświęcony kwestiom
technicznym. Teraz jesteś jednak przygotowany na zmierzenie się ze skompli-
kowanymi wątkami w rozwijanych aplikacjach. Aby zapewnić trochę różnorod-
ności, w ostatnim podrozdziale przedstawiamy dwie przydatne techniki, dla
których nie znaleźliśmy miejsca gdzie indziej.
Czy kiedykolwiek chciałeś wyświetlać w aplikacji ekran powitalny lub wyko-
nywać inne zadania oparte na zegarach? A może chciałeś tworzyć niestandar-
dowe pętle komunikatów, co jest przydatne w czasie pracy nad grami? Jeśli na
któreś z tych pytań odpowiedziałeś tak, powinieneś kontynuować lekturę tego
rozdziału.

0 TECHNIKA 26. Wyświetlanie ekranów powitalnych za pomocą zegarów

Czasem zadanie warto uruchomić nie natychmiast lub bezpośrednio w reakcji


na zdarzenie w interfejsie użytkownika, ale po upływie określonego czasu. Nie-
raz technika ta nazywana jest opóźnionym wykonaniem pracy lub opóźnionym
wykonaniem zadania. Oczywiście trzeba zastosować odrębny wątek, ponieważ
upływ czasu można zmierzyć tylko przez nieustanne sprawdzanie tego upływu.
0 TECHNIKA 26. Wyświetlanie ekranów powitalnych za pomocą zegarów 269

Nie można tego robić w głównym wątku interfejsu użytkownika, ponieważ


aktywna pętla blokuje jego pracę. Dobrym przykładem opóźnionego wykonania
zadania jest wyświetlanie ekranu powitalnego. Aktywność z ekranem powitalnym
jest uruchamiana w momencie otwarcia programu; po pewnym czasie należy
zastąpić ją docelowym ekranem aplikacji.
PROBLEM
Chcemy z opóźnieniem uruchamiać zadanie, którego kod jest wykonywany po
upływie określonego czasu.
ROZWIĄZANIE
Można użyć standardowej klasy Thread Javy, co pokazaliśmy w technice 21.,
i samodzielnie zaimplementować sprawdzanie upływu czasu, stosując wzorzec
sprawdzenie czasu, uśpienie, powtórzenie. Jest to jednak żmudne. Z pewnością
znajdziemy mechanizm, który automatycznie wykonuje potrzebne operacje.
Oto on: Timer (zegar) — jedna z klas biblioteki klas Javy.
Zegar można traktować jak klasę zarządzającą zadaniami. Planuje wykona-
nie prac (zaimplementowanych z wykorzystaniem klasy TimerTask), a po upły-
wie określonego czasu uruchamia je w odrębnym wątku. TimerTask to specjalny
rodzaj klasy typu Runnable. Udostępnia metodę run, a ponadto ma dodatkowe
funkcje. Może na przykład anulować zadanie oczekujące w kolejce. Zaimple-
mentujmy ekran powitalny aplikacji MyMovies z wykorzystaniem klas Timer
i TimerTask. Na rysunku 6.13 pokazano prosty ekran powitalny.

Rysunek 6.13. Prosty ekran powitalny aplikacji MyMovies.


Za pomocą dostępnego w motywie atrybutu windowNoTitle
usunęliśmy pasek tytułu

POBIERZ PROJEKT MYMOVIESWITHSPLASH-SCREEN.


Kod źródłowy projektu i pakiet APK do uruchamiania aplikacji
znajdziesz w witrynie z kodem do książki Android w prakty-
ce. Ponieważ niektóre listingi skrócono, abyś mógł skoncen-
trować się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/a0DD, plik APK: http://mng.bz/H8LM.
270 ROZDZIAŁ 6. Wątki i współbieżność

Oto niezbędne składniki z przepisu:


Q rysunek powitalny (na przykład plik PNG; nazwijmy go splash.png),
Q aktywność działająca w trybie pełnoekranowym (bez paska tytułu),
Q nowy wpis w manifeście, powiązany z aktywnością wyświetlającą ekran
powitalny.
Ekran powitalny, jak już wiesz, można umieścić w katalogu res/drawables. Aktyw-
ność wyświetlająca ekran powitalny ma minimalistyczny układ. Może on być tak
prosty, jak przedstawiono to poniżej:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView android:scaleType="fitXY"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/splash" />
</merge>

Kod jest prosty i jasny. Trzeba też zdefiniować nową aktywność w pliku mani-
festu (listing 6.10). Ponieważ aktywność ma być uruchamiana jako pierwsza,
należy zapisać ją przed aktywnością MyMovies.

Listing 6.10. Nowa (wyświetlająca ekran powitalny) aktywność w pliku


AndroidManifest.xml

...
<application android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@style/MyMoviesTheme">
<activity android:name=".SplashScreen"
android:label="@string/app_name"
android:theme="@style/SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name=".MyMovies" />

</application>

Może zauważyłeś, że dla aktywności wyświetlającej ekran powitalny zastosowa-


liśmy niestandardowy styl. Jest tak, ponieważ aktywności domyślnie mają pasek
tytułu. Chcemy, aby ekran powitalny zajmował cały wyświetlacz, dlatego w pliku
styles.xml umieściliśmy następujący kod:
<style name="SplashScreen" parent="@android:style/Theme.Black">
<item name="android:windowNoTitle">true</item>
</style>
0 TECHNIKA 26. Wyświetlanie ekranów powitalnych za pomocą zegarów 271

Na razie wszystko idzie dobrze. Przedstawiliśmy kod konfiguracyjny. Najważniejszy


jest jednak kod aktywności z listingu 6.11. W końcu rozdział ten dotyczy wątków.

Listing 6.11. Zegar można wykorzystać do wyświetlania ekranu po upływie


określonego czasu

public class SplashScreen extends Activity {

public static final int SPLASH_TIMEOUT = 1500;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.splash_screen);

new Timer().schedule(new TimerTask() {

@Override
public void run() {
startActivity(new Intent(SplashScreen.this, MyMovies.class));
finish();
}
}, SPLASH_TIMEOUT);
}
}

Kod ten jest dość prosty. Przy użyciu klasy Timer planuje wykonanie nowego
zadania, co gwarantuje, że zostanie ono uruchomione po upływie SPLASH_TIMEOUT
milisekund. Tu czas ustawiono na 1500 milisekund, czyli 1,5 sekundy. Samo
zadanie tworzy intencję do uruchamiania ekranu docelowego (głównej aktyw-
ności MyMovies). Zdumiewająco proste i skuteczne!
Można dodatkowo uatrakcyjnić ekran powitalny. Wielu użytkowników woli
pominąć taki ekran, dlatego warto zaimplementować odbiornik, który w reakcji
na dotknięcie ekranu powitalnego natychmiast przechodzi do ekranu docelo-
wego. Opracowanie tego rozwiązania pozostawiamy jako ćwiczenie.
OMÓWIENIE
Klasa Timer to coś więcej niż komponent szeregujący, który działa w trybie
„wykonaj zadanie X po Y sekundach”. Za pomocą tej klasy można zarządzać
wieloma zadaniami przez umieszczanie ich w kolejce lub okresowo wykonywać
jedno zadanie. Wszystkie zadania zawsze są uruchamiane sekwencyjnie w jed-
nym wątku. Jeśli operacje są wykonywane okresowo, standardowe działanie
(zaimplementowane w metodzie schedule przyjmującej argument period) polega
na planowaniu wykonania zadań w modelu względnym. Następne wykonanie
zadania jest planowane na co najmniej X milisekund od momentu rozpoczęcia
poprzedniego wykonania. Użyliśmy wyrażenia „co najmniej”, ponieważ jeśli sys-
tem jest mocno obciążony, rzeczywiste opóźnienie może być większe, ponieważ
zegar nie otrzymał wystarczająco dużo czasu procesora, aby zaplanować następne
wykonanie w pożądanym terminie.
272 ROZDZIAŁ 6. Wątki i współbieżność

Inaczej wygląda druga strategia planowania, zaimplementowana w metodzie


scheduleAtFixedRate. W niej obowiązuje model absolutny. Polega on na tym, że
opóźnienie wykonania zadania jest mierzone w czasie absolutnym. Gdy zegar
w tym modelu nie otrzyma wystarczająco dużo czasu procesora (z uwagi na obcią-
żenie systemu), spróbuje „nadrobić” czas i zaplanować pominięte wykonania jedno
po drugim, jeśli według planu kod powinien już działać. Niezależnie od zasto-
sowanej strategii okresowego wykonania w obu sytuacjach wielkości opóźnienia
nie da się precyzyjnie przewidzieć. Jeśli zatem wykonywanie kodu w dokładnych
przedziałach czasu jest ważne, nie należy stosować opisanej techniki.
Wątek zegara, podobnie jak zwykły wątek, można też uruchomić jako wątek
usługowy (ang. daemon thread). Zamykana aplikacja nie czeka na ukończenie
pracy takiego wątku, dlatego można go zastosować do kontrolowania innych wąt-
ków, zarządzania nimi lub implementowania określonego rodzaju usług. W wąt-
kach usługowych nie należy wykonywać ważnych operacji aplikacji lub zapisywać
danych, ponieważ aplikacja może znaleźć się w niespójnym stanie.
Klasy używane w tej technice nie są dedykowane dla Androida. Są one częścią
interfejsu API Javy. Ponieważ klasy Thread i ThreadPool przydają się na przykład
do wyświetlania ekranów powitalnych, stwierdziliśmy, że warto o nich wspomnieć
w tym miejscu. Teraz wróćmy do mechanizmów samego Androida. Następna
technika dotyczy bardziej zaawansowanego wykorzystania przedstawionych wcze-
śniej klas Handler i Message. Wykorzystujemy je do zaimplementowania niestan-
dardowych pętli komunikatów.

0 TECHNIKA 27. Implementowanie niestandardowych pętli komunikatów

W technice 22. omówiliśmy klasy Handler i Message. Wyjaśniliśmy tam, jak prze-
kazywać komunikaty między dwoma wątkami, a precyzyjniej — jak przekazywać
wiadomości z wątku roboczego do wątku interfejsu użytkownika. Może Ci się
wydawać, że jesteśmy nadmiernie drobiazgowi, ale to nieprawda. Wspomniana
technika nie pozwala na przesyłanie komunikatów w drugą stronę, z wątku inter-
fejsu użytkownika do wątku roboczego!
Dlaczego tak się dzieje? Z uwagi na kwestię, o której tylko napomknęliśmy.
Główny wątek interfejsu użytkownika obsługuje kolejkę komunikatów i w nie-
skończonej pętli sprawdza, czy kolejka zawiera nowe komunikaty. Domyślnie
działa tak tylko główny wątek interfejsu użytkownika. Ręcznie utworzone wątki
(nawet te zarządzane przez obiekty klasy AsyncTask) tego nie robią.
W wielu programach opisany model jest odpowiedni, ponieważ asynchro-
niczne zdarzenia z interfejsu użytkownika, na przykład dotknięcie lub przewi-
nięcie, to najczęściej występujący rodzaj zdarzeń w każdej aplikacji na Android.
Pętla komunikatów tworzona przez Android zapewnia obsługę takich zdarzeń.
Co jednak zrobić, jeśli wymagania są bardziej złożone? Pomyśl na przykład
o grach. W grach często implementowane są niestandardowe pętle do obsługi zda-
rzeń charakterystycznych dla kodu gry. Obsługa tych zdarzeń w wątku, który
odpowiada również za zdarzenia z interfejsu użytkownika, może być zbyt kosz-
0 TECHNIKA 27. Implementowanie niestandardowych pętli komunikatów 273

towna. Pamiętaj, że zawsze należy dbać o szybką reakcję interfejsu użytkownika.


Dlatego w wątku interfejsu użytkownika nigdy nie należy wykonywać kosztow-
nych operacji lub innych często uruchamianych prac niezwiązanych z interfejsem
użytkownika. Jest to jeden z przykładów czegoś, co w informatyce jest dobrze
znanym wzorcem stosowanym w programach współbieżnych różnego rodzaju.
Chodzi o problem producenta i konsumenta.
Problem ten polega na tym, że działają dwa współbieżne wątki. Wątek pro-
ducenta generuje obiekty i zapisuje je we współużytkowanej kolejce komunika-
tów. Wątek konsumenta używa tych obiektów. Taka sytuacja ma miejsce, kiedy
stosujesz komponent obsługi do aktualizowania czegoś w wątku interfejsu użyt-
kownika. Wątek roboczy (producent) wysyła obiekt (komunikat) do współużyt-
kowanej kolejki komunikatów. Następnie komunikat jest obsługiwany przez
wątek interfejsu użytkownika (konsumenta). Zobaczmy, jak zastosować ten wzo-
rzec do dwóch dowolnych wątków.
PROBLEM
Piszemy aplikację, w której kilka wątków musi wymieniać komunikaty między
sobą (tak jak w problemie producenta i konsumenta).
ROZWIĄZANIE
Wiesz już, jak powiązać z obiektem komponent obsługi i wykorzystać go do
przesyłania komunikatów. Interesujące pytanie dotyczy tego, jak zaimplemen-
tować niestandardową pętlę komunikatów do wykorzystywania komunikatów
poza wątkiem interfejsu użytkownika. W Androidzie pętle komunikatów dla wąt-
ków tworzy się za pomocą klasy Looper. Na pozór technika ta może wydawać się
skomplikowana, jednak w rzeczywistości nie jest złożona. Rozwiązanie jest pro-
ste, ponieważ klasa Looper wykonuje za programistę wszystkie trudne zadania.
Zastosowania wzorca producent-konsument są zwykle charakterystyczne dla
aplikacji. Dlatego zamiast wymyślać konkretny przykład, upraszczamy omówie-
nie i koncentrujemy się na samym wzorcu. Tobie pozostawiamy zdecydowanie,
jak wykorzystać go w swoich aplikacjach. W przedstawionym tu prostym przy-
kładzie istnieją dwa wątki producentów, które generują liczby losowe, i wątek
konsumenta (z pętlą komunikatów), odbierający liczby i dodający wpis w dzienni-
ku, jeśli dana wartość jest parzysta.

POBIERZ PROJEKT PRODUCERCONSUMERWITHLO-


OPER. Kod źródłowy projektu i pakiet APK do uruchamiania
aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł
skoncentrować się na konkretnych zagadnieniach, zalecamy
pobranie kompletnego kodu źródłowego i śledzenie go
w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/91Bu, plik APK: http://mng.bz/7y13.
274 ROZDZIAŁ 6. Wątki i współbieżność

Kod źródłowy całej aplikacji znajduje się na listingu 6.12.

Listing 6.12. Prosty scenariusz z producentem i konsumentem zaimplementowany


za pomocą klasy Looper

public class ProducerConsumer extends Activity {

private Handler handler;

private class Consumer extends Thread {

@Override
public void run() {

Looper.prepare();

handler = new Handler() {


@Override
public void handleMessage(Message msg) {
int number = msg.what;
if (number % 2 == 0) {
Log.d("Konsument", number + " jest podzielna przez 2");
} else {
Log.d("Konsument", number + " nie jest podzielna przez 2");
}
}
};

Looper.loop();
}
}

private class Producer extends Thread {

public Producer(String name) {


super(name);
}

@Override
public void run() {
Random random = new Random();
while (true) {
int number = random.nextInt(100);
Log.d("Producent " + getName(), Integer.toString(number));
handler.sendEmptyMessage(number);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
0 TECHNIKA 27. Implementowanie niestandardowych pętli komunikatów 275

new Consumer().start();
new Producer("A").start();
new Producer("B").start();
}
}

Pierwszy krok polega na utworzeniu współużytkowanej referencji do kompo-


nentu obsługi. Producenci korzystają z tej referencji do przesyłania liczb do
konsumentów . W wątku konsumenta należy utworzyć pętlę komunikatów
i powiązać z obiektem komponent obsługi. Następnie można uruchomić pętlę
komunikatów , która odbiera przychodzące liczby . Po stronie producenta
w nieskończonej pętli aplikacja generuje liczby z przedziału od 0 do 1000
(i okresowo usypia wątki przez wywołanie metody Thread.sleep) oraz wysyła je
do kolejki komunikatów konsumenta . Z uwagi na zwięzłość używamy metody
sendEmptyMessage(int), ponieważ wartość typu int to jedyne przesyłane dane.
Ostatni fragment kodu uruchamia wszystkie trzy wątki w metodzie onCreate .
Na rysunku 6.14 widać dane wyjściowe programu przetwarzającego liczby.
Dane te można wyświetlić za pomocą widoku LogCat w perspektywie Eclipse
DDMS lub przez uruchomienie instrukcji adb logcat z poziomu powłoki.

Rysunek 6.14.
Dane z dziennika
wygenerowane
przez aktywność
ProducerConsumer.
Dwóch producentów
umieszcza losowe
liczby w kolejce
komunikatów
konsumenta, który
może następnie
przetwarzać
te wartości

W porządku, sprawdzanie, czy liczba jest parzysta, czy nie, nie jest specjalnie
przydatne. Przyznajemy to. Powinno być jednak jasne, że wzorzec można
276 ROZDZIAŁ 6. Wątki i współbieżność

wykorzystać do rozdzielania obliczeń lub innych zadań między liczne wątki robo-
cze, które przekazują wyniki z powrotem do innego wątku („nadzorcy” odbie-
rającego wyniki). Pozostawiamy Twojej wyobraźni wymyślenie, jak wykorzystać
pokazane tu możliwości przetwarzania rozproszonego. Może warto uruchomić
program SETI na urządzeniach z Androidem?
OMÓWIENIE
Wyjaśniliśmy już istotne aspekty działania klasy Looper, uważamy jednak, że
koniecznie należy zwrócić uwagę na dwa zagadnienia.
Po pierwsze, interfejs klasy Looper umożliwia zarejestrowanie klasy IdleHandler
do obsługi kolejki komunikatów wątku z pętlą. W klasie IdleHandler zdefinio-
wana jest jedna wywoływana zwrotnie metoda. Aplikacja uruchamia ją, jeśli
w kolejce nie oczekują żadne komunikaty. W ten sposób można wykryć, że wątek
z pętlą oczekuje na dane i zużywa cenne zasoby. Aby zarejestrować obiekt klasy
IdleHandler dla kolejki komunikatów, należy wywołać metodę Looper.myQueue().
´addIdleHandler().
Po drugie, referencję do głównej klasy Looper aplikacji można pobrać przez
wywołanie metody Looper.getMainLooper. Jeśli rozwijasz aplikację, w której wydaj-
ność ma niezwykle duże znaczenie, zastosowanie tej metody i klasy IdleHandler
może być przydatne, ponieważ pozwala ustalić, kiedy klasa Looper interfejsu
użytkownika jest uśpiona. Dzięki temu wątek można wykorzystać do wyko-
nywania innych zadań, przez co zużywane przez niego zasoby się nie marnują.
Pamiętaj, że nie należy stosować tego podejścia do wykonywania zasobochłonnych
zadań, ponieważ w trakcie ich realizowania w kolejce komunikatów w klasie
Looper interfejsu użytkownika może znaleźć się wiele zdarzeń dotyczących tego
interfejsu.

6.4. Podsumowanie
W tym rozdziale pokazaliśmy, jak zapewnić reagowanie aplikacji na poczynania
użytkowników. Wymaga to wykonywania kosztownych zadań w odrębnych wąt-
kach. Zaczęliśmy od prostych rozwiązań — od podstawowych mechanizmów
Javy (na przykład klasy Thread) przeznaczonych do obsługi współbieżności.
Dalej opisaliśmy, jak umożliwić wątkowi roboczemu aktualizowanie interfejsu
użytkownika. Służą do tego dwie klasy Androida związane z przekazywaniem
komunikatów — Handler i Message. Rozbudowaliśmy też aplikację MyMovies,
tak aby asynchronicznie pobierała miniatury ze scenami z filmów. W ten sposób
pokazaliśmy, jak za pomocą wykonawców i pul wątków z języka Java zarządzać
wieloma wątkami pobierającymi dane, oszczędzając przy tym zasoby.
Choć podejście to zapewnia dużą swobodę, klasy zadań są często szablonowe
i obejmują dużo schematycznego kodu. Można uniknąć pisania takiego kodu —
wystarczy użyć klasy AsyncTask Androida, co pozwala uprościć zarządzanie wąt-
kami roboczymi. W końcowej części rozdziału znajduje się opis tego, jak wyko-
6.4. Podsumowanie 277

rzystać zaplanowane opóźnione prace do wyświetlania ekranów powitalnych.


Dowiedziałeś się też, jak tworzyć niestandardowe pętle komunikatów do swo-
bodnego kontaktowania się dowolnej liczby wątków. Nie trzeba przy tym zaj-
mować żadnych blokad obiektów ani używać innych prostych mechanizmów
synchronizacji. Co za wygoda!
Następny rozdział dotyczy najważniejszych rzeczy w aplikacji — danych!
Dowiesz się, jak korzystać z systemu plików Androida, jak zapisywać częściowo
ustrukturyzowane dane z zastosowaniem współużytkowanych plików ustawień,
jak tworzyć strony ustawień, a także jak utrwalać dane i zarządzać nimi za pomocą
baz SQLite. Zapraszamy do dalszej lektury!
278 ROZDZIAŁ 6. Wątki i współbieżność
Lokalne
zapisywanie danych

W tym rozdziale
Q Odczyt i zapis plików
Q Podawanie i zapamiętywanie współużytkowanych
ustawień
Q Korzystanie z baz SQLite

Dane są bardzo cenne i przetrwają dłużej niż same systemy.


Tim Berners-Lee
Dane są kluczowe w każdej aplikacji. Android udostępnia kilka mechanizmów
lokalnego przechowywania danych. Jednak to nie wszystko — dostępne są też
dane z innych aplikacji działających w urządzeniu i w sieci. Techniki dostępu
do tych danych poznasz w dalszych rozdziałach. Na razie jednak koncentrujemy
się na lokalnym przechowywaniu danych.
Omówienie lokalnego przechowywania danych zaczynamy od systemu plików.
Nie zapominaj, że urządzenia z Androidem to małe komputery, mające systemy
plików. Dalej dowiesz się, jak sprawdzić, czy taki system jest dostępny. Zoba-
czysz, jak go używać, jak działają uprawnienia i jakie występują różnice między
pamięcią wewnętrzną a zewnętrzną. Po przyjrzeniu się podstawowym opera-
cjom na plikach poznasz klasę SharedPreferences. Jest to przydatna klasa do zapi-
sywania danych w postaci par klucz-wartość. Klasa SharedPreferences wykorzy-
stuje system plików, jednak ukrywa niektóre operacje i w pewnych sytuacjach

279
280 ROZDZIAŁ 7. Lokalne zapisywanie danych

wygodniej jest korzystać z niej, niż bezpośrednio manipulować plikami. Po zapo-


znaniu się z plikami przejdziesz do bardziej zaawansowanych mechanizmów
zapisywania danych. Zobaczysz, jak używać wbudowanej w Android bazy danych
SQLite. Różni się ona od typowej relacyjnej bazy danych stosowanej na serwerze,
jednak także udostępnia dużo możliwości. Dowiesz się, jak wykorzystać tę bazę,
jak na jej podstawie utworzyć warstwę dostępu do danych, a także jak SQLite
różni się od baz, do których być może jesteś przyzwyczajony.
Pierwsze kroki z lokalnym przechowywaniem danych prowadzą do zagad-
nień opisanych w rozdziale 1. — identyfikatorów użytkownika i uprawnień —
które zawsze mają znaczenie przy korzystaniu z systemu plików.

7.1. Odczyt i zapis plików


Podstawowym sposobem lokalnego przechowywania danych w Androidzie jest
używanie systemu plików do odczytu i zapisu plików. Mechanizm ten można
wykorzystać do utrwalania danych i ich współużytkowania między poszczegól-
nymi komponentami aplikacji, a także między różnymi egzemplarzami programu.
W momencie zamknięcia aplikacji utracony zostaje jej nietrwały stan (o czym
wspomnieliśmy w rozdziale 3.), jednak wszystkie dane zapisane w systemie plików
są zachowywane.
Jeśli znasz bibliotekę java.io, masz już podstawową wiedzę na temat prze-
chowywania plików. Tu omawiamy dodatkowe szczegóły Androida, na przykład
uprawnienia i różnice między pamięcią wewnętrzną a zewnętrzną.

7.1.1. Pamięć wewnętrzna i zewnętrzna


Pierwszą rzeczą, jaką trzeba wyjaśnić przed przejściem do omawiania odczytu
i zapisu plików, są różnice między pamięcią wewnętrzną a zewnętrzną w Andro-
idzie. Na ogólnym poziomie różnice te wyglądają tak:
Q Pamięć wewnętrzna to pamięć wewnętrzna urządzenia. Jest prywatna
i zawsze dostępna.
Q Pamięć zewnętrzna może znajdować się na nośnikach wymiennych. Nie jest
prywatna i nie zawsze można uzyskać do niej dostęp.
Łatwiej jest korzystać z pamięci wewnętrznej, ponieważ zawsze można uzyskać
do niej dostęp (system nigdy jej nie odmontowuje), a ponadto jest bezpieczna.
Pamięć zewnętrzna nie zawsze jest dostępna i nie jest bezpieczna. Użytkownicy
mogą odmontować i usunąć pamięć zewnętrzną lub zamontować ją jako niedo-
stępną dla urządzenia pamięć USB. Aby lepiej zrozumieć, czym jest punkt mon-
towania, i przekonać się, że różne zasoby są montowane za pomocą odmiennych
właściwości, warto uruchomić polecenie mount na urządzeniu z Androidem 2.2.
Efekt pokazano na rysunku 7.1.
7.1. Odczyt i zapis plików 281

Rysunek 7.1. Polecenie mount pozwala zobaczyć wybrane miejsca i typy używane
przez systemy plików z Androida

Ten fragmentaryczny wynik uruchomienia instrukcji mount pozwala stwierdzić,


że Android korzysta z kilku rodzajów systemu plików zamontowanych w różnych
miejscach. Każdy rodzaj ma odmienny zestaw właściwości. Najpierw znajduje
się partycja / typu rootfs. Jest to specjalna partycja, pozwalająca dołączyć wszystkie
pozostałe urządzenia do jednego drzewa. Inną specjalną partycją jest system,
która (podobnie jak /) jest oznaczona jako ro (ang. read only), czyli tylko do odczytu.
Wymienione partycje zawierają najważniejsze pliki i dane systemu operacyjnego.
Zwykle partycji tych nie używa się bezpośrednio. Należy korzystać z innych
punktów montowania, takich jak /data lub /cache.
Punkty montowania /data i /cache reprezentują pamięć wewnętrzną. Można
użyć w nich systemów plików różnego rodzaju (na rysunku zastosowano typ
yaffs2, czyli Yet Another Flash File System 2, zaprojektowany dla urządzeń z wbu-
dowaną pamięcią flash; użyty typ zależy od urządzenia). Wymienione punkty
umożliwiają zapis, na co wskazują uprawnienia rw (ang. read-write), czyli do
odczytu i zapisu.
Oprócz tych i kilku innych specjalnych partycji występuje punkt montowania
/mnt/sdcard typu vfat. Reprezentuje on pamięć zewnętrzną. Ważny jest tu typ
FAT (ang. File Allocation Table) systemu plików. Jest to prosty typ. Niemal każdy
system operacyjny może wczytywać i zapisywać dane w systemach plików tego
typu. To dlatego używa się go w tak wielu kartach pamięci i aparatach fotogra-
ficznych. Ponadto systemy plików tego rodzaju można montować przez port
USB i używać jako nośnika wirtualnego w niemal dowolnym systemie operacyj-
nym. Prostota jest tu zapewniana kosztem bezpieczeństwa.
Pamięć zewnętrzna często jest dostępna jako nośniki wymienne, na przy-
kład karty SD (ang. Secure Digital), stąd nazwa sdcard w ścieżce. Warto jednak
pamiętać, że nie zawsze tak jest. Niektóre urządzenia nie udostępniają pamięci
wymiennej, a w innych pamięć wewnętrzna jest traktowana w Androidzie jak
zewnętrzna. W niektórych urządzeniach występuje pamięć zewnętrzna obu
rodzajów. Oznacza to, że istnieją dwa typy pamięci zewnętrznej — wymienna
i niewymienna. Android w danym momencie montuje i obsługuje tylko jeden
282 ROZDZIAŁ 7. Lokalne zapisywanie danych

typ pamięci zewnętrznej. Większość użytkowników piszących na listach dysku-


syjnych preferuje „wewnętrzną” pamięć zewnętrzną, ponieważ jest uznawana
za bardziej niezawodną.
Więcej o bezpieczeństwie i ścieżkach dowiesz się, kiedy zajmiemy się odczy-
tem i zapisem plików. Zacznijmy od pamięci wewnętrznej.

0 TECHNIKA 28. Korzystanie z pamięci wewnętrznej

Poznałeś już różnice między pamięcią wewnętrzną a zewnętrzną. Wyjaśniliśmy


też, że pamięć wewnętrzna jest bezpieczniejsza i bardziej niezawodna. Możliwe,
że zastanawiasz się, po co w ogóle korzystać z pamięci zewnętrznej. Jak zawsze,
trzeba uwzględnić wady i zalety obu rozwiązań.
Pojemność pamięci wewnętrznej jest ograniczona. Po wyczerpaniu tej pamięci
nie można instalować dodatkowych aplikacji. Dlatego doświadczeni użytkownicy
sprawdzają, ile miejsca zajmuje aplikacja, i jeśli przechowuje ona dużo danych
w pamięci wewnętrznej, rezygnują z niej, co jest słusznym podejściem. Uzasad-
nione jest wykorzystanie kilku megabajtów przez aplikację Google Earth, jednak
jeśli jakaś gra zajmuje 30 megabajtów, oznacza to błąd programistów, który użyt-
kownicy zauważą.
URUCHAMIANIE APLIKACJI W PAMIĘCI ZEWNĘTRZNEJ. Od
ósmej wersji interfejsu API Androida (od wersji 2.2 tej platformy) można
uruchamiać aplikacje z pamięci zewnętrznej. Mechanizm ten można
włączyć w manifeście za pomocą atrybutu android:installLocation. Jeśli
wartość tego atrybutu to preferExternal lub auto, niektóre komponenty
aplikacji można umieszczać w szyfrowanym odrębnym punkcie montowa-
nia w pamięci zewnętrznej. Inne dane, na przykład bazy i prywatne dane
użytkownika, nie są tam zapisywane. Pozwala to zmniejszyć wykorzystanie
wewnętrznej pamięci. Użytkownicy to doceniają. Opisana technika nie
prowadzi do spadku wydajności. Jej jedyną wadą jest to, że jeśli użytkow-
nik zamontuje pamięć zewnętrzną z wykorzystaniem portu USB, wszystkie
aplikacje działające w pamięci zewnętrznej zostaną zatrzymane. Oznacza
to, że techniki tej nie należy stosować w aplikacjach udostępniających
usługi, rejestrujących alarmy lub innych, których zatrzymanie może pro-
wadzić do problemów.
Z uwagi na ograniczoną ilość miejsca trzeba zdecydować, które dane są wystar-
czająco ważne, aby zapisać je w pamięci wewnętrznej, a które można umieścić
w pamięci zewnętrznej. Najłatwiejszy sposób na wprowadzenie tego rozróżnie-
nia to ustalenie, co w aplikacji jest niezbędne, a co można pominąć. Jeśli trzeba
przechowywać rysunki w pamięci podręcznej, lepiej użyć do tego pamięci zew-
nętrznej i wyświetlać obrazki zastępcze, kiedy ta ostatnia nie jest dostępna.
Natomiast model danych aplikacji (nazwiska, miejscowości, tytuły filmów, nazwy
0 TECHNIKA 28. Korzystanie z pamięci wewnętrznej 283

drużyn piłkarskich itd.) zwykle należy przechowywać w pamięci wewnętrznej,


przy czym dane te powinny zajmować jak najmniej miejsca.
PROBLEM
Znamy różne miejsca, w których można przechowywać pliki i inne dane. Chcemy
wykorzystać pamięć wewnętrzną i móc przeglądać zapisane w niej dane.
ROZWIĄZANIE
Tym razem używamy przykładowej aplikacji do odczytu i zapisu danych w pamięci
podręcznej. Następnie korzystamy z powłoki adb do przeglądania danych. Na
rysunku 7.2 widoczne są proste ekrany programu FileExplorer. Przykładowa apli-
kacja nie jest elegancka, ale poprawnie wykonuje zadanie.

Rysunek 7.2. Aplikacja FileExplorer pozwala na zapis i odczyt pliku tekstowego


w pamięci wewnętrznej

POBIERZ PROJEKT FILEEXPLORER. Kod źródłowy projektu


i pakiet APK do uruchamiania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Ponieważ niektóre lis-
tingi skrócono, abyś mógł skoncentrować się na konkretnych
zagadnieniach, zalecamy pobranie kompletnego kodu źródło-
wego i śledzenie go w Eclipse (lub innym środowisku IDE albo
edytorze tekstu).
Źródło: http://mng.bz/FRV9, plik APK: http://mng.bz/XuAp.
Pierwsza aktywność w aplikacji FileExplorer odpowiada ekranowi, na którym
użytkownik może wybrać, czy chce korzystać z pamięci wewnętrznej, czy zew-
nętrznej. Kod tej aktywności jest prosty, dlatego nie przedstawiamy go w tym
miejscu (znajdziesz go w pobranym projekcie). Wybranie pamięci wewnętrznej
prowadzi do uruchomienia aktywności InternalStorage. Obejmuje ona obiekty
284 ROZDZIAŁ 7. Lokalne zapisywanie danych

klas EditText i TextView oraz przycisk widoczne na rysunku 7.2. Użytkownik


może wpisać tekst i kliknąć przycisk Zapisz, aby zapisać tekst do pliku. Kliknięcie
przycisku Wczytaj powoduje wczytanie i wyświetlenie pliku. Ciekawy jest kod
metod do zapisu i odczytu pliku. Przedstawiamy go na listingu 7.1.

Listing 7.1. Metody do odczytu i zapisu plików z aktywności z pliku


InternalStorage.java

public static final String LINE_SEP = System.getProperty("line.separator");

private void write() {


FileOutputStream fos = null;
try {
fos = openFileOutput("test.txt", Context.MODE_PRIVATE);
fos.write(input.getText().toString().getBytes());
Toast.makeText(this, "Plik zapisano", Toast.LENGTH_SHORT).show();
input.setText("");
output.setText("");
} catch (FileNotFoundException e) {
Log.e(Constants.LOG_TAG, "Pliku nie znaleziono", e);
} catch (IOException e) {
Log.e(Constants.LOG_TAG, "Problem z operacjami wejścia-wyjścia", e);
} finally {
try {
fos.close();
} catch (IOException e) {
}
}
}

private void read() {


FileInputStream fis = null;
Scanner scanner = null;
StringBuilder sb = new StringBuilder();
try {
fis = openFileInput("test.txt");
scanner = new Scanner(fis);
while (scanner.hasNextLine()) {
sb.append(scanner.nextLine() + LINE_SEP);
}
Toast.makeText(this, "Plik wczytano", Toast.LENGTH_SHORT).show();
} catch (FileNotFoundException e) {
Log.e(Constants.LOG_TAG, "Pliku nie znaleziono", e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
}
}
if (scanner != null) {
scanner.close();
}
}
output.setText(sb.toString());
}
0 TECHNIKA 28. Korzystanie z pamięci wewnętrznej 285

Najłatwiejszy sposób na zapisanie prostych plików w pamięci podręcznej to uży-


cie metod pomocniczych do obsługi strumieni wejścia i wyjścia. Metody te są
udostępniane przez klasę Context. W tym podejściu odczyt i zapis plików odbywa
się podobnie jak przy użyciu biblioteki java.io.
Najpierw należy utworzyć strumień FileOutputStream za pomocą metody
openFileOutput . Ta specjalna metoda tworzy plik w odpowiednim miejscu
pamięci wewnętrznej aplikacji (jeśli dany plik jeszcze nie istnieje) i umożliwia
ustawienie uprawnień. Pliki wewnętrzne zwykle są prywatne, jednak można
użyć także trybów MODE_WORLD_READABLE i MODE_WORLD_WRITABLE. Po utworzeniu stru-
mienia należy zapisać do niego dane , a po zakończeniu tej operacji zamknąć
strumień .
PRZYPOMNIENIE — UWAGA NA BARDZO UPROSZCZONE PRZY-
KŁADY. W przykładowych aktywnościach do obsługi plików można
dostrzec potencjalny problem. Operacje wejścia-wyjścia są wykonywane
w głównym wątku interfejsu użytkownika. Prawie zawsze jest to zły pomysł.
Odczyt i zapis danych w systemie plików (zarówno wewnętrznym, jak
i zewnętrznym) może spowodować zablokowanie głównego wątku inter-
fejsu użytkownika. W praktyce operacje te należy wykonywać w klasie
Handler lub AsyncTask (po przekazaniu do niej referencji do pliku). Tu nie
stosujemy tego rozwiązania, ponieważ chcemy, aby każdy przykład był
jak najkrótszy i możliwie konkretny. Korzystać z wątków nauczyłeś się
w rozdziale 6.
Po uruchomieniu powłoki adb i przejściu do wewnętrznego katalogu /data/data/
<nazwa-pakietu>/files zobaczysz plik zapisany przez klasę InternalStorage. Po
wprowadzeniu tekstu widocznego na rysunku 7.2 i wciśnięciu przycisku Zapisz
możesz wyświetlić informacje o uprawnieniach do pliku i jego zawartość, urucha-
miając polecenia widoczne na rysunku 7.3.

Rysunek 7.3. Powłoka adb wyświetla informacje o pliku zapisanym w pamięci


wewnętrznej przez program FileExplorer

Inną ważną cechą pamięci wewnętrznej jest to, że klasa Context udostępnia kilka
innych metod pomocniczych do wyświetlania i usuwania plików, a także do pobie-
rania wewnętrznego katalogu z pamięcią podręczną. Znaczenie takich katalogów
opisujemy w technice 30.
286 ROZDZIAŁ 7. Lokalne zapisywanie danych

OMÓWIENIE
Pamięć wewnętrzna jest prosta w użyciu. Ważne są tu metody pomocnicze
Androida, dzięki którym pliki są zapisywane w odpowiednim miejscu i w razie
potrzeby automatycznie tworzone. Następnie odczyt i zapis danych odbywa się
za pomocą standardowych operacji z biblioteki java.io. Powłoka pomaga w spraw-
dzaniu danych i rozwiązywaniu problemów. Możesz użyć powłoki uruchamianej
z wiersza poleceń lub eksploratora plików dostępnego we wtyczce ADT środo-
wiska Eclipse.
Teraz dowiesz się, jak używać pamięci zewnętrznej.

0 TECHNIKA 29. Korzystanie z pamięci zewnętrznej

Jak już wiesz, pamięć zewnętrzna w Androidzie (zarówno wymienna, jak i nie-
wymienna) jest montowana w innym systemie plików niż pamięć wewnętrzna.
Jest z natury mniej bezpieczna, jednak pozwala łatwo korzystać z danych i prze-
chowywać je poza mało pojemną pamięcią wewnętrzną. Liczne pliki aplikacji,
kopie bezpieczeństwa, pamięć podręczną, rysunki itd. warto zapisywać właśnie
w pamięci zewnętrznej. Ponadto takiej pamięci można użyć do przechowywania
danych, które mają być dostępne dla innych aplikacji.
PROBLEM
Chcemy przechowywać dane w pamięci zewnętrznej. Ponadto chcemy móc
łatwo ustalić (niezależnie od używanej wersji pakietu SDK Androida), kiedy
pamięć zewnętrzna jest dostępna, a kiedy nie można z niej korzystać.
ROZWIĄZANIE
Aby pokazać funkcjonowanie pamięci zewnętrznej, zamierzamy kontynuować
rozwijanie przykładowej aplikacji FileExplorer i wykonać w takiej pamięci ope-
racje przeprowadzane wcześniej w pamięci wewnętrznej. Aplikacja pozwala zapi-
sać w polu tekstowym informacje umieszczane w pliku, a następnie wczytać ten
plik. Interfejs użytkownika tej wersji aplikacji (rysunek 7.4) wygląda tak samo jak
przy korzystaniu z pamięci wewnętrznej.
Przedstawiona na listingu 7.2 klasa aktywności ExternalStorage jest prawie taka
sama jak klasa InternalStorage, jednak implementacja metod do odczytu i zapisu
pliku wygląda tu inaczej.

Listing 7.2. Metody klasy aktywności ExternalStorage przeznaczone do odczytu


i zapisu plików

private void write() {


if (FileUtil.isExternalStorageWritable()) {
File dir =
FileUtil.getExternalFilesDirAllApiLevels(
this.getPackageName());
File file = new File(dir, "test.txt");
FileUtil.writeStringAsFile(input.getText().toString(), file);
Toast.makeText(this, "Plik zapisano", Toast.LENGTH_SHORT).show();
0 TECHNIKA 29. Korzystanie z pamięci zewnętrznej 287

Rysunek 7.4. Aplikacja FileExplorer zapisuje plik tekstowy w pamięci zewnętrznej


i wczytuje go

input.setText("");
output.setText("");
} else {
Toast.makeText(this, "Brak możliwości zapisu w pamięci zewnętrznej",
Toast.LENGTH_SHORT).show();
}
}

private void read() {


if (FileUtil.isExternalStorageReadable()) {
File dir =
FileUtil.getExternalFilesDirAllApiLevels(
this.getPackageName());
File file = new File(dir, "test.txt");
if (file.exists() && file.canRead()) {
output.setText(FileUtil.readFileAsString(file));
Toast.makeText(this, "Plik wczytano", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Nie można wczytać pliku: "
+ file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "Brak możliwości zapisu w pamięci zewnętrznej",
Toast.LENGTH_SHORT).show();
}
}

Pierwszą rzeczą, na jaką warto zwrócić uwagę w metodach read i write klasy
ExternalStorage, jest użycie w kilku miejscach klasy FileUtil. Jest to przykładowa
krótka klasa narzędziowa dodana do aplikacji. Obejmuje przydatne metody, które
można zastosować w kilku aktywnościach, a nawet w różnych aplikacjach. Kod
tej klasy znajduje się pod omówieniem metod read i write.
288 ROZDZIAŁ 7. Lokalne zapisywanie danych

Pierwszą operacją, do której wykorzystujemy klasę FileUtil w metodzie write,


jest sprawdzenie, czy możliwy jest zapis pliku w pamięci zewnętrznej . Jeśli
korzystasz z emulatora, koniecznie utwórz kartę SD dla używanego egzemplarza.
Jeżeli zapis w pamięci wewnętrznej jest możliwy, metoda ponownie używa klasy
FileUtil do pobrania referencji do pliku. Referencja ta określa sugerowaną ścieżkę
zewnętrzną do plików aplikacji . Ścieżka ta to /sdcard/Android/data/<nazwa_
pakietu>/files. Możliwe, że dostrzegłeś już wzorzec. Ścieżka ta odpowiada ścieżce
do katalogu z wewnętrznymi danymi (użyto jedynie innego punktu montowania
i katalogu nadrzędnego). Po uzyskaniu ścieżki należy utworzyć plik i zapisać w nim
dane . W metodzie read stosujemy podobne podejście. Metoda ta sprawdza,
czy możliwy jest odczyt z pamięci zewnętrznej , a następnie pobiera ścieżkę
i wczytuje dane . Po otwarciu powłoki można zobaczyć plik w określonym miej-
scu pamięci podręcznej, co pokazano na rysunku 7.5.

Rysunek 7.5. Powłoka adb pozwala sprawdzić plik zapisany w pamięci zewnętrznej
przez przykładową aplikację FileExplorer

Najważniejszy kod do wykonywania operacji wejścia-wyjścia znajduje się w klasie


FileUtil, przedstawionej na listingu 7.3.

Listing 7.3. Klasa FileUtil wykonująca potrzebne w różnych miejscach operacje


na plikach

public final class FileUtil {

private static final String


EXT_STORAGE_PATH_PREFIX = "/Android/data/";
private static final String
EXT_STORAGE_FILES_PATH_SUFFIX = "/files/";
private static final String
EXT_STORAGE_CACHE_PATH_SUFFIX = "/cache/";

public static final Object[] DATA_LOCK = new Object[0];

private FileUtil() {
}

public static boolean isExternalStorageWritable() {


return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED);
}

public static boolean isExternalStorageReadable() {


if (isExternalStorageWritable()) {
0 TECHNIKA 29. Korzystanie z pamięci zewnętrznej 289

return true;
}
return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED_READ_ONLY);
}

public static File getExternalFilesDirAllApiLevels(


final String packageName) {
return FileUtil.getExternalDirAllApiLevels(
packageName, EXT_STORAGE_FILES_PATH_SUFFIX);
}

public static File getExternalCacheDirAllApiLevels(


String packageName) {
return FileUtil.getExternalDirAllApiLevels(
packageName, EXT_STORAGE_CACHE_PATH_SUFFIX);
}

private static File getExternalDirAllApiLevels(


String packageName, String suffixType) {
File dir = new File(Environment.getExternalStorageDirectory()
+ EXT_STORAGE_PATH_PREFIX + packageName + suffixType);
synchronized (FileUtil.DATA_LOCK) {
try {
dir.mkdirs();
dir.createNewFile();
} catch (IOException e) {
Log.e(Constants.LOG_TAG, "Błąd przy tworzeniu pliku", e);
}
}
return dir;
}

public static boolean writeStringAsFile(


String fileContents, File file) {
boolean result = false;
try {
synchronized (FileUtil.DATA_LOCK) {
if (file != null) {
file.createNewFile();
Writer out =
new BufferedWriter(new FileWriter(file), 1024);
out.write(fileContents);
out.close();
result = true;
}
}
} catch (IOException e) {
Log.e(Constants.LOG_TAG,
"Błąd zapisu danych do pliku " + e.getMessage(), e);
}
return result;
}

public static String readFileAsString(File file) {


StringBuilder sb = null;
try {
290 ROZDZIAŁ 7. Lokalne zapisywanie danych

synchronized (FileUtil.DATA_LOCK) {
if ((file != null) && file.canRead()) {
sb = new StringBuilder();
String line = null;
BufferedReader in =
new BufferedReader(new FileReader(file), 1024);
while ((line = in.readLine()) != null) {
sb.append(line + System.getProperty("line.separator"));
}
}
}
} catch (IOException e) {
Log.e(Constants.LOG_TAG,
"Błąd odczytu pliku " + e.getMessage(), e);
}
if (sb != null) {
return sb.toString();
}
return null;
}
}

Na początku klasy FileUtil zdefiniowanych jest kilka stałych określających zale-


cane ścieżki do plików z pamięci zewnętrznej . Wkrótce dowiesz się, dlaczego
stałe te są przydatne. Dalej znajduje się definicja tablicy Object[], używanej póź-
niej jako blokada dla synchronizowanych bloków kodu . Ponieważ do metod
narzędziowych dostęp mają różne wątki, które mogą używać tych samych plików,
należy zsynchronizować pracę tych wątków, aby uniknąć problemów wynikają-
cych z jednoczesnej modyfikacji danych. Dalej zdefiniowane są metody, których
używaliśmy wcześniej — na przykład metody do sprawdzania, czy możliwy jest
zapis i odczyt w pamięci zewnętrznej . Do wykonywania takich operacji służy
klasa Environment, obejmująca metody narzędziowe, które zwracają potrzebne
informacje. Metody klasy Environment można wywoływać także w aktywnościach
(czasem jest to uzasadnione), jednak tu umieściliśmy je w jednym miejscu, aby nie
powielać kodu.
Po metodach do sprawdzania stanu znajdują się metody getExternalFilesDir
´AllApiLevels i getExternalCacheDirAllApiLevels . Są to nakładki na prywatną
klasę getExternalDirAllApiLevels . Nakładki te zapewniają zgodny ze starszymi
wersjami sposób na pobranie zalecanych ścieżek. Jeśli wiadomo, że kod zawsze
będzie działał na urządzeniach z obsługą interfejsów API w wersji 8. lub nowszej,
można wywołać metodę Context.getExternalFilesDir lub Context.getExternal
´CacheDir. Tu jednak tego nie wiemy. Wielu użytkowników nadal korzysta
z urządzeń ze starszymi wersjami Androida, dlatego nie należy polegać na wymie-
nionych metodach. Z tego powodu utworzyliśmy metody narzędziowe wykonu-
jące potrzebne operacje dla dowolnej wersji interfejsu API. Służy do tego metoda
getExternalStorageDirectory (dostępna we wszystkich wersjach); do wyniku jej
działania należy dołączyć zalecane ścieżki zapisane we wspomnianych wcze-
śniej stałych.
0 TECHNIKA 29. Korzystanie z pamięci zewnętrznej 291

DLACZEGO WARTO STOSOWAĆ ZALECANE ŚCIEŻKI? Jeśli korzy-


stałeś z Androida przez dłuższy czas i przyjrzałeś się obszarowi z pamięcią
zewnętrzną, prawdopodobnie zauważyłeś, że pliki były umieszczane
w rozmaitych katalogach. Wynikało to z tego, że w kilku pierwszych wer-
sjach Androida nie istniały zalecane ścieżki i każda aplikacja mogła zapisy-
wać dane w dowolnym miejscu. Było to kłopotliwe, ponieważ prowadziło
do znacznego wzrostu liczby katalogów. Ponadto żadne z plików nie były
usuwane w momencie deinstalacji programu. Stosowanie zalecanych ście-
żek sprawia, że dane są lepiej uporządkowane. Ponadto użytkownicy (i inne
aplikacje) wiedzą, gdzie można znaleźć dane, a platforma może usunąć
niepotrzebne pliki.
Po metodach pomocniczych znajdują się metody writeStringAsFile i readFileAs
´String. Nie ma w nich nic charakterystycznego dla Androida. Wykorzystaliśmy
tu standardową bibliotekę java.io i przekazaliśmy do metod wymagane referencje
do pliku. Do odczytu i zapisu danych posłużyły klasy FileWriter i FileReader .
Jeśli potrzebna jest większa kontrola, na przykład w celu określenia kodowania
pliku, można zastosować niskopoziomowe klasy FileInputStream i (lub) FileOutput
´Stream. Tu użyliśmy klas FileReader i FileWriter, ponieważ pozwalają pisać
zwięźlejszy kod, a domyślne kodowanie systemowe jest odpowiednie.
Choć w opisanym podejściu ilość kodu jest mniejsza niż przy korzystaniu
z innych klas Javy do obsługi wejścia-wyjścia, kod ten nie jest zbyt elegancki.
Nie jest to kod, który chcielibyśmy powielać w różnych komponentach Androida.
To dlatego operacje umieściliśmy w klasie narzędziowej.
UKRYWANIE DANYCH APLIKACJI. Jeśli przechowujesz rysunki,
muzykę lub inne pliki, które mogą zostać znalezione w pamięci zewnętrz-
nej przez androidowy mechanizm wykrywania multimediów, możesz też
umieścić w danym katalogu plik .nomedia. Jest to plik ukryty (dlatego jego
nazwa rozpoczyna się od kropki), informujący mechanizm wykrywania
multimediów, że ma pominąć dany katalog. Jeśli nie zastosujesz takiego
pliku, rysunki z programu znajdą się w aplikacji Gallery. Prawdopodobnie
nie jest to pożądane.
Wspomnieliśmy już o zalecanych ścieżkach do pamięci zewnętrznej. Istnieje też
kilka innych konwencji do wskazywania danych, które mają być publiczne. Jeśli
chcesz współużytkować pliki z pamięci podręcznej, zacznij od ścieżki zwraca-
nej przez metodę getExternalStorageDirectory, a następnie dołącz odpowiedni
standardowy katalog (na przykład /Music, /Movies lub /Pictures). Więcej o multi-
mediach i publicznych ścieżkach do współużytkowania danych w pamięci zew-
nętrznej dowiesz się z rozdziału 11.
292 ROZDZIAŁ 7. Lokalne zapisywanie danych

OMÓWIENIE
Zapisywanie danych zewnętrznych przypomina zapisywanie danych wewnętrz-
nych. Najpierw należy użyć metod pomocniczych Androida do ustalenia odpo-
wiednich katalogów, a następnie zastosować bibliotekę java.io do wykonania
operacji. Przy korzystaniu z danych zewnętrznych trzeba się upewnić, że kata-
logi określone w ścieżce są dostępne, i przygotować rozwiązania rezerwowe (na
przykład rysunki zastępcze) na wypadek, gdyby nie można było użyć potrzeb-
nego katalogu. Należy też ustalić, czy wszystkie katalogi umożliwiają odczyt lub
zapis (czy nie są zabezpieczone). Ponadto sprawdzanie zawartości pamięci zew-
nętrznej i rozwiązywanie związanych z nią problemów wygląda tak samo jak dla
pamięci wewnętrznej (przy czym należy zacząć od ścieżki /sdcard).
Po omówieniu podstaw odczytu i zapisu plików dalej objaśniamy działanie
katalogów na pamięć podręczną.

0 TECHNIKA 30. Używanie katalogów na pamięć podręczną

Android udostępnia katalogi na pamięć podręczną zarówno w pamięci wewnętrz-


nej, jak i zewnętrznej. Co sprawia, że katalogi te są wyjątkowe, i dlaczego są
potrzebne? Są to katalogi zarządzane w pewnym stopniu przez platformę. Zarzą-
dzanie to polega między innymi na usuwaniu danych w momencie zamknięcia
powiązanej aplikacji. Ponadto katalogi te są automatycznie opróżniane, jeśli sys-
tem potrzebuje dodatkowej pamięci.
PROBLEM
Chcemy przechowywać dane tymczasowe w predefiniowanym miejscu, tak aby
Android pomagał w zarządzaniu nimi.
ROZWIĄZANIE
Jeśli masz dane (na przykład rysunki z usług sieciowych), które chcesz prze-
chowywać tylko przez określony czas, powinieneś wykorzystać katalogi z pamię-
cią podręczną w pamięci wewnętrznej i zewnętrznej. Stosowanie pamięci pod-
ręcznej to sztuka, dlatego używanie tej pamięci zależy od sytuacji — rodzaju
danych, jakie trzeba zapisywać, i sposobów zarządzania nimi. Android pomaga
w pracy, ponieważ udostępnia w wewnętrznym i zewnętrznym systemie plików
specjalne katalogi na pamięć podręczną.
Metoda Context.getCacheDir zwraca referencję do katalogu na pamięć pod-
ręczną z pamięci wewnętrznej — /data/data/<nazwa_pakietu>/cache. Choć
system może usunąć zawartość tego katalogu, kiedy musi odzyskać pamięć, nie
można zakładać, że na pewno tak się stanie. To w aplikacji należy zarządzać pamię-
cią podręczną i uważać na to, aby w katalogu nie znalazło się zbyt wiele danych
(zgodnie z dokumentacją zalecanym maksimum jest 1 megabajt).
W interfejsach API od wersji 8. dostępna jest też podobna metoda Context.
´getExternalCacheDir. Zwraca ona referencję do katalogu na pamięć podręczną
z pamięci zewnętrznej. Jeśli chcesz, aby aplikacja działała w starszych wersjach
0 TECHNIKA 31. Stosowanie synchronizacji przy zapisie plików 293

interfejsu API, możesz zastosować kod podobny do tego z listingu 7.3 i ręcznie
tworzyć ścieżkę zwracaną przez metodę getExternalCacheDir.
OMÓWIENIE
Zamiast tworzyć własne specjalne katalogi na pamięć podręczną, należy korzystać
ze ścieżek zalecanych przez platformę, prowadzących do zarządzanych katalogów.
Kiedy aplikacja prawidłowo używa katalogów na pamięć podręczną platformy,
system może odpowiednio zarządzać pamięcią. Może usunąć pliki, aby w razie
potrzeby odzyskać pamięć. Może też całkowicie usunąć katalogi na pamięć pod-
ręczną po deinstalacji powiązanej aplikacji. Techniki te pomagają porządkować
i kontrolować pliki oraz umożliwiają aplikacjom współdziałanie i wydajniejsze
współużytkowanie zasobów.
Bardzo ważna jest wiedza o tym, jak i gdzie należy zapisywać pliki — a nawet
określone typy plików, na przykład przeznaczone do umieszczania w pamięci
podręcznej. Niestety, nie jest to jedyna kwestia, jaką trzeba uwzględnić. Aby mieć
pewność, że pliki są zapisywane na dysku w odpowiednim momencie, trzeba
też umieć je synchronizować.

0 TECHNIKA 31. Stosowanie synchronizacji przy zapisie plików

W większości urządzeń z Androidem w wersjach do 2.2 działają systemy plików


(na przykład YAFFS) bez agresywnego buforowania danych. Pliki są natych-
miast zapisywane na dysku. W niektórych nowszych urządzeniach (i niestandar-
dowych nośnikach pamięci ROM), zwłaszcza w tych z systemem Android 2.3,
stosuje się systemy plików z księgowaniem (ang. journaled filesystems), na przy-
kład ext4. Systemy tego rodzaju wykorzystują buforowanie w większym stopniu.
Oznacza to, że nie zawsze natychmiast zapisują pliki na dysku. Buforowanie
pozwala zwiększyć niezawodność systemu plików (zapewnić lepszą obsługę
awarii) i wydajniej przeprowadzać zapis bloków danych, jednak czasem sprawia
problemy programistom.
PROBLEM

Chcemy zagwarantować natychmiastowy zapis plików na dysku niezależnie od


systemu plików i wersji platformy.
ROZWIĄZANIE

Czasem przed przejściem do dalszych operacji trzeba zagwarantować, że plik


zostanie zapisany na dysku. Jeśli na przykład zapisujesz plik w jednym procesie,
a w drugim aplikacja ma go wczytywać, przed rozpoczęciem odczytu warto się
upewnić, że dane zostały zapisane. Aby zagwarantować natychmiastowy zapis
pliku niezależnie od systemu plików, można ręcznie wywołać metodę sync. Syn-
chronizacja sprawia, że bufor jest dopasowywany do zawartości dysku fizycznego.
294 ROZDZIAŁ 7. Lokalne zapisywanie danych

Zaskoczeniem może być informacja, że metody klasy FileOutputStream, na przy-


kład flush, write, a nawet close, tego nie zapewniają.
Metoda sync należy do klasy FileDescriptor Javy. FileDescriptor to niskopo-
ziomowy uchwyt do operacji na właściwym dla urządzenia systemie plików.
Z klasy FileOutputStream można pobrać referencję do obiektu klasy FileDescriptor,
a następnie wywołać metodę sync. Rozwiązanie to pokazujemy na listingu 7.4.

Listing 7.4. Używanie klasy FileDescriptor do zapewnienia zapisu danych w systemie


plików

public static boolean syncStream(FileOutputStream fos) {


try {
if (fos != null) {
try {
fos.getFD().sync();
} catch (IOException e) {
Log.e(Constants.LOG_TAG,
"Błąd synchronizacji obiektu systemu plików " + e.getMessage(), e);
}
return true;
}
return false;
}

Nie zawsze pożądana jest natychmiastowa synchronizacja plików, ponieważ wiąże


się ona z pewnymi kosztami i nie jest konieczna, jeśli aplikacja zapisuje tylko jeden
plik w jednym procesie. Warto jednak wiedzieć, że można (i należy) stosować tę
technikę, kiedy trzeba zagwarantować natychmiastowy zapis danych do pliku.
OMÓWIENIE

Jeśli korzystasz w Androidzie z własnej pamięci na pliki, powinieneś pamiętać


o synchronizacji i systemie plików. Jeżeli natomiast korzystasz z innych interfej-
sów API Androida, na przykład bazy SQLite lub współużytkowanych ustawień
(klasa SharedPreferences), synchronizacja następuje automatycznie. Oba wymie-
nione mechanizmy omawiamy w dalszej części rozdziału.
Po przedstawieniu różnic między pamięcią wewnętrzną a zewnętrzną, poka-
zaniu podstawowych operacji wejścia-wyjścia, omówieniu pamięci podręcznej
i wyjaśnieniu synchronizacji pora przejść do następnego poziomu abstrakcji
w obszarze zarządzania pamięcią — do wspomnianej już klasy SharedPerformances.

7.2. Przechowywanie ustawień


Android udostępnia łatwą w użyciu klasę do przechowywania danych, SharedPre
´ferences, umożliwiającą odczyt i zapis prostych par klucz-wartość. Nie jest
tajemnicą, że platforma ułatwia zarządzanie plikami ze współużytkowanymi usta-
wieniami. Dzięki ustawieniom można zapisywać dane łatwiej i zwięźlej niż przy
użyciu zwykłych plików.
0 TECHNIKA 32. Odczyt i zapis ustawień 295

0 TECHNIKA 32. Odczyt i zapis ustawień

Klasa SharedPreferences umożliwia odczyt i zapis danych, a także określanie try-


bów dostępu do plików z ustawieniami. Oznacza to, że klasy tej można używać
do współużytkowania danych między poszczególnymi komponentami (aktyw-
nościami, usługami itd.), a nawet między różnymi programami, choć to ostatnie
zdarza się rzadko, ponieważ aplikacje muszą wtedy znać nazwy pakietów innych
programów i używać plików umożliwiających zapis dowolnym jednostkom lub
współużytkowanych identyfikatorów użytkownika, a z technik tych należy korzy-
stać tylko w wyjątkowych sytuacjach.
PROBLEM
Potrzebujemy łatwego sposobu na zapisywanie prostych informacji, na przykład
łańcuchów znaków lub wartości typów prostych.
ROZWIĄZANIE
Można użyć klasy SharedPreferences do łatwego zapisywania i pobierania danych.
Przykładowy kod pokazujemy na listingu 7.5.

Listing 7.5. Używanie klasy SharedPreferences do zapisu i odczytu danych

SharedPreferences prefs = getSharedPreferences("myPrefs",


Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("HW_KEY", "Witaj, świecie");
editor.commit();

// Dalej lub w innym komponencie.


String helloWorld = prefs.getString("HW_KEY", "Wartość domyślna");

Aby móc stosować ustawienia, najpierw należy pobrać (poprzez obiekt klasy
Context) referencję do obiektu klasy SharedPreferences, a następnie użyć obiektu
klasy Editor do zapisu danych i prostych metod get do ich odczytu.
OMÓWIENIE
Obiekty klasy SharedPreferences są przydatne i łatwe w użyciu. Możesz utwo-
rzyć własny obiekt tego typu, tak jak zrobiliśmy to na rysunku. Inna możliwość,
jeszcze prostsza, to użycie jednej z kilku metod pomocniczych dostępnych
w platformie. Domyślne ustawienia można pobrać z każdego komponentu za
pomocą wywołania PreferenceManager.getDefaultSharedPreferences(Context c).
Metoda ta zwraca obiekt z preferencjami na podstawie nazwy pakietu reprezen-
towanego przez kontekst. Można też użyć metody Activity.getPreferences(int
mode), która zwraca obiekt na podstawie nazwy klasy. Warto pamiętać, że na zaple-
czu obiektom klasy SharedPreferences odpowiadają pliki XML przechowywane
w katalogu /data/data/<nazwa_pakietu>/shared_prefs w wewnętrznym systemie
plików. Jeśli chcesz ręcznie zmodyfikować taki plik lub sprawdzić jego zawartość,
znajdziesz go w podanym miejscu.
296 ROZDZIAŁ 7. Lokalne zapisywanie danych

Klasa SharedPreferences obsługuje też odbiorniki. Można dołączyć odbiornik


OnSharedPreferenceChangeListner, który działa jak wywołanie zwrotne. Pozwala to
powiadamiać aplikację o zmianie ustawień. Działanie tego mechanizmu przedsta-
wiamy w bardziej rozbudowanym przykładzie, gdzie używamy też klasy Preference
´Activity.

0 TECHNIKA 33. Korzystanie z klasy PreferenceActivity

W Androidzie ustawienia służą nie tylko do zapisywania danych i współużytko-


wania ich między komponentami. Można też automatycznie powiązać je z usta-
wieniami użytkownika wyświetlanymi na ekranie. Do tego w Androidzie służy
specjalna klasa aktywności PreferenceActivity.
PROBLEM
Chcemy umożliwić użytkownikom określanie ustawień aplikacji i w łatwy sposób
utrwalać je w plikach typu SharedPreferences.
ROZWIĄZANIE
Prawdopodobnie widziałeś już działanie klasy PreferenceActivity. Używa się
jej dla głównego ekranu ustawień Androida, a także w wielu wbudowanych apli-
kacjach. Tu pokazujemy, jak działa ta klasa. Pozwoli Ci to ją zastosować. Zoba-
czysz też, jak zwiększyć użyteczność tej klasy przez wyświetlanie bieżących usta-
wień (a nie tylko ich opisu) i natychmiastowe odzwierciedlanie zmian.
Aby pokazać potrzebne mechanizmy, wzbogacamy projekt MyMovies z roz-
działu 4. Tworzymy nową wersję projektu, w której wprowadzamy istotne zmiany.
Dodajemy bazę danych i funkcję pobierania danych z sieci. Więcej o tych mecha-
nizmach dowiesz się już wkrótce. Najpierw jednak wyjaśnimy, jak dodać stronę
ustawień pozwalającą włączyć lub wyłączyć ekran powitalny.

POBIERZ PROJEKT MYMOVIESDATABASE. Kod źró-


dłowy projektu i pakiet APK do uruchamiania aplikacji znaj-
dziesz w witrynie z kodem do książki Android w praktyce.
Ponieważ niektóre listingi skrócono, abyś mógł skoncentro-
wać się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
Źródło: http://mng.bz/5M06, plik APK: http://mng.bz/03ta.
Na rysunku 7.6 przedstawiamy działanie aktywności PreferencesActivity z apli-
kacji MyMoviesDatabase. W gotowym projekcie widoczna opcja jest dostępna
w menu.
0 TECHNIKA 33. Korzystanie z klasy PreferenceActivity 297

Rysunek 7.6. Strona ustawień w aplikacji MyMoviesDatabase wyświetla informację


o tym, czy ekran powitalny ma być pokazywany, czy nie

Strona z ustawieniami jest oparta na dwóch komponentach. Jednym z nich jest


plik XML zasobu z ustawieniami, w którym zdefiniowane są elementy. Drugi to
sama aktywność PreferenceActivity. Uporządkowanie elementów jest takie samo
jak w standardowym zasobie układu lub zwykłej aktywności, przy czym jest
charakterystyczne dla ustawień. Zaczynamy od omówienia zasobu w formacie
XML, przedstawionego na listingu 7.6.

Listing 7.6. Plik zasobu preferences.xml z definicją hierarchii ustawień

<?xml version="1.0" encoding="UTF-8"?>


<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="Ustawienia aplikacji">
<CheckBoxPreference android:title="Ekran powitalny"
android:key="showsplash" android:summary="Disabled"
android:defaultValue="false" />
</PreferenceCategory>
</PreferenceScreen>

Każdy zasób ustawień w formacie XML rozpoczyna się od elementu głównego


PreferenceScreen . Dalej podawane są kategorie z nagłówkami i elementy
odpowiadające ustawieniom. Istnieje kilka typów wbudowanych obiektów usta-
wień, na przykład DialogPreference, ListPreference, EditTextPreference i uży-
wany tu CheckBoxPreference . Każdy obiekt ustawień składa się z nagłówka,
klucza, wartości i opisu. Opis najczęściej ma postać statycznego tekstu, na przy-
kład „Włącz lub wyłącz ekran powitalny”. Dalej pokazujemy, jak dynamicznie
zmieniać ten tekst i używać go do wyświetlania bieżącego ustawienia (tak jak na
rysunku 7.6). Drugim komponentem jest klasa PreferenceActivity. Przedstawiamy
ją na listingu 7.7.

Listing 7.7. Aktywność z pliku Preferences.java aplikacji MyMoviesDatabase

public class Preferences extends PreferenceActivity {

private CheckBoxPreference showSplash;

@Override
298 ROZDZIAŁ 7. Lokalne zapisywanie danych

public void onCreate(final Bundle savedInstanceState) {


super.onCreate(savedInstanceState);

addPreferencesFromResource(R.layout.preferences);

showSplash = (CheckBoxPreference)
getPreferenceScreen().findPreference("showsplash");

setCheckBoxSummary(showSplash);

SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(
new OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(
SharedPreferences prefs, String key) {
if (key.equals("showsplash")) {
setCheckBoxSummary(showSplash);
}
}
});
}

private void setCheckBoxSummary(CheckBoxPreference pref) {


if (pref.isChecked()) {
pref.setSummary("Włączony");
} else {
pref.setSummary("Wyłączony");
}
}
}

Najpierw wykorzystujemy pomoc ze strony Androida i tworzymy klasę pochodną


od klasy PreferenceActivity . Klasa ta wyświetla użytkownikowi hierarchiczną
listę obiektów klasy Preference i automatycznie zapisuje wybrane ustawienia do
pliku powiązanego z obiektem klasy SharedPreferences. W aktywności używane
są obiekty ustawień, na przykład klasy CheckBoxPreference , które wskazujemy
w pliku XML.
Tu używamy tylko jednego ustawienia, dlatego kod jest prosty. Jednak nie-
zależnie od liczby ustawień obowiązuje ten sam wzorzec. W aktywności należy
określić XML-ową hierarchię ustawień. Służy do tego metoda addPreferences
´FromResource . Aby pobrać referencję do konkretnego obiektu ustawień zade-
klarowanego w pliku XML, trzeba wywołać metodę findPreference . Działa
ona podobnie jak metoda findViewById, ale jest przeznaczona dla ustawień.
Po początkowych operacjach konfiguracyjnych aplikacja uzyskuje dostęp do
domyślnego obiektu klasy SharedPreferences , ponieważ to w nim aktywność
PreferenceActivity zapisuje dane. Do aplikacji dołączany jest też odbiornik
OnSharedPreferenceChangeListener . Przy każdej aktualizacji ustawień zgłaszane
jest zdarzenie obejmujące ustawienia i klucz ze zmodyfikowanej pary. W odbior-
niku należy sprawdzić, czy podany klucz jest ważny. Jeśli tak, aplikacja modyfi-
7.3. Korzystanie z bazy danych 299

kuje opis obiektu ustawienia, używając do tego wewnętrznej metody pomocniczej


setCheckBoxSummary . Jeżeli na stronie znajduje się wiele pól wyboru, można dla
każdego z nich zastosować tę samą metodę.
OMÓWIENIE
Za pomocą aktywności PreferenceActivity i powiązanego pliku klasy SharedPre
´ferences można nie tylko wyświetlać stronę ustawień, ale też automatycznie
ją aktualizować i pokazywać użytkownikowi aktualny stan lub wybrane ustawie-
nia. Choć tu ustawienie jest tylko jedno, użytkownicy docenią to, że domyślnie
ekran powitalny pojawia się tylko raz. Następnie użytkownicy mogą wybrać, czy
program ma go pokazywać; nie zmuszamy ich do oglądania ekranu powitalnego
przy każdym uruchomieniu aplikacji.
Znasz już kilka sposobów na dostęp do pliku klasy SharedPreferences i wiesz,
jak go używać. Ponadto rozpoczęliśmy rozwijanie przykładowej aplikacji
MyMoviesDatabase. Dalej (żeby uzasadnić człon „Database” w nazwie aplika-
cji) dołączamy bazę SQLite służącą do lokalnego przechowywania listy wybra-
nych filmów.

7.3. Korzystanie z bazy danych


Kiedy aplikacja ma przechowywać złożone dane, często warto zapisywać je w rela-
cyjnej bazie. Bazy mają umożliwiać na przykład wstawianie danych w ramach
transakcji i obsługę wielu połączeń. Większość programistów zna pewnego rodzaju
systemy zarządzania relacyjnymi bazami danych (ang. relation database mana-
gement system — RDBMS). Korzystanie z takich systemów to często stosowany
i rozbudowany sposób na porządkowanie oraz przechowywanie danych. Systemy
te działają zgodnie z zasadami algebry relacji.
Aby przedstawić korzystanie z baz danych w aplikacji na Android, modyfi-
kujemy program MyMovies z wcześniejszych rozdziałów. W nowej wersji ma
on obejmować lokalną bazę do przechowywania danych na temat filmów (baza ta
zastępuje zwykły tekstowy plik zasobu) i umożliwiać użytkownikom wyszukiwa-
nie filmów dodawanych do kolekcji. Dane o filmach pochodzą z usługi sieciowej
i są umieszczane w kilku tabelach bazy danych. W tej wersji aplikacji MyMovies
koncentrujemy się na zagadnieniach związanych z bazą, jednak zainteresowani
Czytelnicy w pakiecie z projektem znajdą kod do obsługi komunikacji siecio-
wej i przetwarzania plików XML. Zagadnienia te opisujemy w rozdziale 9. Na
rysunku 7.7 widoczna jest gotowa aplikacja MyMoviesDatabase.
Aby w aplikacji MyMoviesDatabase umożliwić zapis lokalnych danych o filmie,
tworzymy architekturę warstwową, pozwalającą komponentom aplikacji (tu są
nimi aktywności) na łatwe zapisywanie zwykłych obiektów Javy w bazie i pobie-
ranie ich z niej. Dokładnie omawiamy każdy poziom aplikacji, najpierw jednak
przedstawiamy dostępny w Androidzie system zarządzania bazami danych, który
odpowiada za zapis informacji. Jest to system SQLite.
300 ROZDZIAŁ 7. Lokalne zapisywanie danych

Rysunek 7.7. Aplikacja


MyMoviesDatabase wyświetla
strony z listą filmów, wyszukiwarką
i szczegółowymi informacjami
o filmach

Czym jest SQLite? Większość systemów zarządzania bazami danych to duże


aplikacje serwerowe. Na przykład wiele aplikacji sieciowych korzysta po stronie
serwera z licznych serwerów i klastrów baz danych. Android może uzyskać dostęp
do takich systemów przez sieć, jednak (jak wspomnieliśmy w rozdziale 1.) udo-
stępnia też małą osadzaną bazę danych o otwartym dostępie do kodu źródło-
wego — SQLite. Programiści często używają jej w aplikacjach do zarządzania
lokalnymi danymi. SQLite działa w takich programach, jak Apple OS X, Dropbox,
Firefox i Chrome, a także w wielu innych aplikacjach oraz produktach.
W SQLite wykorzystywany jest (jak wskazuje nazwa) język SQL (ang.
Structured Query Language), co umożliwia tworzenie i przechowywanie tabel,
a także wstawianie i pobieranie danych. Choć SQLite obsługuje SQL-a, bazy
tej nie należy traktować jak zastępnika bardziej rozbudowanych serwerów firm
Oracle, Microsoft, IBM itd. Baza SQLite ma być mała, szybka i łatwa w użyciu
do zarządzania danymi w procesach. Jeśli już korzystałeś z SQL-a, większość roz-
wiązań z SQLite powinna być Ci znana. Jeżeli nie znasz tego języka, to też dobrze,
ponieważ SQLite pozwala rozpocząć naukę SQL-a bez komplikacji związanych
z większymi systemami.
Choć baza SQLite jest mała i szybka, daje duże możliwości. Obsługuje trans-
akcje (są one atomowe nawet w obliczu awarii systemu), klucze obce, funkcje,
wyzwalacze i inne mechanizmy. Więcej o tych mechanizmach dowiesz się, kiedy
będziemy korzystać z nich w aplikacji MyMoviesDatabase. Ponadto choć SQLite
udostępnia wiele rozwiązań z innych systemów opartych na SQL-u, niektóre
mechanizmy są tu niedostępne. SQLite nie obsługuje niektórych rodzajów złą-
czeń (RIGHT OUTER i FULL OUTER) i pewnych instrukcji ALTER, a także traktuje typy
danych swobodniej niż inne systemy. Nie przedstawiamy tu wszystkich aspektów
bazy SQLite, mamy jednak nadzieję, że omówienie pomoże Ci zacząć pracę
7.3. Korzystanie z bazy danych 301

i zapewni informacje o najczęściej stosowanych wzorcach, których prawdopodob-


nie będziesz potrzebował przy tworzeniu aplikacji na Android. Więcej informacji
na temat bazy SQLite znajdziesz w doskonałej dokumentacji internetowej pod
adresem http://www.sqlite.org/.
Aby móc używać bazy SQLite, najpierw trzeba zdefiniować przechowywane
dane, następnie utworzyć bazę, a w ostatnim kroku zbudować kilka warstw, aby
ukryć skomplikowane szczegóły i umożliwić łatwe utrwalanie danych w kompo-
nentach aplikacji. Zaczynamy od przeglądu dostępnych w Androidzie pakietów
związanych z bazami danych.

7.3.1. Pakiety Androida związane z danymi


Android udostępnia dwa podstawowe pakiety do korzystania z baz danych. Pierw-
szy z nich, android.database, nie jest przeznaczony dla żadnego konkretnego
rodzaju baz. W tym pakiecie znajdziesz interfejs Cursor, podstawowe implemen-
tacje, kilka typów danych i obserwatorów treści, a także klasy pomocnicze. Drugi
pakiet, android.database.sqlite, jest przeznaczony dla baz SQLite. Tu znajduje
się implementacja kursora, klasy do tworzenia i aktualizowania baz SQLite, klasy
do kierowania zapytań o dane itd. W tabeli 7.1 przedstawiamy w ogólnym ujęciu
oba pakiety. Pełne informacje znajdziesz w dokumentacji interfejsu API.
Tabela 7.1. Przegląd pakietów Androida związanych z bazami danych i wybranych
najważniejszych klas

Pakiet Klasa Opis


android.database Cursor lub Klasa Cursor zapewnia bezpośredni
AbstractCursor dostęp do zbioru wyników w trybie
do odczytu i zapisu.
AbstractCursor to klasa bazowa
z implementacją.
DatabaseUtils Obejmuje wiele metod narzędziowych
do tworzenia łańcuchów znaków
zapytań z odpowiednimi znakami
ucieczki, do korzystania z kursorów
i do wykonywania prostych, często
stosowanych zapytań.
android.database.sqlite SQLiteCursor Implementacja kursora do obsługi
wyników z baz typu SQLiteDatabase.
SQLiteDatabase Nakładka udostępniająca metody bazy
SQLite, służące między innymi
do otwierania i zamykania połączeń oraz
wykonywania zapytań i instrukcji.
SQLiteOpenHelper Klasa pomocnicza do tworzenia
i aktualizowania baz danych oraz
zarządzania wersjami schematu.
SQLiteQueryBuilder Klasa pomocnicza do tworzenia zapytań
do baz SQLite.
SQLiteStatement Typ wstępnie kompilowanych instrukcji
SQL-a używany w bazach SQLite.
302 ROZDZIAŁ 7. Lokalne zapisywanie danych

Jeśli nigdy wcześniej nie stosowałeś kursorów lub nie znasz tego pojęcia, nie
martw się — to proste zagadnienie. Obiekty klasy Cursor umożliwiają poruszanie
się po zbiorach wyników z bazy danych. Można ująć to tak: kursory przechodzą
po zbiorach wyników i zapewniają dostęp do jednego wiersza danych naraz.
Działanie tego mechanizmu przedstawiamy w kontekście przykładowego kodu.
Ponadto omawiamy inne najważniejsze klasy związane z tworzenie i stosowaniem
baz SQLite w Androidzie.
Nie zamierzamy jednak opisywać w tym rozdziale wszystkich klas z interfejsów
API związanych z danymi. Nie planujemy też wyjaśniać podstaw SQL-a. Koncen-
trujemy się na ogólnym ujęciu i głównych klasach z Androida. Dalej dokładniej
opisujemy interfejsy API, zaczynamy jednak od ogólnego schematu, wzorca, który
posłuży do utworzenia warstwy dostępu do danych.

7.3.2. Tworzenie warstwy dostępu do danych


W kilku następnych punktach definiujemy potrzebne tabele i tworzymy bazę
danych. Dalej używamy SQL-a do wstawiania, aktualizowania, pobierania i usu-
wania danych. Wcześniej jednak należy zrobić krok wstecz i zastanowić się nad
architekturą oraz projektem.
Nie chcemy tworzyć zbyt skomplikowanej architektury. Nie planujemy
w końcu misji na Marsa — tworzymy prostą warstwę dostępu do danych osadzoną
w aplikacji. Zamierzamy jednak ukryć wszystkie szczegóły, co pozwoli uniknąć
stosowania w komponentach aplikacji SQL-a i mechanizmów utrwalania danych.
Chcemy używać prostych obiektów Javy (nazywamy je obiektami modelu) i pro-
stego interfejsu do zapisywania oraz pobierania tych obiektów. Jeśli stosowałeś
wcześniej wzorzec DAO (ang. Data Access Object), opis ten powinien brzmieć
znajomo. W aplikacji tworzymy obiekty DAO dla obiektów modelu, a także war-
stwę menedżera danych, który jest nakładką na obiekty DAO i pozwala umieścić
cały kod do obsługi danych w jednym miejscu.
Tworzymy też instrukcje w SQL-u, jednak robimy to w obiektach DAO, dzięki
czemu obiekty te wykonują tylko konkretne operacje. Na rysunku 7.8 przedsta-
wiamy ostateczny kształt warstwowej architektury.
PO STRONIE SERWERA, ALE BEZ PRZESADY. Niesławny szef Dil-
berta z komiksu Scotta Adamsa kazał kiedyś swojemu podwładnemu spra-
wić, aby witryna była „bardziej internetowa, ale bez przesady” (http://search.
dilbert.com/comic/Webbish). W kilku następnych punktach przedstawiamy
zasady dostępu do danych, które są przydatne w Androidzie, ponieważ
pozwalają rozdzielić zadania wykonywane w poszczególnych plikach i two-
rzyć określony kod. Należy jednak pamiętać, że nie jest to jedyny sposób
korzystania z baz w Androidzie. Nie chcemy tu powielać wzorców używa-
nych po stronie serwera i podobnych rozwiązań. Należy pamiętać, że nie
używamy tu kodu działającego po stronie serwera, ale małej, osadzonej
bazy danych.
0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu 303

Rysunek 7.8.
Schemat
najważniejszych
części warstwy
dostępu do danych.
Obejmuje elementy
od klasy
SQLiteOpenHelper,
przez inne
komponenty,
po samą bazę
danych

Zaczynamy od zaprojektowania tabel, następnie tworzymy obiekty modelu odpo-


wiadające tym tabelom, a później przygotowujemy obiekty DAO. W ostatnim
kroku umieszczamy obiekty DAO w implementacji interfejsu menedżera danych,
który ma odpowiadać za zapisywanie i pobieranie informacji.

0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu

Zanim w aplikacji na Android będzie można zacząć używać bazy danych, trzeba
ją utworzyć. A przed zbudowaniem bazy danych należy ustalić, co ma się w niej
znajdować i jakie są relacje między jej elementami. Potrzebne są definicje tabel
i obiekty modelu, które można wykorzystać w kodzie aplikacji. Dalej tworzymy
kilka klas pomocniczych z potrzebnymi instrukcjami w SQL-u.
PROBLEM
Chcemy utworzyć bazę i obiekty modelu przeznaczone do zapisywania oraz
pobierania danych. Definicje tabel mają być odrębne od siebie i od podstawowego
kodu do tworzenia bazy danych. Jest to przydatne, ponieważ pomaga skoncen-
trować się w każdej klasie na konkretnym zadaniu. Dzięki temu kod związany
z bazą danych można łatwiej zrozumieć i konserwować.
ROZWIĄZANIE
Android zapewnia wiele wygodnych mechanizmów w zakresie tworzenia i uży-
wania baz danych. Kilka dodatkowych własnych klas pozwala jeszcze bardziej
ułatwić pracę. Tu zaczynamy od opracowania schematu tabel, które mają znaleźć
się w bazie. Potem tworzymy obiekty modelu używane do zapisywania i pobie-
rania danych przechowywanych w tych tabelach. Następnie dla każdej tabeli
tworzymy odrębną klasę. W klasach tych ma znajdować się kod potrzebny do
304 ROZDZIAŁ 7. Lokalne zapisywanie danych

tworzenia i aktualizowania schematu bazy danych. Klasy te wykorzystujemy


w implementacji klasy SQLiteOpenHelper (jest to dostępna w Androidzie klasa bazowa
do tworzenia i aktualizowania baz oraz do uzyskiwania dostępu do danych).
Po zdefiniowaniu tabel, utworzeniu obiektów modelu i umożliwieniu genero-
wania bazy danych za pomocą klasy SQLiteOpenHelper można zdefiniować obiekty
DAO i interfejs menedżera danych. Zaczynamy od zdefiniowania potrzebnych
tabel.
Tabele
Do przedstawienia potrzebnych tabel używamy diagramu encja-związek (ang.
entity relationship diagram). W aplikacji MyMoviesDatabase znajdują się tylko
trzy tabele, dlatego diagram jest mały. Pomaga jednak zwizualizować tabele. Dia-
gram ten pokazany jest na rysunku 7.9.

Rysunek 7.9. Diagram encja-związek z tabelami bazy danych aplikacji


MyMoviesDatabase

Trzy użyte tu tabele to Movie, MovieCategory i Category. Tabele Movie i Category


obejmują unikatowe identyfikatory. Identyfikatory te znajdują się w kolumnie _id,
która w Androidzie ma określone znaczenie. Jeśli chcesz współużytkować dane
w różnych aplikacjach na Android (tak działa wbudowana baza danych z kon-
taktami), musisz utworzyć dostawcę treści, czyli obiekt klasy ContentProvider.
Więcej o dostawcach treści dowiesz się z następnego rozdziału. Na razie warto
zapamiętać, że jeśli chcesz użyć dostawcy treści do udostępniania tabel, muszą one
mieć kolumnę _id. Znajdują się w niej klucze główne (unikatowe identyfikatory
elementów tabeli).
KIEDY NALEŻY UŻYWAĆ DOSTAWCY TREŚCI? Dostawcę treści
możesz też zastosować w aplikacji do uzyskania dostępu do lokalnej bazy
danych. W tym kontekście pojawia się pytanie, kiedy należy bezpośrednio
używać lokalnej bazy danych, a kiedy warto dołożyć dodatkowych starań
i utworzyć dostawcę treści. Niestety, nie da się udzielić jednoznacznej
odpowiedzi. Dostawcy treści mają wygodne cechy, jednak korzystanie
z nich jest bardziej skomplikowane niż bezpośrednie używanie lokalnej
0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu 305

bazy danych. Ogólnie jeśli chcesz udostępniać dane innym aplikacjom,


musisz utworzyć dostawcę treści. Jeżeli nie potrzebujesz współużytkować
danych, zwykle prościej jest bezpośrednio korzystać z bazy.
Tabela Movie ma atrybuty homepage, name, rating itd. Wszystkie te atrybuty
umożliwiają sortowanie i wyświetlanie filmów. Tabela Category jest jeszcze prost-
sza od tabeli Movie — obejmuje pola _id i name. Aplikacja ma wyświetlać kategorie
filmów na stronie ze szczegółowymi informacjami. Kategorie można wykorzystać
do sortowania danych i wykonywania podobnych operacji. Tabela MovieCategory
różni się od pozostałych. Nie służy bezpośrednio do wyświetlania danych, pełni
natomiast funkcję tabeli wiążącej. Umożliwia przedstawianie potrzebnych związ-
ków wiele do wielu — jeden film może należeć do wielu kategorii, a jednak
kategoria może obejmować liczne filmy. Nie trzeba powtarzać nazw kategorii we
wszystkich miejscach ich występowania, ponieważ dane są znormalizowane.
Obiekty modelu
Oprócz tabel tworzymy obiekty modelu (w sposób podobny jak w technologii
JavaBeans). Służą one do reprezentowania encji danych. Obiekty modelu to
obiekty klas, których aktywności i inny kod używają do zapisywania, pobierania
i wyświetlania danych na temat filmów. Obiekty modelu nie odpowiadają dokład-
nie tabelom, ale są ich przybliżonymi odpowiednikami. W aplikacji znajdują się
obiekty dla tabel Movie i Category. Obiekt modelu Movie przedstawiamy na
listingu 7.8.

Listing 7.8. Obiekt modelu Movie (zbliżony do obiektów z technologii JavaBeans)

public class Movie extends ModelBase {

private String providerId;


private String name;
private int year;
private double rating;
private String url;
private String homepage;
private String trailer;
private String tagline;
private String thumbUrl;
private String imageUrl;
private Set<Category> categories;

// Konstruktor, metody do ustawiania i pobierania wartości oraz


// metody equals i hashCode ze względu na zwięzłość pominięto.
}

Różnica między tabelami bazy danych a obiektami modelu polega na związkach


między filmami a kategoriami. Na listingu 7.8 klasa Movie ma składową w postaci
kolekcji obiektów klasy Category. Ponadto nie istnieje klasa MovieCategory. Nato-
miast tabele są niezależne od siebie. Problem różnicy między reprezentacją
związku w bazie danych a jego reprezentacją w kodzie w Javie rozwiązujemy
306 ROZDZIAŁ 7. Lokalne zapisywanie danych

w instrukcjach w SQL-u. Klasa Category jest podobna do klasy Movie. Jest to ziarno,
jednak obejmuje tylko jedną właściwość (name typu String). Klasa ModelBase, po
której dziedziczą klasy Movie i Category, obejmuje tylko właściwość id typu long.
Klasa SQLiteOpenHelper
Teraz, kiedy wiemy już, jakie dane chcemy zapisywać, potrzebujemy sposobu na
poinformowanie Androida, że ma utworzyć tabele w momencie uruchamiania apli-
kacji. Potrzebne informacje znajdują się w pokazanej na listingu 7.9 klasie pochod-
nej od klasy SQLiteOpenHelper.

Listing 7.9. Klasa pochodna od klasy SQLiteOpenHelper służąca do tworzenia


i aktualizowania baz danych

public class OpenHelper extends SQLiteOpenHelper {

private Context context;

OpenHelper(final Context context) {


super(context, DataConstants.DATABASE_NAME, null,
DataManager.DATABASE_VERSION);
this.context = context;
}

@Override
public void onOpen(final SQLiteDatabase db) {
super.onOpen(db);
}

@Override
public void onCreate(final SQLiteDatabase db) {
CategoryTable.onCreate(db);
CategoryDao categoryDao = new CategoryDao(db);
String[] categories =
context.getResources().getStringArray(
R.array.tmdb_categories);
for (String cat : categories) {
categoryDao.save(new Category(0, cat));
}

MovieTable.onCreate(db);

MovieCategoryTable.onCreate(db);
}

@Override
public void onUpgrade(final SQLiteDatabase db,
final int oldVersion, final int newVersion) {

MovieCategoryTable.onUpgrade(db, oldVersion, newVersion);

MovieTable.onUpgrade(db, oldVersion, newVersion);

CategoryTable.onUpgrade(db, oldVersion, newVersion);


}
}
0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu 307

Android udostępnia klasę SQLiteOpenHelper do konfigurowania baz danych i otwie-


rania połączeń. Aby móc używać lokalnej bazy danych, zaczynamy od utworzenia
klasy pochodnej od wspomnianej klasy . W konstruktorze podajemy nazwę
i wersję bazy danych oraz wywołujemy konstruktor klasy nadrzędnej.
Dalej znajduje się implementacja metod cyklu życia udostępnianych przez
klasę OpenHelper. Te metody to onOpen (jej przesłanianie jest opcjonalne; dla
porządku przedstawiamy ją w kodzie, choć nie wykonuje ona żadnych wyjątko-
wych operacji), onCreate i onUpgrade . Framework wywołuje te metody, kiedy
są potrzebne. Standardowo zwracane jest połączenie. Jeśli baza danych jeszcze
nie istnieje, aplikacja ją tworzy, a jeśli numer nowej wersji jest wyższy od numeru
właśnie używanej bazy, następuje aktualizacja.
W metodzie onCreate stosujemy wzorzec przydatny w pracy z lokalnymi bazami
danych Androida. Używamy klas przeznaczonych dla poszczególnych tabel. Kod
tych klas znajdziesz w kilku następnych listingach. Ważne jest to, aby oddzielić
kod służący do definiowania, tworzenia i aktualizowania każdej tabeli od kodu
klasy OpenHelper. Nie jest to konieczne, jednak dzięki temu klasa OpenHelper nie
jest duża i skomplikowana. Ponadto tabele można w razie potrzeby wykorzystać
w innych projektach. Każdy obiekt tabeli udostępnia metody statyczne onCreate
i onUpgrade wywoływane w metodach klasy OpenHelper.
Zapełnianie bazy predefiniowanymi danymi
W metodzie onCreate z listingu 7.9 konfigurujemy bazę danych, wczytujemy
plik zasobu R.array.tmdb_categories i używamy obiektu CategoryDao do zapisania
danych w bazie . Dzięki temu w bazie bezpośrednio po jej utworzeniu zapi-
sywane są początkowe kategorie. W niektórych bazach tego typu wstępne dane
są niezbędne. Zwykle za pomocą tego podejścia wczytywane są państwa, stany,
role, kategorie itd. Jeśli danych jest niewiele, technika ta sprawdza się dobrze.
Jeżeli jednak w aplikacji trzeba umieścić dużą ilość informacji, opisane podejście
jest zbyt wolne (ponieważ wymaga wywołania instrukcji INSERT dla każdego
elementu z danych). Dlatego wtedy lepiej jest wstępnie utworzyć plik bazy danych
SQLite (każda taka baza jest przechowywana w jednym pliku) i udostępniać go
jako materiały (ang. asset) wraz z aplikacją. Następnie możesz zastąpić tym plikiem
bazę potrzebną w aplikacji (zrób to tylko raz!). W czasie wykonywania programu
bazy aplikacji są przechowywane w katalogu /data/data/<nazwa_pakietu>
/databases/ w pamięci podręcznej.
SQLiteDatabase
Po przygotowaniu obiektu klasy SQLiteOpenHelper można użyć go w dowolnym
miejscu aplikacji do utworzenia obiektu klasy SQLiteDatabase. Obiekt SQLiteData
´base to najważniejszy komponent w kontekście pracy baz danych SQLite
w Androidzie. To w tym obiekcie należy nawiązywać połączenia oraz wykonywać
operacje na danych (pobieranie, aktualizowanie, wstawianie i usuwanie).
308 ROZDZIAŁ 7. Lokalne zapisywanie danych

Działanie wspomnianego obiektu pokazujemy w technice 35., gdzie omawiamy


interfejs DataManager i klasę z jego implementacją, używaną przez aplikację jako
nakładka na wszystkie metody manipulujące danymi. Na razie przyjrzyj się przy-
kładowi zastosowania obiektu klasy OpenHelper do pobrania referencji do obiektu
klasy SQLiteDatabase:
SQLiteOpenHelper openHelper = new OpenHelper(this.context);
SQLiteDatabase db = openHelper.getWritableDatabase();

Metoda getWritableDatabase klasy SQLiteOpenHelper w pierwszym wywołaniu


uruchamia metodę onCreate, a w późniejszych — metodę onOpen. W ten sposób
aplikacja uruchamia metody pomocnicze i rozpoczyna „łańcuch reakcji”. Metodę
getWritableDatabase można wywołać dowolną liczbę razy (obiekt bazy jest prze-
chowywany w pamięci podręcznej), jednak koniecznie wywołaj metodę close po
zakończeniu korzystania z obiektu bazy.
Klasy reprezentujące tabele
Implementacja klasy OpenHelper pozwala zacząć pracę, jednak ponieważ w klasie
tej wywoływane są metody klas reprezentujących tabele, nadal nie wiesz, jak
wykonywać operacje na bazie danych. Tu przedstawiamy bardziej szczegółowe
informacje. Zaczynamy od klasy MovieTable, którą znajdziesz na listingu 7.10.

Listing 7.10. Klasa MovieTable z metodami statycznymi i klasą wewnętrzną


MovieColumns

public final class MovieTable {

public static final String TABLE_NAME = "movie";

public static class MovieColumns implements BaseColumns {


public static final String HOMEPAGE = "homepage";
public static final String NAME = "movie_name";
public static final String RATING = "rating";
public static final String TAGLINE = "tagline";
public static final String THUMB_URL = "thumb_url";
public static final String IMAGE_URL = "image_url";
public static final String TRAILER = "trailer";
public static final String URL = "url";
public static final String YEAR = "year";
}

public static void onCreate(SQLiteDatabase db) {


StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE " + MovieTable.TABLE_NAME + " (");
sb.append(BaseColumns._ID + " INTEGER PRIMARY KEY, ");
sb.append(MovieColumns.HOMEPAGE + " TEXT, ");
// Nazwy filmów nie są niepowtarzalne, jednak dla uproszczenia
// stosujemy ograniczenie niepowtarzalności.
sb.append(MovieColumns.NAME + " TEXT UNIQUE NOT NULL, ");
sb.append(MovieColumns.RATING + " INTEGER, ");
sb.append(MovieColumns.TAGLINE + " TEXT, ");
sb.append(MovieColumns.THUMB_URL + " TEXT, ");
sb.append(MovieColumns.IMAGE_URL + " TEXT, ");
0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu 309

sb.append(MovieColumns.TRAILER + " TEXT, ");


sb.append(MovieColumns.URL + " TEXT, ");
sb.append(MovieColumns.YEAR + " INTEGER");
sb.append(");");
db.execSQL(sb.toString());
}

public static void onUpgrade(SQLiteDatabase db,


int oldVersion,
int newVersion) {
db.execSQL("DROP TABLE IF EXISTS "
+ MovieTable.TABLE_NAME);
MovieTable.onCreate(db);
}
}

W każdej klasie reprezentującej tabelę zaczynamy od zdefiniowania stałej, w której


zapisujemy nazwę tabeli . Następnie tworzymy zagnieżdżoną klasę wewnętrzną
z implementacją interfejsu BaseColumns. W stałych w tej klasie zdefiniowane są
nazwy kolumn . BaseColumns to androidowy interfejs, w którym definiowana jest
wspomniana wcześniej kolumna _id. Po nazwach znajdują się statyczne metody
onCreate i onUpgrade , w których używamy poleceń SQL-a służących do two-
rzenia i (lub) aktualizowania tabel . Warto zauważyć, że obecna wersja metody
onUpgrade usuwa tabelę, a następnie ją odtwarza . W wersji produkcyjnej nie
jest to odpowiednie podejście. Trzeba zrobić coś więcej, na przykład najpierw
pobrać aktualne dane, potem zaktualizować schemat, a następnie w razie potrzeby
ponownie wstawić informacje.
Polecenie CREATE jest dość oczywiste. Pamiętaj jednak, że nie dla każdej bazy
danych można go użyć; polecenie to jest przeznaczone dla systemu SQLite.
W poleceniu CREATE aplikacja określa typ danych (na przykład TEXT lub INTEGER)
każdej kolumny. Użyliśmy też kilku ograniczeń, na przykład UNIQUE i NOT NULL.
Ograniczenia są zrozumiałe, jednak wymóg podawania niepowtarzalnych tytu-
łów to nadmierne uproszczenie. W praktyce tytuły filmów mogą się powtarzać.
W bazie moglibyśmy zapisać różne filmy o tym samym tytule, podając odmienne
identyfikatory (pełniące funkcję klucza głównego). To jednak skomplikowałoby
przykład. Aby zachować prostotę, zdecydowaliśmy się na obsługę tylko niepowta-
rzalnych tytułów.
„DYNAMICZNE” TYPY DANYCH W SQLITE. Cechą odróżniającą
SQLite od wielu innych systemów zarządzania bazami jest używanie dyna-
micznych typów danych. Oznacza to, że można zadeklarować kolumnę typu
TEXT i zapisać w niej liczbę. Można też umieścić tekst w kolumnie typu
INTEGER. Wynika to z tego, że bazy SQLite korzystają ze spokrewnionych
z konkretnymi typami klas pamięci (ang. storage classes), ale przekształcają
otrzymane dane w możliwie najlepszy sposób. Jeśli nie znasz tego
310 ROZDZIAŁ 7. Lokalne zapisywanie danych

mechanizmu, może on wydawać się mylący. Model ten wpływa też na


przebieg sortowania i działanie operatorów. Pełne informacje znaj-
dziesz w dokumentacji typów danych baz SQLite: http://www.sqlite.org/
datatype3.html.
W innych klasach tabel stosujemy dokładnie ten sam wzorzec. Klasa CategoryTable
jest prosta (dlatego pomijamy ją w tym miejscu). Obejmuje jedynie identyfikator
i niepowtarzalną nazwę kategorii. Klasa MovieCategoryTable jest bardziej skom-
plikowana, ponieważ znajdują się w niej referencje do klucza obcego. Klasę tę
przedstawiamy na listingu 7.11.

Listing 7.11. Klasa MovieCategoryTable z deklaracją referencji do klucza obcego

public final class MovieCategoryTable {

public static final String TABLE_NAME = "movie_category";

public static class MovieCategoryColumns {


public static final String MOVIE_ID = "movie_id";
public static final String CATEGORY_ID = "category_id";
}

public static void onCreate(SQLiteDatabase db) {


StringBuilder sb = new StringBuilder();

sb.append("CREATE TABLE " + MovieCategoryTable.TABLE_NAME + " (");


;
sb.append(MovieCategoryColumns.MOVIE_ID + " INTEGER NOT NULL, ");
sb.append(MovieCategoryColumns.CATEGORY_ID + " INTEGER NOT NULL, ");
sb.append("FOREIGN KEY(" + MovieCategoryColumns.MOVIE_ID + ")
REFERENCES " + MovieTable.TABLE_NAME + "("
+ BaseColumns._ID + "), ");
sb.append("FOREIGN KEY(" +
MovieCategoryColumns.CATEGORY_ID + ")
REFERENCES " + CategoryTable.TABLE_NAME + "("
+ BaseColumns._ID + ") , ");
sb.append("PRIMARY KEY ( " + MovieCategoryColumns.MOVIE_ID + ", "
+ MovieCategoryColumns.CATEGORY_ID + ")");
sb.append(");");
db.execSQL(sb.toString());
}

public static void onUpgrade(SQLiteDatabase db, int oldVersion,


int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + MovieCategoryTable.TABLE_NAME);
MovieCategoryTable.onCreate(db);
}
}

Klasa MovieCategoryTable zaczyna się w taki sam sposób, jak inne klasy tabel —
od deklaracji stałej na nazwę tabeli. Dalej znajduje się statyczna klasa zagnież-
dżona reprezentująca kolumny (także zapisane w stałych). Różnica polega na tym,
że tu w klasie z nazwami kolumn nie implementujemy interfejsu BaseColumns .
0 TECHNIKA 34. Tworzenie bazy danych i obiektów modelu 311

Wynika to z tego, że ta tabela nie obejmuje klucza _id i nie jest udostępniana
przez dostawcę treści (ponieważ jest wewnętrzną tabelą odwzorowania i nie repre-
zentuje encji danych).
Następnym ważnym aspektem klasy MovieCategoryTable jest to, że obejmuje
odwzorowanie FOREIGN_KEY z referencjami (REFERENCES) do innych tabel . W tej
tabeli wiążącej znajdują się kolumny MOVIE_ID i CATEGORY_ID prowadzące do tabel
Movie i Category. Dlaczego stosujemy tę technikę? Po co używamy kluczy obcych?
W celu zachowania integralności referencyjnej. Klucze obce pozwalają zagwa-
rantować, że związki między tabelami są sensowne. Nie można na przykład
usunąć filmu i pozostawić powiązanej z nim referencji do kategorii . Można
pominąć klucze obce i samodzielnie sprawdzać związki, jednak łatwiej jest wyko-
rzystać mechanizmy udostępniane przez bazę danych i szybko zgłaszać błędy po
napotkaniu nieoczekiwanego warunku.

Obsługa kluczy obcych w bazach SQLite


Warto zauważyć, że nie wszystkie wersje baz SQLite wymuszają przestrzeganie
ograniczeń związanych z kluczami obcymi. W wersjach 1.5, 1.6 i 2.1 Androida
działa baza SQLite 3.5.9, która przetwarza instrukcje z ograniczeniami opartymi
na kluczach obcych, ale nie wymusza przestrzegania takich ograniczeń. Instrukcje
z wykorzystaniem kluczy obcych można stosować w każdej wersji Androida, należy
jednak pamiętać, że nie zawsze są one uwzględniane. Brak wymuszania prze-
strzegania ograniczeń często nie ma negatywnych skutków, jeśli nie używasz ogra-
niczeń w instrukcjach warunkowych, do kaskadowego usuwania danych i w podo-
bnych operacjach. Jeżeli chcesz zagwarantować przestrzeganie ograniczeń,
w momencie tworzenia bazy możesz sprawdzać poziom obsługi kluczy obcych i jeśli
nie jest on odpowiedni, stosować wyzwalacze.

OMÓWIENIE
Wiemy już, co chcemy zapisywać i jakie są związki między tabelami. Utworzyli-
śmy obiekty modelu pozwalające manipulować danymi w kodzie w Javie, a także
obiekty tabel, co pozwala przechowywać kod poszczególnych tabel w odręb-
nych klasach. Przygotowaliśmy też implementację klasy SQLiteOpenHelper. Klasa
ta służy do tworzenia i aktualizowania bazy, a także udostępnia referencje do
obiektów klasy SQLiteDatabase, pozwalających zapisywać i pobierać dane.
Większość dotychczasowego kodu jest standardowa. Obiekty modelu są typo-
wym sposobem reprezentowania danych (a choć tu obiekty te są celowo uprosz-
czone, mogą też obejmować kod operacji), a klasa SQLiteOpenHelper jest wymagana
przez Android. Jedyne niestandardowe lub opcjonalne rozwiązanie to użycie
odrębnych obiektów tabel. Utworzyliśmy je samodzielnie i uważamy, że dzięki
nim kod jest przejrzysty i konkretny.
Po utworzeniu klasy OpenHelper i klas tabel otrzymujemy gotową bazy danych.
Co dalej? Potrzebny jest sposób na zapisywanie i pobieranie informacji. Do
tego potrzebne są klasa pomocnicza DataManager i kilkukrotnie wspomniane
obiekty DAO.
312 ROZDZIAŁ 7. Lokalne zapisywanie danych

0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych

Android zapewnia kilka sposobów dostępu do danych z SQL-owych baz. Obiekt


SQLiteDatabase zapewnia niskopoziomowe metody, takie jak execSql(String sql),
różne wysokopoziomowe metody do pobierania i wstawiania danych, dostęp do
obiektu SQLiteStatement ze skompilowanymi SQL-owymi instrukcjami, metody
do określania granic transakcji itd. Tu nie chcemy jednak zaśmiecać głównego
kodu aplikacji niskopoziomowymi operacjami na bazie danych. Jeśli to możliwe,
nie chcemy nawet tego, aby w głównym kodzie znajdowały się informacje o tym,
że mechanizmem utrwalania danych jest baza.
PROBLEM
Chcemy utworzyć prosty interfejs API, z którego aplikacja może korzystać do
zapisywania i pobierania danych. Nie chcemy umieszczać SQL-owych instruk-
cji i innych operacji na bazie danych obok kodu zadań wykonywanych przez
aplikację.
ROZWIĄZANIE
Przez utworzenie obiektów DAO ukrywających SQL-owe instrukcje dla każdej
tabeli i opracowanie rozbudowanej warstwy menedżera danych (której kompo-
nenty aplikacji mogą używać, aby uzyskać dostęp do informacji) możemy oddzie-
lić kod programu od szczegółów związanych z utrwalaniem danych. Pozwala to
uniknąć powtórzeń i sprawić, że kod będzie wykonywał konkretne zadania. Naj-
pierw tworzymy obiekt DAO dla każdej tabeli, a następnie definiujemy warstwę
menedżera danych.
OBIEKTY DAO
Wielu programistów zna wzorzec tworzenia obiektów DAO. Reprezentują one
różne poziomy interakcji z tabelami i bazą. Obiekty DAO można tworzyć w różny
sposób. Tu używamy po jednym takim obiekcie na tabelę. Każdy obiekt DAO
obsługuje tu tylko swoją tabelę (bez uwzględniania związków między tabelami).
Dzięki temu obiekty te są dobrze zdefiniowane i zapewniają interfejs do mecha-
nizmu utrwalania danych.
Na listingu 7.12 przedstawiamy interfejs obiektów DAO z aplikacji MyMovies-
Database.

Listing 7.12. Interfejs obiektów DAO określający standardowe operacje na danych

public interface Dao<T> {


long save(T type);
void update(T type);
void delete(T type);
T get(long id);
List<T> getAll();
}
0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych 313

Ten interfejs obiektów DAO jest prosty i typowy. Jedynym ciekawym aspek-
tem tego interfejsu jest jego parametryzacja. Typ T reprezentuje klasę modelu
danych, którą obiekt manipuluje (typem tym mogą być opisane wcześniej klasy
Movie i Category). Za pomocą interfejsu i powiązanych z nim implementacji można
łatwo zapisywać oraz aktualizować obiekty modelu. Pozwala to ukryć wszystkie
szczegóły w obiektach DAO.
Należy zauważyć, że stosowanie obiektów DAO nie zawsze jest odpowied-
nim rozwiązaniem. Obiekty te są stosunkowo ogólne i mogą zwracać zbyt dużo
danych. Jeśli na przykład chcesz tylko zapełnić pola wyboru tytułami filmów
z systemu, i tak musisz pobrać wszystkie dane. Problem można rozwiązać przez
rozbudowanie interfejsu w obiektach DAO, które potrzebują innych metod
dostępu do danych. Warto jednak pamiętać, że obiekty te nie są pozbawione
wad. Ponadto obiekty DAO wymagają utworzenia kilku kolejnych klas i napi-
sania dodatkowego kodu, uważamy jednak, że wyraźny podział zadań i łatwość
używania kodu są często (choć nie zawsze) warte zachodu.
Aby wyjaśnić implementacje klas obiektów DAO, przedstawiamy najbardziej
skomplikowaną klasę tego typu z aplikacji MyMoviesDatabase — MovieDao.
W klasie tej (jej pierwsza część znajduje się na listingu 7.13) pokazujemy wiele
technik stosowania SQL-u, które mogą się przydać w trakcie pisania aplikacji na
Android.

Listing 7.13. Pierwsza część klasy MovieDao — zapisywanie nowego filmu

public class MovieDao implements Dao<Movie> {

private static final String INSERT =


"insert into " + MovieTable.TABLE_NAME
+ "(" + MovieColumns.HOMEPAGE + ", " + MovieColumns.NAME + ", "
+ MovieColumns.RATING + ", " + MovieColumns.TAGLINE + ", "
+ MovieColumns.THUMB_URL + ", "+ MovieColumns.IMAGE_URL + ", "
+ MovieColumns.TRAILER + ", " + MovieColumns.URL + ", "
+ MovieColumns.YEAR + ")
values (?, ?, ?, ?, ?, ?, ?, ?, ?)";

private SQLiteDatabase db;


private SQLiteStatement insertStatement;

public MovieDao(SQLiteDatabase db) {


this.db = db;
insertStatement = db.compileStatement(MovieDao.INSERT);
}

@Override
public long save(Movie entity) {
insertStatement.clearBindings();
insertStatement.bindString(1, entity.getHomepage());
insertStatement.bindString(2, entity.getName());
insertStatement.bindDouble(3, entity.getRating());
insertStatement.bindString(4, entity.getTagline());
insertStatement.bindString(5, entity.getThumbUrl());
314 ROZDZIAŁ 7. Lokalne zapisywanie danych

insertStatement.bindString(6, entity.getImageUrl());
insertStatement.bindString(7, entity.getTrailer());
insertStatement.bindString(8, entity.getUrl());
insertStatement.bindLong(9, entity.getYear());
return insertStatement.executeInsert();
}

Klasa MovieDao ma obejmować implementację interfejsu obiektów DAO . Dalej


znajduje się stała INSERT typu String. Bezpośrednio podano w niej wszystkie
kolumny, w których aplikacja ma zapisywać dane. Znaki zapytania to symbole
zastępcze dla zapisywanych danych . Kolejnym elementem jest konstruktor,
do którego przekazywany jest obiekt klasy SQLiteDatabase. Obiekt ten służy do
nawiązywania połączenia z bazą i wykonywania operacji . Łańcuch znaków
z operacją INSERT kompilowany jest do obiektu klasy SQLiteStatement .
Stosowanie skompilowanej instrukcji zamiast nieprzetworzonego SQL-owego
polecenia INSERT przyspiesza działanie kodu, ponieważ framework może wstęp-
nie przetworzyć i ponownie wykorzystać plan wykonania instrukcji. Skompilowa-
nych instrukcji można jednak używać do wykonywania tylko tych zadań, które
nie zwracają żadnych wierszy lub zwracają tylko jeden wiersz i kolumnę (poje-
dynczą wartość typu long lub String). Ponieważ omawiane instrukcje zapewniają
wysoką wydajność (jednak nie zwracają wielu wierszy), doskonale nadają się do
wstawiania danych.
Po konstruktorze znajduje się metoda save, w której użyto obiektu insert
´Statement . Najpierw należy usunąć wcześniejsze wiązania, a potem powiązać
każdy symbol zastępczy z instrukcji z odpowiednią wartością z obiektu modelu.
Po zakończeniu wiązania aplikacja wywołuje metodę executeInsert i zwraca
identyfikator określający, w którym wierszu tabeli Movie zapisano dane. To już
wszystko na temat wstawiania. Kiedy dane są zapisywane tylko w jednej tabeli,
operacja ta jest prosta.
Następny fragment klasy MovieDao to metoda update. Przedstawiamy ją na
listingu 7.14.

Listing 7.14. Drugi fragment klasy MovieDao — aktualizowanie danych o filmie

public void update(Movie entity) {


final ContentValues values = new ContentValues();
values.put(MovieColumns.HOMEPAGE, entity.getHomepage());
values.put(MovieColumns.NAME, entity.getName());
values.put(MovieColumns.RATING, entity.getRating());
values.put(MovieColumns.TAGLINE, entity.getTagline());
values.put(MovieColumns.THUMB_URL, entity.getThumbUrl());
values.put(MovieColumns.IMAGE_URL, entity.getImageUrl());
values.put(MovieColumns.TRAILER, entity.getTrailer());
values.put(MovieColumns.URL, entity.getUrl());
values.put(MovieColumns.YEAR, entity.getYear());
db.update(MovieTable.TABLE_NAME, values,
BaseColumns._ID + " = ?", new String[] {
String.valueOf(entity.getId()) });
}
0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych 315

Metoda update najpierw tworzy obiekt klasy ContentValues, w którym zapisywane


są pary klucz-wartość z nazwami kolumn i nowymi danymi . Klasę ContentValues
omawiamy w następnym rozdziale w kontekście tworzenia dostawców treści.
Na razie możesz traktować ją jak odwzorowanie danych, które należy zaktuali-
zować. Po zakończeniu przygotowań wywoływana jest metoda update obiektu
klasy SQLiteDatabase. Do metody tej należy przekazać nazwę tabeli, wartości, klau-
zulę WHERE i argumenty tej klauzuli .
Metoda update obejmuje standardowy dla Androida kod. To samo dotyczy
metody delete, którą przedstawiamy na listingu 7.15.

Listing 7.15. Trzeci fragment klasy MovieDao — usuwanie filmu

@Override
public void delete(Movie entity) {
if (entity.getId() > 0) {
db.delete(MovieTable.TABLE_NAME,
BaseColumns._ID + " = ?", new String[]
{ String.valueOf(entity.getId()) });
}
}

Metoda delete działa w bardzo podobny sposób jak metoda update (różnicą jest to,
że w metodzie delete nie trzeba podawać nowych wartości). Należy przekazać
nazwę tabeli, klauzulę WHERE i argumenty tej klauzuli . Po metodzie delete nastę-
pują metody get i getAll. Kierują one zapytanie do tabeli Movie i za pomocą
kursora zwracają obiekt klasy Movie (listing 7.16).

Listing 7.16. Czwarty fragment klasy MovieDao — pobieranie danych o filmach

@Override
public Movie get(long id) {
Movie movie = null;
Cursor c =
db.query(MovieTable.TABLE_NAME,
new String[] {
BaseColumns._ID, MovieColumns.HOMEPAGE,
MovieColumns.NAME, MovieColumns.RATING, MovieColumns.TAGLINE,
MovieColumns.THUMB_URL, MovieColumns.IMAGE_URL,
MovieColumns.TRAILER, MovieColumns.URL, MovieColumns.YEAR },
BaseColumns._ID + " = ?", new String[] { String.valueOf(id) },
null, null, null, "1");
if (c.moveToFirst()) {
movie = this.buildMovieFromCursor(c);
}
if (!c.isClosed()) {
c.close();
}
return movie;
}

@Override
public List<Movie> getAll() {
List<Movie> list = new ArrayList<Movie>();
316 ROZDZIAŁ 7. Lokalne zapisywanie danych

Cursor c =
db.query(MovieTable.TABLE_NAME, new String[] {
BaseColumns._ID, MovieColumns.HOMEPAGE,
MovieColumns.NAME, MovieColumns.RATING, MovieColumns.TAGLINE,
MovieColumns.THUMB_URL, MovieColumns.IMAGE_URL,
MovieColumns.TRAILER, MovieColumns.URL, MovieColumns.YEAR },
null, null, null, null, MovieColumns.NAME, null);
if (c.moveToFirst()) {
do {
Movie movie = this.buildMovieFromCursor(c);
if (movie != null) {
list.add(movie);
}
} while (c.moveToNext());
}
if (!c.isClosed()) {
c.close();
}
return list;
}

private Movie buildMovieFromCursor(Cursor c) {


Movie movie = null;
if (c != null) {
movie = new Movie();
movie.setId(c.getLong(0));
movie.setHomepage(c.getString(1));
movie.setName(c.getString(2));
movie.setRating(c.getInt(3));
movie.setTagline(c.getString(4));
movie.setThumbUrl(c.getString(5));
movie.setImageUrl(c.getString(6));
movie.setTrailer(c.getString(7));
movie.setUrl(c.getString(8));
movie.setYear(c.getInt(9));
}
return movie;
}

Metody do pobierania danych są bardziej skomplikowane od wcześniejszych


metod. Przede wszystkim warto zauważyć, że metody do pobierania danych
zwracają kursor (obiekt klasy Cursor) . Jeśli korzystałeś z technologii Java
JDBC, prawdopodobnie używałeś obiektów klasy ResultSet. Klasa ta to kursor
wzbogacony o pewne dodatkowe mechanizmy. Niektóre metody kursorów po-
winny być znane programistom Javy. Kursory są częścią większości baz danych,
w tym baz SQLite. Ponieważ Android nie obsługuje technologii JDBC, nie wystę-
puje tu klasa ResultSet.
DLACZEGO NIE JDBC? Korzystanie z technologii JDBC ma pewne
wady i zalety. Gdyby Android obsługiwał tę technologię, łatwiej byłoby
tworzyć przenośny kod lub ponownie wykorzystać gotowy już kod dla baz
SQLite. Jednak w Androidzie celowo zrezygnowano z obsługi JDBC.
Prawdopodobnie wynikało to z kosztów stosowania tej technologii i z tego,
0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych 317

że w Androidzie istnieje prosty w użyciu interfejs API w postaci pakietu


android.database.sqlite. Jeśli obiecasz, że nikomu o tym nie powiesz,
wyjawimy Ci jednak pewien sekret — w Androidzie dostępny jest sterow-
nik JDBC dla baz SQLite. Sterownik ten działa, jest jednak nieudokumen-
towany i brakuje dla niego wsparcia technicznego. Ponadto jest on niedo-
stępny na niektórych urządzeniach. Dlatego choć istnieje i wspominamy
o nim, zdecydowanie odradzamy korzystanie z jakichkolwiek elementów
Androida pozbawionych wsparcia technicznego.
Metody query (w klasie SQLiteDatabase istnieje kilka przeciążonych wersji metody
o tej nazwie) przyjmują nazwę tabeli, klauzulę wyboru i argumenty tej klauzuli.
Dostępnych jest też kilka dodatkowych klauzul, na przykład order by, group by,
having i limit. Te elementy SQL-owej instrukcji select umożliwiają dostosowanie
zapytań do potrzeb programisty. Po analizie składni każdej metody query wykony-
wana jest instrukcja select, po czym metoda zwraca kursor.
W metodzie get wywoływana jest metoda query . Jeśli zwrócony kursor
obejmuje pierwszy wiersz danych , wywoływana jest metoda buildMovieFrom
´Cursor w celu utworzenia obiektu klasy Movie na podstawie danych z wier-
sza . Po zakończeniu pracy aplikacja zamyka kursor . Ta ostatnia operacja
jest bardzo ważna. Jeśli aplikacja nie zamknie kursora, nastąpi wyciekanie zwią-
zanych z nim zasobów. Nie będzie można też usunąć komponentu, który utworzył
kursor, i mogą wystąpić innego rodzaju problemy.
Oprócz metody get istnieje metoda getAll. W metodzie getAll w instrukcji
select nie jest określony identyfikator, dlatego metoda zwraca wiersze dotyczące
wszystkich filmów. Wiersze te są przetwarzane w pętli do-while przy użyciu
metody moveToNext kursora . W każdej iteracji ponownie wywoływana jest
metoda buildMovieFromCursor. Przetwarza ona wiersz przez wywołanie metod get
dla odpowiednich typów w celu pobrania każdego pola z danymi.
Po utworzeniu metod save, update, delete, get i getAll obiektów DAO
rozwiązanie jest już prawie gotowe. Należy jeszcze tylko dodać do interfejsu
mechanizm do wyszukiwania filmów na podstawie tytułów. Potrzebny kod poka-
zano na listingu 7.17.

Listing 7.17. Ostatni fragment klasy MovieDao — wyszukiwanie filmów według


tytułów

public Movie find(String name) {


long movieId = 0L;
String sql = "select _id from " + MovieTable.TABLE_NAME
+ " where upper(" + MovieColumns.NAME + ") = ? limit 1";
Cursor c = db.rawQuery(sql,
new String[]
{ name.toUpperCase() });
if (c.moveToFirst()) {
movieId = c.getLong(0);
}
318 ROZDZIAŁ 7. Lokalne zapisywanie danych

if (!c.isClosed()) {
c.close();
}
return this.get(movieId);
}

Metoda find, służąca do wyszukiwania filmów zapisanych już w bazie, działa


podobnie jak inne metody do pobierania danych, choć wywołuje metodę raw
´Query . Nie jest to konieczne, jednak chcemy pokazać, że można zastosować
to podejście. Pozwala ono wygodnie dodać do zapytania instrukcję limit i użyć
funkcji bazy SQLite.
Tu użyto funkcji upper bazy SQLite do porównania wartości (po przekształce-
niu jej na same wielkie litery) pola bazy danych z obiektem typu String (także
przekształconym na wielkie litery). Przekształcanie znaków pozwala dopasować
łańcuchy niezależnie od wielkości liter. Widać tu, że baza SQLite — podobnie
jak wiele innych systemów zarządzania bazami danych — obsługuje funkcje. Pełną
listę tych funkcji znajdziesz w dokumentacji.
Ostatnią rzeczą, na którą warto zwrócić uwagę w metodzie find, jest dwu-
krotne komunikowanie się z bazą danych. Pierwsze zapytanie pobiera identyfi-
kator filmu, a drugie jest zgłaszane przy wywoływaniu zdefiniowanej wcześniej
metody get . Nie jest to najwydajniejszy sposób pobierania danych, jednak
akceptujemy to. Rozwiązanie to jest przystępne i łatwe w konserwacji, a koszty
ponoszone przy małej ilości danych są akceptowalne. Jeśli później zauważysz
problemy z wydajnością, zawsze możesz wykonywać zadanie w jednym zapyta-
niu, nie trzeba jednak optymalizować kodu przed wystąpieniem kłopotów.
MovieDao to najbardziej skomplikowana klasa DAO w przykładzie. Wstawia,
aktualizuje, usuwa i pobiera dane na kilka sposobów. Inna klasa DAO, CategoryDao,
wykonuje podobne operacje na tabeli Category. Czy jesteśmy gotowi udostęp-
nić klasy DAO? Czy możemy zacząć korzystać z nich w aktywnościach i innych
komponentach? Nie do końca. Najpierw, jak już wspomnieliśmy, należy utworzyć
jeszcze jedną warstwę, aby umieścić klasy DAO w łatwym w użyciu menedżerze
danych.
Tworzenie menedżera danych
Obiekty DAO reprezentują poszczególne encje danych (Movie i Category)
potrzebne w aplikacji. Jednak w klasach DAO celowo pominęliśmy związki między
tabelami. Na przykład w czasie zapisywania nowego filmu obiekt modelu obej-
muje kolekcję List<Category>, jednak klasa MovieDao go nie przetwarza — jej
jedynym zadaniem jest obsługa tabeli Movie.
Ponieważ obiekty DAO nie znają tabel innych niż te, za które odpowiadają,
należy utworzyć interfejs DataManager i klasę z jego implementacją. Klasa ta ma
obejmować obiekty DAO i wykonywać inne zadania. Te zadania to na przykład
zapisywanie danych w wielu tabelach i obsługa transakcji. Komponenty aplikacji
0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych 319

mają korzystać z tej klasy do zapisywania i pobierania danych. Rozwiązanie to,


przedstawione na listingu 7.18, pozwala umieścić cały kod SQL-a i logikę obsługi
baz niezależnie od komponentów oraz widoków aplikacji.

Listing 7.18. Interfejs DataManager z definicjami wszystkich możliwych operacji

public interface DataManager {

public Movie getMovie(long movieId);


public List<Movie> getMovieHeaders();
public Movie findMovie(String name);
public long saveMovie(Movie movie);
public boolean deleteMovie(long movieId);

public Category getCategory(long categoryId);


public List<Category> getAllCategories();
public Category findCategory(String name);
public long saveCategory(Category category);
public void deleteCategory(Category category);
}

Interfejs menedżera danych jest prosty. Obejmuje zestaw metod (takich jak get,
save i delete) do wykonywania standardowych operacji na każdym z podstawo-
wych obiektów modelu — Movie i Category. Komponenty aplikacji używają refe-
rencji do interfejsu do wykonywania operacji na danych.
Ciekawsze aspekty opisywanej warstwy kryją się w powiązanej z bazą danych
klasie z implementacją, w której używamy obiektów DAO. Pierwszą część tej
klasy przedstawiamy na listingu 7.19.

Listing 7.19. Pierwsza część klasy DataManagerImpl z implementacją interfejsu


DataManager

public class DataManagerImpl implements DataManager {

private static final int DATABASE_VERSION = 1;

private Context context;

private SQLiteDatabase db;


private CategoryDao categoryDao;
private MovieDao movieDao;
private MovieCategoryDao movieCategoryDao;

public DataManager(Context context) {

this.context = context;

SQLiteOpenHelper openHelper =
new OpenHelper(this.context);
db = openHelper.getWritableDatabase();

categoryDao = new CategoryDao(db);


movieDao = new MovieDao(db);
320 ROZDZIAŁ 7. Lokalne zapisywanie danych

movieCategoryDao = new MovieCategoryDao(db);


}

// Dalszą część klasy znajdziesz na następnym listingu.

DataManagerImpl nie jest klasą pochodną od jednej z klas Androida ani nie opiera
się na mechanizmach tej platformy. Jest naszym pomysłem i zaimplementowali-
śmy w niej interfejs DataManager . Omawiana klasa obejmuje stałą, w której zapi-
sujemy aktualną wersję bazy danych i zmienne składowe, w których należy
zapisać każdy obiekt DAO . W konstruktorze aplikacja tworzy obiekt klasy
SQLiteOpenHelper i używa go do nawiązania połączenia z bazą danych .
Po zmiennych składowych i początkowych operacjach konfiguracyjnych znaj-
dują się metody pełniące funkcję nakładek na obiekty DAO. Metody te przed-
stawiono na listingu 7.20.

Listing 7.20. Pozostała część klasy DataManagerImpl z metodami do obsługi


danych (nakładkami na obiekty DAO)

public Movie getMovie(long movieId) {


Movie movie = movieDao.get(movieId);
if (movie != null) {
movie.getCategories().addAll(
movieCategoryDao.getCategories(movie.getId()));
}
return movie;
}

public List<Movie> getMovieHeaders() {


return movieDao.getAll();
}

public Movie findMovie(String name) {


Movie movie = movieDao.find(name);
if (movie != null) {
movie.getCategories().addAll(
movieCategoryDao.getCategories(movie.getId()));
}
return movie;
}

public long saveMovie(Movie movie) {


long movieId = 0L;

try {
db.beginTransaction();
movieId = movieDao.save(movie);

if (movie.getCategories().size() > 0) {
for (Category c : movie.getCategories()) {
long catId = 0L;
Category dbCat = categoryDao.find(c.getName());
if (dbCat == null) {
catId = categoryDao.save(c);
} else {
catId = dbCat.getId();
0 TECHNIKA 35. Tworzenie obiektów DAO i menedżera danych 321

}
MovieCategoryKey mcKey =
new MovieCategoryKey(movieId, catId);
if (!movieCategoryDao.exists(mcKey)) {
movieCategoryDao.save(mcKey);
}
}
}

db.setTransactionSuccessful();
} catch (SQLException e) {
Log.e(Constants.LOG_TAG,
"Błąd przy zapisie filmu (transakcję anulowano)", e);
movieId = 0L;
} finally {
db.endTransaction();
}

return movieId;
}

public boolean deleteMovie(long movieId) {


boolean result = false;
try {
db.beginTransaction();
Movie movie = getMovie(movieId);
if (movie != null) {
for (Category c : movie.getCategories()) {
movieCategoryDao.delete(
new MovieCategoryKey(movie.getId(), c.getId()));
}
movieDao.delete(movie);
}
db.setTransactionSuccessful();
result = true;
} catch (SQLException e) {
Log.e(Constants.LOG_TAG,
"Błąd przy usuwaniu filmu (transakcję anulowano)", e);
} finally {
db.endTransaction();
}
return result;
}

public Category getCategory(long categoryId) {


return categoryDao.get(categoryId);
}

public List<Category> getAllCategories() {


return categoryDao.getAll();
}

public Category findCategory(String name) {


return categoryDao.find(name);
}

public long saveCategory(Category category) {


return categoryDao.save(category);
322 ROZDZIAŁ 7. Lokalne zapisywanie danych

public void deleteCategory(Category category) {


categoryDao.delete(category);
}

// Klasę wewnętrzną OpenHelper znajdziesz na listingu 7.7.


}

Istotą klasy DataManagerImpl są metody do zarządzania danymi. W metodach tych


wykorzystujemy obiekty DAO. Jedną ze wspomnianych metod jest getMovie ,
w której używamy klas MovieDao i CategoryDao do zwrócenia jednego komplet-
nego obiektu klasy Movie. Metoda getMovieHeaders zwraca kolekcję filmów bez
kategorii . Wymienione w nazwie metody „nagłówki” (ang. headers) można
wykorzystać do wyświetlania danych o filmach, kiedy nie są potrzebne szcze-
gółowe informacje.
Jeden z najciekawszych aspektów klasy DataManagerImpl, a jednocześnie powód,
dla którego używamy odrębnego menedżera danych, jest związany z metodą
saveMovie . Tu ponownie używamy odrębnych obiektów DAO, jednak dzieje
się to w ramach transakcji. Transakcja gwarantuje, że jeśli jedna jej część się nie
powiedzie, wszystkie operacje zostaną anulowane. Zapobiega to przejściu w nie-
spójny stan. Jeśli na przykład można zapisać nowy film i powiązane z nim kate-
gorie , jednak z pewnych przyczyn nie da się wstawić do bazy powiązań kategorii
z filmami , nie należy zachowywać żadnych danych. Nie chcemy przecież zapi-
sywać filmu lub kategorii bez właściwych powiązań między nimi. Transakcja na
tym poziomie ma sens (ponieważ menedżer ma informacje kontekstowe pozwa-
lające stwierdzić, że transakcja jest potrzebna), jednak stosowanie jej w samym
obiekcie DAO może być błędem.
Zarządzanie transakcją polega na jej bezpośrednim rozpoczęciu , ustawie-
niu stanu na „powodzenie”, jeśli nie wystąpiły żadne wyjątki , i na jej zakoń-
czeniu . Warto zauważyć, że w klasie nie ma metody do anulowania transakcji.
Jeśli jednak metoda beginTransaction uruchomiła proces, a przed wywołaniem
metody setTransactionSuccessful nastąpiło wywołanie metody endTransaction,
system automatycznie anuluje transakcję. Metoda endTransaction znajduje się
w bloku finally, dlatego jest wywoływana nawet po wystąpieniu wyjątków.
Po metodzie saveMovie w menedżerze znajduje się podobna metoda, delete
´Movie, która także obsługuje transakcje . Dalej występują inne podobne
metody nakładkowe dla kategorii (choć w metodach tych używamy tylko poje-
dynczych obiektów DAO, punktem dostępu do tych metod również jest mene-
dżer). Uzupełnieniem klasy DataManager i całej warstwy zarządzania danymi są
pozostałe metody nakładkowe dla obiektów DAO oraz przedstawiona wcześniej
klasa wewnętrzna OpenHelper.
Korzystanie z klasy DataManager
Jak wykorzystać nową warstwę zarządzania danymi w aplikacji MyMoviesData-
base? Należy utworzyć egzemplarz menedżera w obiekcie Application, dostępnym
7.4. Badanie baz SQLite 323

wszystkim aktywnościom i innym komponentom. Następnie można kierować


wywołania do menedżera. Na przykład: aktywność MyMovies nadal korzysta z głów-
nego widoku ListView potrzebnego w aplikacji. Jednak zamiast używać adaptera
i kolekcji List<String>, korzystamy z adaptera i kolekcji List<Movie>. Pobieranie
danych z bazy odbywa się teraz tak:
private MovieAdapter adapter;
private List<Movie> movies;
...
movies.addAll(app.getDataManager().getMovieHeaders());
adapter.notifyDataSetChanged();

Inna możliwość to napisanie klasy CursorAdapter. CursorAdapter to adapter, który


potrafi pobierać dane z bazy. Czasem takie rozwiązanie jest wygodne — na przy-
kład kiedy wiele komponentów może modyfikować bazę. Za pomocą adaptera
CursorAdapter widok ListView może zarządzać kursorem i automatycznie aktu-
alizować elementy widoku w reakcji na dodanie danych. Wadą stosowania adap-
terów CursorAdapter jest to, że kod elementów związanych z bazami danych, mię-
dzy innymi kursorów, pojawia się w kodzie aktywności (na przykład w kontekście
obsługi kliknięcia elementów listy). Dlatego korzystanie z adapterów CursorAdapter
czasem ma sens, natomiast w innych sytuacjach wygodniej jest z nich zrezygno-
wać. W kodzie źródłowym aplikacji MyMoviesDatabase zastosowaliśmy oba
podejścia (z komentarza w aktywności MyMovies dowiesz się, jak przełączać
się między nimi).
OMÓWIENIE
Teraz w komponentach aplikacji można wygodnie używać obiektów modelu
i implementacji interfejsu DataManager, który jest nakładką na obiekty DAO.
Cały kod do obsługi bazy danych umieściliśmy w jednym miejscu i ukryliśmy
wszystkie szczegółowe operacje. Teraz można zmienić implementację klasy Data
´Manager i używać w niej plików lub wywoływać usługę sieciową (co nie jest naj-
lepszym pomysłem, jednak teoretycznie można tak postąpić) bez konieczności
modyfikowania głównego kodu aplikacji, czyli aktywności i innych komponentów.
Oprócz warstwy menedżera utworzyliśmy też odrębne klasy do tworzenia
i aktualizowania tabel, a także do tworzenia i otwierania bazy danych. Gotowe
są już wszystkie wymienione wcześniej warstwy. Tworząc je, omówiliśmy wiele
szczegółowych aspektów pracy z bazami w Androidzie. Po przygotowaniu warstwy
do obsługi bazy danych pora przejść do badania baz SQLite i rozwiązywania
problemów, które ich dotyczą.

7.4. Badanie baz SQLite


Poza aplikacją często trzeba bezpośrednio uzyskać dostęp do baz SQLite i zbadać
je. Chcemy na przykład się upewnić, że tabele są dostępne i mają odpowiednią
strukturę. Warto wykonać zapytania testowe i przyjrzeć się wynikom. Wszystkie
te operacje wymagają użycia narzędzi do obsługi danych.
324 ROZDZIAŁ 7. Lokalne zapisywanie danych

W kilku następnych punktach przyglądamy się bazie mymovies.db, korzy-


stając z powłoki baz SQLite uruchamianej w wierszu poleceń i niezależnego
narzędzia z graficznym interfejsem, SQLiteManagera. Istnieje wiele niezależnych
narzędzi tego rodzaju. Wybraliśmy SQLiteManagera, ponieważ działa w różnych
systemach (jest rozszerzeniem przeglądarki Firefox), jest bezpłatny i rozpo-
wszechniany jako oprogramowanie o otwartym dostępie do kodu źródłowego.

Powłoka baz SQLite


Baza SQLite obejmuje powłokę poleceń. Jest to narzędzie sqlite3, służące do
zarządzania bazami SQLite, uruchamiane w wierszu poleceń, wszechstronne jak
szwajcarski scyzoryk. Aby uruchomić sqlite3 na urządzeniu z Androidem lub
w emulatorze, trzeba najpierw nawiązać połączenie z powłoką adb, a następnie
uruchomić powłokę baz SQLite dla odpowiedniej bazy. Na rysunku 7.10 powłoka
jest uruchomiona dla aplikacji MyMoviesDatabase i pliku mymovies.db.

Rysunek 7.10. Korzystanie z narzędzia sqlite3 (powłoki baz SQLite) w przypadku


bazy mymovies.db

W widocznej na rysunku 7.10 sesji powłoki adb najpierw przeszliśmy do katalogu


/data/data/<nazwa_pakietu>/databases, a następnie wyświetliliśmy jego zawartość.
Widać tu plik mymovies.db. Dalej wywołujemy powłokę baz SQLite za pomocą
instrukcji sqlite3 mymovies.db. Po uruchomieniu powłoki baz SQLite uzyskujemy
dostęp do wszystkich jej poleceń. Niektóre z nich przedstawiamy w tabeli 7.2.
Tabela 7.2. Przydatne polecenia powłoki sqlite3 do zarządzania bazami SQLite

Polecenie sqlite3 Opis


.help Wyświetla wszystkie polecenia i opcje.

.tables Wyświetla tabele z wybranej bazy danych.

.schema Wyświetla instrukcje CREATE użyte do utworzenia wybranej bazy danych.


.explain Przetwarza i analizuje instrukcje SQL-a oraz wyświetla plan ich
wykonania.

W powłoce często bezpośrednio uruchamia się instrukcje w SQL-u, na przykład


polecenia select, insert, delete itd. Jeśli chcesz dowiedzieć się czegoś więcej
o rozbudowanym narzędziu, jakim jest sqlite3, zajrzyj do pełnej dokumentacji
na stronie http://www.sqlite.org/sqlite.html.
7.5. Podsumowanie 325

SQLITEMANAGER
Jeśli do badania baz i manipulowania nimi wolisz stosować narzędzie z graficznym
interfejsem użytkownika, jednym z najłatwiejszych w użyciu jest rozszerzenie
Firefoksa SQLiteManager. Aby móc korzystać z tego narzędzia, należy zainsta-
lować Firefoksa i rozszerzenie ze strony http://mng.bz/iG6q. Po zainstalowaniu
rozszerzenia można uruchomić SQLiteManagera z menu Narzędzia w przeglą-
darce Firefox. Po pierwszym uruchomieniu pojawia się nowe, puste okno. Aby coś
zrobić, trzeba wskazać plik z bazą SQLite.
Nie istnieje wygodny (i bezpieczny) sposób na automatyczne połączenie się
z działającym urządzeniem lub emulatorem oraz znalezienie pliku z bazą. Dlatego
taki plik trzeba skopiować na lokalny komputer. Można to zrobić za pomocą pole-
cenia adb pull lub przy użyciu eksploratora plików Androida, co pokazano na
rysunku 7.11. Taki eksplorator jest dostępny w narzędziach Eclipse ADT i DDMS.

Rysunek 7.11.
Kopiowanie
pliku z bazą
z uruchomionego
urządzenia
lub emulatora
na lokalny komputer
z wykorzystaniem
narzędzia DDMS

Po uzyskaniu dostępu do pliku z bazą wystarczy otworzyć go w SQLiteManagerze.


Należy kliknąć plik i można przeglądać zawartość bazy, zmieniać ustawienia,
a nawet wykonywać polecenia w SQL-u. Na rysunku 7.12 widać zawartość pliku
mymovies.db.
Przykładowa aplikacja z plikiem ustawień, warstwą dostępu do danych, obiek-
tami DAO i obsługą baz jest już gotowa. Zobaczyłeś też, jak przeglądać dane
i korzystać z narzędzi do sprawdzania schematu, uruchamiania przykładowych
zapytań, a nawet wyświetlania planu ich wykonania. To już koniec przeglądu
mechanizmów do lokalnego przechowywania danych w Androidzie.

7.5. Podsumowanie
Omówiliśmy wiele zagadnień z obszaru przechowywania i utrwalania danych.
Nie opisaliśmy wszystkich możliwości, ponieważ jest ich zbyt wiele, jednak przed-
stawiliśmy najczęściej stosowane i najprzydatniejsze techniki zapisywania oraz
pobierania lokalnych danych w aplikacjach na Android.
Zaczęliśmy od systemu plików oraz podstawowych sposobów odczytu i zapisu
danych w plikach. Następnie omówiliśmy także oparty na plikach, ale łatwiejszy
w użyciu mechanizm współużytkowanych ustawień (klasa SharedPreferences).
326 ROZDZIAŁ 7. Lokalne zapisywanie danych

Rysunek 7.12. Przeglądanie pliku z bazą SQLite w SQLiteManagerze

Na zakończenie pokazaliśmy, jak używać lokalnych baz SQLite. Bazy danych opi-
saliśmy bardzo szczegółowo, ponieważ w Androidzie są one najbardziej rozbu-
dowanym mechanizmem do lokalnego przechowywania danych. W przykładowej
aplikacji w osobnych klasach zapisaliśmy kod do tworzenia bazy danych i poszcze-
gólnych tabel, kod do obsługi danych umieściliśmy w obiektach DAO, a jako
nakładkę na operacje dotyczące baz utworzyliśmy menedżera danych. Zbudo-
waliśmy więc architekturę warstwową opartą na bazie danych i ukryliśmy szcze-
gółowe operacje przed komponentami aplikacji. Podróż po danych lokalnych
zakończyliśmy badaniem baz danych i rozwiązywaniem problemów z poziomu
powłoki poleceń oraz zewnętrznych narzędzi.
Wszystko to zrobiliśmy, aby przenieść przykładową aplikację MyMovies
z poprzednich rozdziałów na wyższy poziom. Zmodyfikowaliśmy ją, aby móc
wyszukiwać informacje, stosować ustawienia i zapisywać dane w bazie. Teraz
aplikacja nie korzysta z predefiniowanych danych z pliku zasobów — jest dyna-
miczna i zależna od poczynań użytkownika.
Dalej, w rozdziale 8., wychodzimy poza lokalne przechowywanie danych
i pokazujemy, jak używać danych z innych aplikacji, na przykład z wbudowanego
menedżera kontaktów. Zobaczysz też, jak udostępniać własne dane. Oba zagad-
nienia związane są z klasą ContentProvider.
Współużytkowanie danych
między aplikacjami

W tym rozdziale:
Q Współużytkowanie danych między procesami
Q Współużytkowane pliki ustawień
Q Dostęp do współużytkowanych danych

Ludzie przyzwyczaili się nie tylko do udostępniania większej ilości


informacji różnego rodzaju, ale też do ujawniania ich bardziej otwarcie
i większej liczbie osób.
Mark Zuckerberg
Aplikacja na Android może mieć wiele funkcji. Możliwości są niemal nieskoń-
czone. Jednak jedną z cech odróżniających Android od innych systemów jest
to, że platforma ta umożliwia współpracę między aplikacjami, a nawet skłania
do tego. W rozdziale 5. dokładnie opisaliśmy, jak duże możliwości daje wielo-
zadaniowość w Androidzie. Dlatego można przyjąć, że w czasie działania danej
aplikacji uruchomione są też inne programy. Prowadzi to do ciekawych wniosków.
Grupa współdziałających aplikacji jest dla użytkownika wartościowsza niż jaki-
kolwiek pojedynczy program.
Niezwykle ważnym aspektem integrowania aplikacji jest współużytkowanie
danych. Istotne są też inne kwestie, na przykład przepływ sterowania, jednak
niewiele można zrobić, jeśli aplikacje nie potrafią współużytkować danych. Nie
powinno zaskakiwać, że Android udostępnia kilka technik z tego obszaru, co
pozwala programistom współużytkować dane w sposób najbardziej dostosowany

327
328 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

do sytuacji. W tym rozdziale szczegółowo przedstawiamy różne sposoby współ-


użytkowania danych w aplikacjach na Android. Opisujemy też sytuacje, w których
warto wykorzystać poszczególne techniki.

8.1. Współużytkowanie danych między procesami


W rozdziale 6. przedstawiliśmy liczne mechanizmy Androida ułatwiające progra-
mowanie współbieżne. Zacznijmy od omówienia współużytkowania w tym kon-
tekście, aby móc je porównać ze współużytkowaniem danych między procesami,
które dalej opisujemy szczegółowo. Współbieżność jest wygodna do momentu,
w którym trzeba współużytkować dane. Programowanie współbieżne w formie
opisywanej do tego miejsca polega na programowaniu wielowątkowym. Kiedy
dane są współużytkowane, używają ich różne wątki działające na tej samej maszy-
nie wirtualnej i w tym samym procesie Linuksa. Na rysunku 8.1 pokazujemy, że
może to wyglądać inaczej.

Rysunek 8.1. Wątki


i współużytkowanie
danych przez
przekazywanie
komunikatów
oraz wspólne dane

W tym miejscu koncentrujemy się na współużytkowaniu danych między przez


różne procesy, a nie wątki. Na rysunku 8.2 pokazujemy, jak może się to odbywać.
Wątki mogą wczytywać dane z tego samego miejsca z pamięci lokalnej. Jest to
najbardziej „intymny”, ale jednocześnie najmniej bezpieczny sposób współużyt-
kowania danych. Interfejs API Handler Androida zapewnia bezpieczniejszą metodę
współużytkowania danych między wątkami. Procesy (aplikacje) nie mogą współ-
0 TECHNIKA 36. Stosowanie intencji 329

Rysunek 8.2. Procesy i współużytkowanie danych przez przekazywanie


komunikatów (danych) oraz wspólne dane

użytkować danych przez odczyt informacji z tego samego miejsca z pamięci lokal-
nej. To niebezpieczne rozwiązanie nie jest dostępne. Zamiast tego trzeba prze-
syłać dane między granicami procesów. Najczęściej używa się do tego intencji.

0 TECHNIKA 36. Stosowanie intencji

Intencje poznałeś już wcześniej. Najczęściej używa się ich do przechodzenia


między aktywnościami w aplikacji. Intencje mogą też obejmować dane przeka-
zywane do aktywności. Jest to ich najważniejsza cecha, dzięki której umożliwiają
przechodzenie do aktywności z innych aplikacji i współużytkowanie danych
między programami.
PROBLEM
Chcemy przesyłać żądania z danymi do innej aplikacji lub wysyłać odpowiedź
na żądanie z innego programu. Aplikacja zgłaszająca żądanie przekazuje sterowa-
nie innej aplikacji w celu zrealizowania zadania.
ROZWIĄZANIE
Jak wspomnieliśmy wcześniej, dane można współużytkować między aplikacjami
na wiele sposobów. Dlatego ważne jest, aby zrozumieć subtelne różnice między
poszczególnymi rozwiązaniami. Chcemy, aby w omawianej tu technice inna apli-
kacja wykonywała pewne operacje. Dlatego pozwalamy tej aplikacji przejąć kon-
trolę nad interfejsem użytkownika. Aby pokazać, w jakich sytuacjach warto sto-
sować tę technikę, przedstawiamy dwie przykładowe aplikacje.
330 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

Pierwsza z nich to GoodShares. Pozwala ona użytkownikowi wybrać zdjęcie


z telefonu i przekazać je do innej przykładowej aplikacji, ImageMash. Ten drugi
program przeprowadza przekształcenie afiniczne rysunku na podstawie okre-
ślonych przez użytkownika skali i rotacji. Aplikację GoodShares przedstawiono
na rysunku 8.3.
Na rysunku widać, że użytkownik wybrał zdjęcie, które chce przekształcić.
Naciśnięcie przycisku Przekształć powoduje uruchomienie drugiej aplikacji —
ImageMash. Grafika to dane współużytkowane przez aplikacje. Teraz użytkow-
nik może wprowadzić parametry przekształcenia i zmodyfikować obraz. Efekt
przedstawiono na rysunku 8.4.

Rysunek 8.3. Aplikacja GoodShares Rysunek 8.4. Przekształcony rysunek


z wybranym rysunkiem w aplikacji GoodShares

Afiniczne?
Przekształcenie afiniczne polega na przeprowadzeniu na obiekcie (rysunku) prostych
transformacji geometrycznych w przestrzeni dwuwymiarowej. Przykładowa aplika-
cja skaluje rysunek w pionie i poziomie, a następnie go rotuje. W algebrze liniowej
przekształcenie afiniczne można przedstawić w postaci mnożenia macierzy.

Na rysunku 8.4 widać przekształcony rysunek. Warto zauważyć, że aplikacja


GoodShares wyświetla obraz w zmodyfikowanej postaci. Aplikacja ImageMash nie
tylko przekształca rysunek, ale też wysyła go z powrotem do aplikacji GoodShares,
która go pokazuje. Współużytkowanie rysunku odbywa się za pośrednictwem
intencji. Przepływ danych z wykorzystaniem intencji pokazujemy na rysunku 8.5.
0 TECHNIKA 36. Stosowanie intencji 331

Rysunek 8.5. Przesyłanie


danych między aplikacjami
za pośrednictwem intencji

Na wcześniejszych zrzutach nie pokazaliśmy, w jaki sposób użytkownik może


wybrać rysunek. Jak widać na rysunku 8.5, wykorzystujemy do tego aplikację
Gallery poprzez kod z listingu 8.1.

Listing 8.1. Używanie aplikacji Gallery do wybierania zdjęcia (kod z pliku


ShareActivity.java)

public class ShareActivity extends Activity {


Uri photoUri0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.share);
Button button = (Button) findViewById(R.id.btn0);
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Intent request = new
Intent(Intent.ACTION_GET_CONTENT);
request.setType("image/*");
startActivityForResult(request, 0);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == 0){
photoUri0 =
(Uri) data.getParcelableExtra(Intent.EXTRA_STREAM);
ImageView imgView0 = (ImageView) findViewById(R.id.pic0);
332 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

imgView0.setImageURI(photoUri0);
}
}
}

Kod z listingu 8.1 to fragment klasy ShareActivity (jest to główna aktywność


aplikacji GoodShares). To ten kod odpowiada za działanie interfejsu użytkownika
z rysunku 8.3. Jego najważniejszym zadaniem jest podłączenie odbiornika do
przycisku z rysunku 8.3.
Naciśnięcie przycisku prowadzi do utworzenia intencji z akcją Intent.ACTION_
´GET_CONTENT . Jest to jedna z wielu standardowych intencji z Androida, które
uruchamiają aktywności dotyczące często wykonywanych zadań, na przykład
wybierania zdjęć z aplikacji Gallery. Aby określić, że aplikacja ma pobrać rysu-
nek, należy ustawić właściwość type intencji. Powoduje to, że intencja jest prze-
kazywana do aplikacji Gallery. Następnie używamy metody startActivityForResult
aktywności . Prowadzi to do uruchomienia odpowiedniej aktywności (w apli-
kacji Gallery w innym procesie) i przekazania do niej sterowania. Wywołanie
metody startActivity przyniesie podobny efekt.
Ważnym aspektem tego rozwiązania jest to, że kiedy inna aktywność zwraca
sterowanie, przechodzi ono do pokazanej tu aktywności. Ponadto inna aktywność
może określić wynik, a aktywność omawiana ma dostęp do niego i do powiązanych
z nim informacji. Pobieranie wyniku odbywa się w metodzie onActivityResult
aktywności. Program Gallery ustawia dane odpowiedzi w innej intencji, prze-
kazywanej z powrotem do opisywanej aplikacji. Aplikacja ta może pobrać dane
z intencji za pomocą jednej z metod getXXXExtra. Trzeba w związku z tym
znać nazwę klucza (mieć dodatkowe informacje) dla danych przekazanych przez
program Gallery i znać typ tych danych. Potrzebna wiedza znajduje się w doku-
mentacji Javadoc dla klasy android.content.Intent w pakiecie SDK Android. Oka-
zuje się, że kluczem jest stała Intent.EXTRA_STREAM, a wartością — identyfikator
android.net.Uri prowadzący do wybranego rysunku. Pobraną grafikę należy
wyświetlić użytkownikowi w sposób pokazany na rysunku 8.3. Użytkownik może
wtedy nacisnąć przycisk, aby przesłać rysunek do aplikacji ImageMash. Kod
wywołujący tę aplikację pokazujemy na listingu 8.2.

Listing 8.2. Wywoływanie aplikacji ImageMash (kod z pliku ShareActivity.java)

public class ShareActivity extends Activity {


Uri photoUri0;
@Override
public void onCreate(Bundle savedInstanceState) {

Button button1 = (Button) findViewById(R.id.btn1);


button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent request =
new Intent("com.manning.aip.mash.ACTION");
request.addCategory(Intent.CATEGORY_DEFAULT);
0 TECHNIKA 36. Stosowanie intencji 333

request.putExtra("com.manning.aip.mash.EXTRA_PHOTO",
photoUri0);
startActivityForResult(request, 1);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == 1){
Uri photoUri1 = (Uri) data.getParcelableExtra(
"com.manning.aip.mash.EXTRA_RESULT");
ImageView imgView1 = (ImageView) findViewById(R.id.pic1);
imgView1.setImageURI(photoUri1);
}
}
}

Kod z listingu 8.2 jest podobny do kodu z listingu 8.1. Wykorzystaliśmy tu ten
sam wzorzec, jednak zamiast używać znanej akcji, stosujemy akcję niestandar-
dową i przekazujemy intencję do aplikacji ImageMash. Tu chcemy współ-
użytkować dane z inną aplikacją, dlatego dodajemy dodatkowe informacje do
intencji . Owa „inna aplikacja” (ImageMash) musi znać nazwę dodatkowych
informacji, aby mogła pobrać je z intencji . Kiedy druga aplikacja zwraca ste-
rowanie, można pobrać zmodyfikowany rysunek z przekazanej intencji. Zobaczmy,
co się dzieje w aplikacji ImageMash.

Komunikacja międzyprocesowa i obiekty typu Parcelable


Może zauważyłeś, że na listingach 8.1 i 8.2 użyliśmy metody getParcelableExtra
obiektu intencji. Jeśli przeczytałeś rozdział 5., znasz już obiekty typu Parcelable.
Jest to odpowiednik typu Serializable Javy. Każdy szeregowany obiekt (zarówno
przekazywany między procesami, jak i zapisywany na dysku) musi być typu Parce
´lable. W przykładach przekazujemy między procesami obiekt android.net.Uri.
Jest to możliwe, ponieważ obiekt ten jest typu Parcelable. Także niestandardowe
obiekty mogą być tego typu. Z rozdziału 5. dowiesz się, jak tworzyć takie obiekty.

Aby inna aplikacja mogła ją wywołać, w intencji trzeba użyć akcji com.manning.aip.
´mash.ACTION (tak jak na listingu 8.2). Oznacza to, że należy zastosować filtr
intencji:
<activity android:name=".MashActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="com.manning.aip.mash.ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

Teraz trzeba dodać obsługę odebranych danych w aktywności. Na listingu 8.3


pokazujemy, jak to zrobić.
334 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

Listing 8.3. Przekształcanie odebranego rysunku

public class MashActivity extends Activity {


public static final String EXTRA_PHOTO =
"com.manning.aip.mash.EXTRA_PHOTO";
private static final int RESULT_ERROR = 99;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Intent request = getIntent();
if (request != null &&
request.hasExtra(EXTRA_PHOTO)){
final Uri uri =
(Uri) request.getParcelableExtra(EXTRA_PHOTO);
ImageView image = (ImageView) findViewById(R.id.image);
image.setImageURI(uri);

Button button = (Button) findViewById(R.id.button);


button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
try {
Bitmap bmp = BitmapFactory.decodeStream(
getContentResolver().openInputStream(uri));
Bitmap mashed = mash(bmp);
Uri resultUri = saveImage(mashed);
Intent response = new Intent();
response.putExtra(
"com.manning.aip.mash.EXTRA_RESULT",
resultUri);
MashActivity.this.setResult(Activity.RESULT_OK,
response);
} catch (FileNotFoundException e) {
Log.e("MashActivity", "Wyjątek przy przekształcaniu", e);
MashActivity.this.setResult(RESULT_ERROR);
}
finish();
}
});

}
}
}

Na listingu 8.3 znajduje się kod głównej aktywności aplikacji ImageMash. Kod
pobiera identyfikator Uri modyfikowanego rysunku przez wyodrębnienie tego
identyfikatora z intencji użytej do uruchomienia aktywności. Następnie, kiedy
użytkownik określi wartości i naciśnie przycisk, aktywność wczytuje rysunek ,
przekształca go i zapisuje z powrotem na karcie SD, używając metody saveImage
(pominiętej na listingu). Metoda ta zwraca identyfikator Uri zmodyfikowanego
rysunku. Aplikacja zapisuje identyfikator w nowej intencji , przekazywanej
z powrotem do jednostki, która wywołała daną aktywność. Potem wywoływana
jest metoda setResult aktywności . Aplikacja informuje w ten sposób, że wynik
0 TECHNIKA 37. Zdalne wywołania procedur 335

jest prawidłowy, i przekazuje intencję z powrotem do jednostki wywołującej.


Ostatecznie aplikacja wywołuje metodę finish wykonywanej aktywności i zwraca
w ten sposób sterowanie do wywołującej aktywności .
OMÓWIENIE
W tej przykładowej aplikacji pokazujemy, jak używać intencji do przekazywania
danych między aplikacjami. Pokazaliśmy, jak stosować jedną z wielu standar-
dowych intencji do przekazywania informacji tam i z powrotem między pod-
stawowymi aplikacjami (takimi jak Gallery) oraz niestandardowymi programami.
Intencje dają dużo możliwości. Nie musisz znać nazwy lub klasy aktywności,
z którą chcesz zintegrować aplikację. Potrzebna jest jedynie nazwa akcji (i opcjo-
nalnie jej kategoria). Czasem niezbędne są też nazwy i typy parametrów ocze-
kiwanych w danych wejściowych i wykorzystywanych w danych wyjściowych.
Wszystkie te mechanizmy są zgodne z branżowymi standardami tworzenia luźno
powiązanych systemów.
Możesz się zastanawiać, co się dzieje, jeśli obsługę konkretnej akcji zade-
klarowano w kilku aplikacjach. Wtedy system operacyjny Androida wyświetla
użytkownikowi okno dialogowe umożliwiające wybór aplikacji. Oznacza to, że
inne aplikacje mogą „przejąć” intencję przeznaczoną dla konkretnego programu.
Jeśli jest to nieakceptowalne, można zastosować inną technikę współużytkowania
danych, na przykład zdalne wywołania procedur.

0 TECHNIKA 37. Zdalne wywołania procedur

Ważną cechą poprzedniej techniki jest to, że aplikacja przekazuje sterowanie


innemu programowi i czeka na to, aż użytkownik przestanie z niego korzystać.
Rozwiązanie to nie jest odpowiednie, jeśli sterowanie ma pozostać w danej apli-
kacji. Wtedy warto zastosować jedną z wersji zdalnych wywołań procedur. Tu
omawiamy dwie obsługiwane w Androidzie odmiany zdalnych wywołań proce-
dur — synchroniczne i asynchroniczne.
PROBLEM
Chcemy przekazać dane do innego programu, aby mógł wykonać pewne ope-
racje na danych i zwrócić wynik do głównej aplikacji. Nie chcemy jednak prze-
kazywać sterowania do innego programu. Interakcje z nim mają pozostać nie-
zauważone.
ROZWIĄZANIE
Najważniejszą cechą tej techniki jest pozostawienie sterowania w głównej apli-
kacji. Komunikuje się ona z innym programem i współużytkuje z nim dane, jednak
proces ten jest niedostrzegalny dla użytkownika. Podstawową technologią są tu
usługi Androida. Występują one w dwóch odmianach. W jednej z nich interakcje
odbywają się synchronicznie, a w drugiej — asynchronicznie. W zmodyfikowanej
aplikacji GoodShares przedstawiamy obie odmiany usług. W tym podpunkcie
336 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

tworzymy nową wersję aktywności z rysunku 8.3,


w której wykorzystujemy zdalne wywołania pro-
cedur (ang. remote procedure call — RPC). Na
rysunku 8.6 pokazujemy, jak działa aktywność
oparta na wywołaniach RPC.
Najważniejsza różnica między interfejsami
użytkownika pokazanymi na rysunkach 8.3 i 8.6
polega na tym, że w nowej wersji można podać
skalę dotyczącą osi X i Y oraz kąt rotacji, zamiast
określać te informacje w aplikacji ImageMash.
Po ustaleniu danych aktywność wywołuje apli-
kację ImageMash, aby przekształciła rysunek.
Po otrzymaniu wyniku aktywność wyświetla
rysunek użytkownikowi. Aby technika działała,
aplikacja ImageMash musi udostępniać usługę,
którą można wywołać w aktywności aplikacji
GoodShares z rysunku 8.6. Niezbędną integra- Rysunek 8.6.
cję można osiągnąć na kilka sposobów. Współużytkowanie
z wykorzystaniem
wywołań RPC
Integracja synchroniczna
Integracja pierwszego rodzaju przebiega synchro-
nicznie. Aplikacja GoodShares wywołuje usługę z programu ImageMash i ocze-
kuje na odpowiedź, a po jej otrzymaniu aktualizuje interfejs użytkownika. Aby
interakcja tego rodzaju była możliwa, aktywność aplikacji GoodShares trzeba
powiązać z usługą i bezpośrednio wywoływać w tej aktywności operacje usługi.
Odpowiedzialny za to kod przedstawiamy na listingu 8.4.

Listing 8.4. Synchroniczne wywoływanie usługi innej aplikacji

public class ShareRpcActivity extends Activity {


Uri photoUri0;
IMashService mashService;
Button mashButton;
int bindCount = 0;
ServiceConnection conn = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName
className, IBinder service) {
mashService = IMashService.Stub.asInterface(service);
mashButton.setEnabled(true);
}
@Override
public void onServiceDisconnected(ComponentName className) {
mashService = null;
mashButton.setEnabled(false);
}
};

@Override
0 TECHNIKA 37. Zdalne wywołania procedur 337

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.share_rpc);
mashButton = (Button) findViewById(R.id.button);
CheckBox syncBox = (CheckBox) findViewById(R.id.syncBox);
syncBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
@Override
public void onCheckedChanged(CompoundButton button,
boolean checked) {
if (checked){
mashButton.setEnabled(false);
bindService(new Intent("com.manning.aip.mash.ACTION"),
conn,
BIND_AUTO_CREATE);
bindCount += 1;
} else {} }}
);
mashButton.setOnClickListener(new OnClickListener(){

@Override
public void onClick(View v) {
EditText input0 = (EditText) findViewById(R.id.input0);
float scaleX =
Float.parseFloat(input0.getText().toString());
EditText input1 = (EditText) findViewById(R.id.input1);
float scaleY =
Float.parseFloat(input1.getText().toString());
EditText input2 = (EditText) findViewById(R.id.input2);
float angle =
Float.parseFloat(input2.getText().toString());
Uri result;
if (bindCount > 0){
try {
result = mashService.mash(photoUri0,
scaleX,
scaleY,
angle);
ImageView image =
(ImageView) findViewById(R.id.image);
image.setImageURI(result);
} catch (RemoteException e) {} }}});}}

W kodzie z listingu 8.4 pokazujemy, jak za pomocą usług w synchroniczny sposób


pobierać dane i przesyłać je między dwoma aplikacjami. Na początek potrzebny
jest interfejs reprezentujący zdalną usługę i opisujący udostępniane przez
nią operacje . Do opisu interfejsu zdalnej usługi używamy przedstawionego
w rozdziale 5. języka AIDL. Oto kod w AIDL-u opisujący usługę z aplikacji
ImageMash:
package com.manning.aip.mash;

import android.net.Uri;

interface IMashService{
Uri mash(in Uri uri, float scaleX, float scaleY, float angle);
}
338 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

To prawie czysta Java! W aplikacji niezbędna jest kopia tego pliku AIDL, a narzę-
dzia Androida generują namiastkę tego pliku, którą można podawać w aplikacji.
To bardzo ważny aspekt techniki — aby móc w synchroniczny sposób współ-
użytkować dane z usługą innej aplikacji, w danym programie trzeba utworzyć
plik AIDL z opisem tej usługi. Jeśli chcesz umożliwić innym aplikacjom inte-
grację z usługą z danego programu, także musisz udostępnić plik AIDL.
Wróćmy do listingu 8.4. W aktywności potrzebne jest też połączenie typu
ServiceConnection ze zdalną usługą . Połączenie to pełni funkcję interfejsu do
wywołań zwrotnych i pozwala stwierdzić, że aktywność powiązała się z usługą
oraz że można bezpiecznie zacząć wywoływać operacje tej usługi. W przykładzie
proces wiązania inicjujemy w metodzie onCreate aktywności. Udostępniamy
pole wyboru do określania, czy komunikacja ma przebiegać synchronicznie. Jeśli
tak jest, kliknięcie przycisku Przekształć! powoduje wywołanie usługi . Ponie-
waż wywołanie jest synchroniczne, zwraca odpowiedź, a interfejs użytkownika
jest natychmiast aktualizowany .
W tym miejscu tylko poruszyliśmy temat usług i języka AIDL oraz powią-
zane zagadnienia. Znacznie więcej informacji znajdziesz w rozdziale 5. Warto
zauważyć, że do synchronicznego współużytkowania danych nie używamy intencji.
Zamiast nich wykorzystujemy plik AIDL, co ma wyraźne zalety związane z okre-
ślaniem nazw i typów współużytkowanych danych.
Integracja asynchroniczna
Zamiast wywołań synchronicznych można stosować wywołania asynchroniczne
oparte na intencjach. Na listingu 8.5 pokazujemy asynchroniczną wersję kodu.

Listing 8.5. Asynchroniczne wywoływanie zdalnej usługi

public class ShareRpcActivity extends Activity {


Button mashButton;
int bindCount = 0;

@Override
protected void onCreate(Bundle savedInstanceState) {
mashButton = (Button) findViewById(R.id.button);
mashButton.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
// Pobieranie danych z kontrolek.
if (bindCount > 0){ // Wywołanie synchroniczne.
} else {
Intent request =
new Intent("com.manning.aip.mash.ACTION");
request.putExtra("com.manning.aip.mash.EXTRA_PHOTO",
photoUri0);
request.putExtra("com.manning.aip.mash.EXTRA_SCALE_X",
scaleX);
request.putExtra("com.manning.aip.mash.EXTRA_SCALE_Y",
scaleY);
0 TECHNIKA 37. Zdalne wywołania procedur 339

request.putExtra("com.manning.aip.mash.EXTRA_ANGLE",
angle);
startService(request);
}
}
});
mashButton.setEnabled(true);
BroadcastReceiver receiver = new BroadcastReceiver(){
@Override
public void
onReceive(Context context, Intent intent) {
Uri result = intent.getParcelableExtra(
"com.manning.aip.mash.EXTRA_RESULT");
ImageView image = (ImageView) findViewById(R.id.image);
image.setImageURI(result);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction("com.manning.aip.mash.ACTION_RESPONSE");
registerReceiver(receiver, filter);
}
}

Rozwiązanie asynchroniczne rozpoczyna się od utworzenia intencji i określenia


jej akcji . Akcja, podobnie jak w intencjach przesyłanych do aktywności, jest
używana do skierowania intencji do odpowiedniej usługi. Także podobnie jak
w intencjach z techniki 36., współużytkowane dane przesyłamy za pomocą metody
putExtra .
Oznacza to, że trzeba znać nazwy i typy dodatkowych informacji. Usługa
pobiera te dane, dlatego musi wiedzieć, czego ma oczekiwać. Po poprawnym
utworzeniu intencji wywołujemy metodę startService (zamiast używanej w tech-
nice 36. metody startActivityForResult). Jest to wywołanie asynchroniczne.
Usługa odbiera to wywołanie i reaguje przez rozsyłanie intencji obejmującej
odpowiedź. Do odbierania odpowiedzi używamy metody onReceive odbiornika .
Metoda ta pełni funkcję wywołania zwrotnego w asynchronicznym wywołaniu
usługi. Aplikacja otrzymuje intencję wysłaną przez usługę i wyodrębnia dane
odpowiedzi . Służą one do zaktualizowania interfejsu użytkownika. Aby odbior-
nik mógł otrzymać intencję wysłaną przez usługę, trzeba zarejestrować go za
pomocą filtru intencji i akcji używanej przez usługę . Kod usługi przedstawiamy
na listingu 8.6.

Listing 8.6. Obsługa intencji w usłudze aplikacji ImageMash

public class MashService extends Service {

@Override
public int onStartCommand(Intent intent,
int flags, int startId) {
Uri imageUri =
intent.getParcelableExtra("com.manning.aip.mash.EXTRA_PHOTO");
float scaleX =
340 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

intent.getFloatExtra("com.manning.aip.mash.EXTRA_SCALE_X", 1.0f);
float scaleY =
intent.getFloatExtra("com.manning.aip.mash.EXTRA_SCALE_Y", 1.0f);
float angle =
intent.getFloatExtra("com.manning.aip.mash.EXTRA_ANGLE", 0.0f);
try {
Uri resultUri = stub.mash(imageUri, scaleX, scaleY, angle);
Intent response =
new Intent("com.manning.aip.mash.ACTION_RESPONSE");
response.putExtra("com.manning.aip.mash.EXTRA_RESULT",
resultUri);
sendBroadcast(response);
} catch (RemoteException e) {
Log.e("MashService", "Wyjątek przy asynchronicznym przekształcaniu", e);
}
return START_STICKY;
}

Jest to fragment kodu usługi MashService. Pokazujemy tylko część, która prze-
twarza przychodzące intencje, takie jak te z listingu 8.5. Intencje te są obsługi-
wane przez metodę onStartCommand . Tu wysyłane są intencje tylko jednego
rodzaju. Gdyby było ich więcej, moglibyśmy sprawdzać akcję, aby ustalić typ
żądania. Po ustaleniu typu żądania można pobrać z intencji odpowiednie dane .
Po przetworzeniu danych i przygotowaniu odpowiedzi należy utworzyć nową
intencję, która posłuży do jej odesłania . Aby intencja trafiła do odpowiedniego
odbiornika, należy podać w niej odpowiednią akcję. Następnie trzeba dodać do
intencji wszystkie potrzebne dane. Służy do tego znana już metoda putExtra .
Ostatecznie intencja jest rozsyłana za pomocą metody sendBroadcast.
OMÓWIENIE
Występują pewne oczywiste i poważne różnice między synchronicznym a asyn-
chronicznym sposobem wymiany danych z usługami z innych aplikacji. Ważna
jest z pewnością odmienna natura komunikacji synchronicznej i asynchronicz-
nej. Istotny jest też interfejs. W trybie synchronicznym interfejs jest bezpo-
średnio zdefiniowany w pliku AIDL. Dokładnie wiadomo, jak wywołać usługę,
a odpowiedź jest zwracana natychmiast (w tym sensie, że wątek zostaje zablo-
kowany do czasu otrzymania odpowiedzi przez usługę; bądź ostrożny przy stoso-
waniu tej techniki w głównym wątku interfejsu użytkownika). W podejściu asyn-
chronicznym nic nie jest bezpośrednio określone. Niezbędna jest znajomość
nazw i typów danych oczekiwanych oraz generowanych przez usługę, jednak
informacje te nie są podawane w kodzie w języku AIDL. Takie rozwiązanie jest
bardziej narażone na błędy. Ponadto trzeba znać nazwę akcji używanej do wysy-
łania danych do usługi, a także nazwę akcji wykorzystywanej do rejestrowania
odbiornika, który pobiera odpowiedź od usługi.
Tryby synchroniczny i asynchroniczny się nie wykluczają. Załóżmy, że udo-
stępniamy usługę w podejściu synchronicznym z wykorzystaniem języka AIDL,
ale działanie jednej z operacji zajmuje dużo czasu. Wtedy aktywność można
0 TECHNIKA 38. Współużytkowanie danych (i innych elementów) przez współdzielenie kontekstu 341

powiązać z usługą za pomocą obiektu klasy AsyncTask lub podobnego mechani-


zmu, aby nie blokować wątku interfejsu użytkownika w czasie działania usługi.
Wtedy długi czas wykonywania operacji nie jest szkodliwy. Inna możliwość to
zwrócenie komunikatu z informacją, że aplikacja otrzymała żądanie, jednak odpo-
wiedź częściowo lub w całości nadejdzie później. Usługa może wówczas roze-
słać intencję z dodatkowymi danymi, kiedy staną się one dostępne. Technikę tę
często stosuje się w sytuacji, kiedy usługa obejmuje lokalną pamięć podręczną
na dane, które znajdują się w chmurze.
Poznałeś kilka przydatnych sposobów współużytkowania danych między
aplikacjami za pośrednictwem intencji. Techniki te można zastosować w dwóch
dowolnych aplikacjach, które mają tylko ograniczone informacje na swój temat
(znają swoje akcje, dodatkowe dane itd.). Jeśli jednak aplikacje mają dokładniejsze
informacje o sobie, można zastosować inną technikę. Aplikacje mogą wtedy współ-
użytkować kontekst.

0 TECHNIKA 38. Współużytkowanie danych (i innych elementów)


przez współdzielenie kontekstu
Rozdział ten dotyczy przede wszystkim współużytkowania danych między pro-
cesami, dlatego technika ta teoretycznie do niego nie pasuje, ponieważ polega
na współużytkowaniu danych między aplikacjami w jednym procesie. Technika
ta opiera się na współużytkowaniu jednego procesu przez wiele aplikacji. Dla-
czego stosować to rozwiązanie? Wspomnieliśmy wcześniej, że każda aplikacja
na Android ma niepowtarzalny linuksowy identyfikator użytkownika i że dla
każdej uruchamianej aplikacji tworzony jest odrębny proces systemowy. Ponadto
wyjaśniliśmy, że podejście to ma zapewniać bezpieczeństwo przez rozdzielenie
kodu i zasobów poszczególnych niepowiązanych ze sobą aplikacji.
Czasem opisane podejście może utrudniać pracę. Przypomina to trochę sytu-
ację, w której możesz wejść do swojej kuchni, ale nie masz wstępu do salonu.
Są to odrębne pomieszczenia w Twoim domu i powinieneś mieć możliwość
swobodnego przemieszczania się! Co możemy zrobić, jeśli utworzymy dwie
zależne od siebie aplikacje, które muszą współużytkować prywatny plik konfi-
guracyjny lub nawet kod? Wyobraź sobie, że obok jednej aplikacji chcesz udo-
stępnić drugą, z panelem kontrolnym dla programisty, umożliwiającą kontrolo-
wanie wewnętrznych ustawień głównej aplikacji. Za pomocą drugiej aplikacji
można kontrolować na przykład to, czy główny program łączący się z interfejsem
API usługi sieciowej powinien kontaktować się z serwerami produkcyjnymi,
czy z serwerami używanymi na etapie rozwijania programu. Ustawienie to nie
powinno być dostępne w gotowej aplikacji, dlatego warto umieścić je w drugim
programie. Nie występuje tu zagrożenie bezpieczeństwa. Sam napisałeś obie
aplikacje i ufasz własnemu kodowi, prawda? Jak można obejść ograniczenia plat-
formy w kontrolowany sposób?
342 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

PROBLEM
Rozwijamy dwie ściśle powiązane i zależne od siebie aplikacje (lub większą ich
liczbę). Chcemy współużytkować prywatne zasoby, na przykład pliki lub kod,
które nie mogą być dostępne dla innych programów. Z uwagi na obowiązujące
w Androidzie ścisłe reguły bezpieczeństwa nie możemy uzyskać pożądanych
efektów.
ROZWIĄZANIE
Wcześniej stwierdzono, że nie możemy uzyskać pożądanych skutków z dwóch
powodów. Oto one:
1. Poszczególne aplikacje działają w różnych linuksowych procesach
systemowych.
2. Poszczególne aplikacje mają odmienne linuksowe identyfikatory
użytkownika.
Rozwiązanie polega na współużytkowaniu przez oba programy procesu aplikacji
i identyfikatora użytkownika. Załóżmy, że istnieją dwie aplikacje. Dla uprosz-
czenia przyjmijmy, że ich nazwy to App1 i App2. W aplikacji App2 chcemy
wykorzystać zasoby z aplikacji App1. Ujmijmy do dokładniej — chcemy wczy-
tywać klasy z pakietu aplikacji App1 (pliku APK tego programu) i wczytywać
ustawienia zapisane przez aplikację App1 w obiektach typu SharedPreferences.
Staramy się zachować prostotę, aby nie komplikować problemu. Dlatego apli-
kacja App1 działa w następujący sposób: zapisuje krótki fragment tekstu w pliku
ustawień, który ma współużytkować z programem App2, a także obejmuje nie-
standardową metodę toString, możliwą do wywołania w aplikacji App2. Wypró-
buj przykładowy projekt. Zauważ, że aplikacja App2 może wczytać dane standar-
dowo dostępne tylko w programie App1 (rysunek 8.7).
POBIERZ PROJEKTY. Kod źródłowy pro-
jektów i pakiety APK do uruchamiania apli-
kacji znajdziesz w witrynie z kodem do
książki Android w praktyce. Ponieważ nie-
które listingi skrócono, abyś mógł skoncen-
trować się na konkretnych zagadnieniach,
zalecamy pobranie kompletnego kodu źró-
dłowego i śledzenie go w Eclipse (lub innym
środowisku IDE albo edytorze tekstu).
Zauważ, że tym razem dostępne są dwie przykładowe aplikacje ściśle
powiązane ze sobą. Aby uzyskać pożądany efekt, najpierw uruchom apli-
kację SharedProcessApp1, a potem SharedProcessApp2.
Źródło: http://mng.bz/x5a0, http://mng.bz/5141.
Plik APK: http://mng.bz/16sP, http://mng.bz/CXgT.
0 TECHNIKA 38. Współużytkowanie danych (i innych elementów) przez współdzielenie kontekstu 343

Rysunek 8.7. Aplikacja App1 współużytkuje kod i ustawienia z programem


App2. Jest to możliwe dzięki androidowemu modelowi współużytkowanych
procesów i identyfikatorów użytkownika

W tym scenariuszu App1 jest dostawcą danych, dlatego najpierw przedstawiamy


implementację tej aplikacji (listing 8.7).

Listing 8.7. Plik App1.java obejmuje kod metody toString() i zapisuje plik
ze współużytkowanymi ustawieniami

public class App1 extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

SharedPreferences prefs = getSharedPreferences(


"app1prefs", MODE_PRIVATE);
String value = "Powitania z pliku ustawień aplikacji App1!";
prefs.edit().putString("shared_value", value).commit();
}

@Override
public String toString() {
return "Powitania z metody toString() aplikacji App1!";
}
}

Aplikacja najpierw tworzy plik ze współużytkowanymi ustawieniami . Jest to


plik konfiguracyjny w formacie XML przechowywany w katalogu z danymi apli-
kacji App1 (rozdział 7.). Do tworzenia tego pliku służy tryb MODE_PRIVATE, co
oznacza, że dostęp do pliku mają tylko komponenty aplikacji App1 (na przykład
344 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

aktywności lub usługi). Następnie aplikacja zapisuje w tym pliku wartość o kluczu
shared_value . Na listingu znajduje się też niestandardowa implementacja
metody toString .
Na razie wszystko wygląda dobrze. Aplikacja tworzy plik współużytkowa-
nych ustawień, który jednak jest dostępny tylko w programie App1, ponieważ
jest tworzony w systemie plików za pomocą linuksowego identyfikatora użytkow-
nika powiązanego z tym programem. Ponadto aplikacja tworzy plik w trybie pry-
watnym, dlatego tylko użytkownik (aplikacja) ma dostęp do danych. Plik można
też utworzyć w trybie umożliwiającym odczyt danych dowolnej aplikacji. Dla
ciekawych przedstawiamy w tabeli 8.1, jakie maski uprawnień odpowiadają
poszczególnym trybom (jeśli chcesz przypomnieć sobie, jak Linux obsługuje
uprawnienia do plików, wróć do rozdziału 1.).
Tabela 8.1. Tryby tworzenia plików typu SharedPreferences

Maska uprawnień Maska uprawnień


Tryb*
(użytkownik-grupa-inni) (w kodzie ósemkowym)

MODE_PRIVATE rw-rw---- 660


MODE_WORLD_READABLE rw-rw-r-- 664
MODE_WORLD_WRITEABLE rw-rw--w- 662
* Tryb jest maską bitową. Opcje można łączyć za pomocą bitowego operatora OR ('|').

Te same uprawnienia dotyczą metody toString, generującej miłe powitanie.


Metoda ta jest ukryta w ładowarce klas aplikacji App1, dlatego jest niewidoczna
dla innych programów. Z dotychczasowych obserwacji wynika, że trzeba roz-
wiązać dwa problemy, aby aplikacja App2 mogła wywoływać metodę toString
programu App1 i miała dostęp do jej pliku ustawień. Oto te problemy:
1. Musimy uzyskać dostęp do zasobów z pakietu aplikacji App1. Wywołanie
metody getResources w programie App2 zwraca zasoby tylko z tego
programu.
2. Musimy uzyskać prawo do dostępu do zasobów. Nawet jeśli uda nam się
znaleźć sposób na dotarcie do nich, nadal są to zasoby aplikacji App1,
a nie programu App2.
Rozwiązaniem pierwszego problemu jest metoda createPackageContext z klasy
android.content.Context. Metoda ta tworzy uchwyt do obiektu kontekstu, który
reprezentuje pakiet aplikacji innej niż aktualnie używana. Za pomocą zwróco-
nego przez metodę obiektu kontekstu można pobrać referencję do ładowarki
klasy i wczytywać klasy z innej aplikacji oraz tworzyć ich egzemplarze. Obiekt
kontekstu pozwala też uzyskać uchwyt do pakietu zasobów lub współużytko-
wanych ustawień. Kod źródłowy, w którym używana jest wspomniana metoda,
przedstawiamy na listingu 8.8.
0 TECHNIKA 38. Współużytkowanie danych (i innych elementów) przez współdzielenie kontekstu 345

Listing 8.8. W pliku App2.java za pośrednictwem metody createPackageContent()


uzyskujemy dostęp do kontekstu aplikacji App1

public class App2 extends Activity {

private Context app1;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

try {
app1 = createPackageContext(
"com.manning.aip.app1", CONTEXT_INCLUDE_CODE);
Class<?> app1ActivityCls =
app1.getClassLoader()
.loadClass("com.manning.aip.app1.App1");
Object app1Activity = app1ActivityCls.newInstance();
Toast.makeText(this, app1Activity.toString(),
Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
return;
}

SharedPreferences prefs =
app1.getSharedPreferences("app1prefs", MODE_PRIVATE);
TextView view =
(TextView) findViewById(R.id.hello);
String shared = prefs.getString("shared_value", null);
if (shared == null) {
view.setText("Błąd współużytkowania!");
} else {
view.setText(shared);
}
}
}

Najpierw zapisujemy referencję do aplikacji App1. Ta referencja to obiekt typu


Context . Następnie można uzyskać dostęp do ładowarki klas aplikacji App1,
tworzyć egzemplarze tych klas i wywoływać ich metody tak, jakby były częścią
danej aplikacji ( i ). W podobny sposób można użyć zasobów lub współużyt-
kowanych ustawień. Także do nich można uzyskać dostęp poprzez kontekst
zewnętrznej aplikacji ( i ).
Wygląda na to, że kod z listingu 8.8 pozwala uporać się z pierwszym pro-
blemem, jednak na razie rozwiązanie nie działa. Powód jest oczywisty — mamy
jeszcze jeden problem na liście. Aplikacja App2 nie ma uprawnień do wykonania
potrzebnych operacji. Problem można rozwiązać, wykorzystując dwa atrybuty
ustawiane w plikach manifestu obu aplikacji. Te atrybuty to android:process
i android:sharedUserId. Pierwszy pozwala określić proces systemowy, w którym
aplikacja ma działać (ustala się w ten sposób pokrewieństwo procesów). Drugi
346 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

informuje Android, jakiego linuksowego identyfikatora użytkownika ma użyć do


zainstalowania aplikacji i utworzenia dla niej plików. Odpowiadające sobie atry-
buty w obu aplikacjach muszą mieć tę samą wartość.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="..."
android:sharedUserId="com.manning.aip">

<application android:process="com.manning.aip">
<activity … />
</application>

</manifest>

Spróbuj ponownie uruchomić aplikacje i sprawdź, czy rozwiązanie działa popraw-


nie. Powinieneś zobaczyć, że zarówno komunikat typu Toast, jak i widok tek-
stowy w aplikacji App2 są aktualizowane wartościami z pakietu aplikacji App1.
Działanie rozwiązania można sprawdzić także przez przełączenie się do perspek-
tywy DDMS (lub uruchomienie programu adb shell ps) i ustalenie, że system
tworzy tylko jeden proces, nawet jeśli obie aplikacje działają w tym samym
momencie!
OMÓWIENIE
Jak się prawdopodobnie domyślasz, otwiera to nowe możliwości. Atrybut android:
´process można zdefiniować zarówno dla komponentów, jak i dla elementu
<application>. Można swobodnie określać wartość tego atrybutu. Nie tylko
pozwala on wykonywać dwie aplikacje w jednym procesie, co zrobiliśmy w tej
technice, ale też uruchomić każdą usługę lub aktywność aplikacji w odrębnym
procesie systemowym. Przed przejściem do wniosków chcemy jednak odradzić
stosowanie tego podejścia. Podtrzymywanie procesów systemowych jest jeszcze
kosztowniejsze niż zarządzanie wątkami. Każdy proces wymaga uruchomienia
odrębnego egzemplarza maszyny wirtualnej Dalvik, co zwiększa zużycie pamięci
i energii.
Spostrzegawcze osoby mogły też zauważyć poważne ograniczenie w sposo-
bie współużytkowania kodu. Problem z opisanym podejściem polega na tym, że
w zakresie tworzenia egzemplarzy klas trzeba polegać na interfejsie API mecha-
nizmu refleksji z Javy. Jest to bardzo ważne — klasy z aplikacji App1 nie znajdują
się w ścieżce klas aplikacji App2. Powoduje to, że nie można rzutować obiektów
klas z aplikacji App1 na typy inne niż Object! Wywołanie Class<T>.newInstance()
zwraca obiekt klasy Object, który trzeba zrzutować w dół, aby wywołać metody
spoza tej klasy. Problem ten można rozwiązać na dwa sposoby. Jednym z nich
jest zdefiniowanie zestawu interfejsów Javy, które należy spakować do pliku JAR
i zaimplementować we współużytkowanych klasach. Plik JAR z interfejsami
można następnie umieścić w obu aplikacjach, co pozwala w programie App2
rzutować obiekty na typy z interfejsu i wywoływać implementacje z programu
8.2. Dostęp do niestandardowych danych 347

App1. Drugie rozwiązanie, przydatne zwłaszcza przy korzystaniu z wielu wywo-


łań funkcji z zewnętrznych aplikacji, to zastosowanie przedstawionego w tech-
nice 37. mechanizmu RPC Androida.
Należy wspomnieć o jeszcze jednej pułapce. Jeśli planujesz współużytko-
wać kod lub zasoby z wykorzystaniem tego podejścia, zaprojektuj aplikacje tak,
aby od początku obsługiwały opisaną technikę. Jeśli już udostępniasz w sklepie
Android Market aplikację bez zdefiniowanego niestandardowego pokrewieństwa
procesów, nie możesz dodać aktualizacji, w której zastosowane są inny proces
i inny identyfikator użytkownika. Użytkownicy muszą ręcznie odinstalować starszą
wersję, ponieważ mechanizm aktualizacji ze sklepu Android Market nie usuwa
plików ustawień i baz danych utworzonych przez aplikacje (chroni to przed utratą
danych użytkownika); zmodyfikowana wersja nie może zapisywać informacji
z tych plików. Warto zatem we wszystkich aplikacjach definiować atrybut android:
´process. Pozwoli to w przyszłości swobodnie dodać do zestawu zgodne aplikacje!
Opisaliśmy różne sposoby interakcji z innymi aplikacjami w celu przesyłania
i odbierania danych. Teraz warto przyjrzeć się nieco prostszemu zagadnieniu.
Zobaczmy, jak udostępniać dane, do których można dotrzeć w sposób bezpośredni,
niskopoziomowy.

8.2. Dostęp do niestandardowych danych


Do tej pory koncentrowaliśmy się na bezpośredniej interakcji aplikacji z inną
aplikacją. Używaliśmy do tego albo intencji (do komunikowania się z aktywno-
ściami i usługami innej aplikacji), albo wczytanego obiektu typu Context innego
programu (obiekt ten zapewnia dostęp do prywatnych danych, a nawet kodu
takiego programu). Integrację opartą na intencjach (i kodzie w AIDL-u) można
traktować jako integrację na poziomie interfejsu. Podejście to często stosuje się
w architekturach usługowych. Podejście oparte na współużytkowanym kontekście
to integracja na poziomie binarnym. Innym modelem integrowania aplikacji jest
integracja na poziomie danych, przypominająca technikę nazwaną przez Martina
Fowlera integracyjną bazą danych (ang. integration database). Jeśli wszystkie apli-
kacje wczytują i zapisują dane w tym samym magazynie, można uniknąć pisania
nadrzędnego kodu do zarządzania integracją. Ten model integracji dobrze spraw-
dza się w Androidzie, ponieważ na platformie tej dostępna jest baza SQLite. Przyj-
rzyjmy się temu podejściu. Zacznijmy od tego, jak używać standardowych integra-
cyjnych baz danych dostępnych w każdym urządzeniu z Androidem.

0 TECHNIKA 39. Korzystanie ze standardowych dostawców treści

Integracyjna baza danych nie jest pomysłem opracowanym na potrzeby tej


książki. Nie jest to nawet wzorzec oparty na aplikacjach niezależnych progra-
mistów. Mechanizm ten jest wbudowany w sam Android. Zastosowano go nie
tylko w wielu wbudowanych aplikacjach z Androida, ale też w samym pakiecie
348 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

SDK znajduje się interfejs API do korzystania z integracyjnych baz danych i ich
tworzenia. Tym interfejsem jest klasa abstrakcyjna android.content.ContentProvider.
Pakiet obejmuje też kilka implementacji klasy ContentProvider. Służą one do
wykonywania wielu standardowych zadań w Androidzie. Omawianie klas Content
´Provider zaczynamy od przyjrzenia się temu, jak wykorzystać jednego ze stan-
dardowych dostawców treści z Androida — dostawcę kontaktów.
PROBLEM
Chcemy wyszukiwać dane kontaktowe jednej lub kilku osób w książce adresowej
użytkownika. Potrzebujemy też szczegółowych informacji na temat konkretnej
osoby z tej książki.
ROZWIĄZANIE
Tworzymy tu prostą przykładową aplikację, która imituje proces rejestracji.
Użytkownik ma podać imię i nazwisko, numer telefonu i adres e-mail, aby zareje-
strować się w usłudze. Proces ten ma być jak najprostszy. Możliwe, że wszystkie
potrzebne informacje znajdują się w książce adresowej. Dlatego pomysł polega
na wyszukiwaniu takich danych i wyświetlaniu podpowiedzi w trakcie wprowa-
dzania informacji. Na rysunku 8.8 pokazujemy, jak może to wyglądać.
Na rysunku 8.8 widać, że w trakcie wprowadzania numeru telefonu przez użyt-
kownika aplikacja pobiera wszystkie pasujące numery z książki adresowej. Użyt-
kownik może dotknąć odpowiedniego numeru, a aplikacja automatycznie uzupeł-
nia wtedy dane. Działanie tego mechanizmu przedstawiono na rysunku 8.9.

Rysunek 8.8. Automatyczne Rysunek 8.9. Automatycznie


sugerowanie danych na podstawie uzupełniony formularz rejestracyjny
numeru telefonu
0 TECHNIKA 39. Korzystanie ze standardowych dostawców treści 349

Jak widać, użytkownik musi wpisać tylko kilka cyfr, a następnie raz dotknąć
ekranu, aby wypełnić formularz rejestracyjny. Aby rozwiązanie działało, trzeba
pobrać dane z bazy kontaktów. Wymaga to użycia dostawcy treści typu Contacts
´Contract. Na następnym listingu pokazujemy, jak użyć tego dostawcy do pobra-
nia listy numerów telefonów wyświetlanej w widocznym na rysunku 8.8 widoku
AutoCompleteTextView.

Listing 8.9. Wyszukiwanie pasujących numerów telefonu

import android.provider.ContactsContract.CommonDataKinds;
public class ContactManager {
private final ContentResolver resolver;

public ArrayList<Contact> findByPhoneSubString(String phoneSubStr){


String[] projection = {Phone.CONTACT_ID, Phone.NUMBER};
String selection = Data.IN_VISIBLE_GROUP + "=1 AND " +
Phone.NUMBER + " LIKE ?";
String[] selectionArgs = {"%" + phoneSubStr + "%"};
if (phoneSubStr == null){
selection = null;
selectionArgs = null;
}
Cursor phoneCursor = null;
ArrayList<Contact> contacts = new ArrayList<Contact>();
try{
phoneCursor = resolver.query(Phone.CONTENT_URI,
projection,
selection,
selectionArgs,
null);
int idCol = phoneCursor.getColumnIndex(Phone.CONTACT_ID);
int numCol = phoneCursor.getColumnIndex(Phone.NUMBER);
while (phoneCursor.moveToNext()){
long id = phoneCursor.getLong(idCol);
String phoneNum = phoneCursor.getString(numCol);
Contact contact = new Contact();
contact.phone = phoneNum;
contact.id = String.valueOf(id);
contacts.add(contact);
}
} finally {
if (phoneCursor != null) phoneCursor.close();
}
return contacts;
}
}

Jeśli korzystałeś kiedyś w kodzie z baz danych, klasa powinna wydać Ci się
prosta. Do zgłoszenia zapytania potrzebny jest obiekt klasy android.content.
´ContentResolver . Po jego przygotowaniu aplikacja programowo tworzy zapy-
tanie. Najpierw należy utworzyć projekcję, w której określane są kolumny pobie-
rane z bazy. Kolumny te podawane są w formie tablicy łańcuchów znaków.
Dostępne łańcuchy znaków są zdefiniowane jako stałe w klasie ContactsContract.
350 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

´CommonDataKinds.Phone. Wzorzec ten często się powtarza przy korzystaniu


z dostawców treści. Nazwy kolumn są definiowane jako stałe. W ten sposób
programiści dokumentują schemat bazy danych. Dalej tworzymy klauzulę Where
zapytania. Przykładowy kod jest dość nietypowy, ponieważ w klauzuli tej uży-
wamy wyrażenia LIKE. Pozwala to pobrać dane wszystkich osób, których numer
telefonu obejmuje wprowadzony łańcuch znaków. W klauzuli Where znajduje się
symbol zastępczy (znak zapytania). Jest on zastępowany argumentami przekaza-
nymi do zapytania. Symbole procentów wokół łańcucha znaków z numerem tele-
fonu oznaczają, że przekazany podłańcuch może znajdować się w dowolnym
miejscu numeru. Następnie za pomocą obiektu klasy ContentResolver można
skierować zapytanie do dostawcy treści . Warto zauważyć, że pierwszym argu-
mentem metody query jest Phone.CONTENT_URI. Jest to następna stała. Określa ona
identyfikator URI. Jeśli lubisz myśleć w kategoriach baz danych, możesz przy-
jąć, że identyfikator określa bazę, schemat i tabelę. Jednoznacznie wskazuje więc
szukane dane. Zauważ też, że ostatni argument w metodzie query ma wartość
null. Parametr ten określa sposób sortowania i zdecydowaliśmy się go pominąć.
W odpowiedzi zapytanie zwraca kursor. Można przejść po kursorze i pobrać
z niego dane. Następnie aplikacja zapisuje wartości w strukturze danych i prze-
kazuje je z powrotem do jednostki, która wywołała metodę. W ten sposób można
uzyskać listę numerów telefonów. Jak widać na rysunku 8.8, numery te określają
sugerowane osoby z listy kontaktów.
DOSTAWCY KONTAKTÓW KIEDYŚ I DZIŚ. W tym przykładzie uży-
wamy dostawcy typu android.provider.ContactsContract. W pakiecie
android.provider znajduje się też dostawca Contacts. Był on używany do
wersji 2.0 Androida. Obecnie jest uznawany za przestarzały, jednak nadal
jest częścią pakietu SDK. Jeśli chcesz, aby aplikacja działała w wersji 1.6
Androida i starszych, a potrzebujesz używać kontaktów, musisz zastosować
obu dostawców. W czasie wykonywania programu możesz sprawdzić
wersję systemu (android.os.Build.VERSION) i wybrać na tej podstawie odpo-
wiedniego dostawcę.
Po wybraniu przez użytkownika numeru z listy podpowiedzi należy uzupełnić
pozostałe dane. Efekt tej operacji przedstawiono na rysunku 8.9, a na listingu 8.10
pokazujemy potrzebny kod.

Listing 8.10. Pobieranie danych kontaktowych

public class ContactManager {


public Contact getContact(Contact partial){
Contact contact = new Contact();
contact.id = partial.id;
contact.phone = partial.phone;
String[] projection = new String[] {StructuredName.GIVEN_NAME,
StructuredName.FAMILY_NAME,
StructuredName.RAW_CONTACT_ID,
0 TECHNIKA 39. Korzystanie ze standardowych dostawców treści 351

StructuredName.CONTACT_ID};
String selection = StructuredName.CONTACT_ID+ " = ? AND " +
Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE +"'";
String[] selectionArgs = new String[] {contact.id};
Cursor nameCursor = null;
try{
nameCursor = resolver.query(Data.CONTENT_URI,
projection,
selection,
selectionArgs,
null);
if (nameCursor.moveToFirst()){
contact.firstName = nameCursor.getString(
nameCursor.getColumnIndex(
StructuredName.GIVEN_NAME));
contact.lastName = nameCursor.getString(
nameCursor.getColumnIndex(
StructuredName.FAMILY_NAME));

}
} finally {
if (nameCursor != null) nameCursor.close();
}
projection = new String[] {Email.DATA1, Email.CONTACT_ID};
selection = Email.CONTACT_ID + " = ?";
Cursor emailCursor = null;
try{
emailCursor = resolver.query(Email.CONTENT_URI,
null,
selection,
selectionArgs,
null);
if (emailCursor.moveToFirst()){
contact.email = emailCursor.getString(
emailCursor.getColumnIndex(Email.DATA1));
}
} finally{
if (emailCursor != null) emailCursor.close();
}
return contact;
}

Metoda najpierw pobiera imię i nazwisko użytkownika . Warto zauważyć, że


dane te są przechowywane w odrębnej tabeli, reprezentowanej przez identyfika-
tor ContactsContract.Data.CONTENT_URI. Jest to generyczna tabela danych obej-
mująca rozmaite dane na temat określonej osoby — w tym imię (GIVEN_NAME)
i nazwisko (FAMILY_NAME). Trzeba podać rodzaj danych potrzebnych dla danej
osoby. W tym celu należy określić w klauzuli Where typ Data.MIMETYPE. Następ-
nie metoda wykorzystuje identyfikator pobrany na listingu 8.9.
Po uzyskaniu imienia i nazwiska można pobrać adres e-mail danej osoby .
Warto zauważyć, że w projekcji (tablicy kolumn bazy) podajemy tu wartość
Email.DATA1. To dość niestandardowe określenie adresu e-mail. W Androidzie 3.0
do typu ContactsContract.CommonDataKinds.Email dodano nową stałą — ADDRESS.
352 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

Jej wartość jest taka sama jak stałej Email.DATA1 (ta wartość to data1). Pokazany
kod jest napisany dla Androida 2.2, dlatego zamiast stałej Email.DATA1 trzeba użyć
stałej Email.ADDRESS. Warto też zauważyć, że używamy innego identyfikatora URI
(innej tabeli). Aplikacja kieruje więc zapytania do trzech różnych tabel, aby pobrać
dane potrzebne do zarejestrowania użytkownika.
OMÓWIENIE
Wspomnieliśmy wcześniej, że jedną z głównych zalet korzystania z integracyj-
nej bazy danych jest to, że nie trzeba pisać kodu do zarządzania integracją.
Oznacza to, że aplikacja w celu wczytania danych musi tylko skierować zapyta-
nie do odpowiedniego dostawcy treści. Interfejs API dostawcy treści to prosta
warstwa nad bazą danych SQLite, dlatego trzeba używać kursorów. Jednak kiedy
nauczysz się korzystać z jednego dostawcy treści, używanie innych okaże się pro-
ste. Opisane podejście zapewnia też inne korzyści związane z bazami danych.
Zauważ na przykład, że można użyć klauzuli LIKE %XYZ% do wyszukiwania tekstu
w danych.
W przykładzie użyliśmy dostawcy ContactsContract. Pakiet android.provider
obejmuje też dostawców kalendarza i multimediów. W rozdziale 11. dokładniej
opisujemy używanie dostawców treści do przeglądania wszystkich plików muzycz-
nych z urządzenia. Okazuje się, że odbywa się to podobnie jak w przykładach
z tego rozdziału. Korzystanie z dowolnego dostawcy treści (także niestandardo-
wego) przebiega w zbliżony sposób. Zobaczmy, jak utworzyć własnego dostawcę
treści i udostępnić go innym.

0 TECHNIKA 40. Korzystanie z niestandardowego dostawcy treści

W poprzedniej technice zobaczyłeś, jak korzystać z dostawcy treści. Ponieważ


wiele mechanizmów Androida, na przykład książka adresowa i kalendarz, jest
dostępnych poprzez dostawców treści, warto wiedzieć, jak ich używać. Ponadto
po przyzwyczajeniu się do korzystania ze standardowych dostawców treści
z Androida praca z takimi dostawcami z innych aplikacji jest stosunkowo prosta.
Możliwe jednak, że chcesz utworzyć własnego dostawcę treści, aby umożliwić
innym programom współużytkowanie danych z rozwijaną aplikacją.
PROBLEM
Chcemy udostępniać dane z rozwijanej aplikacji w innych programach, a nawet
pozwolić tym ostatnim na wzbogacanie takich danych. Autorzy innych programów
powinni mieć możliwość wyboru sposobu pobierania danych, a my nie chcemy
tworzyć warstwy aplikacji lub usług służącej do udostępniania informacji.
ROZWIĄZANIE
Pokazujemy tu, jak utworzyć niestandardowego dostawcę danych. Przykładowa
aplikacja umożliwia użytkownikowi wprowadzenie tytułów filmów i zapisanie ich
za pomocą niestandardowego dostawcy treści. Dostawcę może następnie wyko-
0 TECHNIKA 40. Korzystanie z niestandardowego dostawcy treści 353

rzystać inny program, w którym potrzebne są filmy interesujące użytkownika. Na


rysunku 8.10 pokazano wygląd rozwijanej aplikacji.
Dotknięcie jednego z filmów z listy (rysunek 8.10) powoduje wyświetlenie szcze-
gółowych informacji o nim. Widok z takimi informacjami przedstawiono na
rysunku 8.11.

Rysunek 8.10. Lista Rysunek 8.11. Widok


filmów użytkownika ze szczegółowymi informacjami o filmie

Na podstawie widoku ze szczegółowymi informacjami można stwierdzić, jakie


dane ma udostępniać niestandardowy dostawca treści. Przyjrzyjmy się, jak zaim-
plementować takiego dostawcę, aby umożliwić aplikacjom (w tym rozwijanej!)
korzystanie z danych. Najpierw należy zadeklarować niestandardowego dostawcę
w pliku AndroidManifest.xml:
<provider android:name =
"com.manning.aip.mymoviesdatabase.provider.MyMoviesProvider"
android:authorities = "com.manning.aip.mymoviesdatabase" />

Teraz trzeba utworzyć klasę pochodną od abstrakcyjnej klasy android.content.


´ContentProvider. W nowej klasie należy zaimplementować metody do pobie-
rania, wstawiania, aktualizowania i usuwania danych, aby udostępnić wszystkie
standardowe operacje CRUD (ang. create, read, update, delete, czyli „utwórz,
odczytaj, aktualizuj, usuń”). W przykładowym kodzie (listing 8.11) koncentru-
jemy się na mechanizmach pobierania danych.
354 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

Listing 8.11. Interfejs do pobierania danych z dostawcy informacji o filmach

public class MyMoviesProvider extends ContentProvider {


private SQLiteDatabase db;

@Override
public Cursor query(Uri uri, final String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
HashSet<String> projectionCols = new HashSet<String>();
if (projection != null) {
projectionCols = new HashSet<String>(Arrays.asList(projection));
if (!MyMoviesContract.Movies.MovieColumns.projectionMap.keySet().
containsAll(projectionCols)) {
throw new IllegalArgumentException(
"Nieznana kolumna w projekcji");
}
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
switch (uriMatcher.match(uri)) {
case MOVIES:
qb.setTables(MovieTable.TABLE_NAME);
return qb.query(db,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
case MOVIE_ID:
long movieId = ContentUris.parseId(uri);
StringBuilder tables = new StringBuilder(MovieTable.TABLE_NAME)
.append(" as outer_movie");
LinkedList<String> newSelectionArgs = new LinkedList<String>();
newSelectionArgs.add(String.valueOf(movieId));
if (selectionArgs != null) {
newSelectionArgs.addAll(Arrays.asList(selectionArgs));
}
String[] allSelectionArgs =
newSelectionArgs.toArray(new String[0]);
if (projectionCols.contains(
MyMoviesContract.Movies.MovieColumns.CATEGORIES)) {
tables.append(" left outer join (select group_concat(")
.append(CategoryColumns.NAME)
.append(") as names from ")
.append(MovieCategoryTable.TABLE_NAME)
.append(", ")
.append(CategoryTable.TABLE_NAME)
.append(" where ")
.append(MovieCategoryTable.TABLE_NAME)
.append(".")
.append(MovieCategoryColumns.MOVIE_ID)
.append("= ? and ")
.append(MovieCategoryTable.TABLE_NAME)
.append(".")
.append(MovieCategoryColumns.CATEGORY_ID)
.append("=")
.append(CategoryTable.TABLE_NAME)
0 TECHNIKA 40. Korzystanie z niestandardowego dostawcy treści 355

.append(".")
.append(CategoryColumns._ID)
.append(") mcat");
}
StringBuilder where = new StringBuilder()
.append("outer_movie.")
.append(MovieColumns._ID)
.append("= ?");
qb.setProjectionMap(
MyMoviesContract.Movies.MovieColumns.projectionMap);
qb.setTables(tables.toString());
qb.appendWhere(where.toString());
return qb.query(db,
projection,
selection,
allSelectionArgs,
null,
null,
sortOrder);
case UriMatcher.NO_MATCH:
default:
throw new IllegalArgumentException("Nieznany URI " + uri);
}
}

Kod z listingu 8.11 wygląda na rozwlekły i skomplikowany, jest jednak dość


prosty. Przede wszystkim potrzebna jest baza SQLite do zapisywania i pobie-
rania danych. Konfigurujemy ją w metodzie onCreate dostawcy (nie przedstawiamy
tutaj tej metody; znajdziesz ją w pobranym kodzie). Dalej, przy przetwarzaniu
zapytania, trzeba sprawdzić, czy jednostka zgłaszająca zapytanie nie zażądała
danych z nieistniejącej kolumny . Jeśli zażądała, aplikacja zgłasza wyjątek.
Teraz trzeba ustalić, jakie dane należy pobrać z bazy. Wykorzystujemy do
tego określony przez użytkownika identyfikator URI. Pozwala to stwierdzić, czy
użytkownik chce pobrać wszystkie filmy (tak jak na rysunku 8.7), czy interesują
go szczegóły konkretnego obrazu (tak jak na rysunku 8.8). Jeśli potrzebne są
wszystkie filmy , można użyć parametrów metody query do bezpośredniego skie-
rowania zapytania do bazy SQLite. Jeżeli użytkownik żąda szczegółowych infor-
macji o konkretnym filmie , należy przeanalizować identyfikator URI w celu
pobrania identyfikatora filmu, a następnie podać identyfikator filmu w zapytaniu.
Trzeba też sprawdzić, czy użytkownika interesują kategorie filmu , ponieważ
znajdują się one w odrębnej (złączanej) tabeli. Jeśli potrzebne są kategorie, zapy-
tanie jest bardziej skomplikowane, ponieważ trzeba przeprowadzić złączanie.
OMÓWIENIE
Kod z listingu 8.11 staje się skomplikowany w miejscu tworzenia złączenia. Ta
złożoność ułatwia pracę użytkownikom dostawcy, ponieważ mogą w prosty spo-
sób pytać o kategorie bez konieczności zgłaszania wielu wywołań do dostawcy.
Warto porównać to z pobieraniem danych kontaktowych z wcześniejszej tech-
niki, gdzie trzeba było zgłosić trzy różne zapytania, aby pobrać cztery informacje.
356 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami

Dostawca treści wymusza ręczne przeprowadzenie złączenia. Jeśli tworzysz


własnych dostawców treści, możesz sam ustalić punkt równowagi między udo-
stępnianiem schematu bazy danych a udostępnianiem abstrakcji.
Dostawca treści jest już gotowy. W jaki sposób mają korzystać z niego inne
aplikacje? Klienty muszą znać tylko identyfikatory URI dostawców, a także nazwy
i typy kolumn (ze schematu). Różne odmiany informacji tego rodzaju są potrzebne
przy współużytkowaniu danych w dowolnym modelu — czy to poprzez intencje,
czy z wykorzystaniem języka AIDL, czy z użyciem dostawców treści. Informacje
można udostępniać jako stałe w klasach, podobnie jak robi się to w pakiecie SDK
Androida. Możesz spakować zestaw niezbędnych klas do pliku JAR, a nawet umie-
ścić go w bibliotece, aby ułatwić innym zintegrowanie tych klas z aplikacjami.

8.3. Podsumowanie
Rozdział ten mógłby mieć tytuł „Integrowanie aplikacji w Androidzie”. Możli-
wość integrowania aplikacji w celu zapewnienia wartościowszych rozwiązań użyt-
kownikom jest jedną z ważnych cech Androida, która odróżnia tę platformę od
innych mobilnych systemów operacyjnych. Domyślnie model zabezpieczeń
Androida powoduje, że każda aplikacja działa niezależnie od innych programów.
Zewnętrzne programy nie mają dostępu do żadnych danych aplikacji. Nie trzeba
robić nic niezwykłego, aby uzyskać bezpieczeństwo tego rodzaju. Jednak jeśli
chcesz umożliwić współużytkowanie danych i integrację aplikacji z innymi pro-
gramami, możesz to zrobić na wiele sposobów. Nie trzeba w tym celu obchodzić
mechanizmów systemu operacyjnego — punkty integracji są dobrze zdefiniowane.
W pierwszej aplikacji w rozdziale 2. umożliwiliśmy użytkownikom współ-
użytkowanie ofert dnia z innymi aplikacjami. Jakimi? To zależy od tego, jakie
programy są zainstalowane w urządzeniu, ponieważ dane współużytkowane są
za pomocą intencji. Jeśli korzystasz z Androida na smartfonie, może zauważyłeś,
że współużytkowanie w tym trybie stało się normą. Użytkownicy, niezależnie
od tego, czy oglądają stronę internetową, zdjęcie, czy czytają tekst, oczekują, że
za pomocą innych aplikacji będą mogli podzielić się informacjami ze znajomymi.
To dobrze. W tym rozdziale omówiliśmy wiele sposobów umożliwiających apli-
kacjom z jednego urządzenia komunikowanie się ze sobą. Teraz przyjrzyjmy się
temu, w jaki sposób programy mogą kontaktować się z innymi komputerami
z sieci.
Protokół HTTP
i usługi sieciowe

W tym rozdziale
Q Praca w sieci z wykorzystaniem protokołu HTTP
Q Przetwarzanie danych w formacie XML i JSON
Q Jak radzić sobie z awariami sieci?

Internet nie jest wielką ciężarówką, ale siecią kanałów.


Ted Stevens
Bez wątpienia jedną z najbardziej ekscytujących technologicznych innowacji
w świecie telefonii komórkowej był przeskok od wolnych, ograniczonych sieci
mobilnych, opartych na technologiach GPRS (ang. General Packet Radio Service)
i WAP (ang. Wireless Access Protocol), do kompletnych klientów sieciowych.
Choć także wcześniej sieć WWW istniała i szybko się rozwijała, protokół WAP
powstał jako kompromis. Połączenia w technologii GPRS były wolne, a telefony
miały wówczas małe wyświetlacze i niską wydajność, dlatego niemożliwe było
uzyskanie komfortu pracy znanego z tradycyjnych komputerów. Kompromis pole-
gał na tym, że protokół WAP nie umożliwiał dostępu do całej sieci WWW. Korzy-
stać można było tylko ze specjalnych, uproszczonych witryn, napisanych w języku
WML (ang. Wireless Markup Language), a nie w HTML-u.
W trakcie powstawania tej książki coraz popularniejsze stają się połączenia
4G z transferem na poziomie 100 megabitów na sekundę. To z zapasem wystarcza
na transmisję nagrań HD. Telefon Motorola Atrix 4G ma procesor o szybkości
1 gigaherca, dużą ilość pamięci RAM oraz ekran OLED o wysokiej rozdzielczości

357
358 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

i wielkości dłoni. W tym kilkucalowym urządzeniu wykorzystano więcej zaawan-


sowanych technologii niż w tradycyjnych komputerach, kiedy wprowadzano tech-
nologię WAP. Obecnie w telefonach komórkowych można korzystać z całego
stosu protokołów sieciowych i z kompletnych przeglądarek. Cały czas masz dostęp
do wszystkich zasobów sieci WWW!
Otwiera to duże możliwości przed programistami aplikacji mobilnych na
nowoczesne platformy, takie jak Android. W aplikacji można wyświetlać strony
internetowe lub na bieżąco pobierać materiały z usług sieciowych za pomocą
standardowych protokołów, takich jak HTTP. Coraz więcej witryn jest dostęp-
nych w czytelnych dla maszyn formatach, takich jak XML i JSON — i to często
bezpłatnie. Przykładowe witryny oferujące darmowe i publiczne usługi sieciowe
to Amazon, Twitter, eBay, Netflix i Qype. Usługi te umożliwiają utworzenie
własnej przeglądarki książek, klienta Twittera lub aplikacji z recenzjami filmów
albo restauracji. Możliwości są praktycznie nieograniczone.
Android udostępnia wiele klas frameworku pomocnych przy realizowaniu
tych możliwości. Klasy te pozwalają wykonywać różne zadania — od śledzenia
połączeń Wi-Fi i internetowych, przez przesyłanie komunikatów HTTP, po sze-
regowanie i rozszeregowywanie danych w formatach XML i JSON. W tym roz-
dziale poznasz wszystkie te możliwości w ośmiu prostych technikach. Rozdział
składa się z trzech podrozdziałów. W pierwszym omawiamy podstawowy proto-
kół sieci WWW, HTTP, i pokazujemy, jak na Androidzie przesyłać żądania HTTP.
W drugim podrozdziale wyjaśniamy, jak przetwarzać dokumenty w formatach
XML i JSON. Są to dwa obecnie najpopularniejsze formaty do wymiany danych
stosowane w usługach sieciowych. W trzecim podrozdziale przedstawiamy bar-
dziej zaawansowane techniki pracy z siecią, na przykład eleganckie przywracanie
stanu po awariach sieci. Pokazujemy też, jak w prawidłowy sposób reagować na
zmiany w sile połączenia, kiedy użytkownik się porusza.

9.1. Podstawy pracy z siecią z wykorzystaniem


protokołu HTTP
Jeśli pobierasz materiały z sieci, prawdopodobnie są one przesyłane za pośred-
nictwem protokołu HTTP. HTTP (ang. HyperText Transfer Protocol) to proto-
kół warstwy aplikacji (warstwa 7. w modelu OSI), opracowany początkowo na
potrzeby przesyłania stron HTML z serwerów WWW do przeglądarek interne-
towych. Obecnie HTTP jest głównym mechanizmem transferu informacji w sie-
ciach. Zaczyna nawet zastępować bardziej wyspecjalizowane protokoły, takie jak
FTP, stosowane do niezawodnego transferu danych. HTTP to doskonały przykład
na to, jak wartościowe może być nawet proste rozwiązanie. Protokół ten służy do
przesyłania komunikatów tekstowych, które są czytelne dla ludzi. HTTP jest
też elastyczny i można go dostosować do wielu różnych dziedzin przez wykorzy-
stanie mechanizmów w rodzaju nagłówków HTTP do przekazywania charakte-
rystycznych dla dziedziny metadanych wraz z treścią wiadomości. Interfejs
9.1. Podstawy pracy z siecią z wykorzystaniem protokołu HTTP 359

protokołu HTTP jest niezwykle prosty. Obejmuje tylko siedem różnych instruk-
cji (metod HTTP), które można traktować jak funkcje. Instrukcje te to: GET, POST,
PUT, DELETE, HEAD, OPTIONS i TRACE, przy czym standardowo używa się tylko trzech
pierwszych. Sukces lub porażkę komunikacji przez protokół HTTP przedstawia
się przy użyciu standardowych kodów statusu, które są na tyle ogólne, że można
je wykorzystać w różnych dziedzinach. Na rysunku 9.1 przedstawiono typowe
żądanie HTTP. Ma ono formę tekstową. Wypróbuj je — otwórz wiersz poleceń,
połącz się za pomocą instrukcji telnet z serwisem qype.com w porcie 80 i wpro-
wadź żądanie oraz nagłówki. Aby wysłać żądanie, naciśnij dwukrotnie klawisz
Enter. Spowoduje to wygenerowanie sekwencji CR+LF+CR+LF, która kończy
żądanie HTTP.

Rysunek 9.1. Żądanie HTTP to


kilka wierszy instrukcji i opcji
w tekstowym formacie ASCII
oraz dodatkowo opcjonalne ciało
żądania (treść komunikatu).
HTTP stał się podstawowym
narzędziem nie tylko do
pobierania stron internetowych,
ale też do przesyłania danych
binarnych i wywoływania usług
sieciowych oraz zdalnych metod

Z wymienionych przyczyn HTTP standardowo wykorzystuje się też w komuni-


kacji między usługami sieciowymi a ich klientami. W tym kontekście HTTP
pełni czasem funkcję głównego protokołu (jest tak na przykład w usługach REST,
gdzie HTTP służy do sterowania komunikacją). Często HTTP jest (nad)używany
jako mechanizm przekazywania danych. Dzieje się tak w większości usług opar-
tych na protokole SOAP lub technologiach XML i RPC, gdzie rola HTTP ograni-
cza się zwykle do transferu treści komunikatu. Nie analizujemy tu natury różnego
rodzaju usług sieciowych, warto jednak podkreślić niezwykle duże znaczenie
protokołu HTTP w kontekście aplikacji mobilnych łączących się z siecią WWW.
Najpierw skoncentrujmy się na podstawach. Zaczynamy od techniki 41., w któ-
rej pokazujemy, jak przesyłać proste żądania HTTP na serwer WWW z zastoso-
waniem standardowych mechanizmów Javy do pracy z siecią z wykorzystaniem
omawianego protokołu. Jest to szybkie i proste rozwiązanie. Dalej przedstawiamy
wbudowane w Android, bardziej skomplikowane komponenty związane z serwe-
rami Apache HTTP. Pozwalają one budować w Androidzie kompletne, ale bardziej
złożone rozwiązania oparte na HTTP. Aby lepiej je przybliżyć, w rozdziale wra-
camy do aplikacji MyMovies (tak, znowu). Najpierw w technice 42. pokazujemy,
jak zarządzać prostym połączeniem HTTP z usługą sieciową. Następnie w tech-
nice 43. wyjaśniamy, jak zmodyfikować połączenia HTTP, aby można było wyko-
rzystać je w kontekście mobilnym. Zajmijmy się uwalnianiem sieci WWW!
360 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

0 TECHNIKA 41. Protokół HTTP i klasa HttpURLConnection

Przed przejściem do bardziej skomplikowanych rozwiązań warto wyjaśnić, że


standardowa biblioteka klas Javy udostępnia mechanizm do wysyłania i odbiera-
nia komunikatów HTTP. Klasy te, a dokładniej ich implementacja o otwartym
dostępie do kodu źródłowego, dostępne są także w bibliotece klas Javy w Andro-
idzie. Implementacja protokołu HTTP w Javie jest prosta i zapewnia obsługę
takich mechanizmów, jak serwery pośredniczące, pliki cookies (w pewnym zakre-
sie) i protokół SSL. Inne narzędzia do używania protokołu HTTP są często nakład-
kami na standardowe interfejsy z Javy. Jeśli nie potrzebujesz abstrakcji udostęp-
nianej na przykład przez interfejsy HttpClient Apache’a (omawiamy je w następnej
technice), podstawowe klasy Javy mogą okazać się więcej niż wystarczające do
wykonywania prostych zadań. Ponadto — z uwagi na prostą i niskopoziomową
implementację — są wydajne.
PROBLEM
Chcemy za pomocą protokołu HTTP wykonywać proste zadania w sieci, na przy-
kład pobierać plik, i uniknąć przy tym spadku wydajności związanego ze stosowa-
niem wysokopoziomowej, bardziej rozbudowanej i skomplikowanej implementa-
cji klasy HttpClient Apache’a.
ROZWIĄZANIE
W takich sytuacjach dobrym wyborem są wbudowane klasy HTTP Javy. Ujmij-
my to konkretnie — zamierzamy użyć klas URL i HttpURLConnection. Obie znaj-
dują się w pakiecie java.net. Te dwie klasy współdziałają ze sobą. Nie można
używać jednej bez drugiej. Aby przesłać żądanie HTTP, najpierw należy określić
docelowy adres URL żądania w obiekcie klasy URL, a następnie użyć tego obiektu
do utworzenia uchwytu do połączenia HttpURLConnection. URL pełni więc funkcję
klasy fabrycznej. Należy przekazać jej podstawowe dane (adres z sieci WWW) —
wygeneruje ona odpowiedni obiekt połączenia. W kodzie wygląda to tak:
URL url = new URL("http://www.przyklad.pl/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect();
...
conn.disconnect();

„Zaraz, zaraz!”, możesz powiedzieć. „Czy adres URL nie może reprezentować
adresu dowolnego serwera, który niekoniecznie obsługuje protokół HTTP?”.
To prawda! Okazuje się, że HttpURLConnection to klasa pochodna od ogólniejszej
klasy URLConnection, która reprezentuje połączenie ogólnego użytku z jakimś
serwerem korzystającym z jakiegoś protokołu. Jak obiekt typu URL ustala, jakiego
rodzaju połączenie ma zwrócić? Na podstawie struktury adresu URL (na przykład
członu http). Klasa obsługi protokołów sprawdza strukturę i stara się znaleźć
pasującą implementację połączenia. Biblioteka klasy Javy (i Android) udostęp-
niają klasy obsługi protokołów dla wszystkich standardowych typów adresów,
0 TECHNIKA 41. Protokół HTTP i klasa HttpURLConnection 361

takich jak HTTP(S), FTP, MAILTO, FILE itd. Dlatego zwykle nie musisz przej-
mować się tym aspektem. Możesz też tworzyć własne klasy obsługi protokołów
tworzące egzemplarze niestandardowych podklas klasy URLConnection, jednak
rozwiązanie to rzadko jest potrzebne, dlatego nie omawiamy go w tym miejscu.
Warto też wspomnieć, że klasa URLConnection korzysta z gniazd TCP i stan-
dardowych klas strumieni java.io. Oznacza to, że operacje wejścia-wyjścia są
blokujące. Dlatego pamiętaj, aby nigdy nie uruchamiać ich w głównym wątku
interfejsu użytkownika.
Zobaczmy na praktycznym przykładzie, jak działa opisana technika. Wzboga-
camy aplikację MyMovies o okno dialogowe z najnowszymi informacjami pobra-
nymi z serwera WWW. Dzięki temu użytkownik zawsze ma świeże informacje
na temat zmian w ostatniej wersji aplikacji. Aby rozwiązanie działało, trzeba
umieścić plik tekstowy z aktualizacją na serwerze WWW, pobierać i wczytywać
ten plik oraz wyświetlać jego zawartość w oknie dialogowym z komunikatem.
Na rysunku 9.2 pokazano wygląd gotowej aplikacji.

Rysunek 9.2. Każde uruchomienie aplikacji powoduje


wyświetlenie okna dialogowego z informacjami
o aktualizacjach. Tekst w oknie dialogowym pochodzi
z serwera WWW, a nie z pakietu APK aplikacji

POBIERZ PROJEKT MYMOVIESWITHUPDATENOTICE.


Kod źródłowy projektu i pakiet APK do uruchamiania aplikacji
znajdziesz w witrynie z kodem do książki Android w praktyce.
Ponieważ niektóre listingi skrócono, abyś mógł skoncentro-
wać się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/mvwd, plik APK: http://mng.bz/DRKz.
362 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Dla uproszczenia okno dialogowe wyświetlamy przy każdym uruchomieniu apli-


kacji. To rozwiązanie może irytować użytkowników wersji produkcyjnej, jednak
tu sprawdza się dobrze. Zamierzamy napisać zadanie AsyncTask, które nawiązuje
połączenie z serwerem HTTP z wykorzystaniem obiektu klasy HttpURLConnection.
Następnie aplikacja ma pobierać plik z tekstem o aktualizacji i przesyłać ten
tekst za pomocą obiektu typu Handler do głównej aktywności, w której można
wyświetlić tekst w oknie typu AlertDialog. Przyjrzyjmy się najpierw klasie aktyw-
ności MyMovies (listing 9.1). Klasa ta obejmuje wywołanie zwrotne do obiektu
obsługi, który wyświetla wyskakujące okno dialogowe. Oprócz fragmentu tworzą-
cego okno dialogowe kod powinien być już znany. Fragmenty, które nie zmie-
niły się w porównaniu z poprzednimi wersjami, pomijamy w celu zachowania
zwięzłości.

Listing 9.1. Plik MyMovies.java zmodyfikowany w taki sposób, aby wyświetlał


okno dialogowe z informacjami o aktualizacji

public class MyMovies extends ListActivity implements Callback {

private MovieAdapter adapter;

public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);

setContentView(R.layout.main);
...
new UpdateNoticeTask(new Handler(this)).execute();
}
...
public boolean handleMessage(Message msg) {
String updateNotice = msg.getData().getString("text");
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle("Nowe funkcje");
dialog.setMessage(updateNotice);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setPositiveButton(getString(android.R.string.ok),
new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
dialog.show();
return false;
}
}

Oprócz kilku wierszy instrukcji do tworzenia okna dialogowego pozostały kod


powinien wyglądać znajomo po przeczytaniu kilku wcześniejszych rozdziałów.
Ciekawsza jest klasa UpdateNoticeTask, której używamy w ostatnim wierszu metody
onCreate. To w tej klasie odbywa się pobieranie danych. Na listingu 9.2 przedsta-
wiono jej kod.
0 TECHNIKA 41. Protokół HTTP i klasa HttpURLConnection 363

Listing 9.2. Klasa pochodna od AsyncTask, pobierająca tekst o aktualizacji


przez połączenie HttpURLConnection

public class UpdateNoticeTask extends AsyncTask<Void, Void, String> {

private static final String UPDATE_URL =


"http://android-in-practice.googlecode.com/files/update_notice.txt";

private HttpURLConnection connection;

private Handler handler;

public UpdateNoticeTask(Handler handler) {


this.handler = handler;
}

@Override
protected String doInBackground(Void... params) {
try {
URL url = new URL(UPDATE_URL);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "text/plain");
connection.setReadTimeout(10);
connection.setConnectTimeout(10);
connection.connect();
int statusCode = connection.getResponseCode();
if (statusCode != HttpURLConnection.HTTP_OK) {
return "Błąd pobierania informacji o aktualizacji";
}
return readTextFromServer();
} catch (Exception e) {
return "Błąd: " + e.getMessage();
} finally {
if (connection != null) {
connection.disconnect();
}
}
}

private String readTextFromServer() throws IOException {


InputStreamReader isr =
new InputStreamReader(connection.getInputStream());
BufferedReader br = new BufferedReader(isr);

StringBuilder sb = new StringBuilder();


String line = br.readLine();
while (line != null) {
sb.append(line + "\n");
line = br.readLine();
}
return sb.toString();
}

@Override
protected void onPostExecute(String updateNotice) {
Message message = new Message();
364 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Bundle data = new Bundle();


data.putString("text", updateNotice);
message.setData(data);
handler.sendMessage(message);
}
}

Po wczytaniu adresu URL z parametrów należy użyć obiektu typu URL do pobra-
nia odpowiedniego egzemplarza podklasy klasy URLConnection . Tutaj tą podklasą
jest HttpURLConnection, ponieważ adres URL obejmuje człon http://. Warto zau-
ważyć, że wywołanie metody openConnection nie powoduje natychmiastowego
nawiązania połączenia z serwerem. Aplikacja tworzy tylko egzemplarz obiektu
połączenia. Następnie trzeba skonfigurować żądanie HTTP . Najpierw okre-
ślamy, że aplikacja ma użyć metody GET do zażądania pliku. To ustawienie można
pominąć, ponieważ GET to domyślnie stosowana metoda. Ustawiamy też nagłó-
wek HTTP Accept, aby poinformować serwer, jakiego rodzaju dokumentu apli-
kacja oczekuje (tu jest to dokument tekstowy). Podajemy też limit czasu ocze-
kiwania, tak aby po wystąpieniu problemów z siecią wywołanie nie blokowało
wątku w nieskończoność. Na tym etapie żądanie jest skonfigurowane i można je
przesłać na serwer przez wywołanie metody connect . W zależności od odpo-
wiedzi serwera aplikacja albo zwraca komunikat o błędzie (jeśli status komuni-
katu jest inny niż 200/OK) , albo wczytuje tekst z ciała odpowiedzi . Trzeba
pamiętać o zamknięciu połączenia po zakończeniu przetwarzania odpowiedzi .
W ostatnim kroku tekst jest przesyłany z serwera do głównej aktywności za
pomocą obiektu typu Handler . Odbywa się to podobnie jak w rozdziale 6.
OMÓWIENIE
Ten przykład jest bardzo łatwy. Użyliśmy tu najprostszego rodzaju żądania.
W podobnych sytuacjach klasa HttpURLConnection dobrze się sprawdza i nie powo-
duje ponoszenia praktycznie żadnych dodatkowych kosztów. Problemem jest
architektura tej klasy. Duża część interfejsu klasy HttpURLConnection jest taka
sama jak w ogólnej klasie URLConnection, po której HttpURLConnection dziedziczy.
Dlatego nazwy niektórych metod są nieco abstrakcyjne. Jeśli nigdy wcześniej
nie korzystałeś z klasy HttpURLConnection, prawdopodobnie zastanawiasz się, do
czego służy metoda setRequestProperty. Umożliwia ona ustawianie nagłówków
HTTP, czego niełatwo się domyślić. Wynika to z tego, że w niektórych innych
protokołach nie występują pola nagłówków, jednak interfejs dla tych protokołów
jest taki sam, dlatego nazwy metod w omawianej klasie są ogólne.
Choć ogólne nazwy mogą wydawać się mało istotne, związane są z innym pro-
blemem — niewystarczającym rozdzieleniem mechanizmów w klasie URLConnec
´tion. Żądanie, odpowiedź oraz mechanizmy do ich wysyłania i odbierania są
obsługiwane w jednej klasie, dlatego programiści mogą często się zastanawiać,
które metody służą do zarządzania poszczególnymi z tych aspektów. Przypomina
to wrzucenie pięciodaniowego obiadu do blendera. Po zmiksowaniu posiłek
0 TECHNIKA 41. Protokół HTTP i klasa HttpURLConnection 365

nadal można podać, ale nie jest już apetyczny. Ponadto trudno jest zmodyfikować
każdą część, a jeszcze trudniej jest opracować makietę aplikacji na potrzeby testów
jednostkowych (nad tym zagadnieniem koncentrujemy się w rozdziale 13.). Oma-
wiana klasa nie jest najlepszym przykładem dobrego projektowania obiektowego.
Z klasą HttpURLConnection związane są też problemy dotyczące aspektów
praktycznych. Jeśli chcesz przechwytywać żądania, aby wstępnie je przetwarzać
i modyfikować, do wysyłania żądań HTTP lepiej nie używać klasy HttpURLConnec
´tion. Dobrym przykładem jest podpisywanie komunikatów w środowiskach
z bezpieczną komunikacją, gdzie nadawca musi obliczyć podpis na podstawie
właściwości z żądania, a następnie zmodyfikować je w celu dołączenia podpisu.
Treść żądania jest przesyłana bez buforowania, dlatego nie można dotrzeć do niej
w nieinwazyjny sposób. Ponadto implementacja klasy HttpURLConnection z pro-
jektu Apache Harmony miała poważne błędy. Jeden z nich opisujemy w ramce
„HttpURLConnection i pola nagłówków HTTP”.

HttpURLConnection i pola nagłówków HTTP


Jak już wiesz, biblioteka klas Javy w Androidzie jest oparta na projekcie Apache
Harmony (jest to implementacja Javy o otwartym dostępie do kodu źródłowego
rozwijana przez fundację Apache). Do wersji 2.2 Androida (FroYo, interfejs API
numer 8) występował w niej poważny błąd związany z przesyłaniem komunika-
tów HTTP za pomocą klasy HttpURLConnection. Przesyłane nazwy nagłówków
HTTP zapisane były małymi literami. Jest to niezgodne ze specyfikacją protokołu
HTTP i uniemożliwia działanie wielu serwerów HTTP, które usuwają nagłówki tego
rodzaju. Może to prowadzić do różnych skutków — od przesyłania dokumentów
w nieodpowiednim formacie (w wyniku pominięcia nagłówka Accept) po zupełnie
nieudaną obsługę żądań chronionych zasobów (z powodu niewykrycia nagłówka
Authorization). Problem rozwiązano w Androidzie 2.3 (Gingerbread, interfejs
API numer 9), jednak programiści często chcą zapewnić obsługę także starszych
wersji platformy. Obejście polega na rezygnacji z klasy HttpURLConnection i uży-
waniu w zamian klasy HttpClient Apache’a (omawiamy ją w następnej technice).
Oficjalne informacje o błędzie znajdziesz na stronie http://mng.bz/6T1I.

Podsumujmy rozważania. HttpURLConnection to klasa do prostego, ale niskopozio-


mowego przesyłania komunikatów HTTP. Ma ona kilka wad:
Q Nieelegancki interfejs utrudnia korzystanie z niej.
Q Monolityczny i nieobiektowy projekt utrudniają testowanie
i konfigurowanie klasy oraz dostosowywanie jej do potrzeb.
Q Występują w niej błędy, które mogą uniemożliwiać pracę aplikacji.
Do wykonywania prostych zadań, takich jak opisane w technice pobieranie plików,
klasa ta nadaje się dobrze i nie wymaga ponoszenia dodatkowych kosztów —
w końcu nie trzeba używać armaty, aby upolować muchę. Jeśli jednak chcesz
wykonywać bardziej skomplikowane operacje, na przykład przechwytywać żąda-
nia, zarządzać pulą połączeń lub pobierać pliki w częściach, zapomnij o klasie
366 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

HttpURLConnection. W Javie istnieje dużo lepszy sposób na wykonywanie takich


zadań. Dzięki inżynierom z Google’a mechanizm ten jest wbudowany w Android!

0 TECHNIKA 42. Praca z protokołem HTTP za pomocą klasy HttpClient


Apache’a

Jeśli stwierdzisz, że klasa HttpURLConnection nie odpowiada Twoim potrzebom,


jednak nie chcesz dodawać do aplikacji 200 kilobajtów kodu bibliotecznego,
mamy dla Ciebie dobrą wiadomość — nie będziesz potrzebował takiego kodu.
W pakiecie SDK Android znajduje się biblioteka Apache HTTP Components.
Jest to otwarta implementacja specyfikacji HTTP w Javie, powstała na podstawie
projektów Apache Jakarta i Apache Commons.
Apache HTTP Components składa się z dwóch części. HttpCore obejmuje
niskopoziomowe klasy do obsługi połączeń HTTP, a HttpClient składa się z klas
wyższego poziomu opartych na HttpCore. Te ostatnie klasy służą do implemento-
wania typowych klientów HTTP (czyli aplikacji łączących się z serwerami WWW).
Możesz traktować HttpCore jak podstawę, natomiast HttpClient jak ostateczny
produkt, obejmujący różne „wodotryski”. W odróżnieniu od prostej i ubogiej
klasy HttpURLConnection implementacja Apache’a jest wysokopoziomowa, roz-
budowana i daje duże możliwości. Pozwala w kilku wierszach kodu wykonywać
skomplikowane operacje. Ma standardowe, gotowe do użycia mechanizmy,
pozwalające na przykład obsługiwać równoległe żądania, pule połączeń, pono-
wienia żądań, przechwytywanie żądań itd. W porównaniu z klasą HttpURLConnec
´tion rozwiązanie Apache’a udostępnia też znacznie wygodniejsze i obiektowe
interfejsy, które są łatwe i intuicyjne w użyciu. Piękna rzecz!
PROBLEM
Implementujemy klienta HTTP, na przykład odbiorcę danych z usługi sieciowej,
i szukamy bogatego w funkcje, rozbudowanego, a jednocześnie łatwego w użyciu
rozwiązania do obsługi komunikacji HTTP z serwerem.
ROZWIĄZANIE
W tej technice modyfikujemy kod z poprzedniej techniki. Do pobierania pliku
używamy tu klasy HttpClient Apache’a zamiast klasy HttpURLConnection. To ćwicze-
nie pozwoli Ci zrozumieć różnice między obiema klasami.

POBIERZ PROJEKT MYMOVIESWITHHTTPCLIENT. Kod


źródłowy projektu i pakiet APK do uruchamiania aplikacji
znajdziesz w witrynie z kodem do książki Android w praktyce.
Ponieważ niektóre listingi skrócono, abyś mógł skoncentro-
wać się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
0 TECHNIKA 42. Praca z protokołem HTTP za pomocą klasy HttpClient Apache’a 367

Zauważ, że wszystkie zmiany wprowadzone w technikach w dalszej


części rozdziału są ujęte w pliku APK. Jest to więc ostatni plik do tego
rozdziału.
Kod źródłowy: http://mng.bz/iR21, plik APK: http://mng.bz/QLuf.
Komunikacja z serwerem HTTP za pomocą klasy HttpClient Apache’a zwykle
odbywa się przy użyciu pięciu różnych interfejsów niezbędnych do wykonywa-
nia żądań. Te interfejsy to klasy: HttpRequest (i klasy z implementacją HttpGet,
HttpPost itd. — służą do konfigurowania żądań), HttpClient (służy do wysyłania
żądań), HttpResponse (służy do przetwarzania odpowiedzi od serwera), HttpContext
(służy do zarządzania stanem komunikacji) i HttpEntity (reprezentuje treść żąda-
nia lub odpowiedzi). Biblioteka ta obejmuje znacznie więcej klas, jednak najczę-
ściej używa się właśnie tych. Na rysunku 9.3 pokazano, jak klasy te współdziałają
w zakresie nawiązywania połączenia HTTP i korzystania z niego.

Rysunek 9.3. Najważniejsze obiekty w komunikacji HTTP opartej na klasie


HttpClient Apache’a. Zauważ, że każdemu aspektowi komunikacji między klientem
a serwerem odpowiada odrębna klasa

W klasie HttpURLConnection znajduje się cały kod klienta, połączenia, żądania


i odpowiedzi HTTP. Tu trzeba zastanowić się nad zasięgiem obiektów przed-
stawionych na rysunku 9.3. Zwykle używa się jednego obiektu typu HttpClient
na aplikację (warto ukryć go w klasie Application lub przechowywać w polu
statycznym), jednego obiektu typu HttpContext dla grupy par żądanie-odpowiedź
i po jednym obiekcie typów HttpRequest i HttpResponse na zgłaszane żądanie.
UWAGA. Klasa HttpContext służy do przechowywania stanu dla kilku
par żądanie-odpowiedź, nie pomyl jej jednak z tradycyjnymi sesjami
HTTP (często implementowanymi za pomocą plików cookies HTTP).
HttpContext to nic więcej, jak kontekst wykonania po stronie klienta. Możesz
368 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

traktować go jak listę atrybutów, które można przechowywać i śledzić


dla kilku żądań. Jeśli nie wiesz, do czego można wykorzystać tę klasę,
prawdopodobnie jej nie potrzebujesz. Ponieważ dotyczy to większości pro-
gramistów, metoda execute klasy HttpClient domyślnie tworzy kontekst
wykonania i zarządza nim automatycznie, zatem możesz go pominąć. Dla-
tego nie wspominamy dalej o klasie HttpContext.
W tej technice rozwijamy nową wersję prostego mechanizmu do przesyłania
informacji o aktualizacji. W nowej wersji używamy klasy HttpClient zamiast
HttpURLConnection. Cały kod właściwy dla protokołu HTTP ukrywamy w metodzie
doInBackground zadania, dlatego koncentrujemy się właśnie na tej metodzie. Nowy
kod przedstawiono na listingu 9.3. Fragmenty, które nie zmieniły się w porówna-
niu z poprzednim listingiem, pozostawiamy bez zmian.

Listing 9.3. Zadanie UpdateNoticeTask pobierające plik za pomocą klasy HttpClient

public class UpdateNoticeTask extends AsyncTask<Void, Void, String> {


...
@Override
protected String doInBackground(Void... params) {
try {
HttpGet request = new HttpGet(UPDATE_URL);
request.setHeader("Accept", "text/plain");
HttpResponse response = MyMovies.getHttpClient()
.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
return "Błąd pobierania danych o aktualizacji";
}
return EntityUtils.toString(response.getEntity());
} catch (Exception e) {
return "Błąd: " + e.getMessage();
}
}

@Override
protected void onPostExecute(String updateNotice) {
...
}
}

Najpierw tworzymy żądanie GET na podstawie uzyskanego adresu URL (zobacz


też listing 9.2) i konfigurujemy je, aby pobrać zwykły plik tekstowy . Dalej
następuje „wymiana” obiektu żądania na obiekt odpowiedzi przez wywołanie
metody HttpClient.execute . Warto zauważyć, że obiekt odpowiedzi pobierany
jest z głównej aktywności z wykorzystaniem statycznej metody pobierającej.
Konfigurowanie współużytkowanego egzemplarza klasy HttpClient omawiamy
w następnej technice. Wywołanie metody execute prowadzi do otwarcia połącze-
nia i przesłania żądania za pomocą domyślnego kontekstu wykonania (istnieje
0 TECHNIKA 42. Praca z protokołem HTTP za pomocą klasy HttpClient Apache’a 369

też inna wersja metody execute, przyjmująca niestandardowy obiekt typu Http
´Context), jednak na tym etapie aplikacja nie pobiera jeszcze ciała odpowiedzi.
Aby wczytać tekst z ciała odpowiedzi (reprezentowanego na rysunku 9.3 przez
obiekt HttpEntity), używamy funkcji pomocniczej z biblioteki. Funkcja ta wczy-
tuje dane ze strumienia InputStream do łańcucha znaków . W poprzedniej tech-
nice podobne operacje musieliśmy wykonać ręcznie.
OMÓWIENIE
Wyraźnie widać, że jeśli chcesz przesyłać komunikaty HTTP na Androidzie,
klasa HttpClient udostępnia znacznie wygodniejszy interfejs niż klasa HttpURL
´Connection. Ponadto korzystanie z niej pozwala dobrze rozdzielić zadania. Ist-
nieją odrębne obiekty żądania i odpowiedzi, a ponadto obiekt klienta, który wysyła
i odbiera żądania oraz odpowiedzi, i obiekt encji, będący nakładką na treść komu-
nikatu. Dostępne są też klasy pomocnicze. Pozwalają one natychmiast przetwo-
rzyć odpowiedź przez wczytanie jej do łańcucha znaków (to rozwiązanie sto-
sujemy w kodzie), tablicy bajtów itd. Inną wartościową cechą klasy HttpClient
jest to, że jeśli chcesz utworzyć proste rozwiązanie, możesz to zrobić. Jeżeli jed-
nak zamierzasz odwrócić każdy bit, także to jest możliwe. Jest to dobry przy-
kład strategii „najpierw konwencja, potem konfiguracja” (ang. convention over
configuration). Pozwala ona uruchomić rozwiązanie minimalnym nakładem pracy,
jeśli jednak programista ma duże wymagania, może skonfigurować każdy szcze-
gół. Wspomnieliśmy już, że związane jest to z pewnymi kosztami. Rozwiązanie
jest wolniejsze i wymaga więcej pamięci. Dlatego dobrze się zastanów, które
zadania chcesz wykonywać za pomocą szybkiej, ale nieeleganckiej klasy HttpURL
´Connection, a do których operacji warto użyć wygodnej, ale „ciężkiej” klasy
HttpClient Apache’a.
W tej technice nie koncentrowaliśmy się na konkretnym typie obiektu
HttpClient. Najczęściej używa się obiektu typu DefaultHttpClient, który ustawia
rozsądne, domyślne opcje dla połączeń, dostosowane do protokołu HTTP/1.1.
Ustawienia dotyczą domyślnego nagłówka User-Agent, domyślnego rozmiaru
bufora gniazda TCP, domyślnego komponentu obsługi ponawiania żądania itd.
(komponent ten próbuje do trzech razy ponowić żądanie, jeśli jest to bezpieczne —
czyli kiedy żądania są powtarzalne). Obiekt rejestruje też w porcie 443 kompo-
nenty obsługi połączeń z adresami URL z członem HTTPS (HTTP z wykorzy-
staniem SSL-u).
Choć domyślna implementacja jest bardzo prosta (konstruktor nie wymaga
nawet argumentów; czy można wyobrazić sobie wygodniejsze rozwiązanie?),
kusząca jest myśl o korzystaniu z niej w każdym miejscu, jednak zwykle odra-
dzamy to podejście. Wynika to z poważnej pułapki, z której wielu programistów
nie zdaje sobie sprawy. Problem jest na tyle groźny, że Google w Androidzie 2.2
(interfejs API numer 8) udostępnia alternatywną implementację i zachęca do
jej używania. Nie we wszystkich aplikacjach można ograniczyć się do korzystania
370 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

ze stosunkowo niedawnego interfejsu API numer 8, dlatego zdecydowaliśmy


dodać następną technikę. Pokazujemy w niej, jak uniknąć problemów przez odpo-
wiednie skonfigurowanie domyślnej implementacji.
OSTRZEŻENIE! Jak już wspomnieliśmy, następna technika obejmuje
rozwiązania, których standardowo nie trzeba wprowadzać ręcznie, jeśli
używa się Androida 2.2 (interfejs API numer 8) lub nowszych wersji plat-
formy. Rozwiązania te są wbudowane w klasę AndroidHttpClient (zobacz
wcześniejszy podpunkt „Omówienie”). Jeśli zamierzasz rozwijać aplikacje
przeznaczone na Android 2.2 i nowsze wersje platformy, możesz pominąć
następną technikę. Zachęcamy jednak do zapoznania się z nią, ponieważ
pozwala zrozumieć klasę HttpClient i sposoby dostosowywania jej do swo-
ich potrzeb.

0 TECHNIKA 43. Konfigurowanie obiektu klasy HttpClient


bezpiecznego ze względu na wątki

W poprzedniej technice wspomnieliśmy, że często w całej aplikacji działa tylko


jeden egzemplarz klasy HttpClient. Można utworzyć pojedynczą metodę dostępu
do niego i przechowywać referencję do obiektu w kontekście aplikacji, a nawet
w polu statycznym. Takie rozwiązanie powoduje, że wszystkie części aplikacji
kierują żądania do tego samego egzemplarza klasy HttpClient, kiedy potrzebują
dostępu do sieci WWW. Wyobraź sobie teraz, że w aplikacji działa kilka obiek-
tów klasy AsyncTask i każdy z nich korzysta ze współużytkowanego obiektu klasy
HttpClient do komunikowania się z serwerem WWW. Mamy więc wątki i współ-
użytkowany stan. Czy dostrzegasz potencjalne problemy? Jeśli nie, wróć do roz-
działu 6. Współużytkowanie stanu między różnymi wątkami zawsze wymaga
synchronizacji z wykorzystaniem blokad obiektów lub pól typu volatile. Jeśli
nie zastosujesz tego rodzaju technik, aplikacja może działać nieprawidłowo.
Objawy mogą być różne — od występowania nieoczekiwanych wyjątków po
blokowanie połączeń.
Źródłem problemów jest klasa DefaultHttpClient, która przy domyślnych
ustawieniach korzysta z menedżera SingleClientConnManager do obsługi połączeń
HTTP. Menedżer ten tak naprawdę niczym nie zarządza, ponieważ przecho-
wuje tylko jeden obiekt połączenia, używany dla wszystkich połączeń HTTP. Jeśli
w danym momencie więcej niż jeden wątek żąda połączenia, wątki współza-
wodniczą o pojedynczy obiekt połączenia i ostatecznie wszystkie korzystają z niego
w tym samym czasie! Przypomina to jednoczesne wysyłanie faksem dwóch wia-
domości do dwóch różnych odbiorców — to po prostu niemożliwe.
Potrzebny jest lepszy sposób na obsługę połączeń, jeśli występuje zagroże-
nie, że kilka wątków będzie próbowało w tym samym czasie uzyskać dostęp do
jednego egzemplarza obiektu HttpClient.
0 TECHNIKA 43. Konfigurowanie obiektu klasy HttpClient bezpiecznego ze względu na wątki 371

PROBLEM
Korzystamy z wątków, które muszą komunikować się z serwerem WWW przez
jeden współużytkowany egzemplarz klasy HttpClient. Dlatego musimy się upew-
nić, że połączenia są nawiązywane w niezależny, bezpieczny ze względu na wątki
sposób.
ROZWIĄZANIE
Sztuczka polega na poinformowaniu obiektu klasy HttpClient o tym, którego
menedżera połączeń ma użyć. Najlepiej, jeśli będzie to menedżer bezpieczny
ze względu na wątki (opracowany z myślą o współbieżności). Na szczęście nie
musimy samodzielnie implementować takiego menedżera. Znajduje się on
w bibliotece i ma trafną nazwę ThreadSafeClientConnManager. Ten menedżer połą-
czeń obsługuje całą pulę połączeń. Połączenie można pobrać z puli, przydzielić
do wątku (który ma od tej pory wyłączny dostęp do danego połączenia), a następ-
nie zwrócić do puli, kiedy wątek już go nie potrzebuje. Jeśli ten sam lub inny
wątek żąda połączenia dla tej samej trasy, odpowiednie połączenie można pobrać
z puli i natychmiast wykorzystać. Nie trzeba najpierw zamykać go i ponownie
otwierać. Pozwala to uniknąć kosztów wymiany wstępnych informacji przez HTTP
przy nawiązywaniu nowego połączenia. Działanie mechanizmu zaprezentowano
na rysunku 9.4.

Rysunek 9.4. Przy


stosowaniu klasy
ThreadSafeClientCon
nManager wolne
połączenie jest
pobierane z puli,
kiedy wątek chce
przesłać żądanie
HTTP. Menedżer nie
zamyka połączenia
zwróconego przez
wątek. Umieszcza
dane połączenie
w puli, a inne wątki
mogą je ponownie
wykorzystać

UWAGA. Pule połączeń nie działają według tożsamości wątków (wtedy


dany wątek za każdym razem otrzymuje to samo połączenie), ale według
tras. Trasa żądania w klasie HttpClient to sekwencja hostów, przez które
żądanie jest przekazywane (czyli seria przeskoków, na przykład przez ser-
wery pośredniczące). Ważny jest też rodzaj połączenia (warstwowy lub —
jak w HTTPS — tunelowy). Oznacza to, że połączenie z puli można ponow-
nie wykorzystać dla żądania tylko wtedy, gdy dociera do docelowego hosta
poprzez te same hosty pośrednie i ma te same parametry trybu warstwo-
wego lub tunelowego.
372 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Pewnym utrudnieniem przy ręcznym ustawianiu menedżera połączeń jest to,


że trzeba podać zestaw parametrów konfiguracyjnych połączenia HTTP i schemat
protokołu. Musisz to zrobić także wtedy, gdy chcesz zastosować parametry obiektu
klasy SingleClientConnManager domyślnie ustawiane w klasie DefaultHttpClient.
GDZIE MOŻNA ZASTOSOWAĆ KONFIGURACJĘ W FORMACIE
XML? Usłyszeliśmy kiedyś pewne pytanie: „Android umożliwia definio-
wanie i konfigurowanie łańcuchów znaków, układów, widoków i innych
elementów w plikach XML; czy oznacza to, że wszystkie aspekty platformy
można skonfigurować w XML-u, a nie programowo, w kodzie?”. Niestety,
nie. Format XML można wykorzystać tylko do konfigurowania zasobów,
na przykład widoków, i manifestu aplikacji. Wszystkie pozostałe ustawie-
nia, w tym konfigurację połączeń HTTP, trzeba podawać w kodzie w Javie.
Poniżej pokazujemy najprostszą konfigurację (listing 9.4). Dalej opisujemy ten
kod. Tu zdecydowaliśmy się utworzyć obiekt klienta jako statyczną referencję
w aktywności MyMovies.

Listing 9.4. Można wykorzystać statyczny kod inicjujący do skonfigurowania


egzemplarza klasy HttpClient bezpiecznego ze względu na wątki

public class MyMovies extends ListActivity implements Callback {

private static final AbstractHttpClient httpClient;


...
static {
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
...
ThreadSafeClientConnManager cm =
new ThreadSafeClientConnManager(
new BasicHttpParams(), schemeRegistry);
...
httpClient = new DefaultHttpClient(cm, null);
}

public static HttpClient getHttpClient() {


return httpClient;
}
...
}

Inaczej niż w poprzedniej technice tu obiektu klasy DefaultHttpClient nie two-


rzymy za pomocą konstruktora domyślnego. W zamian przechowujemy statyczną
referencję typu final do takiego obiektu i przeprowadzamy niestandardową
konfigurację w bloku statycznego kodu inicjującego . Obiekt można przeka-
zywać z zastosowaniem publicznej statycznej metody pobierającej. Aby skonfi-
gurować obiekt klienta, trzeba najpierw podać ustawienia protokołu w obiekcie
klasy SchemeRegistry . Ten ostatni obiekt odpowiada za wiązanie schematu URI
0 TECHNIKA 43. Konfigurowanie obiektu klasy HttpClient bezpiecznego ze względu na wątki 373

(na przykład http lub https) i numeru portu (80) z gniazdem TCP utworzonym
przez odpowiednią fabrykę gniazd. Na podstawie obiektu klasy SchemeRegistry
i domyślnych parametrów menedżera połączeń można utworzyć obiekt klasy
ThreadSafeClientConnManager , a następnie za pomocą tego ostatniego skonfigu-
rować obiekt klienta HTTP . Zauważ, że do menedżera przekazujemy nowy
egzemplarz klasy BasicHttpParams, ale do nowego egzemplarza klasy DefaultHttp
´Client przekazujemy pusty zbiór parametrów (null). Wynika to z pewnej nie-
spójności w bibliotece. Przekazanie wartości null do konstruktora klienta powo-
duje, że konstruktor sam tworzy zbiór parametrów i dobiera sensowne ustawienia
domyślne. Jednak do menedżera nie można przekazać wartości null. Menedżer
oczekuje poprawnego obiektu klasy HttpParams. Jeśli w obiekcie tym nie ma zapi-
sanych wartości, menedżer używa ustawień domyślnych. W przykładzie używamy
wartości domyślnych wybranych przez bibliotekę. Jeśli nie do końca rozumiesz
działanie klasy HttpParams, nie martw się — wracamy do niej w dalszych przy-
kładach.
OMÓWIENIE
Jak widać, wystarczy kilka wierszy kodu, aby utworzyć implementację klienta
bezpieczną w użyciu w aplikacjach współbieżnych. Jeśli więcej niż jeden wątek
może próbować nawiązywać połączenie, zawsze używaj opisanego tu podejścia,
ponieważ gwarantuje ono poprawną izolację połączeń używanych przez różne
wątki. Wykorzystywane są do tego mechanizmy synchronizacji z Javy. Opisana
technika pozwala zgłaszać żądania HTTP w jednym wątku bez obaw o to, czy inne
nie zrobią w tym samym czasie tego samego!
Jak już wspomnieliśmy, używamy domyślnych parametrów dla menedżera
połączeń i obiektu klienta. Co to oznacza i jakie parametry są dostępne? Zacznijmy
od tego, że obiekt klasy HttpParams to odwzorowanie z parami klucz-wartość. To,
które pary są ważne dla obiektu otrzymującego to odwzorowanie (menedżera
połączeń), jest określane przez twórcę tego obiektu. W każdym menedżerze
ClientConnManager obsługiwane parametry są zdefiniowane w klasie ConnManager
´Params, gdzie znajdują się metody pomocnicze umożliwiające pobieranie i usta-
wianie parametrów.
W obiektach klasy ThreadSafeClientConnManager domyślna maksymalna liczba
połączeń to 20, a maksymalna liczba połączeń na trasę to 2. Ponieważ chcemy
przygotować aplikację MyMovies w taki sposób, aby mogła komunikować się
z usługą sieciową, musimy wybrać odpowiedniejsze wartości.
HttpParams connManagerParams = new BasicHttpParams();
ConnManagerParams.setMaxTotalConnections(connManagerParams, 5);
ConnManagerParams.setMaxConnectionsPerRoute(connManagerParams,
new ConnPerRouteBean(5));

ThreadSafeClientConnManager cm =
new ThreadSafeClientConnManager(connManagerParams,
schemeRegistry);
374 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Wykorzystaliśmy kod konfiguracyjny z listingu 9.4 i ustawiliśmy maksymalną


liczbę połączeń na trasę i wszystkich połączeń na tę samą wartość, ponieważ
wszystkie żądania są kierowane do tego samego hosta i z tego samego portu. Ogra-
niczyliśmy też liczbę połączeń do 5, ponieważ nie chcemy, aby aplikacja otwierała
ich zbyt wiele naraz.
Do tej pory wszystko wygląda dobrze. Można jednak jeszcze bardziej dostoso-
wać kod, na przykład sam obiekt klienta. Zawsze warto ustawić domyślny nagłó-
wek HTTP User-Agent, tak aby aplikacja mogła określić swoją tożsamość w usłudze
sieciowej. Warto też ograniczyć limit czasu nawiązywania połączenia i limit czasu
bezczynności przy pobieraniu danych, ponieważ w urządzeniach przenośnych
często występują problemy z połączeniem.
HttpParams clientParams = new BasicHttpParams();
HttpProtocolParams.setUserAgent(clientParams, "MyMovies/1.0");
HttpConnectionParams.setConnectionTimeout(clientParams, 15 * 1000);
HttpConnectionParams.setSoTimeout(clientParams, 15 * 1000);
httpClient = new DefaultHttpClient(cm, clientParams);

Zmodyfikować można także liczne inne ustawienia. Tu wybraliśmy wartości


domyślne odpowiednie do korzystania z usługi sieciowej. Ty możesz mieć inne
potrzeby, zależne od planowanego sposobu komunikowania się z siecią. Skoro
wiesz już, jak zmodyfikować obiekt klienta HTTP, możemy wyjawić Ci pewien
sekret — jeśli piszesz aplikację na Android 2.2 lub nowsze wersje platformy,
nie musisz ręcznie wykonywać żadnych operacji opisanych w tej technice! Jak
już wspomnieliśmy, od wersji 2.2 Android obejmuje niestandardową imple-
mentację klasy HttpClient, AndroidHttpClient. Implementacja ta jest zoptymali-
zowana pod kątem aplikacji mobilnych i wykonuje operacje w rodzaju ustawiania
odpowiednich limitów czasu oraz właściwego menedżera połączeń bezpiecznych
ze względu na wątki. Domyślnie obsługuje też połączenia HTTPS. Aby zastosować
tę klasę, wystarczy zastąpić kod z punktu z listingu 9.4 następującą instrukcją:
httpClient = AndroidHttpClient.newInstance("MyMovies/1.0");

Możesz dostosować ustawienia obiektu za pomocą parametrów poznanych w tej


technice. W końcu klasa ta to tylko następna implementacja klasy HttpClient,
dlatego przyjmuje te same wywołania i parametry. Zauważ, że łańcuch znaków
przekazany do metody newInstance jest ustawiany jako wartość nagłówka HTTP
User-Agent wysyłanego w każdym żądaniu HTTP. To wywołanie to odpowiednik
instrukcji HttpProtocolParams.setUserAgent(clientParams, "MyMovies/1.0")
z poprzedniego fragmentu kodu.
Klasa AndroidHttpClient oprócz ustawiania odpowiedniej konfiguracji prze-
twarza też treść komunikatów w formacie gzip (usługi sieciowe często kompre-
sują odpowiedzi, aby przyspieszyć pobieranie danych przez klienty) i obsługuje
rejestrator cURL, który wyświetla każde żądanie w formacie używanym w narzę-
dziach cURL. Pozwala to łatwo powtarzać żądania w wierszu poleceń. Jeśli uży-
wasz Androida w wersji wcześniejszej niż 2.2, jednak nie chcesz samodzielnie
9.2. Korzystanie z usług sieciowych generujących dane w formatach XML i JSON 375

wykonywać wszystkich operacji, przyjrzyj się bibliotece ignition (https://github.


com/kaeppler/ignition). Większość opisanych tu optymalizacji wbudowana jest
w klasę IgnitedHttp wspomnianej biblioteki. Klasa ta udostępnia też kilka innych
abstrakcji i mechanizmów dodatkowo ułatwiających używanie protokołu HTTP
w Androidzie.
Wygląda na to, że jesteśmy gotowi do połączenia aplikacji MyMovies z usługą
sieciową. Może pobierzemy dane na temat filmów z sieci WWW? Zobaczmy,
jak działa takie rozwiązanie.

9.2. Korzystanie z usług sieciowych generujących dane


w formatach XML i JSON
W pierwszym podrozdziale tego rozdziału pokazaliśmy, jak nawiązać połączenie
z siecią WWW i pobrać dane przez protokół HTTP. Techniki te wystarczą do
pobrania pliku w celu zapisania go w urządzeniu lub wyświetlenia treści w pier-
wotnej postaci. Tak działa aplikacja pobierająca informacje o aktualizacjach.
Większość aplikacji mobilnych łączących się z siecią WWW robi to jednak
w innym celu — aby pobrać dane z usługi sieciowej.
DEFINICJA. Usługa sieciowa to zestaw interfejsów dostępny na serwerze
poprzez technologie internetowe (na przykład HTTP w obszarze przesy-
łania danych lub XML i JSON w zakresie ich serializowania). Odbiorcami
usług sieciowych — inaczej niż w przypadku witryn internetowych — są
maszyny, a nie ludzie.
Ponieważ dane w usługach sieciowych zawsze są ustrukturyzowane (przeważnie
pochodzą z działającej na zapleczu bazy), trzeba je serializować na potrzeby prze-
syłania, a następnie odtwarzać po stronie klienta z zachowaniem ich struktury.
Serializowanie danych (nazywane też szeregowaniem) polega na przekształca-
niu danych w formie wierszy tabeli lub obiektów na uporządkowany, dobrze
ustrukturyzowany i stabilny format. Klient może następnie przeprowadzić dese-
rializację odpowiedzi od usługi na zrozumiałą reprezentację danych (na przykład
na obiekt Javy). Na rysunku 9.5 pokazano typowy przebieg komunikacji między
usługą sieciową a mobilnym klientem.
Proces ten przypomina pisanie listu. Myśli przechowywane bezpiecznie
w Twojej głowie (w bazie danych) przenosisz na papier, zapisując na kartce
słowo po słowie. W ten sposób „serializujesz” myśli! Może zauważyłeś, że działa-
nie mechanizmu wymaga spełnienia dwóch podstawowych warunków. Po pierw-
sze, nadawca i odbiorca muszą posługiwać się tym samym językiem. Po drugie,
muszą korzystać z tego samego mechanizmu wymiany informacji. Jeśli któryś
z warunków nie jest spełniony, kontakt się nie powiedzie. Format używany do
przekazywania informacji w sposób zrozumiały dla wielu jednostek to wspólny
format wymiany danych, a mechanizm przekazywania informacji to warstwa trans-
portowa. W analogii wspólnym formatem wymiany danych jest język, natomiast
376 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Rysunek 9.5. Typowy scenariusz serializowania i deserializowania w środowisku


sieciowym. Aplikacja żąda obiektu od usługi sieciowej przez protokół HTTP. Obiekt
jest najpierw wczytywany z tabeli z bazy danych, następnie serializowany na format
XML, przesyłany za pomocą protokołu HTTP i ostatecznie deserializowany przez
aplikację na obiekt Javy

warstwą transportową jest list. W sieci WWW wspólny format wymiany danych
to zwykle XML (ang. Extensible Markup Language) lub JSON (ang. JavaScript
Object Notation), a warstwą transportową jest zazwyczaj protokół HTTP.
Z poprzedniego podrozdziału dowiedziałeś się, jak przesyłać dane za pośred-
nictwem protokołu HTTP. Tu pokazujemy, jak wykorzystać przesłane przez
usługę sieciową odpowiedzi w formacie XML lub JSON.
UWAGA. O formatach XML i JSON piszemy tu w kontekście usług siecio-
wych, ponieważ wynika to ze struktury rozdziału. Nie oznacza to, że tech-
niki z tego podrozdziału są przydatne tylko w sieci WWW! Omówione tu
mechanizmy związane z formatami XML i JSON można wykorzystać do
przetwarzania dokumentów z dowolnych źródeł, w tym prostych plików
z urządzenia. Uważamy jednak, że ciekawe jest przedstawianie tych mecha-
nizmów w połączeniu z usługami sieciowymi.
Oto plan dalszej części podrozdziału. Zaczynamy od mechanizmów do przetwa-
rzania danych w XML-u, ponieważ XML to najpopularniejszy obecnie format do
wymiany informacji w sieci WWW. Przedstawiamy dwa różne sposoby prze-
twarzania danych w tym formacie. Są to implementacje specyfikacji SAX (tech-
nika 44.) i XmlPull (technika 45.). Jeśli znasz XML-owe interfejsy API, zauważysz,
że pomijamy model DOM. Wynika to z jego niskiej wydajności, przez co lepiej
unikać go w urządzeniach przenośnych. Jeśli potrzebujesz rozwiązania, które
(podobnie jak w modelu DOM) buforuje cały dokument w pamięci, istnieje znacz-
nie prostszy mechanizm — format JSON, który omawiamy w technice 46. Aby
uatrakcyjnić przykłady, do aplikacji MyMovies dodajemy nową funkcję. Długie
9.2. Korzystanie z usług sieciowych generujących dane w formatach XML i JSON 377

przyciśnięcie elementu listy powoduje pobranie oceny filmu z usługi sieciowej


TMDb (The Movie Database). Przetwarzanie odpowiedzi odbywa się za pomocą
trzech omawianych w podrozdziale technik. Działanie aplikacji przedstawiono
na rysunku 9.6.

Rysunek 9.6. Długie przyciśnięcie pozycji z listy


filmów powoduje wywołanie usługi sieciowej TMDb
i pobranie informacji na temat danego filmu.
Na podstawie tych danych aplikacja wyświetla
w oknie dialogowym ocenę filmu z bazy IMDb

Aby dodać nową funkcję, która jest nieco skomplikowana, musieliśmy wprowadzić
drobne zmiany w istniejących klasach aplikacji. Oto te zmiany:
1. Dodaliśmy klasę Movie (jest to obiekt POJO (ang. plain old Java object)
obejmujący pola na identyfikator, tytuł i ocenę). Metoda toString tej klasy
zwraca tytuł filmu.
2. Zmodyfikowaliśmy adapter listy, aby zarządzał obiektami typu Movie,
a nie łańcuchami znaków (typ adaptera zmieniliśmy z ArrayAdapter<String>
na ArrayAdapter<Movie>). Przekazanie do adaptera ArrayAdapter obiektu
innego niż łańcuch znaków prowadzi do wywołania metody toString tego
obiektu i pobrania etykiety dla elementu listy (tu jest nią tytuł filmu),
dlatego ogólnie adapter działa tak samo jak wcześniej.
3. Do aktywności MyMovies dodaliśmy interfejs OnItemLongClickListener.
W aktywności tej uruchamiamy nowe zadanie typu AsyncTask. Odpowiada
ono za komunikację z usługą sieciową TMDb. Kod źródłowy zadania
przedstawiamy dalej.
Pomijamy kod tych zmian, ponieważ poprawki są niewielkie i nie obejmują niczego
nowego ani ważnego w kontekście tego rozdziału (zainteresowani znajdą pełny
kod źródłowy w internecie). Warto jednak przyjrzeć się nowej klasie zadania,
która odpowiada za połączenie z usługą TMDb. Kod nowej klasy przedstawiono
na listingu 9.5.
378 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Listing 9.5. Zadanie GetMovieRatingTask pobiera za pomocą usługi sieciowej


oceny filmów z bazy IMDb

public class GetMovieRatingTask extends AsyncTask<String, Void, Movie> {

private static final String API_KEY =


"624645327f33f7866355b7b728f9cd98";

private static final String API_ENDPOINT =


"http://api.themoviedb.org/2.1";

private static final int PARSER_KIND_SAX = 0;


private static final int PARSER_KIND_XMLPULL = 1;
private static final int PARSER_KIND_JSON = 2;

private int parserKind = PARSER_KIND_SAX;

private Activity activity;

public GetMovieRatingTask(Activity activity) {


this.activity = activity;
}

@Override
protected Movie doInBackground(String... params) {
try {
String imdbId = params[0];
HttpClient httpClient = MyMovies.getHttpClient();
String format = parserKind == PARSER_KIND_JSON ? "json" : "xml";
String path =
"/Movie.imdbLookup/en/" + format + "/" + API_KEY + "/";

HttpGet request = new HttpGet(API_ENDPOINT + path);


HttpResponse response = httpClient.execute(request);
InputStream data = response.getEntity().getContent();

switch (parserKind) {
case PARSER_KIND_SAX:
return SAXMovieParser.parseMovie(data);
case PARSER_KIND_XMLPULL:
return XmlPullMovieParser.parseMovie(data);
case PARSER_KIND_JSON:
return JsonMovieParser.parseMovie(data);
default:
throw new RuntimeException("Nieobsługiwany parser");
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

@Override
protected void onPostExecute(Movie movie) {
if (movie == null) {
Toast.makeText(activity, "Błąd!", Toast.LENGTH_SHORT).show();
}
Dialog dialog = new Dialog(activity);
dialog.setContentView(R.layout.movie_dialog);
0 TECHNIKA 44. Przetwarzanie danych w XML-u za pomocą interfejsu SAX 379

dialog.setTitle("Ocena filmu \"" + movie.getTitle() + "\" w bazie IMDb:");

TextView rating =
(TextView) dialog.findViewById(R.id.movie_dialog_rating);
rating.setText(movie.getRating());

dialog.show();
}
}

To zadanie na podstawie identyfikatora filmu z bazy IMDb (przekazany jako


łańcuch znaków) tworzy obiekt klasy Movie . Movie to klasa Javy z kilkoma
polami — ma identyfikator, tytuł i ocenę. Najpierw trzeba utworzyć ścieżkę
pozwalającą znaleźć film w usłudze sieciowej TMDb. Przy wyszukiwaniu na
podstawie identyfikatorów z bazy IMDb usługa wymaga kilku parametrów wystę-
pujących w adresie URL. Te parametry to na przykład język (/en) i format odpo-
wiedzi (/xml). Trzeba też podać klucz do interfejsu API (klucz pozwala zidentyfi-
kować aplikację w usłudze sieciowej) i identyfikator filmu . Warto zauważyć,
że klucz do interfejsu API jest współużytkowany przez wszystkich użytkowni-
ków aplikacji. Klucz jest potrzebny dla całej aplikacji, a nie dla każdego użytkow-
nika. Następnie aplikacja w sposób opisany w poprzednich technikach przesyła
żądanie GET pod utworzony adres URL . Teraz zaczyna się ciekawy fragment.
Aplikacja przekazuje ciało odpowiedzi do kilku różnych klas parserów, które
trzeba dopiero utworzyć . Klasy te rozwijamy w następnych technikach. Jeśli
przetwarzanie kończy się powodzeniem, aplikacja wczytuje odpowiednie pola
z obiektu klasy Movie i wyświetla informacje w oknie dialogowym .
Kod klasy pozostaje prawie taki sam w całym podrozdziale. Zmienić trzeba
tylko dwa wiersze kodu, które określają format odpowiedzi w docelowym adre-
sie URL i wywołują parser. Dlatego dalej nie wracamy do tej klasy. Jest ona pod-
stawą używaną do wywoływania parserów. Tu koncentrowaliśmy się na tej klasie,
a dalej omawiamy wspomniane parsery.

0 TECHNIKA 44. Przetwarzanie danych w XML-u


za pomocą interfejsu SAX

Zapomnijmy na chwilę o usłudze sieciowej i wróćmy do głównego tematu. Mamy


pobrany z dowolnego źródła dokument XML (czyli tekst o strukturze drzewia-
stej z węzłami elementów i węzłami z treścią) i musimy przekształcić zapisane
w nim tekstowe dane na obiekty Javy, który można następnie wykorzystać
w aplikacji.
Zadanie to można wykonać na wiele sposobów. Poszczególne rodzaje parserów
XML-a przetwarzają dokumenty w różny sposób. Android udostępnia interfejsy
API trzech rodzajów parserów (DOM, SAX i XmlPull) o określonych wadach
i zaletach. Jakie różnice występują między poszczególnymi typami parserów?
380 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

OBSŁUGA TECHNOLOGII STAX. Android nie udostępnia parsera StAX


(ang. Streaming API for XML), który jest częścią oficjalnego pakietu Sun
JDK 6 (biblioteka klas Androida jest oparta na Javie 5). StAX obejmuje API
parsera typu pull. Parser ten można traktować jako ustandaryzowanego
następcę parsera XmlPull, jednak ponieważ funkcjonalnie oba mechanizmy
sobie odpowiadają, a StAX nie jest częścią Androida, pomijamy tu tę
technologię.
Jedna z różnic między parserami związana jest z tym, czy trzeba, czy nie trzeba
wczytywać całego dokumentu XML do pamięci przed rozpoczęciem pracy.
Parsery oparte na modelu DOM (ang. Document Object Model) tego wymagają.
Przetwarzają dokumenty XML na strukturę drzewiastą, po której można się poru-
szać w pamięci, aby wczytać zawartość dokumentu. Pozwala to poruszać się po
dokumencie w dowolnej kolejności i tworzyć oparte na modelu DOM przydatne
interfejsy API, takie jak XPath (jest to język zapytań wykorzystujący ścieżki;
opracowano go do pobierania danych z drzew). Przed wersją 2.2 Androida (FroYo,
interfejs API numer 8) interfejsy API dla języka XPath nie były częścią Androida.
Dlatego jeśli nie piszesz aplikacji na Android w wersji 2.2 lub nowszej, musisz
dołączyć do aplikacji implementację języka XPath, na przykład bibliotekę Jaxen.
Korzystanie z samego modelu DOM nie przynosi korzyści, ponieważ interfejs
API modelu jest niewygodny, a konieczność każdorazowego wczytywania całego
dokumentu (nawet jeśli nie jest to potrzebne) powoduje wysokie koszty. Dlatego
parsery DOM zwykle nie są najlepszym rozwiązaniem do przetwarzania doku-
mentów XML w Androidzie.
Prowadzi to do drugiej grupy parserów, które nie muszą najpierw wczyty-
wać dokumentu. Są to parsery strumieniowe. Przetwarzają one dokument XML
już w trakcie wczytywania go ze źródła danych (sieci WWW lub dysku). Oznacza
to, że (inaczej niż w modelu DOM) nie zapewniają dostępu swobodnego do drzewa
dokumentu XML, ponieważ nie przechowują wewnętrznej reprezentacji danych.
Parsery strumieniowe także można podzielić na dwie kategorie. Jedna obejmuje
parsery typu push, które w trakcie wczytywania dokumentu kierują wywołania
zwrotne do aplikacji po napotkaniu nowego elementu. Do tej kategorii należą
omawiane w tej technice parsery SAX. Druga grupa to parsery typu pull, które
bardziej przypominają iteratory lub kursory. W tym podejściu to klient musi
bezpośrednio zażądać pobrania następnego elementu. Tak działają opisane w nas-
tępnej technice parsery XmlPull. Typy parserów opisano w tabeli 9.1.
Tabela 9.1. Różne rodzaje parserów dokumentów XML dostępne w Androidzie

Interfejs API DOM SAX XmlPull


Model wewnętrzny Drzewiasty Strumieniowy Strumieniowy
Sposób pobierania Pull (zapytania) Push Pull
Dostęp swobodny Tak Nie Nie
0 TECHNIKA 44. Przetwarzanie danych w XML-u za pomocą interfejsu SAX 381

W tej technice używamy parsera SAX z Androida. SAX to specyfikacja sterowa-


nego zdarzeniami parsera typu push. Jest to technologia niskopoziomowa, dla-
tego nie powoduje dużych kosztów. Parsery SAX istnieją od wielu lat (od czasu
zyskania popularności przez format XML), co jest widoczne w interfejsie API tych
narzędzi. Interfejs ten jest stosunkowo niewygodny, ale prosty i szybki.
PROBLEM
Szukamy prostego sposobu na przetwarzanie dokumentów XML bez konieczności
przechowywania ich w pamięci. Potrzebujemy parsera, który kieruje wywołania
zwrotne do aplikacji po napotkaniu elementu w dokumencie XML (czyli parsera
typu push).
ROZWIĄZANIE
Ponieważ przetwarzanie w modelu SAX jest sterowane zdarzeniami, należy odróż-
nić parser zgłaszający zdarzenia od odbierającego je obiektu. Ten ostatni to kom-
ponent obsługi SAX-a i trzeba w nim zaimplementować interfejs ContentHandler.
Wygodny sposób na wykonanie tego zadania to utworzenie klasy pochodnej od
klasy DefaultHandler i przeciążenie odpowiednich części interfejsu. Na rysunku 9.7
pokazano w uproszczony i ogólny sposób, jak działa parser SAX.

Rysunek 9.7. Parser SAX wczytuje dokument XML jako strumień z obiektu klasy
InputStream Javy i kieruje wywołania zwrotne do obiektu klasy ContentHandler
po wczytaniu encji w rodzaju elementu XML lub węzła tekstowego. Jest więc
sterowany zdarzeniami i „wypycha” (ang. push) treść dokumentu do komponentu
obsługi

Przed rozpoczęciem przetwarzania dokumentu XML należy poznać jego


strukturę — jakie znaczniki aplikacja napotka i w jaki sposób ma przetwarzać war-
tości (można na przykład przetwarzać łańcuchy znaków z wartościami na typy
liczbowe Javy). Ponieważ przykładowa aplikacja pobiera dokument XML z usługi
TMDb, warto przyjrzeć się typowej odpowiedzi zwracanej przez tę usługę
w reakcji na żądanie jednego filmu (w celu zwiększenia czytelności kodu na lis-
tingu 9.6 skróciliśmy niektóre informacje).
382 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Listing 9.6. Format odpowiedzi XML od metody Movie.imdbLookup usługi TMDb

<?xml version="1.0" encoding="UTF-8"?>


<OpenSearchDescription
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<opensearch:Query searchTerms="tt1375666"/>
<opensearch:totalResults>1</opensearch:totalResults>
<movies>
<movie>
<popularity>3</popularity>
<translated>true</translated>
<adult>false</adult>
<language>en</language>
<name>Inception</name>
<alternative_name>Eredet</alternative_name>
<type>movie</type>
<id>27205</id>
<imdb_id>tt1375666</imdb_id>
<url>http://www.themoviedb.org/movie/27205</url>
<overview>In a world where technology exists to enter
the human mind through dream invasion, a single idea within
one's mind can be the most dangerous weapon or the most
valuable asset.</overview>
<rating>9.0</rating>
...
</movie>
</movies>
</OpenSearchDescription>

Nie ma tu nic wyjątkowo zaskakującego. Usługa zwraca tytuł filmu, identyfikator,


gatunki, czas trwania, ocenę (tego potrzebujemy) i kilka innych informacji. Aby
przetworzyć dokument za pomocą parsera SAX, trzeba reagować na zdarzenia
komponentu obsługi po napotkaniu potrzebnego dokumentu XML. Zwykle
potrzebne zdarzenia SAX są związane z granicami dokumentu, granicami ele-
mentu i prostymi węzłami tekstowymi. Zdarzenia te i powiązane z nimi metody
ContentHandler przedstawiono w tabeli 9.2.

Tabela 9.2. Zdarzenia parsera SAX i odpowiadające im metody z interfejsu


ContentHandler

Wywoływane zwrotnie metody


Zdarzenie parsera SAX
interfejsu ContentHandler
Początek i koniec dokumentu startDocument, endDocument
Otwarcie i zamknięcie elementu (znacznika) startElement, endElement
Znaleziono tekst characters

W specyfikacji SAX zdefiniowanych jest więcej zdarzeń, dotyczących na przykład


wiązania przestrzeni nazw i przetwarzania instrukcji, ale w omawianym przykła-
dzie nie mają one znaczenia.
Standardowe podejście polega na wykonaniu w metodzie startDocument ope-
racji wstępnych, takich jak utworzenie pustego obiektu (tu jest to obiekt klasy
Movie) na przetworzone dane z dokumentu, a następnie pobraniu tekstu (na przy-
0 TECHNIKA 44. Przetwarzanie danych w XML-u za pomocą interfejsu SAX 383

kład tytułu filmu) w metodzie characters i wykorzystaniu tekstu w metodzie end


´Element do zapełnienia pola obiektu.
Tu interesuje nas tylko ocena filmu i — dla porządku — jego tytuł. Na lis-
tingu 9.7 znajduje się kod komponentu obsługi parsera SAX.

Listing 9.7. Parser SAXMovieParser przetwarza dokument z danymi o filmie z usługi


TMDb na podstawie specyfikacji SAX

public class SAXMovieParser extends DefaultHandler {


private Movie movie;
private StringBuilder elementText;

public static Movie parseMovie(InputStream xml)


throws Exception {
SAXMovieParser parser = new SAXMovieParser();
Xml.parse(xml, Encoding.UTF_8, parser);
return parser.getMovie();
}

public Movie getMovie() {


return movie;
}

@Override
public void startDocument() throws SAXException {
elementText = new StringBuilder();
}

@Override
public void startElement(String uri,
String localName, String qName,
Attributes attributes) throws SAXException {
if ("movie".equals(localName)) {
movie = new Movie();
}
}

@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
elementText.append(ch, start, length);
}

@Override
public void endElement(String uri, String localName,
String qName)
throws SAXException {

if ("name".equals(localName)) {
movie.setTitle(elementText.toString().trim());
} else if ("rating".equals(localName)) {
movie.setRating(elementText.toString().trim());
}
elementText.setLength(0);
}
}
384 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Wspomnieliśmy już, że najłatwiej jest utworzyć klasę pochodną od klasy Default


´Handler , ponieważ uzyskujemy w ten sposób domyślne implementacje
wszystkich metod interfejsu (przy czym domyślne implementacje nie wykonują
żadnych operacji), dlatego wystarczy zaimplementować tylko potrzebne wywoła-
nia zwrotne. Warto też zdefiniować metodę pomocniczą uruchamiającą przetwa-
rzanie , która tworzy obiekt komponentu obsługi i przekazuje go do metody
narzędziowej Xml.parse Androida. Metoda ta tworzy egzemplarz parsera SAX
i rozpoczyna przetwarzanie. Po zakończeniu przetwarzania egzemplarz klasy
Movie ma określone wartości wszystkich pól, dlatego można go zwrócić do jed-
nostki wywołującej.
Jednym z wywołań zwrotnych parsera jest tu metoda startDocument , w któ-
rej wykonujemy pewne operacje konfiguracyjne. W celach demonstracyjnych
przesłaniamy też metodę startElement , aby wykryć znacznik <movie>. Po jego
napotkaniu tworzymy nowy egzemplarz klasy Movie. Aplikacja przetwarza tylko
jeden film, dlatego operację tę można umieścić w innym miejscu, jeśli jednak
konieczne jest przetworzenie listy elementów, należy zastosować przedstawione
tu rozwiązanie.
Metodę characters aplikacja wywołuje przy każdym napotkaniu tekstu,
który nie jest elementem strukturalnym (czyli znacznikiem lub nagłówkiem).
Trzeba zauważyć, że takim „tekstem” może być odstęp między dwoma znaczni-
kami, dlatego warto przeprowadzać testy i pomijać odstępy. Ponadto dwa przyle-
głe fragmenty z jednego węzła tekstowego mogą spowodować zgłoszenie kilku
zdarzeń napotkania tekstu, dlatego aby pobrać całą zawartość węzła, należy zapi-
sywać jej fragmenty w buforze do momentu napotkania znacznika zamykającego.
Natrafienie na niego prowadzi do wywołania metody endElement . Na tym etapie
wiadomo, że bufor obejmuje tekst danego elementu. Można więc przypisać
zawartość bufora do odpowiedniego pola, na przykład z tytułem lub oceną. Należy
też opróżnić bufor, aby nie zapisywać w nim tekstu wszystkich elementów.
OMÓWIENIE
Po zapoznaniu się z opartym na zdarzeniach modelu działania parsera SAX
przetwarzanie krótkich dokumentów powinno być proste. Ponieważ SAX to par-
ser strumieniowy, nie przechowuje wewnętrznej reprezentacji dokumentu, dla-
tego umożliwia wydajne przetwarzanie nawet długich plików.
Brak wewnętrznej reprezentacji jest jednocześnie największą wadą parse-
rów SAX, ponieważ dostępnych jest niewiele kontekstowych informacji na temat
miejsca wystąpienia zdarzenia. Co zrobić na przykład, jeśli w dokumencie ist-
nieje wiele znaczników o tej samej nazwie? W przykładowym kodzie w XML-u
z listingu 9.6 znajduje się znacznik name z tytułem filmu. Załóżmy, że nazwy
kategorii nie są zapisywane jako atrybuty, ale są zapisywane w odrębnych znacz-
nikach, także nazwanych name (jest to zupełnie poprawne podejście). Po napo-
tkaniu elementu name nie wiadomo wtedy, czy obejmuje on tytuł filmu, czy nazwę
kategorii. Dlatego w bardziej złożonych dokumentach o średnim lub wysokim
0 TECHNIKA 45. Przetwarzanie dokumentów XML na podstawie specyfikacji XmlPull 385

stopniu zagnieżdżenia trzeba w komponencie obsługi przechowywać informacje


o aktualnej pozycji w dokumencie. Można też ustawiać flagi logiczne, określające,
że parser znajduje się w elemencie kategorii. Zarządzanie takim rozwiązaniem
bywa żmudne i uciążliwe.
Inną wadą parserów SAX jest to, że zgłaszane są wywołania zwrotne dotyczące
wszystkich zdarzeń, także tych, które nie są aplikacji potrzebne. Konieczne jest
zaimplementowanie wszystkich wywołań zwrotnych (choć przynajmniej nie
trzeba pisać kodu każdego z nich, ponieważ istnieje klasa DefaultHandler parsera
SAX i dodatkowe klasy pomocnicze w pakiecie android.sax). Jednym z rozwią-
zań tego problemu jest poruszanie się po dokumencie XML fragment po frag-
mencie i pomijanie elementów, których aplikacja nie potrzebuje. Tak działają
parsery typu pull. Opisujemy je w następnej technice.

0 TECHNIKA 45. Przetwarzanie dokumentów XML na podstawie


specyfikacji XmlPull

Czasem potrzebne są konkretne informacje ukryte w dużym dokumencie XML.


Aplikacja ma przejść do odpowiedniego elementu, wczytać jego wartość, a następ-
nie zakończyć przetwarzanie dokumentu. W innych sytuacjach programista może
przetwarzać cały dokument, ale bez posługiwania się wywołaniami zwrotnymi.
Ma to pozwolić zachować większą kontrolę nad poruszaniem się po dokumencie
przez bezpośrednie żądania pobrania następnego elementu. W obu sytuacjach
przydatna może być androidowa implementacja parsera typu pull.
PROBLEM
Szukamy prostego sposobu na przetwarzanie dokumentów XML bez koniecz-
ności przechowywania ich w pamięci. Parser ma bezpośrednio pobierać następną
encję (element lub tekst) z dokumentu, zamiast korzystać z wywołań zwrotnych.
Odpowiednie cechy mają parsery typu pull.
ROZWIĄZANIE
Android udostępnia implementację parsera typu pull (KXML2) opartą na spe-
cyfikacji XmlPull (http://www.xmlpull.org). Inaczej niż w specyfikacji SAX, tu
nie trzeba informować komponentu obsługi z wywołaniami zwrotnymi o zda-
rzeniach w rodzaju przetwarzania elementu. Za przetwarzanie w całości odpo-
wiada klient, który musi bezpośrednio zażądać pobrania następnej encji. Możliwe,
że znasz już ten model pobierania. Tak samo działają iteratory Javy i kursory
relacyjnych baz danych. Atrakcyjność modelu wynika z jego szybkości i pro-
stoty. Wystarczy kilka wierszy, aby przetworzyć dokument. Oto krótki fragment
pseudokodu pokazujący, jak można przetwarzać dokumenty XML za pomocą
parserów XmlPull w Javie:
int event = parser.getEventType();
while (event != XmlPullParser.END_DOCUMENT) {
switch (event) {
386 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

case XmlPullParser.START_DOCUMENT:
doSetupWork();
break;
case XmlPullParser.START_TAG:
readElementText(parser.nextText());
break;
...
}
event = parser.next();
}

Model ten ma tę zaletę, że można łatwo przejść do konkretnego elementu, wczy-


tać jego wartość, a następnie szybko zakończyć przetwarzanie, jeśli dalsze infor-
macje nie są potrzebne. Można też przetworzyć cały dokument element po
elemencie. W tym celu należy wywoływać metodę next, która pobiera następną
encję z dokumentu XML. Ta technika działa podobnie jak iteratory, co przed-
stawiono na rysunku 9.8.

Rysunek 9.8. Parser XmlPull (w odróżnieniu od parserów SAX) wczytuje tylko


następną encję z dokumentu. Robi to na żądanie (w reakcji na wywołanie metody
next). Działa więc podobnie jak iterator Javy lub kursor bazy danych

W ramach bezpośredniego porównania implementujemy parser XmlPull będący


odpowiednikiem parsera SAX z poprzedniego przykładu. Nowy parser (listing 9.8)
pobiera tytuł i ocenę filmu z odpowiedzi od usługi TMDb. Trudno wyobrazić
sobie prostsze rozwiązanie. XmlPullParser i powiązane klasy (w odróżnieniu od
klas modeli SAX i DOM) nie są częścią standardowego pakietu JDK 5. Są dołą-
czone do Androida jako niezależna biblioteka org.xmlpull.v1.

Listing 9.8. Klasa XmlPullMovieParser przetwarza dokument z danymi o filmie


z usługi TMDb zgodnie ze specyfikacją XmlPull

public class XmlPullMovieParser {

private XmlPullParser xpp;

public static Movie parseMovie(InputStream xml)


throws Exception {
return new XmlPullMovieParser().parse(xml);
}
0 TECHNIKA 45. Przetwarzanie dokumentów XML na podstawie specyfikacji XmlPull 387

public Movie parse(InputStream xml) throws Exception {


Movie movie = new Movie();

xpp = XmlPullParserFactory.newInstance().newPullParser();
xpp.setInput(xml, "UTF-8");

skipToTag("name");
movie.setTitle(xpp.nextText());

skipToTag("rating");
movie.setRating(xpp.nextText());

return movie;
}

private void skipToTag(String tagName) throws Exception {


int event = xpp.getEventType();
while (event != XmlPullParser.END_DOCUMENT
&& !tagName.equals(xpp.getName())) {
event = xpp.next();
}
}
}

Podobnie jak w komponencie obsługi parsera SAX, tak i tu definiujemy sta-


tyczną metodę pomocniczą do wywoływania parsera typu pull. Jednak tu frag-
ment ten uproszczony jest do jednego wiersza kodu, który tworzy egzemplarz
parsera danych o filmie i kieruje wywołanie do tego parsera . Główne operacje
wykonujemy w metodzie parse. Najpierw należy utworzyć i zainicjować obiekt
klasy XmlPullParser . Warto zauważyć, że zwykle lepiej jest zapisać ten egzem-
plarz w pamięci, zamiast za każdym razem tworzyć go od nowa. Podobnie jak
w parserze SAX, tak i tu do tworzenia egzemplarza służy fabryka, ponieważ
w Javie w pakietach XML-a zdefiniowane są tylko interfejsy, natomiast ich imple-
mentacje są typowe dla bibliotek. Na przykład pakiet Sun JDK obejmuje inną
implementację parsera SAX niż platformy Apache Harmony i Android. W kodzie
można też wykorzystać inną implementację klasy XmlPullParser. Rzadko jest to
przydatne, jednak warto wiedzieć, że taka możliwość istnieje.
Dalsza część metody parse jest niezwykle prosta. Interesują nas tylko dwa
pola (z tytułem i oceną filmu), dlatego możemy przejść do odpowiadających im
pól i użyć tekstu do zapełnienia właściwych pól obiektu klasy Movie . Metoda
pomocnicza skipToTag uruchamia pętlę, która kończy działanie albo w momen-
cie, kiedy nie ma dalszych danych do przetworzenia (po napotkaniu końca doku-
mentu), albo po znalezieniu potrzebnego znacznika. Metoda next przechodzi do
następnej encji (elementu, tekstu, instrukcji przetwarzania itd.), określa, jakiego
rodzaju jest to encja, i ustala zwracaną stałą liczbową. Po znalezieniu wszystkich
potrzebnych danych można zakończyć pracę parsera.
388 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

OMÓWIENIE
Choć ogólnie wydajność parserów XmlPull jest podobna do wydajności parserów
SAX (przy czym obie te klasy są znacznie szybsze od parserów DOM, na co wska-
zuje wiele testów porównawczych), parsery XmlPull są szybsze przy przetwa-
rzaniu dużych dokumentów, kiedy aplikacja ma wczytać tylko niewielki frag-
ment całego dokumentu. Wynika to z tego, że parser nie otrzymuje wywołań
zwrotnych dla wszelkich możliwych zdarzeń. Ponadto przetwarzanie można prze-
rwać w dowolnym momencie, co pozwala szybko zakończyć pracę.
Inną zaletą parserów XmlPull w porównaniu z parserami SAX jest prosty
i intuicyjny interfejs. Jest on zgodny z regułą projektową „dostajesz to, czego
żądasz”, co często ułatwia napisanie i zrozumienie kodu. Parsery XmlPull
(w odróżnieniu od parserów SAX) nie zwracają na przykład niepotrzebnych odstę-
pów, chyba że programista sam tego zażąda. Dlatego nie trzeba wywoływać uży-
wanej na listingu 9.7 metody trim.
Wadą wspólną dla parserów XmlPull i SAX jest brak wewnętrznej struktury
danych reprezentującej przetwarzany dokument. Oznacza to, że niemożliwy jest
dostęp swobodny do dokumentu. Ponadto dla złożonych dokumentów trzeba prze-
chowywać informacje o kontekście aktualnie przetwarzanego elementu. XmlPull
udostępnia w tym celu metodę getDepth, określającą, jak głęboko dany element
jest zagnieżdżony w drzewie dokumentu XML. Stosowanie parsera strumienio-
wego związane jest więc z pewnymi kompromisami.
UWAGA. Warto wspomnieć, że specyfikację SAX można traktować jak
abstrakcję specyfikacji XmlPull, ponieważ każdą implementację specyfi-
kacji SAX można przedstawić za pomocą parsera typu pull. W zasadzie
wszystkie parsery SAX są też parserami typu pull, ponieważ przed zgłosze-
niem wywołania zwrotnego muszą wczytać encję ze strumienia. Można
więc stwierdzić, że parsery XmlPull działają na niższym poziomie, a przy
tym udostępniają elegancki, wygodny interfejs.
Choć parsery SAX i XmlPull pozwalają na sprawdzanie poprawności dokumentu,
a nawet typów danych (pod warunkiem, że istnieje umożliwiający to plik sche-
matu), jedyny typ danych znany na poziomie interfejsu API to typ tekstowy.
Oznacza to, że trzeba samodzielnie przekształcać łańcuchy znaków na liczby,
a nawet poddrzewa na obiekty. Przypomnij sobie podelement kategorii z elementu
filmu. Konieczne może być przekształcenie takiego podelementu w odrębny
obiekt.
Omówiliśmy już w pewnym zakresie dokumenty XML. Istnieje też zupełnie
inne rozwiązanie opisywanych tu problemów. Pozwala ono wczytać do pamięci
dokument jako pojedynczą strukturę danych, po której można łatwo się poruszać.
Odbywa się to jednak bez charakterystycznego dla modelu DOM spadku wydaj-
ności. Pora opuścić świat XML-a i wkroczyć w świat JavaScriptu. Chwileczkę,
JavaScriptu? Poznaj JSON.
0 TECHNIKA 46. Przetwarzanie danych w formacie JSON 389

0 TECHNIKA 46. Przetwarzanie danych w formacie JSON

Moda na Ajax (ang. Asynchronous JavaScript and XML) związana z nurtem


Web 2.0 miała duży wpływ na powstanie nowoczesnych witryn działających
podobnie jak tradycyjne aplikacje. Co ciekawe, Ajax doprowadził do spopulary-
zowania nie tylko klasy XMLHttpRequest (nigdy wcześniej wymyślenie lub ponowne
odkrycie jednej klasy nie miało tak dużego wpływu), ale też innego rozwiązania,
które było dostępne od lat, choć programiści nie potrafili dostrzec jego pełnego
potencjału. Chodzi o format JSON (ang. JavaScript Object Notation).
Format JSON powstał w 1999 roku, jednak długo był znany głównie w spo-
łeczności programistów witryn internetowych. JSON, podobnie jak XML, to
otwarty, ustandaryzowany format wymiany danych, oparty jednak na podzbiorze
języka JavaScript. Każdy obiekt w JSON-ie jest też poprawnym obiektem Java-
Script, choć zasada ta nie działa w drugą stronę. JSON z natury dobrze nadaje
się do serializowania danych przesyłanych z serwera WWW do środowiska
z obsługą JavaScriptu, na przykład do przeglądarek, ponieważ odpowiedzi w tym
formacie można bez przekształceń wykonywać po stronie klienta. Wymaga to
tylko wywołania funkcji eval JavaScriptu. Ponieważ format JSON jest „lekki”
i prosty, a przy tym pozwala przedstawiać dane dowolnego rodzaju, programiści
spoza świata JavaScriptu i HTML-u odkryli, że JSON można wykorzystać jako
uniwersalny format do reprezentowania i wymiany danych. Jest on przydatny
zwłaszcza w usługach sieciowych kierujących dane do dowolnych klientów, nie
tylko do przeglądarek. Choć JSON-a rzadziej używa się do lokalnego przecho-
wywania danych w formie tekstowej, jest on stosowany jako format odpowiedzi
w coraz większej liczbie usług sieciowych, na przykład w serwisach Twitter, Qype
i FireEage Yahoo!.
PROBLEM
Chcemy albo zastosować w aplikacji usługę sieciową obsługującą tylko format
JSON, albo wykorzystać zalety JSON-a, na przykład możliwość wydajnego two-
rzenia i przetwarzania tekstowej reprezentacji struktur danych w pamięci.
ROZWIĄZANIE
Przed przejściem do interfejsu API JSON-a z Androida warto przyjrzeć się temu,
jak w JSON-ie reprezentowane są obiekty. Jeśli znasz język JavaScript, zasto-
sowane rozwiązanie powinno wyglądać znajomo. Jednak nawet jeżeli nie znasz
JavaScriptu, szybko zrozumiesz format JSON.
Obiekty w JSON-ie można traktować jak odwzorowania lub obiekty skrótu.
Występuje w nich jeden element nadrzędny, a inne elementy (wartości) są powią-
zane z identyfikatorami (kluczami). Książkę tę można przedstawić w JSON-ie tak:
{
"title": "Android w praktyce",
"price": 49.99,
"authors": [
390 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

{ "name": "C. Collins" },


{ "name": "M. Galpin" },
{ "name": "M. Kaeppler" }
]
}

W tej prostej strukturze danych występują wszystkie elementy składniowe


z JSON-a. Wreszcie natrafiliśmy na rozwiązanie, które jest niezwykle proste, a przy
tym bardzo przydatne. Współcześnie w przemyśle komputerowym rzadko można
natrafić na takie połączenie!
Nawiasy klamrowe w JSON-ie służą do oddzielania obiektów. Obiekt to
odwzorowanie. Odwzorowuje klucze (cudzysłów wokół nich jest konieczny) na
wartości. Wartością może być obiekt, łańcuch znaków (dowolna wartość umiesz-
czona w cudzysłowie jest traktowana jak łańcuch znaków; dlatego cudzysłów
będący częścią samego łańcucha trzeba poprzedzić znakiem ucieczki, na przy-
kład: \"Witaj!\") lub liczba (z kropką dziesiętną lub bez niej). Ponadto wartość
może być tablicą wymienionych wcześniej elementów. Tablice wyodrębniane są
za pomocą nawiasów kwadratowych. Przykładowa tablica występuje na listingu 9.9.

Listing 9.9. Odpowiedź w formacie JSON uzyskana od metody Movie.ImdbLookup


usługi TMDb

[
{
"popularity":3,
"translated":true,
"adult":false,
"language":"en",
"original_name":"Inception",
"name":"Inception",
"alternative_name":"Eredet",
"movie_type":"movie",
"id":27205,
"imdb_id":"tt1375666",
"url":"http://www.themoviedb.org/movie/27205",
"votes":52,
"rating":9.0,
"certification":"PG-13",
"overview":"Dom Cobb (Leonardo DiCaprio) is a skilled thief,
the best in the dangerous art of extraction: stealing valuable
secrets from deep within the subconscious during the dream
state when the mind is at its most vulnerable. ...",
"released":"2010-07-16",
"runtime":148,
"version":226,
"last_modified_at":"2010-08-19 16:04:03",
...
}
]

Choć samodzielne przetwarzanie dokumentów w formacie JSON jest proste, nie


trzeba tego robić. Android udostępnia wzorcową implementację JSON-a (na
witrynie json.org), której możesz użyć. Zanim pokażemy, jak korzystać z tej
0 TECHNIKA 46. Przetwarzanie danych w formacie JSON 391

implementacji do przetwarzania danych w JSON-ie, warto przypomnieć, że


informacje pochodzą z usługi sieciowej TMDb, która zwraca dane także w tym
formacie. Przyjrzyjmy się, jak wygląda odpowiedź z listingu 9.6 zwrócona w for-
macie JSON, a nie XML (także tu z uwagi na zwięzłość pomijamy niektóre
elementy).
Jak widać, odpowiedź to jednoelementowa tablica. Wynika to z tego, że apli-
kacja żąda tylko jednego filmu. Teraz należy przekształcić dane na obiekt klasy
Movie. Na tym etapie trzeba użyć parsera JSON z Androida, co pokazano na
listingu 9.10.

Listing 9.10. Klasa JsonMovieParser przetwarza dokument w formacie JSON


(obejmujący dane o filmie) zwrócony przez usługę TMDb

public class JsonMovieParser {

public static Movie parseMovie(InputStream json) throws Exception {


BufferedReader reader = new BufferedReader(
new InputStreamReader(json));
StringBuilder sb = new StringBuilder();

try {
String line = reader.readLine();
while (line != null) {
sb.append(line);
line = reader.readLine();
}
} catch (IOException e) {
throw e;
} finally {
reader.close();
}
JSONArray jsonReply = new JSONArray(sb.toString());

Movie movie = new Movie();


JSONObject jsonMovie = jsonReply.getJSONObject(0);
movie.setTitle(jsonMovie.getString("name"));
movie.setRating(jsonMovie.getString("rating"));

return movie;
}
}

Parser JSON pracuje na danych w pamięci, dlatego najpierw trzeba wczytać cały
łańcuch znaków z odpowiedzią do bufora . Następnie można użyć tego łań-
cucha do utworzenia obiektu JSON (tu jest nim obiekt klasy JSONArray, ponie-
waż element nadrzędny to tablica), co polega na przetworzeniu łańcucha znaków
z JSON-a na wewnętrzne odwzorowanie klucz-wartość . Interesuje nas tu pierw-
szy i jedyny element tablicy, dlatego należy ustawić obiekt klasy JSONObject na
pozycję 0, której odpowiada szukany film . Każdy tego rodzaju obiekt klasy
JSONObject udostępnia metody dostępowe do wczytywania wartości w rodzaju
łańcuchów znaków, liczb i innych obiektów oraz tablic JSON-a.
392 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

OMÓWIENIE
Możliwe, że zauważyłeś pewną prawidłowość. W każdej kolejnej technice parser
filmów jest coraz krótszy! Format JSON to najbardziej bezpośredni i prosty spo-
sób na przetwarzanie odpowiedzi od usługi sieciowej. Jest też wydajny i „lekki”.
Nie jest niczym więcej jak obiektem klasy HashMap Javy z odwzorowaniem łań-
cuchów znaków na inne obiekty. Jeśli potrafisz używać interfejsu Map Javy, możesz
też korzystać z androidowych klas do obsługi JSON-a.
Ponieważ parser opisany w serwisie json.org działa tak, a nie inaczej, może być
niewydajny, jeśli potrzebujesz niewielkiej ilości danych z dużego dokumentu.
Prawdopodobnie nie warto przetwarzać kilku megabajtów tekstu na obiekt JSON,
ponieważ cały dokument trzeba jednocześnie przechowywać w pamięci. Pod
tym względem parser ten działa podobnie jak XML-owy model DOM. Jeśli jest
to dla Ciebie nieakceptowalne, prawdopodobnie najlepszym wyborem okaże się
parser XmlPull.
STRUMIENIOWE PARSERY DOKUMENTÓW JSON. Jeśli dokumenty
są duże, jednak z jakichś powodów musisz używać JSON-a (możliwe, że
potrzebne dane są dostępne tylko w tym formacie), warto przyjrzeć się
strumieniowym parserom dokumentów JSON, na przykład Jacksonowi
(http://jackson.codehaus.org) i narzędziu gson Google’a (http://code.google.
com/p/google-gson). Prowadzi to do uzależnienia kodu od następnej biblio-
teki, dlatego zastanów się nad wyborem.
Powinieneś wiedzieć o pewnym aspekcie używania parserów JSON w Androidzie.
Przy przetwarzaniu odpowiedzi zawsze trzeba z góry wiedzieć, jakiego rodzaju
wartości się w niej znajdują. Jest tak, ponieważ metody get* klasy JSONObject
zgłaszają wyjątek przy próbie dostępu do pola, które nie istnieje. Jeśli elementy
mogą występować w niektórych odpowiedziach, ale nie pojawiają się w innych
(dotyczy to elementów opcjonalnych), należy zastosować metody opt*. Zwra-
cają one null, jeśli pole nie istnieje. Aby na przykład uzyskać dostęp do opcjo-
nalnego pola z łańcuchem znaków, użyj metody optString zamiast getString.
Omówiliśmy już wiele kwestii! Nie tylko zobaczyłeś, jak nawiązać połączenie
z usługą sieciową, ale wiesz już też, jak przetwarzać typowe odpowiedzi za pomocą
różnych parserów, mających swoje wady i zalety. Jesteś gotów do zastosowania
usług sieciowych w aplikacjach na Android.
W ramach uzupełnienia trzeba poruszyć jeszcze kilka kwestii. Omówiliśmy
już protokół HTTP i dwa popularne formaty wymiany danych, XML i JSON.
Jednak musisz się też przygotować na wystąpienie problemów, takich jak awarie
sieci. W ostatnim podrozdziale opisujemy techniki, które może nie są tak cie-
kawe jak nawiązywanie połączenia z „filmową” usługą sieciową, ale mają bardzo
duże znaczenie w każdej aplikacji komunikującej się z siecią przez protokół HTTP;
są one wisienką na torcie. To od ich zastosowania zależy, czy aplikacja będzie
9.3. Elegancka obsługa awarii sieci 393

doskonała, czy tylko dobra. Dlatego nawet jeśli stwierdzenie „obsługa awarii
połączenia” wydaje Ci się nudne, zapoznanie się z dalszą częścią rozdziału okaże
się wartościowe zarówno dla Ciebie, jak i dla użytkowników Twoich aplikacji.

9.3. Elegancka obsługa awarii sieci


Jeśli dotarłeś do tego miejsca, wiesz już wszystko, co jest potrzebne w Androidzie
do pracy z siecią z wykorzystaniem protokołu HTTP. Wiesz też, jak przetwa-
rzać najpopularniejsze formaty odpowiedzi. Nie byłaby to jednak dobra książka
z przepisami, gdybyśmy ograniczyli się do podstaw, prawda?
Jednym z problemów, które do tej pory pomijaliśmy — a wierz nam, na
pewno się z nim zetkniesz, kiedy zaczniesz udostępniać wymyślne aplikacje sie-
ciowe na Android — są awarie sieci. Pamiętaj, że użytkownicy nie zawsze siedzą
na kanapie, kiedy korzystają z aplikacji. Możliwe, że się poruszają, a jeśli robią
to szybko, przechodzą między sieciami Wi-Fi, nadajnikami komórkowymi lub
ze standardu 3G do standardu 2G z uwagi na spadek siły sygnału. Skompliko-
wana sytuacja ma miejsce przy przełączaniu się z sieci Wi-Fi na sieć komórkową
lub w drugą stronę, ponieważ żądania przesyłane przez operatorów sieci komór-
kowych muszą czasem przechodzić przez serwery pośredniczące, a połączeń
Wi-Fi to nie dotyczy. Oznacza to dwie rzeczy:
1. Trzeba zastosować odpowiednią metodę ponawiania nieudanych żądań
(technika 47.).
2. Trzeba reagować na zmiany w konfiguracji sieci (technika 48.).
Zaczynamy od pokazania, jak zaimplementować komponent obsługi ponawiania
żądań HTTP dobrze dostosowany do środowiska mobilnego.

0 TECHNIKA 47. Ponawianie żądań za pomocą komponentów obsługi

Wyobraź sobie, że w czasie przerwy obiadowej idziesz coś zjeść. Jeszcze w budynku
firmy wyciągasz telefon z Androidem, uruchamiasz ulubioną aplikację i zaczynasz
szukać informacji o pobliskich restauracjach. Telefon jest połączony z firmową
siecią Wi-Fi, jednak kiedy wychodzisz na ulicę (nadal szukając dobrego miejsca
na obiad), połączenie nagle zostaje zerwane. Sygnał sieci Wi-Fi jest zbyt słaby,
dlatego Android przełącza się na sieć mobilną.
Jest to częsty scenariusz. Jeśli w aplikacji często wykorzystuje się połączenia
z siecią mobilną, żądania nieustannie pozostają nieobsłużone. Takie połączenia
z natury są niestabilne. Co to oznacza dla autora aplikacji? Co się dzieje, kiedy
aplikacja próbuje przesłać żądanie HTTP i w tym samym czasie następuje zerwa-
nie połączenia? Można przechwycić błąd i poinformować o nim użytkownika.
To jednak za mało, prawda? Jeśli żądanie nie zostało obsłużone, ponieważ nastą-
piło przełączenie z sieci Wi-Fi na sieć 3G, zgłoszenie następnego żądania praw-
dopodobnie zakończy się sukcesem. Dlatego zamiast kłopotać użytkownika oknem
394 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

dialogowym z komunikatem o błędzie i prosić o ponowienie próby, aplikacja


może sama jeszcze zgłosić żądanie. Wymaga to odpowiedniego kodu do pona-
wiania żądań.
PROBLEM
Ponieważ wiemy, że żądania HTTP mogą nie zostać obsłużone, kiedy użyt-
kownik się porusza, chcemy dodać odpowiedni system ponawiania żądań. Kiedy
żądanie nie zostanie obsłużone z uwagi na niestabilną sieć, można automatycz-
nie je ponowić.
ROZWIĄZANIE
Jeśli programista używa klasy HttpClient Apache’a, a nie klasy HttpURLConnection
Javy, znajduje się w komfortowej sytuacji. Klasa HttpClient ma wbudowany pro-
sty system ponawiania żądań. Wydaje się więc, że problem jest rozwiązany, ale
to nieprawda. Przedstawiamy tu działanie wspomnianego systemu, a następnie
wyjaśniamy, dlaczego często okazuje się niewystarczający. Pokazujemy też, jak
go usprawnić.
AbstractHttpClient to domyślna klasa bazowa z biblioteki HttpClient Apache’a.
W każdej klasie typu HttpClient pochodnej od klasy AbstractHttpClient (także
w klasie DefaultHttpClient używanej w technikach 42. i 43.) trzeba określić, czy
żądanie należy przesłać ponownie po wystąpieniu wyjątku IOException i czy tylko
wtedy. Jeśli nieudana obsługa żądania wynika z wystąpienia wyjątku innego
rodzaju, nie następuje ponowienie próby, a komunikat o błędzie jest przekazywany
do jednostki wywołującej. Za decyzję o ponowieniu próby odpowiada obiekt
klasy HttpRequestRetryHandler. Zdefiniowana jest w nim służąca do podejmowa-
nia decyzji metoda retryRequest, która zwraca wartość true, jeśli należy ponowić
żądanie, i wartość false w przeciwnym razie. Metoda ta otrzymuje w postaci
argumentów zgłoszony wyjątek, liczbę podjętych już prób i kontekst wykonania
żądania. Na podstawie tych informacji metoda ustala, czy ma ponowić żądanie.
Jeśli korzystasz z klasy DefaultHttpClient, nie musisz tworzyć własnego kodu
do podejmowania decyzji. W klasie tej zdefiniowana jest klasa DefaultHttpRequest
´RetryHandler z dobrym domyślnym algorytmem decyzyjnym. Oto sytuacje,
w których nie następuje ponowienie żądania:
Q Przekroczenie limitu prób. Ponowienie żądania nie następuje, jeśli
przekroczono maksymalną liczbę prób.
Q Wystąpienie nierozwiązywalnych problemów. Ponowienie próby nie
następuje, jeśli problem wynika z braku możliwości znalezienia hosta
(błąd DNS), przekroczenia czasu obsługi żądania, bezpośredniej odmowy
nawiązania połączenia przez serwer lub nieudanej wymiany informacji
przez protokół SSH.
0 TECHNIKA 47. Ponawianie żądań za pomocą komponentów obsługi 395

Q Brak powtarzalności żądań. Ponowienie próby nie następuje, jeśli żądanie


nie jest powtarzalne (czyli niebezpieczne jest ponowne przesyłanie go; jest
tak zawsze przy zgłaszaniu żądań POST i DELETE). Warunek ten można
wyłączyć.
Bezpośrednio w komponencie obsługi ponawiania żądań można zmienić mak-
symalną liczbę prób (wartość domyślna to 3) i określić, czy aplikacja ma ponawiać
żądania, jeśli nie jest to bezpieczne (wartość domyślna to false). Dalej pokazu-
jemy, jak to zrobić. Wprowadzanie dodatkowych zmian wymaga utworzenia wła-
snej implementacji.
Choć przedstawione warunki są przydatne, w kontekście mobilnym z syste-
mem ponawiania żądań związany jest pewien ogólny problem. Ponowienie ma
miejsce natychmiast po nieudanej próbie. Wróćmy do przykładu z przełącza-
niem się z sieci Wi-Fi na sieć 3G, co może zająć przynajmniej sekundę. Wtedy
system ponawiania prób jest bezużyteczny, ponieważ wysyła kolejne żądania
zbyt szybko. Problem można złagodzić przez ustawienie maksymalnej liczby
ponowień na znacznie wyższą wartość, na przykład 20, jednak przydatniejszy
jest mechanizm oparty na czasie. Lepsze rozwiązanie polega na zdefiniowaniu
niestandardowego komponentu obsługi ponowień żądań, który wykorzystuje kod
do podejmowania decyzji z domyślnego komponentu, jednak wprowadza krótki
okres bezczynności, aby umożliwić telefonowi ponowne nawiązanie połączenia.
Potrzebny kod znajdziesz na listingu 9.11.

Listing 9.11. Niestandardowy komponent obsługi ponawiania żądań z aplikacji


MyMovies

public class MyMovies extends ListActivity implements Callback,


OnItemLongClickListener {

private static final AbstractHttpClient httpClient;

private static final HttpRequestRetryHandler retryHandler;

static {
...
httpClient = new DefaultHttpClient(cm, clientParams);

retryHandler =
new DefaultHttpRequestRetryHandler(5, false) {

public boolean retryRequest(IOException ex, int execCount,


HttpContext context) {
if (!super.retryRequest(ex, execCount, context)) {
Log.d("Komponent ponawiania żądań", "Bez ponowienia");
return false;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
396 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Log.d("Komponent ponawiania żądań", "Ponowienie żądania...");


return true;
}
};
httpClient.setHttpRequestRetryHandler(retryHandler);
}

public static HttpClient getHttpClient() {


return httpClient;
}
...
}

Najpierw trzeba zmienić typ z HttpClient na AbstractHttpClient , ponieważ


metody potrzebne do ustawienia niestandardowych komponentów obsługi nie
są częścią interfejsu HttpClient. Aplikacja zapisuje też statyczną referencję do nie-
standardowego komponentu obsługi . W statycznej metodzie inicjującej, gdzie
aplikacja ustawia obiekt klienta, inicjujemy komponent obsługi ponawiania żądań
przez utworzenie klasy pochodnej od implementacji domyślnej . W tym miejscu
określamy też maksymalną liczbę prób na pięć (w parametrze requestSentRetry
´Enables zostawiamy domyślną wartość false). Decyzję o ponowieniu próby
nadal podejmuje implementacja domyślna , ponieważ — jak już wspomnieli-
śmy — dobrze wykonuje to zadanie. Dodajemy okres bezczynności równy dwóm
sekundom (wątek zostaje na ten czas uśpiony), po czym następuje ponowienie
żądania . Trzeba też poinformować klasę HttpClient, że ma użyć niestandar-
dowego komponentu obsługi zamiast wbudowanego .
OMÓWIENIE
Podsumujmy to, co zrobiliśmy. Przez zaimplementowanie i skonfigurowanie nie-
standardowej klasy HttpRequestRetryHandler poinformowaliśmy klasę HttpClient,
co ma zrobić po zerwaniu przez serwer WWW połączenia w czasie próby wysła-
nia żądania. Zerwanie połączenia oznacza tu dowolną sytuację powodującą do
zgłoszenia wyjątku IOException Javy w trakcie przetwarzania żądania sieciowego.
Aplikacja ponawia żądanie do pięciu razy, a decyzję o tym, czy próbę należy
ponowić, podejmuje obiekt klasy DefaultHttpRequestRetryHandler. Przed ponow-
nym przesłaniem żądania aplikacja na krótki czas usypia wątek, aby umożliwić
urządzeniu przywrócenie stanu na przykład po zmianie sieci z Wi-Fi na mobilną.
Przedstawiony kod działa w niemal każdej sytuacji, ma jednak pewne ograni-
czenie. Nie radzi sobie z awariami połączeń spowodowanymi przez serwer WWW.
Jeśli serwer jest mocno obciążony, może wysłać do klienta kod odpowiedzi 5xx.
Takie kody, na przykład 503 (usługa niedostępna) i 504 (przekroczenie limitu
czasu oczekiwania na bramę), to objawy dużego obciążenia systemu, często wska-
zujące na tymczasowe awarie. Na przykład w usłudze sieciowej serwer aplikacji
może oczekiwać na odpowiedź od bazy danych, która nie reaguje wystarczająco
szybko. Limit czasu oczekiwania może być tu krótki, ponieważ serwery baz danych
powinny zwykle odpowiadać szybko.
0 TECHNIKA 48. Obsługa zmian konfiguracji sieci 397

W sytuacjach podobnych do opisanej prawdopodobne jest, że po odczekaniu


sekundy lub dwóch następne żądania zostaną prawidłowo obsłużone, dlatego
warto ponowić próbę. Niestety, w obecnym rozwiązaniu tak się nie dzieje, ponie-
waż ponowienie żądania ma miejsce tylko po błędach wejścia-wyjścia, natomiast
kod 5xx to prawidłowa i kompletna odpowiedź HTTP, która z perspektywy klasy
HttpClient nie jest żadnym błędem. Jeśli chcesz napisać niezawodny kod do łącze-
nia się z usługą sieciową, powinieneś samodzielnie dodać drugą warstwę obsługi
ponawiania prób, działającą nad klasą HttpClient. Warstwa ta może być prosta
i obejmować pętlę, która kilkukrotnie usypia wątek na kilka sekund i ponawia
żądanie.
W początkowym fragmencie wspomnieliśmy, że zerwanie połączenia nie jest
jedynym problemem, który trzeba rozwiązać. Ponowne przesłanie żądania po
zmianie konfiguracji sieci (na przykład ustawień serwera pośredniczącego) może
prowadzić do tego, że wszystkie kolejne próby zakończą się niepowodzeniem.
W następnej technice wyjaśniamy, na co należy zwrócić uwagę i jak rozwiązać
problem.

0 TECHNIKA 48. Obsługa zmian konfiguracji sieci

W poprzedniej technice wyjaśniliśmy, że w aplikacjach mobilnych warto pona-


wiać nieobsłużone żądania. Jest to przydatne na przykład wtedy, kiedy urządze-
nie przełącza się z punktu dostępowego sieci Wi-Fi na punkt dostępowy sieci
komórkowej (sieci operatora APN), ponieważ użytkownik wyszedł z zasięgu sieci
Wi-Fi. Możliwe też, że urządzenie przełącza się z jednej sieci operatora APN
do drugiej z uwagi na przejście użytkownika między sieciami różnych operatorów.
Występuje jednak pewien problem. Ponowienie żądania za pomocą nowego połą-
czenia sieciowego (z siecią operatora APN zamiast z siecią Wi-Fi) może prowadzić
do kolejnych błędów. Tym razem wynikają one z innego powodu — w nowej
sieci operatora APN używane są inne ustawienia.
Nawet jeśli użytkownik nie przechodzi między sieciami, w sieciach operato-
rów APN stosuje się specjalne, właściwe dla danej sieci ustawienia, które trzeba
uwzględnić w aplikacji przy wysyłaniu żądań przez określoną sieć. Te ustawienia
to na przykład nazwa użytkownika i hasło do sieci operatora APN, a także ser-
wery pośredniczące, przez które kierowane są żądania. Na rysunku 9.9 widoczny
jest domyślny interfejs Androida do zmiany ustawień sieci operatora APN w tele-
fonie (zrzut wykonano na emulatorze Androida).
Aplikacja musi uzyskać dostęp do wspomnianych ustawień i otrzymywać
powiadomienia na przykład po zmianie serwera pośredniczącego. Pozwala to
zaktualizować obiekt klasy HttpClient i kierować żądania przez nowy serwer
pośredniczący.
398 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

Rysunek 9.9. Na stronie Edit Access Point


użytkownicy mogą skonfigurować ustawienia
operatora APN dla mobilnych połączeń
do transferu danych (można na przykład
ustawić serwery pośredniczące). Po zapisaniu
tych ustawień w czasie działania aplikacji
warto je uwzględnić, aby uniknąć problemów
z obsługą żądań

PROBLEM
Przemieszczanie się użytkownika powoduje, że w trakcie działania aplikacji zmia-
nie może ulec konfiguracja sieci. Aby zapewnić działanie kodu do obsługi sieci,
trzeba reagować na zmiany w ustawieniach.
ROZWIĄZANIE
Do zarządzania zmianami w konfiguracji sieci operatora APN lub w stanie
połączenia (takimi jak zerwanie komunikacji) służy klasa ConnectivityManager
frameworku. W reakcji na zmianę klasa ta rozsyła do wszystkich subskrybentów
komunikaty rozgłoszeniowe z informacjami o nowym stanie sieci. Komunikaty
rozgłoszeniowe są w Androidzie wysyłane za pomocą intencji rozgłoszeniowych,
dlatego do obsługi takich wiadomości trzeba użyć odbiornika typu Broadcast
´Receiver. Zacznijmy od tego, jak zarejestrować odbiornik tego typu w aktywności.
public void onCreate(Bundle savedInstanceState) {

registerReceiver(new ConnectionChangedBroadcastReceiver(),
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}

Trzeba więc zasubskrybować zdarzenie CONNECTIVITY_ACTION klasy Connection


´Manager, używając obiektu klasy IntentFilter. Ten ostatni obiekt gwarantuje,
że odbiornik będzie otrzymywał rozgłoszeniowe intencje tylko konkretnego
rodzaju (w tej technice interesują nas tylko one). To już wszystkie operacje konfi-
guracyjne. Od tej pory aplikacja zawsze będzie informowana o zmianach w stanie
połączenia! Ciekawszy jest kod odbiornika ConnectionChangedBroadcastReceiver.
0 TECHNIKA 48. Obsługa zmian konfiguracji sieci 399

Odbiornik ten udostępnia tylko jedną metodę, onReceive, w której obsługiwane są


zmiany w konfiguracji sieci. Na listingu 9.12 pokazano, jak w aplikacji MyMovies
zaimplementować tę metodę pod kątem zmiany serwera pośredniczącego.
UWAGA. Aby aplikacja otrzymywała rozsyłane zdarzenia o zmianach
w stanie połączenia, w manifeście Androida trzeba zażądać uprawnienia
ACCESS_NETWORK_STATE.

Listing 9.12. Odbiornik obsługujący zmiany w konfiguracji sieci

public class ConnectionChangedBroadcastReceiver extends BroadcastReceiver {

public void onReceive(Context context, Intent intent) {


NetworkInfo nwInfo = intent.getParcelableExtra(
ConnectivityManager.EXTRA_NETWORK_INFO);

HttpParams httpParams = MyMovies.getHttpClient().getParams();


if (nwInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
String proxyHost = Proxy.getHost(context);
if (proxyHost == null) {
proxyHost = Proxy.getDefaultHost();
}
int proxyPort = Proxy.getPort(context);
if (proxyPort == -1) {
proxyPort = Proxy.getDefaultPort();
}
if (proxyHost != null && proxyPort > -1) {
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
httpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
} else {
httpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, null);
}
} else {
httpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, null);
}
}
}

Informacje o zmianach są zapisane w obiekcie NetworkInfo, który aplikacja prze-


kazuje w rozgłoszeniowej intencji. Dlatego obiekt ten trzeba najpierw pobrać
z dodatkowych danych intencji . Jeśli aplikacja używa mobilnego połączenia
do transferu danych (TYPE_MOBILE), należy wczytać serwer pośredniczący i jego
port, które to dane użytkownik wprowadził w ustawieniach APN. Jeśli użytkow-
nik nie podał tych informacji, należy użyć wartości domyślnych ( i ). Jeśli obie
informacje są podane i prawidłowe, należy zaktualizować parametry połączenia
w obiekcie typu HttpClient, aby żądania zawsze były przesyłane przez określony
serwer pośredniczący .
I to już wszystko! Teraz aplikacja automatycznie aktualizuje klienta HTTP,
aby korzystał z odpowiednich ustawień serwera pośredniczącego po zmianie
parametrów sieci mobilnej przez użytkownika. Nawet jeśli użytkownik sam nie
określa parametrów serwera, ale operator ustawia domyślny serwer pośredniczący,
400 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe

aplikacja będzie działać poprawnie, ponieważ potrzebna intencja jest wysyłana


nie tylko po zmianie ustawień, ale też w momencie pierwszego uruchomienia
aplikacji.
OMÓWIENIE
Warto bliżej przyjrzeć się obiektowi klasy NetworkInfo. W dokumentacji tej klasy
znajdziesz dodatkowe wartości (na przykład EXTRA_REASON i EXTRA_EXTRA_INFO) prze-
syłane w intencji. Wartości te można wczytać i opcjonalnie wyświetlić użyt-
kownikowi. Jednak w zakresie rejestrowania informacji najprzydatniejsza jest
metoda toString, ponieważ powoduje wyświetlanie wszystkich danych. Zapisuje
ona wszystkie informacje z obiektu w wierszu tekstu odpowiednim dla dzienników
diagnostycznych. Jest to bardzo przydatne do testowania aplikacji w różnych śro-
dowiskach mobilnych. Oto jak wygląda wspomniany wiersz tekstu:
NetworkInfo: type: mobile[UMTS], state: CONNECTED/CONNECTED, reason:
simLoaded, extra: internet, roaming: false, failover: false, isAvailable:
true

Warto śledzić wartości type, state i reason. Zauważ, że wcześniej (przez podanie
wartości ConnectivityManager.CONNECTIVITY_ACTION przy rejestrowaniu odbiornika)
zażądaliśmy informacji o każdej zmianie stanu połączenia, jednak aplikacja obsłu-
guje tylko zmiany ustawień serwera pośredniczącego. Jeśli potrzebne są informacje
tylko o tych zmianach, wystarczy zarejestrować zdarzenie Proxy.PROXY_CHANGE_
´ACTION, a Android nie będzie powiadamiał aplikacji o innych zmianach stanu
połączenia. To już w zasadzie wszystko, co można powiedzieć o obsłudze zmian
stanu połączenia. Pokazaliśmy tu proste rozwiązanie problemu, który przez wielu
programistów — choć nie przez Ciebie — jest niestety ignorowany.

9.4. Podsumowanie
Rozdział ten jest dość techniczny. Zaufaj nam jednak — użytkownicy docenią
aplikację, która w niezawodny sposób komunikuje się z siecią! Podsumujmy,
czego się dowiedziałeś.
Zaczęliśmy od pokazania, jak przesyłać żądania HTTP na Androidzie. Najpierw
użyliśmy wbudowanego mechanizmu z Javy, klasy HttpURLConnection, która jest
szybka, prosta i daje stosunkowo duże możliwości, jest jednak nieelegancka.
Dlatego szybko przeszliśmy do klasy HttpClient Apache’a. Mamy nadzieję, że po
zapoznaniu się z nią zrozumiałeś, dlaczego Google nie ograniczył się do używa-
nia klasy HttpURLConnection. Klasa HttpClient jest łatwiejsza w użyciu, bardziej
elastyczna i udostępnia bardzo liczne funkcje. Jednak jej prosty i intuicyjny
interfejs zgodny z modelem „najpierw konwencja, potem konfiguracja” może być
zwodniczy w środowiskach wielowątkowych, takich jak Android. Dlatego poka-
zaliśmy też, jak prawidłowo skonfigurować obiekt klasy HttpClient i uniknąć
problemów.
9.4. Podsumowanie 401

W drugim podrozdziale przeszliśmy poza konieczne, ale nudne podstawy


korzystania z HTTP. Napisaliśmy tu nową wersję aplikacji MyMovies, która
nawiązuje połączenie z usługą sieciową i przetwarza odpowiedzi od niej z wyko-
rzystaniem różnych parserów (typu push dla XML-a, typu pull dla XML-a i dla
JSON-a), co pozwoliło dobrze przybliżyć wady i zalety poszczególnych rozwiązań.
W ostatnim podrozdziale wróciliśmy do protokołu HTTP i sieci. Pokazali-
śmy, jak zwiększyć niezawodność kodu przez radzenie sobie z awariami sieci za
pomocą komponentów obsługi ponawiania żądań i przez prawidłową obsługę
konfiguracji sieci (konfiguracja ta może się zmieniać w trakcie przemieszczania
się użytkownika). Zauważ, że wiele z tych mechanizmów jest zaimplementowa-
nych w bibliotece narzędziowej ignition, co ułatwia pracę!
Skoro już wspomnieliśmy o przemieszczaniu się — następny rozdział dotyczy
lokalizacji urządzenia. Możliwość określania miejsca pobytu użytkownika na
podstawie sygnału z telefonu jest dziś niemal powszechna. Mechanizm ten jest
niezwykły i daje bardzo duże możliwości. Dlatego z rozdziału 10. dowiesz się
wszystkiego o GPS-ie i innych dostępnych w Androidzie usługach opartych na
lokalizacji urządzenia.
402 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe
Najważniejsza
jest lokalizacja

W tym rozdziale
Q Wykorzystywanie szerokości i długości
geograficznej
Q Określanie położenia użytkownika
Q Tworzenie aplikacji wykorzystujących mapy

Nie ma równoleżnika, który nie uważa, że nie mógłby być równikiem,


gdyby tylko miał jego prawa.
Mark Twain
Jedną z najatrakcyjniejszych cech smartfonów jest to, że umożliwiają one okre-
ślanie lokalizacji. To niezwykłe, że możemy nosić w kieszeni urządzenie, które
dokładnie wskazuje nasze położenie na planecie. Dzięki genialnym inżynierom
pomysły rodem z fantastyki naukowej stały się rzeczywistością. Współczesne
urządzenia przenośne potrafią jeszcze więcej — zapewniają dostęp do danych
z sieci WWW i są wyposażone w różne aparaty i czujniki.
Programiści korzystają z androidowych mechanizmów określania lokalizacji
do tworzenia wielu ciekawych aplikacji. Programy te aktywują inne funkcje tele-
fonu na podstawie położenia użytkownika w danym momencie, pomagają śle-
dzić trasę w trakcie pieszych i rowerowych wycieczek czy biegów, informują
o pobliskich atrakcjach i usługach, umożliwiają znalezienie zgubionego lub zapo-
mnianego urządzenia, mierzą szybkość i inne parametry, zapewniają precyzyjne
dane na potrzeby reklamy oraz innych komercyjnych zastosowań itd.

403
404 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Teraz nadszedł Twój czas. Po poznaniu dostępnych w Androidzie interfejsów


API i możliwości związanych z określaniem położenia możesz zbudować nową,
doskonałą aplikację wykrywającą lokalizację użytkowników. Aby Ci w tym pomóc,
w tym rozdziale zaczynamy od wprowadzenia do współrzędnych geograficznych
i działania usług opartych na ich wykorzystaniu. Dalej opisujemy implementacje
dostawców LocationProvider z Androida. Poszczególne wersje zapewniają wiele
różnych sposobów określania położenia. Pokazujemy, jak za pomocą klasy Loca
´tionManager uzyskać dostęp do dostawców, jak ustalić, jakie dane można wyko-
rzystać, a także wyjaśniamy, jak skonfigurować odbiorniki usług opartych na okre-
ślaniu położenia. Omawiamy też inne mechanizmy związane z lokalizacją, na
przykład przekształcanie adresu na współrzędne (i na odwrót) za pomocą klasy
Geocoder.
Po opanowaniu podstawowych mechanizmów można zbudować aplikację, która
wykorzystuje interfejsy API Google’a do ustalenia bieżącej lokalizacji, a następnie
umieszcza znaczniki na interaktywnej mapie. Wymaga to użycia klasy MapView i
zarządzania nią w aktywności MapActivity.

10.1. Krótkie wprowadzenie


do współrzędnych geograficznych
Zanim przejdziemy do tworzenia androidowych aplikacji wykorzystujących współ-
rzędne geograficzne, warto krótko zdefiniować powiązane z tym pojęcia. Obie-
cujemy, że wprowadzenie to nie będzie długie. Chcemy tylko pokrótce przed-
stawić podstawowe informacje dla osób nieobeznanych z omawianymi tu
zagadnieniami. Jeśli znasz systemy współrzędnych geograficznych oraz wiesz,
czym jest szerokość i długość geograficzna, możesz od razu przejść do podroz-
działu 10.2, gdzie omawiamy dostawców położenia (obiekty typu LocationProvider)
dostępnych w większości urządzeń z Androidem.

10.1.1. Długość i szerokość geograficzna


Choć większość programistów z pewnością wie, czym jest długość i szerokość
geograficzna, nie wszyscy muszą znać się na geografii. Ponadto ważne są pewne
subtelności związane z przedstawianiem danych geograficznych w różnych kon-
tekstach. Wyjaśniamy tu długość i szerokość geograficzną, ponieważ zagadnienia
te stanowią podstawę kartografii i nawigacji, a także są używane w dostawcach
położenia w Androidzie. Zaczynamy od formalnych definicji z Wikipedii, a następ-
nie je omawiamy.
Q Szerokość geograficzna. Odległość kątowa (mierzona w kierunku północnym
lub południowym) danego miejsca na Ziemi od równika. Szerokość
geograficzna to kąt, zwykle podawany w stopniach (symbol °). Szerokość
geograficzna równika wynosi 0°, bieguna północnego — 90° szerokości
północnej, a bieguna południowego — 90° szerokości południowej.
10.1. Krótkie wprowadzenie do współrzędnych geograficznych 405

Szerokość określa, czy dany punkt na Ziemi jest położony „na górze”,
czy „na dole” globusa. Na wierzchołku globusa znajduje się biegun północny
(90° szerokości północnej). Jeśli pociągniesz palcem w dół globusa i dotrzesz
do jego połowy, natrafisz na równik (0°). Jeżeli przejdziesz jeszcze niżej,
dotrzesz do bieguna południowego (90° szerokości południowej). Szerokość
geograficzna określa miejsce w wymiarze północ – południe, a równoleżniki
tworzą równoległe linie wokół globusa.
Q Długość geograficzna. Jest to odległość kątowa południka danego punktu
względem południka zerowego. Długość geograficzna to kąt, zwykle
podawany w stopniach (symbol °). Linie wyznaczające długość geograficzną
nazywa się południkami. Południk zerowy ma długość geograficzną 0°,
a południk leżący po przeciwnej stronie — długość geograficzną 180°.
Długość geograficzna określa miejsca na lewo i na prawo na globusie.
Długość trudniej jest ustalić niż szerokość, ponieważ trzeba wybrać punkt
początkowy. Nie istnieją tu naturalne bieguny związane z osią rotacji
Ziemi. Południk zerowy zgodnie z przyjętym na świecie zwyczajem
przebiega przez brytyjskie Royal Observatory w Greenwich w Londynie
(dawniej różne stowarzyszenia umieszczały południk zerowy w różnych
miejscach; na szczęście obecnie wszyscy są zgodni co do jego położenia).
Jest to umownie ustalony punkt początkowy do wyznaczania długości
geograficznej.
Do określania długości i szerokości geograficznej używamy stopni, ponieważ oba
wymiary określają kąt. Aby dokładniej podać długość lub szerokość, można też
użyć minut i sekund. Na przykład zwrotnik Koziorożca (jest to jeden z równo-
leżników, którym — podobnie jak równikowi — nadano specjalną nazwę) znaj-
duje się na szerokości 23° 26' i 21" (23 stopni, 26 minut i 21 sekund szerokości
północnej). Każdą wartość określającą stopnie, minuty i sekundy można przedsta-
wić w formie dziesiętnej, posługując się następującym wzorem:
Decimal value = Degrees + (Minutes/60) + (Seconds/3600)

Oznacza to, że szerokość 23° 26' i 21" można zapisać jako –23,439167. Warto
zauważyć, że w formie dziesiętnej liczby dodatnie oznaczają północ i wschód,
a ujemne — południe i zachód. Narzędzia i interfejsy API Androida zwykle obsłu-
gują oba sposoby podawania szerokości i długości, dlatego możesz stosować wygod-
niejszy dla Ciebie format (lub przekształcać wartości z danego formatu na drugi).
W Androidzie domyślnie stosuje się naturalny dla komputerów zapis dziesiętny.
Na rysunku 10.1 długość i szerokość nałożono na kulę symbolizującą Ziemię.
Widać tu, że za pomocą szerokości i długości można dokładnie ustalić lokalizację
dowolnego punktu na Ziemi1.

1
Teoretycznie na biegunach długość jest niezdefiniowana, jednak o ile nie planujesz ekstremalnych
wypraw, nie ma to większego znaczenia.
406 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Rysunek 10.1. Długość i szerokość


przedstawione na kuli. Widoczne
są tu dodatnie i ujemne wartości
służące do przedstawiania szerokości
i długości w postaci dziesiętnej

To krótkie wprowadzenie do systemu współrzędnych geograficznych nie jest


oczywiście kompletne, jednak powinno zapewnić Ci informacje niezbędne do
rozpoczęcia korzystania z dostawców położenia i danych.

10.1.2. Potencjalne problemy


Do tej pory wszystko było proste. Nietrudno jest zrozumieć, czym są długość
i szerokość geograficzna. Trzeba jednak zwrócić uwagę na kilka komplikacji.
Opisano je w tabeli 10.1.
Tabela 10.1. Pułapki, o których należy pamiętać w czasie korzystania z długości
i szerokości geograficznej

Na co zwrócić uwagę? Dlaczego?


Wartości dodatnie i ujemne Jeśli szerokość jest przedstawiana jako liczba całkowita, wartości
dodatnie oznaczają północ, a ujemne — południe. Dla długości
wartości dodatnie oznaczają wschód, a ujemne — zachód.
Zwykle szerokość jest Powszechnie najpierw podaje się szerokość geograficzną (tak
podawana jako pierwsza postępują między innymi członkowie organizacji International
Maritime Organization). Interfejsy API Androida też tak działają,
jednak w niektórych narzędziach platformy jest inaczej!
Instrukcja geo fix i interfejs GUI narzędzia DDMS umieszczają
na początku długość.
Problemy w obliczeniach Przy obliczaniu długości i szerokości trzeba zwrócić uwagę na
bieguny i — w mniejszym stopniu — południk 180°. Ponieważ
południki zbiegają się w biegunach (w przeciwieństwie do
równoleżników nie są równoległe), stają się niezdefiniowane.
Ponadto na południku 180° występuje nieciągłość, którą trzeba
uwzględnić. W tym miejscu następuje zmiana z wartości
dodatnich na ujemne, a czasem potrzebne są współrzędne miejsca
po obu stronach tego południka.

Zwykle nie trzeba przeprowadzać skomplikowanych obliczeń na biegunach lub


południku 180°. Należy jednak pamiętać o tym, że współrzędne w interfejsach API
Androida mają wartości dodatnie i ujemne, a narzędzia działają w niespójny
sposób. Jeśli próbujesz na przykład znaleźć Buenos Aires w Argentynie i pomył-
kowo wprowadzisz współrzędne 34,60/58,37 zamiast –34,60/–58,37, dotrzesz na
10.2. Menedżery, dostawcy i odbiorniki położenia 407

pustynię w Iranie. Buenos Aires znajduje się na południe od równika i na zachód


od południka zerowego, dlatego zarówno szerokość, jak i długość mają wartości
ujemne. Ponadto jeśli użyjesz instrukcji geo fix w powłoce emulatora i przekażesz
prawidłowe współrzędne Buenos Aires (–34,60/–58,37), trafisz w miejsce na
Oceanie Południowym, ponieważ w niektórych androidowych narzędziach pierw-
sza współrzędna określa długość geograficzną.
Omówiliśmy już długość i szerokość geograficzną. Wiesz już też, na jakie
problemy możesz natrafić, posługując się współrzędnymi w Androidzie. Przydat-
nych jest ponadto kilka innych wskaźników.

10.1.3 Inne wskaźniki


Android obsługuje różne czujniki geoprzestrzenne. Oprócz długości i szerokości,
które reprezentują lokalizację punktu w poziomie i pionie na kuli ziemskiej,
określają one także inne informacje, na przykład wysokość nad poziomem morza,
azymut i szybkość.
Wysokość to odległość (pionowo w górę) od poziomu morza. Azymut w róż-
nych kontekstach definiowany jest w odmienny sposób. W nawigacji morskiej
azymut oznacza kąt w stopniach na wschód od północy geograficznej do doce-
lowego punktu, wyznaczany przez bieżące miejsce pobytu. Szybkość określa, jak
szybko dana osoba się porusza.
W trakcie programowania na Android programista korzysta z menedżera
LocationManager do określania dostępnych klas LocationProvider, a następnie używa
dostawców do sprawdzania szerokości, długości, wysokości, azymutu i szybkości.

10.2. Menedżery, dostawcy i odbiorniki położenia


W ramach omawiania mechanizmów Androida związanych z położeniem zaczy-
namy od utworzenia przykładowej aplikacji, która pozwala poznać podstawowe
techniki. Aplikacja ta, LocationInfo, spełnia dwie funkcje. Umożliwia zapoznanie
się z klasami LocationManager i LocationProvider, a także pozwala ustalić bieżące
położenie za pomocą klasy LocationListener. Po przedstawieniu pierwszej aplikacji
tworzymy bardziej rozbudowany program na podstawie dalszych informacji.
Klasa LocationManager to „drzwi” do wszystkich androidowych usług związa-
nych z położeniem. Jest ona usługą systemową, umożliwiającą dostęp do dostaw-
ców położenia, konfigurowanie odbiorników aktualizacji położenia oraz alarmów
zbliżeniowych itd. Klasa LocationProvider zapewnia dane o położeniu. Egzempla-
rze klasy LocationListener używają określonych dostawców do asynchronicznego
przekazywania do aplikacji aktualizacji położenia.
W pierwszej części przykładowej aplikacji LocationInfo przedstawiamy typy
dostępnych klas LocationProvider. Pokazujemy też, jakie dane potrafią zwracać
poszczególni dostawcy. Na rysunku 10.2 pokazano, jak wygląda gotowa aplikacja
na urządzeniu z Androidem.
408 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Rysunek 10.2.
Aplikacja LocationInfo
wyświetla dostępnych
dostawców położenia
(po lewej)
i szczegółowe
informacje
o wybranym dostawcy
(po prawej)

POBIERZ PROJEKT LOCATIONINFO. Kod źródłowy pro-


jektu i pakiet APK do uruchamiania aplikacji znajdziesz w wi-
trynie z kodem do książki Android w praktyce. Ponieważ
niektóre listingi skrócono, abyś mógł skoncentrować się na
konkretnych zagadnieniach, zalecamy pobranie kompletnego
kodu źródłowego i śledzenie go w Eclipse (lub innym środowi-
sku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/b1X5, plik APK: http://mng.bz/pLh2.
Jak widać na rysunku 10.2, dostępne typy dostawców położenia są wyświetlane
w widoku listy. Można tu wybrać dowolnego dostawcę. Aplikacja kieruje wtedy
do niego zapytanie i wyświetla wszystkie udostępniane przez niego dane.
Druga część aplikacji LocationInfo związana jest z przyciskiem Określ poło-
żenie za pomocą GPS-u widocznym w dolnej części rysunku 10.2. Po kliknięciu
przycisku przez użytkownika aplikacja sprawdza, czy GPS jest włączony. Jeśli nie,
program wyświetla prośbę o włączenie go. Pojawia się wtedy ekran z ustawie-
niami. Po włączeniu GPS-u aplikacja sprawdza ostatnie położenie. Jeśli jest nie-
dostępne, używa obiektu klasy LocationListener do zaktualizowania położenia.
Zrzuty z tej części aplikacji przedstawiono na rysunku 10.3.
Wiesz już, jak ma wyglądać przykładowa aplikacja LocationInfo. Pora więc
przystąpić do jej tworzenia. Zaczynamy od menedżera LocationManager. Zapewnia
on dostęp do androidowych usług związanych z położeniem.
10.2. Menedżery, dostawcy i odbiorniki położenia 409

Rysunek 10.3. Włączanie dostawcy danych z GPS-u i używanie go do pobrania


informacji o aktualnym położeniu

Urządzenie a emulator
Dla kilku przykładów z tego rozdziału emulator wyświetla inne informacje, niż są
widoczne na zrzutach. Emulator może nie mieć potrzebnych mechanizmów do
określania położenia lub informacji o lokalizacji. Dlatego w podpisach pod niektó-
rymi rysunkami znajduje się wzmianka „na urządzeniu”. Zachęcamy, aby w miarę
możliwości uruchamiać przykładowy kod na urządzeniach. Przykładów możesz
używać także wtedy, gdy nie posiadasz odpowiedniego sprzętu, jednak musisz włą-
czyć odpowiednich dostawców na stronie Menu/Settings/Location and Security
i za pomocą formularza DDMS Emulator Control/Location Controls przesłać do
emulatora informacje o położeniu.

10.2.1. Rejestrowanie się w menedżerze LocationManager


Za pomocą menedżera LocationManager można zażądać listy dostępnych dostaw-
ców położenia, uzyskać referencję do dostawcy na podstawie jego nazwy lub
możliwości, pobrać ostatnie znane położenie od dostawcy o określonej nazwie
(dostawca może zwrócić null lub dawne dane), zarejestrować, że aplikacja ocze-
kuje kilku rodzajów aktualizacji położenia i kilku rodzajów alarmów itd.
Przed rozpoczęciem używania menedżera LocationManager należy wspomnieć,
że dostęp do usług związanych z położeniem wymaga podania specjalnych upraw-
nień w manifeście aplikacji. Uprawnienia wykorzystywaliśmy już w kilku wcze-
śniejszych aplikacjach; ich omówienie znajdziesz w rozdziale 2. W aplikacji
LocationInfo używamy w manifeście następującego uprawnienia:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Użytkownik w czasie instalowania aplikacji widzi to uprawnienie i akceptuje je.


Jeśli nie chce pozwolić aplikacji na określanie położenia, może zrezygnować z jej
410 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

instalowania. Brak podanego uprawnienia sprawia, że nie można używać mene-


dżera LocationManager ani egzemplarzy klas LocationProvider. Próba zastosowania
obiektu tego typu prowadzi wtedy do zgłoszenia wyjątku SecurityException.
Po dodaniu uprawnień należy skonfigurować główną aktywność aplikacji
LocationInfo. W aktywności tej pokazujemy, jak uzyskać uchwyt do menedżera
LocationManager, a następnie jak użyć menedżera do pobrania listy dostępnych
dostawców położenia. Kod aktywności przedstawiono na listingu 10.1.

Listing 10.1. Główna aktywność aplikacji LocationInfo

public class Main extends Activity implements OnItemClickListener {

public static final String LOG_TAG = "LocationInfo";


public static final String PROVIDER_NAME = "PROVIDER_NAME";

private LocationManager locationMgr;


private ListView providersList;
private Button getLoc;

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

locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);

ArrayAdapter<String> adapter =
new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
locationMgr.getAllProviders());

providersList = (ListView) findViewById(R.id.location_providers);


providersList.setAdapter(adapter);
providersList.setOnItemClickListener(this);

getLoc = (Button) findViewById(R.id.getloc_button);


getLoc.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
startActivity(new Intent(Main.this, GetLocationWithGPS.class));
}
});
}

@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
TextView textView = (TextView) view;
String providerName = textView.getText().toString();
Intent intent = new Intent(Main.this, ProviderDetail.class);
intent.putExtra(PROVIDER_NAME, providerName);
startActivity(intent);
}
}
10.2. Menedżery, dostawcy i odbiorniki położenia 411

Dostęp do menedżera LocationManager , podobnie jak do innych usług syste-


mowych, można uzyskać przez wywołanie metody getSystemService (tu z argu-
mentem w postaci stałej Context.LOCATION_SERVICE) . Po otrzymaniu referencji do
menedżera LocationManager można pobrać listę wszystkich dostępnych dostawców
położenia . Tu lista nazw dostawców służy do zapełnienia adaptera widoku listy.
Aby aplikacja reagowała na kliknięcie przez użytkownika nazwy dostawcy
w widoku listy, należy utworzyć odbiornik kliknięcia dla obiektu this i lokal-
nie przesłonić metodę onItemClick . Intencja w odbiorniku kliknięcia uruchamia
aktywność ProviderDetail. W dodatkowych danych intencji znajduje się nazwa
wybranego dostawcy. Aktywność ProviderDetail tworzy egzemplarz wybranego
dostawcy położenia i — co pokazujemy dalej — sprawdza ostatnie znane położe-
nie oraz ustawienia.

10.2.2. Korzystanie z dostawcy położenia


Na rysunku 10.2 pokazano, że na urządzeniu używanym do wykonywania zrzu-
tów menedżer LocationManager zwraca trzech dostępnych dostawców położenia.
Zwróć uwagę na nazwy dostawców:
Q network,
Q gps,
Q passive.
Dostawca network korzysta z sieci mobilnej lub Wi-Fi, aby jak najdokładniej
określić położenie na podstawie informacji z punktu dostępowego lub poprzez
triangulację. Technika ta działa mniej dokładnie niż dostawca gps, który korzysta
z odbiornika GPS z urządzenia do przeprowadzenia triangulacji z wykorzystaniem
satelitów. Dostawca passive działa jak dostawca pośredniczący. Wprowadzono go
w Androidzie 2.2 na potrzeby odbierania aktualizacji położenia, kiedy inne apli-
kacje lub usługi systemowe tego zażądają. Dostawcy passive nie inicjują ustalania
lokalizacji.
USTALANIE POŁOŻENIA JEST KOSZTOWNE. Określanie lokalizacji
to kosztowny proces. Może trwać sporo czasu i zużywać wiele zasobów.
Jeśli nie potrzebujesz natychmiastowej aktualizacji, ponieważ akcepto-
walne są okresowe informacje o zmianie położenia, rozważ zastosowanie
dostawcy passive.
NIE ZAPOMINAJ O DOSTAWCY NETWORK. Choć dostawca gps jest
dokładniejszy od dostawcy network (w przykładach używamy właśnie
dostawcy gps, ponieważ emulator go obsługuje), nie zapominaj o tym
ostatnim. W wielu sytuacjach — zwłaszcza wtedy, kiedy urządzenie znaj-
duje się w pomieszczeniu — dostawca network jest najlepszym rozwią-
zaniem. Często warto korzystać z obu dostawców i płynnie przechodzić
między nimi, jeśli drugi nie jest dostępny.
412 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Po wybraniu dostawcy menedżer pobiera referencję do niego. Można ją wyko-


rzystać do ustalenia ostatniego znanego położenia i sprawdzenia cech dostawcy.
Cechy określają, jak dokładny jest dany dostawca, czy korzystanie z niego jest
bezpłatne, ile energii zużywa, czy udostępnia wysokość oraz azymut itd. W przed-
stawionej na listingu 10.2 aktywności ProviderDetail pokazujemy, jak wykorzystać
te informacje.

Listing 10.2. Aktywność ProviderDetail

public class ProviderDetail extends Activity {

private LocationManager locationMgr;

private TextView title;


private TextView detail;

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

locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);

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


detail = (TextView) findViewById(R.id.detail);
}

@Override
protected void onResume() {
super.onResume();

String providerName =
getIntent().getStringExtra("PROVIDER_NAME");
Location lastLocation =
locationMgr.getLastKnownLocation(providerName);

LocationProvider provider =
locationMgr.getProvider(providerName);

StringBuilder sb = new StringBuilder();

sb.append("Informacje z menedżera położenia");


sb.append("\n--------------------------------");
if (lastLocation != null) {
sb.append("\n");
Printer printer = new StringBuilderPrinter(sb);
lastLocation.dump(printer, "Ostatnie położenie: ");
} else {
sb.append("\nOstatnie położenie: brak\n");
}
sb.append("\n");
sb.append("\nCechy dostawcy");
sb.append("\n--------------------------------");
10.2. Menedżery, dostawcy i odbiorniki położenia 413

sb.append("\nDokładność: " + provider.getAccuracy());


sb.append("\nZużycie energii: "
+ provider.getPowerRequirement());
sb.append("\nWymaga opłat: "
+ provider.hasMonetaryCost());
sb.append("\nOkreśla wysokość: "
+ provider.supportsAltitude());
sb.append("\nOkreśla azymut: "
+ provider.supportsBearing());
sb.append("\nOkreśla szybkość: "
+ provider.supportsSpeed());
sb.append("\nWymaga sieci komórkowej: "
+ provider.requiresCell());
sb.append("\nWymaga sieci z danymi: "
+ provider.requiresNetwork());

// Z uwagi na zwięzłość informacje związane z GPS-em pominięto


// (są dostępne tylko dla dostawcy gps).

title.setText("Dostawca: " + providerName);


detail.setText(sb.toString());
}
}

Pierwszy krok w aktywności ProviderDetail polega na ustaleniu nazwy dostawcy


wybranego przez użytkownika. Umożliwiają to dodatkowe dane z intencji .
Dalej aplikacja pobiera ostatnie znane położenie (wkrótce wrócimy do tej
kwestii), a następnie po raz pierwszy używa dostawcy położenia.
Tu dostawcę położenia pobieramy na podstawie jego nazwy za pomocą mene-
dżera . Po uzyskaniu dostępu do dostawcy położenia aplikacja wywołuje jego
metody, aby sprawdzić wymagania i możliwości . Tu ograniczamy się do
wyświetlenia informacji, aby pokazać, co jest dostępne. Łatwo można jednak
domyślić się, jak użyć uzyskanych wartości do ustalenia, czy dostawca nadaje się
do wykonania zadania.
Wróćmy do ostatniego znanego położenia. Wiemy, że informacje te można
pobrać bez tworzenia egzemplarza dostawcy — wystarczy wykorzystać jego
nazwę w menedżerze LocationManager. Jest to wygodne rozwiązanie, jednak nie
należy na nim polegać. Ostatnie znane położenie może mieć wartość null lub być
nieaktualne. Jeśli jednak jest dostępne i nowe, może zaoszczędzić wiele czasu
oraz problemów związanych z ustalaniem bieżącej (lub niedawnej) pozycji.
W Androidzie wszystkie informacje o położeniu znajdują się w klasie Location.
Klasa ta obejmuje wszystkie oczekiwane pola: szerokość i długość geograficzną,
nazwę dostawcy, czas, szybkość, azymut itd. Niektóre pola są puste — zależy to
od możliwości dostawcy. Niezwykle ważny jest czas. Jest on reprezentowany
jako liczba milisekund od 1 stycznia 1970 roku czasu UTC. Ostatnie położenie
mogło zostać ustalone kilka sekund wcześniej; wtedy urządzenie prawdopo-
dobnie nadal znajduje się blisko tego miejsca. Możliwe też, że lokalizację ostatnio
ustalono kilka dni lub tygodni wcześniej. Wtedy można przyjąć, że jej bieżące
wskazania nie są trafne.
414 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Jeśli ostatnie znane położenie nie jest dostępne lub jest już nieaktualne,
możesz skonfigurować odbiornik LocationListener, aby spróbować uzyskać
nowsze dane.

10.2.3. Używanie odbiornika LocationListener


Aby pobrać bieżące położenie lub regularnie otrzymywać powiadomienia o zmia-
nach lokalizacji, należy utworzyć odbiornik LocationListener. Odbiornik ten reje-
strujemy za pomocą jednej z kilku metod menedżera LocationManager, umożli-
wiających przekazanie nazwy potrzebnego dostawcy wraz z wywoływanym
zwrotnie odbiornikiem (można też podać kilka innych informacji, na przykład czas
i odległość).

0 TECHNIKA 49. Sprawdzanie stanu dostawcy położenia

W teorii klasa LocationListener jest prosta. Obejmuje wywoływane zwrotnie


metody, które informują o zmianie położenia, zmianie stanu dostawcy lub jego
włączeniu i wyłączeniu. Mimo tej prostoty prawidłowe korzystanie z odbiornika
bywa skomplikowane.
PROBLEM
Jak ustalić, czy dany dostawca jest włączony? A jeśli nie jest włączony, jak wyświe-
tlić użytkownikowi prośbę o uruchomienie tego dostawcy, gdy właśnie jest
potrzebny aplikacji?
ROZWIĄZANIE
Aplikacja LocationInfo w reakcji na kliknięcie przycisku Określ położenie
za pomocą GPS-u (rysunek 10.2) uruchamia aktywność GetLocationWithGPS
(listing 10.3). Aktywność ta wykonuje kilka ważnych operacji. Pierwszą z oma-
wianych tutaj jest sprawdzanie, czy dostawca gps jest włączony. Jeśli nie jest,
aktywność wyświetla użytkownikowi prośbę o włączenie go i otwiera ustawienia
Androida. Opisane operacje są wykonywane w metodzie onResume. Używamy
w niej okna AlertDialog i intencji.

Listing 10.3. Metoda onResume sprawdza, czy dostawca gps jest włączony

public class GetLocationWithGPS extends Activity {

public static final String LOC_DATA = "LOC_DATA";

private LocationManager locationMgr;


private Handler handler;

private TextView title;


private TextView detail;

// Metodę onCreate znajdziesz na następnym listingu.

@Override
0 TECHNIKA 49. Sprawdzanie stanu dostawcy położenia 415

protected void onResume() {


super.onResume();

if (!locationMgr.isProviderEnabled(
android.location.LocationManager.GPS_PROVIDER)) {
AlertDialog.Builder builder =
new AlertDialog.Builder(this);
builder.setTitle("GPS nie jest włączony")
.setMessage(
"Chcesz przejść do ustawień
i włączyć GPS?")
.setCancelable(true)
.setPositiveButton("Tak",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
startActivity(
new Intent(
Settings.ACTION_SECURITY_SETTINGS));
}
})
.setNegativeButton("Nie",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
finish();
}
});
AlertDialog alert = builder.create();
alert.show();
} else {
LocationHelper locationHelper =
new LocationHelper(locationMgr,
handler, Main.LOG_TAG);
locationHelper.getCurrentLocation(30);
}
}

Jeśli korzystasz z Androida od dłuższego czasu, prawdopodobnie zetknąłeś


się już z oknem dialogowym podobnym do tego pokazanego po lewej stronie
rysunku 10.3. W oknie tym aplikacja wyświetla prośbę o włączenie GPS-u. Wbu-
dowana aplikacja Maps z Androida pokazuje podobne okno, kiedy próbuje
wyświetlić trasę dojazdu.
Aplikacja najpierw sprawdza, czy dostawca jest włączony. Używa do tego
menedżera LocationManager . Jeśli dostawca nie jest włączony, aplikacja konfi-
guruje okno AlertDialog , które za pomocą intencji wyświetla ustawienia zabez-
pieczeń, gdzie użytkownik może włączyć GPS . Z przyczyn bezpieczeństwa
(dlatego omawiana opcja znajduje się na stronie Location & Security, w głównym
menu ustawień) aplikacja nie może automatycznie włączyć ustalania dokładnego
położenia urządzenia lub osoby. Jeśli użytkownik zdecyduje się włączyć GPS
(lub zrobił to wcześniej), aktywność przechodzi do innej ścieżki i konfiguruje
obiekt klasy LocationHelper .
416 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

OMÓWIENIE
Sprawdzanie, czy określony dostawca położenia jest włączony, to proste zadanie.
Jeśli dostawca jest niedostępny, a aplikacja go potrzebuje, należy szybko zgłosić
błąd. Trzeba poinformować użytkownika, że program nie może kontynuować
pracy bez dostawcy, i umożliwić jego włączenie. Jeżeli użytkownik nie włączy
danego dostawcy, można ograniczyć możliwości aplikacji lub zakończyć jej
działanie.
Kiedy dostawca jest już włączony, należy wykorzystać go do pobrania danych
lub zaktualizowania położenia. Do aktualizowania lokalizacji służy odbiornik
LocationListener.

0 TECHNIKA 50. Określanie aktualnego położenia za pomocą odbiornika


LocationListener

Dostawcy położenia przekazują aplikacjom informacje o lokalizacji poprzez


obserwatory zarejestrowane w klasie LocationListener. Tu z odbiornika Loca
´tionListener korzystamy w klasie LocationHelper. W ten sposób ukrywamy
proces pobierania bieżącego położenia, co ułatwia późniejsze ponowne wyko-
rzystanie kodu.
PROBLEM
Chcemy ustalić bieżące położenie urządzenia za pomocą odbiornika Location
´Listener. Odbiornik trzeba podłączyć, oczekiwać na informacje przez określony
czas w celu ustalenia położenia, a następnie (w celu ograniczenia zużycia zasobów)
odłączyć go, kiedy nie jest już potrzebny.
ROZWIĄZANIE
Odbiornik LocationListener wykorzystujemy w klasie LocationHelper przedsta-
wionej w końcowej części listingu 10.3. Należy utworzyć i usunąć odbiornik,
a także umożliwić mu komunikowanie się z aktywnością za pomocą komponentu
obsługi. Przejdźmy teraz do zaprezentowanej na listingu 10.4 metody onCreate
z aktywności GetLocationWithGPS. W metodzie tej konfigurujemy komponent
obsługi i używamy klasy LocationHelper.

Listing 10.4. Metoda onCreate i obiekt klasy Handler używany wraz z klasą
LocationHelper

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

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


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

title.setText("Określ położenie za pomocą GPS-u");


detail.setText("Próba ustalenia bieżącego położenia...\n
(może zająć kilka sekund)");
0 TECHNIKA 50. Określanie aktualnego położenia za pomocą odbiornika LocationListener 417

locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);

handler = new Handler() {


public void handleMessage(Message m) {
if (m.what ==
LocationHelper.MESSAGE_CODE_LOCATION_FOUND) {
detail.setText("ZAKOŃCZONO PRZETWARZANIE\nSzer.:"
+ m.arg1 + "\nDł.:" + m.arg2);
} else if (m.what ==
LocationHelper.MESSAGE_CODE_LOCATION_NULL) {
detail.setText("ZAKOŃCZONO PRZETWARZANIE\nNie można ustalić położenia");
} else if (m.what ==
LocationHelper.
MESSAGE_CODE_PROVIDER_NOT_PRESENT) {
detail.setText("ZAKOŃCZONO PRZETWARZANIE\nBrak dostawcy");
}
}
};
}

Klasa Handler służy do przesyłania i przetwarzania obiektów typu Message


(i Runnable) z kolejki MessageQueue wątku. Klasę Handler po raz pierwszy omówili-
śmy w rozdziale 5. Tu umożliwia ona klasie LocationHelper przekazywanie danych
z powrotem do aktywności.
Aplikacja konfiguruje obiekt klasy Handler przez utworzenie anonimowego
egzemplarza z przesłoniętą metodą handleMessage . W metodzie tej obsługi-
wane są trzy sytuacje, które mogą wystąpić przy próbie uzyskania bieżącego poło-
żenia. Oto te sytuacje: znalezienie i zwrócenie położenia , położenie nie zostało
znalezione (ma wartość null) , dostawca ma wartość null .
Większość operacji związanych z ustalaniem położenia wykonujemy w poka-
zanej na listingu 10.5 klasie LocationHelper.

Listing 10.5. Klasa LocationHelper

public class LocationHelper {

public static final int MESSAGE_CODE_LOCATION_FOUND = 1;


public static final int MESSAGE_CODE_LOCATION_NULL = 2;
public static final int MESSAGE_CODE_PROVIDER_NOT_PRESENT = 3;

private static final int FIX_RECENT_BUFFER_TIME = 30000;

private LocationManager locationMgr;


private LocationListener locationListener;
private Handler handler;
private Runnable handlerCallback;
private String providerName;
private String logTag;

public LocationHelper(LocationManager locationMgr,


Handler handler, String logTag) {
this.locationMgr = locationMgr;
this.locationListener = new LocationListenerImpl();
418 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

this.handler = handler;
this.handlerCallback = new Thread() {
public void run() {
endListenForLocation(null);
}
};

Criteria criteria = new Criteria();


criteria.setAccuracy(Criteria.ACCURACY_FINE);
this.providerName = locationMgr.getBestProvider(criteria, true);

this.logTag = logTag;
}

public void getCurrentLocation(int durationSeconds) {

if (this.providerName == null) {
sendLocationToHandler(MESSAGE_CODE_PROVIDER_NOT_PRESENT, 0, 0);
return;
}

Location lastKnown = locationMgr.getLastKnownLocation(providerName);


if (lastKnown != null &&
lastKnown.getTime() >=
(System.currentTimeMillis() - FIX_RECENT_BUFFER_TIME)) {
sendLocationToHandler(MESSAGE_CODE_LOCATION_FOUND,
(int) (lastKnown.getLatitude() * 1e6),
(int) (lastKnown.getLongitude() * 1e6));
} else {
listenForLocation(providerName, durationSeconds);
}
}

private void sendLocationToHandler(int msgId, int lat, int lon) {


Message msg = Message.obtain(handler, msgId, lat, lon);
handler.sendMessage(msg);
}

private void listenForLocation(String providerName,


int durationSeconds) {
locationMgr.requestLocationUpdates(providerName, 0, 0,
locationListener);
handler.postDelayed(handlerCallback, durationSeconds * 1000);
}

private void endListenForLocation(Location loc) {


locationMgr.removeUpdates(locationListener);
handler.removeCallbacks(handlerCallback);
if (loc != null) {
sendLocationToHandler(MESSAGE_CODE_LOCATION_FOUND,
(int) (loc.getLatitude() * 1e6),
(int) (loc.getLongitude() * 1e6));
} else {
sendLocationToHandler(MESSAGE_CODE_LOCATION_NULL, 0, 0);
}
}
}
0 TECHNIKA 50. Określanie aktualnego położenia za pomocą odbiornika LocationListener 419

W klasie LocationHelper wykonywanych jest wiele operacji. Najpierw jednostka


wywołująca musi utworzyć egzemplarz tej klasy i przekazać przy tym obiekty
typów LocationManager, Handler i String (w tym ostatnim trzeba podać tekst zapi-
sywany w dzienniku) . Następnie tworzymy wątek dla wywołania zwrotnego .
Wątek tworzony jest jako zmienna egzemplarza, co pozwala usunąć wywołanie
zwrotne po zakończeniu pracy. Dalej zobaczysz, jak używać tego wywołania.
Następnie tworzymy dostawcę gps. Używamy do tego klasy Criteria zamiast
nazwy . Tu stosujemy wartość ACCURACY_FINE, co oznacza, że dostawca gps
musi być dostępny. Możesz jednak wykorzystać najniższe akceptowalne usta-
wienie. Wartość ACCURACY_COARSE pozwala zastosować dostawcę gps lub network,
przy czym jeśli dostępni są dostawcy obu typów, używany jest dostawca gps. Tu
zdecydowaliśmy się na dostawcę gps, ponieważ umożliwia to łatwiejsze zapre-
zentowanie przykładów przy korzystaniu z różnych urządzeń i emulatora.
Po kodzie do pobierania referencji do dostawcy znajduje się metoda getCurrent
´Location, wywoływana przez aktywności i inne komponenty Androida .
Jednostki wywołujące przekazują do tej metody czas, przez jaki klasa LocationHelper
ma próbować ustalić położenie. Limit ten jest potrzebny, ponieważ ustalanie
położenia nie odbywa się natychmiast, a nie chcemy, aby aplikacja w nieskoń-
czoność czekała na efekty tej operacji.
DO CZEGO SŁUŻY WARTOŚĆ 1E6? W obiektach typu Location
w Androidzie długość i szerokość są przechowywane jako wartości typu
double. Inne klasy (na przykład GeoPoint, z którą zetkniesz się w kontekście
interfejsu API dla map) używają typu int. W obu sytuacjach przedsta-
wiany jest ten sam punkt. Typ int służy do zapisywania współrzędnych
w mikrostopniach. Wartość 1e6 powoduje pomnożenie wartości typu double
przez milion. Wartości można mnożyć i dzielić przez 1e6, aby przekształcać
reprezentację współrzędnych.
W metodzie getCurrentLocation wykonujemy kilka bardzo ważnych operacji.
Po pierwsze, jeśli dostawca ma wartość null, należy zwrócić ją jako wynik za
pomocą metody sendLocationToHandler. Po drugie, jeżeli dostawca jest dostępny,
trzeba sprawdzić ostatnią znaną lokalizację. Jeśli jest określona i stosunkowo
niedawna, można ją zwrócić. Wtedy resztę kodu klasy można pominąć. W ogóle
nie trzeba korzystać wówczas z odbiornika LocationListener. Jeżeli jednak nie
można użyć ostatniego położenia, należy uruchomić odbiornik przez wywołanie
metody listenForLocation .
OSTATNIE ZNANE POŁOŻENIE W EMULATORZE. Przy stosowaniu
instrukcji geo fix we wtyczce adb lub przy ręcznym określaniu położenia
w narzędziu DDMS czas ustalenia lokalizacji jest ustawiany na 0 i zwięk-
szany o jedną sekundę przy każdym ręcznym przesłaniu aktualizacji.
Działa to dobrze dla aktywnych odbiorników, jednak jest nieodpowiednie
w kontekście sprawdzania czasu ustalenia ostatniego znanego położenia.
420 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Aby sprawdzić, czy aplikacja właściwie korzysta z ostatniego znanego poło-


żenia i uwzględnia czas jego ustalenia, trzeba użyć prawdziwego
urządzenia.
W metodzie listenForLocation używamy metody requestLocationUpdates mene-
dżera położenia. Istnieje kilka przeciążonych wersji tej metody. Tu używamy
wersji, która umożliwia określenie dostawcy, minimalnego czasu między aktu-
alizacjami (podawanego w milisekundach), minimalnej odległości (w metrach)
między aktualizacjami i używanego odbiornika LocationListener. Implementację
tego odbiornika omawiamy dalej. Po zarejestrowaniu, że aplikacja oczekuje
aktualizacji położenia, używamy metody postDelayed obiektu handler. Przeka-
zujemy do niej ustawione wcześniej wywołanie zwrotne i czas opóźnienia przed
uruchomieniem. Pozwala to zatrzymać odbiornik po upływie określonego czasu.
To ważne — nie chcemy, aby odbiornik działał w nieskończoność. Częste spraw-
dzanie aktualizacji położenia (odstępy czasu i odległości między aktualizacjami
są ustawione na 0, dlatego aktualizacje są przeprowadzane tak często, jak to moż-
liwe) prowadzi do szybkiego zużycia energii. Odbiornik ma rozpocząć pracę,
pobrać potrzebne informacje i zakończyć pracę.
Przedstawione wcześniej wywołanie zwrotne wywołuje metodę endListen
´ForLocation . Metoda ta usuwa odbiornik LocationListener, usuwa wywołanie
zwrotne i za pomocą obiekty klasy Handler przesyła komunikat o stanie do pier-
wotnej jednostki wywołującej.
Aby uzupełnić aplikację LocationInfo, trzeba przyjrzeć się wspomnianej
implementacji odbiornika LocationListener. Na listingu 10.6 przedstawiono tę
klasę wewnętrzną (znajduje się ona w klasie LocationHelper).

Listing 10.6. Klasa wewnętrzna LocationListener z klasy LocationHelper

private class LocationListenerImpl


implements LocationListener {
@Override
public void onStatusChanged(String provider, int status,
Bundle extras) {
Log.d(logTag, "Zmiana stanu położenia na:" + status);
switch (status) {
case LocationProvider.AVAILABLE:
break;
case LocationProvider.TEMPORARILY_UNAVAILABLE:
break;
case LocationProvider.OUT_OF_SERVICE:
endListenForLocation(null);
}
}

@Override
public void onLocationChanged(Location loc) {
if (loc == null) {
return;
}
0 TECHNIKA 50. Określanie aktualnego położenia za pomocą odbiornika LocationListener 421

Log.d(logTag, "Zmiana położenia na:" + loc.toString());


endListenForLocation(loc);
}

@Override
public void onProviderDisabled(String provider) {
endListenForLocation(null);
}

@Override
public void onProviderEnabled(String provider) {
}
}

LocationListener to interfejs. Zaimplementowaliśmy go za pomocą klasy


wewnętrznej . W interfejsie jest zdefiniowanych kilka metod. Pierwsza z nich
to onStatusChanged . W przykładowej aplikacji w metodzie tej rejestrujemy to,
co się wydarzyło, i zatrzymujemy aktualizowanie położenia, kiedy dostawca prze-
staje pracować. Dalej znajduje się metoda onLocationChanged . Powiadamia
ona o ustaleniu nowego położenia. W metodzie tej także zatrzymujemy aktuali-
zacje, a ponadto przesyłany nowe współrzędne. To podejście sprawia, że po szyb-
kim zaktualizowaniu położenia nie trzeba czekać całego czasu określonego przez
jednostkę wywołującą (można zakończyć pracę od razu po uzyskaniu nowych
danych). Jako ostatnia występuje metoda onProviderDisabled . Także w tej
metodzie zatrzymujemy aktualizowanie. Jest to zabezpieczenie przed wyłącze-
niem dostawcy w trakcie oczekiwania aplikacji na aktualizacje.
OMÓWIENIE
Podsumujmy rozwiązanie. Klasa LocationHelper sprawdza ostatnie znane położe-
nie z danego dostawcy i jeśli dane te są dostępne, zwraca je. Jeżeli dane są nie-
dostępne, klasa uruchamia odbiornik LocationListener i zwraca sterowanie albo
po ustaleniu położenia przez odbiornik, albo po upływie limitu czasu. Wyniki są
zwracane przez obiekt klasy Handler za pomocą komunikatu ze stałymi informu-
jącymi o tym, co się wydarzyło. Przekazywane są też współrzędne (jeśli są
dostępne). Jak wspomnieliśmy na początku, w klasie LocationHelper wykonywa-
nych jest wiele operacji. Jest to jeden z powodów, dla których utworzyliśmy ją
jako klasę pomocniczą. W aktywności szczegółowe operacje z klasy pomocniczej
nie są znane. Aktywność musi tylko utworzyć obiekt klasy Handler, ustalić, jak
długo aplikacja ma oczekiwać na aktualizację położenia, a następnie wywołać
metodę getCurrentLocation.
POBIERANIE SZCZEGÓŁOWYCH DANYCH O STANIE GPS-U. Moż-
liwe, że na zrzutach zauważyłeś informacje o stanie GPS-u. Android udo-
stępnia w klasie LocationManager metodę getGpsStatus, która informuje
o liczbie dostępnych satelitów i czasie ustalania połączenia. Metoda ta
pozwala też uzyskać więcej danych o poszczególnych satelitach. Żadne
422 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

z tych danych nie są szczególnie istotne w rozwijanej aplikacji, jednak


pobieramy je, aby zademonstrować tę możliwość. Więcej informacji na
temat tej techniki znajdziesz w pobranym kodzie aplikacji LocationInfo.
Poznałeś już klasy LocationManager i LocationProvider. Używałeś również klas
Location, Criteria i LocationListener. Jesteś gotów do utworzenia aplikacji, która
korzysta z opisanych mechanizmów w konkretnym celu. W następnym podroz-
dziale wzbogacisz swoją wiedzę o ustalaniu położenia w Androidzie przez zbu-
dowanie aplikacji korzystającej z androidowego interfejsu API Google Maps.

10.3. Tworzenie aplikacji z wykorzystaniem map


W następnej aplikacji, BrewMap, korzystamy z informacji przedstawionych we
wcześniejszej części rozdziału i wprowadzamy zagadnienia związane z mapami.
Aplikacja BrewMap wyświetla położenie browarów, pubów i sklepów z piwem
na mapie interaktywnej. Po umieszczeniu danych na mapie można wybrać lokal,
aby uzyskać dodatkowe informacje. Na rysunku 10.4 pokazano główne ekrany
aplikacji BrewMap.

Rysunek 10.4. Główne


ekrany aplikacji
BrewMap

POBIERZ PROJEKT BREWMAP. Kod źródłowy projektu


i pakiet APK do uruchamiania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Ponieważ niektóre
listingi skrócono, abyś mógł skoncentrować się na konkretnych
zagadnieniach, zalecamy pobranie kompletnego kodu źródło-
wego i śledzenie go w Eclipse (lub innym środowisku IDE
albo edytorze tekstu).
Kod źródłowy: http://mng.bz/43jV, plik APK: http://mng.bz/YbR6.
10.3. Tworzenie aplikacji z wykorzystaniem map 423

Jak widać na rysunku 10.4, aplikacja BrewMap najpierw wyświetla ekran powi-
talny. Główna aktywność aplikacji ma dwie funkcje: znajdowanie pobliskich lokali
z piwem lub wyszukiwanie odpowiednich placówek. Kiedy jedna z tych funkcji
jest używana i zwraca dane, aplikacja uruchamia aktywność MapActivity, która
wyświetla widok MapView ze znacznikami w postaci ikon kufli. Wybranie jednego
ze znaczników prowadzi do wyświetlenia dodatkowych informacji o lokalu na
ekranie ze szczegółowymi danymi.
Dane z aplikacji BrewMap pochodzą z projektu Beer Mapping (http://
beermapping.com/api/). Interfejs API projektu Beer Mapping udostępnia wiele
informacji i jest bezpłatny, choć do korzystania z niego niezbędny jest klucz.
Celem autorów projektu Beer Mapping jest „umożliwienie wyświetlania lub
wykorzystywania danych we własnych aplikacjach”. Wiemy, że mapa z piwem nie
trafia do gustu wszystkich użytkowników, jednak przykładowa aplikacja jest cieka-
wa i kompletna oraz pozwala zapoznać się z obsługą położenia i map w Androidzie.
Przed przejściem do dalszego omówienia trzeba zainstalować rozszerzenie
Google APIs Add-On. Obsługa map nie jest wbudowana w otwarty projekt
Android. Można ją dołączyć jako dodatek udostępniany przez Google. Aby wyko-
rzystać ten dodatek, w czasie tworzenia projektu w środowisku Eclipse należy
ustawić w opcji Build Target odpowiednią wersję interfejsów API Google’a.

10.3.1. Pobieranie dodatku Google APIs Add-On


Dodatek Google APIs Add-On dla Androida obejmuje bibliotekę Maps i kilka
innych niestandardowych komponentów systemu (częścią dodatku jest też wer-
sja beta interfejsów API mechanizmu Cloud to Device Messaging). Tu najbar-
dziej interesuje nas biblioteka Maps dla Androida, udostępniająca interfejs Google
Maps API.
Ponieważ omawiane narzędzie jest dodatkiem, nie wchodzi w skład domyśl-
nego pakietu SDK Androida. Aby zainstalować dodatek, zastosuj się do instrukcji
podanych na stronie Installing the Google APIs Add-On w witrynie dla progra-
mistów: http://mng.bz/863c.
W trakcie instalacji należy za pomocą pakietu SDK Androida i narzędzia
AVD Manager (można je uruchomić z wiersza poleceń przy użyciu programu
android) dołączyć dodatek Google APIs Add-On jako komponent pakietu SDK.
Zwykle polega to na przejrzeniu sekcji Third-Party Add-ons w narzędziu do zarzą-
dzania pakietem SDK i zaznaczeniu pola Google APIs by Google Inc. z odpo-
wiednią wersją platformy.
Po zainstalowaniu dodatku na komputerze używanym do programowania
trzeba uzyskać klucz do interfejsu Maps API. Klucz ten można otrzymać drogą
internetową przez wklejenie skrótu MD5 certyfikatu programisty Androida. Jeśli
nie wiesz, co to oznacza lub gdzie znajduje się taki certyfikat, nie martw się —
proces jest prosty. Zastosuj się do instrukcji i dokładnie przejrzyj warunki korzy-
stania z usługi na stronie rejestracji klucza do interfejsu API: http://mng.bz/E91h.
424 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Są to dodatkowe utrudnienia dla programistów, jednak cały proces jest sto-


sunkowo prosty, a rozbudowane interfejsy API, do których możesz uzyskać dostęp,
są warte włożonego wysiłku. Otrzymany klucz do interfejsu Maps API zapisz
w łatwo dostępnym miejscu, ponieważ będzie potrzebny w jednym z układów
w projekcie BrewMap. Klucz API z pobranego kodu jest powiązany z kompute-
rami, których używamy do programowania. Nie działa w pakietach API utworzo-
nych na innych maszynach.
Dalej rozpoczynamy tworzenie aplikacji BrewMap i opisujemy, jak dołączyć
dodatek Maps Add-On do androidowego projektu.

10.3.2. Tworzenie aplikacji BrewMap


Po zainstalowaniu dodatku Google APIs Add-On i uzyskaniu klucza do interfejsu
Maps API należy zarejestrować bibliotekę do obsługi map w manifeście aplikacji.
Kod manifestu przedstawiono na listingu 10.7.

Listing 10.7. Manifest aplikacji BrewMap z elementem uses-library

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


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.brewmap" android:versionCode="1"
android:versionName="1.0">
<uses-permission
android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />
<application android:icon="@drawable/beer_icon"
android:theme="@android:style/Theme.Black"
android:label="@string/app_name" android:name=".BrewMapApp">
<uses-library
android:name="com.google.android.maps" />
<activity android:name=".Splash" 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 android:name=".Main" />
<activity android:name=".MapResults" />
<activity android:name=".BrewLocationDetails"
android:windowSoftInputMode="adjustResize"/>
</application>
</manifest>

Większość manifestu aplikacji BrewMap powinna być dla Ciebie zrozumiała.


Deklarujemy tu pakiet i wersję. Następnie określamy uprawnienia i definiujemy
element application. Ważne są tu potrzebne do obsługi GPS-u uprawnienie
ACCESS_FINE_LOCATION i element includes-library dołączający interfejs
Maps API .
0 TECHNIKA 51. Przekształcanie adresu na współrzędne geograficzne 425

Następnym po manifeście elementem aplikacji BrewMap jest główna aktyw-


ność, która obejmuje odbiorniki kliknięć dla elementów interfejsu użytkownika
przedstawionych na rysunku 10.4 (wspomniana aktywność znajduje się na dru-
gim zrzucie od lewej). W tekście nie zamieszczamy całego kodu tej aktywności,
ponieważ obejmuje ona głównie omówione już techniki. Używamy klasy AsyncTask
do pobrania za pomocą parsera typu pull XML-owych danych z interfejsu API
projektu Beer Mapping. Dodajemy też okno ProgressDialog z informacją o prze-
biegu pobierania. Aplikacja sprawdza też, czy dostępny jest dostawca gps. Jeśli nie
jest, aplikacja wyświetla prośbę o włączenie go na stronie ustawień. Służy do
tego taki sam kod jak w projekcie LocationInfo. Ponadto używamy utworzonej
w projekcie LocationInfo klasy LocationHelper do ustalenia aktualnego położenia,
jeśli użytkownik chce znaleźć pobliskie lokale.

0 TECHNIKA 51. Przekształcanie adresu na współrzędne geograficzne

Mamy już zestaw danych o położeniu lokali piwnych pobrany z projektu Beer
Mapping. Dostępne są adresy placówek, ale nie ich współrzędne. Następny pro-
blem związany jest więc z przekształcaniem adresów na długość i szerokość
geograficzną, co pozwoli nałożyć symbole lokali na mapę.
PROBLEM
Chcemy przekształcać adresy na współrzędne geograficzne (lub dokonywać prze-
kształceń w drugą stronę).
ROZWIĄZANIE
Jednym ważnym nowym aspektem głównej aktywności aplikacji BrewMap jest
użycie klasy Geocoder Androida w celu uzyskania współrzędnych geograficznych
na podstawie standardowego adresu pocztowego. Geokodowanie polega na
przekształcaniu takiego adresu na długość i szerokość geograficzną. Odwrotne
geokodowanie to odwrotny proces, pozwalający ustalić adres na podstawie współ-
rzędnych. W aplikacji BrewMap potrzebujemy standardowego geokodowania.
Wykonujemy je w zadaniu AsyncTask, w głównej aktywności. Na listingu 10.8
pokazano metodę doInBackground, która przeprowadza przekształcenia.

Listing 10.8. Geokodowanie adresów pocztowych w celu uzyskania długości


i szerokości geograficznej

@Override
protected List<BrewLocation> doInBackground(List<BrewLocation>... args) {
List<BrewLocation> result = new ArrayList<BrewLocation>();
if (args == null) {
return result;
}

if (args[0] != null && !args[0].isEmpty()) {


for (BrewLocation bl : args[0]) {
publishProgress(bl.getName());
try {
426 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

List<android.location.Address> addresses =
geocoder.getFromLocationName(
bl.getAddress().getLocationName(), 1);
if (addresses != null && !addresses.isEmpty()) {
android.location.Address a = addresses.get(0);
bl.setLatitude(a.getLatitude());
bl.setLongitude(a.getLongitude());

if (bl.getLatitude() == 0 || bl.getLongitude() == 0) {
Log.d(Constants.LOG_TAG, "Pomijam lokal "
+ bl.getName()
+ " z powodu błędu geokodowania.");
} else {
result.add(bl);
}
}
} catch (IOException e) {
Log.e(Constants.LOG_TAG, "Błąd w geokodowaniu", e);
}
}
}

return result;
}

W trakcie geokodowania należy przejść po wszystkich obiektach klasy BrewLocation


otrzymanych ze źródła danych (interfejsu API projektu Beer Mapping) i użyć
metody getFromLocationName obiektu geocoder . Jeśli geokodowanie zakończyło
się powodzeniem, należy ustawić otrzymane długość i szerokość w obiekcie
klasy BrewLocation . W trakcie przetwarzania
każdego takiego obiektu aplikacja wyświetla
okno dialogowe z nazwą lokalu, co pokazano na
rysunku 10.5.
Choć nie przedstawiamy tu całego kodu klasy
BrewLocation, warto wyjaśnić, że jest to prosta
klasa, podobna do ziaren JavaBean. Jest ona
częścią modelu aplikacji. Obejmuje metody do
pobierania i ustawiania identyfikatora, nazwy,
stanu, adresu, odnośnika do recenzji, odnośnika
do mapy z pobliskimi lokalami, adresu i numeru
telefonu, a także długości i szerokości geogra-
ficznej. Prawie wszystkie te informacje (wyjątkiem
są długość i szerokość geograficzna) pochodzą
z interfejsu API projektu Beer Mapping. Szero-
kość i długość są potrzebne do naniesienia sym-
boli lokali na mapę, dlatego w aplikacji trzeba
Rysunek 10.5. Wyświetlanie
użyć klasy Geocoder. okna ProgressDialog
w trakcie geokodowania
lokali piwnych
0 TECHNIKA 52. Tworzenie aktywności MapActivity z powiązanym widokiem MapView 427

ZACHOWAJ ROZWAGĘ PRZY GEOKODOWANIU. Geokodowanie to


kosztowna operacja. Każde wywołanie obiektu klasy Geocoder wymaga
skierowania wywołania sieciowego do usługi Google’a. Dlatego geoko-
dowanie jest możliwe tylko wtedy, kiedy dostępna jest sieć, a sprawdzanie
każdego adresu może trwać kilka sekund. Aplikacja BrewMap geokoduje
wiele adresów — takie rozwiązania należy stosować ostrożnie. Jeśli geo-
kodowanie jest konieczne, trzeba je przeprowadzić niezależnie od głównego
wątku interfejsu użytkownika w trybie nieblokującym (niemodalnym).
W aplikacji BrewMap dla uproszczenia korzystamy z modalnego okna
ProgressDialog. Rozwiązanie to działa, ale jest „naiwne”.

OMÓWIENIE
Geokodowanie to przydatna usługa dostępna na urządzeniach z Androidem
dzięki odrębnej usłudze Google’a. Klasa kliencka Geocoder jest częścią Androida,
ale mechanizm działający po stronie serwera nie wchodzi w skład tej platformy.
Po stronie serwera działa usługa Google Geocoding API platformy Google Maps.
Kompletną dokumentację geokodowania znajdziesz na stronie The Google
Geocoding API: http://mng.bz/04wX.
Interfejs Geocoding API udostępnia geokoder także jako usługę sieciową
zwracającą dane w kilku formatach. Z usługi tej można korzystać także poza
Androidem. Zanim to zrobisz, zapoznaj się z warunkami, jakie się z tym wiążą.
Geokoder obsługuje do 2500 żądań z jednego adresu IP w okresie 24 godzin.
Możesz też używać go w trybie komercyjnym, jednak trzeba za to zapłacić. Jeśli
chcesz dowiedzieć się więcej na ten temat, poszukaj informacji o usłudze Google
Maps API Premier.
Dostępne są już mapa i współrzędne uzyskane przez geokodowanie. Można
umieścić symbole na mapie i wybrać lokal, do którego chcemy pójść na piwo.

10.3.3. Aktywność MapActivity


Aplikacja BrewMap przetwarza już dane w formacie XML z interfejsu API pro-
jektu Beer Mapping i geokoduje adresy, aby otrzymać współrzędne geograficzne.
Następnie należy wyświetlić lokale na mapie. Poznaj aktywność MapActivity.

0 TECHNIKA 52. Tworzenie aktywności MapActivity z powiązanym


widokiem MapView

MapActivity to specjalna aktywność obsługująca wiele operacji związanych


z wyświetlaniem w Androidzie widoku MapView, podobnego w wyglądzie do map
Google’a.
PROBLEM
Chcemy utworzyć interaktywną mapę, którą użytkownik może przesuwać i przy-
bliżać za pomocą gestów na ekranie.
428 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

ROZWIĄZANIE
Dostępny w bibliotece dodatku Maps pakiet com.google.android.maps obejmuje
kilka niezwykle przydatnych klas umożliwiających aplikacjom na Android wyko-
rzystanie możliwości usługi Google Maps. W tabeli 10.2 wymienione są najważ-
niejsze klasy z tego pakietu.
Tabela 10.2. Najważniejsze klasy pakietu com.google.android.maps

Klasa Opis

MapActivity Specjalna aktywność obsługująca standardowe operacje związane


z wyświetlaniem widoku MapView.
MapView Widok wyświetlający pola map z interfejsu Google Maps API.

MapController Menedżer obsługujący przesuwanie i przybliżanie widoku MapView.


Overlay Klasa bazowa dla danych, które można nałożyć na mapę.

ItemizedOverlay Klasa nakładkowa z poszczególnymi nakładanymi elementami (często


symbolami widocznymi na mapie).
GeoPoint Para szerokość – długość (współrzędne podaje się tu w mikrostopniach).

Aby dodać widok MapView i wyświetlać mapy, najpierw należy utworzyć klasę
pochodną od MapActivity. W aplikacji BrewMap taka klasa pochodna ma nazwę
MapResults. Jej kod przedstawiono na listingu 10.9.

Listing 10.9. Klasa aktywności MapResults (pochodna od MapActivity) z aplikacji


BrewMap

public class MapResults extends MapActivity {

private MapView map;


private List<Overlay> overlays;

private BrewMapApp app;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.map_results);

app = (BrewMapApp) getApplication();


map = (MapView) findViewById(R.id.map);
map.setBuiltInZoomControls(true);

List<BrewLocation> brewLocations = app.getBrewLocations();


BrewLocationOverlay brewLocationOverlay =
new BrewLocationOverlay(this, brewLocations,
this.getResources().getDrawable(R.drawable.beer_icon_small));
overlays = map.getOverlays();
overlays.add(brewLocationOverlay);

map.getController().setCenter(
new GeoPoint((int) (brewLocations.get(0).getLatitude() * 1e6),
(int) (brewLocations.get(0).getLongitude() * 1e6)));

map.getController().zoomToSpan(
0 TECHNIKA 52. Tworzenie aktywności MapActivity z powiązanym widokiem MapView 429

brewLocationOverlay.getLatSpanE6(),
brewLocationOverlay.getLonSpanE6());
}

@Override
protected boolean isRouteDisplayed() {
return false;
}
}

Jak można się domyślić, każdą aktywność MapActivity trzeba powiązać z wido-
kiem MapView . Klasa pochodna od MapActivity obsługuje cykl życia tego widoku.
Obok widoku MapView zapisujemy kolekcję nakładek wyświetlanych później na
mapie . Podobnie jak przy stosowaniu innych widoków, położenie i podstawowe
cechy widoku MapView zwykle należy definiować w zasobie układu, a następnie
korzystać z tego zasobu w kodzie . Ponieważ używamy mapy pełnoekranowej,
w aktywności MapResults korzystamy z układu LinearLayout z pojedynczym
elementem.
<com.google.android.maps.MapView android:id="@+id/map"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="<WPISZ_KLUCZ>" />

Jak widać, w układzie można zdefiniować kilka właściwości widoku MapView, na


przykład to, czy widok można kliknąć, a także używany klucz API. W tym miejscu
wpisz klucz API, jeśli samodzielnie tworzysz aplikację BrewMap lub korzystasz
z pobranego kodu źródłowego. Jeśli zainstalowałeś pakiet APK z aplikacją, nie
musisz przejmować się kluczem. Kiedy aplikacja jest podpisana i wyeksportowana
do formatu API, korzysta z klucza stosowanego w czasie jej rozwijania. Nie można
jednak użyć nieodpowiedniego klucza w trakcie budowania i eksportowania
programu.
Po uzyskaniu referencji do widoku MapView używamy implementacji klasy
ItemizedOverlay . Klasa ItemizedOverlay obejmuje listę egzemplarzy klasy Overlay
´Item. Każdy obiekt klasy OverlayItem odpowiada punktowi, który można przed-
stawić na mapie. Tu obiekt klasy ItemizedOverlay tworzymy w odrębnej klasie
BrewLocationOverlay, która jest omawiana dalej. Gdy zostanie utworzony obiekt
klasy z nakładkami, można umieścić je na mapie za pomocą metody addOverlay .
Kiedy potrzebne komponenty są już gotowe, można użyć obiektu klasy
MapController do wyśrodkowania i odpowiedniego przybliżenia mapy . Klasa
MapController obejmuje liczne przydatne metody. Wyśrodkowanie mapy nie
wymaga wyjaśnień. Przybliżanie obszaru jest równie łatwe do przeprowadzenia,
jednak trudniejsze do zrozumienia. Aplikacja tworzy prostokąt na podstawie
lewego górnego i prawego dolnego punktu, a następnie przybliża ten obszar. Bez
tej wygodnej techniki musielibyśmy sprawdzić wszystkie punkty i samodzielnie
przeprowadzić obliczenia.
430 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

OMÓWIENIE
Klasa MapResults wyświetla mapę z funkcją przy-
bliżania i poruszania się po niej. Wygląd goto-
wej aplikacji przedstawiono na rysunku 10.6.
Dotknięcie mapy powoduje wyświetlenie ikon
przybliżania i oddalania. Klasa ItemizedOverlay
odpowiada za rozmieszczanie na mapie symboli
i obsługę związanych z nimi kontrolek.

10.3.4. Używanie nakładek na mapę


Map Google’a najczęściej (jeśli pominąć nawi-
gację) używa się do umieszczania na mapie
symboli i tras na podstawie niestandardowych
lub własnych źródeł danych. W omówieniu po-
przedniego listingu wyjaśniliśmy, że dane są zapi-
sywane w obiekcie klasy ItemizedOverlay. Pora
utworzyć tę klasę, aby uzupełnić kod zwią- Rysunek 10.6. Aktywność
zany z mapami. MapResults aplikacji
BrewMap wyświetla
pełnoekranowy widok
0 TECHNIKA 53. Wyświetlanie elementów
MapView z symbolami
OverlayItems w widoku MapView pobranych lokali

Klasa MapView za pomocą nakładek wyświetla


dane nad polami mapy. Można używać wielu nakładek, a na mapie można
umieścić dane dowolnego rodzaju. Tworząc klasę bazową, zapewniasz sobie peł-
ną kontrolę nad tym, co jest wyświetlane i w którym miejscu. Jeśli nie potrze-
bujesz tego rodzaju kontroli i chcesz umieścić na mapie standardowe symbole,
możesz użyć wygodnej podklasy do obsługi nakładek — ItemizedOverlay.
PROBLEM
Chcemy umieścić symbole w określonych miejscach widoku MapView. Symbole te
mają reagować na gesty użytkownika.
ROZWIĄZANIE
Overlay to klasa bazowa używana do umieszczania danych na mapach w Andro-
idzie. Klasa ta obsługuje proste zdarzenia dotknięcia i obejmuje kilka metod
z rodziny draw. Jeśli masz szczególne wymagania, możesz zacząć od tej klasy,
jednak częściej tworzy się klasy pochodne od ItemizedOverlay, która jest dosto-
sowana do umieszczania w widoku MapView wielu symboli za pomocą obiektów
graficznych.
Klasa BrewLocationOverlay z listingu 10.9 to implementacja klasy Itemized
´Overlay. To w przedstawionej tu klasie aplikacja BrewMap tworzy i wyświetla
symbole. Kod klasy pokazany jest na listingu 10.10.
0 TECHNIKA 53. Wyświetlanie elementów OverlayItems w widoku MapView 431

Listing 10.10. Klasa BrewLocationOverlay z aplikacji BrewMap

public class BrewLocationOverlay


extends ItemizedOverlay<OverlayItem> {

private List<BrewLocation> brewLocations;


private Context context;

public BrewLocationOverlay(Context context,


List<BrewLocation> brewLocations, Drawable marker) {
super(boundCenterBottom(marker));
this.context = context;
this.brewLocations = brewLocations;
if (brewLocations == null) {
brewLocations = new ArrayList<BrewLocation>();
}
populate();
}

@Override
protected OverlayItem createItem(int i) {
BrewLocation brewLocation = brewLocations.get(i);
GeoPoint point =
new GeoPoint(
(int) (brewLocation.getLatitude() * 1e6),
(int) (brewLocation.getLongitude() * 1e6));
return new OverlayItem(point, brewLocation.getName(), null);
}

@Override
public boolean onTap(final int index) {
BrewLocation brewLocation = brewLocations.get(index);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("BrewLocation")
.setMessage(brewLocation.getName()
+ "\n\nChcesz przejść na stronę lokalu?")
.setCancelable(true)
.setPositiveButton("Tak", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent i = new Intent(context, BrewLocationDetails.class);
i.putExtra(BrewMapApp.PUB_INDEX, index);
context.startActivity(i);
}
})
.setNegativeButton("Nie", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();

return true;
}

@Override
public int size() {
432 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

return brewLocations.size();
}
}

Jak wspomnieliśmy, BrewLocationOverlay to klasa pochodna od ItemizedOverlay .


Ta ostatnia klasa obsługuje wiele operacji związanych z wyświetlaniem i obsługą
dotknięć. Do konstruktora klasy BrewLocationOverlay trzeba przekazać kolekcję
obiektów klasy BrewLocation i domyślny obiekt graficzny . Kolekcja określa
punkty, które pojawią się na mapie.
W konstruktorze wywołujemy konstruktor klasy bazowej i metodę boundCenter
´Bottom . Metoda ta sprawia, że inne metody wyświetlają symbole wyśrod-
kowane względem piksela znajdującego się w połowie dolnego wiersza. Także
w konstruktorze wywołujemy ważną metodę populate . Jest to metoda inicja-
lizacyjna, która wykonuje wewnętrzne operacje porządkowe. Dla nowych obiek-
tów klasy ItemizedOverlay zawsze trzeba wywoływać tę metodę. Według doku-
mentacji należy ją wywołać bezpośrednio po uzyskaniu danych przez obiekt
klasy ItemizedOverlay, lecz przed wykonywaniem dalszych operacji.
Po kodzie konstruktora następuje kod metody createItem . Metoda ta tworzy
obiekt klasy GeoPoint na podstawie podanego elementu z danymi (ustalanego
według przekazanego indeksu), a następnie obiekt klasy OverlayItem. Każdy
obiekt klasy OverlayItem obejmuje symbol, nagłówek i kilka innych właściwości,
a także stan. Obiekty tego typu są wyświetlane na ekranie.
W metodzie onTap określamy operacje wykonywane po dotknięciu nałożo-
nych elementów . Metoda onTap przyjmuje indeks, dlatego można stwierdzić,
który element został wybrany, i odpowiednio zareagować. Aplikacja BrewMap
wyświetla okno AlertDialog umożliwiające użytkownikowi przedstawienie szcze-
gółowych informacji o lokalu. W końcowej części metoda onTap zwraca wartość
true . Oznacza to, że zdarzenie nie ma być przekazywane do innych nałożonych
warstw (jeśli takie istnieją). Zdarzenie ma być obsługiwane tylko w danej metodzie.
Aktywność BrewLocationDetails (jej działanie pokazano na prawym zrzucie
z rysunku 10.6) jest bardzo prosta, dlatego nie prezentujemy jej kodu. Aplikacja
za pomocą dodatkowych danych z intencji przekazuje do aktywności wskaźnik
z informacją, który element użytkownik wybrał. Następnie aktywność wyświetla
szczegółowe informacje i pozwala użytkownikowi uzyskać dodatkowe dane za
pośrednictwem telefonu, przeglądarki albo wbudowanej aplikacji z mapami
(rysunek 10.7).
TWORZENIE ZMIENNEJ KLASY ITEMIZEDOVERLAY. Trzeba
pamiętać, że nakładki w aplikacji BrewMap są niezmienne. Ich działanie
ogranicza się do wyświetlenia kolekcji punktów. W niektórych aplikacjach
trzeba dynamicznie dodawać lub usuwać elementy z nakładki. Aby było to
możliwe, należy przesłonić metodę removeOverlay i wywołać w niej metodę
setLastFocusedIndex(-1), a następnie ponownie metodę populate. Pominię-
cie tych wywołań prowadzi do wystąpienia wyjątków.
10.4. Podsumowanie 433

Rysunek 10.7.
Używanie intencji
do otrzymania
dodatkowych
informacji na temat
wybranego lokalu

W Androidzie korzystanie z nakładek i nakładanych elementów jest proste. Można


utworzyć klasy pochodne od wielu pomocniczych klas wykonujących standardowe
operacje. Jeśli chcesz, możesz też zaimplementować własne wyspecjalizowane
klasy od podstaw. Ponadto możesz utworzyć kilka nakładek na jedną mapę, aby
przedstawić różne zbiory danych, choć tu użyliśmy tylko jednej warstwy.
OMÓWIENIE
Po utworzeniu klasy BrewLocationOverlay otrzymujemy widok MapView z danymi
oraz obsługą przeciągania i przybliżania. Kod mapy kończy aplikację BrewMap.
Program ten umożliwia użytkownikom wyszukiwanie lokali w okolicy ich bie-
żącego położenia (lub innego podanego miejsca), a wyświetlane dane pochodzą
z interfejsu API projektu Beer Mapping. Gotowy produkt to kompletna i przydatna
aplikacja, która zaprowadzi nas do kufla zimnego piwa w dowolnym miejscu
świata — a przynajmniej w tych licznych krajach, które uwzględniono w projekcie
Beer Mapping.

10.4. Podsumowanie
W tym rozdziale omówiliśmy dużo materiału. Przedstawiliśmy najważniejsze
pojęcia i definicje związane ze współrzędnymi geograficznymi, a następnie opi-
saliśmy przykładową aplikację, w której wykorzystaliśmy odpowiednie interfejsy
API Androida i Google’a.
434 ROZDZIAŁ 10. Najważniejsza jest lokalizacja

Najpierw omówiliśmy podstawowe kwestie związane z pobieraniem infor-


macji o położeniu za pomocą menedżera LocationManager i z używaniem dostaw-
ców LocationProvider. Pokazaliśmy, że poszczególni dostawcy mają różne moż-
liwości i wymagania. Dalej użyliśmy klasy LocationHalper do zarządzania
odbiornikiem LocationListener w celu ustalenia bieżącego położenia urządzenia.
Po omówieniu podstaw przeszliśmy do pracy z mapami.
Wyjaśniliśmy, jak zainstalować dodatek Google APIs Add-On dla Androida
(dodatek ten obejmuje pakiety do obsługi map). Wytłumaczyliśmy też, jak uzyskać
klucz do interfejsu Maps API. Następnie utworzyliśmy kompletną aplikację
z aktywnością MapActivity i widokiem MapView. W programie wykorzystaliśmy
dane z nakładki ItemizedOverlay. Pokazaliśmy, jak połączyć wszystkie elementy
interfejsu Maps API w funkcjonalną interaktywną mapę w aplikacji na Android.
Uatrakcyjnianie aplikacji
za pomocą multimediów

W tym rozdziale
Q Wykrywanie możliwości urządzenia
Q Odtwarzanie plików multimedialnych
Q Używanie aparatu
Q Nagrywanie dźwięku i filmów

Spośród wszystkich wynalazków służących do masowej komunikacji


obraz nadal jest najpowszechniej rozumianym językiem.
Walt Disney
Android Market to tętniący życiem i pełny zróżnicowanych produktów rynek
z aplikacjami i grami. Znajdziesz tu między innymi rozmaite programy multime-
dialne: odtwarzacze plików muzycznych, aplikacje do edycji zdjęć czy programy
do rozsyłania sygnału wideo. Oczywiste jest, że jeśli chcesz utworzyć aplikację
z tej kategorii, musisz opanować wiele androidowych interfejsów API związanych
z multimediami. A jeżeli nie chcesz rozwijać programów multimedialnych? Jeśli
jesteś jednym z wielu programistów tworzących aplikacje, w których to nie mul-
timedia są najważniejsze? Czy w takiej sytuacji ma znaczenie, które interfejsy API
są potrzebne do odtwarzania muzyki lub tworzenia miniatur z kadrem z filmu?
To podchwytliwe pytania. Ten rozdział dotyczy multimediów, dlatego nie
możemy zacząć go od napisania, że informacje na ten temat nie będą dla Ciebie
przydatne. Z pewnością okażą się wartościowe. Niezależnie od tego, jakiego
rodzaju aplikacje rozwijasz, prawdopodobnie będziesz potrzebował podzbioru

435
436 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

interfejsów API związanych z multimediami. Multimedia stają się coraz waż-


niejszym aspektem wszystkich aplikacji na Android.
Wynika to z prostej przyczyny. Urządzenia mają zaawansowane funkcje mul-
timedialne, a każdego roku poziom rozwiązań z tego obszaru rośnie. Ponieważ
dostępne są tak innowacyjne mechanizmy, właściciele smartfonów zaczęli się
posługiwać multimediami do wyrażania siebie i komunikowania się z innymi.
Dlatego zamiast pytania: „Czy w aplikacji są potrzebne funkcje multimedialne?”,
można zadać inne: „Czy użytkownicy aplikacji powinni móc komunikować się
z innymi?”. Z pewnością ludzie nadal korzystają z komunikacji tekstowej (przez
SMS-y, e-maile czy aktualizacje statusu w sieciach społecznościowych) i telefo-
nicznej, jednak coraz częściej przesyłają zdjęcia i filmy. Z tego rozdziału dowiesz
się, jak używać Androida do korzystania z multimediów (na przykład wyświe-
tlania zdjęć i słuchania nagrań dźwiękowych) i ich generowania (robienia zdjęć,
nagrywania filmów itd.). Aby przedstawić różne techniki, rozwijamy aplikację
MediaMogul. Aplikacja ta umożliwia użytkownikom wybieranie zdjęć, plików
muzycznych i filmów w celu utworzenia pokazu slajdów. Pozwala też robić nowe
zdjęcia i nagrywać filmy oraz rejestrować dźwięki dodawane do pokazu.
Zanim zajmiemy się zawiłym opisem wykonywania na Androidzie różnych
zadań związanych z multimediami, trzeba najpierw przedstawić środowisko pracy.
Interfejsy API byłyby nieprzydatne, gdyby nie sprzęt, do którego dają dostęp.
To nie żart. Urządzenia z Androidem są tak zróżnicowane, jak aplikacje w sklepie
Android Market. Musisz uwzględnić to, czy urządzenie ma aparat. A czy ma
aparat po stronie wyświetlacza? Czy nagrywa filmy? A dźwięki? Nawet jeśli apli-
kacja ma tylko odtwarzać filmy, powraca pytanie o rozmiar ekranu urządzenia.
Ponadto jak szybkie jest połączenie do transferu danych? Jeśli odpowiesz sobie
na takie pytania, może przejść Ci ochota na udostępnianie filmu w formacie HD
do odtwarzania na określonym urządzeniu. Tak więc zanim zajmiemy się inter-
fejsami API, omawiamy różne funkcje urządzeń z Androidem dostępne poprzez
pakiet SDK.

11.1. Funkcje zbyt zaawansowane


dla telefonu wielofunkcyjnego
Eksperci z dziedziny technologii czasem używają pojęcia telefon wielofunkcyjny
(ang. feature phone) w znaczeniu pejoratywnym. Termin ten jest stosowany jako
antonim określenia smartfon. Każdy telefon z Androidem jest uznawany za smart-
fon, przy czym smartfony z tą platformą rzadko są ubogie w funkcje. Odtwarza-
nie plików MP3 i filmów oraz aparaty to funkcje z telefonów wielofunkcyjnych,
powszechnie występujące także w smartfonach z Androidem. Jak ustalić, które
funkcje obsługuje dany telefon? Zobaczmy, jak odpowiedzieć na to pytanie w spo-
sób najprzydatniejszy dla programistów aplikacji.
0 TECHNIKA 54. Wykrywanie możliwości 437

0 TECHNIKA 54. Wykrywanie możliwości

W czasie powstawania tej książki na rynku dostępnych było ponad 200 różnych
smartfonów z Androidem i ponad 30 tabletów z tą platformą. Firmy zapowie-
działy już wprowadzenie 50 nowych urządzeń z Androidem, choć sprzęt ten nie
trafił jeszcze do sklepów. Większość urządzeń z omawianą platformą ma aparaty,
a w kilkunastu smartfonach znajduje się aparat po stronie wyświetlacza. Roz-
dzielczość tych aparatów wynosi od 1 do 10 megapikseli, jednak wiele aparatów
po stronie wyświetlacza to tak zwane aparaty VGA o rozdzielczości 0,3 mega-
piksela. Zróżnicowanie jest bardzo duże. Powinieneś to uwzględnić, jeśli zamie-
rzasz korzystać z funkcji multimedialnych w aplikacjach na Android.
PROBLEM
Tworzymy aplikację używającą multimediów. Chcemy się upewnić, że aplika-
cja działa prawidłowo na każdym urządzeniu, na którym można ją zainstalować,
i optymalnie wykorzystuje jego możliwości.
ROZWIĄZANIE
Zróżnicowanie urządzeń jest nieodłączne od Androida. W rozdziale 4. pokaza-
liśmy, w jaki sposób Android pozwala radzić sobie z różnorodnością wielkości
i rozdzielczości wyświetlaczy. Jest to sposób charakterystyczny dla Androida.
Nie trzeba borykać się z problemem zróżnicowania, ponieważ rozwiązanie jest
wbudowane w platformę. To samo dotyczy funkcji sprzętowych. W Androidzie
są one elementami pierwszej kategorii, a w pliku manifestu należy dokładnie okre-
ślić funkcje potrzebne aplikacji. Listing 11.1 to fragment pliku manifestu aplikacji
MediaMogul.

Listing 11.1. Deklarowanie wymagań sprzętowych w manifeście aplikacji

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.media"
android:versionCode="1"
android:versionName="1.0">
<!-- Element application pominięto. -->
<uses-feature android:name="android.hardware.camera"
android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus"
android:required="true"/>
<uses-feature android:name="android.hardware.camera.flash"
android:required="false" />
<uses-feature android:name="android.hardware.camera.front"
android:required="false" />
<uses-feature android:name="android.hardware.microphone"
android:required="true"/>
<uses-permission
android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-sdk android:minSdkVersion="9" />
</manifest>
438 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

Za pomocą elementu <uses-feature> można zadeklarować funkcje sprzętu i opro-


gramowania potrzebne aplikacji. W przykładzie deklarujemy, że aplikacja używa
aparatu, a także jego pewnych mechanizmów, na przykład autofocusu, lampy
błyskowej i aparatu po stronie wyświetlacza. Niektóre z tych funkcji są wymagane,
a inne są opcjonalne. Warto zauważyć, że domyślna wartość atrybutu required
to true.
Czy użytkownik urządzenia bez aparatu może zainstalować taką aplikację?
Tak, jest to możliwe. System operacyjny nie sprawdza pliku manifestu w czasie
instalacji. Jednak aplikacja Market porównuje manifest z listą funkcji urządze-
nia i odfiltrowuje aplikacje, których użytkownik nie mógłby uruchomić. Gdy
użytkownik pobiera aplikacje tylko ze sklepu Android Market, nigdy nie zobaczy
danego programu, jeśli urządzenie nie ma potrzebnych funkcji.
Warto też zauważyć, że na listingu 11.1 pokazano dwa elementy <uses-per
´missions> z manifestu. W starszych wersjach Androida zawartość elementu
uses-feature platforma określała na podstawie elementu uses-permissions. Upraw-
nienie CAMERA oznaczało, że potrzebne są funkcje camera i camera.autofocus. Podob-
nie z uprawnienia RECORD_AUDIO wynikało, że niezbędna jest funkcja microphone.
Nadal można ograniczyć się do podawania uprawnień (element uses-permission)
i automatycznie uzyskać metadane z elementów uses-feature, jednak lepiej jest
bezpośrednio określać listę potrzebnych funkcji i uprawnień.
Może zauważyłeś, że w kodzie podana jest funkcja camera.front, z czego
wynika, iż oczekujemy aparatu po stronie wyświetlacza. Jednak wartość atry-
butu required to false, dlatego funkcja ta jest opcjonalna. Jeśli urządzenie nie
ma aparatu po stronie wyświetlacza, aplikacja Market nie odfiltrowuje aplikacji
z plikiem manifestu z listingu 11.1. Dlatego należy w kodzie programu sprawdzić,
czy urządzenie ma odpowiedni aparat. Można to zrobić na kilka sposobów.
Jedna z możliwości to użycie klasy android.content.pm.PackageManager do bez-
pośredniego sprawdzenia dostępności funkcji. Oto przykładowy kod:
private boolean hasFrontFacingCamera(){
PackageManager mgr = this.getPackageManager();
for (FeatureInfo fi : mgr.getSystemAvailableFeatures()){
if (fi.name.equals(
PackageManager.FEATURE_CAMERA_FRONT)){
return true;
}
}
return false;
}

Jeśli w manifeście jako opcjonalną ustawiłeś dowolną inną funkcję, możesz podać
nazwę odpowiadającej jej stałej z klasy PackageManager, zamiast używać podanej
tu funkcji. Dla aparatu po stronie wyświetlacza możliwe jest jeszcze prostsze
rozwiązanie.
0 TECHNIKA 54. Wykrywanie możliwości 439

private Camera getFrontFacingCamera(){


for (int i=0;i<Camera.getNumberOfCameras()){
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing ==
Camera.CameraInfo.CAMERA_FACING_FRONT){
return Camera.open(i);
}
}
return null;
}

Jeśli metoda getFrontFacingCamera zwraca null, wiadomo, że w urządzeniu nie


ma aparatu po stronie wyświetlacza. Na podobnej zasadzie działa dawna metoda
statyczna Camera.open(), która zwraca null, gdy w urządzeniu w ogóle nie ma
aparatu. W ten sposób można sprawdzić, ile aparatów znajduje się w urządzeniu.
Jeżeli aplikacja ma włączać dodatkowe funkcje w zależności od tego, czy dostępny
jest aparat po stronie wyświetlacza, w opisany sposób ustala, czy może to zrobić.
OMÓWIENIE
Programiści aplikacji internetowych doskonale znają model stopniowego wzbo-
gacania. Polega on na pisaniu aplikacji sieciowej pod kątem „najmniejszego
wspólnego mianownika”, czyli najprostszej przeglądarki, na której program ma
działać. Następnie za pomocą pomysłowych technik można sprawdzać, czy
dostępne są bardziej zaawansowane funkcje. Jeśli są, należy zmodyfikować
interfejs użytkownika i (lub) działanie aplikacji w celu wykorzystania tych funk-
cji. Tworzenie aplikacji na urządzenia z Androidem może przebiegać podobnie.
Jednak w Android wbudowano mechanizmy do uwzględniania różnic między
urządzeniami.
Nieprzypadkowo sprawdzamy dostępność aparatu po stronie wyświetlacza.
Jednym z bardzo atrakcyjnych aspektów Androida jest to, że różne elementy
(na przykład sprzęt, sieć, system operacyjny itd.) mogą być usprawniane nieza-
leżnie od siebie. Producenci urządzeń mogli dodać aparaty po stronie wyświe-
tlacza przed utworzeniem odpowiednich androidowych interfejsów API. Kod
dwóch pokazanych tu metod można skompilować w wersjach 2.3 i nowszych
Androida, jednak aparaty po stronie wyświetlacza znajdowały się już w smart-
fonach z Androidem 2.1.
Rejestrowanie nagrań multimedialnych jest jednym z obszarów, w których
warto przemyśleć różnice sprzętowe między urządzeniami. Odtwarzanie mul-
timediów jest prostsze. Przy wyświetlaniu filmów należy uwzględnić wielkość
ekranu (zobacz klasę android.view.WindowManager), a przy odtwarzaniu nagrań
dźwiękowych lub wideo przez internet warto też zastanowić się nad przepusto-
wością sieci (zobacz klasę android.net.ConnectivityManager). Przyjrzyjmy się teraz,
jak znajdować, wczytywać i odtwarzać multimedia.
440 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

11.2. Zarządzanie multimediami


Funkcje odtwarzania multimediów na urządzeniach z Androidem dają progra-
mistom wiele ciekawych możliwości. Jednak zanim zaczniesz korzystać z tych
funkcji do udostępniania multimediów, musisz dotrzeć do odpowiednich plików.
Pliki multimedialne w urządzeniu można znaleźć w wielu miejscach. Mogą być
integralną częścią aplikacji. Wtedy są dostępne bezpośrednio w jej pakiecie.
Inne pliki nie stanowią integralnej części programu. Niektóre z nich należą do
użytkownika. Mogą to być pliki, które użytkownik sam zapisał w urządzeniu (na
przykład utwory muzyczne) lub zarejestrował przy jego użyciu, robiąc zdjęcia lub
nagrywając filmy za pomocą wbudowanego aparatu. Pliki multimedialne zwykle
przechowuje się w pamięci zewnętrznej urządzenia, na karcie SD. Znane są pewne
konwencje dotyczące miejsca przechowywania multimediów na kartach SD, jed-
nak w praktyce pliki mogą znajdować się w różnych katalogach. Na szczęście
Android udostępnia wiele sposobów na znajdowanie i wczytywanie multimediów
z różnych źródeł. Zacznijmy od najprostszej sytuacji — wczytywania multimediów
z zasobów lub plików.

0 TECHNIKA 55. Korzystanie z zasobów i plików

Większość języków programowania obsługuje operacje wejścia-wyjścia. Dotyczy


to także języka Java używanego w Androidzie. W pewnych sytuacjach Android
wzbogaca możliwości Javy lub udostępnia alternatywne rozwiązanie. Jeśli chodzi
o operacje wejścia-wyjścia, można używać większości powiązanych z nimi inter-
fejsów API z Javy do wczytywania różnych plików multimedialnych na potrzeby
ich odtwarzania. W niektórych sytuacjach Android udostępnia dodatkowe metody
pomocnicze. Zobaczmy, jak za pomocą plikowych operacji wejścia-wyjścia wczy-
tywać multimedia.
PROBLEM
Chcemy znaleźć pliki multimedialne i wczytać je do aplikacji. Pliki te mogą sta-
nowić część programu lub być niezależne od niego i znajdować się na karcie SD.
W obu sytuacjach trzeba uzyskać uchwyt do pliku i użyć go w androidowych
interfejsach API do odtwarzania multimediów. W ten sposób można odtworzyć
plik użytkownikowi.
ROZWIĄZANIE
Jak już wspomnieliśmy, istnieje kilka różnych sposobów na wyszukiwanie i wczy-
tywanie odtwarzanych multimediów. Zacznijmy od najprostszej techniki — wyko-
rzystania androidowych zasobów. Używamy ich w wielu miejscach książki. Na
tym etapie prawdopodobnie znasz już zasoby w postaci układu, łańcuchów
znaków i obiektów graficznych (w tym rysunków). Pora pokazać, jak używać pli-
ków innego rodzaju, w tym multimediów.
0 TECHNIKA 55. Korzystanie z zasobów i plików 441

Aplikacja MediaMogul na początku umożliwia użytkownikowi wybranie zdjęć,


które program wyświetli w pokazie slajdów. Na rysunku 11.1 przedstawiono
wygląd gotowej aplikacji.
Użytkownik czasem ma w urządzeniu dużą
liczbę zdjęć. W czasie, kiedy je przegląda, apli-
kacja może odtwarzać w tle przyjemną muzykę.
Plik dźwiękowy z nagraniem jest rozpowszech-
niany razem z aplikacją. Najlepiej umieścić go
w katalogu /res/raw. Dla plików z tego katalogu
są generowane identyfikatory zasobów, jednak
narzędzie do budowania aplikacji na Android nie
przetwarza tych plików w inny sposób i dołącza
je do programu w ich pierwotnej postaci. Na lis-
tingu 11.2 pokazano, jak używać takich plików
w aplikacji.
POBIERZ PROJEKT MEDIAMOGUL. Kod
źródłowy projektu i pakiet APK do urucha-
miania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Rysunek 11.1. Siatka
Ponieważ niektóre listingi skrócono, abyś ze zdjęciami użytkownika
mógł skoncentrować się na konkretnych
zagadnieniach, zalecamy pobranie kompletnego kodu źródłowego i śle-
dzenie go w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/In6j, plik APK: http://mng.bz/X0Mk.

Listing 11.2. Używanie zasobów multimedialnych dołączonych do aplikacji

public class ImageBrowserActivity extends Activity {


private MediaPlayer player;
// Dalszy kod pominięto.

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Dalszy kod pominięto.
playThemeSong();
}

private void playThemeSong(){


player = MediaPlayer.create(this, R.raw.constancy);
player.start();
}
}

Kod z listingu 11.2 to fragment aktywności ImageBrowserActivity z rozwijanej


aplikacji. Kod tej aktywności generuje interfejs użytkownika umożliwiający wybra-
nie zdjęć do tworzonego przez program pokazu slajdów. Na listingu większość
442 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

kodu pominięto, co pozwala skoncentrować się na kodzie do odtwarzania muzyki.


Do wykonania tej operacji potrzebny jest egzemplarz klasy android.media.Media
´Player . Egzemplarz tej klasy można utworzyć na wiele sposobów. Tu uży-
liśmy jednej z kilku metod fabrycznych. Przyjmuje ona obiekt typu Context i iden-
tyfikator zasobu (czyli odtwarzanego pliku multimedialnego). Ten ostatni para-
metr to wygenerowana wartość R.raw.constancy. Prawdopodobnie domyślasz
się już, gdzie zapisany jest plik z nagraniem. Struktura katalogów pokazana na
rysunku 11.2 pozwoli Ci się upewnić.
Jak widać na rysunku 11.2, w katalogu
/res/raw można umieścić plik dowolnego
rodzaju. Narzędzie aapt tworzy dla zapisa-
nego pliku identyfikator tak samo jak dla
innych zasobów. Na listingu 11.2 pokazano,
że identyfikatora można używać bezpośre-
dnio w obiekcie klasy MediaPlayer, co po-
zwala na wygodne używanie zasobów multi-
medialnych będących częścią aplikacji.
Kod z listingu 11.2 jest prosty, jednak
używamy w nim wiele często stosowanych
mechanizmów wczytywania multimediów
powiązanych z aplikacją. Zakładamy, że
istnieje konkretny plik multimedialny, który
należy wczytać. Prowadzi to do ścisłego
powiązania kodu aplikacji z plikami. Jeśli Rysunek 11.2. Struktura katalogów
potrzebujesz większej elastyczności, powi- aplikacji MediaMogul
nieneś użyć klasy android.content.res. z zaznaczonym zasobem
dźwiękowym
´AssetManager. Załóżmy, że zamiast jed-
nego pliku aplikacja ma odtwarzać cały katalog. Możliwe, że menedżer produktu
nie potrafi ustalić, które utwory udostępnić, dlatego chcemy zminimalizować
powiązania między nagraniami (tytułami, ich liczbą itd.) a kodem. Do uzyska-
nia takiego efektu można wykorzystać klasę AssetManager, co pokazano na lis-
tingu 11.3.

Listing 11.3. Używanie klasy AssetManager do wczytywania utworów do kolejki

public class ImageBrowserActivity extends Activity {


private MediaPlayer player;
// Pozostały kod pominięto.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Pozostały kod pominięto.
playThemeMusic();
}

private void playThemeMusic() {


0 TECHNIKA 55. Korzystanie z zasobów i plików 443

player = new MediaPlayer();


AssetManager mgr = getResources().getAssets();
String audioDir = "audio";
try {
final LinkedList<FileDescriptor> queue =
new LinkedList<FileDescriptor>();
for (String song : mgr.list("audio")){
queue.add(mgr.openFd(audioDir
+ "/" + song).getFileDescriptor());
}
playNextSong(queue);
player.setOnCompletionListener(
new OnCompletionListener(){
@Override
public void onCompletion(MediaPlayer mp) {
try {
playNextSong(queue);
} catch (IOException e) {
Log.e(LOG_TAG,
"Wyjątek przy wczytywaniu utworu",e);
}
}
});
} catch (IOException e) {
Log.e(LOG_TAG, "Wyjątek przy wczytywaniu utworu",e);
}
}

private void playNextSong(LinkedList<FileDescriptor> queue)


throws IOException {
if (!queue.isEmpty()){
FileDescriptor song = queue.poll();
player.setDataSource(song);
player.prepare();
player.start();
}
}
}

Klasa AssetManager zapewnia dostęp do zasobów multimedialnych wchodzą-


cych w skład aplikacji. W klasie tej wykorzystywane są interfejsy API wejścia-
wyjścia z Javy. Uchwyt do obiektu klasy ResourceManager można łatwo uzyskać
za pomocą obiektu klasy Context . Następnie przy użyciu uchwytu można pobrać
poszczególne pliki lub katalogi. Na listingu 11.3 uchwyt służy do pobrania listy
wszystkich plików z katalogu audio . Następnie można przejść po plikach, przy
czym na etapie kompilacji nie trzeba znać ich nazw. Dla każdego pliku można
uzyskać deskryptor java.io.FileDescriptor , który można przekazać do obiektu
klasy MediaPlayer zamiast identyfikatora (używanego na listingu 11.2). Być
może zastanawiasz się, jakie pliki są wczytywane przez kod z listingu 11.3. Nową
strukturę katalogów pokazano na rysunku 11.3.
Tu pliki multimedialne są zapisane w katalogu /assets, poza katalogiem /res
z zasobami. W katalogu /assets można tworzyć podkatalogi pomagające w po-
rządkowaniu plików. W przykładzie pliki znajdują się w podkatalogu audio. Na
444 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

listingu 11.3 występuje ścisłe powiązanie kodu z nazwą tego podkatalogu, ale
nie ma powiązania z konkretnymi plikami. W podkatalogu tym może znajdować
się jeden plik lub sto nagrań (podkatalog
może nawet być pusty). Aplikacja umieszcza
wszystkie utwory w kolejce i odtwarza je.
Dwie opisane do tej pory techniki
dobrze sprawdzają się dla zasobów dołą-
czonych do aplikacji. Często jednak po-
trzebny jest dostęp do plików multimedial-
nych przechowywanych przez użytkownika
w urządzeniu. Aplikacja MediaMogul ma
wyświetlać zdjęcia użytkownika i pozwa-
lać wybrać zestaw do pokazu slajdów. Pliki
tego rodzaju znajdują się na karcie SD.
Istnieje kilka sposobów na znalezienie
takich multimediów i uzyskanie do nich
dostępu. Najłatwiej jest traktować multi-
media jak pliki, podobnie jak przy korzy-
staniu z klasy AssetManager. Na listingu 11.4
przedstawiono kod tej prostej techniki.
Rysunek 11.3. Zapisywanie plików
Listing 11.4. Adapter zapewniający dostęp multimedialnych jako materiałów
do zdjęć z karty SD

import static android.os.Environment.*;


private class GridAdapter extends BaseAdapter{
private List<File> imageFiles;
private List<Bitmap> thumbs;
// Pozostały kod pominięto.
public GridAdapter(){
File picturesDir =
getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
int maxNumFiles;
String[] nameArray = picturesDir.list();
if (nameArray == null){
maxNumFiles = 0;
} else {
maxNumFiles = nameArray.length;
}
ArrayList<File> theFiles = new ArrayList<File>(maxNumFiles);
if (maxNumFiles == 0) return;
for (String fileName : nameArray){
File file = new File(picturesDir, fileName);
if (file.isFile()){
theFiles.add(file);
}
}
imageFiles = Collections.unmodifiableList(theFiles);
ArrayList<Bitmap> tempThumbs =
new ArrayList<Bitmap>(imageFiles.size());
0 TECHNIKA 55. Korzystanie z zasobów i plików 445

for (int i=0;i<imageFiles.size();i++){


tempThumbs.add(makeThumb(i));
}
thumbs = Collections.unmodifiableList(tempThumbs);
}
}

Kod na listingu 11.4 to fragment klasy GridAdapter. Jest to klasa wewnętrzna


z klasy ImageBrowserActivity, której fragmenty znajdują się na listingach 11.2 i 11.3.
Jak wskazuje nazwa klasy GridAdapter, jest ona implementacją adaptera (android.
´widget.Adapter) powiązaną z widokiem android.widget.GridView. Widok GridView
służy do wyświetlania siatki z wszystkimi zdjęciami, które użytkownik ma do
wyboru. Zdjęcia pochodzą z publicznie dostępnego katalogu . Aplikacje mogą
przechowywać zdjęcia w dowolnym miejscu na karcie SD, jednak obowiązują
pewne konwencje, określające, gdzie należy zapisywać pliki tego rodzaju. Na
listingu 11.4 używamy metody Environment.getExternalStoragePublicDirectory
i kilku stałych określających standardowe katalogi. W tabeli 11.1 przedstawiono
kilka takich stałych i rodzaje plików przechowywanych w poszczególnych
katalogach.
Tabela 11.1. Stałe określające publiczne katalogi na multimedia

Stała Opis

DIRECTORY_PICTURES Zdjęcia, które użytkownik zapisał w urządzeniu.

DIRECTORY_MUSIC Pliki muzyczne, które użytkownik zapisał w urządzeniu.

DIRECTORY_MOVIES Filmy, które użytkownik zapisał w urządzeniu.

DIRECTORY_DCIM Zdjęcia i filmy, które użytkownik zarejestrował za pomocą aparatu


z urządzenia.

Są to często używane katalogi, do których dostęp może być potrzebny, jeśli


aplikacja korzysta z multimediów. Jest też kilka innych przydatnych katalogów.
Niektóre z nich przechowują pewne typy plików dźwiękowych, na przykład
dzwonki, sygnały powiadomienia itd. Na listingu 11.4 metoda getExternalStorage
´PublicDirectory zwraca obiekt typu java.io.File. Następnie można łatwo
przejść po plikach z katalogu . Należy utworzyć obiekt typu File dla każdego
elementu, który nie jest katalogiem , i ostatecznie zapisać każdy rysunek
w obiekcie typu Bitmap . Zauważ, że obiekty klasy Bitmap zapisujemy w pamięci
podręcznej. Używamy ich w widoku GridView, lecz nie chcemy, aby aplikacja
tworzyła wspomniane obiekty przy przewijaniu widoku. Zapisywanie takich
obiektów w pamięci podręcznej sprawia, że przewijanie jest płynniejsze.
Każdy użytkownik po uruchomieniu aplikacji widzi zdjęcia zapisane na jego
urządzeniu. Kod z listingu 11.4 to dowód na to, jak łatwo można pobrać pliki
multimedialne, a następnie korzystać z nich tak samo jak z innych obiektów klasy
File w Javie. Tu wczytujemy zdjęcia do obiektów klasy Bitmap, jednak równie
łatwo można wczytać pliki dźwiękowe lub filmowe do obiektu klasy MediaPlayer.
446 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

Urządzenia bez karty SD


W poprzednim przykładzie użyliśmy metody Environment.getExternalStoragePublic
´Directory. Pamięć zewnętrzna to zwykle karta SD (przeważnie typu microSD).
Co się jednak dzieje, jeśli urządzenie nie ma takiej karty? Dotyczy to urządzeń
trzeciej generacji z Androidem, na przykład Nexusa S, a także innych urządzeń z tą
platformą, takich jak GoogleTV. W takich urządzeniach część wewnętrznej (wbu-
dowanej) pamięci pełni w Androidzie funkcję wirtualnej pamięci zewnętrznej.
Użytkownicy mogą zamontować ten obszar i korzystać z niego jak z systemu plików.
Następnie aplikacje mogą uzyskać dostęp do tego obszaru za pomocą metod takich
jak getExternalStoragePublicDirectory.

OMÓWIENIE
Pliki multimedialne są przede wszystkim plikami. Android udostępnia kilka pro-
stych sposobów korzystania z takich plików. Techniki te są oparte na znanych
wielu programistom plikowych interfejsach API Javy. Z tego rozdziału dowie-
działeś się już, jak używać takich interfejsów, jeśli pliki są zapisane w katalogu
/res/assets aplikacji lub na karcie SD urządzenia. Jeżeli znasz mechanizmy wej-
ścia-wyjścia z Javy, wiesz, że pozwalają na dostęp także do plików innych niż
lokalne. Można użyć sieci i otworzyć strumień java.io.InputStream, aby wczy-
tać plik graficzny czy dźwiękowy przez sieć. Nietrudno jest zmodyfikować kod
z listingu 11.4 i pobrać zdjęcia z konta użytkownika z serwisu Flickr lub z innej
witryny do udostępniania fotografii. Przykładowe rozwiązanie tego rodzaju przed-
stawiono w rozdziale 2., gdzie aplikacja wczytywała przez sieć zdjęcia ofert dnia
z eBaya.
Inne podobieństwo między kodem z rozdziału 2. a kodem z listingu 11.4
dotyczy tego, że w obu miejscach przy pracy z kolekcjami zdjęć używana jest
pamięć podręczna. W rozdziale 2. w pamięci podręcznej aplikacja zapisywała zdję-
cia pobrane przez sieć. W tym rozdziale zdjęcia są przechowywane lokalnie
w urządzeniu, jednak są też przechowywane w pamięci podręcznej. Aby zrozu-
mieć sens stosowania tej techniki, umieść dużą liczbę zdjęć w publicznym kata-
logu na zdjęcia, a następnie uruchom aplikację MediaMogul. Widok GridView
będzie obejmował wiele wierszy. Jeśli zdjęcia są zapisane w pamięci podręcznej,
można płynnie przewijać widok GridView. Jednak jeżeli przy każdym wyświe-
tlaniu komórki z widoku GridView trzeba wywołać metodę makeThumb, przewijanie
nie przebiega płynnie. Wspomniana metoda wczytuje zdjęcie i zmienia jego
wielkość, aby pasowało do widoku GridView. Morał z tego taki, że nie należy two-
rzyć „w locie” zdjęć wyświetlanych w listach lub siatkach.
Przechowywanie zdjęć w pamięci ma oczywistą wadę — może nastąpić
wyczerpanie pamięci. Standardowym rozwiązaniem tego problemu jest odcze-
kanie z rozpoczęciem wczytywania zdjęć z listy lub siatki do momentu, kiedy
przewijanie się kończy. Dlatego zdjęcia pojawiają się, kiedy użytkownik kończy
przewijać okna. Do czasu wyświetlenia rzeczywistych zdjęć można pokazywać
jeden rysunek zastępczy.
0 TECHNIKA 56. Korzystanie z dostawców treści multimedialnych 447

Do tej pory omawialiśmy pliki multimedialne będące częścią aplikacji bądź


zapisane we współużytkowanym katalogu urządzenia. Jednak pliki multimedialne
mogą znajdować się także w innych miejscach. Na szczęście Android udostęp-
nia dobry sposób na znalezienie wszystkich takich plików. Umożliwia to klasa
MediaStore. W dwóch następnych technikach omawiamy różne metody korzysta-
nia z tej przydatnej klasy.

0 TECHNIKA 56. Korzystanie z dostawców treści multimedialnych

Jeśli regularnie używasz urządzenia z Androidem, może zwróciłeś uwagę na ska-


ner multimediów. Pracę tego mechanizmu najłatwiej zauważyć w momencie
rozruchu urządzenia, kiedy to na pasku powiadomień pojawia się komunikat ze
skanera. Jeśli ilość pamięci w urządzeniu jest duża, wyszukiwanie multimediów
może trwać stosunkowo długo. Skaner działa też w innych momentach. Zawsze
jednak wyszukuje pliki multimedialne i tworzy współużytkowaną bazę danych
dotyczącą takich plików. Rozwiązanie to jest wygodne i daje dużo możliwości.
Zobaczmy, jak uzyskać dostęp do danych ze wspomnianej bazy.
PROBLEM
Mamy wyszukać na urządzeniu wszystkie multimedia określonego typu, a także
uzyskać dotyczące ich metadane. Chcemy wiedzieć, jak znajdować i otwierać
poszczególne pliki. Nie zamierzamy jednak wczytywać wszystkich plików z urzą-
dzenia lub przetwarzać ich nagłówków i metadanych. Proces ten byłby bardzo
długi i wymagałby napisania dużej ilości kodu, który jest zupełnie zbędny, ponie-
waż androidowy skaner multimediów wykonuje potrzebne operacje. Potrzebu-
jemy tylko dostępu do danych zebranych przez skaner.
ROZWIĄZANIE
Skaner multimediów jest dostępny dla aplikacji poprzez androidowego dostawcę
treści (obiekt klasy ContentProvider). Dostawców treści opisaliśmy dokładnie
w rozdziałach 7. i 8., dlatego tu nie omawiamy szczegółowo ich działania. Zamiast
tego koncentrujemy się na dostawcach potrzebnych do wyszukiwania w urządze-
niu plików multimedialnych.
Wróćmy do aplikacji MediaMogul. Na tym etapie gotowy jest interfejs, który
umożliwia użytkownikowi przeglądanie zapisanych na urządzeniu zdjęć i wybie-
ranie spośród nich tych przeznaczonych do pokazu slajdów. Następny krok
dotyczy wybierania muzyki odtwarzanej w trakcie pokazu. Chcemy, aby aplikacja
znajdowała wszystkie utwory muzyczne na urządzeniu i następnie pozwalała
użytkownikowi wybrać jeden z nich. Nie chcemy jednak wyświetlać wszystkich
plików dźwiękowych, ponieważ niektóre z nich to dzwonki, dźwięki alarmów
i powiadomień, a także inne efekty dźwiękowe używane w aplikacjach. Aplikacja
ma pokazywać tylko utwory muzyczne. Obiekt androidowej klasy MediaStore
448 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

(tworzony przez skaner multimediów) udostępnia metadane wszystkich plików


multimedialnych, w tym plików dźwiękowych, dlatego jest dobrym rozwiązaniem.
Tę część aplikacji pokazano na rysunku 11.4.

Rysunek 11.4. Wybór utworu


do pokazu slajdów

Na rysunku 11.4 widać listę utworów. Przy każdym z nich znajduje się przycisk
opcji i przycisk odtwarzania. Źródłem danych z tej listy jest adapter. To w nim
trzeba uzyskać dostęp do dostawcy danych, aby pobrać utwory znajdujące się
w urządzeniu. Kod tego adaptera przedstawiono na listingu 11.5.

Listing 11.5. Wyszukiwanie wszystkich utworów z urządzenia

import static android.provider.BaseColumns._ID;


import static android.provider.MediaStore.Audio.AudioColumns.ARTIST;
import static android.provider.MediaStore.Audio.AudioColumns.IS_MUSIC;
import static android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
import static android.provider.MediaStore.MediaColumns.DATA;
import static android.provider.MediaStore.MediaColumns.TITLE;
private class AudioListAdapter extends BaseAdapter{
private Cursor cursor;
private Activity activity = AudioBrowserActivity.this;
//Pozostały kod pominięto.
public AudioListAdapter(){
super();
String[] columns = {TITLE, ARTIST,_ID, DATA};
String whereClause = IS_MUSIC + " = ?";
0 TECHNIKA 56. Korzystanie z dostawców treści multimedialnych 449

String[] whereValues = {"1"};


cursor = managedQuery(EXTERNAL_CONTENT_URI,
columns,
whereClause,
whereValues,
null
);
}

@Override
public int getCount() {
return cursor.getCount();
}

@Override
public Object getItem(int position) {
Song song = new Song();
cursor.moveToPosition(position);
song.title = cursor.getString(cursor.getColumnIndex(TITLE));
song.artist = cursor.getString(cursor.getColumnIndex(ARTIST));
song.id = cursor.getLong(cursor.getColumnIndex(_ID));
song.setUri(cursor.getString(
cursor.getColumnIndex(DATA)));
return song;
}
}

Adapter z listingu 11.5 zapewnia dane dla widoku ListView widocznego na


rysunku 11.4. Adapter ten korzysta z kursora, czyli obiektu typu android.database.
´Cursor . Takie obiekty są zwykle pobierane od dostawców treści lub z bazy
SQLite (szczegółowe omówienie obu możliwości znajdziesz w rozdziale 7.). Kur-
sor jest tworzony przez adapter w konstruktorze. Adapter określa przy tym, jakie
dane chce otrzymać od dostawcy treści z klasy MediaStore . Tu potrzebne są
tylko wewnętrzne identyfikatory utworów, tytuły i wykonawcy, a także identyfi-
katory URI utworów. Ponadto zgodnie z deklaracją adapter żąda od obiektu
klasy MediaStore tylko plików muzycznych; dzwonki i efekty dźwiękowe są pomi-
jane . W ostatnim kroku adapter tworzy kursor za pomocą metody managedCursor
z klasy bazowej aktywności . Prowadzi to do powiązania cyklu życia kursora
z cyklem życia aktywności, przez co nie trzeba ręcznie zamykać kursora i wyko-
nywać podobnych operacji.
Utworzony za pomocą klasy MediaStore kursor można wykorzystać do pobra-
nia obiektów klasy Song odpowiadających elementom widoku ListView . Obiekt
klasy Song użyty na listingu 11.5 to prosta struktura danych ukrywająca informa-
cje od dostawcy danych z klasy MediaStore. Jedynym wartym uwagi aspektem
jest to, że klasa Song jest typu Parcelable, dlatego można przekazywać jej egzem-
plarze między aktywnościami. Dane w tej klasie są w większości proste. Są to
łańcuchy znaków i liczby typu long. Jedynym złożonym polem jest uri, które
przechowuje obiekt klasy android.net.Uri. W zamian można też zastosować łań-
cuch znaków (tego typu dane zwraca kursor), jednak klasa Uri jest wygodniejsza,
ponieważ to właśnie jego potrzebują obiekty klasy MediaPlayer odtwarzające
450 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

utwory. Klasa Uri jest ponadto typu Parcelable. Tak więc metoda setUri klasy Song
tworzy identyfikator URI na podstawie pobranego z kursora łańcucha znaków .
OMÓWIENIE
Dostawcy treści w Androidzie dają duże możliwości, choć mają kiepski interfejs
(w postaci kursorów), który nie ukrywa implementacji. W tej technice w prostym
przykładzie pokazano, jak z dostawcy z klasy MediaStore pobierać podstawowe
informacje o wszystkich piosenkach z urządzenia. Można uzyskać też inne dane
o utworach, na przykład wielkość pliku, kodowanie (mp3, ogg itd.), rok nagrania
czy numer utworu na płycie. Więcej informacji znajdziesz w klasie android.
´provider.MediaStore.Audio.

Kursory i adaptery
Programiści często korzystają z kursorów w adapterach widoków ListView lub
GridView w aplikacjach na Android. Nie jest zaskoczeniem, że w Androidzie dostęp-
nych jest kilka klas, które ułatwiają stosowanie tego rozwiązania. Pierwsza z nich
to klasa abstrakcyjna CursorAdapter. Klasa ta przyjmuje kursor i wymaga zaimple-
mentowania metod do tworzenia nowego widoku oraz wiązania danych z kursora
z widokiem. Klasa automatycznie zarządza przesuwaniem kursora. Jeśli tworzysz
widok na podstawie pliku XML z układem, możesz użyć klasy ResourceCursorAdapter
(jest to abstrakcyjna klasa pochodna od CursorAdapter). Często można jeszcze
bardziej skrócić kod przez użycie klasy SimpleCursorAdapter. Jest to konkretna
klasa pochodna od ResourceCursorAdapter, dlatego układ musi być zapisany w pliku
XML. W przypadku wielu prostych widoków ListView i GridView można zastosować
klasę SimpleCursorAdapter bez żadnego dodatkowego kodu. Wystarczy użyć jed-
nego lub kilku prostych obiektów powiązanych z widokami TextView lub ImageView.
Obiekty te należy wskazywać za pomocą identyfikatorów. W bardziej skompliko-
wanych aplikacjach, takich jak opisywana w tym rozdziale, można utworzyć imple-
mentację metody SimpleCursorAdapter.ViewBinder.

W urządzeniu użytkownika znajdują się pliki multimedialne inne niż dźwiękowe.


W technice 55. pokazaliśmy, jak wczytywać zdjęcia ze współużytkowanego kata-
logu na pliki graficzne. Za pomocą dostawcy treści z klasy MediaStore można zna-
leźć zdjęcia z tego katalogu, a także z innych folderów z pamięci zewnętrznej.
Informacje o plikach graficznych z urządzenia udostępnia klasa android.provider.
´MediaStore.Images. Ostatni typ multimediów, które można pobrać za pośred-
nictwem dostawcy treści z klasy MediaStore, to filmy. Prawdopodobnie domyślasz
się już, że informacje o filmach udostępnia klasa android.provider.MediaStore.
´Video. W następnej technice pokazujemy, jak pobrać filmy w aplikacji gene-
rującej pokazy slajdów.

0 TECHNIKA 57. Używanie intencji i aktywności

W poprzedniej technice pokazaliśmy, jak proste jest pobranie wszystkich utwo-


rów muzycznych z urządzenia za pomocą dostawcy treści. Dostawca ten posłu-
żył do utworzenia widoku ListView przedstawionego na rysunku 11.4. Jeszcze
0 TECHNIKA 57. Używanie intencji i aktywności 451

wcześniej bezpośrednio wczytywaliśmy pliki graficzne z pamięci zewnętrznej,


aby utworzyć widok GridView. W obu technikach użytkownik miał wybrać pliki
multimedialne wykorzystywane w pokazie slajdów. Interfejs użytkownika ma
kilka ciekawych funkcji. Umożliwia na przykład wybranie wielu zdjęć i odtwo-
rzenie utworu bez jego zaznaczania. W trzecim kroku tworzenia pokazu slajdów
użytkownik ma wybrać jeden film wyświetlany na zakończenie pokazu. W apli-
kacjach często trzeba poprosić użytkownika o wybranie jednego pliku multime-
dialnego; Android umożliwia łatwe wykonanie tego zadania.
PROBLEM
Użytkownik musi wybrać w aplikacji jeden plik multimedialny. Możliwe, że
aplikacja ta działa w sieci społecznościowej, a użytkownik ma wybrać jedno zdję-
cie lub jeden film, aby podzielić się nim ze znajomymi. Możliwe też, że pro-
gram to gra, a użytkownik ma wskazać utwór odtwarzany w trakcie rozgrywki.
W obu sytuacjach potrzebny jest tylko jeden plik. Tworzenie niestandardowego
interfejsu użytkownika do wykonywania tak prostego zadania wydaje się przesadą.
ROZWIĄZANIE
W Androidzie rozwiązanie tego problemu obejmuje dwa istotne mechanizmy.
Podstawą jest oparta na intencjach architektura Androida. Pozwala ona na two-
rzenie luźno powiązanych aplikacji, a przede wszystkim umożliwia działanie dru-
giej części rozwiązania — aktywności wielokrotnego użytku, służących do wybie-
rania plików multimedialnych. Na rysunku 11.5 pokazano wygląd aplikacji.

Rysunek 11.5. Wybieranie filmu wyświetlanego po pokazie slajdów

Aktywność można ponownie wykorzystać tylko wtedy, gdy znana jest nazwa akcji,
która ją uruchamia. Na listingu 11.6 pokazano, jak zastosować gotową aktywność.

Listing 11.6. Używanie standardowej aktywności do wybierania pliku wideo

public class VideoChooserActivity extends Activity {


private static final int SELECT_VIDEO = 1;
private Uri videoUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
452 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

super.onCreate(savedInstanceState);
setContentView(R.layout.video_chooser);
Button vidBtn = (Button) findViewById(R.id.vidBtn);
vidBtn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View button) {
Intent videoChooser =
new Intent(Intent.ACTION_GET_CONTENT);
videoChooser.setType("video/*");
startActivityForResult(
videoChooser, SELECT_VIDEO);
}
});
// Dalszy kod interfejsu użytkownika pominięto.
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != SELECT_VIDEO || resultCode != RESULT_OK){
return;
}
VideoView video = (VideoView) findViewById(R.id.video);
videoUri = data.getData();
video.setVideoURI(videoUri);
// Kod do odtwarzania filmów pominięto.
}
}

Aktywność pobiera uchwyt do przycisku z ekranu . Użytkownik musi dotknąć


przycisk, aby rozpocząć proces wybierania filmu. W tym momencie należy uru-
chomić standardową aktywność do pobierania plików. Aktywność tę można wska-
zać za pomocą stałej Intent.ACTION_GET_CONTENT . Omawiana aktywność obej-
muje filtr intencji, określający, że obsługiwane są wszystkie intencje z akcją
Intent.ACTION_GET_CONTENT. Aktywność można wykorzystać dla różnego rodzaju
plików i multimediów, jednak tu potrzebujemy tylko filmów. Dlatego należy usta-
wić typ MIME na video/* . Odpowiada on dowolnym plikom filmowym, nieza-
leżnie od formatu i kodowania. Następnie wywoływana jest metoda startActivity
´ForResult nadrzędnej aktywności . Metoda ta uruchamia aktywność pobie-
rającą dane.
Metoda startActivityForResult działa asynchronicznie (zwraca wartość void),
powoduje jednak wyświetlenie potrzebnej aktywności na pierwszym planie,
dlatego z perspektywy użytkownika metoda pracuje synchronicznie. Z uwagi na
asynchroniczną naturę trzeba jednak określić wywołanie zwrotne pozwalające
pobrać dane. Odpowiednie wywołanie zwrotne — metoda onActivityResult —
jest zdefiniowane w klasie Activity. Tu przesłaniamy tę metodę . Przyjmuje
ona intencję. Metodę getData tej intencji można wykorzystać do otrzymania
identyfikatora URI filmu wybranego przez użytkownika .
Po lewej stronie rysunku 11.5 przedstawiono interfejs użytkownika. Widać
na nim przycisk Wybierz film użyty na listingu 11.6. Dotknięcie tego przycisku
11.3. Odtwarzanie multimediów 453

prowadzi do uruchomienia aktywności pobierającej dane. Do wyświetlania plików


do wyboru służy kontrolka Gallery pokazana po prawej stronie rysunku 11.5.
Kontrolka ta wyświetla pliki uporządkowane według katalogów. Jeśli filmów jest
więcej, są pokazywane w formie stosu. Dotknięcie stosu powoduje przekształ-
cenie go w siatkę, w której użytkownik może wybrać film. To wygodny interfejs
użytkownika, a na dodatek tworzony automatycznie! Ponadto interfejs ten działa
w wielu aplikacjach na Android, dlatego użytkownicy znają go i wiedzą, jak działa.
OMÓWIENIE
To ostatnia z trzech technik, które można wykorzystać w aplikacjach w celu
wyszukiwania i wybierania plików multimedialnych. Z perspektywy programi-
sty techniki te odpowiadają różnym warstwom abstrakcji. Na najniższym pozio-
mie możliwy jest bezpośredni dostęp do systemu plików. Nawet w tej warstwie
Android ułatwia wyszukiwanie plików. Następny poziom abstrakcji to używanie
dostawcy treści do pobierania metadanych plików przechowywanych w urzą-
dzeniu. Nie trzeba wiedzieć, gdzie znajdują się te pliki, aby pobrać informacje
na ich temat. Trzecia technika odpowiada najwyższemu poziomowi abstrakcji.
W tym podejściu aplikacja zna tylko identyfikator URI jednego pliku wybranego
przez użytkownika. Często jest to jedyna potrzebna informacja. Następnie w razie
konieczności można wykonać operacje z niższych poziomów abstrakcji. Tech-
nika ta ma też dodatkową zaletę, ponieważ nie trzeba pisać kodu interfejsu użyt-
kownika i procesu wybierania plików.
Wspomnieliśmy też, że interfejs użytkownika wykorzystywany w aktywności
obejmuje kontrolkę Widget. Jest to popularna kontrolka do wyświetlania zdjęć
i filmów. Mogliśmy jej użyć także w technice 55., w aktywności do wybierania
zdjęć. Kontrolkę tę należy ustawić w adapterze udostępniającym jej pokazywane
zdjęcia. Kontrolka Gallery widoczna na rysunku 11.5 ma przypisane dodatkowe
style, jednak także w domyślnej postaci jest atrakcyjna.
Poznałeś już wiele sposobów na wyszukiwanie i wybieranie plików multime-
dialnych w Androidzie. Pora pokazać, jak korzystać z pobranych plików. Można
na przykład wyświetlać zdjęcia, a także odtwarzać nagrania dźwiękowe i filmowe.
W niektórych aplikacjach jest to niepotrzebne. Możliwe, że użytkownik musi
tylko wybrać pliki multimedialne do udostępnienia ich przez usługę sieciową.
Jednak aplikacja MediaMogul potrzebuje wybranych plików, ponieważ tworzy
pokaz slajdów na podstawie wszystkich multimedialnych materiałów wskazanych
przez użytkownika.

11.3. Odtwarzanie multimediów


Wyszukiwanie i wczytywanie plików multimedialnych zapisanych w urządze-
niu lub nawet dostępnych przez sieć jest przydatniejsze, jeśli pliki te można
odtworzyć. Android ułatwia wykonanie tego zadania, a przy tym udostępnia
454 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

pewne zaawansowane opcje. Odtwarzanie plików multimedialnych jest zwykle


prostsze niż ich wyszukiwanie i wczytywanie. W tym podrozdziale omawiamy
trzy techniki korzystania z plików graficznych, dźwiękowych i wideo. Cały opi-
sany tu kod znajduje się w jednej aktywności aplikacji MediaMogul. Aktywność
ta wyświetla pokaz slajdów na podstawie rysunków, utworu muzycznego i filmu
wybranych przez użytkownika. Niestety, zrzuty ilustrujące pracę aplikacji nie
oddają jej atrakcyjności, ponieważ nawet proces wyświetlania zdjęć jest animo-
wany. Pierwsza technika dotyczy właśnie wyświetlania zdjęć i efektu animacji.

0 TECHNIKA 58. Zdjęcia i proste animacje

Wyświetlanie zdjęć w aplikacji na Android jest proste. Pokazaliśmy już, jak to


zrobić. Zwykle należy utworzyć obiekt klasy Bitmap i przekazać go jako dane
źródłowe do widoku ImageView. Operacji tej nie trzeba tu omawiać, jednak cza-
sem wyświetlanie zdjęć można uatrakcyjnić. Jedną z możliwości jest tworzenie
animacji. Są one przydatne przy wyświetlaniu pokazów slajdów, takich jak w apli-
kacji MediaMogul. W każdym programie wyświetlającym serię zdjęć warto zasto-
sować animacje przejść między poszczególnymi obrazami.
PROBLEM
Chcemy wyświetlić serię zdjęć. Jednocześnie aplikacja ma pokazywać tylko ich
podzbiór, na przykład jedno zdjęcie. Aby zwiększyć atrakcyjność pokazu, chcemy
użyć animowanych przejść między obrazami.
ROZWIĄZANIE
Miłym zaskoczeniem może być informacja, że tworzenie animacji w Androidzie
jest proste. Pakiet android.view.animation obejmuje liczne klasy do tworzenia ani-
macji różnego typu. W tabeli 11.2 pokazano te wygodne klasy i opisano ich wyso-
kopoziomowe funkcje.
Tabela 11.2. Podstawowe typy animacji w Androidzie

Klasa animacji Opis

AlphaAnimation Animacja w formie zmiany przezroczystości widoku.

RotateAnimation Animacja w formie rotacji widoku.

ScaleAnimation Animacja w formie zmiany wielkości widoku.

TranslationAnimation Animacja w formie zmiany pozycji widoku.

Oprócz stosowania tych podstawowych rodzajów animacji można tworzyć wła-


sne lub łączyć proste animacje. Do niektórych klas można dodać własne klasy
Interpolator i (lub) Transformation, aby dostosować podstawowe animacje do
potrzeb. Zauważ też, że w tabeli 11.2 napisano, iż każdą animację można zastoso-
wać do dowolnego widoku, a nie tylko do widoków ImageView. Za pomocą andro-
idowych narzędzi związanych z animacjami możesz naprawdę rozwinąć swoją
0 TECHNIKA 58. Zdjęcia i proste animacje 455

kreatywność. Tu używamy animacji AlphaAnimation, tak aby widok ImageView


zanikał na ekranie i pojawiał się na nim. Aby zrozumieć działanie kodu, przyjrzyj
się najpierw układowi interfejsu użytkownika (listing 11.7).

Listing 11.7. Układ pokazu slajdów (w formacie XML)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView android:id="@+id/slide0"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView android:id="@+id/slide1"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>

Układ pokazu slajdów jest prosty. Nadrzędny układ FrameLayout obejmuje dwa
widoki ImageView. Opis układu FrameLayout znajduje się w punkcie 4.3.2. Układ
ten dokuje wszystkie widoki podrzędne do górnego lewego narożnika ekranu
i wyświetla tylko jeden taki widok naraz. Tu układ dokuje dwa widoki Image
´View — slide0 i slide1. W danym momencie widać tylko jeden z tych widoków.
Domyślnie jest to widok slide0, zdefiniowany jako pierwszy. Przyjrzyjmy się
teraz aktywności korzystającej z tego widoku, przedstawionej na listingu 11.8.

Listing 11.8. Aktywność do tworzenia pokazu slajdów

public class SlideshowActivity extends Activity {

private ImageView leftSlide;


private ImageView rightSlide;
private Handler handler = new Handler();
private static final int TIME_PER_SLIDE = 3*1000;
private boolean playingSlides = true;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.slideshow);
leftSlide = (ImageView) findViewById(R.id.slide0);
rightSlide = (ImageView) findViewById(R.id.slide1);
// Dodatkowy kod do obsługi plików dźwiękowych i filmu pominięto.
}
@Override
public void onResume() {
super.onResume();
final DissolveTransition animation =
new DissolveTransition();
handler.postDelayed(new Runnable(){
@Override
public void run() {
456 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

animation.nextSlide();
}
}, 100);
}
}

Aktywność do tworzenia i wyświetlania pokazu slajdów to SlideshowActivity.


Kiedy jest tworzona po raz pierwszy, generuje zmienne składowe na dwa widoki
ImageView zdefiniowane na listingu 11.7 . Przy kolejnych uruchomieniach
tworzy egzemplarz niestandardowej animacji zdefiniowanej na listingu 11.9.
Następnie używa obiektu handler do uruchomienia (po odczekaniu 100 milise-
kund) animacji w głównym wątku interfejsu użytkownika . Jak widać, większość
skomplikowanych operacji wykonuje klasa DissolveTransition. Na listingu 11.9
pokazano, w jaki sposób klasa ta tworzy efekt zanikania i wyłaniania się używany
w pokazie slajdów.

Listing 11.9. Niestandardowa animacja z pokazu slajdów

private class DissolveTransition{


private ArrayList<String> images;
private int count = 0;
private Bitmap currentImage = null;
private Bitmap nextImage = null;
public DissolveTransition() {
images =
getIntent().getStringArrayListExtra("imageFileNames");
currentImage = getNextImage();
leftSlide.setImageBitmap(currentImage);
nextImage = getNextImage();
rightSlide.setImageBitmap(nextImage);
count = 1;
}
private void nextSlide() {
AlphaAnimation animation = new AlphaAnimation(0.0f, 1.0f);
if ((count % 2) == 0) {
animation = new AlphaAnimation(1.0f, 0.0f);
}
animation.setStartOffset(TIME_PER_SLIDE);
animation.setDuration(TIME_PER_SLIDE);
animation.setFillAfter(true);
animation.setAnimationListener(new
Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
if (playingSlides){
nextImage = getNextImage();
ImageView backgroundImage =
(count % 2 == 0) ? rightSlide :
leftSlide;
backgroundImage.setImageBitmap(
nextImage);
0 TECHNIKA 58. Zdjęcia i proste animacje 457

count++;
nextSlide();
}
}
});
rightSlide.startAnimation(animation);
currentImage = nextImage;
}
}

Klasa do obsługi animacji najpierw pobiera tablicę przekazanych zdjęć , które


ma wyświetlać w pokazie. Następnie używa metody getNextImage, aby pobrać
losowe zdjęcie z przekazanej listy . Metody tej nie ma na listingu; jeśli chcesz
się dowiedzieć, jak działa, zajrzyj do kompletnego kodu źródłowego. Metoda ta
wybiera losowe zdjęcie spośród jeszcze niewyświetlonych. Na listingu 11.8 widać,
że aplikacja uruchamia animację przez wywołanie metody nextSlide. W meto-
dzie tej używamy efektu AlphaAnimation. Animacja na zmianę odsłania zdjęcie
(wartość alpha rośnie od 0 do 1) i ukrywa je (wartość alpha spada od 1 do 0) .
Dalej ustawiamy odbiornik animationListener animacji AlphaAnimation przez utwo-
rzenie wewnętrznej klasy anonimowej z implementacją interfejsu Animation.
´AnimationListener. W interfejsie tym zdefiniowane są trzy wywołania zwrotne —
dla początku, końca i powtórzenia animacji. Tu ważne jest tylko zakończenie ani-
macji . Aplikacja wyświetla kolejne zdjęcia przez wywoływanie metody next
´Slide. Opisany kod pozwala utworzyć przejścia z efektem zanikania, często
używane w popularnym oprogramowaniu do tworzenia prezentacji.
OMÓWIENIE
Ta technika to krótkie wprowadzenie w świat animacji. Wyjaśniliśmy, że Android
udostępnia kilka podstawowych animacji. Możesz ich używać do tworzenia bar-
dziej złożonych wersji. Opisywane tu animacje czasem nazywa się przejściami
(ang. tween). Jeśli korzystałeś kiedyś z programów do tworzenia interaktywnych
grafik, na przykład z narzędzia Adobe Flash, wiesz, jak dużo można osiągnąć
z zastosowaniem przejść.
W animacjach stan jest często mniej istotny niż w opisanym przykładzie.
Takie animacje wykonują przejście dla danego widoku i nie wykonują żadnych
innych operacji. Przejście można wówczas zdefiniować w samym XML-u. Następ-
nie można wczytać animację za pomocą identyfikatora zasobu, używając klasy
AnimationUtils. Po wczytaniu animacji można zastosować ją do dowolnego widoku.
Pokazany wcześniej kod wyświetla wszystkie wybrane przez użytkownika
zdjęcia i wykorzystuje efekt przejścia z zanikaniem z listingu 11.9. Teraz wystar-
czy dodać jednoczesne odtwarzanie muzyki i uzyskamy pokaz slajdów. Warto
też zastanowić się, jak aplikacja ma działać po wstrzymaniu i ponownym uru-
chomieniu pokazu. Aby przeprowadzać te operacje, trzeba się dowiedzieć, jak
odtwarzać i kontrolować pliki dźwiękowe.
458 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

0 TECHNIKA 59. Kontrolowanie dźwięku

Czy to odtwarzacz plików MP3, który jest też telefonem? A może to telefon,
który potrafi odtwarzać takie pliki? Nie ma to znaczenia. Użytkownicy oczekują,
że urządzenie potrafi odtwarzać muzykę. Z pewnością oczekują tego użytkow-
nicy Androida. Odtwarzanie muzyki jest przydatne nie tylko w przeznaczonych do
tego aplikacjach. Dźwięki — czy to całe utwory, czy efekty informujące o wystą-
pieniu określonych zdarzeń — są przydatne w wielu programach.
PROBLEM
Chcemy odtwarzać w aplikacji pliki dźwiękowe (z muzyką czy efektami). Pliki
mogą być zapisane lokalnie lub w sieci. Nieistotny jest ich format. Mogą to być
pliki MP3 bądź Ogg, przy czym w urządzeniu zainstalowany musi być odpowiedni
kodek. Chcemy też na podstawie zdarzeń zachodzących w aplikacji kontrolować,
kiedy muzyka jest odtwarzana, a kiedy należy ją zatrzymać.
ROZWIĄZANIE
We wcześniejszej części rozdziału, na listingach 11.2 i 11.3, używana jest klasa
MediaPlayer. W rozwijanej aplikacji służy ona do odtwarzania muzyki w tle. Klasa
ta to główny mechanizm odtwarzania plików dźwiękowych. Używamy jej też
w kilku innych miejscach aplikacji MediaMogul — między innymi na ekranie
wyboru utworów przedstawionym na rysunku 11.4. Na ekranie tym znajdują
się przyciski Odtwórz, pozwalające użytkownikowi wysłuchać fragmentu utworu
i zdecydować, czy aplikacja ma odtwarzać ten utwór w trakcie pokazu slajdów.
Na listingu 11.5 znajduje się kod adaptera powiązanego z listą utworów. Brakuje
tam jednak kodu metody getView, która tworzy interfejs użytkownika z każdym
utworem, a przede wszystkim dodaje przycisk Odtwórz. Kod tej metody przed-
stawiono na listingu 11.10.

Listing 11.10. Tworzenie interfejsu użytkownika z listą utworów

@Override
public View getView(int position, View row, ViewGroup parent) {
// Pozostały kod interfejsu użytkownika pominięto.
final Song song = (Song) getItem(position);
final Button playBtn = holder.playBtn;
if (playingSongs.contains(song.id)){
playBtn.setText(R.string.pause);
} else {
playBtn.setText(R.string.play);
}
playBtn.setOnClickListener(new OnClickListener(){
private Handler handler = new Handler();
MediaPlayer player = null;
long maxTime = 15L*1000; // 15 sekund
long timeLeft = maxTime;
Runnable autoStop;
@Override
public void onClick(View button) {
if (player == null){
0 TECHNIKA 59. Kontrolowanie dźwięku 459

player = MediaPlayer.create(
activity, song.uri);
}
if (!playingSongs.contains(song.id)){
player.start();
playingSongs.add(song.id);
autoStop = new Runnable(){
@Override
public void run() {
player.pause();
player.seekTo(0);
playingSongs.remove(
song.id);
playBtn.setText(
R.string.play);
timeLeft = maxTime;
}
};
handler.postDelayed(autoStop,
timeLeft);
playBtn.setText(R.string.pause);
} else {
player.pause();
playingSongs.remove(song.id);
timeLeft = maxTime –
player.getCurrentPosition();
playBtn.setText(R.string.play);
handler.removeCallbacks(autoStop);
}
}

});
// Kod generujący przyciski opcji pominięto.
return row;
}
}

Kod z listingu 11.10 wykonuje wiele operacji. Zaczynamy od utworzenia obiektu


klasy Song odpowiadającego elementom listy (kod tworzący takie obiekty
znajdziesz na listingu 11.5). Następnie pobieramy uchwyt playBtn do przycisku
Odtwórz. Adapter obejmuje obiekt klasy HashSet z wszystkimi odtwarzanymi
utworami. Czy oznacza to, że można wybrać więcej niż jeden utwór? Właśnie
tak. Użytkownik może dotknąć przycisk Odtwórz przy kilku plikach, a aplikacja
jednocześnie odtworzy je wszystkie. Nie jest to specjalnie przyjazne użytkow-
nikowi rozwiązanie, jednak nietrudno je zmodyfikować. Wystarczy wstrzymywać
odtwarzany utwór w reakcji na dotknięcie innego. Chcemy jednak zademon-
strować, że domyślnie obiekt klasy MediaPlayer nie jest singletonem. Jednocześnie
może istnieć wiele obiektów tej klasy, a każdy z nich może odtwarzać utwór.
Jeśli chcesz zmodyfikować kod, tak aby w danym momencie aplikacja odtwarzała
tylko jeden utwór, utwórz jeden obiekt klasy MediaPlayer współużytkowany przez
wszystkie nagrania z listy.
460 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

Wróćmy do listingu 11.10. W kodzie sprawdzamy, czy wybrany utwór jest odtwa-
rzany, i zmieniamy tekst na przycisku playBtn z Odtwórz na Zatrzymaj (lub na
odwrót). Następnie ustawiamy komponent obsługi kliknięć przycisku playBtn.
Najpierw należy utworzyć obiekt klasy Handler używany do automatycznego
zatrzymywania odtwarzania utworu . Aplikacja ma odtwarzać tylko 15 pierw-
szych sekund pliku. Jeśli użytkownik wstrzyma utwór po 5 sekundach, program
ma to zapamiętać i po wznowieniu odtworzyć tylko 10 następnych sekund. Dla-
tego w kodzie tworzymy zmienną lokalną używaną do śledzenia, ile czasu zostało
do końca utworu .
Dalej znajduje się implementacja metody onClick komponentu obsługi klik-
nięć. Metoda najpierw sprawdza, czy obiekt klasy MediaPlayer jest już utworzony.
Jeśli nie, należy utworzyć taki obiekt (z wykorzystaniem identyfikatora URI
utworu). Następnie metoda sprawdza, czy utwór jest już odtwarzany . Jeżeli nie,
metoda tworzy obiekt typu Runnable z mechanizmem automatycznego wstrzymy-
wania odtwarzania . Obiekt ten jest uruchamiany po 15 sekundach odtwarzania
utworu. Wtedy następuje wstrzymanie pracy obiektu klasy MediaPlayer i powrót
do początku pliku. Uruchamianie obiektu typu Runnable można zaplanować na
podstawie tego, ile z 15 sekund pozostało do końca odtwarzania .
Jeśli w momencie dotknięcia przycisku playBtn trwa odtwarzanie, należy
wstrzymać pracę obiektu klasy MediaPlayer i obliczyć, ile czasu pozostało z limitu
15 sekund . Następnie aplikacja anuluje utworzony wcześniej obiekt typu
Runnable odpowiedzialny za automatyczne wstrzymywanie odtwarzania. Do anu-
lowania pracy obiektu służy metoda removeCallbacks komponentu obsługi klik-
nięć . Dotknięcie ekranu w celu odsłuchania utworu po raz wtóry prowadzi do
odtworzenia obiektu typu Runnable i ponownego zaplanowania jego działania na
podstawie czasu do upływu limitu.
W kodzie z listingu 11.10 pokazujemy, jak kontrolować pracę obiektu klasy
MediaPlayer na podstawie interakcji z użytkownikiem. Demonstrujemy też, jak
automatycznie zatrzymać odtwarzanie utworu po upływie limitu 15 sekund.
Aplikacje powinny automatycznie zarządzać obiektem klasy MediaPlayer na
podstawie cyklu życia aplikacji, która odtwarza utwór. Tak ma działać aktywność
SlideshowActivity z listingu 11.8. Może przypominasz sobie, że jest to aktyw-
ność wyświetlająca pokaz slajdów. Możliwe, że użytkownik wybrał długi utwór
do pokazu. Jeśli użytkownik z jakiejś przyczyny zamknie główną aktywność, apli-
kacja powinna zakończyć odtwarzanie utworu. Na listingu 11.11 pokazano, jak
uzyskać pożądany efekt.

Listing 11.11. Uzależnianie odtwarzania od cyklu życia aktywności

public class SlideshowActivity extends Activity {


private Song song;
private MediaPlayer player;
@Override
public void onCreate(Bundle savedInstanceState) {
// Pozostały kod interfejsu użytkownika pominięto.
0 TECHNIKA 59. Kontrolowanie dźwięku 461

song = getIntent().getParcelableExtra("selectedSong");
player = MediaPlayer.create(this, song.uri);
player.setOnCompletionListener(
new OnCompletionListener(){
@Override
public void onCompletion(MediaPlayer mp) {
// Kod pominięto.
}
});
}
@Override
public void onResume() {
super.onResume();
player.start();
}
@Override
public void onPause(){
super.onPause();
if (player != null && player.isPlaying()){
player.pause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (player != null && player.isPlaying()){
player.stop();
}
player.release();
}
// Pozostały kod pominięto.
}

W kodzie z listingu 11.11 przedstawiamy podstawowe mechanizmy do uzależnia-


nia odtwarzania multimediów od cyklu życia aktywności. W metodzie onCreate
aplikacja pobiera utwór i tworzy obiekt klasy MediaPlayer (jest on zmienną
składową aktywności). Tworzymy też odbiornik OnCompletionListener wywo-
ływany po zakończeniu odtwarzania utworu przez obiekt klasy MediaPlayer (wtedy
należy rozpocząć wyświetlanie filmu; w technice 60. dowiesz się, jak to zrobić).
Dalej, w metodzie onResume aktywności, uruchamiamy odtwarzanie utworu przez
obiekt klasy MediaPlayer . Aplikacja wywołuje tę metodę za każdym razem,
kiedy aktywność zaczyna pracę na pierwszym planie. Tak więc odtwarzanie
utworu rozpoczyna się przy pierwszym uruchomieniu aktywności, a także przy
jej wznowieniu lub ponownym uruchomieniu wstrzymanego utworu, jeśli dana
aktywność została wcześniej uruchomiona, ale potem inna aktywność (z innej
aplikacji) zajęła jej miejsce na pierwszym planie. W tym scenariuszu wywoływana
jest metoda onPause aktywności. To w tej metodzie należy wstrzymać pracę obiektu
klasy MediaPlayer . Dzięki temu obiekt klasy MediaPlayer nie kontynuuje
działania, kiedy aktywność nie znajduje się na pierwszym planie. W końcowym
etapie cyklu życia aktywności wywoływana jest jej metoda onDestroy. Należy
462 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

w niej zwolnić wszystkie zasoby powiązane z obiektem klasy MediaPlayer. Pamię-


taj, że obiekt ten wczytuje dane z otwartego strumienia i wysyła dźwięk do kanału
audio urządzenia. Używane są więc różne zasoby wejścia-wyjścia.
OMÓWIENIE
W tej technice pokazaliśmy najczęściej stosowane sposoby kontrolowania odtwa-
rzania dźwięku z wykorzystaniem klasy MediaPlayer. W pokazanych przykładach
wszystkie utwory są zapisane w lokalnych plikach dźwiękowych w urządzeniu.
Klasa MediaPlayer dobrze nadaje się też do odtwarzania nagrań z sieci i automa-
tycznie obsługuje buforowanie oraz zapewnia stabilność sygnału. Jednak dźwięki
można odtwarzać w Androidzie także na kilka innych sposobów.
Jeśli tworzysz grę, powinieneś przyjrzeć się klasie android.media.SoundPool.
Doskonale nadaje się ona do odtwarzania zestawu krótkich dźwięków, zwłasz-
cza jeśli krótki ma być czas między zdarzeniem a wygenerowaniem dźwięku.
W grach jest to potrzebne często, na przykład kiedy plusk ma być odtwarzany
dokładnie w momencie wrzucenia przedmiotu do wody. Android udostępnia też
inny związany z dźwiękiem zasób przydatny programistom gier. Jest to klasa
android.media.JetPlayer, która służy do tworzenia ścieżek dźwiękowych do gier
i pomaga synchronizować muzykę ze zdarzeniami z gry.
MediaPlayer, SoundPool i JetPlayer to stosunkowo wysokopoziomowe inter-
fejsy API dostępne w Androidzie. Jeśli potrzebujesz interfejsów API niższego
poziomu, przyjrzyj się klasie android.media.AudioTrack. Jeżeli stwierdzisz, że
któryś z opisanych wcześniej interfejsów API daje Ci zbyt mało możliwości,
prawdopodobnie powinieneś użyć właśnie tej klasy. Tylko przy jej zastosowaniu
możesz na przykład odtwarzać pliki dźwiękowe z kodowaniem niedostępnym w
systemie operacyjnym, ale obsługiwanym przez daną aplikację. Istnieje też spo-
sób na modyfikowanie dźwięku „w locie”. W Androidzie 2.3 wprowadzono nowy
mechanizm dodawania efektów do odtwarzanych utworów. Tym mechanizmem
jest pakiet android.media.audiofx. Obejmuje on kilka konfigurowalnych efektów
do ustawiania korekcji (poziomu basów i wysokich tonów), dodawania echa lub
imitacji przestrzenności dźwięku.
Liczba możliwości odtwarzania dźwięku w Androidzie może przytłaczać. Jed-
nak w zdecydowanej większości sytuacji można korzystać z klasy MediaPlayer.
Nawet jeśli chcesz dodać efekty dźwiękowe, możesz zastosować tę klasę w połą-
czeniu z pakietem audiofx. Android udostępnia podobne wysokopoziomowe
interfejsy API do wyświetlania filmów. Opisujemy je w następnej technice.

0 TECHNIKA 60. Wyświetlanie filmów

Jeszcze kilka lat temu nikt nie myślał o odtwarzaniu filmów na telefonach. Obec-
nie jest to następna standardowa funkcja oczekiwana przez użytkowników. Także
w tym obszarze Android się wyróżnia. Liczne urządzenia z tą platformą mają
duże ekrany i obsługują połączenia 4G do transferu danych. Jest to doskonała
0 TECHNIKA 60. Wyświetlanie filmów 463

kombinacja do oglądania filmów internetowych w formacie HD. Nie jest więc


zaskoczeniem, że Google ułatwia programistom odtwarzanie filmów — zwłaszcza
że niekwestionowany lider rynku filmów internetowych, YouTube, jest częścią
imperium Google’a. Zobaczmy, jak proste jest wyświetlanie filmów.
PROBLEM
Chcemy odtwarzać filmy w aplikacji. Film może znajdować się w urządzeniu lub
być pobierany strumieniowo przez sieć. Sytuacja jest podobna jak z odtwarza-
niem dźwięku. Także i tu kodowanie nagrania nie ma znaczenia. Jeśli urządzenie
ma odpowiedni kodek, chcemy, aby aplikacja odtwarzała film.
ROZWIĄZANIE
Odtwarzanie filmów w Androidzie jest ba-
nalnie proste. Odbywa się podobnie jak
odtwarzanie dźwięku, jednak pod niektó-
rymi względami jest jeszcze łatwiejsze,
ponieważ można przyjąć, że użytkownik
aktywnie ogląda film. Ludzie rzadko uru-
chamiają film w tle i korzystają w tym czasie
z innych aplikacji (choć jest to możliwe).
Nieczęsto też odtwarzanie filmu jest uza-
leżnione od zdarzeń zachodzących w apli-
kacji (przy czym także dodanie tego typu
powiązań jest możliwe). Na rysunku 11.16
pokazano, jak wygląda film odtwarzany
przez aplikację.
Do utworzenia odtwarzacza filmów po-
dobnego do tego z rysunku 11.6 używamy
klasy MediaPlayer. W kontekście listingu
11.11 wspomnieliśmy, że aplikacja ma uru-
chamiać wybrany przez użytkownika film po
Rysunek 11.6. Film i przyciski
zakończeniu odtwarzania muzyki powiąza- do sterowania odtwarzaniem
nej z pokazem slajdów. Zobaczmy, jak uzy-
skać taki efekt (listing 11.12).

Listing 11.12. Odtwarzanie filmów

public class SlideshowActivity extends Activity {


private MediaController videoPlayer;
private VideoView video;
private boolean playingSlides = true;

@Override
public void onCreate(Bundle savedInstanceState) {
// Pozostały kod pominięto.
player.setOnCompletionListener(new OnCompletionListener(){
464 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

@Override
public void onCompletion(MediaPlayer mp) {
FrameLayout frame =
(FrameLayout) findViewById(R.id.frame);
frame.removeAllViews();
playingSlides = false;
video =
new VideoView(SlideshowActivity.this);
video.setLayoutParams(new LayoutParams(
LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
frame.addView(video);
video.setVideoURI(
(Uri) getIntent().
getExtras().get("videoUri"));
videoPlayer = new MediaController(
SlideshowActivity.this);
videoPlayer.setMediaPlayer(video);
video.setMediaController(videoPlayer);
video.requestFocus();
video.start();
}
});
}
// Pozostały kod pominięto.
}

Kod z listingu 11.12 to kontynuacja kodu z listingu 11.11. Po zakończeniu odtwa-


rzania muzyki z pokazu slajdów aplikacja usuwa interfejs użytkownika , a następ-
nie tworzy widok android.widget.VideoView do wyświetlania filmów i dodaje
go do układu ekranu. Następnie ustawiamy źródło widoku VideoView przez
pobranie identyfikatora URI filmu wybranego przez użytkownika (na stronie
z rysunku 11.5). Identyfikator można uzyskać z intencji utworzonej przez daną
aktywność . Następnie tworzymy kontrolkę MediaController . Prowadzi to
do dodania do widoku VideoView kontrolek do sterowania odtwarzaniem filmu.
W ostatnim kroku aplikacja automatycznie rozpoczyna wyświetlanie nagrania .
Na rysunku 11.6 widać widok VideoView z aktywnymi przyciskami kontrolki
MediaController. Umożliwiają one wstrzymywanie i wznawianie odtwarzania,
przewijanie do tyłu, szybkie przewijanie do przodu i przeskakiwanie do wybra-
nych fragmentów. Przyciski te można utworzyć samodzielnie za pomocą inter-
fejsu API widoku VideoView (interfejs ten jest podobny do interfejsu API klasy
MediaPlayer), jednak zwykle łatwiej jest użyć wbudowanej kontrolki. Z oczywi-
stych powodów stosuje się ją w wielu aplikacjach na Android, dlatego większość
użytkowników wie, jak z niej korzystać.
OMÓWIENIE
Użycie klasy VideoView nie jest jedynym sposobem na odtwarzanie filmów
w Androidzie, podobnie jak przy odtwarzaniu dźwięku dostępne są różne techniki.
Możesz na przykład utworzyć widok android.view.SurfaceView. Widok SurfaceView
to płótno, po którym można rysować. Po utworzeniu takiego widoku można użyć
11.4. Rejestrowanie multimediów 465

jego metody getHolder, aby uzyskać dostęp do obiektu klasy SurfaceHolder.


Następnie można bezpośrednio użyć obiektu klasy MediaPlayer i przekazać do
jego metody setDisplay obiekt klasy SurfaceHolder. Pozwala to kontrolować
odtwarzanie filmu za pomocą obiektu klasy MediaPlayer i wyświetlać nagranie
w widoku SurfaceView. Nie można jednak bezpośrednio korzystać wtedy z kon-
trolki MediaController użytej na rysunku 11.12. Aby zastosować tę kontrolkę,
należy zaimplementować interfejs android.widget.MediaController.MediaPlayer
´Control, wykorzystując obiekt klasy MediaPlayer. Jednak widok VideoView auto-
matycznie wykonuje tę operację za programistę. Wykorzystuje na zapleczu obiekt
klasy MediaPlayer i deleguje do niego wiele wywołań. Jeszcze inna technika
polega na utworzeniu własnych kontrolek interfejsu użytkownika dla klas
SurfaceView i MediaPlayer.
Do tej pory przedstawiliśmy wiele różnych sposobów na udostępnianie mul-
timediów użytkownikom aplikacji. Ograniczyliśmy się jednak tylko do korzysta-
nia z multimediów. Urządzenia z Androidem potrafią też tworzyć pliki multime-
dialne. Użytkownicy często chcą samodzielnie robić zdjęcia i nagrywać filmy, aby
podzielić się nimi ze znajomymi lub zapisać je w urządzeniu. Przyjrzyjmy się
różnym sposobom rejestrowania multimediów w urządzeniach z Androidem.

11.4. Rejestrowanie multimediów


We wcześniejszej części rozdziału wspomnieliśmy, że multimedia służą użyt-
kownikom smartfonów do dzielenia się informacjami. Do tej pory koncentro-
waliśmy się na korzystaniu z multimediów — na wyświetlaniu zdjęć, słuchaniu
muzyki i oglądaniu filmów w aplikacjach na Android. W dalszej części rozdziału
omawiamy rejestrowanie multimediów — robienie zdjęć oraz nagrywanie dźwięku
i filmów. Może Cię zdziwić, jak łatwe jest wykonywanie tych operacji w Andro-
idzie. Można stosować też bardziej skomplikowane rozwiązania, jednak tu kon-
centrujemy się na prostych sposobach rejestrowania multimediów w aplikacjach.
W odpowiednich miejscach omawiamy też bardziej zaawansowane podejścia
i nietypowe sytuacje. Zaczynamy od zdecydowanie najprzydatniejszej techniki —
robienia zdjęć.

0 TECHNIKA 61. Robienie zdjęć

Ponieważ rozwijasz aplikacje na Android, prawdopodobnie używasz smartfonu


z tą platformą. Jeśli tak, zastanów się, z których aplikacji korzystasz najczęściej.
Prawdopodobnie umieściłeś je na stronie głównej telefonu. Ile z tych programów
umożliwia robienie zdjęć? U nas 7 z 17 aplikacji ze strony głównej pozwala w ten
lub inny sposób na rejestrowanie obrazu z aparatu, a w 6 spośród tych progra-
mów wykorzystano opisywaną tu technikę. Umożliwia ona łatwe rejestrowanie
zdjęć. Na tym właśnie koncentrujemy się w tym miejscu. Technika ta jest prosta
dla programistów i wygodna dla użytkowników.
466 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

PROBLEM
Chcesz umożliwić użytkownikom aplikacji robienie zdjęć za pomocą jednego
z aparatów urządzenia z Androidem. Zdjęcia te mają być następnie dostępne
w programie. Użytkownik powinien móc wykorzystać wszystkie możliwości urzą-
dzenia, takie jak dostęp do aparatu po stronie wyświetlacza (jeśli urządzenie
jest wyposażone w taki aparat) lub specjalne funkcje, jak rejestrowanie panoram,
lampa błyskowa itd.
ROZWIĄZANIE
Wyobraź sobie taką sytuację: aplikacja informuje urządzenie, że użytkownik chce
zrobić zdjęcie, po czym urządzenie wykonuje wszystkie potrzebne operacje. Czyż
to nie wspaniałe? Możesz uzyskać taki efekt. Rozwiązanie to jest stosowane
w większości aplikacji. Na listingu 11.13 pokazano, jak ono działa.

Listing 11.13. Robienie zdjęć w Androidzie

import static
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
public class TitlePageActivity extends Activity {
private Uri photoUri;
private final static int TAKE_PHOTO = 1;
private final static String PHOTO_URI = "photoUri";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.title_page);
Button takePhotoBtn = (Button) findViewById(R.id.takePhotoBtn);
takePhotoBtn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View button) {
Intent intent =
new Intent(
MediaStore.ACTION_IMAGE_CAPTURE);
photoUri = getContentResolver().insert(
EXTERNAL_CONTENT_URI, new ContentValues());
intent.putExtra(MediaStore.EXTRA_OUTPUT,
photoUri);
startActivityForResult(intent,TAKE_PHOTO);
}
});
// Kod interfejsu użytkownika pominięto.
if (savedInstanceState != null){
photoUri = (Uri) savedInstanceState.get(PHOTO_URI);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(PHOTO_URI, photoUri);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
0 TECHNIKA 61. Robienie zdjęć 467

if (resultCode != Activity.RESULT_OK
|| requestCode != TAKE_PHOTO){
return;
}
ImageView img =
(ImageView) findViewById(R.id.photoThumb);
try {
InputStream stream =
getContentResolver().openInputStream(photoUri);
Bitmap bmp = BitmapFactory.decodeStream(stream);
img.setImageBitmap(bmp);
} catch (FileNotFoundException e) {
Log.e("TitlePageActivity", "FileNotFound",e);
}
}
}

W kodzie na listingu 11.13 pokazano prostą aktywność wyświetlającą przycisk,


który użytkownik może dotknąć, aby zrobić zdjęcie. Dotknięcie przycisku pro-
wadzi do utworzenia intencji z żądaniem wykonania zdjęcia . Pomysł polega
na użyciu wbudowanej aplikacji Camera do zrobienia zdjęcia; następnie efekt tej
operacji wykorzystywany jest w aktywności programu, który rozwijamy. Trzeba
poinformować aplikację Camera, gdzie ma zapisać wykonane zdjęcie. Dlatego
najpierw używamy obiektu klasy ContentResolver do wstawienia nowego wiersza
do tablicy obrazów dostawcy MediaStore (2). W ten sposób uzyskujemy identy-
fikator URI nowego wiersza . Identyfikator można następnie przekazać do
aplikacji Camera w dodatkowych danych intencji. Potem wywołujemy metodę
startActivityForResult , do której przekazujemy utworzoną intencję oraz kod
żądania pozwalający później otrzymać efekt wykonania tego żądania (jest to przy-
datne, jeśli aplikacja może uruchamiać także inne aktywności).
MIEJSCA ZAPISYWANIA ZDJĘĆ I POŁĄCZENIA SKANERA
MEDIÓW. Na listingu 11.13 określamy identyfikator URI odpowiadający
zdjęciom zapisanym w dostawcy MediaStore. W aplikacji można też wska-
zać dowolne miejsce na zdjęcia. W tym celu należy użyć dodatkowych
danych intencji powiązanych ze stałą MediaStore.EXTRA_OUTPUT. Możliwe,
że także w tym scenariuszu zdjęcia mają pojawiać się w androidowej apli-
kacji Gallery. Jest to aplikacja przedstawiona na rysunku 11.5, gdzie użyto
jej do przeglądania listy filmów dostępnych w urządzeniu. Za jej pomocą
można też przeglądać zdjęcia. Jest ona wykorzystywana w wielu innych
programach, podobnie jak intencja w przypadku robienia zdjęć. Aby się
upewnić, że zdjęcie pojawi się w aplikacji Gallery (lub w odpowiedzi na
zapytanie o multimedia — zobacz listing 11.5), można zażądać jego „zeska-
nowania” przez skaner multimediów. Wymaga to utworzenia egzemplarza
klasy android.media.MediaScannerConnection, a także egzemplarza klasy
MediaScannerConnection.Client. Następnie należy oczekiwać zgłoszenia
468 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

gotowości od obiektu klasy MediaScannerConnection. Wtedy można zażądać


„zeskanowania” zapisanego zdjęcia. Więcej informacji znajdziesz w doku-
mentacji klasy MediaScannerConnection.
Ostatnią operacją w metodzie onCreate jest zapisanie stanu aktywności. Jest to
konieczne, ponieważ aplikacja uruchamia nową aktywność — z aplikacji Camera.
Jedyny stan zapisywany w głównej aktywności to identyfikator URI wskazujący
miejsce zapisania nowego zdjęcia w urządzeniu. Identyfikator ten umieszczany
jest w zmiennej egzemplarza photoUri. Aplikacja sprawdza, czy metoda saved
´InstanceState zwraca zapisany stan (typu Bundle). Jeśli tak, należy przypisać
go do zmiennej photoUri . Aby ta technika działała, trzeba się upewnić, że war-
tość zmiennej jest zapisywana przed przeniesieniem aktywności na tło. Można
to zrobić przez przesłonięcie metody onSaveInstanceState aktywności i przypi-
sywanie w niej zmiennej photoUri do zwracanego obiektu typu Bundle .
Kiedy użytkownik zrobił już (za pomocą aplikacji Camera) zdjęcie do aplikacji
MediaMogul, wywoływana jest metoda onActivityResult aktywności. Można
w niej przeprowadzić dowolne operacje na zdjęciu. Jeśli chcesz przesłać zdjęcie
na serwer, to w tej metodzie można utworzyć zadanie AsyncTask lub wywołać
potrzebną usługę. Aplikacja MediaMogul wyświetla zdjęcia w interfejsie użyt-
kownika. W tym celu pobiera widok ImageView zadeklarowany w używanym
w aktywności układzie w formacie XML. Następnie na podstawie wartości zmien-
nej photoUri otwiera strumień InputStream i przekazuje go do klasy Bitmap
´Factory, która tworzy bitmapę. Ta bitmapa to źródłowe dane do widoku
ImageView.

Inne sposoby dostępu do zdjęć


Jeśli chcesz dowiedzieć się, jak korzystać z intencji do robienia zdjęć (użyliśmy
jej na listingu 11.13), inne sposoby dostępu do nich znajdziesz w wywoływanej
zwrotnie metodzie onActivityResult. W niektórych urządzeniach można uzyskać
bezpośredni dostęp do informacji poprzez dodatkowe dane intencji przekazanej
do wspomnianej metody. Jednak technika ta w poszczególnych urządzeniach działa
w odmienny sposób.
Praca aplikacji Camera jest często zależna od urządzenia, a to właśnie ta aplikacja
tworzy intencję przekazywaną do metody onActivityResult. Technika z listingu 11.13
działa na wszystkich urządzeniach. W tym rozwiązaniu aplikacja Camera zapisuje
zdjęcie w dowolnym wskazanym katalogu, dlatego zawsze można wczytać z tego
miejsca fotografię wykonaną przez użytkownika.

OMÓWIENIE
Wspomnieliśmy, że programiści wielu aplikacji używają tej techniki (często
w połączeniu z innymi rozwiązaniami) do umożliwiania użytkownikom robienia
zdjęć. Użytkownicy mogą na przykład wybrać, czy chcą zachować dane zdjęcie,
czy zrobić nowe. Ta druga opcja wymaga zastosowania aktywności ACTION_GET_
´CONTENT w celu uruchomienia aktywności aplikacji Gallery. Podobnie działa
0 TECHNIKA 61. Robienie zdjęć 469

kod z listingu 11.6. Jedyna różnica polega na tym, że należy wybrać odpowiedni
typ MIME (image/*), aby aplikacja Gallery wyświetlała tylko zdjęcia, a nie filmy.

Uwaga na błędy!
Wykonywanie i zapisywanie zdjęć jest w Androidzie przeprowadzane przez inne
aplikacje. Intencje zapewniają luźny kontrakt między danym programem a wbu-
dowanymi aplikacjami Camera i Gallery. Niestety, producenci realizują ten kontrakt
w niespójny sposób (zwłaszcza w starszych urządzeniach). Dlatego w niektórych
telefonach poprawna interakcja z aparatem wymaga stosowania obejść i sztuczek.

Możesz się zastanawiać, dlaczego programiści tak często korzystają z wbudowanej


aplikacji Camera (tak jak zrobiliśmy na listingu 11.13), zamiast tworzyć własne
programy do obsługi aparatu. Można łatwo rejestrować dane z aparatu i wyświetlać
je bezpośrednio w programie. Pozwala to uniknąć kosztów uruchamiania odrębnej
aktywności (która działa we własnym procesie) i zapisywania stanu obecnej aktyw-
ności danej aplikacji, co było konieczne na listingu 11.13. Jednak samodzielna
obsługa aparatu ma też poważne wady. Wspomnieliśmy już, że aplikacja Camera
jest dostosowana do urządzenia, na którym działa. Jest to konieczne, aby aplikacja
mogła w pełni wykorzystać możliwości danego urządzenia. Kiedy na przykład
w urządzeniach po raz pierwszy pojawiły się aparaty po stronie wyświetlacza, pakiet
SDK Androida nie umożliwiał bezpośredniego dostępu do nich. Jednak aplikacja
Camera obejmowała kontrolkę pozwalającą użytkownikowi na łatwe przełączanie
się między aparatami. Jeśli zatem programista chciał utworzyć własne kontrolki do
obsługi aparatu, musiał albo wykorzystać kontrolki z urządzenia (oparte na inter-
fejsach API producentów, które nie zawsze były publiczne), albo uniemożliwić
używanie aparatu po stronie wyświetlacza. Liczne wersje aplikacji Camera
obejmują też inne kontrolki przeznaczone dla danego sprzętu, na przykład do ob-
sługi lampy błyskowej lub zaawansowanych funkcji w rodzaju tworzenia panoram.
Przekazanie sterowania aplikacji Camera sprawia, że użytkownik ma dostęp do
wszystkich funkcji, a programista nie musi samodzielnie pisać kodu do ich obsługi.
Ponadto z uwagi na to, że aplikacja Camera jest używana w tak wielu programach,
użytkownicy wiedzą, jak się nią posługiwać.
Mimo opisanych zastrzeżeń czasem przydatny jest bezpośredni dostęp do
sprzętu. Można zastosować klasę android.view.Surface, aby na bieżąco wyświetlać
podgląd obrazu z aparatu. Rozwiązanie to jest najprzydatniejsze, kiedy użytkownik
chce nie tylko wykonać zdjęcie, ale i przetworzyć zarejestrowany obraz. Jest to
potrzebne na przykład w czytnikach kodów QR i kodów kreskowych. Wcześniej
wspomniano, że sześć z siedmiu programów na stronie głównej naszego smartfonu
wykorzystuje do wykonywania zdjęć aplikację Camera. Jedynym wyjątkiem
jest aplikacja do skanowania kodów kreskowych. Inne aplikacje, w których przy-
datny jest bezpośredni dostęp do aparatu, to programy do wyświetlania wzboga-
conej rzeczywistości. Programy tego rodzaju nie służą do robienia zdjęć.
Zobaczmy teraz, jak rejestrować dźwięk i filmy.
470 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

0 TECHNIKA 62. Rejestrowanie dźwięku i filmów

Rejestrowanie filmów pod wieloma względami przypomina robienie zdjęć.


W kodzie z listingu 11.13 możesz zmienić akcję intencji z MediaStore.ACTION_
´IMAGE_CAPTURE na MediaStore.ACTION_VIDEO_CAPTURE. W ten sposób można uru-
chomić aplikację Camera w trybie nagrywania filmów. Aplikacja zapisuje wtedy
nagranie w miejscu podanym w dodatkowych danych EXTRA_OUTPUT. Można okre-
ślić też inne dodatkowe dane, EXTRA_VIDEO_QUALITY, i dzięki temu kontrolować
jakość nagrania (0 to niska jakość, a 1 — wysoka). Technika ta, podobnie jak przy
rejestrowaniu zdjęć, w wielu sytuacjach jest odpowiednia. Po co więc stosować
inne rozwiązania? Nagrywanie filmów i dźwięku jest jednak bardziej skompli-
kowane od robienia zdjęć. Oprócz rejestrowania warto też pamiętać o innych
zastosowaniach, na przykład o strumieniowej transmisji dźwięku lub obrazu.
Ponadto wiele operacji potrzebnych przy ręcznym nagrywaniu filmów jest takich
samych jak przy ręcznym robieniu zdjęć, dlatego łatwo można zmodyfikować
niniejszą technikę, aby osadzić w aplikacji mechanizm do wykonywania fotografii.
Zobaczmy, jak przebiega rejestrowanie dźwięku i filmów.
PROBLEM
Chcemy, aby użytkownik aplikacji mógł za pomocą urządzenia z Androidem
rejestrować filmy i dźwięki. Następnie możliwe ma być odtwarzanie, przesyłanie
i przetwarzanie nagrań w programie. Mechanizm rejestrowania filmów ma być
osadzony bezpośrednio w aplikacji. Nie chcemy używać aplikacji Camera.
ROZWIĄZANIE
W technice 61. pokazaliśmy, jak za pośrednictwem systemowej intencji używać
aplikacji Camera. Tu za wyświetlanie i rejestrowanie informacji w całości odpo-
wiada aplikacja.
Bezpośrednie rejestrowanie filmów w ten sposób przedstawiony wymaga
wykonania kilku prostych kroków. Najpierw trzeba utworzyć widok SurfaceView,
który można wykorzystać do podglądu obrazu z aparatu. Następnie należy użyć
klasy android.media.MediaRecorder do rejestrowania filmów i dźwięku. Nagrywa-
nie filmów nie powoduje automatycznego zapisu dźwięku. Trzeba zadbać o oba te
aspekty. To dlatego omawiamy rejestrowanie dźwięku i filmów w jednej technice.
Nagrywanie dźwięku możesz traktować jak specjalny przypadek nagrywania fil-
mów. Potrzebne operacje nie są skomplikowane, jednak każda wymaga uwzględ-
nienia pewnych kwestii. Przyjrzyjmy się szczegółom aktywności używanej do
rejestrowania dźwięku i filmów, przedstawionej na listingu 11.14.

Listing 11.14. Aktywność do nagrywania dźwięku i filmów

public class VideoRecorderActivity extends Activity {


private static final String LOG_TAG = "VideoRecorderActivity";
private SurfaceHolder holder;
private Camera camera;
private MediaRecorder mediaRecorder;
0 TECHNIKA 62. Rejestrowanie dźwięku i filmów 471

private File tempFile;


private CameraPreview preview;
private boolean isRecording = false;
private final int maxDurationInMs = 20000;
private final long maxFileSizeInBytes = 500000;
private final int videoFramesPerSecond = 20;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
preview = new SurfaceView(this);
holder = preview.getHolder();
holder.addCallback(cameraman);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setContentView(preview);
tempFile = new File(getCacheDir(), "temp.mov");
if (tempFile.length() > 0){
tempFile.delete();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu){
MenuInflater inflater = new MenuInflater(this);
inflater.inflate(R.menu.recorder_menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item){
if (item.getItemId() == R.id.menu_rec_item){
startRecording();
} else if (item.getItemId() == R.id.menu_stop_item){
stopRecording();
}
return true;
}
// Pozostały kod pominięto.
}

Aktywność ta najpierw usuwa pasek tytułu z górnej części ekranu . Potrzebny


jest cały ekran, ponieważ aparat zwykle udostępnia tylko kilka trybów podglądu
obrazu. Następnie aktywność tworzy służący do wyświetlania podglądu widok
SurfaceView i pobiera referencję do obiektu klasy SurfaceHolder z tego widoku.
Za pomocą tej referencji można korzystać z obiektu klasy Surface z widoku.
Dalej znajduje się wywołanie zwrotne, które oczekuje na zdarzenia cyklu życia
obiektu klasy Surface. Zdarzenia te pozwalają ustalić, kiedy należy rozpocząć
wyświetlanie filmu nagrywanego przy użyciu aparatu. Kod wywołania zwrotnego
znajdziesz na listingu 11.15. Ostatnią operacją w metodzie onCreate aktywności
jest utworzenie pliku tymczasowego , w którym aplikacja zapisuje zarejestrowa-
ny film. Następnie aktywność tworzy kilka opcji menu pozwalających użytkow-
nikowi kontrolować rejestrowanie dźwięku i filmów . Przyjrzyjmy się zastosowa-
nej wcześniej zmiennej egzemplarza cameramen. Jej kod pokazano na listingu 11.15.
472 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

Listing 11.15. Kontrolowanie podglądu filmu w cyklu życia obiektu klasy Surface

private SurfaceHolder.Callback cameraman = new SurfaceHolder.Callback(){


@Override
public void surfaceCreated(SurfaceHolder holder) {
camera = Camera.open();
try {
camera.setPreviewDisplay(holder);
} catch (IOException e) {
camera.release();
Log.e(LOG_TAG, "Wyjątek przy konfigurowaniu " +
"podglądu",e);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width,int height) {
Parameters params = camera.getParameters();
List<Size> sizes =
params.getSupportedPreviewSizes();
Size optimalSize = getOptimalPreviewSize(sizes, width, height);
params.setPreviewSize(optimalSize.width, optimalSize.height);
camera.setParameters(params);
camera.startPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
camera.stopPreview();
camera.release();
}
};

Zmienna egzemplarza cameramen to anonimowa implementacja interfejsu android.


´view.SurfaceHolder.Callback. W interfejsie tym zdefiniowane są trzy metody
obsługi cyklu życia: surfaceCreated, surfaceChanged i surfaceDestroyed. Po utwo-
rzeniu obiektu klasy Surface wywoływana jest metoda surfaceCreated i aplikacja
uzyskuje referencję do aparatu . Jeśli urządzenie ma aparaty z przodu i z tyłu,
wywołanie zwróci referencję do aparatu z tylnej strony. Po uzyskaniu referencji
można wykorzystać obiekt klasy Surface z widoku SurfaceView do wyświetlenia
podglądu filmu. W tym celu należy przekazać do obiektu reprezentującego aparat
obiekt klasy SurfaceHolder powiązany ze wspomnianym obiektem klasy Surface .
Po dopasowaniu wielkości obiektu klasy Surface do wymiarów ekranu wywo-
ływana jest metoda surfaceChanged. W niej należy ustawić rozmiar podglądu
obrazu otrzymywanego z aparatu. Wielkość podglądu zależy od wymiarów obiektu
klasy Surface, w którym podgląd jest wyświetlany, jednak każdy aparat obsłu-
guje tylko określone wymiary . Chcemy więc wyznaczyć najlepszą wielkość na
podstawie wymiarów obiektu klasy Surface. Umożliwia to statyczna metoda get
´OptimalPreviewSize (nie przedstawiamy jej w tym miejscu). Metodę tę zapo-
życzyliśmy z przykładowego kodu z pakietu SDK Androida (powinna się ona
znaleźć w tym pakiecie). Po przeprowadzeniu obliczeń można ustawić wielkość
0 TECHNIKA 62. Rejestrowanie dźwięku i filmów 473

podglądu i nakazać aparatowi rozpoczęcie przesyłania tego podglądu do


obiektu klasy Surface. Użytkownik może wtedy zobaczyć obraz.
Ostatnim zadaniem jest wykonanie operacji porządkowych w momencie prze-
noszenia aktywności na tło. Wtedy aplikacja usuwa obiekt klasy Surface i wywo-
łuje metodę surfaceDestroyed. Należy zatrzymać podgląd i — co najważniejsze —
zwolnić zasoby aparatu . Jest to niezwykle istotne. Bez tego kroku aplikacja
nadal będzie blokować aparat, przez co inne programy nie będą mogły z niego
korzystać.
Cały kod z listingu 11.15 służy do wysyłania podglądu filmu do widoku Surface
´View dostępnego dla użytkownika. To krok pierwszy mechanizmu rejestrowania
dźwięku i filmów . A co z krokiem drugim ? Z listingu 11.14 można wywnio-
skować, że do kontrolowania procesu rejestrowania służy menu aktywności.
Jak widać na listingu 11.14, dotknięcie opcji menu prowadzi do wywołania
metody startRecording lub stopRecording. Metody te służą do obsługi rejestro-
wania filmów . Na listingu 11.16 przedstawiono pierwszą z tych metod —
startRecording.

Listing 11.16. Konfigurowanie rejestrowania dźwięku i filmów

private void startRecording(){


if (isRecording){
return;
}
isRecording = true;
camera.unlock();
mediaRecorder = new MediaRecorder();
mediaRecorder.setCamera(camera);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
mediaRecorder.setMaxDuration(maxDurationInMs);
Log.d(LOG_TAG, "Użyto pliku tempFile=" + tempFile.getPath());
mediaRecorder.setOutputFile(tempFile.getPath());
mediaRecorder.setVideoFrameRate(videoFramesPerSecond);
mediaRecorder.setVideoSize(preview.getWidth(), preview.getHeight());
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
mediaRecorder.setPreviewDisplay(holder.getSurface());
mediaRecorder.setMaxFileSize(maxFileSizeInBytes);
try {
mediaRecorder.prepare();
mediaRecorder.start();
Log.d(LOG_TAG, "Rozpoczęto nagrywanie");
} catch (IllegalStateException e) {
Log.e(LOG_TAG, "Wyjątek związany ze stanem w trakcie nagrywania", e);
} catch (IOException e) {
Log.e(LOG_TAG, "Wyjątek wejścia-wyjścia w trakcie nagrywania", e);
}
}
474 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

Metoda ta obejmuje kilka prostych operacji potrzebnych do skonfigurowania pro-


cesu rejestrowania dźwięku i filmów. Najpierw trzeba odblokować obiekt klasy
Camera , co pozwala innym obiektom uzyskać dostęp do niego. Następnie należy
utworzyć nowy egzemplarz klasy MediaRecorder . Jest to najważniejsza w Andro-
idzie klasa do nagrywania dźwięku i filmów. Następnie można przekazać do
wspomnianego egzemplarza referencję do obiektu klasy Camera. Potem kilka
prostych metod konfiguruje źródło dźwięku i filmów, format wyjściowego pliku,
liczbę klatek filmu, wymiary obrazu, a także typ kodowania. Na listingu 11.16
używane są głównie wartości domyślne. Dostępne opcje znajdziesz w dokumen-
tacji interfejsu API. Jednym z najważniejszych etapów konfiguracyjnych jest usta-
wienie pliku wyjściowego dla obiektu klasy MediaRecorder . Jest to plik utworzony
wcześniej na listingu 11.14. Teraz można rozpocząć rejestrowanie dźwięku i fil-
mów, dlatego aplikacja wywołuje metody przygotowujące i rozpoczynające proces
nagrywania . Po jego zakończeniu użytkownik może dotknąć przycisk Wstrzy-
maj nagrywanie z menu. Poniżej pokazano wywoływaną w reakcji na to metodę
stopRecording.

Listing 11.17. Kończenie nagrywania i operacje porządkowe

private void stopRecording(){


if (!isRecording){
return;
}
isRecording = false;
mediaRecorder.stop();
try {
camera.reconnect();
} catch (IOException e) {
Log.e(LOG_TAG, "Wyjątek przy próbie ponownego połączenia się z aparatem", e);
}
camera.lock();
}

Kod z listingu 11.17 jest prosty. Najpierw metoda nakazuje obiektowi klasy Media
´Recorder wstrzymanie nagrywania dźwięku i filmu . Prowadzi to do zamknię-
cia pliku wyjściowego, w którym obiekt zapisuje dane. Tu jest to plik tymcza-
sowy utworzony na listingu 11.14. Przedstawiona metoda jest też dobrym miej-
scem na uruchomienie przesyłania lub „skanowania” pliku. Operacje te można
wykonać w innym wątku, ponieważ kod z listingu 11.17 działa w głównym wątku
interfejsu użytkownika. Dalej metoda ponownie pobiera referencję do aparatu,
używając metody reconnect . Pozwala to znowu wyświetlić podgląd lub roz-
począć nowe nagranie. W ostatnim kroku metoda ponownie blokuje aparat, aby
żaden inny obiekt nie miał do niego dostępu . Teraz film jest gotowy. Użytkow-
nik może rozpocząć nowe nagranie lub wybrać inne funkcje aplikacji.
11.5. Podsumowanie 475

OMÓWIENIE
Na początku opisu tej techniki wspomnieliśmy, że większość jej kodu można
wykorzystać także do ręcznego robienia zdjęć. Jednak wykonywanie fotografii
nie wymaga używania klasy MediaRecorder. Zamiast tego można wywołać metodę
takePicture obiektu klasy Camera. Do metody takePicture można przekazać kilka
obiektów wywołań zwrotnych, aby uzyskać dostęp do zdjęć zwracanych przez
aparat. Korzystanie z tego rodzaju interfejsów API jest proste. Trudniejsze jest
opracowanie dobrego interfejsu użytkownika nałożonego na podgląd obrazu z apa-
ratu. Interfejs ten powinien dawać dostęp do wszystkich dostępnych w urządze-
niu opcji robienia zdjęć.
Wspomnieliśmy też, że nagrywanie dźwięku to specjalny przypadek rejestrowa-
nia filmów, dlatego oba aspekty omawiamy w jednym miejscu. Na listingach 11.16
i 11.17 przedstawiono cały kod potrzebny do nagrywania samego dźwięku. Trzeba
ustawić źródło dźwięku, plik wyjściowy, maksymalną wielkość pliku i kodowa-
nie. Można pominąć ustawienia właściwe dla filmu, na przykład liczbę klatek
i wyświetlanie podglądu.

11.5. Podsumowanie
Teraz jesteś już ekspertem od multimediów! Rozdział ten obejmuje wiele infor-
macji. Oto ważne zagadnienia, które warto zapamiętać:
Q Jeśli aplikacja ma przyjmować, że dana funkcja jest dostępna, funkcję
tę trzeba wymienić w manifeście aplikacji. Jeżeli dana funkcja jest
opcjonalna, trzeba sprawdzać jej dostępność.
Q Można dołączyć do aplikacji pliki multimedialne, do których dostęp
ma tylko dany program.
Q Dostępne są współużytkowane katalogi z karty SD, jednak multimedia
można przechowywać w dowolnym miejscu. Dostawca treści z klasy
MediaStore pozwala znaleźć multimedia i odfiltrować materiały
nieodpowiedniego rodzaju.
Q Jeśli chcesz odtwarzać dźwięk lub filmy, powiąż odtwarzanie z cyklem
życia nadrzędnej aktywności.
Q W obszarze rejestrowania zdjęć i filmów łatwiej dla programisty oraz lepiej
dla użytkownika jest stosować system intencji Androida i komunikować
się za jego pomocą z wyspecjalizowaną aplikacją Camera.
Multimedia to jeden z obszarów, w których zachodzą szybkie zmiany. Na przykład
w kodzie prezentowanym w tym rozdziale używamy interfejsów API wprowadzo-
nych w Androidzie 2.2 (zapewniają dostęp do publicznych katalogów z muzyką
i zdjęciami) oraz 2.3 (zapewniają dostęp do aparatu po stronie wyświetlacza).
Android udostępnia wysokopoziomowe interfejsy API, które ułatwiają korzysta-
nie z multimediów różnego rodzaju. Zauważ, że nigdzie nie musieliśmy określać
476 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów

(lub — co jeszcze gorsze — wczytywać) kodeków do odtwarzania dźwięku lub


filmów. Nie trzeba być ekspertem, aby przetwarzać zdjęcia i dodawać animacje.
Potrzebne mechanizmy są łatwo dostępne dla programistów.
Korzystanie z mikrofonów i aparatów w Androidzie jest proste. Dostępne są
też niskopoziomowe interfejsy API, co otwiera wiele ciekawych możliwości.
W sklepie Android Market znajdziesz aplikacje, których twórcy w pomysłowy
sposób wykorzystali mikrofon i (lub) aparat. Są to programy związane z wideokon-
ferencjami, zakupami, modą, zdrowiem, finansami osobistymi, a nawet aplikacje
wyświetlające zabawne, gadające zwierzaki. Niezależnie od tego, czy potrzebujesz
złożonego, czy prostego dostępu do danych multimedialnych, Android zaspokoi
Twoje wymagania. Nie obawiaj się multimediów. W niektórych sytuacjach apli-
kacja ma wyświetlać dynamicznie zmieniające się grafiki zamiast statycznych
zdjęć. Na szczęście w Androidzie dostępne są biblioteki graficzne, które wykonują
potrzebne zadania. Biblioteki te omawiamy w następnym rozdziale.
Grafika dwu-
i trójwymiarowa

W tym rozdziale
Q Manipulowanie grafiką „w locie”
Q Kształty dwuwymiarowe
Q Tworzenie grafiki trójwymiarowej i ruch

Ćwicz się w tym, co już znasz. Pomoże ci to zrozumieć to, czego jeszcze
nie umiesz.
Rembrandt van Rijn
Omówiliśmy już wiele zagadnień związanych z programowaniem aplikacji na
Android. Tworzyliśmy interfejsy użytkownika za pomocą kontrolek i widoków
z frameworku, a także składaliśmy aplikacje z aktywności, usług i innych mecha-
nizmów. Rozwijanie aplikacji w rodzaju gier, w których istotną rolę odgrywa gra-
fika, wymaga pełnej kontroli nad ekranem, co pozwala tworzyć elementy wizualne.
W tym obszarze przydatne są androidowe biblioteki do obsługi grafiki dwu-
i trójwymiarowej.
Biblioteka do obsługi grafiki dwuwymiarowej jest oparta na Skii (to biblioteka
o otwartym dostępie do kodu źródłowego) i przydaje się w aplikacjach, w których
potrzebne są proste dwuwymiarowe elementy wizualne i rozmaite efekty. Do
wykonywania bardziej skomplikowanych operacji służy biblioteka OpenGL ES.
Umożliwia ona tworzenie złożonej grafiki dwu- i trójwymiarowej oraz wykorzy-
stanie akceleracji sprzętowej (jeśli jest dostępna). W tym rozdziale używamy obu
wymienionych bibliotek.

477
478 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Zaczynamy od prostych linii rysowanych w dwóch wymiarach, a potem prze-


chodzimy do okręgów, prostokątów, tekstu, rysunków, efektów i innych zagadnień.
Po umieszczeniu na dwuwymiarowym płótnie kilku przykładów pokazujemy, jak
używać biblioteki OpenGL ES. Wyjaśniamy, czym jest ta biblioteka, jakie jej
wersje są dostępne i jak z niej korzystać. Przy okazji tłumaczymy, w jaki sposób
biblioteka OpenGL ES przetwarza perspektywę i sceny trójwymiarowe.
Jest to tylko krótki przegląd i nie próbujemy opisywać w tym rozdziale
wszystkich aspektów obu rozbudowanych bibliotek. Chcemy jednak pomóc Ci
zacząć pracę i pokazać, jak działają oba narzędzia. Dzięki temu poznasz pod-
stawowe możliwości.

12.1. Rysowanie z wykorzystaniem bibliotek


do obsługi grafiki dwuwymiarowej
W aplikacjach na Android na pierwszym planie może działać tylko jedna aktyw-
ność. Obejmuje ona widok SurfaceView, w którym umieszczane są elementy inter-
fejsu użytkownika. Widok ten to okno, z którym użytkownik wchodzi w interak-
cje przez dotknięcia (jeśli urządzenie obsługuje ten mechanizm) i rysowanie na
powierzchni ekranu. Obszar służący do rysowania to płótno (obiekt klasy Canvas).
Na płótnie można narysować dowolny dwuwymiarowy kształt. Umożliwia ono
stosowanie kolorów, rysowanie, dodawanie tekstu, tworzenie figur geometrycz-
nych i rysunków, a także stosowanie różnych filtrów i transformacji. Wszystkie
te mechanizmy omawiamy, przedstawiając na przykładzie płótna możliwości
biblioteki do obsługi grafiki dwuwymiarowej.

12.1.1. Podstawowe informacje o klasie Canvas


Klasa Canvas udostępnia zestaw metod, które pozwalają na wykonanie niemal
wszystkich operacji możliwych w dwuwymiarowej przestrzeni. Aby przedstawić
klasę Canvas, bierzemy ołówki i pędzle i zaczynamy rysować w nowym projekcie —
CanvasDemo.
POBIERZ PROJEKT CANVASDEMO. Kod źródłowy projektu i pakiet
APK do uruchamiania aplikacji znajdziesz w witrynie z kodem do książki
Android w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skon-
centrować się na konkretnych zagadnieniach, zalecamy pobranie komplet-
nego kodu źródłowego i śledzenie go w Eclipse (lub innym środowisku
IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/UbW4, plik APK: http://mng.bz/CnyQ.
Początkowy ekran projektu CanvasDemo odpowiada głównej aktywności, która
wyświetla kilka zwykłych przycisków prowadzących do innych przykładów i jeden
wymyślny, niestandardowy przycisk, oparty na utworzonej przez nas klasie.
12.1. Rysowanie z wykorzystaniem bibliotek do obsługi grafiki dwuwymiarowej 479

Nie omawiamy tu banalnego kodu głównego ekranu (ekran przedstawiono na


rysunku 12.1).
Główna aktywność prowadzi do innych aktywności, w których używamy róż-
nych metod rysowania dostępnych w klasie Canvas. Zaczynamy od aktywności
obejmującej jeden niestandardowy widok, w którym cały obszar jest wypełniany
losowym kolorem (rysunek 12.2).

Rysunek 12.1. Główny ekran Rysunek 12.2. Niestandardowy widok


aktywności CanvasDemo wykorzystuje klasę Canvas do wypełnienia
całego obszaru losowym kolorem

Poniżej na listingu 12.1 znajdziesz kod niestandardowego widoku CanvasView


(który wyświetla losowy kolor) i powiązanej z nim aktywności.

Listing 12.1. Wyświetlanie losowego koloru za pomocą niestandardowego widoku

public class Canvas2DRandomColorActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CanvasView(this));
}

class CanvasView extends View {


Random random = new Random();

public CanvasView(Context context) {


super(context);
}

@Override
480 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

protected void onDraw(Canvas canvas) {


canvas.drawRGB(random.nextInt(256),
random.nextInt(256), random.nextInt(256));
}
}
}

Pierwszy przykład wykorzystania płótna jest krótki i prosty. Podstawowa aktyw-


ność jako widok zawartości obszaru na rysunki ustawia niestandardowy widok .
Dalej znajduje się implementacja niestandardowej klasy widoku (CanvasView) .
Klasa ta obejmuje zmienną Random służącą do generowania liczb losowych. Dalej
znajduje się definicja konstruktora i nowa wersja przesłanianej wymaganej metody
onDraw . W metodzie onDraw aplikacja przyjmuje przekazany do niej obiekt
klasy Canvas i wypełnia go za pomocą koloru odpowiadającego trzem losowym
wartościom składowych RGB. Powoduje to wypełnienie całego ekranu przypad-
kową barwą.
KOLOR PŁÓTNA. W Androidzie dla płótna używany jest schemat kolo-
rów ARGB (kanały alfa, czerwony, zielony i niebieski). Kolor reprezento-
wany jest jako jedna liczba typu int. Każdy komponent ma wartość od
0 do 255 włącznie. Dla kolorów 0 oznacza brak danej składowej, a 255 —
jej pełną intensywność. Dla składowej alfa 0 to przezroczystość, a 255 —
nieprzezroczystość. Aby ułatwić sobie zapamiętanie koloru, można użyć
zapisu szesnastkowego. Nieprzezroczysty zielony ma składowe ARGB
255, 0, 255, 0. Po przekształceniu ich na zapis szesnastkowy otrzymujemy:
FF, 00, FF, 00. Aby użyć tego zapisu w Androidzie, należy podać wartości
jedna po drugiej: int color = 0xFF00FF00.
Już w tym prostym przykładzie korzystania z płótna pokazaliśmy, jak rysować
po ekranie ręcznie. Jednak pojedynczy kolor nie jest specjalnie przydatny. Dalej
opisujemy kształty i tekst, ale najpierw warto poruszyć ważny aspekt radzenia
sobie z ograniczoną wielkością ekranu w Androidzie — przechodzenie do trybu
pełnoekranowego.

0 TECHNIKA 63. Przechodzenie do trybu pełnoekranowego

W pierwszym przykładzie dużą część ekranu zajmował nagłówek okna i andro-


idowy pasek stanu. Jeśli chcesz rozwijać zaawansowane gry lub inne aplikacje,
w których grafika odgrywa istotną rolę (na przykład odtwarzacze filmów), powi-
nieneś stosować tryb pełnoekranowy.
PROBLEM
Chcemy wykorzystać całą powierzchnię ekranu.
0 TECHNIKA 64. Rysowanie prostych kształtów 481

ROZWIĄZANIE
Aby zająć całą powierzchnię ekranu, trzeba zmienić wygląd okna przed dodaniem
widoku do aktywności. Musi to mieć miejsce przed ustawieniem widoku zawar-
tości, należy więc wykorzystać metodę onCreate aktywności:
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

Te dwa wywołania zgłaszane przed ustawieniem widoku zawartości są informacją


dla androidowego menedżera okna, że ma włączyć tryb pełnoekranowy. Urucho-
mienie kodu prowadzi do tych samych efektów co wcześniej, jednak tym razem
aplikacja nie wyświetla nagłówka ani paska systemowego.
OMÓWIENIE
Jest to podstawowe rozwiązanie często występującego problemu. Możliwe, że
znasz już ten mechanizm, jednak warto przedstawić go osobom, które jeszcze się
z nim nie zetknęły. Najważniejsze jest, aby nie nadużywać tej techniki. Należy
stosować ją tylko przy rozwijaniu niestandardowych aplikacji pełnoekranowych.
Przejście do trybu pełnoekranowego może irytować użytkownika. Pasek stanu
służy do szybkiego ustalania siły połączenia, sprawdzania godziny, reagowania na
powiadomienia itd. Jest elementem przydatnym wielu użytkownikom. Ukrywać
go należy tylko wtedy, jeśli aplikacja naprawdę tego wymaga. Trzeba zachować
przy tym ostrożność.
Po omówieniu przechodzenia do trybu pełnoekranowego pora wrócić
do płótna i sprawdzić, jak korzystać z niego do rysowania prostych kształtów
i wyświetlania tekstu.

0 TECHNIKA 64. Rysowanie prostych kształtów

W ramach demonstracji rysowania kształtów i linii próbujemy zapełnić płótno


jak Wassily Kandinsky, choć bez jego geniuszu. Zamiast dobierać odpowiednie
formy i kolory, posługujemy się losowymi wartościami. Niektóre z wygenerowa-
nych rysunków mogą wydać Ci się atrakcyjne (rysunek 12.3), choć zależy to od
Twojego gustu.
Aby utworzyć kolorowe linie, prostokąty i kółka, wykonujemy nowe operacje
na płótnie i posługujemy się klasą Paint — to ważna klasa umożliwiająca defi-
niowanie ustawień dla płótna.
PROBLEM
Chcemy rysować na ekranie linie i kształty.
ROZWIĄZANIE
Płótno, jak wskazuje nazwa, to powierzchnia do rysowania. Można na nim umiesz-
czać punkty, linie, kółka, łuki, prostokąty itd. Płótno obsługuje piksele i subpiksele,
482 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

jednak tu dla uproszczenia przyjmujemy, że


punkt to piksel. Pozwala to używać liczb cał-
kowitych w metodach do rysowania elementów.
Klasa Canvas korzysta z obiektu klasy Paint
do rysowania prostych elementów. Przed rozpo-
częciem rysowania trzeba utworzyć taki obiekt.
Możesz traktować go jak pędzel, który należy
przygotować przed przystąpieniem do malowa-
nia. „Pędzel” w aplikacji ma określoną grubość,
styl, kolor i inne cechy. Przedstawiona na lis-
tingu 12.2 druga aktywność CanvasDemo modyfi-
kuje niestandardowy widok. Pozwala to zobaczyć,
w jaki sposób klasy Canvas i Paint współdziałają
w procesie rysowania prostych kształtów na
płótnie.
Rysunek 12.3. Rysowanie
Listing 12.2. Rysowanie losowych linii, wielobarwnych linii i kształtów
kółek i prostokątów na płótnie na płótnie z wykorzystaniem
public class Canvas2DRandomShapesActivity extends klasy Paint
Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(new CanvasView(this));
}

class CanvasView extends View {


Paint paint;
Random random = new Random();

public CanvasView(Context context) {


super(context);
}
protected void onDraw(Canvas canvas) {
canvas.drawRGB(0, 0, 0);
for (int i = 0; i < 10; i++) {
paint = new Paint();
paint.setARGB(255, random.nextInt(256),
random.nextInt(256), random.nextInt(256));
canvas.drawLine(random.nextInt(canvas.getWidth()),
random.nextInt(canvas.getHeight()),
random.nextInt(canvas.getWidth()),
random.nextInt(canvas.getHeight()), paint);
canvas.drawCircle(random.nextInt(canvas.getWidth() - 30),
random.nextInt(canvas.getHeight() - 30),
random.nextInt(30), paint);
canvas.drawRect(random.nextInt(canvas.getWidth()),
random.nextInt(canvas.getHeight()),
0 TECHNIKA 64. Rysowanie prostych kształtów 483

random.nextInt(canvas.getWidth()),
random.nextInt(canvas.getHeight()), paint);
}
}
}
}

Aby narysować losowe kształty, trzeba najpierw wypełnić cały ekran czarnym
kolorem, używając składowych RGB (tak jak w poprzednim przykładzie) .
Dalej znajduje się uruchamiana 10-krotnie pętla . W pętli aplikacja tworzy
egzemplarz klasy Paint , a następnie ustawia losowy nieprzejrzysty kolor uży-
wany dla każdego kształtu z danej iteracji . Później bierzemy pędzel i rysujemy
linię , kółko i prostokąt .
Do określenia miejsca rysowania kształtów i ustalenia dostępnej przestrzeni
potrzebna jest informacja o rozdzielczości ekranu. Na płótnie obowiązuje układ
współrzędnych kartezjańskich, a jego początek (punkt 0, 0) znajduje się w lewym
górnym narożniku ekranu. Do wyznaczenia obszaru, w którym mogą pojawiać
się losowe kształty, korzystamy z metod getWidth i getHeight. Zapewnienie obsługi
różnych rozdzielczości ekranu wymaga przeskalowania wartości w obu wymia-
rach. Jeśli aplikacja działa dla konkretnej rozdzielczości ekranu, trzeba też
uwzględnić orientację urządzenia.
Przez zmianę wartości kanału alfa egzem-
plarza klasy Paint na losową liczbę można uzy-
skać jeszcze ciekawsze efekty, co pokazano na
rysunku 12.4.
Klasa Canvas automatycznie łączy kolory ze
zmodyfikowanymi wartościami kanału alfa. Za
pomocą kilku prostych metod i liczb losowych
uzyskujemy ciekawe rysunki.
OMÓWIENIE
Jak widać, klasa Canvas jest prosta w użyciu,
a przy tym udostępnia wiele możliwości. Dalej
poznasz inne metody do rysowania, jednak
ogólny mechanizm korzystania z nich powinien
być już dla Ciebie zrozumiały. Należy ustawić
parametry pędzla w obiekcie klasy Paint, a na- Rysunek 12.4. Klasa Canvas
stępnie można rysować elementy na płótnie. łączy kolory o różnych
Do tej pory widoki wyświetlaliśmy na ekra- wartościach kanału alfa
nie tylko raz. Metoda onDraw jest wywoływana
jednokrotnie, po czym ekran jest uznawany za aktualny. Co zrobić, aby widok
ponownie się wyświetlał, kiedy jest to potrzebne?
484 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

0 TECHNIKA 65. Ciągłe wyświetlanie widoku w wątku interfejsu


użytkownika

W animacjach i innych warunkach, w których zmienia się stan, trzeba aktualizo-


wać widok.
PROBLEM
Chcemy, aby widok ponownie się wyświetlał, kiedy potrzebna jest aktualizacja.
ROZWIĄZANIE
Ponieważ używamy klasy Canvas, ponowne wyświetlenie widoku jest proste.
Wystarczy ustalić ekran jako nieaktualny. Aby to zrobić, można dodać do metody
onDraw widoku CanvasView poniższy wiersz. Powinien się on znajdować na końcu,
po wykonaniu wszystkich operacji związanych z rysowaniem.
protected void onDraw(Canvas canvas) {
// Rysowanie
invalidate();
}

Ta instrukcja nakazuje ponowne wyświetlenie ekranu. Ponieważ generowanie


losowych kształtów ma miejsce w metodzie onDraw, po każdym wyświetleniu
ekranu jego zawartość jest inna. Jeśli dodasz wiersz dezaktualizujący widok
i uruchomisz nową wersję aplikacji, otrzymasz ekran ze zmieniającymi się
kształtami.
OMÓWIENIE
Choć ciągłe wyświetlanie w wątku interfejsu użytkownika sprawdza się w pro-
stych sytuacjach, przedstawiona metoda nie jest odpowiednia dla aplikacji,
w których wykonuje się dużo operacji graficznych, na przykład dla gier. Aby
poprawić wydajność, trzeba oddzielić wątek wyświetlający obraz od wątku aktu-
alizującego stan. Szczegółowe objaśnianie stosowania odrębnych wątków wykra-
cza poza to omówienie (ogólny opis współbieżności znajdziesz w rozdziale 5.),
warto jednak wiedzieć, że jeśli sytuacja tego wymaga, rysowanie powinno odby-
wać się poza wątkiem interfejsu użytkownika.
Zmodyfikowanie niestandardowego widoku przez zaimplementowanie w nim
interfejsu Runnable i późniejsze uruchomienie widoku w odrębnym wątku może
prowadzić do poprawy wydajności. Ponieważ widok dziedziczy po klasie Surface
´View, można wykorzystać kontener tej ostatniej klasy (obiekt klasy Surface
´Holder) i uzyskać uchwyt do płótna. Rysowanie na płótnie powinno odbywać
się w trybie synchronicznym.
Skoro wiesz już, jak rysować proste kształty i jak w razie potrzeby aktualizo-
wać widok, pora pójść dalej i dodać tekst.
0 TECHNIKA 66. Wyświetlanie tekstu na ekranie 485

0 TECHNIKA 66. Wyświetlanie tekstu na ekranie

W prawie każdej aplikacji, w której wykorzystywany jest ekran, czasem musi być
wyświetlany tekst. Na szczęście klasa Canvas udostępnia zestaw metod drawText,
dzięki którym wyświetlanie tekstu jest proste.
Aby przedstawić działanie tych metod, tworzy-
my następny przykład. Nowa aplikacja wyświetla
trzy kolorowe kształty i napis, co pokazano na
rysunku 12.5.
Możliwe, że rozpoznajesz styl przykładowego
rysunku. Przypomina on znane logo gry LHX
Attack Chopper z lat 90. XX wieku, wyprodu-
kowanej przez Electronic Arts. Naśladując ten
styl, przedstawiamy też inne kształty i po raz
pierwszy wyświetlamy tekst.
PROBLEM
Chcemy wyświetlać tekst na ekranie.
ROZWIĄZANIE
W tej technice do projektu CanvasDemo doda-
jemy następny niestandardowy widok — tym
razem w odrębnej klasie ShapesAndTextView (lis-
ting 12.3). W klasie tej korzystamy z bardziej Rysunek 12.5. Wyświetlanie
kształtów i tekstu
zaawansowanych metod do rysowania kształtów, w niestandardowym widoku
a także z metody drawText.

Listing 12.3. Klasa ShapesAndTextView do wyświetlania kształtów i tekstu

public class ShapesAndTextView extends View {

private Paint paint;


private String text;

public ShapesAndTextView(Context context) {


super(context);
}

public void setText(String text) {


this.text = text;
}

@Override
protected void onDraw(Canvas canvas) {
canvas.drawRGB(0, 0, 0);
drawShapes(canvas);
drawText(canvas);
}

private void drawShapes(Canvas canvas) {


int side = canvas.getWidth() / 5;
486 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

paint = new Paint();


paint.setARGB(255, 255, 0, 0);
canvas.drawRect(side, canvas.getHeight() - 60 - side, side + side,
canvas.getHeight() - 60, paint);
paint.setARGB(255, 0, 255, 0);
canvas.drawCircle(side * 2 + side / 2,
canvas.getHeight() - 60 - side / 2, side / 2, paint);
paint.setARGB(255, 0, 0, 255);
paint.setStyle(Paint.Style.FILL);
Path triangle = new Path();
triangle.moveTo(side * 3 + 30, canvas.getHeight() - 60 - side);
triangle.lineTo(side * 3 + 60, canvas.getHeight() - 60);
triangle.lineTo(side * 3, canvas.getHeight() - 60);
triangle.lineTo(side * 3 + 30, canvas.getHeight() - 60 - side);
canvas.drawPath(triangle, paint);
}

private void drawText(Canvas canvas) {


paint.setColor(Color.WHITE);
paint.setTextSize(48);
canvas.drawText(text, 60, 300, paint);
}
}

Najpierw aplikacja opróżnia ekran i wypełnia go czarnym kolorem . Następnie


metoda drawShapes wyświetla czerwony kwadrat, zielone kółko i niebieski trój-
kąt. Nie opisujemy tu szczegółowo rysowania tych kształtów, ponieważ powi-
nieneś już wiedzieć, jak je wygenerować. Aplikacja używa klasy Paint do zdefinio-
wania ustawień, a następnie rysuje kształty na płótnie w układzie współrzędnych
kartezjańskich. Jedyną nowością jest zastosowanie klasy Path do narysowania
złożonej figury geograficznej, trójkąta . Ciekawe jest też obserwowanie ryso-
wania trójkąta po włączeniu efektu FILL. Więcej o efektach dowiesz się z tech-
niki 69.
Po narysowaniu kształtów aplikacja wywołuje metodę drawText, która wyświetla
na ekranie tekst W stylu LHX . Zwróć uwagę na różne sposoby ustawiania
kolorów. Oprócz metody setARGB można użyć metody setColor z predefiniowa-
nymi stałymi (na przykład Color.WHITE) odpowiadającymi liczbom całkowitym.
Teraz wystarczy użyć nowego widoku jako widoku zawartości dla aktywności.
Służy do tego poniższy fragment kodu:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ShapesAndTextView view = new ShapesAndTextView(this);
view.setText("W stylu LHX");
setContentView(view);
}

Po utworzeniu egzemplarza nowego widoku ShapeAndTextView aplikacja określa


wyświetlany tekst i za pomocą metody setContentView ustawia egzemplarz jako
widok zawartości dla danej aktywności. Prowadzi to do wyświetlenia logo z figu-
rami i tekstu (rysunek 12.4).
0 TECHNIKA 67. Określanie czcionki przy wyświetlaniu tekstu 487

OMÓWIENIE
Jak widać, rysowanie prostych figur geometrycznych, na przykład prostokątów
i kółek, jest łatwe. Narysowanie trójkąta wymaga więcej kodu, jednak także jest
proste — wystarczy użyć obiektu klasy Path składającego się z trzech linii.
Z wykorzystaniem klasy Path można utworzyć dowolny dwuwymiarowy kształt
przez łączenie linii, krzywych i łuków.
Także dodawanie tekstu jest łatwe. Wystarczy skonfigurować pędzel z zasto-
sowaniem obiektu Brush, a następnie wywołać metodę drawText. Jednak wyświe-
tlanie zwykłego tekstu to nie wszystko — Android umożliwia korzystanie z nie-
standardowych czcionek.

0 TECHNIKA 67. Określanie czcionki przy wyświetlaniu tekstu

Aby pokazać, jak stosować niestandardowe czcionki, modyfikujemy klasę Shapes


´AndTextView i używamy w niej znalezionej w internecie bezpłatnej czcionki
256Byte. Wygląd zmodyfikowanej aplikacji przedstawiono na rysunku 12.6. Nie-
standardowa czcionka pozwala na swobodne
kontrolowanie stylu tekstu przy rysowaniu ele-
mentów na ekranie i w niestandardowych kom-
ponentach.
PROBLEM
Chcemy używać niestandardowej czcionki w for-
macie TrueType.
ROZWIĄZANIE
Android udostępnia czcionki poprzez klasę
Typeface, umożliwiającą operacje związane
z czcionkami TrueType. Klasa ta zawiera metodę
fabryczną, która tworzy egzemplarze klasy Type
´face na podstawie plików czcionek TrueType
(.ttf) z katalogu z materiałami.
Aby zastosować niestandardową czcionkę,
trzeba umieścić odpowiedni plik .ttf w katalogu
materiałów. Następnie na podstawie tego pliku
Rysunek 12.6. Wyświetlanie
można utworzyć egzemplarz klasy Typeface tekstu na płótnie
i użyć go. W celu zilustrowania tego podejścia z wykorzystaniem
niestandardowej czcionki
kopiujemy klasę ShapesAndTextView do nowego TrueType
pliku i nieco ją modyfikujemy. Zmodyfikowana
klasa to ShapesAndTextFontView. Przedstawiono
ją na listingu 12.4.
488 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Listing 12.4. Niestandardowy widok ShapesAndTextFontView z obiektem


klasy Typeface

public class ShapesAndTextFontView extends View {

private Paint paint;


private Typeface font;
private String text;

public ShapesAndTextFontView(Context context) {


super(context);
font = Typeface.createFromAsset(context.getAssets(),
"256bytes.ttf");
}

// Metody onDraw i drawShapes pominięto (są takie same jak na poprzednim listingu).

private void drawText(Canvas canvas) {


paint.setColor(Color.WHITE);
paint.setTextSize(40);
paint.setTypeface(font);
canvas.drawText(text, 60, 300, paint);
}
}

Aby zastosować niestandardową czcionkę, trzeba ją najpierw zadeklarować .


Następnie można wczytać czcionkę z katalogu z materiałami (plik czcionki musi
być dołączony do aplikacji) . Po wczytaniu czcionki należy ją zastosować przez
wywołanie metody setTypeface obiektu klasy Paint . Jest to proste rozwiązanie,
daje jednak wiele możliwości z uwagi na bogactwo czcionek TrueType.
PODWÓJNE BUFOROWANIE W KLASIE CANVAS. Może interesuje
Cię to, jak klasa Canvas radzi sobie z tworzeniem i wyświetlaniem rysunków?
Wykorzystywane jest tu podwójne buforowanie. W tej technice używa
się dwóch buforów na dane — jednego do zapisu i drugiego do odczytu.
W klasie Canvas buforami są dwa egzemplarze androidowej klasy Bitmap.
Jeden służy do tworzenia rysunku, a drugi — do jego wyświetlania. Rysu-
nek najpierw powstaje w pamięci. Po zakończeniu rysowania klasa Canvas
wyświetla rysunek przez skopiowanie bitmapy z pamięci RAM do pamięci
VRAM. Rozwiązanie to jest znacznie szybsze niż rysowanie bezpośrednio
w pamięci ekranu.
OMÓWIENIE
Warto pamiętać, że czcionkę można ustawić nie tylko za pomocą klasy Paint.
Także standardowy widok TextView udostępnia metodę setTypeface. Na zapleczu
widok TextView przekazuje czcionkę do odpowiedniego obiektu klasy Paint i
wyświetla tekst w opisany wcześniej sposób. Programiści częściej używają metody
widoku TextView, niż tworzą niestandardowe komponenty. Tu jednak poznałeś
działanie wewnętrznych mechanizmów do zmiany czcionki i dzięki temu możesz
zastosować oba przedstawione podejścia.
0 TECHNIKA 68. Wyświetlanie bitmap 489

Po omówieniu pracy z kształtami i tekstem (w tym stosowania niestandar-


dowych czcionek) pora dodać istniejące rysunki do dwuwymiarowych grafik.
Posłużą do tego bitmapy.

0 TECHNIKA 68. Wyświetlanie bitmap

Wyświetlanie rysunków nie różni się od wyświetlania tekstu z wykorzystaniem


konkretnej czcionki, którą trzeba pobrać. Aby wczytać rysunek, należy wykonać
podobne kroki. W następnej wersji aplikacji CanvasDemo wczytujemy bitmapę,
a do jej wyświetlenia używamy nowych metod klasy Canvas.
Grafiką jest prosta ikona helikoptera z serwisu Wikimedia Commons. Osta-
teczny efekt to połączenie logo w stylu charakterystycznym dla gry LHX z ikoną
helikoptera (rysunek 12.7). Przez łączenie grafik z
wygenerowanymi rysunkami można tworzyć
różne dwuwymiarowe plansze do aplikacji na
Android.
PROBLEM
Chcemy wyświetlać pliki graficzne na ekranie.
ROZWIĄZANIE
Pokazaliśmy już, że klasę Canvas można wykorzy-
stać do rysowania kolorowych kształtów i tekstu.
Przy jej użyciu można też wyświetlać pliki gra-
ficzne przez pokazywanie bitmap. Tu używamy
androidowej klasy BitmapFactory do wczytywania
grafik i uzyskiwania uchwytów do nich. W apli-
kacji pobieramy plik PNG. Ponieważ aplikacja
nie jest przeznaczona dla urządzeń o konkret-
nej wielkości ekranu, plik znajduje się w kata- Rysunek 12.7. Używanie
logu res/drawable-nodpi projektu. klasy Canvas do rysowania
kolorowych kształtów
Grafikę wczytujemy podobnie jak czcionkę. i wyświetlania bitmapy
Aplikacja obejmuje prywatną składową typu
Bitmap, a w konstruktorze widoku wczytuje gra-
fikę przy użyciu jednej z wbudowanych metod Androida. Nowe fragmenty kodu
znajdziesz na listingu 12.5.

Listing 12.5. Wczytywanie pliku PNG za pomocą bitmapy i wyświetlanie


go na płótnie

public class ShapesAndTextBitmapView extends View {

private Paint paint;


private Typeface font;
private Bitmap bitmap;

public ShapesAndTextBitmapView(Context context) {


490 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

super(context);
bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.copter);
}

// Metody onDraw i drawShapes pominięto (są takie same jak na poprzednim listingu).

private void drawBitmap(Canvas canvas) {


paint = new Paint();
canvas.drawBitmap(bitmap, 0, 0, paint);
}
}

OMÓWIENIE

Powinieneś już dobrze znać to podejście. Za pomocą różnych metod do rysowa-


nia z klas Paint i Canvas można kontrolować wszystkie aspekty dwuwymiarowej
przestrzeni. Tu deklarujemy bitmapę, którą chcemy dodać , wczytujemy ją
z zasobów z wykorzystaniem klasy BitmapFactory i wyświetlamy grafikę .
Metoda do rysowania bitmapy jest prosta. Inicjuje obiekt klasy Paint i rysuje gra-
fikę, począwszy od lewego górnego narożnika ekranu (pamiętaj, że punkt (0, 0)
to właśnie lewy górny narożnik).
Do tej pory rysowaliśmy proste kształty oraz wyświetlaliśmy tekst i bitmapy.
Dalej dokładniej omawiamy dostępne efekty.

0 TECHNIKA 69. Stosowanie efektów dwuwymiarowych

Aby zilustrować stosowanie efektów, tworzymy niestandardową klasę widoku —


CustomButton. Wyświetla ona przeskalowany tekst z efektem niewielkiego wgłę-
bienia, a także dynamiczny licznik. Ponadto w tle napisu widoczny jest gradient.
Stosujemy też efekt niewielkiej wypukłości i zaokrąglenia narożników. W goto-
wej aplikacji uzyskujemy wymyślny przycisk przedstawiony w dolnej części
rysunku 12.1. Zauważ, że tu nie zajmujemy się działaniem przycisku, na przykład
stanem (zwykły i wciśnięty). Ważne są tylko atrybuty graficzne. W niestandar-
dowym przycisku nie korzystamy z żadnych nowych metod do rysowania. Sto-
sujemy natomiast efekty dostępne w klasie Paint.
PROBLEM
Chcemy tworzyć niestandardowe efekty graficzne.
ROZWIĄZANIE
Przez zastosowanie klasy Paint i kilku strategii można uzyskać ciekawe efekty.
Aby pokazać działanie aplikacji, przedstawiamy fragmenty kodu z różnych klas,
służące do tworzenia obiektów i rysowania elementów. Jeśli chcesz, możesz
zapoznać się z kompletnym kodem, jednak na listingu 12.6 koncentrujemy się
na konkretnych sztuczkach związanych z klasą Paint, potrzebnych do uzyskania
pożądanych efektów. Zaczynamy od określenia wyglądu obramowania.
0 TECHNIKA 69. Stosowanie efektów dwuwymiarowych 491

Listing 12.6. W klasie widoku CustomButton wykorzystujemy kilka efektów


z klasy Paint

public class CustomButton extends View {

private Paint borderPaint;


private PathEffect borderRadius;

// ...
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle) {

borderPaint = new Paint();


borderRadius = new CornerPathEffect(5);

borderPaint.setPathEffect(borderRadius);
borderPaint.setStyle(Style.STROKE);
borderPaint.setColor(Color.rgb(75, 75, 75));
borderPaint.setStrokeWidth(2F);
borderPaint.setAntiAlias(true);
// ...
}
// ...
}

Do ustawienia wyglądu ramki służą zmienna z klasy Paint i efekt PathEffect ,


w którym określane są właściwości obramowania. W nowej wersji przesłanianego
konstruktora niestandardowego widoku tworzymy egzemplarz klasy CornerPath
´Effect o promieniu 5 i ustawiamy go w taki sposób, aby narożniki były
delikatnie zaokrąglone. Dla ramki używamy też stylu STROKE, szarego koloru i sze-
rokości 2 . W ostatnim kroku włączamy antyaliasing, aby zminimalizować znie-
kształcenia na krawędziach .
W ten sposób określamy właściwości obramowania. Ten sam wzorzec stosu-
jemy do innych obiektów klasy Paint, aby skonfigurować gradient i efekty tekstowe.
Następny fragment kodu (listing 12.7) ustawia gradient wypełniający przycisk.

Listing 12.7. Tworzenie gradientu za pomocą obiektu klasy Paint

public class CustomButton extends View {

private Paint squarePaint;

// ...
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle) {

squarePaint = new Paint();

squarePaint.setStyle(Style.FILL);
squarePaint.setColor(Color.rgb(245, 245, 245));
squarePaint.setPathEffect(borderRadius);
squarePaint.setAntiAlias(true);
// ...
492 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

}
// …
}

Aby dodać gradient, należy utworzyć odrębny obiekt klasy Paint, w którym będą
przechowywane ustawienia , i użyć opcji FILL dla wnętrza przycisku .
Następnie trzeba dopasować gradient do promienia narożników obramowania.
W przeciwnym razie gradient może wychodzić poza zaokrąglony narożnik .
Wypełnianie przycisku gradientem wyjaśniamy w omówieniu metody onDraw.
Następnie ustawiamy efekt tekstowy. Stosujemy tu styl popularny dla nurtu
Web 2.0, możliwe więc, że nie jest to dla Ciebie nic nowego. Używamy czarnego
tekstu z wąskim białym cieniem pod spodem, co daje efekt wgłębienia względem
gradientu. Potrzebny kod przedstawiono na listingu 12.8.

Listing 12.8. Tworzenie efektu tekstowego z wgłębieniem i cieniem za pomocą


obiektu klasy Paint

public class CustomButton extends View {

private Paint textPaint;


// ...
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle) {

textPaint = new Paint();

textPaint = new Paint();


textPaint.setShadowLayer(1.0F, 0F, 2F, Color.WHITE);
textPaint.setTextAlign(Align.CENTER);
textPaint.setColor(Color.BLACK);
textPaint.setStyle(Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setTypeface(Typeface.SANS_SERIF);
// ...
}

Zauważ, że metoda setShadowLayer (używana do dodawania białego cienia pod


tekstem) jest dostępna we wszystkich egzemplarzach klasy Paint. Można ją wyko-
rzystać do dodania cienia dla dowolnego elementu — ścieżki, prostokąta, tekstu
itd. Pozostałe aspekty efektu tekstowego nie są niespodzianką. Można ustawić
wyrównanie, kolor, styl czy czcionkę. Kod klasy kończy pokazana na listingu 12.9
metoda onDraw, która wyświetla tekst na płótnie.

Listing 12.9. Metoda onDraw klasy CustomButton

@Override
public void onDraw(Canvas canvas) {
squarePaint.setShader(new LinearGradient(0F, 0F, 0F, height,
Color.rgb(254, 254, 254),
Color.rgb(221, 221, 221),
Shader.TileMode.REPEAT));
12.2. Grafika trójwymiarowa i biblioteka OpenGL ES 493

textPaint.setTextSize(width * 0.09F);

countPaint.setTextSize(height * 0.3F);
Rect rect = new Rect(0, 0, width, height);
canvas.drawRect(rect, squarePaint);
canvas.drawText(text, (width / 2) - (width / 10) + 10,
(height / 2) + (height / 3), textPaint);
canvas.drawText("" + count, (int) (width * 0.92),
height / 3, countPaint);
}

W metodzie onDraw ustawiamy strategię Shader dla obiektu klasy Linear


´Gradient . Dzięki temu powstaje efekt trójwymiarowości. Strategię ustawiamy
w metodzie onDraw, a nie w konstruktorze (gdzie podajemy pozostałe właściwości),
ponieważ do prawidłowego utworzenia gradientu potrzebna jest jego łączna
wysokość. Ponadto gradient się powtarza, wypełnia więc przycisk w poziomie.
Dalej ustawiamy wielkość tekstu i licznika na podstawie szerokości i wysokości
danego widoku . Ostatnia operacja to wyświetlenie prostokątnego przycisku na
płótnie i dołączenie tekstu .
OMÓWIENIE
Przez utworzenie niestandardowej klasy widoku z nową wersją jednego z domyśl-
nych konstruktorów możemy konfigurować różne obiekty klasy Paint dla poszcze-
gólnych efektów. Po zakończeniu konfigurowania można w metodzie onDraw
dopracować ustawienia na podstawie właściwości środowiska uruchomieniowego.
Tu klasy Paint użyliśmy do ustawienia obramowania, gradientu i efektów teksto-
wych, a gotową grafikę wyświetliliśmy na płótnie. Opisana technika pozwala ściśle
kontrolować wszystkie aspekty niestandardowego widoku.
Omówiliśmy wiele zagadnień związanych z grafiką dwuwymiarową w Andro-
idzie. Wiesz już, jak rysować figury, dodawać tekst, dołączać bitmapy oraz stoso-
wać kolory i efekty. Pora dodać następny wymiar.

12.2. Grafika trójwymiarowa i biblioteka OpenGL ES


Choć klasa Canvas umożliwia obsługę grafiki dwuwymiarowej, nie wystarcza do
tworzenia wydajnych aplikacji, w których trzeba wykonywać wiele operacji
graficznych. Aby przejść na następny poziom, trzeba wkroczyć w świat trójwymia-
rowości i biblioteki OpenGL ES.
Przetwarzanie grafiki związanej z klasą Canvas odbywa się w procesorze.
Procesor potrafi wykonać wiele operacji, ale nie jest wyspecjalizowany pod kątem
grafiki i zwykle musi wykonywać także inne zadania. Wiele współczesnych
urządzeń z Androidem ma specjalne procesory graficzne (ang. graphics processing
unit — GPU). Procesor graficzny może przejmować przetwarzanie grafiki od
głównego procesora. Korzystanie z procesora graficznego pozwala odciążyć główny
procesor i zwiększyć ogólną wydajność aplikacji. Biblioteka OpenGL ES potrafi
494 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

korzystać z procesora graficznego i umożliwia tworzenie efektownych grafik


dwu- i trójwymiarowych.
W tym podrozdziale kontynuujemy wykład o rysowaniu i wkraczamy w świat
biblioteki OpenGL ES. Zaczynamy od kolejnych rysunków dwuwymiarowych,
a następnie przechodzimy do grafiki trójwymiarowej. Przy okazji pokazujemy,
jak stosować kolory i tekstury dla figur trójwymiarowych. Najpierw jednak musimy
wyjaśnić, czym jest biblioteka OpenGL ES.

12.2.1. Czym jest biblioteka OpenGL?


OpenGL (ang. Open Graphics Library) to otwarta platforma do tworzenia grafiki
dwu- i trójwymiarowej. Współdziała z wieloma językami i systemami oraz korzysta
z akceleratora grafiki. Ewentualny człon ES w nazwie OpenGL ES oznacza, że
w Androidzie dostępna jest wersja biblioteki dla systemów osadzanych. Wersja ta
jest zoptymalizowana pod kątem telefonów, tabletów, przystawek STB, konsol itd.
Jest tylko częścią pełnej biblioteki dla komputerów stacjonarnych, jednak daje
dużo możliwości.
DODATKOWE INFORMACJE O BIBLIOTECE OPENGL. Nie oma-
wiamy tu szczegółowo programowania z wykorzystaniem biblioteki
OpenGL. To temat na osobną książkę. Jeśli szukasz dodatkowych informa-
cji na ten temat, doskonałe materiały znajdziesz w lekcjach w serwisie Neon
Helium (http://nehe.gamedev.net). Techniki z pierwszych spośród tych
lekcji i kilku następnych punktów częściowo się pokrywają, jednak nie
zamierzamy tu opisywać wszystkich aspektów programowania z wykorzy-
staniem biblioteki OpenGL. Chcemy jedynie wprowadzić Cię w używanie
jej na Androidzie.
Z technicznego punktu widzenia interfejs API biblioteki OpenGL to szczegó-
łowa specyfikacja zarządzana przez grupę Khronos. Każdy producent sprzętu
udostępnia własną implementację tego interfejsu i musi przejść surowe testy, aby
udowodnić zgodność swoich rozwiązań ze specyfikacją. Czasem jednak występują
pewne różnice w działaniu biblioteki na poszczególnych urządzeniach, przy czym
dotyczą one rzadko wykonywanych zadań. Biblioteka OpenGL ES na urządze-
niach z Androidem dostępna jest w wersjach 1.0, 1.1 i 2.0. Każde urządzenie
obsługuje wersję 1.0. Wersja 2.0 jest obsługiwana na nowszych urządzeniach i nie
jest zgodna z wersjami 1.x.
Tu przedstawiamy wersje OpenGL ES 1.x. Wybór ten wynika z kilku powo-
dów. Po pierwsze, każde urządzenie z Androidem obsługuje te wersje, dlatego
korzystanie z nich jest najbezpieczniejszym sposobem na dotarcie do jak naj-
szerszego grona odbiorców. Po drugie, chcemy skoncentrować się na serii etapów
w procesie korzystania z biblioteki. W wersjach OpenGL ES 1.x etapy te są
ściśle ustalone, natomiast w wersji OpenGL ES 2.0 można je ponownie zaimple-
mentować, dodać nowe kroki, a nawet zmienić ich kolejność. Ponadto zastoso-
12.2. Grafika trójwymiarowa i biblioteka OpenGL ES 495

wanie wersji 1.x pozwala łatwiej skupić się na podstawach i wewnętrznych


mechanizmach, ponieważ w wersji 2.0 niektóre operacje ukryto za dodatkowymi
interfejsami.
INTERFEJS RENDERSCRIPT. W 11. wersji interfejsu (Android 3.0)
wprowadzono nowy interfejs API, Renderscript, przeznaczony do prze-
twarzania grafiki trójwymiarowej. Jest on przenośny między architekturami
systemowymi i daje dużo możliwości, ale nie tak wiele jak OpenGL.
Ponadto Renderscript jest napisany w języku C, dlatego trudniej jest
nauczyć się go programistom, którzy nie znają tego języka. Z tych powodów
nie omawiamy tu tego interfejsu. Warto jednak wiedzieć, że jest dostępny.
Więcej informacji znajdziesz w dokumentacji: http://mng.bz/6i1B.
TWORZENIE ROZWIĄZAŃ DLA KONKRETNEJ WERSJI BIBLIO-
TEKI OPENGL ES. Jeśli chcesz określić, że aplikacja obsługuje tylko
konkretną wersję biblioteki OpenGL ES, możesz to zrobić w manifeście.
Użyj do tego atrybutu android:glEsVersion elementu <uses-feature>. Atry-
but ten pozwala określić najnowszą obsługiwaną wersję (ponadto trzeba
zapewnić obsługę starszych wersji, aż do 1.0). Jeśli nie ustawisz tego atry-
butu, jego domyślną wartością będzie 1.0. Dodatkowe informacje znajdziesz
w dokumentacji manifestu.

Urządzenia bez procesorów graficznych


Urządzenia z Androidem, które nie mają specjalnego procesora graficznego, muszą
obejmować implementację biblioteki OpenGL ES 1.0. Jest to implementacja pro-
gramowa; jej pracę emuluje główny procesor. Z biblioteki OpenGL można korzy-
stać także w takich urządzeniach, jednak przyrost wydajności nie jest w nich tak
duży.

Teraz, kiedy wiesz już, co biblioteka OpenGL ma robić i które z jej wersji są
obsługiwane w Androidzie, pora zobaczyć, jak działa to narzędzie.

12.2.2. Działanie biblioteki OpenGL ES


Jedynym zadaniem biblioteki OpenGL jest łączenie i rysowanie grafiki na ekra-
nie. Biblioteka generuje piksele widoczne na wyświetlaczu. Ostateczny obraz
to efekt serii obliczeń i procesów wykonanych na danych wejściowych.
Jeśli chcemy na przykład wyświetlić prosty obraz domu widzianego z lotu
ptaka, musimy dokładnie opisać bibliotece OpenGL ES scenę za pomocą figur
geometrycznych. Dom składa się z sześcianu z nałożoną piramidą. Załóżmy, że
w ostatniej scenie chcemy pokazać dom z góry pod kątem 45° i z wysokości
6 metrów. Przednia elewacja ma kolor żółty, a trzy pozostałe ściany mają kolor
i teksturę cegły. Biblioteka OpenGL ES pobiera wszystkie informacje i rozpo-
czyna tworzenie grafiki.
496 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Trzeba pamiętać, że biblioteka OpenGL ES tworzy modele z trójkątów


opartych na wierzchołkach.
Q Wierzchołek to punkt w trójwymiarowej przestrzeni. Wyznaczają go trzy
wymagane składowe, które określają jego pozycję na osiach x, y i z.
Można też ustawić dla niego opcjonalne komponenty, na przykład kolor.
Q Trójkąt jest wyznaczany przez trzy wierzchołki.
Q Model lub geometria (nazwy te to synonimy) to każdy element składający
się z trójkątów lub będący jednym trójkątem.
Wszystko w świecie biblioteki OpenGL ES składa się z trójkątów. Kwadrat to
na przykład połączenie dwóch trójkątów, a na sześcian składa się sześć takich
kwadratów. Biblioteka OpenGL ES przyjmuje podane trójkąty, powiązane z nimi
dane oraz perspektywę i na tej podstawie ustala rzut. Następnie dodaje teksturę
obiektów, w razie potrzeby oświetla scenę i wprowadza przekształcenia, aby
uzyskać gotowy obraz z podanej perspektywy. Ponadto określa, co ma znajdować
się na ostatecznym obrazie, a co należy pominąć. W ostatnim kroku tworzy bit-
mapę z pikseli. Wszystkie etapy są wykonywane przez odrębne programy. Są one
uruchamiane po kolei, a dane wyjściowe z każdego etapu są danymi wejściowymi
dla następnego.
OpenGL to bardzo rozbudowana maszyna stanowa. Przekazywane do niej
polecenia określają, w jaki stan powinna przejść, jakie wartości mają mieć bieżące
atrybuty stanu lub że należy wygenerować obraz. Oto kolejne instrukcje w pseu-
dokodzie, generujące obraz na podstawie modeli.
setCurrentState(READ_OBJECTS);
setValue(ObjectArray[0], triangle1);
setValue(ObjectArray[1], triangle2);
setCurrentState(CHANGE_PERSPECTIVE);
issueCommand(move_20feet_up);
issueCommand(look_down_45degrees);
setCurrentState(USE_SCREEN);
setValue(SCREEN_COLOR, black);
issueCommand(clear_screen);
issueCommand(draw_object, ObjectArray[0]);
issueCommand(draw_object, ObjectArray[0]);

W pseudokodzie pokazano, jak zgłaszać instrukcje dla biblioteki OpenGL ES.


Najpierw należy nakazać jej przygotowanie do wczytania obiektów. Dalszy kod
wczytuje dwa trójkąty do repozytorium obiektów. Następnie biblioteka OpenGL
ES ma przejść w stan pozwalający na manipulowanie widokiem sceny. Wyobraź
sobie, że musisz ustawić się z aparatem w taki sposób, aby uzyskać dobre ujęcie
obiektów, które mają znaleźć się na obrazie. Tu polecenia powodują przenie-
sienie aparatu na wysokość 20 stóp i ustawienie go pod kątem 45° w dół. Usta-
wiany jest też kolor tła. Instrukcje z ostatniego etapu powodują wyświetlenie
trójkątów (obiektów). Biblioteka OpenGL ES przeprowadza wszystkie przekształ-
cenia macierzowe i zgodnie z instrukcjami generuje obraz.
12.2. Grafika trójwymiarowa i biblioteka OpenGL ES 497

Warto zapamiętać, że biblioteka OpenGL ES działa na trójkątach zdefinio-


wanych z wykorzystaniem wierzchołków. Dane wejściowe to zbiór trójkątów,
a dane wyjściowe to obraz wyświetlany na ekranie. Na razie szczegóły tych ope-
racji mogą wydawać Ci się niezrozumiałe, jednak nie przejmuj się — w kilku
dalszych punktach zapoznasz się z praktycznymi przykładami i wszystko stanie
się jasne. Zaczynamy od utworzenia pierwszego projektu wykorzystującego
bibliotekę OpenGL.

12.2.3. Tworzenie projektu wykorzystującego bibliotekę OpenGL


Aby omówienie biblioteki OpenGL było bardziej konkretne, tworzymy nowy
projekt. W jego kodzie wykonujemy podstawowe kroki potrzebne do wyświetle-
nia informacji na ekranie. Następnie dodajemy dalsze funkcje. Nowy projekt nosi
nazwę OpenGLDemo. Początkowo aplikacja wyświetla tylko zielony ekran.
Potem tworzymy jeden trójkąt, następnie przekształcamy go w trójwymiarową
piramidę, a na końcu dodajemy do piramidy kolory i teksturę oraz sprawiamy, że
figura się obraca. Efekt pracy to kilka różnych aktywności wykonujących ope-
racje z poszczególnych etapów. Działanie tych aktywności przedstawiono na
rysunku 12.8.

Rysunek 12.8. Ekrany aktywności z aplikacji OpenGLDemo odpowiadają etapom


tworzenia trójwymiarowych kształtów

POBIERZ PROJEKT OPENGLDEMO. Kod źródłowy projektu i pakiet


APK do uruchamiania aplikacji znajdziesz w witrynie z kodem do książki
Android w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skon-
centrować się na konkretnych zagadnieniach, zalecamy pobranie kom-
pletnego kodu źródłowego i śledzenie go w Eclipse (lub innym środowisku
IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/lhyX, plik APK: http://mng.bz/4QuG.
Początkowo projekt OpenGLDemo wyświetla zielony ekran. To prawda, nie jest
to nic niezwykłego, jednak tworzymy pierwszy prosty silnik renderujący i opróż-
niamy ekran, aby wyświetlić kolor. Jest to pierwszy w książce fragment kodu
wykorzystujący bibliotekę OpenGL. Podobnie jak przy korzystaniu z klasy Canvas,
zaczynamy tu od utworzenia niestandardowego widoku z dostępem do sterow-
nika biblioteki OpenGL ES. Potrzebny kod pokazano na listingu 12.10.
498 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Listing 12.10. Aktywność OpenGLGreenScreenActivity przygotowująca bibliotekę


OpenGL do pracy

public class OpenGLGreenScreenActivity extends Activity {

private GLSurfaceView glView;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
glView = new GLSurfaceView(this);
glView.setRenderer(new MyOpenGLRenderer());
setContentView(glView);
}

class MyOpenGLRenderer implements Renderer {

@Override
public void onSurfaceChanged(GL10 gl,
int width, int height) {
Log.d("MyOpenGLRenderer",
"Zmiana powierzchni. Szerokość=" + width + " Wysokość=" + height);
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("MyOpenGLRenderer", "Utworzono powierzchnię");
}

@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.0f, 0.5f, 0.0f, 1f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
}
}
}

W Androidzie wszystkie obrazy generowane przez bibliotekę OpenGL są umiesz-


czane w widoku GLSurfaceView . Jest to androidowa klasa widoku łącząca kod
biblioteki OpenGL z infrastrukturą widoków i interfejsami API Androida. Po
zadeklarowaniu klasy GLSurfaceView kod za pomocą kontekstu tworzy egzemplarz
tej klasy i łączy go z egzemplarzem klasy z rendererem (implementacją inter-
fejsu Renderer) .
Renderer odpowiada za kierowanie do biblioteki OpenGL wywołań gene-
rujących kadr. Tu używamy egzemplarza klasy MyOpenGLRenderer, którą omawiamy
dalej. Kiedy widok jest gotowy, należy ustawić go jako widok zawartości dla
całej aktywności . Widok generowany przez OpenGL można (podobnie jak
każdy inny widok) umieścić w dowolnym miejscu ekranu, wybierając dogodny
układ. Tu jednak stosujemy podejście używane już wcześniej przy rysowaniu
i wykorzystujemy cały ekran.
12.2. Grafika trójwymiarowa i biblioteka OpenGL ES 499

W kodzie klasy MyOpenGLRenderer trzeba przesłonić kilka metod. Są to:


onSurfaceChanged, wywoływana przy zmianie wymiarów ekranu (występuje ona na
przykład przy zmianie orientacji urządzenia) , i onSurfaceCreated, wywoływana
po utworzeniu powierzchni . Tworzenie powierzchni odbywa się w momencie
uruchamiania aplikacji, przy wznawianiu pracy programu po okresie działania
w tle i przy zmianie orientacji. Warto zauważyć, że wszystkie wymienione zdarze-
nia zachodzą po utracie kontekstu, dlatego wszelkie materiały (czyli wczytane
obrazy) zostają utracone i trzeba je odtworzyć. To w wymienionych metodach
trzeba tworzyć (lub odtwarzać) wszystkie materiały i obiekty aplikacji. Niezbędna
jest też metoda onDrawFrame .
Rysowanie odbywa się w metodzie onDrawFrame. Przekazany egzemplarz klasy
GL10 umożliwia kierowanie poleceń do biblioteki OpenGL i informowanie jej, co
ma zrobić. Tu aplikacja najpierw z wykorzystaniem metody glClearColor ustawia
kolor domyślny, używany po opróżnieniu bufora . Jak prawdopodobnie zauwa-
żyłeś, w bibliotece OpenGL najczęściej stosuje się liczby zmiennoprzecinkowe.
Parametry to składowe RGBA (zauważ, że tu kanał alfa podawany jest na końcu).
Pierwszy parametr to składowa czerwona, drugi — składowa zielona, trzeci —
składowa niebieska, a na końcu podawana jest wartość dla kanału alfa. Parametry
przyjmują wartości od 0 do 1. Wartość 0,5 oznacza połowę intensywności. Tak
więc przezroczysty czerwony to (1, 0, 0, 1), a półprzezroczysty niebieski to (0, 0,
1, 0,5). Po ustawieniu koloru wywołujemy polecenie glClear . Powoduje ono
wypełnienie powierzchni ustawionym kolorem.
Biblioteka OpenGL ma wiele stałych — każdej zmiennej z tej biblioteki
odpowiada jedna stała. Rozwiązanie to działa podobnie jak duże odwzorowanie
lub „płaska” baza danych. Bufory można wskazać za pomocą stałej GL10.GL_COLOR_
´BUFFER_BIT, a polecenie glClear powoduje ich opróżnienie. Warto zauważyć,
że każda stała biblioteki OpenGL ma przedrostek GL_, a instrukcje mają przed-
rostek gl.
Po uruchomieniu aktywności OpenGLGreenScreenActivity pojawia się zielona
powierzchnia zajmująca cały ekran (widoczna po lewej stronie na rysunku 12.8).
Za renderowanie obrazu na ekranie odpowiada obiekt typu Renderer, który nie-
ustannie wykonuje swoje zadanie (do czasu nakazania mu przerwania pracy).
Cały czas wywoływana jest metoda onDrawFrame, podobnie jak metoda onDraw
w kodzie obrazującym używanie płótna (pracę metody onDraw kończy instrukcja
invalidate). Szczegółowe omawianie częstości odświeżania ekranu i optymalizacji
wykracza poza zakres tego rozdziału. Warto jednak wspomnieć, że liczba klatek
na sekundę (ang. frames per second — FPS) określa, ile razy na sekundę metoda
onDrawFrame jest wywoływana i kończy pracę.

RENDEROWANIE W ODRĘBNYM WĄTKU. Widok SurfaceView


biblioteki OpenGL — w odróżnieniu od płótna — musi wykonywać
renderowanie w odrębnym wątku. Na szczęście nie trzeba tworzyć takiego
wątku od podstaw, ponieważ dostępny jest interfejs Renderer. Wystarczy
500 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

go zaimplementować i zarejestrować implementację w widoku. Za pozo-


stałe operacje odpowiada framework biblioteki OpenGL. Na koniec ostrze-
żenie — nigdy nie wywołuj biblioteki OpenGL ES z innego wątku, ponie-
waż nie jest to bezpieczne.
Kiedy gotowy jest już widok GLSurfaceView, w którym można rysować, i napisa-
liśmy renderer, pora wyświetlić coś ciekawszego niż jednolity kolor.

0 TECHNIKA 70. Rysowanie pierwszego trójkąta

Zdefiniujmy pierwszy podstawowy obiekt — prosty trójkąt. Wymaga to określe-


nia granic figury, a następnie umożliwienia wyświetlania jej przez zgłaszanie
odpowiednich instrukcji biblioteki OpenGL. Przygotowany trójkąt wyświetlamy
na środku ekranu (rysunek 12.9).
Warto zauważyć, że pierwszy trójkąt to rysu-
nek dwuwymiarowy. Biblioteka OpenGL obsłu-
guje także rysunki dwuwymiarowe (jej funkcje
nie są ograniczone do grafiki trójwymiarowej).
Jeśli potrzebujesz wyższej wydajności — nawet
dla grafiki dwuwymiarowej — OpenGL może
okazać się najlepszym rozwiązaniem. Tu zaczy-
namy od rysunków dwuwymiarowych, a następ-
nie przechodzimy do trójwymiarowych.
PROBLEM
Chcemy rysować proste figury za pomocą biblio-
teki OpenGL.
ROZWIĄZANIE
W wersjach OpenGL ES 1.x używa się tylko
trójkątów, ponieważ ich wierzchołki są współ- Rysunek 12.9. Wyświetlanie
dwuwymiarowego trójkąta
płaszczyznowe (znajdują się na tej samej pła- z użyciem biblioteki OpenGL
szczyźnie), co ułatwia obliczenia procesorowi
graficznemu. Pamiętaj, że trójkąt jest wyznaczany przez trzy wierzchołki (punkty
w przestrzeni). Każdy wierzchołek jest definiowany przez trzy współrzędne: x, y i z.
Oznacza to, że trójkąt jest definiowany przez dziewięć wartości — trzy wierz-
chołki po trzy współrzędne.
Ciekawym aspektem jest pobieranie danych o wierzchołkach przez bibliotekę
OpenGL ES. Dane te mają postać tablic, jednak ponieważ używany jest natywny
interfejs API języka C (a nie API Javy), wartości muszą występować w odpo-
wiedniej kolejności. Java musi zapisać dane na temat trójkąta w bloku pamięci
poza stertą, we współużytkowanej pamięci systemowej, gdzie dostęp do nich ma
sterownik procesora graficznego. Aby mieć pewność, że aplikacja używa odpo-
wiedniej struktury danych, zgodnej z systemem, używamy interfejsu API NIO Javy.
0 TECHNIKA 70. Rysowanie pierwszego trójkąta 501

Dla biblioteki OpenGL ES wygodne są liczby zmiennoprzecinkowe, dlatego


używamy właśnie ich. Każda składowa takiej liczby zajmuje do 4 bajtów pamięci,
tak więc trójkąt może wymagać 36 bajtów. Zobaczmy, jak utworzyć trójkąt za
pomocą interfejsu NIO Javy (listing 12.11).

Listing 12.11. Klasa Triangle

public class Triangle {

private FloatBuffer vertexBuffer;


private float vertices[] = {
100.0f, 150.0f, 0.0f,
219.0f, 150.0f, 0.0f,
160.0f, 279.0f, 0.0f
};

public Triangle() {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3 * 3 * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}

public void draw(GL10 gl) {


gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertices.length / 3);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
}

Tworzenie prostej klasy figury zaczynamy od przygotowania bufora na współ-


rzędne wierzchołków trójkąta . Dane przechowywane są na natywnej stercie,
co umożliwia dostęp do nich sterownikowi biblioteki OpenGL. Następnie poda-
jemy tablicę obejmującą po trzy współrzędne trzech wierzchołków . Zauważ,
że tablica podana jest w stylu języka C, dlatego nie jest dwuwymiarowa. Pierwszy
element to współrzędna x pierwszego wierzchołka; drugi to współrzędna y tego
wierzchołka; trzeci to współrzędna z tego samego wierzchołka; czwarty to
współrzędna x drugiego wierzchołka itd.
Należy zapamiętać kolejność wierzchołków. Trzeba je podawać w kolejności
przeciwnej do kierunku ruchu wskazówek zegara, ponieważ w bibliotece OpenGL
uwzględniane są ściany (ang. faces), co pozwala przyspieszyć renderowanie.
Biblioteka renderuje przednie ściany (zwrócone do widza), a wszystkie pozostałe
pomija. Jest to tak zwane odrzucanie tylnych ścian (ang. backface culling). Tu
sprawiamy, że trójkąt jest wyświetlany przednią ścianą do widza. Służą do tego
współrzędne podane na rysunku 12.10.
502 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Rysunek 12.10. Wierzchołki trójkąta


i wyświetlanie przedniej ściany

Warto zauważyć, że — jak pokazano na rysunku 12.10 — początkiem układu


współrzędnych biblioteki OpenGL jest domyślnie lewy dolny narożnik (dla klasy
Canvas jest to lewy górny narożnik). Po przygotowaniu tablicy współrzędnych
i bufora można przejść do konstruktora, gdzie należy utworzyć trójkąt i zapisać go
na natywnej stercie .
W konstruktorze alokujemy blok 32 bajtów na natywnej stercie (służy do tego
obiekt klasy ByteBuffer) i upewniamy się, że kolejność bajtów jest taka sama jak
w procesorze. Następnie bezpiecznie przekształcamy bufor bajtów na bufor liczb
zmiennoprzecinkowych. Później używamy metody put do skopiowania zawarto-
ści tablicy do bufora FloatBuffer. W ostatnim kroku wykonujemy operację flip
na buforze, co sprawia, że kursor wraca do pierwszego elementu (pozycja 0).
Wtedy można rozpocząć rysowanie.
Do metody draw przekazujemy obiekt GL10, co pozwala uruchamiać polecenia
biblioteki OpenGL ES 1.0 . Najpierw wywołujemy metodę glEnableClientState,
która informuje bibliotekę OpenGL, że wierzchołki używane do rysowania ozna-
czają pozycje. Następnie ustawiamy kolor na półprzezroczysty zielony. Później
używamy metody glVertexPointer, która informuje bibliotekę OpenGL o poło-
żeniu i formacie danych tablicy ze współrzędnymi wierzchołków używanymi do
renderowania.
Pierwszy parametr metody glVertexPointer informuje bibliotekę OpenGL ES,
że pozycja każdego wierzchołka jest wyznaczana przez trzy wartości. Można też
podać dwie wartości; wtedy biblioteka OpenGL przyjmuje dla współrzędnej
z wartość 0. Drugi parametr określa, jakiego typu dane odpowiadają poszczegól-
nym współrzędnym. Tu używamy liczb zmiennoprzecinkowych, o czym infor-
mujemy bibliotekę za pomocą stałej. Dzięki temu biblioteka wie, że ma pobrać
z pamięci 4 bajty z wartością każdej współrzędnej. Trzeci parametr to przesu-
nięcie (ang. stride), określające liczbę innych wartości między dwoma wierzchoł-
kami. Załóżmy, że dla każdego wierzchołka podawana jest składowa koloru. Wtedy
należy przeskoczyć 16 bajtów (zmiennoprzecinkowe składowe RGBA, z których
każda zajmuje do 4 bajtów) do początku następnego wierzchołka. Określa to
podawany w bajtach atrybut przesunięcia.
Teraz można wywołać metodę drawArrays, która nakazuje bibliotece OpenGL
narysowanie trójkąta. Metoda określa, że biblioteka ma narysować podstawową
figurę. Pierwszy parametr metody to GL_TRIANGLES. Drugi parametr określa pozy-
cję pierwszego wierzchołka w tablicy przekazanej do biblioteki. Jest to przy-
datne przy rysowaniu fragmentów siatki. Ostatni parametr informuje bibliotekę
0 TECHNIKA 70. Rysowanie pierwszego trójkąta 503

OpenGL o liczbie wierzchołków używanych do renderowania. Wartość ta zawsze


musi być wielokrotnością liczby 3. Opisane polecenie powoduje przesłanie do
procesora graficznego danych, z których biblioteka OpenGL korzysta przy każdym
wywołaniu instrukcji rysowania. W ostatnim kroku wykonujemy operacje porząd-
kowe przez wyłączenie obsługi tablicy wierzchołków. Nowy obiekt klasy Triangle
można wykorzystać w aktywności w sposób pokazany na listingu 12.12.

Listing 12.12. Klasa OpenGLTriangleActivity

public class OpenGLTriangleActivity extends Activity {

private GLSurfaceView glView;


private Triangle triangle;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
glView = new GLSurfaceView(this);
glView.setRenderer(new MyOpenGLRenderer());
setContentView(glView);
}

class MyOpenGLRenderer implements Renderer {


@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("MyOpenGLRenderer",
"Zmiana powierzchni. Szerokość=" + width + " Wysokość=" + height);
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrthof(0, 320, 0, 480, 1, -1);
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("MyOpenGLRenderer", "Utworzono powierzchnię");
triangle = new Triangle();
}

@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.0f, 0.5f, 0.0f, 1f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
triangle.draw(gl);
}
}
}

Aktywność służąca do wyświetlania obiektu klasy Triangle nie tylko zmienia


widok. Najpierw należy zadeklarować rysowany obiekt klasy Triangle jako zmienną
składową . Dalej wprowadzamy dodatki i zmiany do implementacji interfejsu
Renderer.
504 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Modyfikujemy w kilku miejscach metodę onSurfaceChanged, co pozwala ryso-


wać elementy po zmianie powierzchni. Po zmianie wymiarów powierzchni nastę-
puje modyfikacja wymiarów okna . Okno wyznacza wielkość i rozdzielczość
wyświetlanego obrazu. W środowisku dwuwymiarowym, gdzie wszystko można
wymierzyć co do piksela, jest to wielkość i rozdzielczość ekranu. Dalej defi-
niowany jest typ rzutowania . Biblioteka OpenGL oblicza ostateczny wygląd
obrazu na podstawie macierzy. Tu używane jest rzutowanie ortogonalne. Ozna-
cza to, że trójwymiarowe aspekty obrazu są pomijane i biblioteka ma renderować
scenę w dwóch wymiarach (więcej o rzutowaniu dowiesz się dalej). Następnie
przywracamy domyślny stan macierzy . Na przykład po obróceniu kamery jej
nowa pozycja jest wyznaczana na podstawie wartości z wewnętrznych macierzy.
Przez przywrócenie wyjściowych wartości można przesunąć kamerę do począt-
kowej pozycji. Następnie wywołujemy metodę orthoOf, która nakazuje bibliotece
OpenGL ustawienie wartości macierzy w taki sposób, aby okno miało 320 pikseli
szerokości i 480 pikseli wysokości .
Dalej, w metodzie onSurfaceCreated, tworzymy potrzebny egzemplarz klasy
Triangle i przesłaniamy metodę onDrawFrame . Operacje związane z rysowa-
niem klatki są obsługiwane wewnętrznie w klasie Triangle, co pokazano na
wcześniejszym listingu.
OMÓWIENIE
Uzyskany dwuwymiarowy trójkąt może wydawać się nieciekawy, jest jednak
dobrym punktem wyjścia do nauki programowania z wykorzystaniem biblioteki
OpenGL ES. W ramach tworzenia trójkąta podaliśmy współrzędne, określiliśmy
wymiary powierzchni i rodzaj rzutowania, a także zaczęliśmy rysować figury.
Pamiętaj, że aby uzyskać maksymalną wydajność (i wykorzystać procesor gra-
ficzny, jeśli jest dostępny), warto stosować bibliotekę OpenGL nawet do rysowa-
nia obrazów w dwóch wymiarach. Następny krok to przeniesienie dwuwymiaro-
wego trójkąta w świat figur trójwymiarowych.

0 TECHNIKA 71. Tworzenie piramidy

Tu tworzymy obiekt trójwymiarowy. Oczywistym wyborem jest piramida. Składa


się ona z trzech ścian (na razie pomijamy podstawę), trzeba więc utworzyć trzy
trójkąty. Każda ze ścian ma być zielona; obiekt ma obracać się w przestrzeni.
Efekt działania kodu pokazano na rysunku 12.11.
Ponieważ piramida ma trzy ściany, do biblioteki OpenGL trzeba przekazać
więcej danych. Aby lepiej zrozumieć, jakie dane trzeba podać, przyjrzyj się
schematowi z rysunku 12.12.
Trzy ściany piramidy są zdefiniowane w następujący sposób:
Q ściana 1. — V1, V2, V3;
Q ściana 2. — V1, V3, V4;
Q ściana 3. — V1, V4, V2.
0 TECHNIKA 71. Tworzenie piramidy 505

Rysunek 12.11. Obracająca Rysunek 12.12. Piramida


się zielona trójwymiarowa piramida

Po ustaleniu wszystkich ścian trójwymiarowej bryły jesteśmy już prawie gotowi


do rozpoczęcia pisania kodu. Najpierw warto jednak pokrótce omówić rzuty i wy-
jaśnić, czym są sceny trójwymiarowe.
SCENA TRÓJWYMIAROWA

Przed przejściem do kodu piramidy trzeba omówić sceny trójwymiarowe.


Zacznijmy od schematu takiej sceny, przedstawionego na rysunku 12.13.

Rysunek 12.13.
Schemat sceny
trójwymiarowej

Schemat z rysunku 12.13 to typowa scena trójwymiarowa. Oko symbolizuje punkt


widzenia, a na okno rzutowane są wszystkie obiekty z bryły widzenia (ang. view
frustum). Bryła widzenia to piramida z dwoma płaszczyznami przycinania (ang.
clipping planes). Ponieważ trójwymiarowy świat może być wielki, w danym
506 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

momencie widać tylko jego część (wyobraź sobie gry FPS lub, jeszcze lepiej, rze-
czywisty świat). To ważne, ponieważ biblioteka OpenGL renderuje tylko obiekty
z bryły widzenia.
W trakcie renderowania obiektów biblioteka OpenGL oblicza, co ma zrzutować
na okno. Możesz wyobrażać sobie, że to okno na świat. Zrzutowany obraz świata
to płaski rysunek wyświetlany na powierzchni okna. Zwykle okno pokrywa się
z bliską płaszczyzną przycinania, ale nie zawsze tak jest. Zależy to od tego, jak
chcesz renderować świat.
W ten sposób wracamy do rzutowania. W bibliotece OpenGL stosuje się dwa
rodzaje rzutowania — perspektywiczne i ortogonalne. Opisano je w tabeli 12.1.
Tabela 12.1. Dwa typy rzutowania stosowane w bibliotece OpenGL

Rzutowanie Opis
Rzutowanie Używane w grafice trójwymiarowej. Odpowiada scenie, takiej jak na
perspektywiczne rysunku 12.13. Obiekty bardziej oddalone od widza wyglądają na mniejsze,
natomiast położone bliżej — na większe.
Rzutowanie Używane w grafice dwuwymiarowej. Obiekty w oknie mają pierwotną
ortogonalne wielkość (bez uwzględniania perspektywy).

Rzutowanie ortogonalne zastosowaliśmy w poprzednim przykładzie, dla dwuwy-


miarowego trójkąta. Dalej używamy rzutowania perspektywicznego dla trój-
wymiarowej piramidy. Na schemacie z rysunku 12.14 szczegółowo przedstawiono
ważne aspekty rzutowania perspektywicznego. Ustawienie współczynnika propor-
cji na wartość inną niż stosunek szerokości do wysokości okna powoduje rozcią-
gnięcie obrazu w pionie lub poziomie.

Rysunek 12.14.
Schemat rzutowania
perspektywicznego

Po przedstawieniu podstawowych informacji na temat scen trójwymiarowych


można wykorzystać rzutowanie perspektywiczne do utworzenia trójwymiarowego
obiektu w trójwymiarowym środowisku.
0 TECHNIKA 71. Tworzenie piramidy 507

PROBLEM
Chcemy utworzyć trójwymiarowy obiekt za pomocą rzutowania perspekty-
wicznego.
ROZWIĄZANIE
Aby zastosować rzutowanie perspektywiczne i utworzyć trójwymiarową per-
spektywę, należy zdefiniować nową klasę w podobny sposób jak wcześniej
dla trójkąta. W środowisku trójwymiarowym niezbędne są dodatkowe dane
i kilka nowych ustawień, jednak kod jest bardzo zbliżony. Przedstawiono go na
listingu 12.13.

Listing 12.13. Klasa Pyramid

public class Pyramid {

private FloatBuffer vertexBuffer;


private float vertices[] = {
0.0f, 1.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f,

0.0f, 1.0f, 0.0f,


0.0f, 0.0f, -1.0f,
1.0f, 0.0f, 0.0f,

0.0f, 1.0f, 0.0f,


1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
};

private float rotation = 0.1f;

public Pyramid() {
ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(vertices.length * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}

public void draw(GL10 gl) {


rotation += 0.3f;
gl.glRotatef(rotation, 0f, 1f, 0f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertices.length / 3);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
}

Także klasę Pyramid rozpoczynamy od dodania bufora FloatBuffer na współrzędne


wierzchołków i zdefiniowania wszystkich trójkątów w tablicy wierzchołków .
508 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Tak samo wygląda początkowy kod klasy Triangle, jednak tu potrzebnych jest
więcej danych, aby zdefiniować wszystkie ściany. Zauważ, że tu używamy liczb
zmiennoprzecinkowych zamiast wartości podawanych w pikselach. Wynika to
z zastosowania rzutowania perspektywicznego i względnych odległości. Jednostką
odległości jest wartość 1,0. Po wierzchołkach znajduje się parametr rotation .
Aplikacja rotuje piramidę, aby można było zobaczyć ją w trzech wymiarach.
Wartość parametru rotation określa kąt rotowania piramidy względem począt-
kowej pozycji.
Po początkowych wartościach znajduje się konstruktor, w którym ponownie
zapełniamy bufor na wierzchołki (w taki sam sposób jak wcześniej w klasie
Triangle) . Po konstruktorze znajduje się metoda draw, w której definiowane są
ustawienia związane z trójwymiarowością . W metodzie draw najpierw zwięk-
szamy rotację o 0,3 stopnia. Metoda draw jest wywoływana raz za razem, co ozna-
cza, że kąt ciągle będzie wzrastał. Nie przejmuj się przekroczeniem wartości
360°. Dla procesora graficznego 2893 to taka sama wartość jak 270. Dalej wywo-
łujemy metodę glRotatef, aby zrotować scenę wokół osi y (pionowej) o liczbę
stopni podaną w parametrze określającym kąt. Metoda glRotatef przyjmuje cztery
parametry. Pierwszy określa kąt rotacji sceny, a trzy pozostałe — współrzędne
x, y i z wektora, wyznaczające punkt, względem którego odbywa się rotacja. Wek-
tor (0, 1, 0) wskazuje w górę, tak więc rotacja wygląda tak, jakby piramida zwisała
na sznurku i obracała się wokół własnej osi. Następnie rysujemy trójkąty na
ekranie. Wymaga to użycia tablicy wierzchołków, ustawienia wskaźnika do bufora
i wywołania metody glDrawArrays. Rysowanie szczegółowo przedstawiliśmy dla
klasy Triangle z listingu 12.11. Tu stosujemy to samo podejście.
Aby wykorzystać klasę Pyramid, należy utworzyć nową klasę OpenGLPyramid
´Activity i ponownie napisać nową implementację interfejsu Renderer. Potrzebny
kod przedstawiono na listingu 12.14.

Listing 12.14. Klasa OpenGLPyramidActivity

public class OpenGLPyramidActivity extends Activity {

private GLSurfaceView glView;


private Pyramid pyramid;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
glView = new GLSurfaceView(this);
glView.setRenderer(new MyOpenGLRenderer());
setContentView(glView);
}

class MyOpenGLRenderer implements Renderer {


@Override
0 TECHNIKA 71. Tworzenie piramidy 509

public void onSurfaceChanged(GL10 gl, int width, int height) {


Log.d("MyOpenGLRenderer",
"Zmiana powierzchni. Szerokość=" + width + " Wysokość=" + height);
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 45.0f, (float) width / (float) height,
0.1f, 100.0f);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl,
EGLConfig config) {
Log.d("MyOpenGLRenderer", "Utworzono powierzchnię");
pyramid = new Pyramid();
}

@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 1f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, -10.0f);
pyramid.draw(gl);
}
}
}

W aktywności wprowadzono kilka zmian, jednak najważniejszą z nich jest


zmiana rzutowania z ortogonalnego na perspektywiczne. Ponieważ wkroczyli-
śmy w świat grafiki trójwymiarowej, chcemy, aby obiekty były wyświetlane
w trzech wymiarach.
Kod ponownie zaczyna się od ustawienia okna . Następnie włączamy macierz
rzutowania i przywracamy jej stan oraz ustawiamy perspektywę przy użyciu
dostępnych w Androidzie narzędzi GLU.
GLU to wzorowany na pakiecie GLUT zestaw narzędzi z wygodnymi me-
todami do manipulowania macierzami. Tu używamy metody gluPerspective
do skonfigurowania macierzy rzutowania za pomocą jednego polecenia . Przy-
pomnij sobie schemat i opis perspektywy. Pierwszy parametr to powierzchnia
używana przez bibliotekę, a drugi — pole widzenia. Pole widzenia jest wyrażane
w stopniach, określa kąt względem osi y i wyznacza początek bryły widzenia dla
tej osi. Im bliżej okna znajduje się kamera, tym więcej można zobaczyć (pole
widzenia rośnie). Trzeci parametr to współczynnik proporcji (szerokości do wyso-
kości). W telewizorze ten współczynnik to 16:9. Czwarty parametr wyznacza
bliższą płaszczyznę przycięcia i zawsze ma wartość dodatnią. Piąty i ostatni
parametr to dalsza płaszczyzna przycięcia. Elementy spoza tej płaszczyzny nie
są renderowane. Zauważ, że perspektywa jest konfigurowana przy użyciu arbi-
tralnych jednostek, które należy stosować w całej aplikacji. Rozdzielczość jest
510 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

ustalana na podstawie okna, a biblioteka OpenGL ES określa, ile pikseli trój-


kąt ma zajmować w gotowym obrazie. Proporcje są takie same dla każdej roz-
dzielczości.
Po określeniu perspektywy ustawiamy tryb macierzy modelu i z wyko-
rzystaniem metody glLoadIdentity przywracamy domyślne wartości macierzy.
Następnie w metodzie onSurfaceCreated przy każdej zmianie powierzchni two-
rzymy obiekt klasy Pyramid . Dalej znajduje się metoda onDrawFrame .
Metoda onDrawFrame opróżnia ekran, ponownie przywraca domyślne wartości
macierzy, a następnie za pomocą metody glTranslatef przesuwa obraz o 10 jed-
nostek. To nowa operacja. Pamiętaj, że początek układu współrzędnych znajduje
się w lewym dolnym rogu, przy czym oś z wskazuje w kierunku widza. Przesu-
nięcie o –10 na osi z oznacza przesunięcie o 10 jednostek w głąb obrazu. W ten
sposób określany jest punkt wyjścia do rysowania trójkątów. W świecie trój-
wymiarowym instrukcje dla biblioteki OpenGL wyglądają tak: przesuń się 5 stóp
(jednostek) w lewo od drzewa, narysuj jabłko, przesuń się o 10 stóp w kierunku
widza, narysuj coś innego itd. Po zakończeniu pracy należy wywołać metodę draw
obiektu klasy Pyramid.
OMÓWIENIE
Uruchomienie przedstawionej aktywności prowadzi do wyświetlenia wolno obra-
cającej się zielonej piramidy. Animacja nie wygląda specjalnie atrakcyjnie, jednak
utworzyliśmy pierwszy trójwymiarowy obiekt w pełni trójwymiarowym środowisku.
Teraz możemy utworzyć model dowolnych obiektów. Następny krok polega
na dodaniu koloru i tekstury do ścian.

0 TECHNIKA 72. Kolorowanie piramidy

Punktem wyjścia jest obracająca się trójwymiarowa figura. Trudno jest odróżnić
jej poszczególne ściany. Narzucającym się następnym krokiem jest uatrakcyj-
nienie piramidy przez dodanie różnych kolorów do każdej ściany. W tym celu
trzeba utworzyć nowy podstawowy obiekt o nazwie ColouredPyramid i nową aktyw-
ność (z rendererem) do jego wyświetlania. Efekt pokazano na rysunku 12.15.
Jak wspomnieliśmy, biblioteka OpenGL przyjmuje też inne dane obok współ-
rzędnych przestrzennych. W klasie ColouredPyramid trzeba również określić kolor,
a także poinformować bibliotekę OpenGL, gdzie może go znaleźć i jak ma go użyć.
PROBLEM
Chcemy pokolorować wierzchołki w trójwymiarowej figurze.
ROZWIĄZANIE
Aby określić kolory trójwymiarowej figury, można powiązać dane o barwach
z wierzchołkami. Piramida z przykładu składa się z trzech trójkątnych ścian
(dolną pomijamy) zdefiniowanych za pomocą wierzchołków. Na listingu 12.15
pokazano, jak do klasy ColouredPyramid dodać potrzebne dane o kolorze.
0 TECHNIKA 72. Kolorowanie piramidy 511

Rysunek 12.15. Obracająca się piramida


z różnokolorowymi ścianami

Listing 12.15. Klasa ColouredPyramid

public class ColouredPyramid {

private static final int VERTEX_SIZE = (3 + 4) * 4;


private FloatBuffer vertexBuffer;
private float vertices[] = {
0.0f, 1.0f, 0.0f, 1, 0, 0, 1,
-1.0f, 0.0f, 0.0f, 1, 0, 0, 1,
0.0f, 0.0f, -1.0f, 1, 0, 0, 1,

0.0f, 1.0f, 0.0f, 0, 1, 0, 1,


0.0f, 0.0f, -1.0f, 0, 1, 0, 1,
1.0f, 0.0f, 0.0f, 0, 1, 0, 1,

0.0f, 1.0f, 0.0f, 0, 0, 1, 1,


1.0f, 0.0f, 0.0f, 0, 0, 1, 1,
-1.0f, 0.0f, 0.0f, 0, 0, 1, 1,
};

private float rotation = 0.1f;

public ColouredPyramid() {
ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(VERTEX_SIZE * 3 * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}

public void draw(GL10 gl) {


rotation += 1.0f;
gl.glRotatef(rotation, 1f, 1f, 1f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
512 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

vertexBuffer.position(0);
gl.glVertexPointer(3, GL10.GL_FLOAT, VERTEX_SIZE, vertexBuffer);
vertexBuffer.position(3);
gl.glColorPointer(4, GL10.GL_FLOAT, VERTEX_SIZE, vertexBuffer);

gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3 * 3);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
}
}

Klasa ColouredPyramid nie różni się zanadto od pierwotnej klasy Pyramid, ale
przyjrzyjmy się jej kodowi . Nowa klasa rozpoczyna się od stałej określającej
liczbę bajtów alokowanych na wierzchołek. Tu wierzchołek składa się ze współ-
rzędnych x, y i z oraz czterech składowych wyznaczających kolor. Każda wartość
to liczba typu float, tak więc na każdą z siedmiu składowych należy przeznaczyć
cztery bajty. W tablicy wierzchołków znajdują się nieprzetworzone wartości.
Każdy wiersz tablicy obejmuje składowe (x, y, z, r, g, b, a). W kodzie znajdują
się wiersze dla ściany czerwonej , zielonej i niebieskiej .
Po skonfigurowaniu wszystkich ustawień dochodzimy do niezwykle ważnej
metody draw . Metoda ta rozpoczyna się od zwiększenia wartości zmiennej
rotation. W każdym cyklu renderowania wartość ta rośnie o 1 stopień. Dalej znaj-
duje się wywołanie metody glRotatef, która rotuje scenę względem wektora
współrzędnych (1, 1, 1). Następnie udostępniamy tablicę wierzchołków do ren-
derowania i tablicę kolorów. Potem ustawiamy pozycję na początek bufora
wierzchołków, gdzie zaczynają się współrzędne trójkątów.
Dalej następują operacje związane z rysowaniem. Wiersz gl.glVertexPointer
´(3, GL10.GL_FLOAT, VERTEX_SIZE, vertexBuffer); nakazuje bibliotece OpenGL
użycie bufora wierzchołków wyznaczających trójkąty. Przypominamy, że pierw-
szy parametr dotyczy tego, ile składowych określono dla każdego wierzchołka,
drugi — jakiego typu dane są używane, trzeci wyznacza przesunięcie, a czwarty
obejmuje dane wierzchołka. Tym razem przesunięcie (informujące bibliotekę
OpenGL o liczbie wartości między dwoma wierzchołkami) ma wartość VERTEX_
´SIZE. Trzeba pamiętać, że wskaźnik prowadzi do początku bufora wierzchołków.
Ustawiamy kursor bufora wierzchołków na pierwszy element pierwszej skła-
dowej koloru i informujemy bibliotekę OpenGL, że ma pobrać kolory z tego
samego bufora wierzchołków. Kolor jest określany przez cztery składowe (RGBA),
a przesunięcie wynosi VERTEX_SIZE, tak więc aby pobrać drugi kolor, trzeba
przeskoczyć o 28 bajtów (7·4). Zauważ, że wskaźnik prowadzi do czwartego
elementu tablicy (pozycja 3.), od którego rozpoczynają się składowe koloru.
Ostatecznie aplikacja renderuje trójkąty na ekranie za pomocą metody
glDrawArrays. Proces rozpoczyna się od pozycji 0, a aplikacja renderuje trzy trój-
kąty o trzech wierzchołkach każdy. Następnie wyłączamy dostęp do tablic wierz-
chołków i kolorów.
0 TECHNIKA 73. Dodawanie tekstury do piramid 513

Aktywność korzystająca z nowej klasy ColouredPyramid jest niemal identyczna


z aktywnością używającą pierwotnej klasy Pyramid (listing 12.14). Jedyna różnica
polega na tym, że trzeba utworzyć obiekt klasy ColouredPyramid zamiast obiektu
klasy Pyramid. Oto potrzebny kod:
private ColouredPyramid pyramid;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
pyramid = new ColouredPyramid();
}

Na końcowym ekranie widoczna powinna być obracająca się piramida z czerwoną,


zieloną i niebieską ścianą. Bryłę tę przedstawiono na rysunku 12.15.
OMÓWIENIE
Choć obracająca się, wielokolorowa piramida składa się — dosłownie i w prze-
nośni — z wielu części, powinieneś wiedzieć już, jak korzystać z biblioteki
OpenGL. Trzeba udostępnić nieprzetworzone dane w tablicach reprezentujących
wierzchołki figur i dokładnie poinformować bibliotekę OpenGL, gdzie ma szukać
poszczególnych informacji (gdzie są granice danych) i jak ma z nich korzystać.
Skoro już wiesz, jak rysować proste figury i nadawać im kolor, możesz dodat-
kowo dołączyć teksturę do powierzchni.

0 TECHNIKA 73. Dodawanie tekstury


do piramid

Wcześniej pokazaliśmy, jak korzystać z biblio-


teki OpenGL i określać kolor prostych figur.
Wyświetlanie rysunków na takich figurach wy-
maga tworzenia tekstur i odwzorowywania ich
na figury składające się na obraz.
Aby przedstawić działanie tego mechanizmu,
zamiast koloru umieszczamy na ścianach pira-
midy teksturę. W tym celu odwzorowujemy
fragment postaci z logo Androida na każdą
ścianę. Efekt przedstawiono na rysunku 12.16.
PROBLEM
Chcemy dodać teksturę do trójwymiarowych
brył, aby wzbogacić obraz o nowe elementy. Rysunek 12.16. Obracająca
się trójwymiarowa piramida
z teksturą
ROZWIĄZANIE
Tekstura to po prostu bitmapa. We wcześniej-
szej części rozdziału pokazaliśmy już, jak wczytywać bitmapy. Aby dodać szcze-
góły do bryły lub rysunku za pomocą biblioteki OpenGL, można umieścić bitmapy
na powierzchniach figur.
514 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

Zanim zaczniemy nakładać bitmapy na powierzchnie, chcemy wyjaśnić, jak


przebiega odwzorowywanie tekstur i dlaczego proces ten ma znaczenie. Bitmapy
są prostokątne. Kiedy odwzorowujemy kwadrat na trójkąt, musimy nakazać
bibliotece OpenGL wycięcie trójkątnego fragmentu bitmapy i nałożenie go na
trójkąt. Tak właśnie przebiega odwzorowywanie tekstur. Do tej pory używaliśmy
układu współrzędnych x, y i z. Bitmapy nie mają trzeciego wymiaru — używane
dla nich są tylko współrzędne x i y. Aby uniknąć pomyłek, dla tekstur używamy
liter u i v (lub s i t) zamiast x i y, jak pokazano na rysunku 12.17.
Litery u i v (lub s i t) zamiast x i y nie mają specjalnego znaczenia — to tylko
konwencja. W literaturze czasem używa się terminu odwzorowywanie UV (ang.
UV mapping). Na rysunku 12.17 można też zauważyć, że w bibliotece OpenGL ES
używane są współrzędne znormalizowane, takie jak w rzutowaniu perspekty-
wicznym. Początek układu to 0, a koniec to 1. Wartość 0,5 oznacza połowę.
Aby odwzorować fragment rysunku na trójkąt, należy wyciąć odpowiedni trój-
kątny kształt i powiązać jego współrzędne z poszczególnymi wierzchołkami, co
pokazano na rysunku 12.18.

Rysunek 12.17. Przykładowy Rysunek 12.18. Przykładowy


rysunek ze współrzędnymi tekstury wycinany trójkąt i wierzchołki,
na które jest odwzorowywany

Teraz, kiedy wiesz już, na czym polega odwzorowywanie tekstur, i poznałeś


podstawowe pojęcia, możemy przejść do nakładania bitmap na ściany piramidy.
W tym celu tworzymy nową klasę TexturedPyramid i nową aktywność do wyświe-
tlania piramidy.
Różnice między klasą TexturedPyramid a poprzednimi klasami omawiamy dalej.
Zaczynamy od przedstawienia klasy OpenGLTexturedPyramidActivity z rendererem.
To w tej klasie odbywa się tworzenie tekstury, co przedstawiono na listingu 12.16.

Listing 12.16. Klasa OpenGLTexturedPyramidActivity

public class OpenGLTexturedPyramidActivity extends Activity {

private GLSurfaceView glView;


0 TECHNIKA 73. Dodawanie tekstury do piramid 515

private TexturedPyramid pyramid;


private Bitmap texture;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
glView = new GLSurfaceView(this);
glView.setRenderer(new MyOpenGLRenderer());
setContentView(glView);
}

class MyOpenGLRenderer implements Renderer {

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 45.0f, (float) width / (float) height,
0.1f, 100.0f);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
texture = BitmapFactory.decodeResource(
getResources(), R.drawable.texture);
int textureIds[] = new int[1];
gl.glGenTextures(1, textureIds, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIds[0]);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
texture.recycle();
pyramid = new TexturedPyramid(textureIds[0]);
}

@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 1f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, 0.0f, -5.0f);
pyramid.draw(gl);
}
}
}

Na początku aktywności tworzymy zmienne składowe na obiekt przedstawionej


dalej klasy TexturedPyramid i na obiekt klasy Bitmap przechowujący teksturę .
516 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

W metodzie onSurfaceCreated przygotowujemy teksturę przez wczytanie zasobu


w postaci pliku texture.png . Plik ten jest dołączony do projektu i znajduje się
w katalogu res/drawable-nodpi.

Trzeba używać potęg dwójki!


Aby tekstury działały na wszystkich urządzeniach z Androidem, wymiary (szerokość
i wysokość) obrazów używanych jako tekstury muszą być potęgami dwójki. War-
tości te nie muszą być sobie równe, ale obie muszą być potęgami dwójki. Dopusz-
czalne są na przykład wymiary 32×256.

Po wczytaniu bitmapy konfigurujemy tablicę liczb całkowitych na wygenerowane


identyfikatory tekstur . Ponieważ używamy tylko jednej tekstury, tworzymy
jednoelementową tablicę. Po przygotowaniu tablicy należy wygenerować identy-
fikator tekstury, do czego służy metoda glGenTextures . Ponieważ biblioteka
OpenGL ES ma interfejs API napisany w natywnym języku C, zwraca identyfi-
kator, który pozwala wskazywać wygenerowaną teksturę. Pierwszy parametr
wspomnianej metody określa, ile tekstur należy wygenerować, drugi jest refe-
rencją do tablicy na wygenerowane identyfikatory, a ostatni reprezentuje miejsce,
od którego metoda ma zacząć wstawiać te identyfikatory.
Następny krok polega na powiązaniu tekstur, do czego służy metoda glBind-
Texture . Metoda glGenTextures tworzy nazwy (identyfikatory), natomiast metoda
glBindTexture nakazuje bibliotece OpenGL użycie tekstury o danym identyfi-
katorze, podawanym jako drugi parametr każdego wywołania. Pierwszy parametr
określa element, z którym wiązana jest tekstura. Jak może zauważyłeś, pierwszym
parametrem zawsze jest statyczna stała zdefiniowana w bibliotece OpenGL. Tu
używamy stałej GL_TEXTURE_2D.
Kiedy tekstury mają już nazwy i zostały powiązane, używamy metody texI-
mage2D z biblioteki GLUtils do wczytania grafiki z teksturą do pamięci VRAM .
Takie wczytywanie jest stosunkowo złożonym zadaniem. Na szczęście Android
udostępnia wspomnianą metodę pomocniczą. Jej pierwszy parametr to typ tek-
stury. Jest on taki sam jak używany przy jej tworzeniu (GL_TEXTURE_2D). Drugi
parametr to poziom mipmappingu. Parametr ten ignorujemy (ustawiamy go na 0).
Trzeci parametr określa wczytywaną bitmapę z teksturą (ignorujemy go rów-
nież — ustawiamy go na 0).
Po powiązaniu tekstury i wczytaniu jej do pamięci ustawiamy kilka filtrów .
Jeśli tekstura jest zbyt mała lub za duża, biblioteka musi rozciągnąć ją lub
zmniejszyć, aby dopasować ją do trójkąta. Tu definiujemy typ algorytmu używa-
nego do wykonywania tych operacji. Może zwróciłeś uwagę na sposób ustawiania
atrybutów maszyny stanowej biblioteki OpenGL. Pierwszy parametr określa
rodzaj tekstury. Tu jest to GL_TEXTURE_2D. Drugi parametr to atrybut, który trzeba
ustawić. W tym miejscu używamy filtrów do zmniejszania (TEXTURE_MIN_FILTER)
i powiększania (TEXTURE_MAG_FILTER) tekstury. Ostatni parametr ustawia algorytm
0 TECHNIKA 73. Dodawanie tekstury do piramid 517

najbliższego sąsiada (ang. nearest pixel). W niektórych konfiguracjach sprzętowych


można użyć lepszego algorytmu liniowego, który jednak zanadto obciąża wolniej-
sze urządzenia.
Następnie zrywamy dawne wiązanie tekstury przez powiązanie jej ze spe-
cjalnym identyfikatorem 0 . Przy późniejszym korzystaniu z tekstury można
ponownie odpowiednio ją powiązać. Usuwamy też (za pomocą metody recycle)
rysunek, który może zajmować dużo pamięci . Koniecznie trzeba wykonywać tę
operację, kiedy rysunek nie jest już potrzebny. Warto pamiętać, że tekstura jest
w tym momencie utworzona i przesłana do pamięci VRAM, a bitmapa jest tylko
przechowywaną na stercie kopią tekstury. Usunięcie bitmapy jest korzystne,
ponieważ urządzenia mają ograniczoną pamięć.
Dalej przesłaniamy metodę onDrawFrame i rysujemy elementy, tak jak w innych
przykładach. Renderer obsługuje teraz wczytywanie i wiązanie tekstury w czasie
tworzenia powierzchni.
Zgodnie z obietnicą do napisania pozostała tylko sama klasa TexturedPyramid.
Jej kod znajdziesz na listingu 12.17.

Listing 12.17. Klasa TexturedPyramid

public class TexturedPyramid {

private int textureId;


private FloatBuffer vertexBuffer;
private static final int VERTEX_SIZE = (3 + 2) * 4;
private float vertices[] = {

0.0f, 1.0f, 0.0f, 0.5f, 0.0f,


-1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, -1.0f, 1.0f, 1.0f,

0.0f, 1.0f, 0.0f, 0.5f, 0.0f,


0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f, 1.0f,

0.0f, 1.0f, 0.0f, 0.5f, 0.0f,


1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
};

private float rotation = 0.1f;

public TexturedPyramid(int textureId) {


ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(
TexturedPyramid.VERTEX_SIZE * 3 * 3);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
this.textureId = textureId;
}
518 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa

public void draw(GL10 gl) {


rotation += 1.0f;
gl.glRotatef(rotation, 1f, 1f, 1f);

gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

vertexBuffer.position(0);
gl.glVertexPointer(3, GL10.GL_FLOAT,
TexturedPyramid.VERTEX_SIZE, vertexBuffer);
vertexBuffer.position(3);
gl.glTexCoordPointer(2, GL10.GL_FLOAT,
TexturedPyramid.VERTEX_SIZE, vertexBuffer);

gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3 * 3);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}
}

Jak widać, klasa TexturedPyramid różni się pod kilkoma względami od klasy
ColouredPyramid. Na początku znajduje się zmienna na identyfikator tekstury
używanej dla piramidy . Identyfikator ten jest ustawiany w konstruktorze.
Następnie obliczamy nową wielkość wierzchołka z pięcioma komponentami .
Dalej znajdują się wierzchołki ze współrzędnymi u i v . W kodzie wiersze dla
poszczególnych wierzchołków są oddzielone od siebie. Trzy pierwsze wartości
to współrzędne x, y i z. Elementy czwarty i piąty to współrzędne u i v w znor-
malizowanym układzie współrzędnych tekstury.
Po danych (w tym współrzędnych u i v) znajduje się metoda draw , w której
włączana jest tekstura. Służy do tego metoda glEnable. Przyjmuje ona jeden para-
metr, określający włączaną teksturę, którą tu jest GL_TEXTURE_2D (warto zwró-
cić uwagę na ponowne wykorzystanie stałej z biblioteki OpenGL). Metoda
glBindTexture wiąże tę teksturę z identyfikatorem przekazanym jako parametr
konstruktora. Następnie udostępniamy tablice wierzchołków, obejmujące infor-
macje potrzebne do renderowania modelu i odwzorowywania stosowanej dla niego
tekstury. Dalej na potrzeby renderowania ustawiamy pozycję w buforze na 0,
określamy miejsce, w którym można znaleźć wierzchołki używane do rendero-
wania, i ustawiamy kursor na czwarty element (czyli na pierwszą współrzędną
tekstury). Odbywa się to podobnie jak w przykładzie z kolorową piramidą, jednak
tym razem instruujemy bibliotekę OpenGL, że ma użyć bufora wierzchołków
w celu pobrania współrzędnych u i v, aby zastosować teksturę. Przesunięcie jest
równe wartości stałej VERTEX_SIZE. W ostatnich krokach rysujemy trójkąty i wyłą-
czamy obsługę stanu klienta.
Po uruchomieniu nowej aktywności wyświetla się obracająca się piramida
ze ścianami z teksturą, co pokazano na rysunku 12.16.
12.3. Podsumowanie 519

OMÓWIENIE
Wiesz już, jak za pomocą biblioteki OpenGL odwzorowywać tekstury i je wyświe-
tlać. Informacje z tej techniki są oparte na wcześniejszych przykładach. Mamy
nadzieję, że opanowałeś już wzorzec korzystania z biblioteki OpenGL. Tylko
poruszyliśmy tu jej możliwości, przedstawiliśmy jednak podstawowe zagadnienia,
co pozwoli Ci zacząć rozwijać aplikacje atrakcyjne graficznie.

12.3. Podsumowanie
Rysowanie w Androidzie rozpoczyna się od dwuwymiarowych linii, łuków, pro-
stokątów, kółek, punktów i bitmap. Odbywa się ono z wykorzystaniem klasy Canvas
(płótna) i jej licznych metod draw. Aby określać kolory, zmieniać wymiary i sto-
sować efekty, należy użyć klasy Canvas razem z klasą Paint. W tym rozdziale
pokazaliśmy kilka przykładów z obszaru grafiki dwuwymiarowej — rysowanie
linii i figur, łączenie rysowania z tekstem i obrazkami, a także rysowanie nie-
standardowych widoków z efektami specjalnymi. Techniki te pozwalają wpro-
wadzić do aplikacji wiele nowych elementów. Ograniczeniem może być tylko brak
chęci programisty.
Klasa Canvas daje duże możliwości, jednak jest tylko wierzchołkiem pływają-
cego trójkątnego wielościanu. Za nią rozciąga się świat grafiki dwu- i trójwymia-
rowej opartej na bibliotece OpenGL ES. Jest to rozbudowana i efektywna
biblioteka graficzna, wykorzystująca procesor graficzny (jeśli jest dostępny). Jeżeli
chcesz zacząć tworzyć grafikę trójwymiarową, umożliwi Ci to biblioteka OpenGL.
Pozwala ona rysować figury, tworzyć animacje i wykonywać inne zadania. Tu
zaczęliśmy od czegoś prostego — od utworzenia dwuwymiarowego trójkąta.
Następnie zajęliśmy się grafiką trójwymiarową i utworzyliśmy obracającą się
piramidę, do której dodaliśmy kolory i tekstury. Połącz opisane tu techniki
z wcześniejszym omówieniem aktywności, kontrolek, widoków, usług i innych
mechanizmów, a przekonasz się, że możliwości są niemal nieograniczone.
W następnej części książki wychodzimy poza podstawy i główne komponenty,
na których koncentrowaliśmy się do tego miejsca. Masz już solidne podstawy.
Pora wyjść poza samo pisanie aplikacji i przejść do innych technik zarządzania
projektami oraz pracy z urządzeniami o niestandardowych ekranach. Oznacza to,
że teraz zajmiemy się testowaniem, instrumentacją, automatycznym budo-
waniem, ciągłą integracją oraz nowszymi interfejsami API i funkcjami związa-
nymi z tabletami.
520 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa
Część III
Poza standardowe
rozwiązania

T rzecia (i ostatnia) część książki Android w praktyce ma wzbogacić Twoją


wiedzę o programowaniu aplikacji na Android, a jednocześnie pomóc Ci zapo-
znać się z dojrzalszymi i bardziej niezawodnymi praktykami rozwijania opro-
gramowania. Rozdział 13. dotyczy testów. Zobaczysz, jak stosować różne testy
i frameworki testowe, a także poznasz ogólne praktyki testowania oprogramowania.
Rozdział 14. poświęcony jest budowaniu projektów na Android i zarządzaniu nimi.
Opisujemy w nim pracę z narzędziami do budowania aplikacji, takimi jak Ant
i Maven, oraz pokazujemy, w jaki sposób stosować ciągłą integrację androidowych
projektów. Rozdział 15. kończy książkę. Przedstawiono w nim wybrane nowe
fragmenty interfejsu API Androida, opracowane pod kątem tabletów. Z tego roz-
działu dowiesz się, jak korzystać z funkcji przeciągania, paska akcji i fragmentów.
Testowanie
i instrumentacja

W tym rozdziale
Q Testy jednostkowe
Q Testy aktywności pod kątem instrumentacji
Q Atrapy obiektów i namiastki
Q Testowanie danych wejściowych za pomocą
narzędzia Monkey

Nocą przemierzam ścieżki, o których inni boją się mówić za dnia.


Patrick Rothfuss, „Imię wiatru”
Testy to jeden z tematów, które budzą gorące spory między programistami. Choć
testy powszechnie traktuje się jako zadanie ściśle związane z rozwijaniem opro-
gramowania (a nie jako pracę dla ludzi z działu kontroli jakości), wielu progra-
mistów stara się uniknąć ich wszelkimi sposobami. Jeśli nie należysz do tej grupy,
możesz pominąć ten dydaktyczny wstęp.
Z czego wynika niechęć do testów? Według nas przyczyny są dwie — igno-
rancja i arogancja. Ignorancja najczęściej cechuje programistów, którzy nie
pracowali w środowisku, gdzie stosowano podejście TDD (ang. test-driven deve-
lopment), lub którzy nie znają zalet metod testowania. Niestety, programiści apli-
kacji mobilnych często należą do tej grupy. Nawet jeśli znają potrzebne techniki,
nie doceniają wartości testowania oprogramowania, dlatego uznają pisanie testów
za niedogodność. Wiedzą, że powinni je pisać, ale akurat nie mają na to ochoty.

523
524 ROZDZIAŁ 13. Testowanie i instrumentacja

W końcu trzeba napisać odpowiednie funkcje i poinformować przełożonych


o postępach, po co więc tracić czas na pisanie testów, prawda?
Drugim powodem jest arogancja. Programiści mają o sobie wysokie mnie-
manie. Większość z nich uważa, że ich kod jest szybszy, bardziej pomysłowo
napisany, stabilniejszy i elegantszy niż kod innych programistów. A co najważ-
niejsze, jest też pozbawiony błędów. Po co więc pisać kod do testowania kodu,
który z założenia jest już doskonały? Tak się tylko wydaje.
Wszystko to prowadzi do kluczowego pytania: „Dlaczego przejmować się
testami?”. Otóż dlatego, że pewnego dnia się to opłaci. Być może stanie się to szyb-
ciej, niż podejrzewasz. Po opracowaniu odpowiedniego zestawu testów możesz
uruchamiać go na serwerze budowania każdorazowo po przesłaniu nowego kodu.
Umożliwia to szybkie wykrycie problemów i naprawienie błędów, co ostatecznie
prowadzi do poprawy jakości produktu. Niektórzy programiści idą o krok dalej
i stosują podejście TDD, w którym testy są przygotowywane przed napisaniem
właściwego kodu. Praca zaczyna się więc od wymagań funkcjonalnych (specyfi-
kacji lub kontraktu) sformułowanych jako zadanie testowe. Początkowo kod nie
może go wykonać. Następnie programista pisze i modyfikuje kod, tak aby wyko-
nywał zadanie. Pozwala to opracować logikę aplikacji (a nawet interfejsy) na
podstawie zestawu testów określających, jak aplikacja ma działać. Każdy błąd
wywołany przez przesłanie nowego kodu jest automatycznie wykrywany przy
kolejnym uruchomieniu zestawu testów (wspomnieliśmy już, że serwery budowa-
nia mogą automatycznie przeprowadzać takie testy; opisujemy to w rozdziale 14.).
To prawda, pisanie testów wymaga czasu i wysiłku. Trzeba nieustannie pamię-
tać o dwóch kwestiach — pisaniu poprawnych testów i testowaniu odpowied-
nich rzeczy. Oznacza to, że trzeba ustalić, co ma być testowane, i upewnić się,
że testy rzeczywiście to robią. „Zielone światło” w serwerze budowania może
zapewniać poczucie bezpieczeństwa, które jednak bywa złudne!
Gorąco zachęcamy do testowania pracy programistów. Kiedy zaczniesz ruty-
nowo stosować testy, będziesz czuł się niekomfortowo, gdy zdarzy Ci się dodać
do oprogramowania fragment nieprzetestowanego kodu. Komfort może być naj-
ważniejszym czynnikiem motywującym do pisania testów. Kiedy stwierdzisz,
że dodałeś kod, który prowadzi do awarii budowanej wersji, nie czuj się winny.
Niech pocieszy Cię myśl, że gdyby nie testy, błąd mógłby znaleźć się w wersji
produkcyjnej. Nie bądź ignorantem ani nie bądź arogancki. Dobrzy programiści
piszą testy.
Ta książka dotyczy jednak Androida, a nie testów. Jeśli nadal nie jesteś prze-
konany, że warto je pisać, zachęcamy do zapoznania się z książkami na temat
testów i podejścia TDD. Dostępnych jest wiele takich pozycji, co po części wynika
z dużego wzrostu popularności metod programowania zwinnego w ostatnich latach.
Zakończmy już ten motywujący wstęp. Oto, co obejmuje ten rozdział. W pierw-
szym podrozdziale opisujemy podstawy związane z tym, co i jak testować w apli-
kacjach na Android. Dalej koncentrujemy się na najważniejszych aspektach
13.1 Testowanie aplikacji na Android 525

testów. Wyjaśniamy, jak używać androidowego frameworku do instrumentacji


do pisania testów interfejsu, a także jak tworzyć eleganckie i treściwe testy z wyko-
rzystaniem języków DSL (ang. domain-specific languages — „języki dziedzinowe”,
czyli specjalizowane, dedykowane). W końcowej części rozdziału przechodzimy
do bardziej zaawansowanych technik testowania. Pokazujemy, jak używać atrap
obiektów w celu zmniejszenia powiązań między testami, jak w niestandardowy
sposób testować androidowe interfejsy użytkownika, a nawet jak przeprowadzać
testy obciążeniowe za pomocą testerów interfejsu użytkownika.
Możliwe, że w czasie przeglądania spisu treści zwróciłeś uwagę, iż rozdział
ten jest dłuższy od pozostałych. Zapewniamy, że nie zamierzamy wchodzić
w zbędne szczegóły. Uważamy jednak, że testowanie aplikacji na Android to
temat, któremu poświęca się zbyt mało uwagi. Pisząc ten rozdział, chcieliśmy
pomóc Ci dokładnie zrozumieć omawiane zagadnienia i nie ograniczać się do
powielania oficjalnej dokumentacji ze strony http://mng.bz/TM2V. Przygotuj się
więc na długą podróż. Obiecujemy jednak, że warto ją odbyć.

13.1 Testowanie aplikacji na Android


Ten podrozdział zaczynamy od ogólnego przedstawienia możliwości frameworku
Androida. Odpowiadamy tu na pytania, jakiego rodzaju testy można pisać dla
aplikacji na Android, w jaki sposób przygotować zestaw testów projektu i jak
implementowane są testy na Android. Po przedstawieniu wstępnych informacji
pokazujemy, jak w środowisku Eclipse przygotować projekt testowy aplikacji.
Wyjaśniamy też strukturę i sposób przeprowadzania testów, a następnie podsu-
mowujemy informacje w praktyce. W pierwszej technice w tym rozdziale piszemy
prosty test jednostkowy androidowej klasy Application.

13.1.1. Sposoby testowania aplikacji na Android


Istnieje wiele technik testowania oprogramowania i sposobów kategoryzowania
testów. Ponieważ jest to książka o programowaniu, koncentrujemy się tu na testach
pisanych i przeprowadzanych przez programistów. Android obsługuje dwa rodzaje
takich testów (jednostkowe i funkcjonalne) oraz udostępnia dwa sposoby ich
uruchamiania (w maszynie wirtualnej Javy oraz w emulatorze lub urządzeniu).
TESTY JEDNOSTKOWE
Testy jednostkowe służą do testowania odrębnych jednostek kodu, zwykle klas.
W idealnych warunkach test jednostkowy bada pracę tylko sprawdzanej jednostki,
a nie powiązanego z nią kodu. Dzięki temu test jest precyzyjny i niepodatny na
niepożądane efekty uboczne wywoływane przez kod, który nie jest sprawdzany.
Niemal słyszymy, jak domagasz się przykładu. Oto on. W aplikacji DealDroid
z rozdziału 2. działają dwie aktywności. Aktywność DealList tworzy główny ekran
aplikacji, a aktywność DealDetails jest wyświetlana po kliknięciu oferty na ekra-
nie aktywności DealList. Aktywność DealList można uruchomić tylko z poziomu
526 ROZDZIAŁ 13. Testowanie i instrumentacja

aktywności DealDetails, tak więc aktywność DealDetails zależy od aktywności


DealList. W czasie pisania testu jednostkowego dla klasy dowolnej z tych aktyw-
ności należy sprawdzać tylko funkcje właściwe dla danej klasy. Zadaniem aktyw-
ności DealList jest wyświetlanie listy ofert, a aktywności DealDetails — infor-
macji o ofertach. Jeśli nie będziesz przestrzegał tej reguły, klasa DealList może nie
przejść testu, choć błąd znajduje się w klasie DealDetails (lub na odwrót). Dobrą
praktyką jest izolowanie testów za pomocą testów jednostkowych. Podejście to
pokazano na rysunku 13.1.

Rysunek 13.1. Testy jednostkowe pozwalają skoncentrować się na konkretnym


fragmencie aplikacji niezależnie od innych jednostek kodu. Na rysunku pokazano
dwie jednostki, DealList i DealDetails, które — choć ściśle powiązane — są testowane
niezależnie od siebie

Niestety, odizolowanie jednostek kodu od siebie bywa zaskakująco trudne.


Pomocne są nieraz atrapy obiektów (więcej na ich temat dowiesz się z podroz-
działu 13.3), jednak żeby umożliwić sobie prawidłowe testowanie klas za pomocą
testów jednostkowych, warto już w trakcie pisania klas pomyśleć o testach. Podej-
ście TDD pomaga w projektowaniu luźno powiązanych klas, co ułatwia ich nie-
zależne testowanie. Można o tym napisać całą książkę, nie odbiegajmy jednak
zanadto od tematu. W technice 75. piszemy test jednostkowy klasy DealDetails,
więc dobrze się przygotuj. Czeka Cię ćwiczenie programistyczne.
TESTY FUNKCJONALNE
Używamy tu określenia testy funkcjonalne, ponieważ taka właśnie nazwa jest
używana w dokumentacji Androida dla testów opisywanego tu rodzaju. Przy-
znajemy jednak, że określenie to jest mylące, ponieważ także testy jednost-
13.1 Testowanie aplikacji na Android 527

kowe mogą być testami funkcjonalnymi (do sprawdzania jednej konkretnej


funkcji). Ogólnie każdy test sprawdzający, czy pewna funkcja działa zgodnie
z określoną specyfikacją, jest testem funkcjonalnym — w odróżnieniu od testów
właściwości, które dotyczą różnych od funkcji cech oprogramowania, na przykład
szybkości i skalowalności. My zamiast używanej w kontekście Androida nazwy
„testy funkcjonalne” wolimy określenie story test (czyli testy scenariuszy), ponie-
waż testy funkcjonalne w Androidzie służą głównie do implementowania testów
odpowiadających scenariuszom zadań wykonywanych przez użytkowników.
Testy funkcjonalne w Androidzie umożliwiają sprawdzanie działania kilku
jednostek kodu (zwykle aktywności) z aplikacji i przeprowadzenie testów kom-
pleksowych. Pod tym względem są przeciwieństwem testów jednostkowych,
ponieważ jednostki kodu w testach funkcjonalnych nie działają niezależnie od
siebie, ale wchodzą ze sobą w interakcje. Ten sposób testowania przedstawiono
na rysunku 13.2.

Rysunek 13.2. Testy funkcjonalne w Androidzie umożliwiają sprawdzanie scenariuszy


obejmujących różne jednostki kodu. Testy te mogą badać na przykład przechodzenie
między ekranami w reakcji na kliknięcie elementu listy

Sprawdzanie aplikacji za pomocą testów jednostkowych daje duże możliwości,


ponieważ pozwala przekształcać proces pracy użytkownika bezpośrednio na zestaw
testów, które badają, czy aplikacja działa w oczekiwany sposób. Jeśli to dla Ciebie
za mało, ciekawe jest też obserwowanie automatycznych zmian ekranów, kiedy
emulator Androida przeprowadza jeden z testów funkcjonalnych.
Oprócz podziału testów na jednostkowe i funkcjonalne warto też przedstawić
dwa zasadniczo odmienne sposoby ich przeprowadzania. Są to metoda standardowa
528 ROZDZIAŁ 13. Testowanie i instrumentacja

(oparta na Javie) i metoda z wykorzystaniem infrastruktury Androida służącej do


instrumentacji. Oba podejścia mają wady i zalety wpływające na tworzenie pro-
jektów testowych. Zobaczmy, czym się różnią te metody.
Testowanie oparte na Javie
Przeprowadzanie testów w sposób typowy dla Javy polega na uruchamianiu testów
w standardowej maszynie JVM (nie w Dalviku), tak jak uruchamiane są zwykłe
aplikacje Javy. Takie podejście ma określone wady i zalety. Oto zalety:
Q Szybkość. Testy przebiegają szybciej niż testy pod kątem instrumentacji,
ponieważ nie trzeba najpierw przesyłać kodu testów na emulator lub
urządzenie.
Q Elastyczność. Nie trzeba polegać na środowisku uruchomieniowym
Androida, dlatego można swobodnie korzystać z dowolnego frameworku
testowego, takiego jak JUnit 4 lub TestNG (stosowanie frameworku JUnit
w Androidzie omawiamy dalej).
Q Atrapy obiektów. Można korzystać z zaawansowanych bibliotek atrap
obiektów, w tym z rozwiązań opartych na manipulowaniu kodem bajtowym,
które nie działają w Dalviku.
Najważniejszą wadą wykonywania kodu testów na standardowej maszynie JVM
jest brak dostępu do klas frameworku Androida. Wywołanie dowolnej metody
z pliku android.jar użytej przy kompilacji programu (i kodu testów) prowadzi
do zgłoszenia wyjątku RuntimeException (wyjaśnienie tego zjawiska znajdziesz
w ramce „Pomocy! Jestem tylko namiastką!”). Sprawia to, że bez dodatkowej pracy
przeprowadzanie testów bezpośrednio lub pośrednio korzystających z klas fra-
meworku Androida jest na standardowej maszynie JVM niemożliwe, ponieważ każdy
taki test kończy się błędem czasu wykonania. W technice 79. pokazujemy, jak
rozwiązać ten problem za pomocą doskonałych bibliotek niezależnych producen-
tów. Jednak przeprowadzanie testów na maszynie JVM jest w pełni akceptowal-
nym rozwiązaniem przy testowaniu kodu, który nie korzysta z klas frameworku
Androida. Przykładowym kodem tego rodzaju jest napisany wcześniej generator
liczb losowych.
TESTOWANIE W SPOSÓB CHARAKTERYSTYCZNY DLA ANDROIDA
Drugi (i w kontekście Androida preferowany) sposób przeprowadzania testów
polega na uruchamianiu ich bezpośrednio w urządzeniu. Wymaga to wykonania
wielu dodatkowych operacji, ponieważ kod aplikacji i testu trzeba najpierw
umieścić w urządzeniu. Technika ta jest więc znacznie wolniejsza. Jednak testy
mają pełny dostęp do funkcji Androida (taki sam jak zwykłe aplikacje na Android).
Może to być ważną zaletą, ponieważ pozwala uruchomić niemal dowolny test —
zarówno zależny od platformy, jak i nie. Trzeba jednak uwzględnić pewne wady.
Oto one:
13.1 Testowanie aplikacji na Android 529

Pomocy! Jestem tylko namiastką!


Może zauważyłeś, że plik biblioteczny android.jar dołączany do androidowych pro-
jektów nie obejmuje kodu frameworku Androida. Jeśli otworzysz bibliotekę i przyj-
rzysz się plikom klas, zobaczysz, że każda metoda jest zaimplementowana tak:
throw new RuntimeException("Stub!");
Nie wydaje się to zbyt przydatne. Do czego więc służy ten kod? Powód jest pro-
sty — aplikacje na Android działają w urządzeniach lub emulatorach, w których
biblioteka uruchomieniowa jest częścią obrazu systemu. Plik android.jar ze środo-
wiska Eclipse nie jest rozpowszechniany razem z aplikacjami. Aby skompilować
aplikację na Android, kompilator nie potrzebuje dostępu do treści metod. Konieczne
są tylko sygnatury typów, publiczne składowe itd. Pominięcie implementacji metod
pozwala znacznie zmniejszyć wielkość pliku JAR z pakietu SDK, a jednocześnie wia-
domo, że aplikacja używa tylko tych klas i interfejsów, które są dostępne w urzą-
dzeniu z określoną wersją Androida.

Q Wolne działanie. Umieszczanie testów w emulatorze lub urządzeniu to


długi proces, dlatego rozwiązanie to nie jest skuteczne w podejściu TDD,
gdzie testy mają być szybkie.
Q Ograniczona liczba dostępnych technologii. Uruchamianie testów w Dalviku
sprawia, że trudniej jest testować biblioteki. Jest tak, ponieważ Android
obsługuje tylko dość stary framework JUnit 3. Nie można więc używać
bibliotek atrap, w których wykorzystuje się manipulacje kodem bajtowym
w czasie wykonywania programów w maszynie JVM.
Prawdopodobnie jednak większość testów będziesz pisał właśnie w ramach tego
rozwiązania, ponieważ wówczas lepiej współdziałają one z frameworkiem. Także
zespół z firmy Google rozwijający platformę Android pisze testy w ten sposób.
Podejście to dobrze nadaje się do tworzenia testów interfejsu użytkownika
(obejmujących aktywności).

13.1.2 Zarządzanie testami


Wiesz już, jakiego rodzaju testy możesz pisać. Następny krok to przygotowanie
środowiska dla testów. Testy warto przechowywać niezależnie od projektu apli-
kacji. Pozwala to oddzielić je od kodu produkcyjnego i uniknąć rozpowszechniania
kodu testów razem z aplikacją (Android umieszcza w pliku APK cały kod źró-
dłowy z katalogu projektu — także kod testów). Dlatego w Eclipse obok projektu
aplikacji należy utworzyć odrębny projekt testowy. Jego nazwa powinna kończyć
się członem *Test, na przykład DealDroidTest dla projektu aplikacji DealDroid.
POBIERZ PROJEKT DEALDROIDTEST. Kod źródłowy projektu i pakiet
APK do uruchamiania aplikacji znajdziesz w witrynie z kodem do książki
Android w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skon-
centrować się na konkretnych zagadnieniach, zalecamy pobranie komplet-
nego kodu źródłowego i śledzenie go w Eclipse (lub innym środowisku
IDE albo edytorze tekstu).
530 ROZDZIAŁ 13. Testowanie i instrumentacja

Zauważ, że na potrzeby tego rozdziału tworzymy odgałęzienie aplikacji


DealDroid, w którym zmieniamy widoczność kilku składowych klasy (dzięki
temu zadanie testowe ma dostęp do tych składowych). Wprowadzamy też
nową funkcję eksportu, którą testujemy. Ponieważ ten rozdział dotyczy
głównie kodu testów, a nie aplikacji, znajdziesz tu tylko nieliczne pliki
APK do pobrania.
Kod źródłowy: http://mng.bz/Zk0O.
Pomocna jest wtyczka ADT dla środowiska Eclipse, która udostępnia specjalny
kreator do tworzenia projektów testowych. Kreator można uruchomić z menu
środowiska Eclipse; wystarczy wybrać polecenie File/New/Other, a następnie opcję
Android Test Project z kategorii Android (rysunek 13.3). Zróbmy to i utwórzmy
projekt testowy aplikacji DealDroid.

Rysunek 13.3. Aby utworzyć


nowy projekt testowy
aplikacji na Android,
uruchom kreator;
w tym celu wybierz
opcję File/New/Other,
a następnie Android
Test Project

Gdy klikniesz przycisk Next, pojawi się formularz ustawień nowego projektu
testowego. Oprócz standardowego zbioru ustawień, obejmującego na przykład
nazwę i katalog na przestrzeń roboczą, dostępne jest nowe, dedykowane dla testów
ustawienie — testowany projekt (pole Test target). Tu jest nim aplikacja DealDroid,
dlatego należy wskazać ją w przeglądarce plików. Zauważ też, że kreator umiesz-
cza kod testów w pakiecie test, a jako pakiet nadrzędny ustawia nazwę testo-
wanej aplikacji. Na rysunku 13.4 pokazano, jak wygląda wypełniony formularz
kreatora projektu DealDroidTest.
Gdy klikniesz przycisk Finish, w przestrzeni roboczej pojawi się nowy projekt
testowy. Wygląda on jak zwykły projekt aplikacji na Android. Co jest w nim wyjąt-
kowego? Od zwykłych projektów różni się w bardzo małym stopniu. Ten sam efekt
można uzyskać przez uruchomienie standardowego kreatora projektów aplikacji
na Android (lub polecenia android create project) oraz samodzielne wykonanie
kilku operacji, za które odpowiada kreator projektów testowych. Oto te operacje:
13.1 Testowanie aplikacji na Android 531

Rysunek 13.4. Dostępny


we wtyczce ADT kreator
do tworzenia projektów
testowych ustawia kilka
domyślnych wartości,
na przykład nazwę
pakietu Javy. Upewnij
się, że dla projektów
testowych ustawiona
jest ta sama docelowa
wersja Androida
co dla sprawdzanej
aplikacji

Q Dodanie testowanej aplikacji do ścieżki budowania i referencji projektu.


Q Określenie w manifeście, że Android ma uruchamiać dany projekt jako
testy.
Ciekawy jest drugi z tych punktów. Przyjrzyjmy się plikowi AndroidManifest.xml
wygenerowanemu przez kreator projektów testowych z wtyczki ADT:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.dealdroid.test" ...>
<application>
<uses-library android:name="android.test.runner" />
</application>
<uses-sdk android:minSdkVersion="4" />
<instrumentation android:targetPackage="com.manning.aip.dealdroid"
android:name="android.test.InstrumentationTestRunner" />
</manifest>

Jak widać, dowiązujemy współużytkowaną bibliotekę (android.test.runner). Jest


tak, ponieważ w kodzie znajduje się element instrumentation, określający mecha-
nizm uruchamiania testów zdefiniowany w tej bibliotece. Więcej o instrumen-
tacji dowiesz się z podrozdziału 13.2. Na razie zapamiętaj, że w kodzie podajemy
532 ROZDZIAŁ 13. Testowanie i instrumentacja

mechanizm odpowiedzialny za wykonywanie zestawu testów, kiedy aplikacja


testowa jest uruchamiana w urządzeniu lub emulatorze.
Oprócz podanych różnic struktura projektu testowego jest taka sama jak
w zwykłych projektach aplikacji na Android. Ponieważ projekt testowy sam jest
aplikacją, kod testów to główny kod źródłowy zapisywany w katalogu src. Kod ten
możesz podzielić na mniejsze pakiety. Jeśli obok zwykłych androidowych testów
zamierzasz pisać klasyczne testy działające na maszynie JVM, prawdopodobnie
warto umieścić te ostatnie w odrębnym pakiecie lub katalogu na kod źródłowy,
tak aby podział testów był widoczny w strukturze projektu testowego.

13.1.3. Pisanie i uruchamianie testów


Może zauważyłeś, że projekt testowy nadal jest pusty. Zmieńmy to i napiszmy
prosty test. Android obejmuje framework do testów jednostkowych JUnit 3, dla-
tego zwykle wszystkie testy są pisane właśnie dla tego frameworku. Jeśli danych
testów nie zamierzasz uruchamiać w urządzeniu, możesz dowiązać dowolny fra-
mework testowy jako archiwum JAR. Jednak w testach uruchamianych w urzą-
dzeniu należy używać klas frameworku JUnit wbudowanego w Android, ponie-
waż wszystkie specjalne klasy testów z Androida dziedziczą po klasie TestCase
z JUnit 3. W tej książce omawiamy tylko podstawy frameworku testowego JUnit.
W JUnit testy są łączone w zadania testowe (ang. test cases). Każde zadanie
obejmuje testy przeprowadzane na sprawdzanej jednostce, którą zwykle jest
klasa z właściwej aplikacji. Aby utworzyć zadanie, należy napisać klasę pochodną
od junit.framework.TestCase i umieścić w niej wszystkie testy. Oto przykład:
import junit.framework.TestCase;

public class MyTestCase extends TestCase {

public void testTruth() {


assertTrue(true);
}
}

To zadanie testowe obejmuje jeden test, który sprawdza, czy wartość true ma
wartość true. Test ten jest bezużyteczny, ponieważ zawsze kończy się powodze-
niem, jednak pozwala wyjaśnić strukturę zadań testowych w JUnit. Nie każda
metoda z zadania testowego musi być testem. Tylko metody z przedrostkiem test*
(na przykład testTruth) są wykonywane jako testy w czasie działania programu.
JUnit identyfikuje wówczas te metody z użyciem mechanizmu refleksji. Pozo-
stałe metody nie są wywoływane, jeśli programista bezpośrednio ich nie uruchomi.
Do tworzenia asercji w testach w JUnit służą metody pomocnicze assert*
(na przykład assertTrue). JUnit udostępnia wiele takich metod, na przykład
assertNotNull, assertEquals itd. Także Android ma kilka takich metod w klasie
pomocniczej MoreAsserts. Za pomocą tych prostych cegiełek można przygotować
cały zestaw testów.
0 TECHNIKA 74. Prosty test jednostkowy aplikacji na Android 533

W jaki sposób testy są wykonywane? Wspomnieliśmy wcześniej, że stosuje


się dwa podejścia — zwykłe testy Javy (na maszynie JVM) lub testy Androida
(w urządzeniu). Gdy klikniesz projekt testowy (lub dowolną klasę z testami otwartą
w edytorze Eclipse) prawym przyciskiem myszy, będziesz mógł wybrać opcję
Run As/JUnit Test lub Run As/Android JUnit Test. Przedstawiony wcześniej frag-
ment kodu jest niezależny od frameworku Androida, dlatego można wybrać
dowolną z tych opcji. W dalszej części rozdziału zakładamy, że testy są uru-
chamiane w sposób właściwy dla Androida, w urządzeniu (chyba że piszemy
wprost, iż jest inaczej). Wynik testu pojawia się w widoku JUnit środowiska Eclipse,
otwieranym automatycznie w czasie przeprowadzania testów. Widok ten poka-
zano na rysunku 13.5.

Rysunek 13.5. Standardowy wynik testu JUnit w środowisku Eclipse. Widać tu,
jakie testy przeprowadzono, a także które zakończyły się sukcesem lub
niepowodzeniem albo nie zostały ukończone z powodu błędu

To już koniec omawiania podstaw. Jeśli udało nam się napisać dobre wprowa-
dzenie, wiesz już, dlaczego powinieneś tworzyć testy, jakiego rodzaju testy można
stosować, jak konfigurować projekty testowe, a nawet jak napisać i uruchomić
proste testy JUnit. Mamy nadzieję, że chcesz dowiedzieć się czegoś więcej!
Przejdźmy dalej i napiszmy praktyczny test. Ponieważ już przygotowaliśmy
odpowiedni projekt, w dalszej części rozdziału testujemy aplikację DealDroid.
Zaczynamy od techniki 74., w której piszemy prosty test jednostkowy aplikacji
za pomocą androidowej klasy ApplicationTestCase.

0 TECHNIKA 74. Prosty test jednostkowy aplikacji na Android

Pora zakasać rękawy i napisać pierwszy test aplikacji na Android. Wiesz już, że
każdemu zadaniu testowemu w Androidzie odpowiada klasa TestCase frame-
worku JUnit. W Androidzie są mechanizmy rozbudowujące framework JUnit,
dlatego dostępne są różne rodzaje zadań testowych, określone w hierarchii klas
opartej na klasie bazowej TestCase. Na rysunku 13.6 pokazano hierarchię różnego
rodzaju zadań testowych dostępnych w Androidzie.
Nie omówiliśmy jeszcze kodu związanego z instrumentacją, dlatego najpierw
koncentrujemy się na lewej gałęzi, wychodzącej z klasy TestCase na rysunku 13.6
(kod do instrumentacji opisujemy w technice 75.). Jak widać, w tym obszarze
Android udostępnia trzy rodzaje zadań testowych. Oto one:
534 ROZDZIAŁ 13. Testowanie i instrumentacja

Rysunek 13.6. Wszystkie zadania testowe z Androida dziedziczą po klasie TestCase


frameworku JUnit. W Androidzie testy podzielone są na wymagające (prawe
poddrzewo) i niewymagające (lewe poddrzewo) dostępu do mechanizmów
instrumentacji

Q ApplicationTestCase. Służy do testowania aplikacji (klasy


android.app.Application).
Q ServiceTestCase. Służy do testowania usług (klasy android.app.Service).
Q ProviderTestCase2. Służy do testowania dostawców treści (klasy
android.app.ContentProvider). Jest to zmodyfikowana wersja starszej
klasy ProviderTestCase (uznawanej za przestarzałą).
Wszystkie trzy klasy dziedziczą po klasie AndroidTestCase, która obejmuje kilka
metod pomocniczych, na przykład niestandardowe asercje do testowania, czy
ustawione są określone uprawnienia. Klasa ta udostępnia nawet metodę do dołą-
czania niestandardowych obiektów typu Context, które staną się istotne przy
omawianiu atrap w technice 71. Klasa AndroidTestCase różni się od klasy
InstrumentationTestCase tym, że nie udostępnia instrumentacji. Z podrozdziału 13.2
dowiesz się więcej na ten temat. Do przeprowadzania testów jednostkowych na
obiektach aplikacji, usługach i dostawcach treści instrumentacja nie jest potrzebna,
dlatego możemy przejść dalej.
PROBLEM
Piszemy niestandardową klasę aplikacji, dostawcę treści lub usługę i chcemy
testować dany element w kontrolowany sposób (w izolacji od innych komponen-
tów aplikacji) za pomocą testów jednostkowych.
ROZWIĄZANIE
Choć klasy ApplicationTestCase, ServiceTestCase i ProviderTestCase2 mają specjalne
metody pomocnicze dostosowane do poszczególnych sprawdzanych obiektów,
z klas tych korzysta się w podobny sposób, dlatego dokładnie omawiamy tylko
0 TECHNIKA 74. Prosty test jednostkowy aplikacji na Android 535

jedną z nich — ApplicationTestCase. Warto przypomnieć, że w aplikacji Deal-


Droid zdefiniowana jest specjalna klasa aplikacji (DealDroidApp), pochodna od
klasy android.app.Application. W klasie aplikacji należy umieścić logikę i ustawie-
nia wpływające na całą aplikację, dlatego zawsze warto przygotować kilka testów
i upewnić się, że wszystko jest poprawnie skonfigurowane i działa w odpowiedni
sposób.
Wróćmy do klasy DealDroidApp. Kod odpowiedzialny za inicjowanie obiektu
znajduje się w metodzie onCreate. Oto ten kod:
public class DealDroidApp extends Application {
...

@Override
public void onCreate() {
this.cMgr = (ConnectivityManager)
this.getSystemService(Context.CONNECTIVITY_SERVICE);
this.parser = new DailyDealsXmlPullFeedParser();
this.sectionList = new ArrayList<Section>(6);
this.imageCache = new HashMap<Long, Bitmap>();
}
...
}

Z tego fragmentu wynika, że działanie pozostałego kodu klasy wymaga pełnego


zainicjowania określonych obiektów w metodzie onCreate. Warto więc przete-
stować tę metodę i sprawdzić, czy zawsze inicjuje potrzebne obiekty. Ponadto
chcemy dodać test sprawdzający, czy dla aplikacji poprawnie konfigurowane są
ikona i schemat numerowania wersji. Ujmijmy to dokładniej — chcemy się upew-
nić, że ikoną aplikacji zawsze jest niezmodyfikowany rysunek ddicon z katalogu
res/drawable, a programista pracujący nad aplikacją DealDroid używa dla wersji
numeracji n.m, gdzie n i m to cyfry (na przykład 1.0). Zapiszmy te wymagania
w zadaniu testowym. Przedstawiono je na listingu 13.1.

Listing 13.1. Klasę ApplicationTestCase można wykorzystać do testowania


klas aplikacji

public class DealDroidAppTest


extends ApplicationTestCase<DealDroidApp> {

private DealDroidApp dealdroid;

public DealDroidAppTest() {
super(DealDroidApp.class);
}

@Override
protected void setUp() throws Exception {
super.setUp();
createApplication();
dealdroid = getApplication();
}
536 ROZDZIAŁ 13. Testowanie i instrumentacja

public void testShouldInitializeInstances() {


assertNotNull(dealdroid.sectionList);
assertNotNull(dealdroid.imageCache);
assertNotNull(dealdroid.parser);
}

public void testShouldStartWithEmptySections() {


assertTrue(dealdroid.sectionList.isEmpty());
assertNull(dealdroid.currentSection);
assertNull(dealdroid.currentItem);
}

public void testCorrectProjectProperties()


throws NameNotFoundException {
PackageInfo info =
dealdroid.getPackageManager().getPackageInfo(
dealdroid.getPackageName(), 0);

assertEquals(R.drawable.ddicon, info.applicationInfo.icon);

MoreAsserts.assertMatchesRegex("\\d\\.\\d", info.versionName);
}
}

Jak działa ten kod? Tworzymy zwykłą klasę dziedziczącą po klasie Application
´TestCase. Jest to klasa generyczna, do której trzeba przekazać rodzaj testowanej
aplikacji . Klasa ApplicationTestCase udostępnia metody pomocnicze do testo-
wania klas aplikacji. Do tworzenia nowego obiektu aplikacji służy klasa create
´Application, która uruchamia metodę onCreate aplikacji, a referencje do utwo-
rzonego obiektu można uzyskać za pomocą metody getApplication. Tworzenie
obiektu i pobieranie referencji odbywa się w metodzie setUp . Metoda ta to
udostępniany przez framework JUnit specjalny uchwyt do cyklu życia testów,
uruchamiany przed każdą metodą testową (tu używamy trzech takich metod).
Dlatego w metodzie setUp nie należy wykonywać kosztownych operacji. Zwykle
służy ona do wczytywania konfiguracji testów (ang. text fixture) lub zerowania
i inicjowania stanu testu. Przeprowadzanie testu na zmiennej statycznej pozwala
uruchomić metodę setUp tylko jednokrotnie.
OSTRZEŻENIE. Na podstawie kodu możesz stwierdzić, że metoda Deal
´DroidApp.onCreate jest wywoływana trzykrotnie (przez metodę setUp przed
wykonaniem każdej metody testowej). Nie jest to prawda. Metoda onCreate
jest uruchamiana cztery razy. Wynika to z tego, że klasa Instrumentation
´TestRunner zawsze wywołuje metodę Application.onCreate w ramach
rozruchu. Ma to miejsce przed uruchomieniem testów. Warto o tym
pamiętać, jeśli zamierzasz wykonywać w metodzie onCreate operacje (takie
jak uruchamianie zadań AsyncTask) wpływające na wynik testów.
Dalej znajdują się definicje trzech testów. Pierwszy, testShouldInitializeInstances,
wymaga, aby po wywołaniu metody onCreate wszystkie trzy udostępniane obiekty
0 TECHNIKA 74. Prosty test jednostkowy aplikacji na Android 537

(sectionList, imageCache i parser) były w pełni utworzone (miały wartość różną


od null) . Drugi test, shouldStartWithEmptySections, sprawdza, czy aplikacja jest
uruchamiana z „czystym” stanem. Tu oznacza to, że nie są wczytane żadne oferty
(lista jest pusta) . Ostatni test bada, czy ikona aplikacji zawsze jest ustawiona
na poprawny obiekt graficzny i czy nazwa wersji zawsze jest zgodna ze schema-
tem n.m . Framework JUnit nie udostępnia asercji do sprawdzania wyrażeń
regularnych, jednak Android na szczęście obejmuje kilka niestandardowych
asercji dla JUnit (na przykład użytą tu asercję assertMatchesRegex). Asercje te
znajdują się w klasie pomocniczej android.test.MoreAsserts.
Teraz możemy przejść dalej i uruchomić zadanie testowe przez kliknięcie
klasy prawym przyciskiem myszy i wybranie opcji Run As/Android JUnit test.
Jeśli kod działa poprawnie, w wynikach w widoku JUnit pojawiają się zielone
paski. Możesz też celowo wprowadzić błędy, aby zobaczyć, co się stanie. Otwórz
na przykład plik AndroidManifest.xml aplikacji i zmień wartość atrybutu android:
´versionName z 1.0 na v1.0. Teraz zapisz plik i ponownie uruchom test. Ups!
W nazwie wersji niedozwolone są litery, dlatego test kończy się niepowodzeniem,
co pokazano na rysunku 13.7.

Rysunek 13.7.
Ponieważ napisaliśmy
odpowiedni test,
możemy wykryć
niedozwolone
zmiany w nazwie
aplikacji.
Po wprowadzeniu
takiej zmiany JUnit
zgłasza niespełnioną
asercję w wynikach
w widoku JUnit

Wyjaśnijmy pewną rzecz — niepowodzenie testu w tej sytuacji jest korzystne. Jest
to informacja, że test działa prawidłowo i nie pozwala na stosowanie liter w nazwie
wersji. Teraz należy zmienić nazwę wersji na pierwotną postać. Nie chcemy
przecież pozostawić aplikacji z błędem, prawda?
OMÓWIENIE
Warto wspomnieć o pewnej irytującej kwestii związanej z tworzeniem testów
w frameworku JUnit 3. Chodzi o kolejność wykonywania testów. Spójrz na zadanie
538 ROZDZIAŁ 13. Testowanie i instrumentacja

testowe z listingu 13.1. Widać, że definicja testu sprawdzającego inicjowanie


obiektów (testShouldInitializeInstances) znajduje się przed testem korzystającym
z tych obiektów (testShouldStartWithEmptySections). Wydaje się to sensowne.
Przecież można przyjąć, że dojście do drugiego testu oznacza przejście pierw-
szego, prawda? Niestety, nie! Kolejność definicji testów w zadaniu testowym nie
ma żadnego znaczenia. Framework JUnit 3 nie gwarantuje konkretnej kolejności
wykonywania testów, dlatego nigdy nie należy zakładać, że są uruchamiane
w konkretnym porządku. Problem można rozwiązać przez zdefiniowanie specjal-
nej metody testowej obejmującej wszystkie asercje, które muszą być spełnione
przed wykonaniem testów, i nadanie tej metodzie znaczącej nazwy, na przykład
testPreConditions. Nie oznacza to, że framework traktuje tę metodę w specjalny
sposób, jednak jeśli asercje z niej nie są spełnione, wiadomo, że niepowodzenie
innych testów może wynikać z niespełnionych warunków wstępnych. Wiadomo
też, gdzie zacząć szukać błędów. W firmie Google wzorzec ten stosuje się w zesta-
wach testów frameworku z Androida.
Zobaczyłeś już, jak pisać testy jednostkowe dla podstawowych obiektów,
przy czym na razie stosowaliśmy tylko standardowe funkcje frameworku JUnit
z pomocniczymi komponentami z Androida. Jednak informacje te są niezbędne
do zrozumienia dalszych technik.
Co czeka Cię dalej? Kilkukrotnie wspomnieliśmy już o instrumentacji, jednak
bez przedstawiania szczegółów. Możesz stwierdzić, że JUnit i klasa Android
´TestCase są przydatne, ale na razie nie zajęliśmy się podstawową kwestią, czyli
testowaniem aktywności! W końcu to aktywności są najważniejsze przy rozwijaniu
aplikacji na Android. Możliwość wywołania metody onCreate aktywności nie
wystarczy do ich testowania. Co z klikaniem przycisków lub zgłaszaniem zdarzeń
związanych z klawiszami? Co z uruchamianiem intencji i testowaniem układów?
Wygląda na to, że musimy bliżej przyjrzeć się frameworkowi testowemu Andro-
ida. W następnym podrozdziale wyjaśniamy, jak testować aktywności i interfejs
użytkownika za pomocą androidowego frameworku instrumentacji. Pokazujemy
też, jak z wykorzystaniem języków DSL zwiększyć możliwości w zakresie pisania
testów.

13.2. Pociąganie za sznurki — instrumentacja w Androidzie


Do tej pory testowaliśmy tylko niewidoczne części aplikacji. Mogą one pełnić
bardzo ważne funkcje, jednak są niewidoczne dla użytkownika. Klasa Android
´TestCase wystarcza do przeprowadzania takich testów. Usługi, dostawcy treści
i obiekt aplikacji działają w tle, dlatego nie trzeba symulować interakcji tych
elementów z użytkownikiem. Co jednak zrobić z aktywnościami? Obsługują one
interakcję z interfejsem użytkownika — kliknięcia przycisków, wprowadzanie
tekstu, obracanie ekranu, przewijanie list itd. Jak sprawdzać te operacje za pomocą
zautomatyzowanych testów?
0 TECHNIKA 75. Testy jednostkowe aktywności 539

W Androidzie rozwiązaniem tego problemu jest instrumentacja. Zamiast wywo-


ływać metody i manipulować obiektami w ramach aktywności, można kontro-
lować samą aktywność. W tym celu należy poddać ją instrumentacji. W czasie
pisania standardowego kodu aplikacji posługujemy się tylko wewnętrznymi inter-
fejsami komponentów (na przykład aktywności lub usług). Aplikacja reaguje na
zewnętrzne zdarzenia systemowe w sposób pasywny. Służą do tego na przykład
uchwyty z cyklu życia (takie jak onCreate) lub odbiorniki zdarzeń związanych
z klawiaturą (na przykład zdarzeń onKeyDown). Nie ma sposobu na manipulowanie
komponentami z zewnątrz i przejęcie kontroli nad nimi. Instrumentacja pozwala
wyjść poza to ograniczenie i kontrolować aktywności oraz usługi z zewnątrz.
Wyobraź sobie lalki na sznurkach — instrumentacja służy do pociągania za
sznurki.
Zetknąłeś się już z instrumentacją, choć w ograniczonej postaci. W technice 67.
ręcznie utworzyliśmy obiekt aplikacji z wykorzystaniem metody Application
´TestCase.createApplication. Na tym polega instrumentacja. W standardowych
warunkach kod można powiadamiać o tworzeniu obiektu aplikacji (poprzez
wywołanie Application.onCreate), jednak nie można bezpośrednio kontrolować
tego zdarzenia. Związane jest z tym inne zagadnienie. W podrozdziale 13.1 w pliku
manifestu projektu testowego podaliśmy obiekt klasy InstrumentationTestRunner,
który służy do wykonywania testów przy użyciu instrumentacji. Możesz tu zadać
uzasadnione pytanie, że skoro już się dowiedziałeś, jak stosować instrumentację,
to co jeszcze musisz wiedzieć. Do opanowania pozostało wiele informacji. Wynika
to z dwóch przyczyn. Otóż wszystkie pokazane do tej pory rodzaje zautomaty-
zowanych testów dziedziczą po klasie AndroidTestCase, która — w odróżnieniu
od opisanej dalej klasy InstrumentationTestCase — nie zapewnia dostępu do kom-
pletnego środowiska instrumentacji. Drugi powód jest bardziej skomplikowany.
Nie wspomnieliśmy na razie, że testy wykonywane za pomocą klasy Instrumen
´tationTestRunner są przeprowadzane w odrębnym wątku instrumentacji, poza
głównym wątkiem aplikacji. W czasie testowania komponentów działających
w tle, na przykład usług i dostawców treści, nie ma to znaczenia. Dla tych kom-
ponentów wątek jest nieważny. W rozdziale 6. wyjaśniliśmy jednak, że w kontek-
ście interakcji z interfejsem użytkownika wątki są istotne, ponieważ zdarzenia
interfejsu użytkownika są zawsze przetwarzane w głównym wątku aplikacji,
a manipulowanie widokami spoza tego wątku prowadzi do zgłoszenia błędu.
Wygląda na to, że ograniczenie się do klasy AndroidTestCase oznacza problemy,
ponieważ nie umożliwia ona wykonywania operacji na interfejsie użytkownika
w zadaniu testowym i uruchamiania ich w wątku tego interfejsu.

0 TECHNIKA 75. Testy jednostkowe aktywności

Możliwość kontrolowania interfejsu użytkownika poprzez instrumentację oznacza


nowe komplikacje. Możliwe musi być klikanie przycisków, wprowadzanie tekstu,
przewijanie widoków lub otwieranie opcji menu. Aby ściśle oddzielić testy
540 ROZDZIAŁ 13. Testowanie i instrumentacja

wymagające takich mechanizmów od pozostałych, we frameworku Androida udo-


stępniono specjalny zestaw klas zadań testowych. Klasy te służą do pisania testów
wykorzystujących instrumentację. Testy z tej grupy dziedziczą po klasie bazowej
o trafnej nazwie InstrumentationTestCase. Najważniejszym z tych testów jest
ActivityTestCase (rysunek 13.8). Istnieją też inne testy z wykorzystaniem instru-
mentacji, jednak są dość skomplikowane i mniej przydatne niż ActivityTestCase,
dlatego koncentrujemy się tu właśnie na nim.

Rysunek 13.8. Klas z rodziny InstrumentationTestCase należy używać,


jeśli w testach konieczny jest dostęp do interfejsu API instrumentacji.
Jest to potrzebne w niemal wszystkich sytuacjach związanych z testowaniem
aktywności, a zwłaszcza przy testach funkcjonalnych z wykorzystaniem klasy
ActivityInstrumentationTestCase2

Sama klasa ActivityTestCase nie jest specjalnie interesująca, ponieważ obejmuje


tylko szablonowy kod do testowania aktywności (gdyby nie ta klasa, kod ten
musiałbyś napisać samodzielnie). Ciekawe są różnice między klasami Activity
´UnitTestCase a ActivityInstrumentationTestCase2. Objaśnianie ich odkładamy
do techniki 69., a tu koncentrujemy się na klasie ActivityUnitTestCase.
Klasa ActivityUnitTestCase pozwala przeprowadzać testy jednostkowe aktyw-
ności. We wprowadzeniu wyjaśniliśmy, że służą one do testowania aktywności
w izolacji. Co to oznacza? Przyjrzyj się zwykłej aplikacji na Android, takiej jak
DealDroid. Kiedy aplikacja ta uruchamia aktywność lub przechodzi między ekra-
nami, środowisko uruchomieniowe musi koordynować liczne komponenty uczest-
niczące w tych interakcjach. Trzeba wykonywać kod z uchwytów cyklu życia
(rozdział 3.), wyświetlać elementy na ekranie itd. To zupełne przeciwieństwo
uruchamiania kodu w izolacji! Problem ma dwa aspekty. Po pierwsze, urucha-
mianie kodu różnych komponentów trwa długo. Jeśli chcesz sprawdzić tylko
jeden ekran, po co wczytywać, a nawet uwzględniać inne podrzędne lub nad-
rzędne ekrany? Wydajniej jest pominąć je w kontekście testów. Po drugie, co
0 TECHNIKA 75. Testy jednostkowe aktywności 541

ważniejsze, przeprowadzanie testów w izolacji minimalizuje wpływ innych ele-


mentów na sprawdzany komponent. Pomaga to skoncentrować się na testowaniu
danych wyjściowych i wyjściowych danej aktywności, czyli na testowaniu jej na
poziomie interfejsu. Spróbujmy przekształcić te rozważania w zwięzły opis
problemu.
PROBLEM
Zamierzamy testować wewnętrzny kod aktywności. Nie chcemy, aby komuniko-
wała się z innymi komponentami platformy. Dlatego trzeba zrezygnować z wyko-
nywania aktywności w pełni skonfigurowanym środowisku uruchomieniowym
na rzecz znacznego przyspieszenia wykonywania zadania testowego.
ROZWIĄZANIE
Jeśli zamierzasz przeprowadzać testy typu „Dla danych wejściowych X pocho-
dzących od intencji aktywność powinna zrobić Y” lub „Po utworzeniu aktywności
widoki A i B powinny być w pełni zainicjowane”, klasa ActivityUnitTestCase jest
doskonałym wyborem. Jak już wspomnieliśmy, testy zdefiniowane za pomocą tej
klasy są uruchamiane niezależnie od systemu, co pozwala zminimalizować zależ-
ność od innych komponentów.
PAMIĘTAJ! Także w opisywanym podejściu aplikacja jest uruchamiana
w ramach testów. W poprzedniej technice wyjaśniliśmy, że klasa Instru
´mentationTestRunner zawsze uruchamia aplikację przez wywołanie metody
onCreate.

Android wykorzystuje mechanizmy instrumentacji do uruchamiania aktywności


w kontrolowany sposób, całkowicie niezależnie od pozostałego kodu. Zauważ,
że aplikacja nie przechodzi tu normalnego cyklu życia. W momencie urucha-
miania testu jednostkowego wywoływana jest tylko metoda onCreate (więcej na ten
temat już za chwilę). Podejście to doskonale nadaje się do testowania wewnętrz-
nego stanu aktywności, na przykład sprawdzania działania interfejsu lub tego,
czy widoki są odpowiednio skonfigurowane. Można uruchomić test sprawdza-
jący, czy aktywność zgłasza błąd, jeśli została uruchomiona za pomocą intencji
nieodpowiedniego typu. Jest to test poprawnych danych wejściowych. Ponadto
można sprawdzić, czy aktywność tworzy właściwą intencję do uruchamiania
innej aktywności (jednak w ramach tego testu nie należy włączać tej ostatniej). To
przykładowy test poprawnych danych wyjściowych. Najczęściej jednak sprawdza
się układ aktywności i konfigurację widoków.
Napiszmy test jednostkowy aktywności DealDetails. Nadaje się ona dobrze
do przeprowadzania testów jednostkowych. Jej działanie polega na wyświetlaniu
informacji na temat wybranego elementu. Aktywność ta umożliwia też otwarcie
przeglądarki Androida w celu wczytania strony produktu w witrynie eBay. Po
ujęciu tych operacji w formie asercji otrzymujemy kod z listingu 13.2 (zauważ,
że z uwagi na zwięzłość pomijamy tu niektóre widoki aktywności DealDetails).
542 ROZDZIAŁ 13. Testowanie i instrumentacja

Listing 13.2. Klasa ActivityUnitTestCase pozwala przeprowadzić testy jednostkowe


pojedynczego ekranu

public class DealDetailsTest extends ActivityUnitTestCase<DealDetails> {

private Item testItem;

public DealDetailsTest() {
super(DealDetails.class);
}

@Override
protected void setUp() throws Exception {
super.setUp();

testItem = new Item();


testItem.setItemId(1);
testItem.setTitle("Element testowy");
testItem.setConvertedCurrentPrice("1");
testItem.setLocation("USA");
testItem.setDealUrl("http://example.com");

DealDroidApp application = new DealDroidApp();


application.setCurrentItem(testItem);
setApplication(application);
}

public void testPreConditions() {


startActivity(new Intent(getInstrumentation().getTargetContext(),
DealDetails.class), null, null);

Activity activity = getActivity();


assertNotNull(activity.findViewById(R.id.details_price));
assertNotNull(activity.findViewById(R.id.details_title));
assertNotNull(activity.findViewById(R.id.details_location));
}

public void testThatAllFieldsAreSetCorrectly() {


startActivity(new Intent(getInstrumentation().getTargetContext(),
DealDetails.class), null, null);

assertEquals("$" + testItem.getConvertedCurrentPrice(),
getViewText(R.id.details_price));
assertEquals(testItem.getTitle(), getViewText(R.id.details_title));
assertEquals(testItem.getLocation(),
getViewText(R.id.details_location));
}

public void testThatItemCanBeDisplayedInBrowser() {


startActivity(new Intent(getInstrumentation().getTargetContext(),
DealDetails.class), null, null);

getInstrumentation().invokeMenuActionSync(getActivity(),
DealDetails.MENU_BROWSE, 0);

Intent browserIntent = getStartedActivityIntent();


assertEquals(Intent.ACTION_VIEW, browserIntent.getAction());
assertEquals(testItem.getDealUrl(), browserIntent.getDataString());
0 TECHNIKA 75. Testy jednostkowe aktywności 543

private String getViewText(int textViewId) {


return ((TextView) getActivity().findViewById(textViewId)).getText()
.toString();
}
}

Jak pokazaliśmy w poprzedniej technice, metoda setUp służy do inicjowania


testów . Tu konfigurujemy stan testowy (ang. test fixture), który obejmuje fik-
cyjną ofertę z danymi przekazywanymi aktywności. Ponadto używamy metody
setApplication do dołączenia egzemplarza aplikacji.
Dalej znajdują się metody testowe. Ponownie stosujemy metodę testPre
´Conditions. Tu przeprowadza ona odrębny test. Wymaga on, aby dane widoki
były poprawne, jeśli inne testy mają zakończyć się powodzeniem . Metoda
testThatAllFieldsAreSetCorrectly sprawdza, czy dla ustawionego wcześniej fikcyj-
nego elementu odpowiednie widoki wyświetlają prawidłowe dane (getViewText
to metoda pomocnicza zdefiniowana w celu łatwego wczytywania tekstu z widoków
TextView). Dalej znajdują się ciekawsze metody. W testThatItemCanBeDisplayed
´InBrowser sprawdzamy, czy wciśnięcie przycisku menu o identyfikatorze
MENU_BROWSE powoduje uruchomienie intencji wyświetlającej ofertę na podstawie
jej adresu URL . Aby uzyskać ten efekt, trzeba wykorzystać instrumentację do
programowego wywołania akcji menu za pomocą metody invokeMenuActionSync.
Następnie należy wywołać metodę getStartedActivityIntent i sprawdzić, czy
intencja została uruchomiona. Ważne jest, aby zrozumieć, że w ramach testów
nie otwieramy menu, nie klikamy przycisku ani nie uruchamiamy przeglądarki.
Takie zadania są wykonywane w testach integracyjnych w ramach scenariusza
użytkownika, a nie w testach jednostkowych. Kod omawianych testów sprawdza,
czy zostałaby uruchomiona intencja ACTION_VIEW z adresem URL oferty, gdyby
ktoś kliknął opcję menu w urządzeniu. Test nie robi nic więcej.
Nie omówiliśmy jeszcze pierwszego wiersza metod testowych — wywołania
startActivity. Wywołanie to powoduje wykorzystanie instrumentacji do odzwier-
ciedlenia uruchomienia aktywności DealDetails, jednak bez rzeczywistego jej
włączania. Następuje pośrednie wywołanie metody onCreate, ale bez wywołań
innych uchwytów cyklu życia (na przykład onResume, onStart itd.) związanych
z pełnym uruchomieniem aktywności. Jeśli chcesz wywoływać inne metody,
użyj metod pomocniczych getInstrumentation().callActivityOn*.
W każdej metodzie testowej trzeba wywołać metodę startActivity. W prze-
ciwnym razie wywołanie getActivity (w celu pobrania egzemplarza aktywności)
zwróci wartość null. Trzeba samodzielnie określić intencję używaną do symu-
lowania uruchomienia aktywności. Na tym właśnie polega uruchamianie kompo-
nentów przez klasę ActivityUniteTestCase w kontrolowany sposób. Można nawet
przesłać własny kontekst. Tu jednak używamy metody pomocniczej getTarget
´Context do utworzenia standardowego w Androidzie egzemplarza klasy Context.
544 ROZDZIAŁ 13. Testowanie i instrumentacja

OMÓWIENIE
Jak może się domyślasz, instrumentacja aktywności daje dużo możliwości
w zakresie testów. Najważniejsza jest tu klasa Instrumentation. Jej egzemplarz jest
dostępny w każdym obiekcie klasy InstrumentationTestCase poprzez akcesor
getInstrumentation(). Nie wymieniamy tu wszystkich metod klasy Instrumentation,
warto natomiast wiedzieć, że umożliwiają one uruchamianie i zatrzymywanie
aktywności, zgłaszanie ważnych zdarzeń, uruchamianie akcji w głównym wątku
aplikacji itd. W następnej technice stosujemy klasę Instrumentation i niektóre
z jej zaawansowanych funkcji.
Choć testy jednostkowe aktywności są doskonałym sposobem na testowanie
kodu w izolacji, czasem chcemy zobaczyć dosłownie cały scenariusz użytkownika,
a nie tylko poszczególne jego fragmenty. Czyż nie byłoby wspaniale, gdybyśmy
mogli uruchomić w testach całą serię operacji obejmującą kilka ekranów?
Można to zrobić za pomocą niezbyt trafnie nazwanej klasy ActivityInstrumenta
´tionTestCase2.

0 TECHNIKA 76. Scenariusz użytkownika jako testy funkcjonalne

We wprowadzeniu wspomnieliśmy, że Android udostępnia nie tylko testy jed-


nostkowe, ale też testy funkcjonalne. Warto pokrótce przypomnieć, że testy
funkcjonalne umożliwiają testowanie aplikacji (lub pojedynczego komponentu)
w kompletnym środowisku uruchomieniowym, takim jak przy samodzielnym
uruchamianiu aplikacji. Znacznie różni się to od wcześniej opisanej sytuacji,
w której testy są przeprowadzane w kontrolowanym środowisku, w izolacji od
reszty systemu. W testach funkcjonalnych można wyjść poza granice danej aktyw-
ności, aby uruchomić inną, a następnie kontynuować testy w nowej aktywności.
Pozwala to odzwierciedlić scenariusz użytkownika na zestawy testów i przepro-
wadzać kompleksowe testy aplikacji.
Wróćmy do aplikacji DealDroid. Przed napisaniem jej kodu musieliśmy okre-
ślić wymagania funkcjonalne, czyli zestaw funkcji, które aplikacja ma udostęp-
niać. Jednym ze sposobów na określenie takich wymagań jest utworzenie scena-
riuszy użytkownika, w których każde wymaganie jest zapisywane jako jedno
zdanie, zwięźle oddające to, co oprogramowanie ma wykonać. Oto przykład:
Chcę otrzymać listę ofert i wyświetlać szczegółowe informacje na ich temat.
Drobiazgowe osoby mogą stwierdzić, że zadanie to można rozbić na dwa scena-
riusze użytkownika — scenariusz dotyczący pobierania listy ofert i scenariusz
związany z wyświetlaniem szczegółowych informacji o danej ofercie — jednak
w omawianym przykładzie ten opis zadania jest odpowiedni. Przedstawiony
scenariusz na szczęście zaimplementowano już w aplikacji DealDroid. Ekran
wejściowy wyświetla wybrane oferty z eBaya, a kliknięcie dowolnej z nich powo-
duje otwarcie nowego okna ze szczegółowymi informacjami na jej temat (rysu-
nek 13.9).
0 TECHNIKA 76. Scenariusz użytkownika jako testy funkcjonalne 545

Rysunek 13.9. Aplikacja


DealDroid w wersji
z rozdziału 2. Użytkownik
może wybrać pozycję
z listy ofert (lewy
rysunek) i otrzymać
dodatkowe informacje
na temat dowolnej
z nich (prawy rysunek)

Nie zaimplementowaliśmy jeszcze zadania testowego, które programowo określa,


że implementacja działa. Przed napisaniem testów trzeba ustalić kroki, które
użytkownik wykonuje, aby wyświetlić szczegóły oferty. Oto te kroki:
1. Uruchomienie aplikacji.
2. Oczekiwanie na wczytanie listy ofert.
3. Kliknięcie oferty w celu wyświetlenia szczegółowych informacji. Włączenie
aplikacji oznacza uruchomienie aktywności DealList, ponieważ to ona
wyświetla ekran wejściowy. Aby przetestować cały proces, trzeba więc
sprawdzić przejście z aktywności DealList do DealDetails. Najważniejsze
jest to, że nie można użyć klasy ActivityUnitTestCase, ponieważ nie pozwala
ona na interakcję z aktywnościami innymi niż testowana.
PROBLEM
Chcemy przeprowadzić kompleksowe testy, aby sprawdzić ścieżki pokonywane
przez użytkownika w aplikacji. Takie testy wymagają przygotowania zadania testo-
wego działającego w środowisku systemowym, które jest w pełni funkcjonalne.
ROZWIĄZANIE
Opisany scenariusz można zrealizować za pomocą klasy ActivityInstrumentatin
´TestCase2. Najważniejsza różnica między tą klasą a klasą ActivityUnitTestCase
polega na tym, że w opisywanym tu podejściu metody testowe są wykonywane
z wykorzystaniem kompletnej infrastruktury systemowej. W ten sposób można
zasymulować interakcje użytkownika z aplikacją, na przykład dotknięcie przyci-
sku w celu przejścia do nowego ekranu. Podejście to ma ciekawy efekt uboczny —
można śledzić przebieg testów w urządzeniu lub emulatorze, ponieważ wszystkie
interakcje są widoczne na ekranie!
546 ROZDZIAŁ 13. Testowanie i instrumentacja

UWAGA. Testy z wykorzystaniem instrumentacji, takie jak opisane w tej


technice, są przeprowadzane w standardowym środowisku aplikacji, dla-
tego każde wywołanie metody Activity.getApplication (nawet w różnych
zadaniach testowych) zwraca ten sam egzemplarz aplikacji. Przy testach
scenariuszy jest to zazwyczaj pożądane, jednak jeśli jest inaczej, przed
przeprowadzeniem testu trzeba ręcznie wyzerować stan aplikacji.
Za pozornie tajemnicze interakcje odpowiada znana już klasa Instrumentation. Choć
także klasa ActivityUnitTestCase korzysta na zapleczu z klasy Instrumentation, nie
może spożytkować pełni jej możliwości ze względu na ograniczenie do odrębnej
jednostki kodu. Tu omawiamy funkcje klasy Instrumentation pozwalające wyko-
nywać pewne przydatne zadania, takie jak:
Q tworzenie i dołączanie niestandardowych obiektów klas Activity
oraz Application;
Q bezpośrednie wywoływanie metod cyklu życia aktywności;
Q śledzenie, czy aktywność uruchomiono w reakcji na operacje w rodzaju
kliknięcia przycisku;
Q rozsyłanie ważnych zdarzeń;
Q ręczne wykonywanie kodu w wątku interfejsu użytkownika;
Q stosowanie metod pomocniczych umożliwiających uśpienie testu
w czasie, kiedy aplikacja jest bezczynna.
Wróćmy do testowania przedstawionego wcześniej scenariusza użytkownika. Na
liście wymienione są funkcje niezbędne do przeprowadzenia takich testów. Po
samodzielnym uruchomieniu aplikacji DealDroid można zauważyć, że po otwar-
ciu aktywności DealList aplikacja wyświetla okno dialogowe z informacjami
o postępie w trakcie wczytywania danych z usługi sieciowej eBaya. Do czasu
zamknięcia okna interfejs użytkownika jest zablokowany, dlatego trzeba odczekać
do tego momentu. Ponadto musimy programowo kliknąć pozycję z listy i spraw-
dzić, czy prowadzi to do uruchomienia aktywności DealDetails. Na listingu 13.3
pokazano, jak uzyskać pożądany efekt za pomocą klasy Instrumentation.

Listing 13.3. Klasa ActivityInstrumentationTestCase2 pozwala testować


przebieg pracy aplikacji

public class DealListTest extends ActivityInstrumentationTestCase2<DealList> {

public DealListTest() {
super("com.manning.aip.dealdroid", DealList.class);
}

public void testDealListToDetailsUserFlow() throws Exception {


Instrumentation instr = getInstrumentation();
DealList dealList = getActivity();

ParseFeedTask task = dealList.getParseFeedTask();


assertNotNull("Zadanie powinno mieć wartość różną od null", task);
0 TECHNIKA 76. Scenariusz użytkownika jako testy funkcjonalne 547

List<Section> taskResult = task.waitAndUpdate();


assertNotNull("Zadanie nie zwróciło danych", taskResult);

instr.waitForIdleSync();

String dealDetails = DealDetails.class.getCanonicalName();


ActivityMonitor monitor =
instr.addMonitor(dealDetails, null, false);
View firstItem = dealList.getListView().getChildAt(0);
TouchUtils.clickView(this, firstItem);
assertTrue(instr.checkMonitorHit(monitor, 1));

instr.removeMonitor(monitor);
}
}

W kodzie zdefiniowany jest jeden test, testDealListToDetailsUserFlow, z imple-


mentacją scenariusza użytkownika. Zaczynamy od zapisania referencji do egzem-
plarza klasy Instrumentation używanego w zadaniu testowym i do egzem-
plarza aktywności DealList . Warto zauważyć, że metoda getActivity klasy
ActivityInstrumentationTestCase2 najpierw sprawdza, czy aktywność już urucho-
miono; jeśli aktywność jeszcze nie działa, metoda ją włącza. Tu, inaczej niż w teście
jednostkowym, wywoływane są wszystkie uchwyty cyklu życia i następuje prawi-
dłowe uruchomienie aktywności.
Jak już wspomnieliśmy, pewne komplikacje mają miejsce po rozpoczęciu
działania aktywności DealList, kiedy pobiera ona w wątku roboczym dane z sieci
WWW. W trakcie pracy nad testami można przekierować to zadanie do namiastki
lub pośrednika, jednak w ramach kompleksowych testów warto wykonywać je
w standardowy sposób. Trzeba więc zablokować wątek odpowiedzialny za instru-
mentację (wątek mechanizmu do wykonywania testów) do momentu ukończenia
tego zadania. W ten sposób powinna działać metoda AsyncTask.get, jednak
w praktyce nie funkcjonuje ona w stabilny sposób, ponieważ czasem nie wywołuje
metody onPostExecute. Jeśli kilka zadań testowych sprawdza różne aktywności
na rozmaite sposoby, interfejs użytkownika w testach z instrumentacją może pra-
cować niepłynnie. Dlatego w klasie ParseFeedTask warto umieścić metodę pomoc-
niczą waitAndUpdate, która gwarantuje wywołanie metody onPostExecute . Oto
metoda waitAndUpdate:
public List<Section> waitAndUpdate() throws Exception {
final List<Section> sections = this.get();
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
if (!getStatus().equals(Status.FINISHED)) {
onPostExecute(sections);
}
}
});
return sections;
}
548 ROZDZIAŁ 13. Testowanie i instrumentacja

Pora na zastosowanie klasy Instrumentation. Przez wywołanie jej metody wait


´ForIdleSync można się upewnić, że wątek interfejsu użytkownika jest dostępny
(zakończył przetwarzanie zdarzeń z interfejsu użytkownika) . Konkretnie
upewniamy się, że aktywność zamknęła okno dialogowe z informacjami o postępie,
zaktualizowała listę ofert i znajduje się w stanie bezczynności umożliwiającym
interakcje. Gdy piszesz kod klasy ActivityInstrumentationTestCase2, zawsze pamię-
taj, że aplikacja jest testowana w standardowym środowisku. Oznacza to, że często
trzeba najpierw sprawdzić, czy kod zakończył pracę, a dopiero potem przecho-
dzić do dalszych operacji lub asercji. Podobnie zachowuje się użytkownik aplikacji.
Na tym etapie wiadomo, że aktywność DealList wyświetla listę ofert, można
więc kliknąć jedną z nich; służy do tego metoda pomocnicza TouchUtils.
´clickView . Najpierw jednak trzeba poinformować obiekt klasy Instrumentation,
którą aktywność należy uruchomić po kliknięciu widoku. W tym celu trzeba zare-
jestrować obiekt klasy ActivityMonitor . Jest to prosty mechanizm synchroni-
zacji (monitor, na co wskazuje nazwa), który można wykorzystać do wykrywania
uruchomienia aktywności. Tu nie odbieramy wyników od aktywności, dlatego
stosujemy monitor nieblokujący i przekazujemy argumenty null oraz false.
ZMIENNOŚĆ OBIEKTU KLASY INSTRUMENTATION. Zawsze należy
pamiętać, że egzemplarz klasy Instrumentation zwrócony przez metodę
getInstrumentation jest używany we wszystkich zadaniach testowych.
Jest uruchamiany raz, a następnie działa w całym zestawie testów. Oznacza
to, że wszelkie modyfikacje, na przykład dodanie monitorów, wpływają na
wszystkie testy z zestawu, a nie tylko na test, w którym znajduje się wywo-
łanie. Jest to częste źródło błędów. Programista może na przykład zapo-
mnieć usunąć dodany monitor ActivityMonitor. Dobrym miejscem na
usunięcie zmian w obiekcie klasy Instrumentation jest metoda tearDown
zadania testowego, uruchamiana po wykonaniu każdej metody testowej.
Po zarejestrowaniu monitora można kliknąć element na liście i czekać na wyko-
nanie operacji przez system (metoda TouchUtils.clickView wywołuje metodę
waitForIdleSync, dlatego nie trzeba ręcznie uruchamiać tej ostatniej) . Teraz
można sprawdzić, czy aktywność DealDetails dochodzi do monitora (czy został on
uruchomiony) . Oczekujemy, że zdarzy się to tylko raz, stąd argument 1. Trzeba
pamiętać, aby każdemu wywołaniu addMonitor odpowiadało wywołanie remove
´Monitor. Jeśli dodasz monitor, ale zapomnisz go usunąć, będzie on działał
w czasie wykonywania całego zestawu testów, co może mieć niepożądane efekty
uboczne.
OMÓWIENIE
W czasie pisania testów instrumentacji trzeba zwrócić uwagę na to, aby wszyst-
kie akcje związane z interfejsem użytkownika wykonywać w wątku tego interfejsu
(kwestię tę wyjaśniliśmy we wcześniejszych rozdziałach) i tylko tam. Problem
polega na tym, że test z instrumentacją działa w odrębnym wątku, dlatego nie
0 TECHNIKA 77. Eleganckie testy z wykorzystaniem frameworku Robotium 549

można w nim manipulować interfejsem użytkownika. Niedozwolone jest wyko-


nywanie nawet tak prostych operacji, jak wciśnięcie przycisku. Ale chwileczkę,
przecież właśnie to zrobiliśmy — kliknęliśmy element listy za pomocą metody
TouchUtils.clickView, a więc używanie interfejsu użytkownika musi być możliwe,
prawda? Wspomniana metoda pomocnicza ukrywa pewne techniczne szczegóły.
Gdyby nie ona, potrzebną operację można by wykonać w następujący sposób:
instr.runOnMainSync(new Runnable() {
public void run() {
View firstItem = dealList.getListView().getChildAt(0);
firstItem.performClick();
}
});

Metoda Instrumentation.runOnMainSync blokuje aplikację do czasu, kiedy interfejs


użytkownika jest gotowy do przetwarzania komunikatów, a następnie uruchamia
otrzymany obiekt typu Runnable. W ten sposób można się upewnić, że akcje
związane z widokami są wykonywane tam, gdzie jest ich miejsce — w głównym
wątku aplikacji!
Łatwo dostrzec, jak dużo możliwości daje ten sposób testowania. Można
wszystkie ścieżki korzystania z aplikacji ująć w zadania testowe klasy Activity
´InstrumentationTestCase2, a następnie wykonywać te zadania jedno po drugim,
symulując każdą możliwą ścieżkę, jaką użytkownik może wybrać w aplikacji,
i sprawdzając, czy wszystko działa zgodnie z oczekiwaniami. Jest to wyjątkowo
przydatne w połączeniu z automatyzacją budowania, o czym przekonasz się
w następnym rozdziale.
W kodzie napisanych wcześniej testów Twoją uwagę mógł zwrócić brak
elegancji używanych metod i składni. Pomyśl o tym przez chwilę. Czy gdybyśmy
nie omówili monitorów aktywności i metody waitForIdleSync, zrozumiałbyś, jakie
testy przeprowadza kod? Wyjaśniliśmy tu dużo szablonowego kodu, na przykład
bezpośrednie oczekiwanie na przejście głównego wątku aplikacji w stan bezczyn-
ności. Ponadto asercja dotycząca uruchomienia aktywności DealDetails zajmuje
trzy wiersze kodu (definiowanie monitora, oczekiwanie na metodę waitForIdleSync
i sprawdzanie monitora) oraz niewygodna jest konieczność obsługi podstawowych
mechanizmów synchronizacji. Ponieważ jesteśmy estetami, możemy powiedzieć
jedno — musi istnieć lepsze rozwiązanie!

0 TECHNIKA 77. Eleganckie testy z wykorzystaniem frameworku


Robotium

Android jest znakomity w wielu obszarach, jednak interfejs API frameworku


testowego nie jest jednym z nich. Dobry framework testowy musi spełniać dwa
warunki: powinien pozwalać na łatwe pisanie testów, a także — co może waż-
niejsze — umożliwiać łatwe ich zrozumienie. Jeśli pisanie testów jest trudne,
programiści nie będą chcieli tego robić. Jeżeli testy nie są przystępne, inny
programista może nie zrozumieć ich przeznaczenia lub w ogóle nie wiedzieć,
550 ROZDZIAŁ 13. Testowanie i instrumentacja

jak przebiegają. Ponadto jeśli chcesz przekształcać scenariusze użytkownika na


zestawy testów, przydatna jest składnia pasująca do pojęć użytych do pisania
takich scenariuszy. Przedstawione wcześniej rozwiązania nie spełniają podanych
warunków. Przeznaczenie testu często jest w nim ukryte za długimi fragmentami
szablonowego kodu. Wygodnie byłoby mieć testowy interfejs API zaprojektowany
pod kątem opisu kroków, które użytkownik może wykonać w aplikacji na Android
(takich jak wciśnięcie przycisku, wprowadzenie tekstu, przewinięcie listy lub cof-
nięcie się), i tworzenia odpowiednich asercji.
Mówiąc o składni i zbiorach instrukcji swoistych dla konkretnej dziedziny,
takiej jak testy i instrumentacja w Androidzie, wkraczamy w świat różnorodnych
języków DSL (języków dziedzinowych). Niektóre z nich są zaprojektowane od
podstaw, tak jak język OCL (ang. Object Constraint Language) dla UML-a. Inne
są oparte na istniejących językach. Dotyczy to na przykład mechanizmów budo-
wania dokumentów dla języka Ruby. Niektóre języki są krótkie i niezrozumiałe
(podobnie jak wyrażenia regularne), inne — rozwlekłe i naturalne (na przykład
Cucumber; zobacz stronę http://cukes.info). Języki DSL można nawet znaleźć
poza komputerem. Śledziłeś kiedyś turniej w pokera Texas Hold’em? Gracze obok
rozdającego wkładają małą ciemną i dużą ciemną. Pozostali mogą następnie
spasować karty, sprawdzić lub podbić stawkę, po czym na stół wykładany jest flop,
a jeszcze później — turn i river. Jeśli nie grasz w pokera, prawdopodobnie nie
wiesz, na czym to polega. Jest tak, ponieważ gracze używają języka dziedzinowego.
Języki DSL doskonale nadają się do pisania testów, ponieważ pozwalają
opisywać oczekiwane zdarzenia w ściśle określony i zarazem naturalny sposób.
Język Cucumber jest oparty na języku Ruby i umożliwia pisanie scenariuszy testo-
wych w języku podobnym do naturalnego. Oto przykład:
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen

Cucumber przekształca te instrukcje na metody testowe, które można wykonać


w taki sam sposób, jak dowolne inne zadanie testowe. Testy na Androidzie na
razie nie są równie zaawansowane, jednak społeczność związana z tą platformą
pracuje nad tym! W tej technice opisujemy obecne możliwości języków DSL zwią-
zanych z testami na Androidzie.
PROBLEM
Zadania testowe muszą być zrozumiałe nawet dla personelu bez wiedzy tech-
nicznej. Chcemy też tworzyć kod testów, który jest łatwiejszy do napisania i zro-
zumienia, a także lepiej odzwierciedla związaną ze scenariuszami naturę testów.
ROZWIĄZANIE
Robotium to warty uwagi projekt z jeszcze dość skromnego obszaru bibliotek
testowych rozwijanych przez społeczność programistów aplikacji na Android.
0 TECHNIKA 77. Eleganckie testy z wykorzystaniem frameworku Robotium 551

Robotium to bezpłatna i otwarta niezależna biblioteka dostępna na licencji Apache


License 2.0. Jest rozpowszechniana jako zwykły plik JAR, można więc umieścić
ją w projekcie testowym i zacząć z niej korzystać. Bibliotekę tę znajdziesz w ser-
wisie Google Code pod adresem http://code.google.com/p/robotium.
POBIERZ PROJEKT DEALDROIDROBOTIUMTEST. Kod źródłowy
projektu i pakiet APK do uruchamiania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Ponieważ niektóre listingi skrócono,
abyś mógł skoncentrować się na konkretnych zagadnieniach, zalecamy
pobranie kompletnego kodu źródłowego i śledzenie go w Eclipse (lub
innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/5745.
Robotium jest nie tyle samodzielnym frameworkiem testowym, jak sugerują to
autorzy w witrynie projektu, ile rozszerzeniem istniejącego frameworku testowego
z Androida. Możesz traktować Robotium jak dodatek do androidowego frame-
worku do instrumentacji, znacznie ułatwiający pisanie nawet skomplikowanych
scenariuszy testowych. Nie istnieją klasy „frameworku” Robotium, które trzeba
by rozszerzać, pisząc testy z wykorzystaniem biblioteki. Zadania testowe nadal
dziedziczą po standardowej klasie ActivityInstrumentationTestCase2. Jednak do
sterowania pracą interfejsu użytkownika w zadaniu testowym służy klasa Solo.
Wszystkie akcje lub kroki wykonywane przez użytkownika są uruchamiane przez
egzemplarz tej klasy. Operacje te podaje się w formie instrukcji, podobnie jak
wspomniane wcześniej akcje (pressButton, goBack itd.). Pozwala to stosować
standardowy styl pisania kodu, a także łączyć wywołania metody klasy Solo biblio-
teki Robotium z instrukcjami ze standardowych klas frameworku Androida.
ROBOTIUM SŁUŻY DO TWORZENIA TESTÓW CZARNEJ SKRZYNKI.
Bibliotekę Robotium zaprojektowano pod kątem testów czarnej skrzynki,
podobnie jak wysokopoziomowe frameworki testowe (takie jak Cucumber).
W testach czarnej skrzynki przedmiot testów jest nieprzejrzysty. Nie znamy
jego wewnętrznej struktury ani sposobu działania. Należy tylko przekazać
dane do testowego obiektu i obserwować, czy zwraca oczekiwane infor-
macje. Jest to proces odmienny od zastosowanego we wcześniejszych te-
stach, gdzie korzystaliśmy z wiedzy o implementacji (na przykład z iden-
tyfikatorów widoków).
Na rysunku 13.10 przedstawiono miejsce biblioteki Robotium obok frameworku
testowego Androida i zadań testowych opracowanych przez programistę.
Zmodyfikujmy zadanie testowe z poprzedniej techniki przez zastosowanie
biblioteki Robotium. Ponieważ udostępnia ona wygodną, zwięzłą składnię,
możemy przedstawić bardziej skomplikowany proces. Zamiast testować przejście
z widoku listy do aktywności DealDetails, sprawdzamy wybieranie list ofert z okna
wyboru. Na listingu 13.4 pokazano, jak używać klasy Solo.
552 ROZDZIAŁ 13. Testowanie i instrumentacja

Rysunek 13.10. Biblioteka Robotium korzysta z frameworku testowego Androida.


Jest nakładką na niego i wzbogaca jego funkcje. Robotium można wykorzystać
we własnych zadaniach testowych, używając klasy Solo, która jest głównym
punktem dostępu do pomocniczych metod testowych biblioteki Robotium

Listing 13.4. Biblioteka Robotium pozwala używać języka DSL do pisania testów
funkcjonalnych w formie scenariuszy

public class DealListRobotiumTest extends


ActivityInstrumentationTestCase2<DealList> {

private Solo solo;

public DealListRobotiumTest() {
super("com.manning.aip.dealdroid", DealList.class);
}

@Override
protected void setUp() throws Exception {
super.setUp();
solo = new Solo(getInstrumentation(), getActivity());
}

public void testDealListToDetailsWithListChangeUserFlow()


throws Exception {
DealList dealList = getActivity();
dealList.getParseFeedTask().waitAndUpdate();

solo.clickInList(0);
solo.assertCurrentActivity("Oczekiwano aktywności DealDetails",
DealDetails.class);
solo.goBack();
solo.assertCurrentActivity("Oczekiwano aktywności DealList",
DealList.class);
solo.pressSpinnerItem(0, 2);
solo.scrollDown();
solo.clickInList(dealList.getItems().size() - 1);
0 TECHNIKA 77. Eleganckie testy z wykorzystaniem frameworku Robotium 553

solo.assertCurrentActivity("Oczekiwano aktywności DealDetails",


DealDetails.class);
}
}

Pierwsze, co należy zrobić w czasie pisania zadania testowego z wykorzystaniem


biblioteki Robotium, to zdefiniowanie referencji do obiektu klasy Solo . Jest
to najważniejszy obiekt w testach Robotium, służący do instrumentacji aktywności
przez wywoływanie metod o nazwach podobnych do rozkazów . Polecenia nie-
mal nie wymagają wyjaśnień. Mają zrozumiałe nazwy (w języku angielskim)
i wymagają tylko niezbędnych argumentów. Metoda clickInList klika na liście
element o podanym indeksie (Robotium domyślnie przyjmuje, że na danym
ekranie znajduje się tylko jeden widok ListView), metoda goBack wciska przycisk
„wstecz”, aby wrócić do wcześniejszej aktywności, a metoda scrollDown przewija
listę do ostatniego elementu. Po dodaniu słów takich jak when, and, then otrzymu-
jemy tekst podobny do kompletnych zdań w języku angielskim. Ponadto pozby-
wamy się całego zbędnego kodu. Nie ma już wywołań metody waitForIdleSync,
monitorów aktywności ani innych technicznych rozwiązań odciągających czytel-
ników od samego testu.
OMÓWIENIE
Robotium jest darem od losu dla osób, które cenią przejrzysty i naturalny kod
w testach. Składnia jest tu czytelna i wygodna w użyciu, choć trzeba przyznać,
że testy oparte na omawianej bibliotece są wykonywane nieco wolniej niż stan-
dardowe. Wynika to z wyższego poziomu abstrakcji oraz częstych wywołań metod
wait i sleep przez klasę Solo.
Ponieważ biblioteka Robotium służy do pisania testów czarnej skrzynki,
nie można w nich na przykład wskazywać widoków za pomocą identyfikatorów.
Zamiast tego trzeba używać tylko danych widocznych na ekranie, czyli z widoków
tekstowych (etykiet przycisków) lub indeksów dostępnych w narzędziu hierar
´chyviewer. Oznacza to, że z biblioteki Robotium można korzystać nawet do
testowania cudzych aplikacji, choć używanie jej do własnych projektów bywa
niewygodne.
Gotowe są plany rozbudowania możliwości tej biblioteki. Trwają prace nad
zwiększeniem jej rozszerzalności, aby można było łatwiej dodawać do niej nowe
rozwiązania (na przykład wykorzystać istniejące języki do testowania, takie jak
Cucumber). Inny ciekawy plan dotyczy rozszerzenia o nazwie RC (ang. Remote
Control). Rozszerzenie to ma pozwalać na zainstalowanie serwera w emulatorze
lub urządzeniu oraz uruchamianie kodu w całości po stronie klienta (na maszynie
programisty) i wysyłanie na serwer instrukcji. Efektem ma być wyższa szybkość
i więcej możliwości w zakresie wykonywania testów.
Robotium nie jest jedynym rozwijanym obecnie projektem z obszaru bibliotek
testowych dla Androida. Podobnym narzędziem jest biblioteka Calculon. Jednak
jej twórcy nie używają obiektu pośredniczącego do instrumentacji aktywności, ale
554 ROZDZIAŁ 13. Testowanie i instrumentacja

rozszerzają istniejące klasy frameworku o nowe asercje tworzące język DSL.


Użytkownicy biblioteki Calculon mogą pisać „zdania” zaczynające się od instrukcji
assertThat i tworzyć w ten sposób testy. Oto przykładowy kod:
assertThat(R.id.button1).click().starts(MyActivity.class);
assertThat(R.id.button2).click().implies(R.id.some_view).gone();

Autorzy Calculona nawet w większym stopniu niż twórcy biblioteki Robotium


koncentrują się na przejrzystym i zwięzłym zapisie asercji i akcji w zadaniu testo-
wym. Narzędzie to znajduje się jednak na wczesnym etapie rozwoju i użytkownicy
muszą się dopiero przekonać o jego przydatności w środowisku produkcyjnym.
Calculon to biblioteka o otwartym dostępie do kodu źródłowego na licencji Apache
License 2.0. Bibliotekę tę można pobrać ze strony https://github.com/kaeppler/
calculon.

13.3. Poza instrumentację — atrapy i testy losowe


W dwóch pierwszych podrozdziałach pokazaliśmy funkcjonowanie testów
w Androidzie. Wyjaśniliśmy zagadnienia od konfigurowania projektu testowego
do pisania prostych i bardziej złożonych zadań testowych. To jeszcze nie koniec.
Niniejszy, ostatni podrozdział dotyczy zaawansowanych kwestii z obszaru testów
w Androidzie. Wychodzimy tu poza typowe testy z instrumentacją. Zaczynamy
od omówienia atrap i wyjaśniamy, dlaczego i jak należy stosować je w testach.
Później wychodzimy poza testy JUnit i badamy inne techniki testowania apli-
kacji. Mechanizmy te różnią się od dotychczas omawianych, ale można je wyko-
rzystać w celu uzupełnienia zwykłych zadań testowych z Androida.

0 TECHNIKA 78. Atrapy i sposoby ich stosowania

Z testami związana jest podstawowa zasada — nie pozwól, aby wynik testu zale-
żał od czegoś, co nie jest bezpośrednio powiązane z testowaną jednostką (lub, co
gorsza, znajduje się poza kontrolą programisty). Regułę tę zastosowaliśmy przy
pisaniu testu jednostkowego aktywności DealDetails w technice 68., gdzie całe
środowisko testowe, w którym działała aktywność, pełniło funkcję bariery. Test nie
mógł się nie powieść z powodu awarii przeglądarki w trakcie testowania funkcji
wyświetlania w takim programie, ponieważ tak naprawdę nie uruchamialiśmy
procesu z przeglądarką! Testowaliśmy tylko scenariusz „jeśli, to”: jeśli użytkow-
nik wciśnie w urządzeniu daną opcję menu, to uruchomi się przeglądarka. Ozna-
cza to, że testowaliśmy funkcję bez konieczności polegania na niekontrolowanej
przez nas przeglądarce. Jest to pożądane, ponieważ nie interesowało nas dzia-
łanie przeglądarki. Ważne było tylko to, że jeśli przeglądarka pracowała, także
aplikacja powinna była funkcjonować.
W testach z instrumentacją z technik 69. i 70. zastosowaliśmy inne podejście.
Choć testowaną jednostką była aktywność DealList, w testach uwzględnialiśmy
także wiele innych komponentów, w tym aktywności DealDetails, a nawet usługę
0 TECHNIKA 78. Atrapy i sposoby ich stosowania 555

sieciową. Podejście to rodzi dwa problemy. Po pierwsze, testy z instrumentacją,


w których kod jest wykonywany w taki sam sposób, jakby to zwykły użytkownik
korzystał z aplikacji, mogą być długie. Średni czas wykonania testu DealListTest
w emulatorze na naszym komputerze wynosił sześć sekund, przy czym większość
czasu zajmowało wywołanie usługi sieciowej. Można się domyślić, że dla dużej
aplikacji po dodaniu większej liczby testów łączny czas wykonania ich zestawu
może być dość długi. Jest to problem przy stosowaniu podejścia TDD, gdzie
ważne jest szybkie uzyskiwanie informacji zwrotnych pozwalających stwierdzić,
jak zmiany w kodzie źródłowym wpływają na całą aplikację.
Po drugie, i jest to ważniejszy problem, testy z instrumentacją są niestabilne.
Co zrobić, jeśli usługa sieciowa eBaya nie działa? Czy test ma zakończyć się
niepowodzeniem? Raczej nie. W końcu chcemy przetestować tylko naszą apli-
kację, a nie usługę sieciową, nad którą nie mamy kontroli. Ktoś może powiedzieć:
„Ale przeprowadzamy testy integracyjne, testy scenariusza, które powinny symu-
lować to, co użytkownik robi z aplikacją”. To prawda, jednak ten sam efekt można
uzyskać przez zastąpienie wywołań usługi sieciowej statycznymi danymi (listą
obiektów typu Item) i sprawdzenie w odrębnych testach jednostkowych kodu,
który nawiązuje połączenie z usługą sieciową i przetwarza elementy typu Item.
W ten sposób można sprawdzić cały kod, a przy tym odizolować testy. Jeśli test
jednostkowy dla parsera usługi sieciowej kończy się powodzeniem, a w testach
integracyjnych korzystamy tylko z interfejsu API tego parsera, wiadomo, że po
połączeniu poszczególnych komponentów aplikacja będzie działać.
Te rozważania prowadzą do pytania, jak zlikwidować w testach zależność od
komponentów, które pozostają poza kontrolą programisty lub powinny być testo-
wane osobno. Tu przydają się atrapy (ang. mock object) i namiastki (ang. stub).
PROBLEM
Chcemy zastąpić w teście funkcję fikcyjnymi danymi, ponieważ jej działanie może
wpływać na test i prowadzić do jego niepowodzenia, nawet jeśli sprawdzana
jednostka działa prawidłowo.
ROZWIĄZANIE
Atrapy i namiastki to elementy zastępcze w testach. Udostępniają ten sam inter-
fejs API co zastępowane obiekty. W kontekście testów działają identycznie, ale
mają zmodyfikowaną implementację, aby nie zakłócały testów. Często zastępcze
metody zwracają statyczne dane, na przykład z konfiguracji testów dołączonej
do projektu testowego.
ZARZĄDZANIE KONFIGURACJAMI TESTÓW. Konfiguracje testów
często są używane razem z atrapami, ponieważ zastępują dane pobierane
„na żywo” danymi statycznymi, wstępnie określonymi, które należy stoso-
wać tylko w testach. Zastanów się nad wywołaniem usługi sieciowej w apli-
kacji DealDroid. Zamiast zgłaszać wywołanie i pobierać dokument XML
556 ROZDZIAŁ 13. Testowanie i instrumentacja

przez protokół HTTP, można dołączyć do projektu testowego statyczny


plik XML i używać go w testach aktywności DealList. Potrzebne pliki
można umieścić w katalogu res/raw projektu testowego i wczytywać
w metodzie setUp testu.
Podobnie wygląda stosowanie manekinów w testach zderzeniowych. Manekiny
mają kształty, rozmiary i wagę podobne do ludzkiego ciała, ale są tylko jego zastęp-
nikiem. Różnica między atrapą a namiastką polega na tym, że namiastka tylko
zwraca statyczne lub ręcznie podane dane, natomiast atrapa ponadto sprawdza,
czy metoda została wywołana. Atrapy są niezwykle przydatne do testowania
interakcji między obiektami, jeśli efekt wywołania metody nie jest ważny, nato-
miast istotne jest, czy wywołanie to miało miejsce. Wyobraź sobie proces spraw-
dzania poprawności nazwiska właściciela karty przy obsłudze płatności. Nie ma
znaczenia, czy właściciel to Jan Kowalski, czy Andrzej Nowak. Bardzo ważne
jest jednak, aby system uwzględnił nazwisko przy sprawdzaniu, czy płatność jest
prawidłowa. Dla uproszczenia dalej używamy tylko określenia atrapy (niezależ-
nie od tego, czy używamy rzeczywistych atrap, czy tylko namiastek).
Aby przedstawić atrapy w ciekawy sposób, tworzymy nowy scenariusz i doda-
jemy do aplikacji DealDroid prostą funkcję eksportowania ofert. Funkcja ta jest
uruchamiana za pomocą opcji menu, która umożliwia zapisanie wszystkich ele-
mentów z listy ofert do pliku. Dane są zapisywane w wyniku wywołania metody
toString dla elementów listy (rysunek 13.11).

Rysunek 13.11. Nowa funkcja eksportowania. Wybranie jej z menu powoduje


wyeksportowanie listy ofert do pliku tekstowego
0 TECHNIKA 78. Atrapy i sposoby ich stosowania 557

POBIERZ PROJEKT DEALDROIDWITHEXPORT. Kod źró-


dłowy projektu i pakiet APK do uruchamiania aplikacji znaj-
dziesz w witrynie z kodem do książki Android w praktyce.
Ponieważ niektóre listingi skrócono, abyś mógł skoncentro-
wać się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse
(lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/27qZ, archiwum APK: http://mng.bz/1LX1.
Na listingu 13.5 pokazano kod klasy pomocniczej używanej do eksportowania.

Listing 13.5. Nowa klasa eksportuje listę ofert do pliku tekstowego

public class DealExporter {

private Context context;

private List<Item> deals;

public DealExporter(Context context, List<Item> deals) {


this.context = context;
this.deals = deals;
}

public void export() throws IOException {


FileOutputStream fos =
context.openFileOutput("deals.txt", Context.MODE_PRIVATE);
for (Item item : deals) {
fos.write(item.toString().getBytes());
}
fos.close();
}
}

Kod wygląda na nieskomplikowany. Zapisujemy dane do pliku tekstowego otwar-


tego przy użyciu metody pomocniczej openFileOutput. Metoda ta tworzy w urzą-
dzeniu (w katalogu data/files aplikacji) nowy plik, deals.txt. Funkcja jest dostępna
w aktywności DealList poprzez opcję w menu (zainteresowani znajdą pełny kod
źródłowy w pliku DealList.java).
Jak napisałbyś test jednostkowy klasy DealExporter? Nie sprawdzamy tu
aktywności, usługi ani aplikacji. Testujemy obiekt typu POJO, który zależy od
kontekstu. Żadna klasa zadania testowego w Androidzie nie udostępnia w pełni
skonfigurowanego kontekstu, który można wykorzystać do wywołania metody
openFileOutput. Nawet gdyby taka klasa istniała, nie chcemy ani nie potrzebu-
jemy sprawdzać, czy androidowe operacje wejścia-wyjścia na plikach działają —
takie testy dotyczyły samego Androida, a nie nowej klasy. Dlatego warto utworzyć
atrapę kontekstu, od którego zależy ta klasa. Przygotowanie atrapy odbywa się
w dwóch krokach. Pierwszy to zdefiniowanie atrapy w postaci klasy kontekstu
z implementacją namiastki metody openFileOutput. Klasa DealExporter oczekuje,
558 ROZDZIAŁ 13. Testowanie i instrumentacja

że metoda ta zwraca poprawny obiekt typu FileOutputStream, dlatego w drugim


kroku trzeba zaimplementować klasę MockOutputStream, która nie zapisuje danych
do pliku, ale rejestruje wywołania służącej do tego metody i przekierowuje
bajty z tekstem do standardowego wyjścia. Na listingu 13.6 przedstawiono test
DealExporterTest obejmujący wspomniane atrapy.

Listing 13.6. Używanie atrap do rozdzielania testowanych obiektów

public class DealExporterTest extends TestCase {

private List<Item> deals = new ArrayList<Item>();


private int itemsWritten = 0;

private class MockOutputStream extends FileOutputStream {


public MockOutputStream() throws FileNotFoundException {
super(FileDescriptor.out);
}

@Override
public void write(byte[] buffer) throws IOException {
Item currentItem = deals.get(itemsWritten++);
assertEquals(currentItem.toString(), new String(buffer));
}
}

private class MyMockContext extends MockContext {


@Override
public FileOutputStream openFileOutput(String name, int mode)
throws FileNotFoundException {
return new MockOutputStream();
}
}

@Override
protected void setUp() throws Exception {
super.setUp();

Item item1 = new Item();


item1.setTitle("Element testowy 1");
deals.add(item1);

Item item2 = new Item();


item2.setTitle("Element testowy 2");
deals.add(item2);
}

public void testShouldExportItems() throws IOException {


new DealExporter(new MyMockContext(), deals).export();
assertEquals(2, itemsWritten);
}
}

Najpierw trzeba zdefiniować atrapę imitującą generowanie pliku. W tym celu


tworzymy klasę pochodną od klasy FileOutputStream i konfigurujemy ją w taki
sposób, aby zapisywała dane do standardowego wyjścia . Efekt ten uzyskujemy
0 TECHNIKA 78. Atrapy i sposoby ich stosowania 559

przez przekazanie do konstruktora obiektu FileDescriptor.out. W następnym


kroku przesłaniamy metodę write, dlatego przekazany tu obiekt nie ma znacze-
nia, choć musi być poprawnym deskryptorem pliku. Metodę write przesłaniamy,
aby nie zapisywać danych do plików, a przy tym rejestrować liczbę wywołań
w polu itemsWritten i mieć pewność, że do metody przekazywane są oczekiwane
dane, czyli oferty przekształcone na łańcuchy znaków .
Następny krok polega na użyciu atrapy klasy FileOutputStream. Ponieważ klasa
DealExporter zapisuje dane do strumienia wyjścia zwróconego przez metodę
openOutputStream kontekstu, trzeba utworzyć namiastkę tej metody i zwrócić
w niej obiekt klasy MockOutputStream. Android udostępnia klasę bazową (o nazwie
MockContext) do tworzenia atrap kontekstów, jednak klasa ta jedynie zgłasza
wyjątek UnsupportedOperationException, dlatego trzeba zaimplementować potrzebne
metody. Tu piszemy metodę openFileOutput zwracającą nowy egzemplarz klasy
MockOutputStream .
Na razie nie przyjrzeliśmy się jeszcze uruchamianemu testowi. Test testShould
´ExportItems ma wykonywać dwie operacje — wywoływać mechanizm ekspor-
towania za pomocą obiektu klasy MyMockContext i sprawdzać, czy liczba zgłoszo-
nych wywołań jest prawidłowa . To wystarczy, aby się upewnić, czy mechanizm
eksportowania działa!
OMÓWIENIE
W przedstawionym zadaniu testowym zaimplementowaliśmy niestandardową
klasę dziedziczącą po klasie MockContext. Takie rozwiązanie należy zastosować,
kiedy w teście potrzebne są niestandardowe operacje. Jednak czasem nie trzeba
tworzyć nowej klasy. Często pożądany jest kompletny kontekst, który działa
w określony sposób w środowisku testowym. W Androidzie zdefiniowanych jest
kilka wyspecjalizowanych implementacji klasy Context, jednak łatwo je przeoczyć,
ponieważ — w odróżnieniu od klas MockContext, MockApplication i MockService —
nie znajdują się w pakiecie android.test.mock, ale w pakiecie nadrzędnym
android.test. Najważniejszą z tych klas jest RenamingDelegatingContext. Jest to
nakładka na kontekst, którą można wykorzystać w testach z instrumentacją do
przekierowywania danych z bazy lub obiektu klasy SharedPreferences z podsta-
wowego kontekstu do specjalnych plików testowych. Pozwala to zagwarantować,
że test nie zastąpi ustawień ani wpisów w bazie dodanych przez aplikację.
Z korzystaniem z atrap związany jest problem z ich wprowadzaniem (ang.
injection) do aplikacji. Jeśli chcemy na potrzeby testów zastąpić obiekt jego odpo-
wiednikiem, potrzebujemy sposobu na uzyskanie tego efektu. Służą do tego trzy
podstawowe techniki. Oto one:
1. Ręczne używanie metod ustawiających i konstruktorów.
2. Automatyczne używanie metod ustawiających i konstruktorów.
3. Korzystanie z manipulowania kodem bajtowym i generowania go w czasie
wykonywania programu.
560 ROZDZIAŁ 13. Testowanie i instrumentacja

W podejściach 1. i 2. należy udostępnić metody ustawiające lub konstruktory,


które umożliwiają zastąpienie danego obiektu inną implementacją. W tej technice
zrobiliśmy to ręcznie — skonfigurowaliśmy klasę DealExporter w konstruktorze
przy użyciu atrapy kontekstu. Podejście to bywa żmudne. Ten sam efekt można
uzyskać automatycznie. Często używa się do tego frameworków obsługi cyklu
życia obiektów. Frameworki te mają funkcję wprowadzania zależności. Przy-
kładowe narzędzia tego typu to framework Spring lub Google Guice. Pozwalają
one deklarować zależność od innych klas lub interfejsów, przy czym programista
nie musi samodzielnie wskazywać komponentów, od których zależny jest kod,
ponieważ framework automatycznie wiąże wszystkie zarządzane obiekty w czasie
wykonywania programu. Jest to wzorzec odwrócenie sterowania (ang. inversion of
control — IoC), ponieważ w obiektach nie trzeba samodzielnie zarządzać zależ-
nościami — wystarczy zadeklarować zależności, a za podłączanie właściwych
komponentów odpowiada kontener.
Jak wykorzystać to przy stosowaniu atrap w testach? Można wyrazić następu-
jące żądanie: jeśli aplikacja pracuje w trybie testów, należy użyć atrapy; w prze-
ciwnym razie trzeba zastosować zwykłą implementację. Nie trzeba ręcznie
wywoływać procedur ustawiających. Po zainicjowaniu obiektu kontener z wzorca
IoC odpowiada za obsługę wszystkich zależności — zarówno z atrap, jak i zwy-
kłych! Framework Google Guice zaadaptowano do Androida w ramach projektu
RoboGuice (http://code.google.com/p/roboguice/), jednak warto pamiętać, że
choć frameworki tego rodzaju są wygodne w użyciu, zużywają dużo zasobów.
Zwiększają czas uruchamiania aplikacji i ogólne zużycie pamięci. Trzecim i ostat-
nim sposobem na wprowadzenie atrap jest manipulowanie kodem bajtowym
i generowanie go w czasie wykonywania programu. Tak działa większość spraw-
dzonych bibliotek atrap (na przykład Mockito, EasyMock lub PowerMock)
w świecie Javy. Biblioteki te potrafią tworzyć atrapy z implementacjami klas i inter-
fejsów w czasie wykonywania programu, a nawet modyfikować istniejące metody,
aby zwracały dowolne dane lub wykonywały odpowiednie operacje. Tajną bronią
autorów tych narzędzi jest biblioteka do generowania kodu Javy, cglib. Choć
bibliotekę EasyMock w pewnym stopniu przeniesiono na Android w ramach
projektu android-mock (http://code.google.com/p/android-mock), biblioteki zależne
od biblioteki cglib nie działają w Dalviku, ponieważ kod generowany przez cglib
nie jest zgodny z maszyną wirtualną Dalvik. Ponadto niektóre z omawianych
narzędzi korzystają z pakietu java.beans, którego nie ma w bibliotekach frame-
worku Androida.
Rozwiązaniem jest rezygnacja z androidowych testów z instrumentacją na
rzecz standardowych testów JUnit uruchamianych w maszynie JVM i utworzenie
atrap wszystkich potrzebnych klas frameworku za pomocą wspomnianych biblio-
tek. Aby zobrazować to podejście, pokazujemy, jak zaimplementować atrapy
z listingu 13.6 z wykorzystaniem biblioteki Mockito:
0 TECHNIKA 79. Przyspieszanie testów jednostkowych z zastosowaniem Robolectrica 561

FileOutputStream mockOutput = mock(FileOutputStream.class);


verify(mockOutput.write((byte[]) anyObject()).times(2);
Context mockContext = mock(Context.class);
when(mockContext.openFileOutputStream(anyString(),
anyInt())).thenReturn(mockOutput);

Mockito udostępnia język DSL do tworzenia atrap i asercji. Język ten umożliwia
łatwe pisanie i rozumienie testów z atrapami. Największy problem z tym podej-
ściem polega na tym, że zwykle potrzebnych jest tak wiele klas frameworku
Androida, iż trzeba niemal zaimplementować Android od nowa, używając nie-
standardowych atrap. Ludzie z firm Xtreme Labs i Pivotal szybko dostrzegli
ten problem i wymyślili zupełnie nowe rozwiązanie. Na pomoc przybywa
Robolectric!

0 TECHNIKA 79. Przyspieszanie testów jednostkowych z zastosowaniem


Robolectrica

Poznałeś już wiele rodzajów testów — zwykłe testy JUnit z bibliotekami atrap lub
bez nich, androidowe testy jednostkowe, androidowe testy funkcjonalne, a także
testy z wykorzystaniem mało rozbudowanych atrap z Androida. Zaletą zwykłych
testów JUnit uruchamianych w maszynie JVM jest ich szybkość, trzeba jednak
utworzyć atrapę wielu części biblioteki frameworku Androida, natomiast testy
z instrumentacją pozwalają wykorzystać obiekty platformy, ale działają wolno
i mają tylko ograniczoną obsługę atrap.
Jeśli szybkość ma znaczenie, można zastosować nowy sposób pisania testów
i wykorzystać framework Robolectric do przeprowadzania testów jednostkowych
(http://pivotal.github.com/robolectric/). Robolectric ma pozwolić „okiełznać plik jar
pakietu SDK Androida” i przeprowadzać testy jednostkowe aplikacji w standar-
dowej maszynie JVM bez konieczności bezpośredniego tworzenia atrap każdej
klasy frameworku, która używana jest w drzewie wywołań i może powodować
zgłoszenie w trakcie testów wyjątku RuntimeException("Stub!"). Framework
Robolectric automatycznie sam tworzy atrapy klas frameworku Android. Pro-
gramista nie musi tego robić. Dlatego Robolectric można traktować jako bardzo
rozbudowaną atrapę Androida!
PROBLEM
Chcemy tworzyć szybko działające testy, na przykład na potrzeby podejścia TDD,
jednak uruchamianie testów w maszynie JVM wymaga przygotowania atrap dużej
części frameworku Androida.
ROZWIĄZANIE
Na zapleczu Robolectric udostępnia klasy zastępcze (ang. shadow class) dla nie-
których klas frameworku Androida. Klasa zastępcza wygląda i działa jak jej
androidowy odpowiednik, jednak jest zaimplementowana w standardowej Javie
i działa na zwykłej maszynie JVM. Na przykład w teście opartym na Robolectricu
562 ROZDZIAŁ 13. Testowanie i instrumentacja

egzemplarz klasy Activity z Androida jest automatycznie zastępowany egzempla-


rzem klasy ShadowActivity. Ta ostatnia jest rozbudowaną atrapą, choć zaimple-
mentowane są w niej te same metody co w zwykłej klasie Activity. Obsługiwana
jest na przykład metoda findViewById. Atrapa umożliwia też rozwijanie widoków
do klas i zwraca egzemplarz klasy View z metodą show. Robolectric nie uruchamia
jednak żadnych procedur graficznych wyświetlających układy lub widoki. Jedy-
nie udaje, że to robi. Działa więc szybko, a przy tym pozwala sprawdzać widoki
i układy w taki sposób, jak w zwykłych zadaniach testowych w Androidzie. Oprócz
widoczności i położenia widoku można sprawdzić jego stan i działanie, na
przykład uruchamianie nowych aktywności w reakcji na kliknięcie widoku.
Klasę frameworku może zastępować dowolna klasa. W klasie zastępczej nie
trzeba udostępniać pełnego zestawu metod interfejsu. Metody niezaimplemen-
towane w klasie zastępczej są generowane przez Robolectric i nie wykonują
żadnych operacji lub zwracają wartość null. To podejście to tworzenie częściowej
namiastki lub częściowej atrapy.
Wygodnym aspektem tego rozwiązania jest to, że zwykle nie trzeba przej-
mować się klasami zastępczymi. Robolectric podłącza się do procedury wczytu-
jącej klasę i kiedy wykryje żądanie do podstawowej klasy Androida, automatycznie
podstawia za nią klasę zastępczą. Proces ten przedstawiono na rysunku 13.12.

Rysunek 13.12. Mechanizm uruchamiania testów w Robolectricu rejestruje


niestandardową ładowarkę klas, która przechowuje wszystkie żądania klas
zgłaszane przez testowaną aplikację (na przykład żądania klasy Context).
Następnie zamiast egzemplarza klasy Context zwraca implementację zastępczą

Jak pokazano na rysunku 13.12, testy w Robolectricu to testy JUnit 4 (wersja


JUnit 3 nie jest obsługiwana) wykonywane za pomocą klasy RobolectricTestRunner.
Nie trzeba przeprowadzać dodatkowej konfiguracji. Używane są zwykłe testy
JUnit 4, obsługiwane na zapleczu przez Robolectric.
POBIERZ PROJEKT DEALDROIDROBOLECTRICTEST. Kod źródłowy
projektu i pakiet APK do uruchamiania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Ponieważ niektóre listingi skró-
0 TECHNIKA 79. Przyspieszanie testów jednostkowych z zastosowaniem Robolectrica 563

cono, abyś mógł skoncentrować się na konkretnych zagadnieniach, zale-


camy pobranie kompletnego kodu źródłowego i śledzenie go w Eclipse
(lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/zP3n.
Zauważ, że nie jest to androidowy projekt ze środowiska Eclipse — to
zwykły projekt Javy.
Niestety, prawidłowe skonfigurowanie projektów testowych Robolectrica wymaga
nieco pracy. Szczegółowe instrukcje znajdziesz w witrynie projektu Robolectric
(http://pivotal.github.com/robolectric). Ogólne uwagi i wskazówki przedstawiamy
w ramce „Konfigurowanie projektów testowych Robolectrica”.

Konfigurowanie projektów testowych Robolectrica


Testy Robolectrica (w odróżnieniu od androidowych testów z instrumentacją) dzia-
łają na zwykłych maszynach JVM. Najlepsze podejście polega na tworzeniu testów
wykorzystujących Robolectric w zwykłych projektach Javy, a nie w projektach
testowych Androida. Dla użytkowników środowiska Eclipse oznacza to, że projekt nie
jest dedykowany dla Androida, dlatego trzeba samodzielnie dodać zależność od pli-
ków JAR Androida. W Eclipse można to zrobić na przykład za pomocą opcji User
Libraries.
Kliknij projekt prawym przyciskiem myszy i wybierz opcję Build Path/Add Libra-
ries…/User Library/User Libraries…/New. Wprowadź nazwę i wybierz opcję Add JARs,
a następnie wybierz pliki android.jar i maps.jar z katalogu głównego pakietu SDK.
Ponadto do ścieżki budowania należy dodać plik robolectric-all.jar, a także bibliotekę
JUnit 4.
JUnit 4 — kliknij projekt prawym przyciskiem myszy i wybierz opcję Build Path/
Add Libraries…/JUnit/JUnit 4.
Robolectric — skopiuj plik robolectric-all.jar do katalogu w projekcie testowym,
kliknij ten plik prawym przyciskiem myszy i wybierz opcję Build Path/Add to
Build Path.

W ramach ćwiczenia pokazujemy, jak zmodyfikować test DealDetailsTest


z listingu 13.2 przy użyciu Robolectrica i frameworku JUnit 4. W tym momencie
warto cofnąć się do tego listingu i porównać go z testem wykorzystującym Robo-
lectric (listing 13.7). Pomoże Ci to zrozumieć różnice między przedstawionymi
podejściami.

Listing 13.7. Testy wykorzystujące Robolectric można uruchamiać poza urządzeniem


lub emulatorem

@RunWith(RobolectricTestRunner.class)
public class DealDetailsRobolectricTest {

private DealDetails activity;

private Item testItem;

@Before
564 ROZDZIAŁ 13. Testowanie i instrumentacja

public void setUp() {


testItem = new Item();
testItem.setItemId(1);
testItem.setTitle("Element testowy");
testItem.setConvertedCurrentPrice("1");
testItem.setLocation("USA");
testItem.setDealUrl("http://example.com");

activity = new DealDetails();


DealDroidApp application =
(DealDroidApp) activity.getApplication();
application.setCurrentItem(testItem);

activity.onCreate(null);
}

@Test
public void testPreConditions() {
assertNotNull(activity.findViewById(R.id.details_price));
assertNotNull(activity.findViewById(R.id.details_title));
assertNotNull(activity.findViewById(R.id.details_location));
}

@Test
public void testThatAllFieldsAreSetCorrectly() {
assertEquals("$" + testItem.getConvertedCurrentPrice(),
getViewText(R.id.details_price));
assertEquals(testItem.getTitle(),
getViewText(R.id.details_title));
assertEquals(testItem.getLocation(),
getViewText(R.id.details_location));
}

@Test
public void testThatItemCanBeDisplayedInBrowser() {
activity.onOptionsItemSelected(new TestMenuItem() {
public int getItemId() {
return DealDetails.MENU_BROWSE;
}
});

ShadowActivity shadowActivity =
Robolectric.shadowOf(activity);
Intent startedIntent =
shadowActivity.getNextStartedActivity();

assertEquals(Intent.ACTION_VIEW, startedIntent.getAction());
assertEquals(testItem.getDealUrl(),
startedIntent.getData().toString());
}

private String getViewText(int textViewId) {


return ((TextView) activity.findViewById(textViewId)).getText()
.toString();
}
}
0 TECHNIKA 79. Przyspieszanie testów jednostkowych z zastosowaniem Robolectrica 565

Jak widać, różnice w porównaniu z testem DealDetailsTest nie są znaczne, choć


tym razem w ogóle nie używamy frameworku testowego z Androida. Różnice
w kodzie dotyczą głównie szczegółów. Przede wszystkim używamy tu wersji
JUnit 4, natomiast w androidowych testach zawsze stosuje się znacznie starszą
wersję JUnit 3. W JUnit 4 często korzysta się z przypisów (ang. annotations) Javy,
co pozwala „nieinwazyjnie” wprowadzić bibliotekę testową. W podobny sposób
w Robolectricu nie trzeba tworzyć podklas. Zamiast tego dostępny jest mechanizm
uruchamiania testów JUnit 4 w postaci klasy RobolectricTestRunner. Można go
dodać z wykorzystaniem przypisu @RunWith frameworku JUnit 4 . Metody testowe
nie muszą zaczynać się od członu test*; wystarczy określić ich przeznaczenie
przypisem @Test.
Kod konfiguracyjny znajduje się w metodzie setUp, choć w JUnit 4 metoda
ta może mieć dowolną nazwę, przy czym trzeba podać przypis @Before. Może
zauważyłeś, że egzemplarz klasy DealDetails tworzymy samodzielnie, a nie za
pomocą metody getActivity. Wywołujemy natomiast metodę getApplication
testowanej aktywności, ponieważ wywołanie to jest przechwytywane przez
Robolectric, który automatycznie tworzy egzemplarz klasy DealDroidApp i wywo-
łuje metodę onCreate . Jak to możliwe, że Robolectric jest tak sprytny, iż potrafi
ustalić odpowiednią klasę aplikacji? Analizuje manifest aplikacji w celu określe-
nia nazwy klasy. Jest aż tak sprytny!
W testach wykorzystujących Robolectric zawsze trzeba bezpośrednio wywo-
ływać metody cyklu życia komponentu. W urządzeniu Android wywołuje je auto-
matycznie, jednak Robolectric nie obsługuje zarządzania cyklem życia andro-
idowych komponentów. Uruchamiane są te metody, które Ty sam wywołasz.
Dlatego w kodzie bezpośrednio wywołujemy metodę activity.onCreate z argu-
mentem null .
Metody testowe są niemal takie same jak wcześniej. Wyjątkiem jest metoda
testThatItemCanBeDisplayedInBrowser. Dobrze widać w niej niektóre z najważniej-
szych cech typowych dla Robolectrica. Przypomnijmy, co testuje ta metoda —
sprawdza, czy po wybraniu opcji menu odpowiadającej identyfikatorowi MENU_
´BROWSER uruchamiana jest intencja, która powoduje uruchomienie przeglądarki
Androida i otwarcie strony z ofertą w witrynie eBaya.
Warto pamiętać, że testy nie są przeprowadzane w urządzeniu, dlatego nie
można wyświetlić opcji menu i wcisnąć przycisku. Można jednak uruchomić funk-
cję, którą Android wywołałby, gdyby użytkownik otworzył menu. Jest to bezpieczne
podejście, o ile twórcy Androida nie zmodyfikują procesu konfigurowania i obsługi
menu aplikacji.
Tu symulujemy wciśnięcie opcji menu przez bezpośrednie uruchomienie
wywołania zwrotnego onOptionsItemSelected i przekazanie do niego obiektu klasy
TestManuItem Robolectrica . Obiekt ten odpowiada przyciskowi Przeglądarka.
Jest to typowy wzorzec stosowany w testach wykorzystujących Robolectric.
Wiemy, że dana metoda jest wywoływana przez Android w czasie wykonywania
566 ROZDZIAŁ 13. Testowanie i instrumentacja

programu, dlatego uruchamiamy ją sami i przekazujemy do niej dowolne dane;


w związku z tym metoda szybko kończy pracę.
Jak jednak sprawdzić, czy to podejście prowadzi do uruchomienia przeglą-
darki? Zwykła aktywność Androida nie umożliwia sprawdzenia, czy uruchomiono
w niej inne aktywności. Jednak aktywność zastępcza pozwala to stwierdzić! Robo-
lectric rejestruje w zastępniku testowanej aktywności wszystkie uruchomione
w niej intencje. Oznacza to, że można pobrać referencję do zastępnika testowa-
nej aktywności i sprawdzić, czy wywołana została odpowiednia intencja. Aby
pobrać referencję, wystarczy wywołać metodę Robolectric.shadowOf. Potem można
wywołać metodę getNextStartedActivity zastępczej aktywności i zastosować
zwykłą asercję dla intencji. Warto zauważyć, że istnieje także zastępnik intencji.
Również ten zastępnik można pobrać za pomocą metody pomocniczej shadowOf.
OMÓWIENIE
Jest kilka ważnych zalet korzystania z Robolectrica. Po pierwsze, framework
ten — co niezwykle istotne — jest szybki. Po drugie, nie wymaga urządzenia ani
emulatora do przeprowadzania testów, ponieważ nie korzysta z natywnego śro-
dowiska uruchomieniowego Androida. Oznacza to, że nie trzeba zarządzać emu-
latorami i obrazami urządzenia (co bywa trudne na bezwejściowych serwerach
budowania), a wyniki testów są dostępne bardzo szybko. Ponadto z uwagi na to,
że Robolectric można traktować jak rozbudowany framework atrap dla Androida,
często można zrezygnować z innych bibliotek atrap, takich jak Mockito (choć
można z nich korzystać). Ponieważ Robolectric oparty jest na JUnit 4 i standar-
dowej Javie, można stosować dowolne dodatkowe biblioteki — nawet te, które nie
działają w Dalviku.
Niestety, Robolectric ma też pewne wady. Cegiełkami są tu klasy zastępcze,
wzorowane na ich odpowiednikach z Androida. Jest to największy problem zwią-
zany z Robolectriciem. Przede wszystkim, gdy pisaliśmy tę książkę, dostępnych
było tylko 75 takich klas. Może się wydawać, że to dużo, jednak to tylko ułamek
setek klas frameworku Androida, które mogą (bezpośrednio lub pośrednio) być
używane w teście. Nie stanowiłoby to dużego problemu, gdyby istniał łatwy
i „nieinwazyjny” sposób na dodawanie własnych klas, jednak choć twórcy Robo-
lectrica utrzymują, że proces dodawania niestandardowych klas zastępczych jest
prosty, mamy na ten temat inne zdanie. Autorzy Robolectrica nie udostępnili
interfejsu API do rejestrowania niestandardowych klas zastępczych. Aby dodać
taką klasę, trzeba wprowadzać zmiany w kodzie źródłowym biblioteki. Dlatego
twórcy Robolectrica zachęcają do umieszczenia kodu źródłowego biblioteki
w podkatalogu testowanej aplikacji (lub powiązanym projekcie testowym) i wpro-
wadzenia potrzebnych zmian.
W czasie korzystania z Robolectrica zastanowiła nas konieczność utworzenia
egzemplarza aplikacji. To dziwne, ponieważ Robolectric nigdy nie wywołuje
metod cyklu życia komponentów — wyjątkiem jest właśnie egzemplarz aplikacji.
Robolectric zawsze tworzy taki egzemplarz i wywołuje jego metodę onCreate.
0 TECHNIKA 80. Przeprowadzanie testów obciążeniowych za pomocą narzędzia Monkey 567

Czasem wymaga to utworzenia atrap w klasie aplikacji, nawet jeśli nie są uży-
wane (na przykład w trakcie testów jednostkowych usługi).
Warto też zauważyć, że przeważnie nie można zakładać, iż przejście testów
oznacza poprawne działanie aplikacji. Ponieważ testy nie są przeprowadzane
w środowisku uruchomieniowym Androida, a tylko w naśladującym go systemie,
nie można mieć całkowitej pewności, że aplikacja będzie działała w taki sam
sposób w telefonie. Jeśli firma Google zdecyduje się na przykład zmienić dzia-
łanie metody findViewById, autorzy Robolectrica będą musieli zmodyfikować
jej implementację. W przeciwnym razie testy będą wykonywane na podstawie
implementacji, która nie odzwierciedla pracy Androida. Z drugiej strony w pro-
stych testach, takich jak sprawdzanie, czy widok istnieje i jest dostępny, można
bezpiecznie stosować asercje za pomocą Robolectrica, ponieważ narzędzie to
dobrze obsługuje widoki. Jednak większość mechanizmów Androida prawdo-
podobnie się nie zmieni, dlatego opisany problem nie jest tak poważny, na jaki
wygląda.
Podsumujmy rozważania — musisz sam zdecydować, czy zalety Robolectrica
przeważają nad wadami. Narzędzie to jest dobrą alternatywą dla androidowych
testów jednostkowych, jednak nie stanowi uniwersalnego rozwiązania.
Omówiliśmy wiele zagadnień. We wszystkich przedstawionych podejściach
występuje pewien wspólny aspekt — koncentracja na funkcjach. Każdy napisany
do tej pory test sprawdzał, czy badana jednostka działa zgodnie z określoną spe-
cyfikacją. Na początku rozdziału wspomnieliśmy, że nie jest to jedyny możliwy
rodzaj testów. Można też sprawdzać cechy aplikacji, takie jak szybkość lub sta-
bilność. W ostatniej technice w tym rozdziale pokazujemy, jak robić to z wyko-
rzystaniem małpy. Małpy? Jeśli to nie zachęci Cię do dalszej lektury, sami nie
wiemy, co jeszcze możemy wymyślić.

0 TECHNIKA 80. Przeprowadzanie testów obciążeniowych za pomocą


narzędzia Monkey

Izolowane testy komponentów aplikacji i oparte na scenariuszach testy kom-


pleksowe są niezbędne do sprawdzenia, czy program działa prawidłowo. To
jednak nie koniec. Bezbłędna aplikacja może działać wolno. Ponadto aplikacje
czasem działają poprawnie w standardowych warunkach, jednak przy obciążeniu
szybko przestają reagować lub występuje w nich wyciekanie pamięci. Opisywane
wcześniej w rozdziale testy funkcjonalne nie są odpowiednie do wykrywania tego
rodzaju defektów.
Aspekty w rodzaju szybkości lub stabilności trudno jest sprawdzić w normal-
nych warunkach, w których zwykle pracują użytkownicy aplikacji. Użytkownicy
korzystają zwykle tylko z niektórych funkcji, wprowadzają dane i klikają przyciski
w normalnym tempie itd. Często defekty aplikacji ujawniają się tylko przy dużym
obciążeniu, dlatego potrzebny jest wygodny sposób na jego zasymulowanie.
Pewna możliwość to zainstalowanie aplikacji w telefonie i obciążenie jej przez
568 ROZDZIAŁ 13. Testowanie i instrumentacja

szybkie wciskanie przycisków w celu sprawdzenia, czy doprowadzi to do awarii.


Jednak nie tego rodzaju wygody oczekujemy. Istnieje lepszy sposób. Poznaj
„małpę” — narzędzie Monkey.
PROBLEM
Chcemy przeprowadzić testy obciążeniowe aplikacji przez wysyłanie do niej
serii losowych zdarzeń i zbieranie informacji o awariach lub wyczerpaniu się
pamięci.
ROZWIĄZANIE
Choć może wydawać się to dziwne, jednym z najlepszych sposobów na spraw-
dzenie stabilności i niezawodności aplikacji jest używanie jej w niestandardowy
sposób. Aplikacje projektuje się i rozwija z myślą o konkretnych ścieżkach ope-
racji wykonywanych przez użytkowników. Jest to uzasadnione podejście. Pro-
gramista zaczyna od pewnego rodzaju opisu funkcji, który zwykle uwzględnia
użytkownika i stosowane elementy interfejsu, a następnie projektuje i implemen-
tuje aplikację zgodnie z tym opisem. Co się jednak dzieje, kiedy użytkownik
wybiera inną ścieżkę, której nie uwzględniono w projekcie? Programista może
stwierdzić: „Z pewnością nikt nie wybierze tej opcji na tym ekranie” lub „Na
pewno nikt nie obróci ekranu w czasie wczytywania danych przez aplikację”.
A może jednak?
Nie zamierzamy posuwać się do porównywania przeciętnego użytkownika
aplikacji do małpy, ale z pewnością część osób korzysta z programów w nieocze-
kiwany sposób. Jeśli w aplikacji wystąpią choćby niewielkie problemy z szybko-
ścią reagowania, można mieć niemal pewność, że użytkownik zacznie chaotycz-
nie stukać w ekran, aby uzyskać jakąkolwiek reakcję. Nie wie jednak, że to tylko
pogarsza sytuację, ponieważ w mocno obciążonym systemie do kolejki trafiają
dodatkowe zdarzenia związane z wprowadzaniem danych. Narzędzie Monkey na
Android pozwala zasymulować taką sytuację. Monkey to tester aplikacji i inter-
fejsu użytkownika. Działa w urządzeniach z Androidem i potrafi wysyłać serie
pseudolosowych zdarzeń związanych z wprowadzaniem danych, aby przepro-
wadzić testy obciążeniowe. Ponieważ narzędzie działa w urządzeniu lub emu-
latorze, trzeba je wywołać zdalnie za pomocą programu adb:

Polecenie adb shell powoduje przekierowanie podanych dalej instrukcji do


powłoki urządzenia (zobacz dodatek A). Tu z użyciem instrukcji uruchamiamy
w urządzeniu narzędzie Monkey. Przyjmuje ono dwa zbiory argumentów — listę
opcji i liczbę zdarzeń, które ma wygenerować (tu jest ich 500). Informacją, którą
0 TECHNIKA 80. Przeprowadzanie testów obciążeniowych za pomocą narzędzia Monkey 569

zawsze należy przekazywać, jest nazwa pakietu testowanej aplikacji, podawana


z wykorzystaniem opcji –p. Przed przejściem do szczegółowego omówienia warto
uruchomić polecenie i zobaczyć, jaki jest tego efekt (rysunek 13.13).

Rysunek 13.13. Trzy zrzuty wykonane w czasie sprawdzania aplikacji DealDroid


przez narzędzie Monkey. Narzędzie to w pseudolosowy sposób porusza się
po aplikacji. Wybiera klawisze i przyciski bez konkretnego celu lub planu

Najlepiej uruchomić polecenie samodzielnie, aby zobaczyć, w jaki sposób działa


narzędzie Monkey. Na rysunku pokazano, co my zobaczyliśmy. Trzy zrzuty
z rysunku 13.13 wykonaliśmy w przypadkowych momentach testowania aplikacji
przez narzędzie. Ekrany pokazane są w kolejności chronologicznej (od lewej do
prawej). Testy obciążeniowe najpierw zawsze uruchamiają początkową aktyw-
ność, którą tu jest DealList. Wyświetla ona modalne okno dialogowe, dlatego
interakcje z aplikacją nie są obsługiwane do czasu wczytania ofert. Jednak narzę-
dzie Monkey przez cały czas generuje zdarzenia. Po wczytaniu listy ofert narzędzie
najpierw zmienia orientację ekranu, potem otwiera pole opcji i wybiera pierwszą
pozycję. Po wyświetleniu nowej listy wybiera ofertę, co prowadzi do uruchomienia
aktywności DealDetails. Na tym ekranie narzędzie przez kilka sekund wybiera
różne pozycje w menu opcji.
W GRANICACH PAKIETU. Niezależnie od czasu działania narzędzia
Monkey można zauważyć, że nigdy nie wychodzi ono poza aplikację Deal-
Droid. Dlaczego? W końcu istnieje opcja Przeglądarka, która powoduje
przejście do witryny z ofertą. Jednak nawet jeśli narzędzie wybierze tę
opcję, nic się nie stanie. Jest tak, ponieważ opcja –p ogranicza pracę narzę-
dzia do danego pakietu. Wszelkie zdarzenia prowadzące do uruchomienia
aktywności spoza pakietu są pomijane. Podejście to doskonale nadaje się
do testowania aplikacji w izolacji. Jeśli chcesz uwzględnić inne aktywności
i aplikacje osiągalne z danego programu, musisz podać pakiet każdej takiej
570 ROZDZIAŁ 13. Testowanie i instrumentacja

aplikacji wraz z opcją –p. Aby umożliwić narzędziu otwarcie przeglądarki,


trzeba uruchomić je za pomocą instrukcji monkey –p com.manning.aip.
´dealdroid –p com.android.browser. Jednak wtedy narzędzie może najpierw
uruchomić przeglądarkę i przez pewien czas ją testować, a dopiero potem
przejść do aplikacji DealDroid.
Dobra wiadomość jest taka, że aplikacja nigdy nie przestała reagować ani nie
uległa awarii. Testy obciążeniowe zakończyły się powodzeniem. Wygląda na to, że
dobrze ją zaimplementowaliśmy! Oto dane wyjściowe z powłoki:
matthias:[~]$ adb shell monkey -p com.manning.aip.dealdroid 500
Events injected: 500
## Network stats: elapsed time=22791ms (22791ms mobile, 0ms wifi, 0ms not
connected)

Narzędzie zgłosiło 500 zdarzeń, a ich obsługa zajęła około 23 sekundy. Tyle
samo czasu aktywne było mobilne połączenie do transferu danych (w emulatorze
nie ma to znaczenia, może być jednak istotne przy korzystaniu z urządzenia).
To dobrze, że aplikacja działa tak sprawnie, jednak z ciekawości wprowadźmy
pewien błąd i zgłośmy wyjątek RuntimeException w metodzie DealList.onCreate.
Oto efekt:
matthias:[~]$ adb shell monkey -p com.manning.aip.dealdroid 500
// CRASH: com.manning.aip.dealdroid (pid 1638)
// Short Msg: java.lang.RuntimeException
// Long Msg: java.lang.RuntimeException: Boom!
// Build Label: generic/google_sdk/generic/:2.2/FRF91/43546:eng/test-keys
// Build Changelist: 43546
// Build Time: 1277937122000
// java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.manning.aip.dealdroid/
com.manning.aip.dealdroid.DealList}: java.lang.RuntimeException: Boom!
// at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2663)
// [lengthy stack trace here]
// ... 11 more
//
** Monkey aborted due to error.
Events injected: 12
## Network stats: elapsed time=1893ms (1893ms mobile, 0ms wifi, 0ms not
connected)
** System appears to have crashed at event 12 of 500 using seed 0

Także tym razem narzędzie Monkey miało zgłosić 500 zdarzeń, jednak przy 12.
z nich nastąpiła awaria — aplikacja zgłosiła dodany przez nas wyjątek. Otrzyma-
liśmy wszystkie standardowe informacje, takie jak klasę wyjątku i powiązany
komunikat, a także stos wywołań (tu skrócony w celu zwiększenia czytelności
przykładu).
KOD WYJŚCIA W NARZĘDZIU MONKEY. Jeśli zamierzasz uruchamiać
narzędzie Monkey w ramach automatycznego budowania (rozdział 14.), nie
stosuj kodu wyjścia tego narzędzia do określania powodzenia lub niepo-
wodzenia testu. Zgodne z UNIX-em narzędzia uruchamiane z wiersza
poleceń zwykle zwracają 0, aby poinformować o powodzeniu, a inna liczba
0 TECHNIKA 80. Przeprowadzanie testów obciążeniowych za pomocą narzędzia Monkey 571

(zwykle –1) oznacza błąd. Narzędzie Monkey zawsze zwraca 0, tak więc
informuje o sukcesie nawet wtedy, kiedy zakończyło pracę z powodu
wystąpienia błędu w aplikacji. Problem ten jest znany i zgłoszony jako
usterka 13562 w oficjalnym systemie śledzenia błędów Androida.
Diagnostyczne dane wyjściowe informują, że w aplikacji wystąpiła awaria, nie
wiadomo jednak, które zdarzenie do niej doprowadziło. Komunikat „Event 12” nie
jest zbyt przydatny. Problem mogło spowodować dowolne zdarzenie. Aby uzyskać
więcej informacji na temat zgłoszonych zdarzeń, można wywołać narzędzie
Monkey z opcją –v, służącą do rejestrowania opisowych informacji. W tym podej-
ściu rejestrowane jest każde zgłoszone zdarzenie. Dołączane jest też podsu-
mowanie określające częstotliwość zgłaszania poszczególnych zdarzeń.
matthias:[~]$ adb shell monkey -p com.manning.aip.dealdroid -v 500
:Monkey: seed=0 count=500
:AllowPackage: com.manning.aip.dealdroid
:IncludeCategory: android.intent.category.LAUNCHER
:IncludeCategory: android.intent.category.MONKEY
// Event percentages:
// 0: 15.0%
// 1: 10.0%
// 2: 15.0%
// 3: 25.0%
// 4: 15.0%
// 5: 2.0%
// 6: 2.0%
// 7: 1.0%
// 8: 15.0%

Najwyraźniej zdarzenia zgłaszane przez narzędzie Monkey nie są tak losowe, jak
to początkowo sugerowaliśmy. To prawda — zdarzenia są generowane pseudolo-
sowo i można nimi sterować, aby niektóre ich rodzaje zgłaszane były częściej od
innych. Pseudolosowość oznacza tu, że Monkey używa ziarna do randomizacji
sekwencji zgłaszanych zdarzeń. Ziarno można podać ręcznie za pomocą opcji –s.
Dla takich samych ziaren Monkey zgłasza dokładnie tę samą sekwencję zdarzeń.
Oznacza to, że po niepowodzeniu testu można go odtworzyć przez przekazanie
do narzędzia tego samego ziarna.
POWTARZALNE PRZEBIEGI TESTOWE. Aby można było powtórzyć
test po wystąpieniu błędu, zawsze należy ręcznie podawać ziarno. Dobry
wybór to użycie aktualnego UNIX-owego znacznika czasu w milisekun-
dach. Znacznik ten można uzyskać z narzędzia date z systemu GNU
w następujący sposób:
$adb shell monkey –p <package> -s `date +%s` -v 500

Znaki ` powodują uruchomienie narzędzia date i połączenie danych wyj-


ściowych z poleceniem. Nie zapomnij użyć opcji –v, aby ziarno użyte do
uruchomienia danego testu znalazło się w dzienniku:
:Monkey: seed=1293818128 count=500
572 ROZDZIAŁ 13. Testowanie i instrumentacja

Określone ziarno znacznie ułatwia uruchamianie narzędzia Monkey


w ramach automatycznego budowania. Zagadnienie to omawiamy w roz-
dziale 14.
Narzędzie Monkey potrafi zgłaszać dziewięć różnych rodzajów zdarzeń. Progra-
mista może kontrolować względną częstotliwość zgłaszania zdarzeń poszcze-
gólnych typów. W tabeli 13.1 znajduje się przegląd różnych rodzajów zdarzeń
i odpowiadających im opcji z wiersza poleceń. Częstotliwość należy podawać
w procentach jako wartości z przedziału od 0 do 100.
Tabela 13.1. Rodzaje zdarzeń obsługiwane przez narzędzie Monkey i opcje
używane do sterowania nimi

Rodzaj zdarzenia Opis Opcja


Dotknięcie Dotknięcie lub przyciśnięcie ekranu --pct-touch
dotykowego (opuszczenie i podniesienie
palca).
Ruch Przeciągnięcie (opuszczenie, przesunięcie --pct-motion
i podniesienie palca).
Trackball Poruszenie trackballem. --pct-trackball
Podstawowa nawigacja Nawigacja za pomocą DPada. --pct-nav
Zaawansowana nawigacja Środkowy przycisk DPada i przycisk Menu*. --pct-majornav
Klawisze systemowe Home, Back, Call, End call, Volume up, --pct-syskeys
Volume down, Mute.
Uruchomienie aktywności Losowe uruchamianie aktywności w celu --pct-appswitch
pełniejszego sprawdzenia aplikacji.
Zmiana orientacji Zmiana orientacji ekranu**. --pct-flip
Inne Inne zdarzenia, na przykład wciśnięcie --pct-anyevent
klawiszy klawiatury.
* Według oficjalnej dokumentacji przycisk „wstecz” nie należy do tej kategorii, lecz do przycisków
systemowych.
** Gdy powstawała ta książka, opcja ta nie była udokumentowana, jednak była rozpoznawana i uwzględniana
w czasie przeprowadzania testów. Dlatego prawdopodobnie będzie dostępna także w przyszłych
wersjach platformy.

Z wykorzystaniem wymienionych opcji można wpływać na testy przeprowadzane


w narzędziu Monkey. Możesz na przykład całkowicie pominąć zdarzenia związane
z menu i zwiększyć częstotliwość zmiany orientacji, jeśli wiesz, że zdarzenia te
powodują problemy w programie (może to być przydatne zwłaszcza przy stoso-
waniu współbieżności; zobacz rozdział 6.). Do tej pory sprawdzaliśmy głównie
stabilność. Narzędzie Monkey potrafi wykrywać także inne defekty, na przykład
błędy ANR (ang. Application Not Responding — „aplikacja nie odpowiada”). Jeśli
stosujesz się do naszych rad z rozdziału 6., tego rodzaju problemy Ci nie grożą.
Warto jednak zawsze być przygotowanym i pamiętać o dwóch korzystnych cechach,
o których wspomnieliśmy na początku rozdziału — nie bądź ignorantem i nie bądź
arogancki! Monkey po wykryciu przekroczenia limitu czasu przez aplikację
kończy pracę. Zwraca wtedy komunikat o błędzie i wyświetla informacje diagno-
13.4. Podsumowanie 573

styczne. Oto do jakich efektów prowadzi zmodyfikowanie standardowej andro-


idowej aplikacji HelloWorld przez wprowadzenie jej w nieskończoną pętlę i póź-
niejsze sprawdzenie programu w narzędziu Monkey:
matthias:[~]$ adb shell monkey -p com.aip.test 50
// NOT RESPONDING: com.android.phone (pid 3784)
ANR in com.android.phone (com.aip.test/.HelloWorld)
Reason: keyDispatchingTimedOut
...
DALVIK THREADS:
(mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)
"main" prio=5 tid=1 SUSPENDED
| group="main" sCount=1 dsCount=0 obj=0x4001f1a8 self=0xce48
| sysTid=3784 nice=0 sched=0/0 cgrp=default handle=-1345006528
| schedstat=( 5143014534 1347433116 135 )
at com.aip.test.HelloWorld.onCreate(HelloWorld.java:~13)
...
// meminfo status was 0
** Monkey aborted due to error.
Events injected: 2
## Network stats: elapsed time=31502ms (31502ms mobile, 0ms wifi, 0ms not
connected)
** System appears to have crashed at event 2 of 50 using seed 0

Od wersji Android 2.3 (Gingerbread) raporty diagnostyczne są stosunkowo długie


i dokładne. Możesz na przykład znaleźć stos wywołań z informacją o tym, w któ-
rym miejscu nastąpiło zablokowanie aplikacji.
STOSY WYWOŁAŃ DLA BŁĘDU ANR W ANDROIDZIE 2.2 I STAR-
SZYCH WERSJACH. Zauważ, że Monkey wyświetla stosy wywołań dla
wszystkich wątków w Androidzie 2.3 i nowszych wersjach. W starszych wer-
sjach stos wywołań dla błędu ANR znajdziesz w pliku /data/anr/traces.txt.
Katalog z tym plikiem w emulatorze lub urządzeniu jest dostępny tylko
z poziomu konta administratora.
OMÓWIENIE
Monkey to niezastąpione narzędzie do testowania rozmaitych niezwiązanych
z funkcjami cech, na przykład szybkości reagowania i stabilności przy dużym
obciążeniu. Zawsze należy pamiętać, że zdarzenia są zgłaszane losowo, dlatego
narzędzie to nie zawsze sprawdza wszystkie elementy aplikacji. Przejście testu
w Monkey nie oznacza zatem, że aplikacja jest pozbawiona błędów. Możliwe, że
narzędzie coś pominęło. Jednym ze sposobów na zwiększenie pokrycia testami
jest użycie opcji -pct-appswitch. Przy wyższych wartościach Monkey może uru-
chomić wszystkie aktywności aplikacji.

13.4. Podsumowanie
Przebyliśmy długą drogę. W tym rozdziale wyjaśniliśmy pewne podstawowe
zagadnienia związane z testami, w tym konfigurowanie projektów testowych
i pisanie prostych testów z zastosowaniem biblioteki JUnit z Androida. Następnie
574 ROZDZIAŁ 13. Testowanie i instrumentacja

przedstawiliśmy instrumentację i pokazaliśmy, jak wykorzystać ją do pisania


kompleksowych testów na podstawie scenariuszy użytkownika. Wspomnieliśmy
też, że framework testowy Android nie jest łatwy w użyciu i nie ma zwięzłej
składni. Dlatego pokazaliśmy, jak korzystać z otwartych bibliotek testowych,
takich jak Robotium, do pisania bardziej eleganckich testów. Biblioteki te pozwa-
lają też zwiększyć produktywność. Następnie przedstawiliśmy atrapy — zarówno
tworzone w stylu typowym dla Androida, jak i w nowy, oparty na maszynie JVM
sposób zastosowany w Robolectricu. Po omówieniu różnych metod przeprowadza-
nia testów funkcjonalnych uzupełniliśmy rozdział przez pokazanie, jak za pomocą
narzędzia Monkey wykryć w aplikacji defekty niezwiązane z funkcjami, na przy-
kład problemy ze stabilnością lub wydajnością.
Co za podróż! Rozdział ten powinien zapewnić Ci solidną wiedzę na temat
automatycznych testów aplikacji. Pozostaje jednak pewien problem. Do tej pory
zawsze musieliśmy pamiętać o uruchomieniu napisanych testów. W idealnych
warunkach należy je wykonywać po każdej zmianie fragmentu kodu, ponieważ
modyfikacje mogą prowadzić do pojawienia się błędów. Czy można zautomaty-
zować także wykonywanie testów? Można! Służy do tego system budowania,
który nie tylko generuje plik APK, ale też przeprowadza automatyczne testy.
Witaj w świecie systemów budowania i serwerów ciągłej integracji.
Zarządzanie budowaniem

W tym rozdziale
Q Automatyczne budowanie
Q Zarządzanie budowaniem za pomocą narzędzia
Maven
Q Ciągłe budowanie

Narz4ędzie jest tylko przedłużeniem ręki człowieka, a maszyny nie są


niczym więcej, jak złożonymi narzędziami. Wynalazca maszyny przyczy-
nia się do zwiększenia możliwości człowieka i dobra ludzkości.
Henry Ward Beecher
W czasie rozwijania każdej choć trochę skomplikowanej aplikacji przychodzi
moment, w którym złożoność oprogramowania przerasta używane narzędzia
i wymaga zmiany początkowej struktury projektu. Jesteś programistą i pewnie nie
jeden raz zetknąłeś się z takim problemem. Wraz ze wzrostem liczby klas i plików
zasobów w aplikacji rośnie też liczba zależności w postaci zewnętrznych biblio-
tek, a rozwiązanie staje się monolityczne i zagmatwane.
Co można zrobić, kiedy zaczyna się chaos? Otóż możesz zacząć dzielić aplika-
cję na odrębne moduły, które można konserwować i budować niezależnie od siebie
(a nawet ponownie wykorzystać je w innych projektach). Kiedy liczba zależności
w postaci bibliotek rośnie, można też zastosować system zarządzania zależnościami,
rozwiązujący konflikty wersji i podobne problemy.
Moduły ze współużytkowanym kodem są konserwowane i budowane odręb-
nie od siebie, dlatego potrzebne jest oprogramowanie do opisywania, budowania
i łączenia projektów w celu uzyskania gotowej, wykonywalnej aplikacji. Często
przydatne jest też zwiększenie kontroli nad samym procesem budowania, aby
575
576 ROZDZIAŁ 14. Zarządzanie budowaniem

można było wykonać dodatkowe kroki, takie jak generowanie raportów i doku-
mentacji projektu, publikowanie i podpisywanie komponentów w czasie budo-
wania itd.
Wzrost złożoności aplikacji i procesu budowania zwykle związany jest z więk-
szą liczbą osób pracujących jednocześnie nad projektem. Dlatego konieczne może
być przygotowanie serwera budowania. Taki serwer automatycznie przeprowadza
testy i łączy nowe wersje aplikacji. Gwarantuje to, że programiści nie naruszą
przypadkowo poprawności projektu w systemie kontroli kodu źródłowego przez
przesłanie błędnego fragmentu kodu. Ponadto serwer budowania regularnie
archiwizuje stabilne wersje.
W kontekście zarządzania projektami i budowaniem we wcześniejszych roz-
działach stosowaliśmy proste, standardowe funkcje wtyczki ADT i kreatora apli-
kacji. Mechanizmy te wystarczą do tworzenia prostych aplikacji, jednak nie nadają
się do rozwijania większych rozwiązań. Oto wady tych mechanizmów:
Q Podział projektu na moduły jest trudnym i w dużym stopniu ręcznie
wykonywanym procesem. W Eclipse i ADT nie występują podmoduły
lub projekty współużytkowane.
Q Zarządzanie zależnościami w Eclipse i ADT ogranicza się do podawania
zależności w formie „A zależy od B”. Zależności przechodnie, wersje
i konflikty wersji w ogóle nie są obsługiwane.
Q Wzbogacanie procesu budowania o niestandardowe etapy jest trudne
i możliwe tylko w ograniczonym zakresie.
Q Eclipse i ADT to narzędzia graficzne bez interfejsu z poziomu wiersza
poleceń, dlatego uniemożliwiają korzystanie z serwera budowania.
Q Poziom automatyzacji budowania możliwy do uzyskania za pomocą Eclipse
i ADT jest ograniczony. Nie można na przykład zdefiniować uchwytów
do automatycznego uruchamiania budowania.
Aby poradzić sobie z wadami środowiska Eclipse i wtyczki ADT, trzeba poszukać
innych rozwiązań, zapewniających dodatkowe możliwości, większą swobodę
i kontrolę w procesie budowania aplikacji na Android. Niestety, oznacza to też,
że trzeba porzucić świat interfejsów graficznych i wrócić do klasycznej powłoki
poleceń.
Choć możesz czuć się nieswojo na myśl o porzuceniu wygodnego graficznego
środowiska Eclipse na czas lektury tego rozdziału, dobra wiadomość jest taka,
że nie musisz zaczynać pracy od podstaw. Android udostępnia zestaw narzędzi
uruchamianych z wiersza poleceń i gotowe zadania Anta (dalej dowiesz się, do
czego służą). Elementy te pomagają w tworzeniu dużych i złożonych środowisk
budowania. Oto obietnica z naszej strony — gdy zapoznasz się z tym rozdziałem,
proces budowania aplikacji na Android za pomocą wiersza poleceń będzie dla
Ciebie równie prosty, jak w środowisku Eclipse; co więcej — budowanie będzie
uruchamiało się samodzielnie!
14.1. Budowanie aplikacji na Android 577

Rozdział ten składa się z trzech podrozdziałów. Każdy z nich dotyczy pew-
nego nadrzędnego tematu. W pierwszym podrozdziale wyjaśniamy, jak urucha-
miać proces budowania aplikacji na Android z wiersza poleceń, ponieważ jest
to niezbędne do wykonywania dalszych zadań. W tym samym podrozdziale
pokrótce omawiamy proces budowania aplikacji na Android, w tym potrzebne
do tego etapy i narzędzia. Ponadto pokazujemy, jak w Androidzie używać zadań
Anta do łatwego uruchamiania budowania z poziomu wiersza poleceń.
W drugim podrozdziale wychodzimy poza narzędzia dostępne w pakiecie SDK
Androida i wyjaśniamy, jak tworzyć strukturę projektu pozwalającą poprawić
podział kodu na moduły i w jaki sposób sprawnie zarządzać zależnościami.
Konkretnie pokazujemy, jak używać Mavena i powiązanej z nim androidowej
wtyczki do opisywania i budowania aplikacji na Android. Omawiamy też dostępny
w Mavenie system zarządzania zależnościami umożliwiający wygodne i proste
zarządzanie zewnętrznymi bibliotekami. Ponadto opisujemy, jak zintegrować
Maven ze znanym już środowiskiem Eclipse.
W ostatnim, choć nie najmniej istotnym podrozdziale pokazujemy, jak wyko-
rzystać serwer budowania do łączenia i testowania aplikacji w całkowicie auto-
matyczny sposób. Wyjaśniamy, jak taki serwer rozpoczyna nowy proces budowa-
nia każdorazowo po przesłaniu kodu, a także jak jednocześnie włączyć zestaw
emulatorów o różnych konfiguracjach, uruchomić i przetestować na nim aplikacje
lub uzyskać informacje o problemach.
Przyznajemy, że omawiamy tu dużo materiału. Automatyzacja budowania to
zaawansowany temat, jednak niezwykle ważny dla firm profesjonalnie rozwijają-
cych aplikacje na Android. Materiał z tego rozdziału jest słabo opisany w oficjalnej
witrynie Androida, dlatego mamy nadzieję, że choć znajduje się tu dużo informacji,
to warto się z nimi zapoznać.

14.1. Budowanie aplikacji na Android


Jeśli Twoje doświadczenie w rozwijaniu aplikacji na Android ogranicza się do
prześledzenia przykładów z tej książki, możliwe, że korzystasz ze środowiska
Eclipse do budowania, testowania i uruchamiania programów. ADT, androidowa
wtyczka dla środowiska Eclipse, dobrze udostępnia te mechanizmy w środowisku
graficznym, przez co są łatwo dostępne dla użytkowników. Aby w Eclipse zbudo-
wać archiwum APK na Android na podstawie kodu źródłowego, wystarczy klik-
nąć przycisk Save. Prowadzi to do uruchomienia łańcucha narzędzi odpowiedzial-
nych za przekształcenie androidowego projektu ze środowiska Eclipse na program
wykonywalny.
Zaskakujące jest, jak dużo dzieje się na zapleczu przy zapisywaniu kodu
źródłowego lub pliku zasobu. Planujemy budować aplikacje na Android z poziomu
wiersza poleceń, dlatego koniecznie trzeba zrozumieć zachodzące przy tym ope-
racje. Z tego powodu w pierwszym podrozdziale dokładnie omawiamy etapy
i narzędzia potrzebne do budowania aplikacji. Opisujemy je w pierwszej w tym
578 ROZDZIAŁ 14. Zarządzanie budowaniem

rozdziale technice, w której pokazujemy, jak za pomocą systemu budowania Apache


Ant zbudować typową aplikację na Android z poziomu wiersza poleceń.

14.1.1. Proces budowania w Androidzie


Proces przechodzenia od zbioru plików źródłowych i zasobów do wykonywalnej
aplikacji na Android jest złożony. Może to być dla Ciebie zaskoczeniem, ponie-
waż w środowisku Eclipse budowanie programów jest niezwykle proste. Prawda
jest taka, że proces obejmuje przynajmniej siedem odrębnych kroków i wymaga
użycia kilku różnych narzędzi. Dlatego warto dokładnie przyjrzeć się temu, co
dzieje się na zapleczu. Etapy budowania androidowego pliku APK przedstawiono
na rysunku 14.1.

Rysunek 14.1.
Standardowy
proces budowania
stosowany
w Androidzie
obejmuje siedem
odrębnych etapów.
Zasoby i kod
źródłowy
są kompilowane
do pliku API.
Następnie należy
podpisać plik
i wyrównać w nim
bajty

W niektórych sytuacjach może być jeszcze więcej etapów. Niektóre firmy na


przykład zaciemniają kod po wygenerowaniu pliku klasy, aby utrudnić lub unie-
możliwić poznanie kodu programu przez zastosowanie inżynierii wstecznej.
Przyjrzyjmy się teraz każdemu etapowi. Warto podkreślić, że omówienie ma
pomóc Ci w lepszym zrozumieniu procesu. Nie musisz ręcznie wykonywać
wszystkich opisanych tu kroków. W technice 81. zobaczysz, że można znacznie
uprościć sobie pracę za pomocą dostępnych w Androidzie celów (ang. target) Anta.
ETAP 1. GENEROWANIE KODU ŹRÓDŁOWEGO W JAVIE
Jak może przypominasz sobie z wcześniejszych rozdziałów, Android obsługuje
zasoby w rodzaju rysunków, układów i łańcuchów znaków przez generowanie
identyfikatorów zasobu, zapisywanych w pliku R.java. Zwykle odpowiada za to
14.1. Budowanie aplikacji na Android 579

wtyczka ADT, dlatego po utworzeniu zasobu (na przykład przez umieszczenie


pliku PNG w katalogu res/drawables i wciśnięcie klawisza F5 w celu odświeżenia
środowiska) wtyczka aktualizuje plik R.java i tworzy nowe pole z identyfikatorem
danego zasobu. To dlatego nie należy ręcznie modyfikować pliku R.java. Wpro-
wadzone w nim zmiany są nadpisywane. W czasie tworzenia pliku APK poza
środowiskiem Eclipse system budowania musi samodzielnie wygenerować plik
R.java. Można użyć do tego narzędzia aapt (ang. Android Asset Packaging Tool)
z pakietu SDK. Narzędzie to umożliwia między innymi wygenerowanie klasy R na
podstawie zbioru zasobów, a także wygenerowanie klasy Manifest (jest to używany
w czasie wykonywania programu odpowiednik pliku AndroidManifest.xml).
GDZIE ZNAJDUJĄ SIĘ NARZĘDZIA PAKIETU SDK? W tym rozdziale
omawiamy kilka narzędzi dostępnych w pakiecie SDK uruchamianych
z wiersza poleceń. Nie opisujemy szczegółowo tych programów, jeśli jed-
nak chcesz korzystać z nich ręcznie lub poeksperymentować z nimi, znaj-
dziesz je w katalogu $ANDROID_HOM/tools lub $ANDROID_HOME/platform-tools.
Innym ważnym elementem są zdalne obiekty zdefiniowane przy użyciu języka
AIDL (ang. Android Interface Definition Language), co opisano w rozdziale 5.
Interfejsy do komunikowania się ze zdalną usługą trzeba wygenerować za pomocą
narzędzia aidl. Interfejsy te są potrzebne do skompilowania aplikacji. Po wyko-
naniu pierwszego etapu wszystkie pliki z kodem źródłowym są gotowe do
skompilowania.
ETAP 2. KOMPILOWANIE KODU ŹRÓDŁOWEGO JAVY
Kod źródłowy aplikacji (z katalogu src/), kod źródłowy Javy klas R i Manifest oraz
interfejsy w AIDL-u (jeśli istnieją) są kompilowane do plików klas Javy. Odbywa
się to przez wywołanie kompilatora javac. Nie ma tu nic charakterystycznego dla
Androida. Na tym etapie powstaje zestaw standardowych plików klas Javy odpo-
wiadających każdej klasie aplikacji (napisanej ręcznie lub wygenerowanej
w poprzednim kroku). Teraz można podłączyć się do cyklu życia procesu budo-
wania i przeprowadzić niestandardowe przetwarzanie plików klas (na przykład
przez wstrzyknięcie kodu bajtowego lub manipulowanie nim). Często odbywa się
wtedy minimalizowanie długości lub zaciemnianie kodu klas z wykorzystaniem
narzędzia ProGuard (zobacz dodatek C).
ETAP 3. PRZEKSZTAŁCANIE KLAS NA KOD BAJTOWY DALVIKA
Pliki klas Javy otrzymane w poprzednim kroku zawierają standardowy kod bajtowy
Javy, który jest niezrozumiały dla Dalvika. Dlatego system budowania musi prze-
kształcić wszystkie pliki klas Javy na format DEX (ang. Dalvik Executable), do
czego służy narzędzie dx (w tekstach anglojęzycznych określane czasem jako dexer).
Modyfikacje dotyczą nie tylko klas napisanych przez programistę, ale też klas
z plików JAR używanych przez aplikację. W efekcie powstaje jeden plik classes.dex
z całym kodem programu i wymaganymi klasami. Plik ten jest zwięzłą i wydajną
580 ROZDZIAŁ 14. Zarządzanie budowaniem

reprezentacją aplikacji, którą można uruchomić w maszynie wirtualnej Dalvik.


Optymalizacje stosowane przez narzędzie dx omówiliśmy już w rozdziale 1., gdy
przedstawialiśmy Dalvik. Kod aplikacji można już umieścić w pakiecie, najpierw
jednak trzeba przetworzyć pliki zasobów aplikacji.
ETAP 4. PAKOWANIE ZASOBÓW APLIKACJI
Kod aplikacji jest już gotowy do uruchomienia, jednak nie można jeszcze zbudo-
wać pliku APK. Najpierw trzeba spakować zasoby używane przez aplikację. Od-
bywa się to podobnie jak dla kodu źródłowego. Służy do tego system budowania
korzystający z narzędzia aapt. W tym etapie narzędzie to przetwarza wszystkie
zasoby z katalogów res/ i assets/ przez przekształcenie ich na bardziej zwięzłą
reprezentację. Pliki binarne, na przykład rysunki, nie są modyfikowane, natomiast
pliki XML (między innymi z układami lub manifestem) są przekształcane na
format binarny. Dzięki temu są krótsze, a ich wczytywanie w czasie wykonywania
programu przebiega szybciej. Wartości w rodzaju łańcuchów znaków nie są
przechowywane w odrębnych plikach — narzędzie zapisuje je bezpośrednio do
pliku resources.arsc wraz z referencjami do zmodyfikowanych zasobów (na przy-
kład układów). Zoptymalizowany plik AndroidManifest.xml, zoptymalizowane
pliki zasobów i plik resources.arsc są następnie pakowane do pliku JAR. Jest to
jednocześnie plik APK aplikacji, choć brakuje w nim jeszcze jej kodu.
ETAP 5. SKŁADANIE PLIKÓW APK
Mamy już kod aplikacji i zasoby gotowe do spakowania do pliku APK. W praktyce
etapy 4. i 5. są wykonywane w aapt w jednym kroku, jednak łatwiej jest omówić je
odrębnie. System budowania musi dodać plik classes.dex do ostatecznego pliku
APK — albo z wykorzystaniem polecenia aapt add, albo przez dołączenie go
w etapie 4. Po etapie 5. otrzymujemy gotowy plik APK z zasobami i kodem. Plik
APK nie ma żadnych wyjątkowych cech — to zwykły plik JAR Javy, a z kolei pliki
JAR to zwykłe pliki ZIP z katalogiem META-INF, w którym znajdują się infor-
macje na temat zawartości pakietu. Jeśli Cię to ciekawi, przejdź do przestrzeni
roboczej środowiska Eclipse i rozpakuj plik .apk za pomocą wybranego narzędzia
do archiwizacji. Uzyskasz strukturę katalogów podobną do tej z rysunku 14.2.

Rysunek 14.2. Pliki APK to zwykłe pliki


ZIP. Po ich rozpakowaniu otrzymujemy
strukturę katalogów podobną
do powyższej

Na tym etapie gotowy jest kompletny plik APK. Czy to nie koniec? Nie, ponie-
waż pliku tego nie można na razie wykorzystać. Plik APK można zainstalować
w emulatorze lub urządzeniu dopiero po podpisaniu go z wykorzystaniem certy-
fikatu bezpieczeństwa.
14.1. Budowanie aplikacji na Android 581

ETAP 6. PODPISYWANIE PLIKU APK


Wspomnieliśmy, że niepodpisanego pliku APK nie można zainstalować nawet
w emulatorze. Takie podejście ma zwiększać zaufanie do instalowanych aplikacji.
Dzięki temu nie można też zastąpić aplikacji inną jej wersją, jeśli sygnatury bez-
pieczeństwa obu wersji nie pasują do siebie. Poprawną sygnaturę może dodać
tylko podmiot, który utworzył daną aplikację. Oszuści modyfikujący cudze apli-
kacje nie mogą ich rozpowszechniać, ponieważ nie mają prywatnego klucza uży-
tego do podpisania pierwotnego pakietu.
Ponieważ plik APK jest plikiem JAR, podpisywanie plików APK odbywa się
tak samo, jak podpisywanie plików JAR. Służy do tego narzędzie jarsigner. Narzę-
dzie to, podobnie jak javac, nie ma nic wspólnego z Androidem. Jest ono częścią
standardowej instalacji pakietu JDK. Narzędzie jarsigner wykorzystuje plik
z kluczem i certyfikat bezpieczeństwa. Certyfikaty do podpisywania plików APK
można tworzyć samodzielnie. Nie trzeba występować o certyfikat do firmy Google
lub innej zaufanej jednostki. Zawsze warto korzystać z tego samego certyfikatu
do podpisywania aplikacji dostępnych w wielu sklepach. Pozwala to zastąpić
w przyszłości wszystkie wersję jedną aktualizacją.
Skoro podpisywanie archiwum APK wymaga plików z kluczem i certyfikatem,
w jaki sposób Eclipse podpisuje aplikację, kiedy programista wybiera opcję Run
As/Android application? Rozwiązanie polega na tym, że wtyczka ADT domyślnie
korzysta z pliku z kluczem, wygenerowanego automatycznie na potrzeby debu-
gowania (w systemach z rodzin Mac OS i UNIX plik ten znajduje się w katalogu
~/.android/debug.keystore). To dobre podejście, kiedy aplikacja działa na kom-
puterze używanym do programowania. Jednak na potrzeby wersji produkcyjnej
należy utworzyć odrębny plik z kluczem i certyfikat bezpieczeństwa powiązane
z firmą. Nie omawiamy tu szczegółowo podpisywania plików APK za pomocą
narzędzia jarsigner. Jeśli interesuje Cię ten temat, pełną dokumentację znajdziesz
w oficjalnej witrynie Androida: http://mng.bz/La8q.
ETAP 7. WYRÓWNYWANIE BAJTÓW ZASOBÓW W PLIKU APK
Etap 6. w zasadzie kończy pracę, ponieważ jego efektem jest podpisany plik APK,
który można instalować w urządzeniach. Etap 7. służy tylko optymalizacji i jest
opcjonalny, choć gorąco zachęcamy do jego wykonania. Według firmy Google
nieskompresowane zasoby, na przykład pliki PNG lub nieprzetworzone zasoby,
zawsze należy wyrównywać w skompresowanych plikach APK do czterobajtowych
granic. Pozwala to Androidowi na wydajniejszy dostęp do zasobów w czasie
wykonywania programu. Umożliwiają to operacje wejścia-wyjścia odwzorowane
w pamięci. Wyrównanie bajtów zasobów można łatwo przeprowadzić narzędziem
zipalign. Wtyczka ADT automatycznie stosuje je do eksportowanych plików APK,
jednak w niestandardowym procesie budowania narzędzie to trzeba uruchomić
ręcznie. Wywołanie programu zipalign zawsze powinno być ostatnim krokiem
procesu budowania. Wszelkie późniejsze modyfikacje pliku psują wyrównanie.
582 ROZDZIAŁ 14. Zarządzanie budowaniem

Wiesz już, jak wygląda proces budowania aplikacji na Android i jakie narzę-
dzia są do tego potrzebne. Pora zastanowić się nad zautomatyzowaniem tego
zadania. Trzeba uwzględnić wiele etapów i opcji, dlatego warto przekształcić
rozbudowany proces na jego łatwiejszy w obsłudze odpowiednik.

14.1.2. W kierunku automatycznego budowania


Ponieważ pamiętanie o wszystkich opisanych etapach i przeprowadzanie ich jest
żmudne oraz łatwo przy tym o błąd, warto poznać narzędzia, które pozwalają
zautomatyzować ten proces. Są to skryptowe narzędzia do obsługi budowania.
Przyjmują one nieprzetworzone pliki aplikacji jako dane wejściowe, wykonują
wyjaśnione wcześniej operacje i generują pliki APK. Narzędzia te możesz trak-
tować jak linię produkcyjną z różnymi maszynami. Umieszczamy coś na początku
(katalog projektu), maszyny (narzędzia platformy) przekształcają różne komponenty
na produkty pośrednie, a na końcu otrzymujemy gotowy towar (plik APK).
System budowania może robić coś jeszcze, na przykład instalować aplikację
w urządzeniu i przeprowadzać zestaw testów. Na rysunku 14.3 pokazano typowe
etapy działania systemu budowania (takiego jak Ant) wykonywane w środowisku
Androida.

Rysunek 14.3. System budowania, na przykład Apache Ant, znacznie upraszcza


omawiany proces, ponieważ pozwala wykonać każdy krok z poziomu skryptu
budowania. System budowania zwykle można uruchomić za pomocą prostego
polecenia powłoki

Istnieje wiele systemów budowania: GNU Make, Ant, Maven, Gradle, SBT, Rake,
Buildr — lista jest długa. To, który z nich będzie najlepszy, zależy od warunków.
Tu w kontekście Androida omawiamy dwa najpopularniejsze narzędzia, Ant
i Maven.
Zaczynamy od prawdopodobnie najprostszego i najbardziej znanego systemu
budowania w świecie Javy. Jest to narzędzie Apache Ant. Na początek ostrze-
żenie — żadna z opisanych dalej technik nie stanowi rozbudowanego wprowadze-
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 583

nia do systemów budowania. Takie omówienie wykracza poza zakres tej książki.
Korzystaniu z Anta lub Mavena poświęcone są całe podręczniki, natomiast ta
książka do nich nie należy. Tu przedstawiamy tylko krótkie wprowadzenie do
systemów budowania i ich najważniejszych cegiełek. Następnie szybko prze-
chodzimy do kwestii dotyczących Androida. Porównujemy też różne systemy
w kontekście budowania aplikacji na Android. Zawsze wskazujemy też, gdzie
można znaleźć szczegółowe informacje na temat omawianego narzędzia.

0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta

Nawet jeśli zacząłeś uczyć się programować aplikacje na Android, nie mając
dużego doświadczenia w stosowaniu Javy, prawdopodobnie przynajmniej słyszałeś
o narzędziu Apache Ant, a może nawet z niego korzystałeś. Ant (ang. Another neat
tool) to system budowania napisany w Javie i dla tego języka. Jego podstawową
funkcją jest budowanie aplikacji w Javie (po to powstał), jednak można go używać
także do wykonywania innych zadań. Jeśli rozwijałeś kiedyś aplikacje na Linux
lub UNIX, możesz traktować Ant jak odpowiednik UNIX-owego narzędzia make.
Jednak w narzędziu make proces budowania opisywany jest za pomocą nieele-
ganckiej składni w plikach Makefile, natomiast w Ancie opis procesu budowania
ma format XML i znajduje się w pliku build.xml.
Jeśli już korzystałeś ze stosunkowo prostych systemów budowania, takich jak
Ant lub GNU Make, domyślasz się pewnie, co teraz napiszemy. Otóż po prze-
kroczeniu pewnego poziomu złożoności projektu (kiedy występuje dużo zależ-
ności, podmodułów itd.) praca może stać się niewygodna. Dalej szczegółowo
omawiamy wady i zalety Anta. Na razie warto zapamiętać, że Ant świetnie nadaje
się do wykonywania prostych zadań związanych z budowaniem nieskomplikowa-
nych aplikacji. Ant jest też standardowym i stosowanym przez firmę Google narzę-
dziem do budowania aplikacji na Android z poziomu wiersza poleceń. Choć Ant
ma pewne wady, jest przydatnym systemem. Jednak zanim zaczniemy go oceniać,
warto zobaczyć, w jaki sposób działa.
PROBLEM
Aplikacja ma prostą strukturę. Występują nieliczne zależności od innych projek-
tów lub bibliotek. Szukamy łatwego sposobu na budowanie plików APK z poziomu
wiersza poleceń.
ROZWIĄZANIE
Jeśli rozwijana aplikacja pasuje do opisu problemu (dotyczy to każdego standar-
dowego projektu dla Androida wygenerowanego za pomocą kreatora projektów
z wtyczki ADT), Ant jest dobrym rozwiązaniem. Z uwagi na prosty interfejs obsłu-
giwany z wiersza poleceń, Ant można uruchamiać bezpośrednio z poziomu
powłoki, co pozwala na łatwą integrację tego narzędzia z innymi środowiskami
obsługi budowania, takimi jak Eclipse (poprzez widok Ant) i serwery budowania.
Tu pokrótce przedstawiamy podstawowe informacje o Ancie.
584 ROZDZIAŁ 14. Zarządzanie budowaniem

SKĄD MOGĘ DOWIEDZIEĆ SIĘ CZEGOŚ WIĘCEJ O ANCIE? We


wprowadzeniu wspomnieliśmy, że systemy budowania omawiamy tu tylko
pokrótce i koncentrujemy się na stosowaniu ich do projektów androido-
wych. Więcej informacji o narzędziu Apache Ant znajdziesz na stronie
http://ant.apache.org/manual/.
Proces budowania w Ancie oparty jest na trzech ważnych cegiełkach — zada-
niach, celach i punktach rozszerzeń. Elementy te są importowane lub definio-
wane w XML-owym pliku deskryptora procesu budowania, build.xml. Zadanie
w Ancie opisuje wykonywaną operację. Może to być cokolwiek — od prostego
tworzenia katalogów po skomplikowane generowanie pliku JAR. Ant rozpo-
wszechniany jest razem z zestawem zadań przydatnych w procesie budowania
w Javie, jednak programiści mogą też pisać własne zadania. Przykładowymi
zadaniami są javac (służy do kompilowania kodu źródłowego) i jar (tworzy plik
JAR). Zadania to zwykle krótkie, atomowe kroki w procesie budowania i same
w sobie nie mają dużej wartości. Dlatego w Ancie można połączyć kilka zadań
w większą jednostkę, tak zwany cel. Cel distribution może składać się z zadań
mkdir, javac, jar i copy. Taki cel tworzy katalog dystrybucji, kompiluje kod źró-
dłowy i umieszcza go w pliku JAR, który kopiuje do wspomnianego katalogu.
Ważną cechą celów jest to, że — w odróżnieniu od zadań — można je wywoły-
wać z poziomu wiersza poleceń przez przekazanie ich jako parametru do pro-
gramu ant. Polecenie $ant documentation distribution najpierw wykonuje cel
o nazwie documentation, a następnie cel distribution. Cele mogą także zależeć
od siebie. Cel distribution może zależeć od celu documentation, tak aby przed
udostępnieniem aplikacji zawsze była generowana dokumentacja. W tym podej-
ściu można pominąć bezpośrednie wywołanie celu documentation w wierszu
poleceń.
Trzecią i ostatnią cegiełką są punkty rozszerzeń. Są to cele bez zdefiniowa-
nych zadań. Służą do grupowania kilku celów w większe jednostki. Zadania
i cele zwykle definiuje się w pliku build.xml, który zazwyczaj znajduje się w kata-
logu głównym aplikacji. Zadania można też pisać w Javie, pakować do plików
JAR i importować takie pliki w skrypcie budowania. W ten sposób działają
skrypty budowania Anta dla aplikacji na Android.
Te informacje wystarczą jako krótkie wprowadzenie do Anta. Systemy budo-
wania najlepiej omawiać na przykładzie. Zobaczmy, jak wykorzystać Ant do zbu-
dowania aplikacji Hello World w sposób charakterystyczny dla Androida.
POBIERZ PROJEKT HELLOANT. Kod źródłowy projektu do uruchamia-
nia aplikacji znajdziesz w witrynie z kodem do książki Android w praktyce.
Ponieważ niektóre listingi skrócono, abyś mógł skoncentrować się na kon-
kretnych zagadnieniach, zalecamy pobranie kompletnego kodu źródłowego
i śledzenie go w Eclipse (lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/g3Vd.
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 585

„Proszę przechodzić, nie ma tu nic do oglądania!”. Nie udostępniamy


plików APK do tego rozdziału, ponieważ uważamy, że byłoby to bezcelowe.
Rozdział ten dotyczy skryptów i narzędzi do budowania, a nie aplikacji.
Wszystkie przykładowe aplikacje są tu jedynie kontenerami do automa-
tyzacji procesu budowania.
Aplikacją jest tu standardowy program typu Hello World wygenerowany przez
kreator projektów z wtyczki ADT. Rozwinęliśmy nieco aplikację przez dodanie
zależności w postaci zewnętrznej biblioteki Apache Commons Lang. Biblioteka
ta obejmuje liczne przydatne klasy narzędziowe.
W aplikacji HelloAnt używamy metody String
´Utils.repeat z takiej klasy do trzykrotnego
powtórzenia powitania w komunikacie typu
toast (rysunek 14.4).
Przykład może wydawać się bezsensowny,
jednak pozwala pokazać obsługę zależności
w postaci bibliotek w Ancie, dlatego czytaj dalej.
Pierwszym elementem potrzebnym w celu zbu-
dowania aplikacji za pomocą Anta jest plik
build.xml. Można napisać go od podstaw, jednak
istnieje prostsze rozwiązanie. Tu generujemy go
narzędziem android.
HEJ, GDZIE SIĘ PODZIAŁ PLIK BUILD.
XML? Jeśli tworzysz aplikacje w środowisku
Eclipse z zastosowaniem kreatora projektów
z wtyczki ADT, plik build.xml nie jest
generowany, ponieważ narzędzia uznają, że
za budowanie aplikacji ma odpowiadać tylko Rysunek 14.4. W aplikacji
środowisko Eclipse. Do wygenerowania pliku HelloAnt używamy metody
StringUtils.repeat z biblioteki
budowania Anta można jednak użyć narzędzia Common Lang do trzykrotnego
android uruchamianego z wiersza poleceń. powtórzenia powitania

W tym podrozdziale wyjaśniamy, jak to zrobić.


Są dwa sposoby na generowanie pliku budowania Anta przy użyciu narzędzia
android. Jedna z technik polega na rozpoczęciu od utworzenia projektu za pomocą
polecenia android create. W ten sposób można przygotować strukturę androido-
wego projektu poza środowiskiem IDE. Generowany jest przy tym plik build.xml.
Większość osób woli korzystać z kreatora projektów ze środowiska Eclipse, ponie-
waż jest to wygodniejsze. Choć nie powstaje wtedy plik build.xml, można wyge-
nerować go później. Aby to zrobić, należy przejść w powłoce do katalogu projektu
i wpisać następującą instrukcję:
$android update project –p .
586 ROZDZIAŁ 14. Zarządzanie budowaniem

Polecenie to nie prowadzi do wykonania żadnych operacji, lecz tylko ustawia


podstawowy katalog projektu na bieżący katalog. Efektem ubocznym jest ponowne
wygenerowanie wszystkich brakujących plików, w tym pliku build.xml, przed-
stawionego na listingu 14.1. W ten sposób można „przechytrzyć” system!

Listing 14.1. Plik build.xml wygenerowany przez narzędzie android


(komentarze pominięto)

<?xml version="1.0" encoding="UTF-8"?>


<project name="AntPoweredApp" default="help">

<property file="build.properties" />


<property file="default.properties" />

<path id="android.antlibs">
<pathelement path="${sdk.dir}/tools/lib/anttasks.jar" />
<pathelement path="${sdk.dir}/tools/lib/sdklib.jar" />
<pathelement path="${sdk.dir}/tools/lib/androidprefs.jar" />
</path>

<taskdef name="setup"
classname="com.android.ant.SetupTask"
classpathref="android.antlibs" />

<!--
<target name="-pre-build"></target>
<target name="-pre-compile"></target>
<target name="-post-compile"></target>
-->
<setup />

</project>

Każdy skrypt budowania w Ancie rozpoczyna się od znacznika project . Znacz-


nik ten przyjmuje kilka opcjonalnych parametrów, takich jak nazwa projektu
(name) i domyślny cel (default) używane w wywołaniach Anta bez podanego
bezpośrednio celu. Trzeci, pominięty tu argument (basedir) pozwala określić
nadrzędną ścieżkę.
W Ancie parametry, stałe i zmienne skryptów budowania deklaruje się jako
właściwości. Można to zrobić za pomocą znacznika property . Właściwości usta-
wia się na siedem sposobów. Najczęściej programiści używają atrybutów name
i value lub wczytują dane z plików właściwości Javy, tak jak w przykładzie.
Używamy tu dwóch plików właściwości — local.properties i default.properties.
Przynajmniej ten ostatni powinien być Ci znany. Jest to plik właściwości gene-
rowany przez wtyczkę ADT (lub narzędzie android) dla każdego androidowego
projektu. Obejmuje ustawienia w rodzaju docelowej wersji Androida. Nowością
jest plik local.properties, który obejmuje ustawienia właściwe dla maszyny (na
przykład bezwzględną ścieżkę do pakietu SDK Androida), niezbędne do zbudo-
wania aplikacji. Dlatego wspomnianego pliku nie należy przesyłać do systemu
kontroli wersji.
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 587

Jak widać, przedstawiony plik budowania jest dość krótki. Wynika to z braku
definicji zadań. W pakiecie SDK Androida zadania są zdefiniowane jako klasy Javy
spakowane do pliku JAR. W trakcie budowania Ant sprawdza i wczytuje zadania,
przechodząc po liście plików JAR zdefiniowanych w elemencie android.antlibs .
Następnie proces budowania jest uruchamiany przez zadeklarowanie zadania
setup w elemencie taskdef Anta . Zwróć uwagę na wskazanie ścieżki do pliku
JAR w atrybucie classpathref i określenie w nim identyfikatora zdefiniowanego
elementu path. Jest to typowy przykład tego, jak w pliku XML Anta należy łączyć
różne elementy.
GDZIE ZNAJDUJĄ SIĘ DEFINICJE ZADAŃ ANTA W ANDRO-
IDZIE? Jeśli chcesz przyjrzeć się różnym zadaniom Anta zdefiniowanym
w Androidzie i ich implementacjom, pobierz kod źródłowy frameworku
i przejdź do katalogu sdk/anttasks. Klasy zadań znajdują się w pakiecie
com.android.ant. Z zadaniami można też zapoznać się w internecie na stro-
nie http://mng.bz/6d7s.
Plik budowania Anta w Androidzie obejmuje też kilka pustych celów, które można
traktować jak uchwyty do procesu budowania. Te cele to: -pre-build, -pre-compile
i –post-compile . Można na przykład usunąć symbole komentarzy i zaimplemen-
tować uchwyt –post-compile, aby modyfikować wygenerowane pliki klas w celu
zaciemnienia kodu.
PRYWATNE ZADANIA ANTA. Możesz się zastanawiać, dlaczego cele
zaczynają się od myślnika (-). Jest to standardowy sposób tworzenia pry-
watnych celów Anta; nie można wywoływać ich bezpośrednio z wiersza
poleceń. W Ancie oficjalnie nie występują prywatne zadania, jednak można
zastosować pewną sztuczkę. Ant traktuje argumenty rozpoczynające się od
myślnika jak opcje narzędzia ant, a nie jak cele. Dlatego celów z myślni-
kiem nie da się wywołać z wiersza poleceń. Jednak w skryptach budowa-
nia można korzystać z nich w normalny sposób.
Ostatnia, choć nie najmniej istotna operacja w pliku budowania to uruchomienie
zadeklarowanego wcześniej zadania setup . Nie przyjmuje ono żadnych argu-
mentów, dlatego wywołanie ma uproszczoną postać <setup />. To na tym etapie
przeprowadzana jest konfiguracja skryptu budowania.
Budowanie aplikacji
Wiesz już, co obejmuje deskryptor budowania. Pora wykorzystać go do zbudo-
wania pliku APK aplikacji za pomocą Anta. Najpierw trzeba ustalić, jakie cele
udostępnia Android. Na listingu 14.2 pokazano ich listę wyświetloną przez uru-
chomienie Anta z opcją –p.
588 ROZDZIAŁ 14. Zarządzanie budowaniem

Listing 14.2. Wyświetlanie dostępnych celów i innych informacji o projekcie

matthias:[HelloAnt]$ ant -p
Buildfile: /Users/matthias/Projects/eclipse/HelloAnt/build.xml
[setup] Android SDK Tools Revision 9
[setup] Project Target: Google APIs
[setup] Vendor: Google Inc.
[setup] Platform Version: 2.2
[setup] API level: 8
[setup]
[setup] ------------------
[setup] Resolving library dependencies:
[setup] No library dependencies.
[setup]
[setup] ------------------
[setup]
[setup] WARNING: No minSdkVersion value set. Application will
install on all Android versions.
[setup]
[setup] Importing rules file: tools/ant/main_rules.xml

Main targets:

clean Removes output files created by other targets.


compile Compiles project's .java files into .class files
debug Builds the application and signs it with a debug key.
install Installs/reinstalls the debug package onto a running
emulator or device. If the application was previously
installed, the signatures must match.
release Builds the application. The generated apk file must be signed
before it is published.
uninstall Uninstalls the application from a running emulator or device.
Default target: help

Aby zobaczyć wszystkie cele, w tym prywatne, użyj opcji –v (służy do wyświetlania
rozbudowanych informacji). Inna możliwość to otwarcie pliku build.xml w widoku
Ant w środowisku Eclipse. Środowisko wyświetla wtedy drzewo z zawartością pliku.
UPEWNIJ SIĘ, ŻE W ZMIENNEJ PATH PODANY JEST KATALOG
Z NARZĘDZIAMI. Ant musi wiedzieć, gdzie są zdefiniowane jego andro-
idowe zadania. Dlatego przed uruchomieniem takich zadań trzeba umie-
ścić katalog $ANDROID_HOME/tools w zmiennej środowiskowej $PATH, używa-
nej do wyszukiwania komponentów podawanych w wierszu poleceń.
Ant w generowanym tekście umieszcza nazwy zadań w nawiasach kwadratowych
(tu jest to zadanie setup przedstawione na listingu 14.1). Przyjrzyj się ostatniemu
wierszowi zadania setup. Ant określa w nim, że zaimportowano plik zasad. Plik
ten, main_rules.xml, obejmuje wszystkie właściwości i elementy taskdef definio-
wane przez Android w standardowych projektach aplikacji na tę platformę. Istnieją
też podobne pliki dla projektów testowych i projektów bibliotecznych Androida.
Zasady dla testów obejmują dodatkowe cele, na przykład run-tests, do urucha-
miania projektów testowych z instrumentacją.
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 589

URUCHAMIANIE ZESTAWÓW TESTÓW ZA POMOCĄ ANTA. Do


wykonywania testów przy użyciu Anta wracamy w podrozdziale 14.3, przy
omawianiu serwerów budowania i ciągłej integracji. Na razie wystarczy
zapamiętać, że testy można uruchamiać z wykorzystaniem Anta.
Pliki z zasadami znajdują się w katalogu tools/ant w instalacji pakietu SDK Andro-
ida. Przyjrzyj się tym plikom. Warto wiedzieć, jakie opcje można zmodyfikować,
aby dostosować proces budowania do potrzeb. Na listingu 14.2 przedstawiono
najważniejsze cele, jednak wspomnieliśmy już, że składają się one z mniejszych
zadań, które można odwzorować na etapy procesu budowania omówione we
wcześniejszym punkcie. W tabeli 14.1 przedstawiono powiązania między zada-
niami a etapami procesu budowania.
Tabela 14.1. Etapy procesu budowania opisane w punkcie 14.1.1 i odpowiadające
im zadania Anta

Numer
Opis Zadania i cele Anta
etapu
1 Generowanie kodu źródłowego plików R.java -resource-src
i Manifest.java oraz interfejsów w AIDL-u
-aidl
2 Kompilowanie całego kodu źródłowego w Javie compile
3 Przekształcanie plików klas na format DEX -dex
4 Pakowanie zasobów aplikacji -package-resources
5* Pakowanie kodu aplikacji i zasobów do pliku APK -package-debug-sign
(tryb debugowania)
-package-release
(tryb produkcyjny)
6* Podpisywanie pliku APK -package-debug-sign
(tryb debugowania)
release (tryb produkcyjny)
7* Wyrównywanie bajtów zasobów w pliku APK debug (tryb debugowania)
release (tryb produkcyjny)
* Dla tych kroków nie istnieje odwzorowanie „jeden do jednego” między etapami a zadaniami
lub celami Anta, ponieważ niektóre zadania i cele obsługują co najmniej dwa etapy.

Zadanie setup jest wykonywane zawsze, niezależnie od uruchomionego celu. Jeśli


nie wywołujesz bezpośrednio żadnego celu, Ant uruchamia cel domyślny, którym
w skryptach Anta w Androidzie jest cel help. Zbudujmy teraz aplikację i zain-
stalujmy ją w działającym emulatorze (jeśli wykonujesz to ćwiczenie, koniecznie
uruchom najpierw emulator!). Potrzebny kod pokazano na listingu 14.3.

Listing 14.3. Budowanie i instalowanie aplikacji za pomocą Anta

matthias:[HelloAnt]$ ant clean install


Buildfile: /Users/matthias/Projects/eclipse/HelloAnt/build.xml
[setup] Android SDK Tools Revision 9
[setup] Project Target: Google APIs
[setup] Vendor: Google Inc.
590 ROZDZIAŁ 14. Zarządzanie budowaniem

[setup] Platform Version: 2.2


[setup] API level: 8
[setup]
[setup] ------------------
[setup] Resolving library dependencies:
[setup] No library dependencies.
[setup]
[setup] ------------------
[setup]
[setup] WARNING: No minSdkVersion value set. Application will
install on all Android versions.
[setup]
[setup] Importing rules file: tools/ant/main_rules.xml

clean:
[delete] Deleting directory /Users/matthias/Projects/eclipse/HelloAnt/bin
[delete] Deleting directory /Users/matthias/Projects/eclipse/HelloAnt/gen

-debug-obfuscation-check:

-set-debug-mode:

-compile-tested-if-test:

-dirs:
[echo] Creating output directories if needed...
[mkdir] Created dir: /Users/matthias/Projects/eclipse/
HelloAnt/bin
[mkdir] Created dir: /Users/matthias/Projects/eclipse/
HelloAnt/gen
[mkdir] Created dir: /Users/matthias/Projects/eclipse/HelloAnt/bin/
classes

-pre-build:

-resource-src:
[echo] Generating R.java / Manifest.java from the resources...

-aidl:
[echo] Compiling aidl files into Java classes...

-pre-compile:

compile:
[javac] /Users/matthias/Library/Development/android-sdk-mac_86
/tools/ant/main_rules.xml:361: warning: 'includeantruntime'
was not set, defaulting to build.sysclasspath=last;
set to false for repeatable builds
[javac] Compiling 2 source files to /Users/matthias/Projects/eclipse/
HelloAnt/bin/classes

-post-compile:

-obfuscate:

-dex:
[echo] Converting compiled files and external libraries into
0 TECHNIKA 81. Budowanie aplikacji za pomocą Anta 591

/Users/matthias/Projects/eclipse/HelloAnt/bin/classes.dex...

-package-resources:
[echo] Packaging resources
[aapt] Creating full resource package...

-package-debug-sign:
[apkbuilder] Creating HelloAnt-debug-unaligned.apk and signing
it with a debug key...

debug:
[echo] Running zip align on final apk...
[echo] Debug Package: /Users/matthias/Projects/eclipse/HelloAnt/bin/
HelloAnt-debug.apk

install:
[echo] Installing /Users/matthias/Projects/eclipse/HelloAnt/bin/
HelloAnt-debug.apk
onto default emulator or device...
[exec] 988 KB/s (154421 bytes in 0.152s)
[exec] pkg: /data/local/tmp/HelloAnt-debug.apk
[exec] Success

BUILD SUCCESSFUL
Total time: 18 seconds

W danych wyjściowych widać, że jedna instrukcja powoduje wykonanie przez Ant


wszystkich etapów procesu budowania aplikacji na Android. Warto zauważyć,
że zadania uruchamiane przez Ant to te opisane w tabeli 14.1.
Wcześniej wspomnieliśmy, że nie bez powodu zapisaliśmy w aplikacji zależ-
ność w postaci biblioteki Apache Commons Lang. Początkowo nie jest oczywiste,
w jaki sposób Ant obsługuje takie zależności. Nie określiliśmy przecież, gdzie
ma znaleźć plik JAR z klasą StringUtils w czasie kompilowania kodu źródłowego.
Mogliśmy tak zrobić, ponieważ potrzebny plik umieściliśmy w katalogu libs/
(biblioteki z tego katalogu są automatycznie dołączane do aplikacji). W Androidzie
w czasie kompilowania aplikacji zadania Anta sprawdzają ten katalog i dodają
jego zawartość do ścieżki klas. Jeśli chcesz wybrać inny katalog, możesz podać
niestandardową ścieżkę we właściwości jar.libs.dir.
OMÓWIENIE
Apache Ant to narzędzie do łatwego budowania aplikacji z wiersza poleceń. Jest
proste w użyciu, ponieważ Android udostępnia zestaw podstawowych zadań
i celów, które programista może zmodyfikować (jeśli w ogóle jest to konieczne).
Problem z Antem polega na tym, że kiedy struktura projektu i zależności stają
się skomplikowane, trudno jest zarządzać plikami budowania. Ant ma dwie
poważne wady, które wcześniej lub później odczujesz.
Po pierwsze, Ant nie ma wbudowanych mechanizmów zarządzania zależno-
ściami. Jeśli liczba potrzebnych plików JAR jest niewielka, można je umieścić
w katalogu libs. Jeżeli jednak korzystasz z licznych bibliotek, możesz natrafić na
592 ROZDZIAŁ 14. Zarządzanie budowaniem

problemy związane ze sprzecznymi zależnościami przechodnimi. Załóżmy, że


dana aplikacja zależy od bibliotek A i B, a te z kolei wymagają różnych wersji
biblioteki C. Prowadzi to do problemów. Ponadto zawsze trzeba samodzielnie
zarządzać zależnościami. Wymaga to znalezienia odpowiednich plików biblio-
tecznych w sieci WWW i pobranie ich do projektu. Także współpracownicy
potrzebują tych plików, dlatego trzeba przesłać je do systemu zarządzania kodem
źródłowym, a wielu programistów nie lubi tego robić. Autorzy projektu Apache Ivy
(http://ant.apache.org/ivy) rozwiązali ten problem przez udostępnienie warstwy
zarządzania zależnościami, którą można wykorzystać w Ancie. Jednak korzysta-
nie z tego projektu wiąże się z dodaniem następnego narzędzia do środowiska
budowania.
Po drugie, Ant nie ma wbudowanej obsługi projektów wielomodułowych. Jeśli
aplikacja obejmuje tylko jeden katalog projektu, nie stanowi to problemu. Jednak
po dodaniu projektów testowych (z poprzedniego rozdziału dowiedziałeś się, że
zawsze należy je stosować) okazuje się, że trzeba zarządzać dwoma niezależnymi
procesami budowania. Czasem trudności są jeszcze większe. W firmie Qype
tworzymy aplikacje na Android składające się z nawet siedmiu projektów: aplikacji,
trzech projektów testowych i trzech projektów bibliotecznych. Połączenie ich
wszystkich w Ancie wymaga manipulowania instrukcjami include, subant i macrodef.
Jest to skomplikowane i żmudne zadanie, które trzeba wykonać ręcznie. W Ancie
nie występują wielomodułowe projekty ani procesy budowania.
Z uwagi na ograniczenia Anta (wynikające z tego, że zarządzanie aplikacjami
odbywa się na poziomie projektu, a nie na poziomie procesu budowania) powstały
inne systemy budowania. Omawiamy tu najbardziej znany i najczęściej stosowany
w świecie Javy system tego typu — Apache Maven. Maven jest dużo bardziej
skomplikowany od Anta. Tu omawiamy tylko podstawy tego narzędzia. Koncen-
trujemy się na stosowaniu Mavena w androidowych projektach, jednak narzędziu
temu poświęcamy cały podrozdział, dlatego mamy nadzieję, że dużo się nauczysz
na temat tego systemu.

14.2. Zarządzanie procesem budowania za pomocą Mavena


System Apache Maven utworzono w ramach frameworku Apache Turbine, prze-
znaczonego do tworzenia aplikacji sieciowych. Maven miał zapewniać środowisko
budowania z wbudowaną obsługą powtarzalnych operacji w średnich i dużych
projektach. Maven jest napisany w Javie. Ponieważ jest rozwijany w ramach pro-
jektu Apache, jest bezpłatny i dostępny jako oprogramowanie o otwartym dostę-
pie do kodu źródłowego.
Maven w odróżnieniu od Anta jest przeznaczony do obsługi całych projektów.
Ich opisywanie i zarządzanie nimi jest główną funkcją Mavena. Ant służy głów-
nie do wykonywania szczegółowych zadań (takich jak kopiowanie), które trzeba
ręcznie powiązać w proceduralny sposób w większe cele (na przykład zbudo-
wanie pakietu z dystrybucją), natomiast Maven ma szerszy zakres działania
14.2. Zarządzanie procesem budowania za pomocą Mavena 593

i zapewnia wbudowaną obsługę powtarzalnych operacji z zakresu zarządzania


projektem. Umożliwia zarządzanie zależnościami, podział projektu na moduły,
zarządzanie procesem budowania, zarządzanie wersjami i dystrybucjami, gene-
rowanie dokumentacji, a nawet generowanie całych witryn projektów. Maven to
coś więcej niż system budowania — to system zarządzania cyklem życia projektu
(choć w celu zachowania prostoty i spójności w dalszej części rozdziału używamy
określenia „system budowania”).
KTÓRĄ WERSJĘ MAVENA OPISUJEMY? Zakładamy, że zainstalowałeś
Maven 3. Narzędzie to możesz pobrać z witryny projektu (http://maven.
apache.org) lub poprzez system zarządzania pakietami. Przykładowy kod
powinien współdziałać także z wersją Maven 2, która jednak została zastą-
piona przez Maven 3 już jakiś czas temu i nie powinna być stosowana.
Najważniejszą różnicą między Mavenem a Antem jest szczegółowość operacji.
Jest to dobrze widoczne w tym, jak w Mavenie postrzegany jest projekt. Skrypty
budowania w Ancie są pisane na dość niskim poziomie szczegółowości pod kątem
wymagań związanych z projektem. W Mavenie punktem wyjścia jest projekt.
Ponieważ projekty sieciowe różnią się od projektów na Android, w Mavenie
można tworzyć szkielet (ang. scaffolding) projektu za pomocą tak zwanych arche-
typów. Przy użyciu archetypów projektu można nakazać Mavenowi wygenero-
wanie domyślnej struktury komunikatów i wyjściowego modelu POM (opisujemy
je dalej). Archetypy dla Androida są dostępne bezpłatnie jako oprogramowanie
o otwartym dostępie do kodu źródłowego dzięki uprzejmości firmy Akquinet
(https://github.com/akquinet/android-archetypes).
Centralnym punktem każdego projektu opartego na Mavenie jest model POM
(ang. Project Object Model). Model ten obejmuje każdy aspekt konfiguracji pro-
jektu — od nazwy, wersji, autorów i współautorów po zależności w postaci biblio-
tek zewnętrznych i repozytoria wersji. Model POM jest opisywany w formacie
XML i przechowywany w katalogu głównym projektu, w pliku pom.xml (dalej
omawiamy, jak wygląda typowa zawartość tego pliku). Polecenia należy podawać
dla uruchamianego z wiersza poleceń narzędzia mvn w taki sam sposób jak
w Ancie. W języku angielskim cele są określane inaczej w Ancie (target), a inaczej
w Mavenie (goal). Jeśli chcesz usunąć pliki tymczasowe, a następnie ponownie
skompilować aplikację, przekaż do Mavena cele1 clean i compile:
$ mvn clean compile

Maven udostępnia rozbudowany system zarządzania zależnościami. W Mavenie


wszystkie elementy uzyskane w wyniku udanego zakończenia budowania to
artefakty. Elementy te można wykorzystać jako dane wejściowe do następnego

1
Drobiazgowi Czytelnicy stwierdzą, że nie jest to ścisły opis; clean i compile to tak naprawdę etapy cyklu
życia, z którymi wiązane są wywoływane później domyślne cele. Jest to jednak tylko szczegół dotyczący
implementacji.
594 ROZDZIAŁ 14. Zarządzanie budowaniem

projektu. Artefaktem może być cokolwiek — od pliku JAR z biblioteką, przez kod
źródłowy lub archiwum JavaDoc, po cały pakiet aplikacji na Android. Elementy
tego typu można przesłać do repozytorium Mavena za pomocą celu deploy, aby
udostępnić je innym programistom. Repozytorium może znajdować się na lokal-
nym komputerze (wkrótce się przekonasz, że taka maszyna zawsze jest dostępna),
a także na serwerze WWW. Ponieważ w Mavenie struktura artefaktów i repo-
zytoriów jest dokładnie zdefiniowana, w dowolnym projekcie, w którym też zasto-
sowano Maven, można wykorzystać zależności z innych projektów przesłane do
internetowego repozytorium Mavena. Wystarczy zadeklarować zależność w modelu
POM danego projektu. Aby można było obsługiwać ten mechanizm, w internecie
udostępnione jest repozytorium nadrzędne o nazwie Maven Central. Oznacza to,
że jeśli aplikacja zależy od biblioteki umieszczonej w tym repozytorium, Maven
automatycznie pobiera tę bibliotekę na lokalny komputer w ramach budowania
i zapisuje ją. Powiązania między modelem POM a repozytoriami różnego rodzaju
pokazano na rysunku 14.5.

Rysunek 14.5. Maven zawsze ma informacje o repozytoriach centralnym i lokalnych,


dlatego nie trzeba ich podawać. Jeśli dodajesz zależność w postaci artefaktu, który
nie znajduje się w repozytorium centralnym, musisz zadeklarować ją w modelu
POM za pomocą znacznika <repository>. Wszystkie artefakty pobierane ze zdalnych
repozytoriów są zapisywane w lokalnym repozytorium

Innym ważnym aspektem Mavena jest jego architektura. Maven, podobnie jak
Eclipse, jest oparty na wtyczkach. Sam Maven to tylko uboga podstawowa war-
stwa, a niemal wszystkie dodatkowe funkcje są dostępne poprzez wtyczki. Na
przykład cel help jest obsługiwany przez wtyczkę maven-help-plugin. Kompilowa-
nie, generowanie dokumentacji JavaDoc — te i inne operacje są dostępne
poprzez wtyczki. Dzięki temu Maven ma wysoce modułowy i rozszerzalny cha-
rakter, co jest ważne w kontekście integracji Mavena z Androidem.
0 TECHNIKA 82. Budowanie za pomocą Mavena 595

Rozpisaliśmy się… Spróbujemy ująć istotę Mavena w jednym zdaniu: jest


bezpłatny i otwarty, jest systemem budowania, systemem zarządzania cyklem
życia projektu, infrastrukturą do dystrybucji oprogramowania i platformą. Aby
lepiej zrozumieć popularność Mavena, przyjrzyj się liczbom. Według rozwijają-
cej to narzędzie firmy Sonatype w centralnym repozytorium przechowywanych
jest obecnie ponad 90 000 artefaktów, a ponad 40 000 firm każdego miesiąca
kieruje do repozytorium blisko 300 milionów żądań. To robi wrażenie!
GDZIE MOGĘ DOWIEDZIEĆ SIĘ CZEGOŚ WIĘCEJ O MAVENIE?
System budowania Maven jest złożony. Nie możemy omówić w tym miej-
scu wszystkich jego aspektów. Staramy się koncentrować na kwestiach
charakterystycznych dla Androida, a przy tym zapewnić Ci podstawy, jeśli
nigdy wcześniej nie używałeś Mavena. W trakcie lektury kilku najbliższych
technik warto jednak mieć pod ręką dokumentację. Przyda się, jeśli będziesz
chciał sprawdzić podstawowe informacje.
Dobrym punktem wyjścia do poznawania Mavena jest strona http://maven.
apache.org/users/index.html.
Skoro masz już podstawową wiedzę, pora przejść do ćwiczeń praktycznych.
Dowiesz się z nich, w jaki sposób przy użyciu Mavena zarządzać procesem budo-
wania w Androidzie. Aby nie przytłoczyć Cię informacjami, w technice 82. zaczy-
namy od przekształcenia prostej aplikacji w stylu Hello World na projekt
Mavena. Choć zalety Mavena ujawniają się w bardziej skomplikowanych projek-
tach, zachowanie prostoty pozwala omówić aplikacje na Android oparte na Mavenie
i pokazać, jakie korzyści daje Maven przy zarządzaniu cyklem życia standardo-
wej aplikacji. W technice 83. przedstawiamy kilka wtyczek środowiska Eclipse,
które umożliwiają budowanie w tym środowisku aplikacji na Android opartych
na Mavenie. Temat kończymy techniką 84., w której pokazujemy, jak ułatwić
sobie zarządzanie lokalnymi bibliotekami Androida jako artefaktami Mavena.

0 TECHNIKA 82. Budowanie za pomocą Mavena

Przedstawiliśmy już Maven i krótki przegląd architektury oraz zalet tego sys-
temu. Pozostaje nam jeszcze dołączyć do tego Android.
PROBLEM
Androidowy projekt stał się na tyle duży, że dojrzały system zarządzania zależ-
nościami, obsługa podmodułów i bogaty zestaw wtyczek z Mavena są dobrym
wyborem do zarządzania procesem budowania. Chcemy pokazać, jak wygląda
typowy model POM dla typowego projektu aplikacji na Android. Omawiamy też
cele charakterystyczne dla Androida.
596 ROZDZIAŁ 14. Zarządzanie budowaniem

ROZWIĄZANIA
Wspomnieliśmy już, że sam Maven jest platformą, do której można dodawać nowe
funkcje z wykorzystaniem wtyczek. Nie powinno zaskakiwać, że to samo dotyczy
obsługi Androida w Mavenie. Służy do tego wtyczka maven-android-plugin.
W pierwszej technice związanej z Mavenem na podstawie aplikacji typu Hello
World z techniki 81. tworzymy plik APK. Wykorzystujemy do tego Maven i andro-
idową wtyczkę. W tej technice koncentrujemy się na modelu POM projektów
aplikacji na Android i celach Mavena dostępnych w androidowej wtyczce, które
ułatwiają programistom pracę.
WTYCZKI I ZALEŻNOŚCI. Zauważmy, że wtyczki w Mavenie są trak-
towane jak zależności w postaci bibliotek. Nie trzeba ręcznie pobierać
ani instalować wtyczek. Wystarczy zadeklarować je w modelu POM,
a Maven automatycznie wczyta wtyczki, które nie są jeszcze dostępne
(jeśli potrafi je znaleźć w znanych repozytoriach).
Po utworzeniu projektu za pomocą kreatora ze środowiska Eclipse lub polecenia
android create project trzeba wykonać tylko jedno zadanie, aby dostosować apli-
kację do Mavena. Należy utworzyć deskryptor projektu (plik pom.xml) w katalogu
głównym projektu. Plik ten można napisać samodzielnie, jednak zwykle łatwiej
jest zacząć od szablonu. Można na przykład użyć instrukcji mvn archetype:generate
i androidowych archetypów opracowanych przez firmę Akquinet. W projekcie do
pobrania przygotowaliśmy wszystkie potrzebne elementy.
POBIERZ PROJEKT HELLOMAVEN. Kod źródłowy projektu do uru-
chamiania aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skoncentrować
się na konkretnych zagadnieniach, zalecamy pobranie kompletnego kodu
źródłowego i śledzenie go w Eclipse (lub innym środowisku IDE albo
edytorze tekstu).
Kod źródłowy: http://mng.bz/a9FY.
Na listingu 14.4 znajduje się pierwsza część pliku pom.xml z przykładowej
aplikacji. Plik jest podzielony na dwie części w celu zwiększenia jego czytelności.
Pierwsza część obejmuje kod powiązany z samą aplikacją i zależnościami, nato-
miast w części drugiej znajdują się ustawienia dotyczące procesu budowania.
W następnej technice zobaczysz, jak obsługiwać model POM za pomocą śro-
dowiska IDE.

Listing 14.4. Plik modelu POM dla prostej aplikacji na Android (część 1.)

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
0 TECHNIKA 82. Budowanie za pomocą Mavena 597

<groupId>com.manning.aip.maven</groupId>
<artifactId>HelloMaven</artifactId>
<version>1.0-SNAPSHOT</version>

<name>HelloMaven</name>
<description>Aplikacja na Android zbudowana za pomocą Mavena</description>

<packaging>apk</packaging>

<properties>
<androidVersion>2.2.1</androidVersion>
<project.build.sourceEncoding>UTF-8
</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
<version>${androidVersion}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
...
[Reszta modelu POM znajduje się na listingu 14.5]
</project>

Najpierw trzeba przekazać Mavenowi metadane . Każdy model POM zaczyna


się od elementu <project>, który przyjmuje kilka atrybutów używanych w narzę-
dziach do sprawdzania poprawności schematu. Element <modelVersion> informuje
interpreter modelu POM Mavena o używanej wersji modelu. Tu jest to wersja
4.0.0. Obecnie jest to jedyna wersja obsługiwana przez Maven (ma się to zmienić
w Mavenie 3.1).
Dalej zaczyna się konfiguracja właściwa dla projektu . Tu kod staje się
ciekawszy. Wspomnieliśmy wcześniej, że typowym efektem procesu budowania
w Mavenie jest artefakt (tu jest nim plik APK). Artefakty często są zapisywane
w publicznych repozytoriach Mavena, dlatego każdy z nich musi mieć globalnie
niepowtarzalne identyfikatory. Do jednoznacznego identyfikowania artefaktów
w Mavenie służą dwa identyfikatory: groupId i artifactId. Identyfikator groupId
określa jednostkę zarządzającą artefaktem. Dlatego na przykład identyfikator
firmy musi być globalnie niepowtarzalny. Podobnie jak w pakietach Javy, tak i tu
jako identyfikator można stosować domenę internetową firmy. Jednostka może
rozwijać wiele artefaktów, dlatego w Mavenie używany jest też drugi identyfikator,
artifactId. Musi on być niepowtarzalny w ramach firmy, jednak nie musi być
598 ROZDZIAŁ 14. Zarządzanie budowaniem

unikatowy w skali globalnej. Połączenie obu identyfikatorów wystarcza do jed-


noznacznego zidentyfikowania artefaktu. Ponieważ artefakty mogą mieć wiele
wersji, trzeba też określić atrybut version (przyjmuje on dowolne wartości).
Wyobraź sobie, że świat artefaktów Mavena to sześcian; w tym świecie wartości
groupId, artifactId i version to współrzędne pozwalające zlokalizować określony
artefakt. Można też podać opisowe atrybuty name i description. Są one opcjonalne
i służą jako dokumentacja.
Domyślnie Maven obsługuje osiem różnych rodzajów pakietów artefaktów
(POM, JAR, WAR, EAR, RAR i inne), jednak nie ma wśród nich pakietów APK.
A przecież chcemy utworzyć aplikację na Android, a nie zwykłe archiwum Javy.
Potrzebny rodzaj pakietów (omawiamy go dalej) jest dołączany przez wtyczkę
maven-android-plugin.
Podobnie jak w plikach budowania Anta, tak i tu można ustawić właściwości.
Służy do tego element property . Niektóre właściwości są częścią Mavena,
a inne pochodzą z różnych wtyczek używanych w typowym procesie budowania.
Można też tworzyć własne właściwości. Tak dodaliśmy na przykład właściwość
androidVersion, która później okaże się przydatna.
W ostatnim kroku deklarujemy zależności aplikacji od innych artefaktów
Mavena . Przede wszystkim informujemy Maven, że potrzebny jest plik
android.jar. Bez niego kompilacja zakończy się niepowodzeniem. Na szczęście
społeczność skupiona wokół Androida i Mavena zdołała umieścić wspomniany
plik w repozytorium Maven Central. Jest on zarchiwizowany pod nazwą com.google.
´android:android. Użyty zapis groupId:artifactId[:version] jest powszechnie
stosowany w Mavenie. Zetkniesz się z nim jeszcze w innych sytuacjach.
GOOGLE I MAVEN — ZWIĄZEK BEZ MIŁOŚCI. Warto podkreślić,
że choć od czerwca 2010 roku artefakty dla Androida znajdują się w repo-
zytorium Maven Central, Google nie jest zaangażowany w prace związane
z Mavenem i wspomnianymi artefaktami. Co więcej, Google odrzucił prośby
skupionej wokół Mavena społeczności o rozwijanie artefaktów w ramach
oficjalnych prac nad Androidem. Dlatego jej członkowie samodzielnie
kontynuowali potrzebne prace. Więcej informacji o tej historii znajdziesz
we wpisie 4577 z systemu śledzenia błędów w Androidzie. Znajduje się
w nim oficjalna odpowiedź Jean-Baptiste’a Queru, kierownika technicznego
projektu AOSP w firmie Google.
Może zauważyłeś, że element scope ma wartość provided. Maven używa ele-
mentu scope do ustalenia, kiedy zależności powinny być widoczne w ścieżce klas.
Domyślna wartość elementu to compile. Oznacza to, że określona zależność jest
niezbędna do kompilowania i uruchamiania aplikacji. Wartość runtime informuje,
że zależność nie jest wymagana przy kompilacji, ale trzeba ją wczytać w trakcie
wykonywania programu. Podanie wartości compile lub runtime sprawia, że Maven
pakuje i instaluje zależność razem z aplikacją. W przypadku pliku android.jar
0 TECHNIKA 82. Budowanie za pomocą Mavena 599

jest to niepożądane, ponieważ biblioteka ta jest częścią obrazu systemu w urządze-


niu, w którym aplikacja działa. W takiej sytuacji przydatna jest wartość provided.
Informuje ona Maven, że zależność jest potrzebna do skompilowania aplikacji, ale
nie należy jej instalować, ponieważ jest dostępna w czasie wykonywania pro-
gramu (w tym kontekście udostępnia ją urządzenie z Androidem).
Aplikacja HelloMaven przypomina aplikację HelloAnt z techniki 74. Zmie-
niliśmy tylko niektóre słowa i plik build.xml Anta zastąpiliśmy plikiem pom.xml
Mavena. Do skompilowania aplikacji potrzebna jest biblioteka Apache Commons
Lang, która pozwala wyświetlić komunikat toast „Witaj, Mavenie” za pomocą
klasy pomocniczej StringUtils. Przypominamy, że Ant szuka bibliotek zewnętrz-
nych w katalogu libs/, natomiast Maven pobiera je z repozytoriów. To dlatego
trzeba zadeklarować bibliotekę commons-lang w zależnościach. Maven automa-
tycznie określa położenie biblioteki i pobiera ją do lokalnego repozytorium (chyba
że biblioteka już się w nim znajduje).
Do tego miejsca konfigurowaliśmy model POM, aby przekazać Mavenowi
wszystkie podstawowe informacje na temat aplikacji, identyfikatory artefaktów,
nazwę i opis oraz listę zależności. Problem polega na tym, że dane te nie wystar-
czają do skompilowania aplikacji. Maven nie wie nawet, czym jest plik APK
podany jako format pakietu. Dlatego w następnym kroku trzeba powiązać wtyczkę
maven-android-plugin z etapem budowania w Mavenie. Okazuje się, że zadanie
to można wykonać w modelu POM. Na listingu 14.5 pokazujemy, jak to zrobić.

Listing 14.5. Plik modelu POM prostej aplikacji na Android (część 2.)

<project xmlns="http://maven.apache.org/POM/4.0.0" ...


...
[Początek pliku modelu POM przedstawiono na listingu 14.4]

<build>
<sourceDirectory>src/</sourceDirectory>

<plugin>
<groupId>com.jayway.maven.plugins.android.
generation2</groupId>
<artifactId>maven-android-plugin</artifactId>
<version>2.8.4</version>

<configuration>
<sdk>
<platform>8</platform>
</sdk>

<undeployBeforeDeploy>true</undeployBeforeDeploy>

<emulator>
<avd>android-2.2-normal-mdpi</avd>
<wait>30000</wait>
</emulator>
</configuration>
600 ROZDZIAŁ 14. Zarządzanie budowaniem

<executions>
<execution>
<id>alignApk</id>
<phase>install</phase>
<goals>
<goal>zipalign</goal>
</goals>
</execution>
</executions>

<extensions>true</extensions>
</plugin>
</plugins>
</build>

</project>

Ponownie prześledźmy kod od początku do końca. Jak widać, w pliku modelu


POM znajduje się nowy element, build. Obejmuje on wszystkie informacje
potrzebne Mavenowi do przekształcenia androidowego projektu na artefakt
generowany w procesie budowania. Trzeba skonfigurować tu dwie podstawowe
rzeczy.
Po pierwsze, należy podać aspekty związane z wejściem i wyjściem procesu
budowania, na przykład katalog z kodem źródłowym, nazwę katalogu na dane
wyjściowe (domyślnie jest to katalog target/), nazwę generowanego pliku z pakie-
tem itd. Informacje te są potrzebne tylko wtedy, gdy standardowe ustawienia
z Mavena są nieodpowiednie. Wspomnieliśmy wcześniej, że w Mavenie przyj-
mowane są odpowiednie założenia dotyczące struktury projektu, w tym jego
katalogów. Na przykład kod źródłowy aplikacji powinien znajdować się w kata-
logu src/main/java, a kod źródłowy testów — w katalogu src/test/java. Typowe
aplikacje na Android mają inną strukturę. Testy znajdują się w odrębnym projekcie
(zobacz rozdział 13.). Ponadto w przykładzie używamy tylko jednego języka pro-
gramowania, dlatego potrzebny jest jeden katalog na kod źródłowy. W elemencie
sourceDirectory informujemy Maven, że kod źródłowy aplikacji znajduje się
w katalogu src/ . Warto zauważyć, że Maven nadal przyjmuje, iż testy są zapisane
w katalogu src/test/java, jednak z uwagi na to, że katalog ten nie istnieje, narzędzie
automatycznie go pomija.
Po drugie, trzeba zadeklarować i skonfigurować wszystkie wtyczki potrzebne
do zbudowania aplikacji. Tu niezbędna jest wtyczka maven-android-plugin. Ta
androidowa wtyczka znajduje się w nieco przypadkowej grupie com.jayway.maven.
´plugins.android.generation2 i w czasie powstawania tej książki dostępna była
w wersji 2.8.4. Takie właśnie dane znajdują się w deklaracji .
Jak widać, konfiguracja androidowej wtyczki składa się z dwóch bloków:
configuration i executions. Element configuration służy do konfigurowania
wtyczki. Najpierw trzeba określić docelową wersję interfejsu API Androida .
Tu jest nią wersja 8., odpowiadająca Androidowi 2.2.
0 TECHNIKA 82. Budowanie za pomocą Mavena 601

WŁAŚCIWOŚCI W MAVENIE. Warto wiedzieć, że właściwości można


stosować jako wartości elementów, używając zapisu ${...}, który stoso-
waliśmy już w Ancie. Istnieją też pewne domyślne właściwości, których nie
trzeba definiować samodzielnie (są one zawsze dostępne). Jedną z nich
jest evn, obejmująca aktualne zmienne środowiskowe z powłoki, w której
uruchomiono polecenie mvn. Inna taka właściwość to project. Można
z niej pobrać wszystkie ustawienia projektu. Jeszcze inna, settings, obej-
muje konfigurację z opcjonalnego pliku settings.xml z katalogu głównego
Mavena. Ponadto można uzyskać bezpośredni dostęp do dowolnej właści-
wości systemowej Javy, którą w innym kodzie można wczytać przy użyciu
instrukcji System.getProperty.
Ponadto za pomocą elementu undeployBeforeDeploy nakazujemy wtyczce odin-
stalowanie istniejących już plików APK przed zainstalowaniem nowych. Pozwala
to uniknąć na przykład problemów z ponowną instalacją z uwagi na różne certy-
fikaty użyte do podpisania plików APK. Ostatni konfigurowany aspekt to emu-
lator. Wtyczka maven-android-plugin, o czym się wkrótce przekonasz, potrafi
automatycznie uruchamiać i zatrzymywać emulator Androida. Nazwę urządzenia
AVD ustawiamy na android-<wersja>-normal-mdpi. Jest to dobry schemat dla nazw
urządzeń AVD. Jeśli potrzebny emulator AVD ma inną nazwę, trzeba użyć właśnie
jej (informacje o zarządzaniu urządzeniami AVD znajdziesz w punkcie 1.6.2) .
Wtyczka jest już skonfigurowana, należy jednak wykonać jeszcze jedną ope-
rację. W technice 74. wyjaśniliśmy, że skrypt budowania Anta automatycznie
używa programu zipalign dla pliku APK przed umieszczeniem go w urządzeniu.
Niestety, wtyczka maven-android-plugin domyślnie nie wykonuje tej operacji,
dlatego trzeba dodać odpowiednie instrukcje. Aby nakazać wtyczce wykonanie
dodatkowych operacji w ramach domyślnego procesu pracy, używamy elementu
Mavena execution . Element ten pozwala wzbogacić zadanie wykonywane
przez wtyczkę. Ma określony identyfikator (tu jest nim alignApk) i uchwyty do
konkretnych punktów w procesie budowania w Mavenie. W Mavenie proces
budowania jest dobrze zdefiniowany i stosunkowo złożony; nie omawiamy go
w tym miejscu. W przykładzie chcemy wywołać cel zipalign wtyczki maven-
-android-plugin w ramach etapu install. Jeśli na razie nie wiesz, co to oznacza,
może stanie się to jasne po wprowadzeniu do różnych celów obsługiwanych
przez wtyczkę. Warto przypomnieć, że wyrównywanie bajtów, jak wspomnieliśmy
w podrozdziale 14.1, jest opcjonalne w trakcie rozwijania oprogramowania, ale
zalecane w procesie budowania wersji produkcyjnej.
To już prawie koniec. Ostatni nowy element to extensions. Jest on niezwykle
ważny, ponieważ bez niego wtyczka nie może wzbogacić domyślnego procesu
budowania w Mavenie. Przyznajemy, przedstawiony tu materiał nie jest specjal-
nie ciekawy, jednak bez tych informacji nie można wykonać w Mavenie żad-
nych zaawansowanych operacji. Co więc możesz teraz zrobić? Czasem tabele są
602 ROZDZIAŁ 14. Zarządzanie budowaniem

bardziej zrozumiałe od zwykłego tekstu. W tabeli 14.2 znajduje się przegląd


nowych celów, dostępnych we wtyczce maven-android-plugin.
Tabela 14.2. Cele udostępniane przez wtyczkę maven-android-plugin

Cel* Opis

apk Tworzy plik APK (domyślnie podpisuje go kluczem z pliku


debug.keystore).
deploy Instaluje zbudowany plik APK lub inny plik APK w podłączonym
urządzeniu.
deploy-dependencies Instaluje wszystkie zależności dla pakietów typu apk (jest to wymagane
w projektach testów z instrumentacją).
dex Przekształca wszystkie pliki klas Javy na format DEX.

emulator-start Uruchamia emulator skonfigurowany w modelu POM.

emulator-stop Zatrzymuje emulator skonfigurowany w modelu POM.

generate-sources Generuje pliki R.java i Manifest.java oraz klasy w AIDL-u.

instrument Przeprowadza w urządzeniu testy z instrumentacją.

pull Kopiuje plik lub katalog z urządzenia na lokalny komputer.

push Kopiuje plik lub katalog z lokalnego komputera na urządzenie.

redeploy Skrót pozwalający odinstalować i zainstalować aplikację.

undeploy Odinstalowuje aplikację z podłączonego urządzenia.

unpack Wypakowuje plik JAR aplikacji do katalogu target/android-classes.

zipalign Wywołuje narzędzie zipalign, aby wyrównać bajty w zasobach


w pliku APK.
* Nie zapomnij poprzedzić tych celów przedrostkiem android: przy podawaniu ich w poleceniu mvn.

Z tabeli wynika, że dobrym pomysłem jest uporządkowanie i zbudowanie aplikacji,


uruchomienie emulatora i zainstalowanie na nim tej aplikacji. Uruchom zatem
powłokę, przejdź do katalogu projektu HelloMaven i wpisz następującą instrukcję:
$mvn clean android:emulator-start install android:deploy

Upewnij się, że zmienna $ANDROID_HOME prowadzi do instalacji pakietu SDK i że


w systemie zainstalowany jest kompilator zgodny z Javą 5. Warto zauważyć, że
cele emulator-start i deploy są poprzedzone kwalifikatorem android:. Jest tak,
ponieważ są to cele udostępniane przez wtyczkę maven-android-plugin, nato-
miast cele clean i install to części domyślnego procesu budowania z Mavena.
Maven poprawnie interpretuje wspomniany kwalifikator dla androidowych pro-
jektów wykorzystujących Maven (wczytywana jest dla nich wtyczka Androida).
Wróćmy do polecenia. Uruchomienie Mavena z czterema użytymi celami
prowadzi do wyświetlenia w standardowym wyjściu przedstawionych dalej infor-
macji (przy pierwszym wywołaniu instrukcji system pobiera też szereg plików).
Na początku znajdują się informacje związane z celami clean i emulator-start:
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------------------------------------------------
[INFO] Building HelloMaven 1.0-SNAPSHOT
0 TECHNIKA 82. Budowanie za pomocą Mavena 603

[INFO] --------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plug-in:2.4.1:clean (default-clean) @ HelloMaven ---
[INFO] Deleting /Users/matthias/Projects/eclipse/HelloMaven/target
[INFO]
[INFO] --------------------------------------------------------------------
[INFO] Building HelloMaven 1.0-SNAPSHOT
[INFO] --------------------------------------------------------------------
[INFO]
[INFO] --- maven-android-plug-in:2.8.4:emulator-start (default-cli)
@ HelloMaven ---[INFO] Android emulator command:
/Users/matthias/Library/Development/android-sdk-mac_86/tools/
emulator -avd android-2.2-normal-mdpi
unknown
[INFO] Starting android emulator with script: /var/folders/0J/
0JjfHEzqFIyzAHVWetQjWk+++TM/-Tmp-//
maven-android-plug-in-emulator-start.sh
[INFO] Waiting for emulator start:30000

Można dostosować 30-sekundowy limit czasu oczekiwania na uruchomienie emu-


latora do szybkości komputera. Warto zauważyć, że jeśli emulator danego urzą-
dzenia AVD jest już gotowy, cel emulator-start nie wykonuje żadnych operacji.
Dane wyjściowe wyglądają wtedy tak:
[INFO] --- maven-android-plug-in:2.8.4:emulator-start (default-cli)
@ HelloMaven ---
[INFO] Android emulator command: /Users/matthias/Library/Development/android-
sdk-mac_86/tools/
emulator -avd android-2.2-normal-mdpi
emulator-5554
[INFO] Emulator emulator-5554 already running. Skipping start and wait.

W tym momencie Maven przechodzi do etapu budowania. Przebiega on podob-


nie jak w Ancie. System kompiluje kod źródłowy, pakuje zasoby, tworzy plik APK
i wyrównuje bajty. Pokazane poniżej dane wyjściowe skrócono. Obejmują one
tylko informacje dotyczące uruchomionych celów.
[INFO] --- maven-android-plug-in:2.8.4:generate-sources
(default-generate-sources) @ HelloMaven ---
[INFO] --- maven-resources-plug-in:2.4.3:resources (default-resources)
@ HelloMaven ---
[INFO] --- maven-compiler-plug-in:2.3.2:compile (default-compile)
@ HelloMaven ---
[INFO] --- maven-jar-plug-in:2.3.1:jar (default-jar) @ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:unpack (default-unpack)
v @ HelloMaven ---
[INFO] --- maven-resources-plug-in:2.4.3:testResources
(default-testResources) @ HelloMaven ---
[INFO] --- maven-compiler-plug-in:2.3.2:testCompile
(default-testCompile) @ HelloMaven ---
[INFO] --- maven-surefire-plug-in:2.6:test (default-test)
@ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:dex (default-dex)
@ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:apk (default-apk)
@ HelloMaven ---
604 ROZDZIAŁ 14. Zarządzanie budowaniem

[INFO] --- maven-android-plug-in:2.8.4:internal-pre-integration-test


(default-internal-pre-integration-test) @ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:internal-integration-test
(default-internal-integration-test) @ HelloMaven ---
[INFO] --- maven-install-plug-in:2.3.1:install (default-install)
@ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:zipalign (alignApk)
@ HelloMaven ---
[INFO] --- maven-android-plug-in:2.8.4:deploy (default-cli)
@ HelloMaven ---

W tym fragmencie widać, w jaki sposób wtyczka maven-android-plugin podłącza


się do domyślnego procesu budowania w Mavenie. Odbywa się to w kilku ściśle
określonych punktach. Na przykład cel android:generate-sources (generujący plik
R.java) jest wstawiany przed celem compile wtyczki maven-compiler-plugin, ponie-
waż jest niezbędny do przeprowadzenia kompilacji.
Wykonywane cele niemal dokładnie odpowiadają uruchamianym wcześniej
celom Anta (i ogólnym etapom budowania opisanym w punkcie 14.1.1). Dodat-
kowych wyjaśnień może wymagać cel install — zwłaszcza jeśli nie używałeś
wcześniej Mavena. Cel install pochodzi z wtyczki maven-install-plugin i odpo-
wiada za skopiowanie budowanego artefaktu do lokalnego repozytorium Mavena.
Repozytorium to znajduje się w katalogu <katalog-główny-użytkownika>/.m2/
repository. Inne aplikacje zarządzane za pomocą Mavena mogą wykorzystać pliki
APK i JAR wygenerowane w procesie budowania. Wymaga to dodania odpo-
wiedniej zależności do modelu POM.
Jeśli wszystko przebiega poprawnie, po zakończeniu całego procesu budo-
wania pojawiają się następujące wiersze:
...
[INFO] --------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] --------------------------------------------------------------------
[INFO] Total time: 1 minute 9 seconds
[INFO] Finished at: Sun Jan 23 18:55:22 CET 2011
[INFO] Final Memory: 51M/123M
[INFO] --------------------------------------------------------------------

Voilà, udany proces budowania przeprowadzony przez Maven!


NIEUDANY PROCES BUDOWANIA. Proces budowania może zakończyć
się niepowodzeniem z kilku przyczyn. Upewnij się, że zmienne środowi-
skowe (JAVA_HOME, ANDROID_HOME i PATH) są odpowiednio skonfigurowane,
a także że używasz najnowszego kompilatora Javy (w czasie powstawania
tej książki była to wersja 6.).
Ponadto narzędziu dx Androida może zabraknąć pamięci przy prze-
twarzaniu dużej liczby klas. Jeśli w trakcie budowania pojawi się błąd
OutOfMemoryError, dodaj do bloku konfiguracji wtyczki poniższy kod:
0 TECHNIKA 82. Budowanie za pomocą Mavena 605

<jvmArguments>
<jvmArgument>-Xms256m</jvmArgument>
</jvmArguments>

Zapewnia to maszynie JVM Mavena potrzebną dodatkową pamięć.


W ostatnim kroku warto wspomnieć o określaniu docelowych urządzeń. Większość
celów wtyczki jest wykonywanych dla urządzenia. Instalowanie pliku APK,
przesyłanie pliku lub przeprowadzanie testów z instrumentacją wymaga urzą-
dzenia z Androidem. Jeśli podłączone jest tylko jedno urządzenie, wtyczka wybiera
właśnie je. Jeżeli jednak podłączonych jest więcej urządzeń (na przykład telefon
i emulator), nie wykonuje żadnych operacji, dlatego trzeba bezpośrednio wskazać
docelowe urządzenie — należy przekazać do Mavena właściwość systemową.
Służy do tego standardowa opcja –D Javy:
$mvn deploy -Dandroid:device=emulator-5556

Jeśli chcesz wiedzieć, jakie opcje można przekazać do wtyczki maven-android-


plugin, możesz się tego dowiedzieć za pośrednictwem wtyczki help Mavena.
Listę dostępnych celów (opisanych w tabeli 14.2) można pobrać przez wywołanie
następującej instrukcji:
$mvn help:describe -Dplugin=com.jayway.maven.plugins.android.
generation2:maven-android-plugin

Działa ona także dla konkretnych celów:


$mvn help:describe -Dcmd=android:<goal_name>

UWAGA. Jeszcze bardziej szczegółowy opis dostępnych celów i parame-


trów otrzymasz po dodaniu do instrukcji help:describe właściwości –Ddetail.
To już koniec praktycznego wprowadzenia do Mavena. Jeśli znałeś ten system
przed lekturą tej techniki, ale nie stosowałeś go do androidowych projektów,
prawdopodobnie cieszysz się, że nie musisz rezygnować z niego przy przejściu
ze świata JSE i JEE do Androida. Jeżeli nigdy wcześniej nie korzystałeś z Mavena,
mamy nadzieję, że zainteresowały Cię jego zalety i możliwości zastosowania go do
własnych aplikacji.
OMÓWIENIE
Nawet w prostej przykładowej aplikacji widać, jak dużo pracy Maven wykonuje
za programistę. Robi wszystko to, co Eclipse i ADT (między innymi kompiluje
kod źródłowy i tworzy pliki APK), a oprócz tego wykonuje dodatkowe zadania.
Maven automatycznie zarządza zależnościami w postaci innych projektów
i niezależnych bibliotek. Między innymi obsługuje zależności przechodnie
i konflikty wersji. Chcesz zastosować na przykład bibliotekę RoboGuice? Nic prost-
szego — zapisz ją na liście zależności w modelu POM, a Maven zrobi resztę.
Kolejną zaletą Mavena jest podział aplikacji na mniejsze podmoduły, co uła-
twia konserwację kodu i jego powtórne wykorzystanie. Kod, który chcesz wyod-
rębnić, możesz umieścić w nowym projekcie, utworzyć model POM dla tego
606 ROZDZIAŁ 14. Zarządzanie budowaniem

projektu, a następnie zadeklarować zależność od niego w innych aplikacjach


zarządzanych za pomocą Mavena. System ten pozwala też połączyć kilka projek-
tów w jeden projekt nadrzędny z wykorzystaniem głównego modelu POM. Jest
to przydatne w projektach aplikacji na Android, gdzie zwykle projekty testowe
są przechowywane niezależnie od projektów aplikacji. Oba projekty można
umieścić w głównym modelu POM, co pozwala budować i zarządzać nimi wspól-
nie. Warto zauważyć, że nie omawiamy tu budowania rozwiązań wielomodu-
łowych. Przykłady takich projektów znajdziesz w witrynie androidowej wtyczki
Mavena: http://code.google.com/p/maven-android-plugin.
Maven można też rozszerzyć, używając różnych innych wtyczek. Pozwala to
dodać wiele istniejących już funkcji. Można na przykład dodać wszystkie funkcje
Anta do procesu budowania w Mavenie (służy do tego wtyczka maven-antrun-
plugin) lub wykorzystać wtyczkę maven-proguard-plugin do modyfikowania wyge-
nerowanych klas.
Musimy też wspomnieć o wadach stosowania Mavena. Jest to dość skompli-
kowany system budowania i początki jego nauki mogą być trudne. Często na
podstawie modelu POM niełatwo jest ustalić, jak wykonać określone zadanie.
Dotyczy to zwłaszcza prostych operacji w rodzaju: „Wyświetl coś w standardo-
wym wyjściu”. Wykonywanie takich zadań w Mavenie bywa niewygodne. Ponadto
choć liczba bibliotek dostępnych jako artefakty Mavena w publicznych repozyto-
riach jest bardzo duża, czasem potrzebny jest plik JAR nieopublikowany w tej
postaci. Oznacza to, że Maven nie może bezpośrednio go zastosować. Programista
musi wtedy ręcznie wykonać potrzebne operacje, czyli pobrać plik i przekształcić
go na lokalny artefakt Mavena za pomocą celu mvn install-file. Sytuację tego
rodzaju opisujemy w technice 77.
CO MOŻNA ZASTOSOWAĆ ZAMIAST MAVENA? Warto wspomnieć
o kilku obiecujących innych narzędziach do budowania aplikacji na Android.
Godne uwagi są przede wszystkim androidowe wtyczki dla systemów
budowania Gradle i SBT. Obie wtyczki oparte są na językach dynamicz-
nych (Groovym i Scali) oraz zgodne z paradygmatem „skrypty budowania
to kod”. Sprawia to, że pisanie skryptów budowania jest znacznie łatwiejsze
i naturalniejsze. Wtyczki znajdziesz na stronach https://github.com/jvoegele/
gradle-android-plugin (dla systemu Gradle) i https://github.com/jberkel/
android-plugin (dla systemu SBT).
Pora przejść dalej. Wiesz już wszystko, co jest potrzebne do budowania aplikacji
z wykorzystaniem Mavena z poziomu wiersza poleceń. Tego właśnie dotyczy
niniejszy podrozdział. Do rozważenia pozostała jeszcze jedna kwestia. Można
zadeklarować w pliku modelu POM wszystkie zależności w postaci bibliotek,
a Maven zajmie się wyszukaniem, pobraniem i powiązaniem wszystkich tych
bibliotek. Jak wykorzystać tę możliwość w środowisku Eclipse? Nie chcemy
przecież utracić zalet tego środowiska, jednak Eclipse nie ma żadnych informacji
0 TECHNIKA 83. Wtyczka Mavena dla środowiska Eclipse 607

na temat repozytoriów Mavena. Z uwagi na to, że nie istnieje katalog libs/, który
można by dodać do ścieżki budowania projektu w środowisku Eclipse, wygląda
na to, iż potrzebny jest mechanizm pozwalający wykorzystać zalety obu środowisk.
Na szczęście przemyśleliśmy to i opracowaliśmy technikę, która pozwala te
środowiska połączyć.

0 TECHNIKA 83. Wtyczka Mavena dla środowiska Eclipse

Ta technika — w porównaniu z poprzednią — jest stosunkowo krótka, ale przy-


datna. Wiesz już, jak za pomocą Mavena budować projekty w programowy sposób
z poziomu wiersza poleceń. Rozwiązanie to doskonale nadaje się do automaty-
zowania zadań w serwerze budowania (opisano to w technice 78.), jednak nadal
chcemy móc pisać kod w środowisku Eclipse bez utraty funkcji zapewnianych
przez wtyczkę ADT. Dlaczego jednak w ogóle obawiamy się utraty tych funkcji?
No cóż, otwórz projekt HelloMaven w środowisku Eclipse i sam się o tym prze-
konaj (rysunek 14.6).

Rysunek 14.6.
Eclipse domyślnie
nie potrafi rozwiązać
zależności
w postaci bibliotek
zdefiniowanych
w pliku modelu POM
Mavena. Wyraźnie
widać, że brakuje
mechanizmu
łączącego Eclipse
z Mavenem

Problem polega na tym, że zależności w postaci bibliotek (tu plik commons-


lang-2.5.jar) nie są przechowywane w katalogu projektu. W poprzedniej technice
dowiedziałeś się, że Maven zapisuje artefakty w lokalnym repozytorium. Można
ręcznie dodać katalog z repozytorium do ścieżki budowania projektu, jest to
jednak niewygodne rozwiązanie, ponieważ po zmianie w modelu POM numeru
wersji potrzebnej biblioteki trzeba ręcznie zmodyfikować ścieżkę budowania
w Eclipse i dostosować ją do modyfikacji.
Poprawnym rozwiązaniem jest zastosowanie innej wtyczki, m2eclipse. Jest
to oficjalna wtyczka Mavena dla środowiska Eclipse. Automatycznie integruje ona
aplikacje oparte na Mavenie ze środowiskiem Eclipse. Jeśli zastanawiasz się:
„Chwileczkę, prezentujecie technikę opisującą, jak zainstalować wtyczkę?”, daj
nam dokończyć. Rozwiązanie nie jest tak proste, jak się wydaje. Mamy trzy różne
komponenty związane z budowaniem, które muszą ze sobą współdziałać. Oto te
komponenty:
608 ROZDZIAŁ 14. Zarządzanie budowaniem

1. Eclipse i JDT. Samo środowisko Eclipse (a dokładniej — wtyczka JDT)


zapewnia podstawowe mechanizmy Javy, na przykład do kolorowania
kodu źródłowego i kompilowania plików z kodem źródłowym w Javie
na kod bajtowy Javy.
2. ADT. Wtyczka z pakietem narzędzi dla Androida wykonuje zadania
właściwe dla tej platformy, na przykład przekształca kod bajtowy w Javie
na kod bajtowy Dalvika i tworzy pliki APK.
3. Maven i maven-android-plugin. Funkcje Mavena pokrywają się z funkcjami
wtyczek JDT i ADT. Wtyczka maven-android-plugin także kompiluje
kod Javy i tworzy pliki APK, jednak w środowisku Eclipse operacje te są
wykonywane przez narzędzia z punktów 1. i 2. Potrzebna jest natomiast
integracja systemu wyszukiwania zależności.
Sama wtyczka m2eclipse nie wystarcza do uzyskania pożądanych efektów. Potrafi
ona przekazać określone w Mavenie zależności do wtyczki JDT, co pozwala
skompilować klasyczną aplikację Javy, w której zależności w postaci bibliotek są
zdefiniowane właśnie za pomocą Mavena. Jednak wtyczka ADT nie obsługuje
modelu POM, dlatego nie wykrywa zależności zdefiniowanych w Mavenie.
W punkcie 13.1.1 napisaliśmy, że w etapie 3., kiedy to Android generuje kod
bajtowy Dalvika, wszystkie zależności w postaci bibliotek JAR stają się częścią
pliku classes.dex. Wtyczka ADT nie ma informacji nawet o tym, że zależności
w postaci bibliotek są zdefiniowane w modelu POM. Dlatego choć aplikację
można skompilować, w czasie jej wykonywania wystąpi wyjątek NotFoundException.
Rozwiążmy ten problem.
PROBLEM
Zarządzamy androidowymi projektami za pośrednictwem Mavena, jednak nie
chcemy stracić możliwości budowania ich w środowisku Eclipse. Ponieważ Maven
stosuje zupełnie odmienny system wyszukiwania zależności niż Eclipse, potrzebny
jest sposób na powiązanie obu tych narzędzi.
ROZWIĄZANIE
Do niedawna nie istniał poprawny sposób rozwiązania opisanego problemu. Można
było budować aplikacje albo za pomocą Mavena, albo przy użyciu wtyczki ADT.
Stosowanie obu tych narzędzi nie było możliwe. Użytkownicy środowiska Eclipse
do wywoływania skryptów budowania Mavena z poziomu środowiska zwykle
używali konfiguracji ustawianej po wybraniu opcji External Tools. Jednak proces
budowania w Mavenie jest dużo wolniejszy niż we wtyczce ADT, dlatego rozwią-
zanie to nie było idealne.
Społeczność dostrzegła problem i wymyśliła rozwiązanie — wtyczkę m2eclipse-
-android-integration. Wtyczka ta nie dodaje żadnych nowych (widocznych)
funkcji do środowiska Eclipse. Ma jedynie wykonywać wspomniane wcześniej
zadanie — łączyć wtyczki ADT i m2eclipse, tak aby ta pierwsza w trakcie budo-
0 TECHNIKA 83. Wtyczka Mavena dla środowiska Eclipse 609

wania plików APK Androida uwzględniała zależności zarządzane przez drugą.


W rozwiązaniu występują cztery różne wtyczki środowiska Eclipse (JDT Javy,
ADT Androida, m2eclipse Mavena i integrująca m2eclipse-android-integration).
Interakcję między nimi pokazano na rysunku 14.7.

Rysunek 14.7.
Aby wtyczka ADT
rozpoznawała
zależności
obsługiwane przez
Maven, potrzebna
jest wtyczka
m2eclipse-android-
-integration.
Jest ona łącznikiem
(zwłaszcza
w miejscu
oznaczonym
wykrzyknikiem),
który scala pozostałe
trzy wtyczki

Kiedy znasz już problem, jego rozwiązanie jest proste. Najpierw trzeba zainsta-
lować wtyczki m2eclipse i m2eclipse-android-integration, co opisano w ramce
„Instalowanie wtyczek”.

Instalowanie wtyczek
Zakładamy, że wiesz, jak instalować wtyczki w środowisku Eclipse. Jeśli nie jesteś
pewien, jak to zrobić, instrukcje znajdziesz na stronie http://mng.bz/o3c3.
m2eclipse. Ta wtyczka nie jest związana z Androidem. Rozwija ją firma Sona-
type, pracująca też nad Mavenem. Adres URL strony z aktualizacjami wtyczki to:
http://m2eclipse.sonatype.org/sites/m2e.
m2eclipse-android-integration. Nad tą wtyczką pracuje społeczność skupiona
wokół Androida. Adres URL strony z aktualizacjami wtyczki to: http://mng.bz/bdMQ.

Zainstalowanie wtyczki m2eclipse zapewnia w środowisku Eclipse pełną obsługę


projektów zarządzanych za pomocą Mavena. Możesz kliknąć prawym przyci-
skiem myszy dowolny standardowy projekt Androida z modelem POM i wybrać
opcję Maven/Enable Dependency Management. Powinien pojawić się nowy kon-
tener zależności, Maven Dependencies (w przykładowym projekcie HelloMaven
kontener ten jest już gotowy). Aby się upewnić, że wszystkie ustawienia z modelu
POM są odzwierciedlone w projekcie Eclipse, można też wybrać opcję
Maven/Update Project Configuration. Opcja ta jest przydatna także po zmodyfi-
kowaniu ustawień projektu Eclipse, jeśli chcesz przywrócić konfigurację z modelu
POM. Aby przetestować aplikację, wybierz opcję Run As/Android application.
Teraz wszystko powinno działać idealnie!
610 ROZDZIAŁ 14. Zarządzanie budowaniem

OMÓWIENIE
Wtyczka m2eclipse dodaje do środowiska roboczego wiele przydatnych funkcji.
Nie tylko automatycznie wczytuje zależności zdefiniowane w modelu POM
(wszelkie dane wyjściowe Mavena można śledzić w konsoli tego narzędzia), ale
pozwala też edytować je w graficznym interfejsie użytkownika. Jeśli klikniesz
dwukrotnie plik pom.xml, pojawi się edytor modelu POM. W widoku Dependency
Hierarchy edytora można zapoznać się z powiązaniami między zależnościami
projektu (rysunek 14.8).

Rysunek 14.8.
Wtyczka m2eclipse
dodaje rozbudowany
edytor modelu POM
do środowiska
roboczego w IDE
Eclipse. W edytorze
można przejrzeć
zależności projektu,
a także modyfikować
model POM
za pomocą narzędzi
graficznych

Na rysunku widać, że zależności związane z plikiem Android JAR z repozytorium


Maven Central są wyświetlane szarym kolorem. Wynika to z określenia, że plik
ten jest udostępniany przez platformę uruchomieniową. Z tego powodu nie jest
on kompilowany wraz z aplikacją. Omówiliśmy już biblioteki Androida i repozy-
torium Maven Central; do wyjaśnienia pozostaje jeszcze jeden szczegół. Jeśli
chcesz korzystać z dostępnej z Androidem biblioteki z rozszerzeniem Google
Maps (określonej jako Google APIs, a nie jako Android), na przykład z uwagi na
zastosowanie w aplikacji widoku MapView, mamy złą wiadomość. Plik biblioteczny
maps.jar, w odróżnieniu od pliku android.jar, nie jest dostępny w repozytorium
Maven Central. Plik maps.jar obejmuje zastrzeżony kod firmy Google, dlatego nie
jest dostępny jako oprogramowanie o otwartym dostępie do kodu źródłowego.
W repozytorium Maven Central mogą znajdować się tylko biblioteki o otwartym
dostępie do kodu źródłowego, dlatego sytuacja jest jednoznaczna. Ponadto jeśli
chcesz kompilować aplikacje za pomocą bibliotek pobranych z repozytorium
Maven Central, musisz pamiętać o kilku kwestiach. Czy oznacza to, że stosowa-
nie Mavena to ślepa uliczka? Nie, jednak sprawa jest dość skomplikowana.

0 TECHNIKA 84. Narzędzie maven-android-sdk-deployer

Możliwość automatycznego pobierania zależności z repozytorium Maven Central


(lub dowolnego innego repozytorium internetowego) to jedna z najatrakcyjniej-
szych cech Mavena. Ponieważ Android jest projektem o otwartym dostępie do
kodu źródłowego, społeczność skupiona wokół Mavena chciała szybko opubli-
kować Android w repozytorium Maven Central. Jednak proces publikacji Androida
0 TECHNIKA 84. Narzędzie maven-android-sdk-deployer 611

jest skomplikowany. Wymaga rozwiązania dwóch poważnych problemów, bezpo-


średnio dotykających programistów.
Otóż nie wszystkie części Androida są oprogramowaniem o otwartym dostępie
do kodu źródłowego. Najlepszym przykładem jest kod związany z usługą Google
Maps. Należy on do firmy Google i nie można go swobodnie rozpowszechniać.
Oznacza to, że kod ten trzeba przechowywać i udostępniać niezależnie od pod-
stawowych klas frameworku Androida. Dlatego też w ustawieniach projektu
można wybrać jedną z dwóch odmian docelowych wersji Androida. Na przykład
na rysunku 14.9 w kolumnie Vendor (czyli producent) w oknie Project Build
Target dla wersji Android 2.2 wartość to Android Open Source Project, a dla
wersji Google APIs 2.2 wartością jest Google Inc.

Rysunek 14.9. Zawsze można wybrać jedną z dwóch wersji Androida. Jedna to
bezpłatna, otwarta, podstawowa platforma, pozbawiona zastrzeżonego kodu;
druga to kompletna dystrybucja, obejmująca biblioteki z zastrzeżonym kodem
(na przykład bibliotekę Google Maps)

Jest tak, ponieważ wersja Google APIs obejmuje nie tylko bezpłatne i otwarte
podstawowe klasy frameworku, ale też zastrzeżone rozszerzenia, na przykład
związane z usługą Google Maps. Stanowi to poważny problem dla społeczności
skupionej wokół Mavena, ponieważ w repozytorium Central Maven nie można
umieszczać tego rodzaju rozszerzeń, a są one potrzebne w każdej aplikacji na
Android wykorzystującej położenie i mapy.
Ponadto klasy frameworku Androida powodują dołączanie do pliku android.jar
innych bibliotek o otwartym dostępie do kodu źródłowego, takich jak Apache
Commons HttpClient, i wzorcowej implementacji JSON-a z serwisu json.org
(zobacz rozdział 9.). Ponieważ biblioteki te są artefaktami w repozytorium Maven
Central, marnotrawstwem jest umieszczanie ich w innych artefaktach (tu takim
artefaktem jest kod Androida). Dlatego społeczność skupiona wokół Mavena
usunęła niezależne biblioteki ze standardowego pliku android.jar i zamiast tego
dodała deklaracje zależności w postaci bibliotek. Można się o tym przekonać
612 ROZDZIAŁ 14. Zarządzanie budowaniem

w przeglądarce zależności Mavena dostępnej w środowisku Eclipse (rysunek 14.8).


Dlaczego zastosowane rozwiązanie stanowi problem? Ponieważ czasem wersje
bibliotek dołączone do Androida nie są dokładnie takie same jak biblioteki z repo-
zytorium Maven Central. Google może swobodnie wprowadzać zmiany w tych
bibliotekach przed dołączeniem ich do Androida lub stosować wersje niedostępne
we wspomnianym repozytorium. Dotyczy to na przykład biblioteki JSON-a. Wersja
z repozytorium Maven Central obejmuje inną implementację klasy JsonStringer
niż wersja z pliku android.jar. Jeśli zatem programista stosuje tę klasę, w czasie
budowania aplikacji z wykorzystaniem pliku android.jar z repozytorium może
wystąpić błąd kompilacji. Potrzebne jest rozwiązanie tego rodzaju problemów.
PROBLEM
Piszemy aplikację określającą położenie i wykorzystujemy w niej usługę Google
Maps (zobacz rozdział 10.). Wymaga to zadeklarowania w Mavenie zależności
w postaci zastrzeżonej biblioteki Google Maps. Możliwe też, że nie chcesz uży-
wać androidowych plików JAR z repozytorium Maven Central, ponieważ nie
odpowiadają one dokładnie bibliotekom z urządzenia.
ROZWIĄZANIE
Wygląda na to, że artefakty z repozytorium Maven Central są odpowiednie do
wykonywania większości zadań związanych z podstawowymi klasami frameworku,
ale jeśli chcemy wyświetlać widok MapView (jest on potrzebny w niemal każdej
aplikacji na Android określającej położenie), mamy problem. Dobra wiadomość
jest taka, że choć nie można umieścić pliku maps.jar w internetowym repozytorium
Mavena, dozwolone jest instalowanie go w repozytorium lokalnym. Pozwala to
wykorzystać go w procesie budowania zarządzanym za pomocą Mavena i popraw-
nie przeprowadzić kompilację. Rozwiązuje to oba problemy jednocześnie, ponie-
waż można całkowicie pominąć repozytorium Maven Central. Jak jednak osią-
gnąć pożądany efekt?
Wtyczka Mavena do obsługi instalacji obejmuje cel install-file, pozwalający
opublikować dowolny plik JAR w lokalnym repozytorium Mavena bez koniecz-
ności pisania modelu POM. Trzeba dodać kilka niezbędnych w każdym arte-
fakcie parametrów (groupId, artifactId i version), a Maven przekształci plik
w kompletny artefakt przez wygenerowanie „na żywo” pliku POM. Wersję 2.2
zastrzeżonej biblioteki maps.jar Androida można zainstalować w lokalnym repo-
zytorium w następujący sposób:
$mvn install:install-file -Dfile=/path/to/maps.jar -DgroupId=android
-DartifactId=maps -Dversion=2.2 -Dpackaging=jar

Następnie można zadeklarować w modelu POM zależność w postaci biblioteki:


<dependency>
<groupId>android</groupId>
<artifactId>maps</artifactId>
0 TECHNIKA 84. Narzędzie maven-android-sdk-deployer 613

<version>2.2</version>
<scope>provided</scope>
</dependency>

Rozwiązanie to funkcjonuje poprawnie, jednak zarządzanie nim może być kłopo-


tliwe. Wraz z pojawieniem się każdej nowej wersji platformy trzeba aktualizować
wszystkie maszyny używane przy programowaniu (komputery zespołu, serwery
budowania itd.) przez instalowanie nowych artefaktów w lokalnych repozytoriach
Mavena. Sytuacja jest jeszcze bardziej skomplikowana, jeśli serwer budowania
przeprowadza budowanie macierzowe (ang. matrix build), w którym aplikacja jest
testowana względem kilku różnych wersji i konfiguracji platformy. Na szczęście
wspaniała społeczność skupiona wokół Mavena i Androida rozwiązała także ten
problem, udostępniając narzędzie maven-android-sdk-deployer. Jest to instalator
dla Mavena, umożliwiający wygodne przekształcenie jednego lub kilku plików JAR
Androida w artefakty Mavena, które można wykorzystać lokalnie w komputerze.
SKĄD POBRAĆ NARZĘDZIE? Omawiany projekt dostępny jest w serwi-
sie GitHub na stronie http://mng.bz/SlaC.
Jeśli korzystasz z systemu kontroli wersji Git, wywołaj instrukcję git
clone dla adresu URL publicznego repozytorium. Możesz też pobrać główną
gałąź projektu jako archiwum ZIP z użyciem opcji Downloads.
Narzędzie maven-android-sdk-deployer jest skryptem budowania Mavena. Skrypt
przeszukuje katalog z pakietem SDK Androida pod kątem dostępnych wersji
Androida, przekształca je na artefakty Mavena, a następnie instaluje w lokalnej
pamięci podręcznej Mavena. Aby technika ta przyniosła pożądany rezultat, trzeba
z zastosowaniem pakietu SDK Androida i menedżera AVD pobrać potrzebne
wersje Androida (które należy przekształcić na artefakty Mavena). Za pomocą
najnowszych wersji pakietu SDK można pobrać wszystkie wersje Androida.
W tym celu należy wywołać z poziomu wiersza poleceń instrukcję android update
sdk --no-ui (upewnij się, że katalog z narzędziami pakietu SDK jest zapisany
w zmiennej PATH). Czasem trzeba wywołać to polecenie kilkukrotnie. Po pobraniu
wszystkich plików narzędzie wyświetla komunikat: „There is nothing to install
or update”.
Zmienną środowiskową ANDROID_HOME trzeba ustawić na katalog główny pakietu
SDK. To już wszystkie potrzebne przygotowania. Jeśli chcesz przekształcić
wszystkie pobrane wersje Androida na artefakty Mavena, przejdź do katalogu
z narzędziem maven-android-sdk-deployer i wprowadź następującą instrukcję:
$ mvn install

Pojawią się długie dane wyjściowe z Mavena, kończące się komunikatem BUILD
SUCCESSFUL. Niepowodzenie zwykle wynika z tego, że programista nie pobrał
wszystkich obrazów platformy, które narzędzie próbuje zainstalować. Jeśli chcesz
zainstalować konkretną wersję platformy, możesz to zrobić za pomocą opcji –P:
614 ROZDZIAŁ 14. Zarządzanie budowaniem

$ mvn install -P 2.3

Narzędzie instaluje wtedy tylko wersję Android 2.3 (Gingerbread). Oto dane
wyjściowe z tej operacji:
[INFO] ------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------
[INFO] Maven Android SDK Deployer ................ SUCCESS [1.642s]
[INFO] Android Platforms ......................... SUCCESS [0.008s]
[INFO] Android Platform 2.3 API 9 ................ SUCCESS [0.264s]
[INFO] Android Add-Ons ........................... SUCCESS [0.007s]
[INFO] Android Add-On Google Platform 2.3 API 9 ..............
SUCCESS [0.022s]
[INFO] ------------------------------------------------------------
[INFO] ------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Sun Jan 30 12:59:14 CET 2011
[INFO] Final Memory: 16M/81M
[INFO] ------------------------------------------------------------

Warto zauważyć, że narzędzie maven-android-sdk-deployer instaluje podstawowy


plik JAR Androida i plik JAR dodatku Maps za pomocą dwóch różnych identy-
fikatorów grup (parametrów groupId). Dla dodatku Maps ten parametr to
com.google.android.maps, natomiast dla pliku android.jar parametr ma wartość nie
com.google.android (jak dla artefaktu w repozytorium Maven Central), lecz android.
Aby przykład był bardziej praktyczny, przedstawiamy nieco zmodyfikowaną
wersję aplikacji HelloMaven (HelloMavenWithMaps), która wyświetla widok
MapView i deklaruje w modelu POM zależność w postaci pliku JAR dodatku
Maps. Ponadto nie używamy tu pliku JAR Androida z repozytorium Central
Maven, ale odpowiednika tego pliku, zainstalowanego przez narzędzie maven-
-android-sdk-deployer.
POBIERZ PROJEKT HELLOMAVENWITHMAPS. Kod źródłowy pro-
jektu do uruchamiania aplikacji znajdziesz w witrynie z kodem do książki
Android w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł
skoncentrować się na konkretnych zagadnieniach, zalecamy pobranie
kompletnego kodu źródłowego i śledzenie go w Eclipse (lub innym środo-
wisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/1Wnt.
Ponieważ zmiany związane z widokiem MapView są niewielkie, pomijamy tu
szczegóły. Korzystanie z takich widoków omówiliśmy w rozdziale 10. Tu ważne są
zmiany w pliku modelu POM. Na listingu 14.6 przedstawiono nowe bloki zależ-
ności w postaci zainstalowanych artefaktów związanych z Androidem.
14.3. Serwery budowania i ciągłe budowanie 615

Listing 14.6. Zaktualizowany model POM z zależnościami w postaci artefaktów


zainstalowanych za pomocą narzędzia maven-android-sdk-deployer

...
<dependencies>
<dependency>
<groupId>android</groupId>
<artifactId>android</artifactId>
<version>2.2_r2</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.google.android.maps</groupId>
<artifactId>maps</artifactId>
<version>8_r2</version>
<scope>provided</scope>
</dependency>

</dependencies>
...

Choć korzystamy tu z zastrzeżonego pliku JAR dodatku Maps, Maven nie zgłasza
zastrzeżeń, że nie może znaleźć artefaktu maps.
OMÓWIENIE
Technika ta jest krótka, ale niezwykle ważna, jeśli trzeba stosować pliki JAR
Mavena i Androida. Opisane podejście daje duże możliwości przy stosowaniu ser-
werów z repozytoriami do zarządzania pracą zespołową, takich jak Nexus firmy
Sonatype, na których narzędzie maven-android-sdk-deployer może automatycz-
nie instalować artefakty.
Choć w poprzednich technikach tylko pobieżnie opisaliśmy Maven, wiesz
już wszystko, co jest potrzebne do stosowania tego narzędzia dla Androida
i wykorzystania wszystkich zalet Mavena, takich jak zaawansowane zarządzanie
zależnościami i obsługa wtyczek. Udało Ci się dotrwać do tego miejsca, choć
opisywaliśmy narzędzia uruchamiane z wiersza poleceń i przedstawialiśmy wpisy
z długich dzienników budowania. Pora na powrót do narzędzi graficznych! Poka-
zaliśmy już, jak uzyskać dość wysoki poziom automatyzacji za pomocą narzędzi
w rodzaju Anta i Mavena. Jesteśmy jednak wymagający, dlatego oczekujemy
jeszcze większej automatyzacji. W końcu opisany proces i tak wymaga ręcznie
wykonywanych operacji, a konkretnie — uruchomienia systemu budowania.
Czy nie lepiej byłoby korzystać z innego narzędzia, które automatycznie wykonuje
potrzebne zadania? W ten sposób dochodzimy do serwerów budowania.

14.3. Serwery budowania i ciągłe budowanie


Choć możliwości i elastyczność systemów budowania w rodzaju Mavena są
wartościowe same w sobie, systemy te mają też inną zaletę, o której na razie tylko
wspomnieliśmy. We wprowadzeniu stwierdziliśmy, że budowanie i testowanie
616 ROZDZIAŁ 14. Zarządzanie budowaniem

nowego pliku APK po każdym przesłaniu kodu wymaga dwóch rzeczy — narzędzi
do budowania i mechanizmu do ich uruchamiania. Narzędzia omówiliśmy już
dość dokładnie, jednak do tej pory uruchamialiśmy je ręcznie. Brakuje elementu,
który podłącza się do systemu kontroli wersji i ustala, kiedy należy uruchomić
budowanie. Tym elementem są serwery budowania. Pokazujemy tu, jak używać
sprawdzonego narzędzia tego rodzaju.
Korzystanie z serwera budowania ma niezliczone zalety. Przede wszystkim
można w całkowicie automatyczny sposób zbudować aplikację i uruchomić zestaw
testów. Przy dobrym pokryciu kodu testami ich pozytywny przebieg gwarantuje,
że aplikacja działa prawidłowo. Serwer budowania może udostępniać do pobra-
nia wszystkie wygenerowane pliki; zespół ma wtedy miejsce, z którego zawsze
może pobrać najnowszą wersję rozwiązania. Ponadto serwer budowania generuje
i archiwizuje różne przydatne dane wyjściowe. Przez interfejs serwera budowania
można pobrać raporty na temat testów, diagramy i podsumowania dotyczące
udanych oraz nieudanych przebiegów budowania, informacje o ilości czasu
potrzebnego na przeprowadzenie jednego testu lub całego procesu budowania,
pliki dzienników modyfikacji i odnośniki pozwalające przesyłać pliki do systemu
kontroli kodu źródłowego. Na rysunku 14.10 pokazano ekran serwera budowania,
którego używamy w firmie Qype. Widoczny jest tu harmonogram procesu budo-
wania opracowanego dla aplikacji na Android.

Rysunek 14.10. Serwer budowania nie tylko automatycznie buduje aplikacje,


ale też udostępnia różne przydatne informacje, na przykład czas procesu budowania,
podsumowanie przebiegu testów i odnośniki do systemu zarządzania kodem
źródłowym

Jedną z największych zalet serwerów budowania jest możliwość powiadamiania


przez nie o nieudanym procesie budowania. Serwer budowania wykrywa zmiany
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona 617

w systemie zarządzania kodem źródłowym i potrafi powiązać przesłane ostatnio


pliki z niepowodzeniem. Dlatego jeśli ktoś wprowadził zmianę, która skutkuje
niepowodzeniem testu, serwer budowania może przesłać do wszystkich człon-
ków zespołu projektowego e-mail ze stosowną informacją. Pozwala to na łatwe
wykrywanie problemów i zachęca do ciągłej integracji. Podejście to polega na
częstym przesyłaniu i integrowaniu zmian we fragmentach oprogramowania
(nawet po kilkadziesiąt razy dziennie), dzięki czemu cykl zbierania informacji
zwrotnych jest krótki i bezpośredni, a prawdopodobieństwo powstania rozbież-
nych drzew z kodem źródłowym — niemal wyeliminowane.
W ostatnim fragmencie rozdziału pokazujemy, jak w pełni zautomatyzować
budowanie aplikacji na Android, używając popularnego serwera budowania
Hudson. Hudson zapewnia doskonałą obsługę budowania aplikacji na Android
(poprzez zaawansowaną architekturę opartą na wtyczkach), jest bezpłatny i dostępny
jako oprogramowanie o otwartym dostępie do kodu źródłowego. Omawiany ser-
wer jest niezależny od platformy i niezwykle łatwy do skonfigurowania, dlatego
dobrze sprawdzi się w niemal każdym środowisku. W tym podrozdziale w mniej-
szym stopniu koncentrujemy się na samym Hudsonie, a w większym — na jego
zastosowaniach w kontekście Androida. W tym celu omawiamy dwie techniki
budowania aplikacji na Android za pomocą Hudsona. W technice 85. pokazu-
jemy, jak skonfigurować wtyczkę Android Emulator opisywanego systemu i zasto-
sować ją w praktyce. W technice 86. rozwijamy ten temat i przedstawiamy budo-
wanie macierzowe — zaawansowany sposób budowania i testowania aplikacji na
Android, w którym kombinacje różnych konfiguracji platform są w pełni automa-
tycznie przekształcane na wersje aplikacji budowane przez Hudsona.

0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona

Hudsona można traktować jak „sympatycznego służącego”. Hudson to jeden


z najpopularniejszych obecnie serwerów budowania. Wynika to z kilku przy-
czyn — narzędzie to jest bezpłatne i dostępne jako oprogramowanie o otwartym
dostępie do kodu źródłowego, można je rozszerzać w dużym zakresie, a także
jest łatwe w użyciu i konfiguracji. Jeśli obawiasz się, że będziemy zamęczać Cię
wieloma stronami instrukcji dotyczących instalacji, nie obawiaj się — nie mamy
takiego zamiaru, ponieważ jest to niepotrzebne. Hudson po uruchomieniu dosko-
nale radzi sobie z pracą. Potrafi aktualizować się w reakcji na kliknięcie przycisku,
a także instalować nowe wtyczki wzbogacające dostępne funkcje. Co ciekawe,
Hudson nie jest powiązany z żadnym konkretnym systemem budowania, sys-
temem zarządzania kodem źródłowym, a nawet systemem operacyjnym. Potrafi
budować oprogramowanie na podstawie wskazanego kodu źródłowego na danym
komputerze. Programista musi określić, co Hudson ma zbudować, gdzie może
znaleźć kod i jak ma przeprowadzić proces budowania. Kiedy trzeba zastosować
konkretną transakcję, na przykład system zarządzania kodem źródłowym Git lub
system budowania Maven, można wykorzystać jedną z wielu wtyczek Hudsona.
618 ROZDZIAŁ 14. Zarządzanie budowaniem

HUDSON CZY JENKINS? Niestety, w trakcie powstawania tej książki


nastąpił podział projektu. Firma Oracle zarejestrowała nazwę „Hudson”
jako markę handlową i zgłosiła zastrzeżenia do sposobu przeniesienia kodu
źródłowego z serwisu java.net do witryny GitHub bez zgody społeczności
skupionej wokół Hudsona. Wywołało to obawy, że Oracle może wykorzy-
stać swoją pozycję do zablokowania lub spowolnienia realizacji przyszłych
decyzji. W efekcie grupa autorów dużych fragmentów kodu zdecydowała
się utworzyć gałąź projektu i nazwać nową wersję Jenkins. Wygląda na to,
że większość autorów kodu zaangażowała się w prace nad nowym projek-
tem. Obecnie obie aplikacje są niemal identyczne, jednak w przyszłości
mogą pojawić się różnice. Drobne rozbieżności nie powinny mieć żad-
nego wpływu na przydatność prezentowanego tu materiału, dlatego możesz
używać zarówno Jenkinsa, jak i Hudsona.
Choć Hudson nie jest powiązany z żadnymi konkretnymi narzędziami, w tej
i następnej technice zakładamy, że korzystasz z Subversiona (SVN) i Anta. Sto-
sujemy tu SVN, ponieważ przy jego użyciu zarządzamy kodem źródłowym do tej
książki, a Hudson ma wbudowaną obsługę połączeń z repozytoriami SVN. Ant
wybraliśmy zamiast Mavena, ponieważ pozwala to uprościć pracę w tej konkret-
nej technice. Chcemy uwzględnić w procesie budowania projekt testowy. Maven
wymagałby utworzenia projektu wielomodułowego, czego nie omawiamy w tej
książce. Zauważ, że użycie Anta w żadnym stopniu nie zmniejsza przydatności
ani znaczenia tej techniki. Dla Hudsona zastosowany system budowania nie ma
żadnego znaczenia. Moglibyśmy użyć nawet prostego skryptu powłoki.
BUDOWANIE ZA POMOCĄ ANTA I WYNIKI TESTÓW. Korzystanie
z zadań Anta z Androida do przeprowadzania testów w Hudsonie związane
jest z poważnym ograniczeniem — Hudson uznaje proces budowania za
udany nawet wtedy, gdy testy kończą się niepowodzeniem. Jest tak, ponie-
waż zadania Anta z Androida wyświetlają wyniki testów w standardo-
wym wyjściu, zamiast generować poprawne raporty JUnit. Jest to wada,
którą — mamy nadzieję — firma Google szybko wyeliminuje. W innych
narzędziach do budowania aplikacji na Android (na przykład we wtyczkach
maven-android-plugin i gradle-android-plugin) problem ten nie występuje.
Tu koncentrujemy się na wtyczce Android Emulator Hudsona. Ponieważ prze-
prowadzanie testów ma być integralnym etapem w procesie budowania, stosu-
jemy prosty projekt HelloAntTest z jednym testem jednostkowym aktywności
z projektu HelloAnt, z techniki 74. Test sprawdza, czy istnieje widok tekstowy
hello.
public class SampleTestCase extends ActivityUnitTestCase<HelloAnt> {

public SampleTestCase() {
super(HelloAnt.class);
}
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona 619

public void testHelloViewExists() {


startActivity(new Intent(), null, null);
assertNotNull(getActivity().findViewById(R.id.hello));
}
}

W kodzie tym nie ma nic nowego, dlatego pomijamy dalsze szczegóły. Jeśli
jednak chcesz przyjrzeć się kodowi źródłowemu projektu, poniżej wyjaśniamy,
gdzie go znaleźć.
POBIERZ PROJEKT HELLOANTTEST. Kod źródłowy projektu do uru-
chamiania aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skoncentrować
się na konkretnych zagadnieniach, zalecamy pobranie kompletnego kodu
źródłowego i śledzenie go w Eclipse (lub innym środowisku IDE albo
edytorze tekstu).
Kod źródłowy: http://mng.bz/T10e.
Łatwo można zrozumieć, w jaki sposób serwer budowania buduje aplikację
HelloAnt — pobiera najnowszą wersję kodu źródłowego i wywołuje antowy
skrypt budowania. Mniej oczywiste jest, w jaki sposób wykonuje testy. W końcu
do ich przeprowadzenia trzeba użyć emulatora lub urządzenia! W tym momencie
przydaje się wtyczka Android Emulator Hudsona.
PROBLEM
Chcemy zautomatyzować proces budowania za pomocą serwera budowania.
Potrzebujemy też wygodnego sposobu na uruchamianie i zamykanie emulatora na
czas budowania, co pozwala przeprowadzić wszystkie testy z instrumentacją.
ROZWIĄZANIE
Zanim przejdziemy dalej, należy wspomnieć, że istnieją różne sposoby na uru-
chomienie emulatora na potrzeby procesu budowania. Na przykład można raz
uruchomić emulator ręcznie i wykonywać na nim wszystkie przebiegi budowania.
Wiąże się to jednak z dwoma problemami. Po pierwsze, chcemy mieć pewność,
że poszczególne przebiegi budowania nie będą miały na siebie wpływu. Jeśli
jeden przebieg zapisze w czasie testów plik ze współużytkowanymi ustawie-
niami, w kolejnych przebiegach początkowy stan będzie inny niż we wcześniej-
szych. Można temu zapobiec przez uruchamianie dla każdego przebiegu budo-
wania nowego emulatora za pomocą nowej maszyny AVD. Po drugie, trzeba
zagwarantować, że emulator zawsze jest dostępny. Ponieważ emulator Androida
nie jest najbardziej stabilnym oprogramowaniem na świecie, trzeba zastosować
program monit (http://mmonit.com/monit/) lub podobne narzędzie do automa-
tycznego wznawiania pracy emulatora po awariach.
Innym (opisanym już) narzędziem do zarządzania emulatorami jest wtyczka
maven-android-plugin. Udostępnia ona cele emulator-start i emulator-stop.
620 ROZDZIAŁ 14. Zarządzanie budowaniem

Wtyczka ta ma jednak pewną wadę, ponieważ wymaga ustalenia stałego limitu


czasu oczekiwania na rozruch emulatora. Wtyczka Hudsona jest „sprytniejsza”.
Oczekuje na zdarzenie zakończenia rozruchu, co pozwala ustalić, kiedy emulator
jest gotowy do przyjmowania instrukcji. Ponadto w Hudsonie używany system
budowania nie ma znaczenia, dlatego możesz zastosować Ant lub Maven.
W tej technice pokazujemy, jak skonfigurować Hudsona, aby przeprowadzał
proces budowania i automatycznie wykonywał w nim następujące operacje:
1. kompilowanie i pakowanie aplikacji HelloAnt;
2. uruchamianie egzemplarza emulatora dla każdego przebiegu budowania;
3. przeprowadzanie w tym emulatorze testów z projektu HelloAntTest;
4. tworzenie podsumowania informacji o przebiegu budowania
i archiwizowanie ich (rysunek 14.11).

Rysunek 14.11. Niebieska „kulka” oznacza udane budowanie. Jeśli Ci to


nie odpowiada i nie jesteś daltonistą, możesz zmienić kolor „kulki” na zielony
przez zainstalowanie wtyczki Green Balls. Wiele osób wykonuje tę operację
jako pierwszą po zainstalowaniu Hudsona

Jeśli jeszcze nie pobrałeś i nie zainstalowałeś Hudsona, zrób to teraz. Jeżeli tylko
eksperymentujesz, pobierz plik hudson.war z witryny http://www.hudson-ci.org
i zacznij z niego korzystać:
$java –jar hudson.war

To powoduje uruchomienie Hudsona. Możesz przejść do niego w wybranej prze-


glądarce przez wpisanie adresu localhost:8080. To naprawdę jest aż tak proste!
Następnie należy zainstalować wtyczkę emulatora Androida. Hudson jest cał-
kowicie niezależny, dlatego wtyczki trzeba instalować z poziomu tego narzędzia.
Wybierz opcję Hudson/Manage Hudson/Manage Plugins/Available, znajdź i zaznacz
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona 621

wtyczkę Android Emulator Plugin (dostępną w kategorii Build Wrappers),


a następnie kliknij przycisk Install. Lista zainstalowanych wtyczek powinna
wyglądać podobnie jak na rysunku 14.12. Aby zobaczyć zmiany, trzeba ponownie
uruchomić Hudsona.

Rysunek 14.12. Hudson jest całkowicie niezależny. Można skonfigurować


w nim wszystkie ustawienia, w tym instalować nowe wtyczki. Warto zauważyć,
że system automatycznie analizuje i instaluje zależności

Teraz można utworzyć nową konfigurację procesu budowania. W Hudsonie takie


konfiguracje są nazywane zadaniami (ang. job), wybierz więc opcję Hudson/New
Job. Zadanie nazwij HelloAnt (tak jak projekt) i wybierz konfigurację Free-style
Software Project. Jest to najprostsze i dające najwięcej możliwości rozwiązanie.
Choć ta konfiguracja pozwala zbudować dowolny projekt, w tym oparty na
Mavenie, dla Mavena warto wybrać konfigurację Maven2/3, ponieważ jest wygod-
niejsza przy korzystaniu z tego systemu (choć wymaga zainstalowania wtyczki
Mavena). Na rysunku 14.13 przedstawiono ekran z konfiguracją zadania.
Przejdź do następnego ekranu — z konfiguracją procesu budowania. Hudsona
trzeba poinformować przynajmniej o trzech aspektach — gdzie ma znaleźć kod
źródłowy projektu, jak uruchomić emulator i jak przeprowadzić proces budowania.
Najpierw trzeba skonfigurować system zarządzania kodem źródłowym używany
do pobierania takiego kodu. Hudson przed uruchomieniem procesu budowania
zawsze pobiera najnowszą wersję rozwiązania z repozytorium kodu źródłowego,
aby uwzględnić najnowsze zmiany. Wspomnieliśmy już, że tu do kontroli wersji
kodu źródłowego używamy systemu SVN. Trzeba przynajmniej podać ścieżkę
do projektu w repozytorium systemu SVN i nazwę katalogu względem katalogu
roboczego zadania. Ponieważ budujemy dwa projekty (z aplikacją i z testami),
musimy podać dwa odrębne katalogi. Przedstawiono to na rysunku 14.14.
622 ROZDZIAŁ 14. Zarządzanie budowaniem

Rysunek 14.13. Hudson obsługuje różne konfiguracje zadań. Wybierz opcję


Free-style configuration, jeśli potrzebujesz największej swobody i nie chcesz
korzystać z dodatkowych funkcji wymaganych dla konkretnych systemów
budowania

Rysunek 14.14. Katalog z kodem źródłowym projektu należy określić w polu


Source Code Management. W tym przykładzie stosujemy repozytorium systemu
SVN z kodem powiązanym z książką

W drugim kroku należy nakazać Hudsonowi uruchomienie emulatora Androida,


aby po utworzeniu aplikacji można było przeprowadzić testy. Ponieważ zainsta-
lowaliśmy androidową wtyczkę, w menu Build Environment znajduje się nowa
opcja — Run an Android Emulator During Build. Po jej wybraniu dostępne stają
się dwie kolejne opcje. Run Existing Emulator umożliwia wskazanie wirtualnego
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona 623

urządzenia z Androidem (jeśli zostało wcześniej utworzone), natomiast opcja Run


Emulator with Properties służy do generowania „w locie” takiego urządzenia
przez wtyczkę. Ta druga opcja jest wygodna, dlatego nakazujemy narzędziu
uruchomienie emulatora dla wersji 2.2 z domyślnymi wymiarami wyświetlacza.
Zaznaczenie pola Reset Emulator State at Start-up powoduje wywołanie przez
wtyczkę emulatora z opcją –wipe-data, która prowadzi do opróżnienia partycji
użytkownika w czasie rozruchu. Możesz też usunąć zaznaczenie opcji Show Emu-
lator Window (rysunek 14.15), jeśli chcesz uruchomić serwer bezwejściowy.

Rysunek 14.15. Androidowa wtyczka zapewnia nową opcję w kategorii Build


Environment. Można tu określić konfigurację platformy używanej do uruchamiania
emulatora

W trzecim i ostatnim kroku należy skonfigurować proces budowania. Ponieważ


wybraliśmy podejście „swobodne”, Hudson pozwala określić jeden lub kilka eta-
pów z wywołaniami skryptów powłoki lub narzędzi do budowania (na przykład
Anta). Tu rozwiązanie jest proste. W polu Build należy dodać etap Invoke Ant build
i wskazać plik budowania projektu testowego. Co ciekawe, nie trzeba robić nic
więcej. Skrypty budowania generowane przez Android są na tyle „inteligentne”,
że wiedzą, iż testowana aplikacja jest potrzebna, dlatego trzeba ją najpierw zbu-
dować. Programista nie musi bezpośrednio tego robić. Ponadto wystarczy wywo-
łać cele clean i run-tests, ponieważ ten ostatni wymaga celów, które kompilują
cały pozostały kod i tworzą pakiety na jego podstawie (rysunek 14.16).
Kliknij przycisk Save i gotowe! Aby ręcznie uruchomić proces budowania,
kliknij odnośnik Build Now na pasku bocznym. W kontekście produkcyjnym
należy skonfigurować wyzwalacz w sekcji Build Triggers. Można na przykład
okresowo sprawdzać system zarządzania kodem źródłowym pod kątem zmian.
624 ROZDZIAŁ 14. Zarządzanie budowaniem

Rysunek 14.16. Kategoria Build służy do informowania Hudsona o tym, co powinien


zrobić. „Swobodny” proces budowania należy skonfigurować przez określenie
jednego lub kilku etapów budowania, takich jak wywołanie Anta

Przebieg budowania można śledzić za pomocą odnośników z paska bocznego.


Prowadzą one do wszystkich przebiegów budowania wyświetlanych w porządku
chronologicznym od najnowszych. Jeśli wszystko potoczyło się prawidłowo,
ostatni ekran powinien wyglądać tak jak na rysunku 14.11.
OMÓWIENIE
Nietrudno się domyślić, że stosowanie serwerów budowania (takich jak Hudson)
jest najprzydatniejsze, jeśli zespół korzysta z fizycznej maszyny do budowania, na
której działa taki serwer. Używanie Hudsona na lokalnym komputerze nie ma
większego sensu — chyba że zależy Ci na ciekawym interfejsie do zarządzania
procesem budowania i wyświetlania podsumowań na ich temat. Maszyny do
budowania działają zwykle bezwejściowo. W jaki sposób należy więc uruchomić
emulator, skoro domyślnie wymaga on okna? Wspomnieliśmy już o opcji, która
umożliwia wyłączenie okna emulatora (w bezpośrednim wywołaniu emulatora
odpowiada ona opcji –no-window). Podejście to ma jednak pewną wadę — jeśli
proces budowania kończy się niepowodzeniem, nie można zobaczyć, co się stało.
Dostępne są tylko pliki dziennika. Ponadto emulator jest niestabilny, kiedy
uruchamia się go bez okna, dlatego warto je wyświetlać. Na szczęście Hudson
udostępnia wtyczkę dla protokołu XVNC, który umożliwia uruchamianie wir-
tualnych wyświetlaczy w systemie X11. Aby to podejście zadziałało, trzeba
zainstalować zarówno wspomnianą wtyczkę, jak i serwer XVNC, na przykład
TightXVNC. W konfiguracji procesu budowania pojawia się wtedy nowa opcja,
pozwalająca uruchomić serwer XVNC w ramach tego procesu, a nawet wykony-
wać zrzuty z nieudanych przebiegów budowania.
Do tej pory w trakcie budowania uruchamialiśmy tylko jeden emulator. Jest
to odpowiednie, jeśli chcesz przeprowadzać testy w podstawowej konfiguracji,
jednak produkcyjne wersje aplikacji warto sprawdzać w różnych kombinacjach
wersji platformy i wymiarów wyświetlacza. Standardowo musielibyśmy utworzyć
0 TECHNIKA 86. Budowanie macierzowe 625

nową konfigurację procesu budowania dla każdej kombinacji parametrów Andro-


ida, dla których chcemy przeprowadzić testy, jednak — jak może już się domy-
ślasz — istnieje łatwiejsze rozwiązanie.

0 TECHNIKA 86. Budowanie macierzowe

Konfiguracja zadania przygotowana w poprzedniej technice ma poważną wadę —


jest statyczna. Bezpośrednio skonfigurowaliśmy środowisko Androida (od wersji
platformy po wymiary wyświetlacza), w którym mają być przeprowadzane testy.
Testy interfejsu użytkownika zwykle są mocno powiązane z wyświetlaczem urzą-
dzenia, a przejście testów na ekranie o określonych wymiarach nie gwarantuje
powodzenia dla ekranu o innej wielkości. Ponadto można skonfigurować wiele
innych cech środowiska, na przykład obecność lub brak karty SD, co pomaga
sprawdzić, czy aplikacje wymagające takiej karty w elegancki sposób kończą
działanie, jeśli jest ona niedostępna. Inne konfigurowalne ustawienia to połącze-
nie sieciowe, położenie geograficzne i wszelkie inne aspekty związane z kon-
kretnym egzemplarzem emulatora. Testowanie aplikacji pod kątem różnych usta-
wień może okazać się żmudne.
Sytuacja jest jeszcze gorsza, kiedy trzeba uwzględnić kombinacje właściwości
konfiguracyjnych. Aplikacja może poprawnie działać tylko dla niektórych kom-
binacji. Wyobraź sobie, że na pracę programu wpływają dwie cechy — wielkość
ekranu i język. Programista zdecydował się dodać obsługę średnich i małych
wyświetlaczy oraz języków angielskiego i hiszpańskiego. Do aplikacji dołączona
jest wyświetlana w tle grafika z tekstem w danym języku. Potrzebne są cztery
wersje takiej grafiki: z językiem angielskim dla małego ekranu, z językiem angiel-
skim dla średniego ekranu, z językiem hiszpańskim dla małego ekranu i z języ-
kiem hiszpańskim dla średniego ekranu. W efekcie powstaje macierz z kombi-
nacjami właściwości, przedstawiona w tabeli 14.3.
Tabela 14.3. Macierz 2×2 z możliwymi konfiguracjami

en_US es_ES

QVGA /res/drawable-en-small/bg.png /res/drawable-pl-small/bg.png

HVGA /res/drawable-en-normal/bg.png /res/drawable-pl-normal/bg.png

Załóżmy, że programista zapomniał o rysunku dla kombinacji QVGA i es_ES. Kod


źródłowy skompiluje się poprawnie, ponieważ jeśli istnieje przynajmniej jeden
rysunek bg.png, generowany jest atrybut R.drawable.bg. Jeśli jednak użytkownik
uruchomi aplikację w niefortunnej konfiguracji, wystąpi awaria, ponieważ
wyszukiwanie zasobów w czasie wykonywania programu zakończy się niepowo-
dzeniem.
626 ROZDZIAŁ 14. Zarządzanie budowaniem

UWAGA. Należy unikać opisanego scenariusza i zawsze stosować konfi-


gurację domyślną z domyślnymi zasobami, które Android może zastosować
w razie wystąpienia problemów. Załóżmy, że rysunek dla kombinacji en_US
i HVGA można przechowywać w katalogu /res/drawable. Wtedy hiszpańscy
użytkownicy zobaczą na małych wyświetlaczach nieprzetłumaczony tekst
i przeskalowany w dół rysunek, ale aplikacja wciąż będzie działać. Więcej
informacji o algorytmie wyszukiwania zasobów w Androidzie znajdziesz
w rozdziale 4. (w podrozdziale 4.7).
Potrzebny jest sposób na wskazanie wymaganych aspektów konfiguracji i uru-
chomienie przez serwer budowania zadań dla wszystkich kombinacji.
PROBLEM
Aplikacja obsługuje różne konfiguracje platformy, dla których może działać
w różny sposób. Chcemy automatycznie testować program dla wszystkich konfi-
guracji bez konieczności tworzenia odrębnych zadań z procesami budowania.
ROZWIĄZANIE
W wersji 1.221 Hudson obsługuje zadania dla projektów o wielu konfiguracjach.
Takie zadania wykonują budowanie macierzowe. Dla Hudsona zadania tego rodzaju
różnią się od zadań, których używaliśmy w poprzedniej technice. Dlatego nie
można zastosować konfiguracji Free-style Software Project. Jednak w praktyce
zadania obu rodzajów są bardzo podobne. Jedyna różnica polega na tym, że
w ustawieniach zadań nowego typu pojawia się nowa kategoria, Configuration
Matrix, gdzie można określić właściwości i wartości, dla których należy przepro-
wadzić budowanie.
Zacznijmy od początku. Jak już wspomnieliśmy, do wykonywania budowania
macierzowego służą zadania nowego rodzaju. Dlatego tu ponownie tworzymy
zadanie przygotowane w poprzedniej technice. Powielamy w nim prawie wszyst-
kie szczegóły konfiguracyjne (dalej zwracamy uwagę na wprowadzone zmiany).
Pierwszy krok polega na utworzeniu zadania dla budowania macierzowego, co
pokazano na rysunku 14.17.
Na następnym ekranie można swobodnie skonfigurować zadanie, tak jak
w poprzednich technikach. Kopiujemy tu z tych technik wszystkie ustawienia
związane z zarządzaniem kodem źródłowym. Nowością jest kategoria Configura-
tion Matrix. W tym miejscu można zdefiniować właściwości i zbiory ich wartości
używane w kombinacjach parametrów procesu budowania. Wybierz opcję Add
Axis dla każdej właściwości, którą chcesz zdefiniować. Tu ustawiamy język i wiel-
kość ekranu. Hudson ma budować aplikację dla każdej kombinacji małego i śred-
niego wyświetlacza oraz języków angielskiego i hiszpańskiego. Na rysunku 14.18
pokazano wygląd macierzy z konfiguracją.
0 TECHNIKA 86. Budowanie macierzowe 627

Rysunek 14.17. Aby utworzyć proces budowania macierzowego, należy wybrać


zadanie typu Build multi-configuration project. Pozwala ono swobodnie
skonfigurować projekt, przy czym dostępne są nowe ustawienia, przeznaczone
dla budowania macierzowego

Rysunek 14.18. Macierz konfiguracji z określonymi właściwościami


i przyjmowanymi przez nie wartościami. Tu właściwościami są wersja interfejsu
API (wersja platformy) i język. Wartości dla każdej właściwości należy wprowadzić
bezpośrednio i oddzielić je odstępami

Po zdefiniowaniu macierzy konfiguracji dla każdej kombinacji Hudson automa-


tycznie tworzy podzadanie w momencie uruchomienia budowania macierzowego.
„Chwileczkę”, możesz wtrącić, „skoro istnieje tylko jedna sekcja z konfiguracją
emulatora Androida, a wersję platformy i język trzeba określić bezpośrednio, to jak
działa to podejście?”. To trafne pytanie. Jak widać na rysunku 14.15, nie można
zostawić pustych opcji dotyczących emulatora Androida. Jednak w ramach pro-
cesu budowania opcje te mają się zmieniać. Rozwiązanie jest proste — należy
użyć zmiennych. Hudson udostępnia wbudowaną obsługę zmiennych, które
628 ROZDZIAŁ 14. Zarządzanie budowaniem

można stosować w konfiguracji procesu budowania jako zmienne środowisko-


we lub bezpośrednio z poziomu wtyczek. Tu każde pole Name właściwości okre-
ślonej w macierzy jest zmienną, którą można podawać w dalszych ustawieniach
budowania. Oznacza to, że nazwy właściwości można podawać jako wartości
w opcjach emulatora Androida, co pokazano na rysunku 14.19.

Rysunek 14.19. Każdą właściwość z macierzy można stosować jak zmienną


w ustawieniach budowania. W tym celu należy poprzedzić nazwę właściwości
znakiem dolara. Tu wersję interfejsu API i język urządzenia ustawiane przez
wtyczkę emulatora Androida określamy przy użyciu zmiennych odpowiadających
właściwościom z macierzy

Zauważ, że zmienne są też przekazywane do każdego automatycznie uruchamia-


nego skryptu Anta. Jeśli po skonfigurowaniu rozwiązania przyjrzysz się danym
wyjściowym z procesu budowania, zauważysz, że zmienna API_LEVEL jest udo-
stępniana z wykorzystaniem opcji –D skryptowi budowania Anta jako właściwość
systemowa Javy.
Skoro już jesteśmy przy skryptach budowania Anta, należy zrobić jeszcze
jedną rzecz. Trzeba poinformować Ant, który egzemplarz emulatora ma wykorzy-
stać do uruchomienia skryptu budowania (podobnie jak wcześniej określaliśmy
konfigurację emulatora Androida). Domyślnie narzędzie ADB wybiera działające
urządzenie. Jeśli działających emulatorów jest więcej, domyślnie występuje wtedy
błąd, dlatego trzeba określić docelowy emulator za pomocą opcji –s. Nie wywo-
łujemy tu bezpośrednio narzędzia ADB (za budowanie odpowiadają skrypty Anta),
dlatego argument trzeba przekazać do ADB w wywołaniu polecenia dla Anta.
Umożliwia to właściwość systemowa adb.device.arg. Na rysunku 14.20 pokazano
nową instrukcję dla Anta, odpowiednią dla budowania macierzowego.
ANDROID_AVD_DEVICE nie jest jedyną zmienną eksportowaną przez wtyczkę obsłu-
gującą emulator Androida. Inne dostępne zmienne to ANDROID_AVD_ADB_PORT
(zapewnia dostęp do narzędzia ADB emulatora) i ANDROID_AVD_USER_PORT (służy
do kontrolowania portów). Ta ostatnia jest przydatna zwłaszcza wtedy, kiedy
etapy budowania wymagają nawiązania połączenia telnetowego w celu przeka-
zywania do emulatora na przykład fikcyjnych współrzędnych GPS (zobacz
dodatek A).
0 TECHNIKA 86. Budowanie macierzowe 629

Rysunek 14.20. Aby wywołanie Anta zadziałało, trzeba z zastosowaniem zmiennej


wskazać konkretny emulator, na którym należy uruchomić daną konfigurację
z macierzy. Wtyczka obsługująca emulator Androida udostępnia numer emulatora
w zmiennej ANDROID_AVD_DEVICE. Można ją przekazać do wewnętrznego zadania
Anta (wykorzystującego narzędzie ADB) za pomocą właściwości adb.device.arg

Wszystko jest już gotowe. Pora zapisać ustawienia zadania i uruchomić proces
budowania. Ekran zadania wygląda teraz nieco inaczej i obejmuje macierz z konfi-
guracjami. Każda konfiguracja odpowiada procesowi budowania, do którego
można przejść przez kliknięcie go (rysunek 14.21).

Rysunek 14.21. Zadanie przeprowadzające budowanie macierzowe wyświetla


podrzędne procesy budowania w tabeli odpowiadającej macierzy konfiguracji.
Można kliknąć „kulkę”, aby przejść do konkretnego procesu budowania

OMÓWIENIE
Nie trzeba chyba wyjaśniać, że budowanie macierzowe to wartościowy sposób
na zautomatyzowanie procesu budowania. Warto jednak wiedzieć, że związany
jest z pewnymi kosztami. Równoległe uruchamianie wielu emulatorów angażuje
630 ROZDZIAŁ 14. Zarządzanie budowaniem

znaczne zasoby systemu, a proces budowania jest wtedy znacznie dłuższy. Upew-
nij się, że serwer budowania jest wystarczająco mocny, aby poradzić sobie
z budowaniem macierzowym. Gorąco zachęcamy do stosowania komputerów
wielordzeniowych z dużą ilością RAM-u. W przeciwnym razie proces budowania
może zakończyć się niepowodzeniem z uwagi na przekroczenie limitu czasu
oczekiwania przez androidową wtyczkę na rozruch emulatora.
Warto też wspomnieć o rzadkich macierzach konfiguracji. Może już wiesz,
że wzrost liczby właściwości i wartości prowadzi do szybkiego wzrostu liczby
kombinacji. Niektóre kombinacje nie wymagają przeprowadzania odrębnego
procesu budowania; czasem wykonywanie takiej operacji nie ma sensu. Rozważmy
na przykład obecność karty SD i jej brak. Jeśli obecność karty SD jest jedną
właściwością, a język drugą, to nie trzeba uruchamiać procesu budowania dla
każdej kombinacji język/obecność lub brak karty, ponieważ właściwości te są
niezależne od siebie. Dlatego można zaznaczyć opcję Combination Filter i utwo-
rzyć rzadką macierz. Następnie za pomocą składni języka Groovy (jest to odmiana
Javy) należy określić warunki logiczne. Każda kombinacja o wartości false jest
pomijana. W warunkach można określić właściwość i wartości, a także użyć spe-
cjalnej zmiennej index (jest to indeks macierzy). W omawianej sytuacji można
uruchomić budowanie dla warunku bez karty SD tylko raz, dla języka angielskiego,
tworząc następujący filtr (zakładamy, że istnieją właściwości SD_CARD i LOCALE):
SD_CARD == "false" && LOCALE == "en_US"

To już koniec omówienia Hudsona i Androida. Przedstawiliśmy tu wiele infor-


macji. Podsumujmy pokrótce, co powinieneś zapamiętać o automatyzacji budo-
wania aplikacji na Android.

14.4. Podsumowanie
Inaczej niż w poprzednich rozdziałach, tu musiałeś zapoznać się z dużą ilością
kodu konfiguracyjnego, a my nawet nie wynagrodziliśmy Ci tego atrakcyjnymi
zrzutami z aplikacji. Pisanie skryptów budowania lub konfigurowanie zadań
w Hudsonie nie jest zbyt ciekawe, mamy jednak nadzieję, że lekturę tego roz-
działu uważasz za dobrą inwestycję czasu. Zobaczmy, co pozwalają uzyskać
techniki opisane w tym rozdziale i jak osiągnąć pożądane cele.
Zaczęliśmy od krytycznej oceny stosowanego wcześniej sposobu budowania
aplikacji na Android. Pokazaliśmy, że w celu uzyskania większej kontroli nad
procesem budowania i wyższego poziomu automatyzacji musimy odejść od
czysto wizualnych środowisk budowania, takich jak Eclipse, do systemów pro-
gramowych, na przykład Anta i Mavena. Stosując taki system, możemy za pomocą
wiersza poleceń utworzyć plik APK na podstawie kodu źródłowego. Pozwala to
wykorzystać serwery budowania, na przykład Hudsona, który potrafi łączyć się
z systemami zarządzania kodem źródłowym i automatycznie uruchamiać proces
budowania, a także przeprowadzać testy w reakcji na przesłanie nowego kodu.
14.4. Podsumowanie 631

Oznacza to, że poznałeś wszystkie aspekty automatyzacji budowania aplikacji na


Android.
Dotarliśmy prawie do końca książki. Prawie! Mamy w zanadrzu jeszcze jeden
rozdział, w którym znów omawiamy coś zupełnie nowego. Jeśli śledzisz nowinki,
na pewno dużo czytałeś o tabletach. W wersji 3.0 Androida (Honeycomb) firma
Google dodała wbudowaną obsługę tabletów, w tym zmodyfikowany interfejs
użytkownika i zestaw nowych interfejsów API. W rozdziale 15. wyjaśniamy, co
wyjątkowego kryje w sobie wersja Honeycomb i czym różni się pisanie aplikacji
na tablety od tworzenia programów na zwykłe smartfony. Pokazujemy też nowe
interfejsy API, które powinieneś dobrze poznać.
632 ROZDZIAŁ 14. Zarządzanie budowaniem
Pisanie aplikacji na tablety
z Androidem

W tym rozdziale
Q Stosowanie fragmentów
Q Pasek akcji
Q Implementowanie przeciągania

Wszystko staje się coraz większe. Dlatego obecnie należy pisać programy
w bardziej wyrafinowany sposób.
Bill Budge
Był rok 2001. Microsoft, największa firma technologiczna na świecie, zaprezen-
tował przełomową wersję niezwykle popularnego systemu operacyjnego —
Windows XP Tablet PC Edition. Zdaniem Microsoftu miał to być początek ery
urządzeń dotykowych. Wiemy, jak to się skończyło. System XP Tablet PC Edition
okazał się niewypałem.
Tak naprawdę komputery z systemem XP Tablet nie były pierwszymi urzą-
dzeniami z wyświetlaczem dotykowym przeznaczonymi na rynek masowy. Dzie-
sięć lat wcześniej inżynierowie z Apple’a opracowali prototyp, który w przekształ-
conej postaci wprowadzono na rynek jako komputer Newton. Opracowane 10 lat
później przez Microsoft komputery Tablet PC były podejrzanie podobne do
Newtona. Jednak prototypowa wersja Newtona nigdy nie trafiła do sprzedaży;
komputery z tej rodziny stały się za to poprzednikami palmtopów.
Stwierdzenie, że urządzenia dotykowe przez wiele lat były ogłaszane jako
następny wielki hit, to poważne niedomówienie. Można uznać, że wcześniejsze

633
634 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

próby zakończyły się niepowodzeniem, ponieważ producenci próbowali prze-


kształcić komputery PC w tablety. Dwadzieścia lat po opracowaniu prototypo-
wego tabletu Newton stało się oczywiste, że naturalnym urządzeniem dotykowym
są smartfony i to na nich należy wzorować tablety. W ten sposób powstały iPad
Apple’a i Android dla tabletów. W wersji Android 3.0 (Honeycomb) wprowadzono
istotne zmiany, opracowane z myślą o tabletach. Co to oznacza dla programistów
aplikacji na Android?
Jeśli już udostępniasz w sklepie Android Market kilka aplikacji, być może
nasuwa Ci się pytanie: „Co powinienem zrobić, aby programy działały na table-
tach?”. Liczba użytkowników tabletów jest znacznie mniejsza niż liczba osób
korzystających z telefonów. Zyskanie popularności przez nowe urządzenia, jakimi
są tablety, wymaga czasu, a użytkowników smartfonów są miliony. Prawdopodob-
nie dużo większe zyski przyniesie inwestowanie w rozwijanie aplikacji na Android
przeznaczonych na smartfony, a nie na tablety. Jest to prawdą zwłaszcza w przy-
padku nowych aplikacji. Tworząc programy na smartfony, dotrzesz do znacznie
większej grupy odbiorców.
Istnieją jednak wyjątki od tej reguły. Niektóre rodzaje aplikacji lepiej nadają
się dla tabletów niż dla smartfonów. Na przykład czytniki wiadomości, aplikacje
z sieci społecznościowych, sklepy internetowe czy inne programy wyświetlające
bogate materiały wyglądają znacznie lepiej na większym ekranie. Na smartfonie
powiązane informacje trzeba wyświetlać na dwóch lub trzech ekranach, a na
tablecie można je zaprezentować na jednej angażującej czytelnika stronie. Nawet
w innych obszarach wczesne wprowadzanie aplikacji na tablety do sklepu Android
Market przynosi poważne korzyści. Program może zyskać popularność z uwagi
na niewielką konkurencję, co zapewnia dobrą pozycję wyjściową, kiedy więcej
osób zacznie kupować tablety i szukać ciekawych aplikacji w sklepie.
Niezależnie od tego, czy lubisz nowinki, czy nie, w pewnym momencie
zechcesz utworzyć aplikację na tablety (w przeciwnym razie prawdopodobnie nie
czytałbyś tego rozdziału). W tym rozdziale wzbogacamy aplikację DealDroid
z rozdziału 2. i tworzymy jej wersję na tablety. Gotowy produkt przedstawiono
na rysunku 15.1.
Decyzji o utworzeniu aplikacji na tablety nie podejmuje się stopniowo. Trzeba
od początku stwierdzić, że program ma działać w tabletach. Rozwijanie takich
aplikacji jest ekscytujące, ponieważ tablety w porównaniu ze smartfonami otwie-
rają przed programistami wiele nowych możliwości. Liczne typowe problemy,
na przykład mały ekran, niewielka ilość pamięci, konieczność obsługi dawnych
wersji Androida i wolna sieć, w tabletach są mniej dotkliwe. Jednak zanim
zaczniemy tworzyć fragmenty pełne widoków StackView, warto zastanowić się,
jakie elementy aplikacji są już gotowe i jak je wykorzystać.
15.1. Przygotowania do tworzenia aplikacji na tablety 635

Rysunek 15.1.
Aplikacja DealDroid
w wersji na tablety

15.1. Przygotowania do tworzenia aplikacji na tablety


Pisanie programów na tablety nie polega tylko na stosowaniu nowych interfejsów
API i większych grafik. Musisz zdecydować, czy chcesz utworzyć odrębną aplikację,
czy rozwinąć istniejący program na smartfony, tak aby działał poprawnie także
na tabletach. W tym rozdziale koncentrujemy się na pisaniu nowych aplikacji.
Pozwala to wykorzystać wszystkie możliwości Androida 3.0. Wszystkie (lub pra-
wie wszystkie) techniki z tego rozdziału można zastosować w czasie tworzenia
standardowych aplikacji.

0 TECHNIKA 87. Wykorzystywanie istniejącego kodu za pomocą


projektów bibliotek

Choć tworzymy odrębną aplikację przeznaczoną na tablety z Androidem, nie


oznacza to, że program jest zupełnie odmienny od rozwiązań tworzonych na
smartfony. Obie wersje aplikacji mają wiele wspólnych funkcji i mogą korzystać
nawet z tych samych danych. Dane te można przechowywać lokalnie w urządzeniu
lub w chmurze na serwerze. Sposób dostępu do takich danych, a nawet porząd-
kowania ich po pobraniu do pamięci lokalnej jest taki sam jak w smartfonach.
Na szczęście istnieje dobry sposób na współużytkowanie kodu przez różne apli-
kacje na Android.
PROBLEM
W nowej aplikacji na tablety zamierzamy wykorzystać kod istniejącego programu
na smartfony. Chcemy, aby istniała tylko jedna kopia kodu. Pozwala to dodawać
nowe funkcje, naprawiać błędy i wykonywać podobne zadania w jednym miejscu.
ROZWIĄZANIE
Sposobem na współużytkowanie kodu między aplikacjami na Android jest zasto-
sowanie projektów bibliotek dla Androida. Takie projekty dobrze nadają się do
porządkowania kodu i wprowadzono je w tym samym czasie co Android 2.2.
Projekty bibliotek nie są przeznaczone tylko dla tabletów i Androida 3.0 —
636 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

pozwalają współużytkować kod także między różnymi aplikacjami na smartfony.


Taki projekt jest bardzo przydatny przy tworzeniu aplikacji, która ma działać na
smartfonach i tabletach.
Wspomnieliśmy już, że projekty bibliotek wprowadzono w tym samym okre-
sie co Android 2.2. Dostosowano je jednak do jeszcze wcześniejszych wersji
Androida. Możliwe, że przechowujesz kod w takich bibliotekach i możesz
natychmiast wykorzystać je do tworzenia aplikacji na tablety na podstawie istnie-
jącego kodu programów na smartfony. Jeśli tak jest, możesz przejść bezpośrednio
do następnej techniki. Jeżeli jednak jeszcze nie stosujesz projektów bibliotek,
czeka Cię refaktoryzacja i zmiana uporządkowania kodu.
W tej technice przekształcamy aplikację DealDroid przedstawioną po raz
pierwszy w rozdziale 2. na program na tablety. Cały kod aplikacji DealDroid
znajduje się w jednym projekcie aplikacji na Android. Musimy to zmienić. Na
rysunku 15.2 pokazano nowy sposób uporządkowania kodu, pozwalający wyko-
rzystać ten kod w nowym projekcie aplikacji na tablety.
Na rysunku 15.2 widoczne są projekty DealsLib i TabletDeals. DealsLib to
projekt biblioteki dla Androida, obejmujący kod współużytkowany przez aplikacje
na smartfony i tablety. TabletDeals to projekt aplikacji na tablety. W tym roz-
dziale opisujemy go bardzo szczegółowo. Na rysunku widać, jaki kod znajduje
się w projekcie biblioteki. Umieszczono w niej (w pakiecie com.manning.aip.
´dealdroid.xml) cały kod do pobierania danych z internetu i ich przetwarzania.
Kod ten może być taki sam dla tabletów i smartfonów. Dane w formacie XML
są przetwarzane na obiekty modelu (określone w pakiecie com.manning.aip.
´dealdroid.model) używane przez aplikację. Także kod tych obiektów jest czę-
ścią biblioteki.
W pakiecie najwyższego poziomu (com.manning.aip.dealdroid) znajduje się
kilka innych klas. Najciekawszą z nich jest DealsApp. Jest to klasa typu Application
używana w obu aplikacjach. Obejmuje pamięć podręczną z danymi, a także stan
aplikacji. Stan w aplikacjach na tablety mógłby być inny, jednak tu jest taki sam,
dlatego wspomnianą klasę można współużytkować w wersjach programu na
smartfony i tablety.
Ponadto między aplikacjami współużytkowane są niektóre zasoby, przede
wszystkim pliki strings.xml i plurals.xml. Współużytkować można także inne
zasoby, na przykład obiekty graficzne.
OMÓWIENIE
Projekty bibliotek dają duże możliwości. Programiści — zwłaszcza piszący pakiety
aplikacji współużytkujących duże fragmenty kodu — dobitnie domagali się tego
mechanizmu. Często wspólny kod służy do obsługi dostępu do sieci i modelu
danych, podobnie jak w przykładzie. Nieraz wspólny jest też kod do uwierzy-
telniania użytkowników i późniejszego zarządzania informacjami o tożsamości
(na przykład znacznikami uwierzytelniającymi i (lub) określającymi uprawnienia).
0 TECHNIKA 87. Wykorzystywanie istniejącego kodu za pomocą projektów bibliotek 637

Rysunek 15.2. Kod przygotowany


do współużytkowania z aplikacją
na tablety

Tego rodzaju kod często obejmuje elementy interfejsu użytkownika, ponieważ


programiści zwykle starają się ujednolicić sposób logowania się do aplikacji.
Uzyskanie tego efektu nie jest trudne, gdyż w bibliotece można umieścić aktyw-
ności, XML-owy kod układu itd. Dla projektu biblioteki trzeba też utworzyć plik
638 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

AndroidManifest.xml. Można w nim deklarować aktywności, usługi i inne ele-


menty — tak jak w innych manifestach. Jeśli jednak chcesz wykorzystać w apli-
kacji aktywność z projektu biblioteki (lub inny komponent deklarowany w mani-
feście), musisz zadeklarować tę aktywność w pliku AndroidManifest.xml aplikacji.
Manifest projektu biblioteki pełni funkcję menu dostępnych komponentów,
które trzeba zadeklarować w pliku manifestu aplikacji.
Kod można współużytkować między projektami na różne sposoby. Jeśli korzy-
stasz ze środowiska Eclipse (albo Anta, Mavena lub innego narzędzia zarządzają-
cego zależnościami), możesz utworzyć własną bibliotekę z kolekcją kodu w Javie
i wykorzystać ją w aplikacji. Zależność między biblioteką a aplikacją może wystę-
pować na poziomie kodu źródłowego lub na poziomie binarnym. W tym drugim
przypadku najpierw należy skompilować projekt biblioteki bądź nawet spakować
go do archiwum JAR. Utrudnieniem jest wtedy tylko konieczność upewnienia się,
że w kodzie biblioteki nie występują żadne standardowe klasy Javy niedozwo-
lone w Androidzie, a także użycie odpowiedniego archiwum android.jar. Przy
stosowaniu projektów bibliotek dla Androida zadania te są wykonywane auto-
matycznie.
Inną ważną zaletą projektów bibliotek dla Androida w porównaniu ze stan-
dardowymi bibliotekami Javy jest obsługa zarządzania zasobami. W przykładzie
w bibliotece umieszczamy standardowy plik strings.xml. Pozwala to współużyt-
kować ten plik w wersjach na smartfony i tablety. Można też zastąpić konkretny
łańcuch znaków lub dodać nowe fragmenty tekstu przez umieszczenie odrębnego
pliku strings.xml w projekcie aplikacji. Kompilator scala wtedy zasoby. Dotyczy
to także innych zasobów, na przykład stylów i obiektów graficznych, a nawet
plików układu.
Teraz, kiedy znasz już dobry sposób na porządkowanie kodu i współużytko-
wanie go w aplikacjach na smartfony i tablety, można dokładniej zastanowić się nad
wersją na tablety. Wspomnieliśmy już, że tworzymy aplikację przeznaczoną
tylko na tablety. Nie zamierzamy rozwijać wersji działającej równie dobrze na
smartfonach i tabletach. Na szczęście stosowanie naszego podejścia jest proste
i przynosi duże korzyści.

0 TECHNIKA 88. Tworzenie aplikacji przeznaczonej na tablety

Programiści aplikacji na Android niechętnie mówili o zróżnicowaniu urządzeń


(ang. fragmentation). Określenie to często stosowali przeciwnicy Androida, twier-
dzący, że zbyt trudno jest tworzyć aplikacje na tę platformę, ponieważ trzeba
zapewnić obsługę urządzeń z ekranami o różnej wielkości i z innymi niejedno-
litymi cechami. Jednocześnie jest to jednak ukryta wartość Androida. Właściwy
sposób programowania wymagał, aby nie robić założeń co do wielkości i pro-
porcji ekranu. Programiści mieli wiele narzędzi do projektowania aplikacji
z układem dostosowującym się do wyświetlacza. Kiedy więc pojawiały się nowe
urządzenia z ekranami o przekątnej wynoszącej 4 lub 4,3 cala albo z mniejszymi,
0 TECHNIKA 88. Tworzenie aplikacji przeznaczonej na tablety 639

2,5-calowymi wyświetlaczami, większość programów działała w nich prawidłowo.


Nawet na pierwszych tabletach z 7-calowymi ekranami i Androidem 2.2 większość
aplikacji funkcjonowała bez problemów (choć były też wyjątki od tej reguły —
niektórzy programiści nie stosowali najlepszych praktyk i w czasie projektowania
układów robili założenia dotyczące wielkości wyświetlacza). Było to wielką zaletą
małych tabletów. Kiedy pojawiły się na rynku, od razu istniało wiele aplikacji, które
prawidłowo na nich działały.
Jednak w czasie prac nad większymi tabletami okazało się, że opracowanie
systemu operacyjnego pod kątem takich urządzeń ma duże zalety. Dlatego powstał
Android 3.0. Platforma ta obejmuje elementy sprawiające, że aplikacje mogą
działać równie dobrze zarówno na smartfonach, jak i na tabletach. Ponadto pozwala
tworzyć atrakcyjne programy przeznaczone tylko na tablety. Wymaga to jednak
zablokowania dostępu do aplikacji użytkownikom mniejszych urządzeń.
PROBLEM
Piszemy aplikację przeznaczoną tylko na tablety. Chcemy wykorzystać duży
ekran i wszystkie możliwości platformy dostępne w tabletach. Nie zamierzamy
dostosowywać aplikacji do urządzeń z mniejszymi ekranami, niezależnie od
wersji Androida działającej na tych urządzeniach.
ROZWIĄZANIE
Możliwe, że już znasz rozwiązanie. W pliku AndroidManifest.xml należy określić
wszystkie wymagania aplikacji. Następnie filtry w sklepie Android Market spra-
wią, że aplikacja nie będzie pojawiać się w urządzeniach innych niż tablety. Na
listingu 15.1 znajduje się fragment manifestu pozwalający uzyskać ten efekt.

Listing 15.1. W manifeście można określić, że aplikacja jest przeznaczona


tylko na tablety

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


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.manning.aip.tabdroid"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="11" />
<supports-screens android:smallScreens="false"
android:normalScreens="false"
android:largeScreens="false"
android:xlargeScreens="true" />
</manifest>

Aby aplikacja była przeznaczona tylko na tablety, w manifeście trzeba podać dwa
podstawowe wymagania. Otóż w urządzeniu musi działać Android 3.0 (Honey-
comb) lub nowsza wersja tej platformy . Może się wydawać, że to wystarczy.
W końcu jeśli dostępny jest interfejs API w wersji 11 (Android 3.0) lub nowszej,
można korzystać ze wszystkich interfejsów API potrzebnych w rozwijanej aplikacji.
Gdy powstawała ta książka, wersja Android 3.0 była najnowsza i działała tylko na
tabletach, ale do czasu trafienia tej pozycji na półki pojawią się prawdopodobnie
640 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

nowsze wersje, które będą obejmować wszystkie funkcje Androida 3.0 i pracować
zarówno na smartfonach, jak i na tabletach. Dlatego trzeba też określić, że apli-
kacja działa tylko na urządzeniach z ekranami xlarge . Ten rozmiar wyświe-
tlaczy wprowadzono w Androidzie 2.3. Odpowiada on ekranom mającym przy-
najmniej siedem cali. Po określeniu w manifeście tych dwóch wymagań można
mieć pewność, że każde urządzenie, na którym uruchamiana jest aplikacja, to
tablet ze zoptymalizowanymi pod jego kątem interfejsami API wprowadzonymi
w wersji Honeycomb.
Należy wspomnieć o jeszcze jednym aspekcie programowania aplikacji na
tablety. W czasie pisania aplikacji na smartfony programiści często zakładają, że
urządzenie zwykle znajduje się w orientacji pionowej. Na szczęście system ope-
racyjny dobrze obsługuje zmiany orientacji, dlatego nawet jeśli programista cał-
kowicie zapomni o przygotowaniu wersji dla poziomego układu ekranu, aplikacja
prawdopodobnie będzie działać poprawnie po obróceniu urządzenia. Warto
jednak zastanowić się nad trybem poziomym, a czasem dobrze jest nawet przy-
gotować dla niego odrębne układy. W Androidzie standardowo należy utworzyć
katalog ze zoptymalizowanymi plikami XML z kodem układów. Inna możliwość
to pominięcie orientacji poziomej i obsługiwanie tylko trybu pionowego. Ma to
pewne zalety, choć użytkownicy urządzeń z wysuwanymi klawiaturami nie będą
zadowoleni z aplikacji napisanej w ten sposób.
Tablety różnią się od smartfonów. Orientacja to jeden z obszarów, gdzie
różnice między tymi typami urządzeń są duże. Z tabletów zwykle korzysta się
w orientacji poziomej. Dlatego standardowo pliki układu dla tej orientacji
umieszcza się w katalogu /res/layout, a pliki dla trybu pionowego — w katalogu
/res/layout-port. Jeśli korzystasz z wtyczki ADT dla środowiska Eclipse, mecha-
nizm tworzenia interfejsu użytkownika z tej wtyczki pomoże Ci w rozwijaniu
aplikacji na tablety, co pokazano na rysunku 15.3.
OMÓWIENIE
Opisane tu podejście pod wieloma względami różni się od tworzenia typowych
aplikacji na Android. Zwykle warto obsługiwać jak najwięcej różnych wyświe-
tlaczy. Tu wykluczamy wszystkie wymiary oprócz jednego. Kiedy pojawiły się
pierwsze tablety z wersją Honeycomb Androida, nie tylko miały podobne wymiary,
ale też tę samą rozdzielczość ekranu. Było to coś nowego dla programistów apli-
kacji na Android, przyzwyczajonych do tworzenia rozwiązań z uwzględnieniem
ekranów o różnej wielkości i rozdzielczości. Od czasów urządzeń G1 nie można
było tworzyć programów dostosowanych do ekranu o konkretnych cechach (przy
czym fizyczne wymiary poszczególnych modeli tabletów były zróżnicowane).
Unikaj jednak stosowania przestarzałych układów AbsoluteLayout lub podawania
wymiarów w układzie za pomocą fizycznych pikseli.
Przedstawiliśmy projekty bibliotek, najnowsze interfejsy API i układy dla
dużych ekranów. Pora rozpocząć tworzenie programów na tablety z Androidem.
15.2. Podstawowe informacje o tabletach 641

Rysunek 15.3. Tworzenie interfejsu na tablety z wykorzystaniem wtyczki ADT

Zaczynamy od podstawowych technik, które powinien znać każdy programista


aplikacji na tablety. Ponadto pokazujemy, że techniki te nie są ograniczone do
tabletów i że można je łatwo wykorzystać także przy tworzeniu aplikacji na
smartfony.

15.2. Podstawowe informacje o tabletach


Tablety z Androidem istniały już na długo przed pojawieniem się Androida 3.0.
Miały ekrany o przekątnej od pięciu do siedmiu cali, były więc mniejsze niż
pierwsze urządzenia z wersją Honeycomb. Takie miniaturowe tablety były cie-
kawe same w sobie. Jak już wspomnieliśmy, większość aplikacji na Android dzia-
łała w nich prawidłowo. Z uwagi na dodatkową przestrzeń niektóre takie pro-
gramy wyglądały całkiem dobrze. Mimo to można było je opisać najwyżej jako
poprawne. Jest to dowód na to, że Android potrafi dostosować aplikację do wyświe-
tlacza bez nadmiernego utrudniania pracy użytkownikom.
Android 3.0 zaprojektowano tak, aby był więcej niż poprawny. Podejście nie
polegało na dostosowaniu Androida do poprawnej pracy na większym ekranie
lub na dodaniu nowych komponentów interfejsu użytkownika. Twórcy platformy
wprowadzili poważne zmiany, aby pomóc programistom w skutecznym pisaniu
aplikacji na urządzenia z większymi wyświetlaczami. Omawianie podstawowych
technik tworzenia aplikacji na tablety zaczynamy od przyjrzenia się jednemu
z najważniejszych mechanizmów wprowadzonych w wersji Honeycomb. Są nim
fragmenty.
642 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

0 TECHNIKA 89. Fragmenty

Wspomnieliśmy już, że Android 3.0 zaprojektowano z myślą o tabletach. Opra-


cowanie nowej wersji nie polegało na dodaniu nowych elementów do wcześniej-
szych odmian Androida. Jednym z najlepszych dowodów na to jest interfejs
API fragmentów. Fragmenty umożliwiają porządkowanie kodu aplikacji w nowy
sposób, znacznie ułatwiający radzenie sobie z tworzeniem układów dostosowanych
do dużych ekranów tabletów z Androidem. Jednak mechanizm ten jest przydatny
nie tylko w programach na tablety.
PROBLEM
Chcemy podzielić kod aplikacji na moduły, aby można było stosować zupełnie
odmienne układy dla orientacji poziomej i pionowej bez konieczności powielania
kodu oraz funkcji.
ROZWIĄZANIE
Rozwiązanie polega na użyciu fragmentów do uporządkowania kodu. Stosowanie
różnych układów dla orientacji poziomej i pionowej nie jest niczym nowym.
W przypadku tabletów istotna jest natomiast ilość miejsca na ekranie. W smart-
fonach, gdzie wyświetlacze są mniejsze, w układach poziomych i pionowych
zwykle dostępne są te same informacje oraz funkcje. Zmiana orientacji prowadzi
do sensownego nowego uporządkowania elementów. W tabletach nie jest niczym
niezwykłym wyświetlanie na ekranie odmiennych komponentów w różnych
układach. Przyjrzyjmy się konkretnemu przykładowi.
Aplikacja DealDroid (rozdział 2.) umożliwia użytkownikom wyświetlanie ofert
dnia z eBaya. Jedna z aktywności aplikacji wyświetla listę ofert, a druga — szcze-
gółowe informacje o wybranych ofertach. Na tablecie oba zadania można wyko-
nywać w jednej aktywności, ale tylko w układzie poziomym. Na rysunku 15.4
pokazano wygląd takiej aktywności.
W aplikacjach na tablety często stosuje się pewien wzorzec. Po lewej stronie
ekranu wyświetla się przewijaną listę, a po prawej — szczegółowe informacje
o wybranym elemencie. Na rysunku 15.5 pokazano, że wybranie elementu z listy
prowadzi do zmiany danych wyświetlanych w dużym obszarze ze szczegółami.
Wróćmy do problemu, czyli wyświetlania różnych komponentów w zależ-
ności od orientacji tabletu. Na rysunku 15.6 widać, co się dzieje po obróceniu
urządzenia.
Porównaj rysunki 15.5 i 15.6. Widać, że obszar ze szczegółowymi informa-
cjami wygląda tak samo w obu orientacjach. Różnice są podobne do tych, które
znamy z aplikacji na smartfony. Jednak listy ofert wyglądają inaczej. Tego właśnie
dotyczy określenie „zupełnie odmienne układy” z opisu problemu. W smart-
fonach takie podejście stosuje się bardzo rzadko, jednak w aplikacjach na tablety
nie jest ono niczym niezwykłym.
0 TECHNIKA 89. Fragmenty 643

Rysunek 15.4. Lista ofert i szczegółowe informacje w układzie poziomym

Rysunek 15.5. Przeglądanie elementów z listy ofert

Najważniejszym mechanizmem przy tworzeniu aplikacji podobnych do pokazanej


są fragmenty. Umożliwiają one podział interfejsu użytkownika na moduły. Na lis-
tingu 15.2 przedstawiono kod układu z rysunku 15.4.
644 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

Rysunek 15.6. Lista ofert


i szczegółowe informacje
w układzie pionowym

Listing 15.2. Kod XML układu ze szczegółowymi informacjami o ofercie


(/res/layout/details.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"
android:id="@+id/details_container">

<fragment
class="com.manning.aip.tabdroid.SectionDetailsFragment"
android:id="@+id/section_list_fragment"
android:visibility="gone"
android:layout_marginTop="?android:attr/actionBarSize"
android:layout_width="300dp"
android:layout_height="match_parent" />

<fragment class="com.manning.aip.tabdroid.DealFragment"
android:id="@+id/deal_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</LinearLayout>
0 TECHNIKA 89. Fragmenty 645

Mamy nadzieję, że miłym zaskoczeniem jest dla Ciebie to, jak prosty jest plik
układu dla widoku ze szczegółowymi informacjami. Kod obejmuje dwa frag-
menty. Pierwszy wyświetla listę ofert po prawej stronie ekranu. Drugi poka-
zuje szczegółowe informacje na temat wybranego elementu. Kod pierwszego
fragmentu przedstawiono na listingu 15.3.

Listing 15.3. Fragment wyświetlający listę ofert (plik SectionDetailsFragment.java)

public class SectionDetailsFragment extends ListFragment {


Section section;
int currentPosition = 0;
DealsApp app;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
app = (DealsApp) this.getActivity().getApplication();
section = app.currentSection;

if (savedInstanceState != null){
currentPosition = savedInstanceState.getInt("currentPosition");
int savedSectionPos =
savedInstanceState.getInt("currentSection", -1);
if (savedSectionPos >= 0){
section = app.sectionList.get(savedSectionPos);
app.currentSection = section;
}
} else if (app.currentItem != null){
for (int i=0;i<section.items.size();i++){
if (app.currentItem.equals(section.items.get(i))){
currentPosition = i;
break;
}
}
}
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
buildUi();
}
private void buildUi(){
ListView listView = this.getListView();
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
String[] dealTitles = new String[section.items.size()];
int i = 0;
for (Item item : section.items){
dealTitles[i++] = item.title;
}
setListAdapter(new ArrayAdapter<String>(getActivity(),
R.layout.deal_title_list_entry, dealTitles));
listView.setSelection(currentPosition);
showDeal(currentPosition);
}
}
646 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

Aby utworzyć fragment, wystarczy utworzyć klasę pochodną od android.app.


´Fragment. Tu tworzymy klasę pochodną od ListFragment , która sama jest
klasą pochodną od klasy Fragment. Klasa ListFragment obejmuje jeden widok
ListView i często służy do wyświetlania list elementów w układzie z podzielonym
ekranem, takim jak na rysunku 15.4. Fragment ma odrębny cykl życia powiązany
z cyklem życia nadrzędnej aktywności. Aktywność ta żąda widoku od fragmentu
przez wywołanie metody onCreateView danego fragmentu. Metoda onCreate
fragmentu jest wywoływana bezpośrednio po wywołaniu metody onCreate aktyw-
ności, jednak przed metodą onCreateView fragmentu. W metodzie onCreate przy-
wracamy lub ustawiamy stan fragmentu (podobnie jak robimy z aktywnością).
Pewien czas po wywołaniu metody onCreateView fragmentu następuje wywołanie
metody onActivityCreated . Jak wskazuje nazwa, wywołanie to ma miejsce
po utworzeniu aktywności. We wspomnianej metodzie konfigurujemy widok
ListView będący częścią fragmentu ListFragment. Widok ten działa jak inne
widoki ListView, dlatego trzeba określić dla niego adapter ListAdapter , który
zapewni dane i układ elementów z widoku.
Warto zauważyć, że ostatnią operacją w ramach konfigurowania interfejsu
użytkownika fragmentu ListFragment jest wywołanie metody showDeal. Metoda
ta wyświetla konkretną ofertę w głównym fragmencie ze szczegółowymi infor-
macjami. Dlatego metodę tę należy wywoływać po wybraniu elementu listy.
Na listingu 15.4 przedstawiono kod do wyświetlania ofert i obsługi dotknięcia
elementu.

Listing 15.4. Wyświetlanie konkretnej oferty (plik SectionDetailsFragment.java)

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
this.currentPosition = position;
showDeal(position);
}

private void showDeal(int position){


app.currentItem = app.currentSection.items.get(position);
DealFragment fragment =
(DealFragment) getFragmentManager().findFragmentById(
R.id.deal_fragment);
fragment.showCurrentItem();
}

Jednym z wygodnych aspektów korzystania z klasy ListFragment jest to, że trzeba


przesłonić metodę onListItemClick, aby obsługiwać dotknięcie elementów listy.
Tu sprawdzamy wybrany element listy . Następnie wywołujemy metodę
showDeal z listingu 15.3. Drugi fragment ma wtedy wyświetlić inną ofertę, dla-
tego potrzebny jest uchwyt do tego fragmentu . Do pobrania uchwytu używamy
egzemplarza klasy FragmentManager, dostępnego w każdym fragmencie. Wróć do
listingu 15.2. Zwróć uwagę, że przypisaliśmy do fragmentu identyfikator, który
0 TECHNIKA 89. Fragmenty 647

można teraz wykorzystać do uzyskania uchwytu. Po jego pobraniu należy wywo-


łać metodę showCurrentItem , aby nakazać ponowne wyświetlenie fragmentu.
Na listingu 15.5 przedstawiono tę metodę i pozostały kod klasy DealFragment.

Listing 15.5. Fragment do wyświetlania ofert (plik DealFragment.java)

public class DealFragment extends Fragment {


DealsApp app;
private ProgressBar progressBar;
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
app = (DealsApp) getActivity().getApplication();
View dealView = inflater.inflate(R.layout.deal_details,
container,
false);
progressBar = (ProgressBar) dealView.findViewById(R.id.progress);
progressBar.setIndeterminate(true);
Item item = app.currentItem;
if (item != null) {
populateDealView(dealView, item);
}
return dealView;
}
private void populateDealView(View dealView, Item item) {
ImageView icon = (ImageView) dealView.findViewById(
R.id.details_icon);
icon.setImageResource(R.drawable.placeholder);
new RetrieveImageTask(icon).execute(item.picUrl);
TextView title =
(TextView) dealView.findViewById(R.id.details_title);
title.setText(item.title);
CharSequence pricePrefix =
getText(R.string.deal_details_price_prefix);
TextView price =
(TextView) dealView.findViewById(R.id.details_price);
price.setText(pricePrefix + item.convertedCurrentPrice);
TextView msrp = (TextView) dealView.findViewById(
R.id.details_msrp);
msrp.setText(item.msrp);
TextView quantity =
(TextView) dealView.findViewById(R.id.details_quantity);
quantity.setText(Integer.toString(item.quantity));
TextView quantitySold = (TextView) dealView.findViewById(
R.id.details_quantity_sold);
quantitySold.setText(Integer.toString(item.quantitySold));
TextView location =
(TextView) dealView.findViewById(R.id.details_location);
location.setText(item.location);
}
public void showCurrentItem(){
Item item = app.currentItem;
View dealView = getView();
populateDealView(dealView, item);
}
}
648 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

To kolejny fragment. Tym razem bezpośrednio tworzymy klasę pochodną od klasy


Fragment . Nie trzeba przejmować się zarządzaniem stanem tego fragmentu,
ponieważ jest on powiązany ze stanem aktywności (i fragmentu ListFragment
z listingu 15.3). Dlatego wystarczy przesłonić metodę onCreateView . Zauważ, że
do tej wywoływanej zwrotnie metody przekazujemy obiekt klasy LayoutInflater.
Wykorzystujemy go do przekształcenia pliku XML układu na widok . Następnie
wiążemy dane wybranej oferty z kontrolkami z pliku XML układu. W końco-
wej części listingu znajduje się metoda showCurrentItem, którą mogą wywoływać
inne fragmenty. Metoda ta sprawdza, który element jest wybrany, i przekazuje
go do używanej już wcześniej metody populateDealView .
Po zmianie orientacji tabletu na pionową należy wyświetlić inny układ,
widoczny na rysunku 15.6. Najprościej uzyskać ten efekt przez zastosowanie
odrębnego pliku XML układu. Kod układu dla orientacji pionowej przedsta-
wiono na listingu 15.6.

Listing 15.6. Układ dla orientacji pionowej (/res/layout-port/details.xml)

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/details_container"
android:gravity="bottom">

<fragment class="com.manning.aip.tabdroid.DealFragment"
android:id="@+id/deal_fragment"
android:layout_marginTop="?android:attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<fragment class="com.manning.aip.tabdroid.FilmstripFragment"
android:id="@+id/section_filmstrip_fragment"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_gravity="bottom"
/>
</LinearLayout>

Kod z listingu 15.6 jest podobny do kodu z listingu 15.2. Ponownie wykorzystu-
jemy tu opisany już fragment DealFragment. Przeznaczeniem fragmentów jest
właśnie umożliwianie powtórnego wykorzystania kodu. Zauważ, że nie pokazu-
jemy kodu aktywności obejmującej fragmenty. Nie ma takiej potrzeby. Fragmenty
są niezależne. W orientacji pionowej zastępujemy klasę SectionDetailsFragment
klasą FilmstripFragment . Kod tej ostatniej znajduje się na listingu 15.7.
Klasa FilmstripFragment jest nieco podobna do klasy SectionDetailsFragment
z listingu 15.3. Obie klasy wyświetlają wszystkie oferty z danej kategorii i umoż-
liwiają dotknięcie oferty w celu wyświetlenia szczegółowych informacji na jej
0 TECHNIKA 89. Fragmenty 649

Listing 15.7. Pozioma lista rysunków używana do wybierania ofert


(plik FilmstripFragment.java)

public class FilmstripFragment extends Fragment {


@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
// Kod do zarządzania stanem pominięto.
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState){
HorizontalScrollView strip =
(HorizontalScrollView) inflater.inflate(R.layout.filmstrip,
container,
false);
fillWithPics(strip);
return strip;
}
private void fillWithPics(HorizontalScrollView strip) {
ViewGroup pics = (ViewGroup) strip.findViewById(R.id.pics);
if (pics.getChildCount() > 0){
pics.removeAllViews();
}
int i =0;
for (Item item : section.items){
ImageView imgView = new ImageView(getActivity());
// Kod do pobierania bitmapy.
imgView.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View img) {
currentPosition = pos;
showDeal(pos);
}
});
}
showDeal(currentPosition);
}
private void showDeal(int position){
app.currentItem = app.currentSection.items.get(position);
DealFragment fragment = (DealFragment) getFragmentManager()
.findFragmentById(R.id.deal_fragment);
fragment.showCurrentItem();
}
}

temat w komponencie DealFragment. Podobny mechanizm stosujemy w innych


fragmentach. Najpierw w metodzie onCreate przywracamy stan. Dalej znajduje
się implementacja metody onCreateView , która przekształca plik XML układu
w zwracany widok. Tym razem nie stosujemy widoku ListView, ale „rolkę filmu” —
przewijany w poziomie zbiór rysunków (jest to widok HorizontalScrollView;
zobacz plik /res/layout-port/filmstrip.xml). Wystarczy zapełnić ten widok obiek-
tami klasy ImageView, obejmującymi bitmapy z rysunkami ofert. Dla każdego
widoku ImageView trzeba ustawić metodę obsługi zdarzeń wywoływaną w reakcji
650 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

na dotknięcie grafiki przez użytkownika. Zdarzenie to prowadzi do pobrania za


pomocą klasy FragmentManager uchwytu do fragmentu DealFragment i wywołania
metody showCurrentItem tego fragmentu .
OMÓWIENIE
Fragmenty umożliwiają porządkowanie kodu aplikacji w nowy sposób. Fragment
pod wieloma względami może być „samowystarczalny”. Samodzielnie zarządza
wyświetlanymi danymi i obsługuje swój stan. Fragmenty z przykładowej aplikacji
zależą od globalnego stanu aplikacji (obiektu typu Application). Można z nich
korzystać w dowolnym miejscu omawianego programu, ale już nie poza nim. Jest
to celowe. Inna możliwość to utworzenie fragmentów zależnych od usług lub
całkowicie niezależnych.
Programiści stosowali ten wzorzec na długo przed wprowadzeniem frag-
mentów, choć framework Androida tego nie ułatwiał. Jedno z często używanych
podejść polegało na tworzeniu komponentów interfejsu użytkownika, które
potrafią zarządzać stanem, pobierać dane i wykonywać podobne operacje. Odmianą
tego wzorca jest rozwijanie komponentów interfejsu użytkownika bezpośrednio
komunikujących się z usługą wykonującą wszystkie skomplikowane operacje.
Choć zdaniem niektórych programistów należy unikać takich rozwiązań, ponie-
waż naruszają paradygmat model-widok-kontroler, opisane wzorce często oka-
zują się przydatne. Załóżmy, że w aplikacji znajduje się nagłówek z informa-
cjami o stanie, na przykład o liczbie nieprzeczytanych wiadomości lub nowych
ofert dnia. Przed wprowadzeniem fragmentów często (oprócz łączenia kodu modelu
z kodem komponentu interfejsu użytkownika) tworzono klasę aktywności do
zarządzania stanem. Następnie aktywność tę stosowano jako klasę bazową dla
wszystkich pozostałych aktywności aplikacji. Rozwiązania te mają wady i zalety,
przy czym fragmenty pozwalają pisać dużo bardziej przejrzysty kod, niż jest to
w przypadku innych podejść.
Technika oparta na bazowej aktywności często służy też do obsługi menu.
Taka aktywność może tworzyć menu wyświetlane we wszystkich aktywnościach.
Jednym z powodów tworzenia menu na poziomie aplikacji jest to, że zazwyczaj
ma ono dla programisty niewielkie znaczenie. Jeśli w menu znajduje się jakaś
ważna opcja, i tak trzeba umieścić ją także w innym miejscu ekranu, ponieważ
bywa, że użytkownicy nie korzystają z menu. W menu nierzadko znajdują się też
typowe opcje, takie jak O programie, Pomoc techniczna, Wyrejestruj się itd.
W wersji Honeycomb wprowadzono rozwiązanie znacznie wygodniejsze od
menu — pasek akcji (ang. Action Bar). W następnej technice opisujemy ten mecha-
nizm i wyjaśniamy, kiedy warto go stosować.

0 TECHNIKA 90. Pasek akcji

Warto stosować menu w Androidzie. Można umieścić w nim wiele skrótów


i przydatnych opcji. Można też udostępniać w nim operacje kontekstowe. Poważ-
nym problemem jest jednak to, że użytkownicy rzadko zaglądają do menu.
0 TECHNIKA 90. Pasek akcji 651

Wskutek tego zaczęto tworzyć paski akcji. Często stosuje się je w tym samym
celu co samo menu, są jednak skuteczniejsze z uwagi na większą widoczność dla
użytkownika.
PROBLEM
Chcemy wyświetlać dodatkowe, ale użyteczne funkcje dostępne w kontekście
używanej akurat aktywności. Nie zamierzamy jednak stosować standardowego
menu Androida, ponieważ użytkownicy często nie korzystają z niego.
ROZWIĄZANIE
Rozwiązanie polega na zastosowaniu paska akcji. Znajduje się on w górnej części
ekranu i jest dobrze widoczny dla użytkowników. Eliminuje to największy kłopot
związany z menu. Na rysunku 15.7 pokazano przykładowy pasek akcji w aplikacji
na tablety.

Rysunek 15.7. Pasek akcji w akcji

Jak widać, pasek akcji znajduje się w górnej części ekranu. W przykładowym
programie na pasku są ikona aplikacji, kilka zakładek i przycisk Podziel się.
Ikona aplikacji pozwala użytkownikom przejść do głównego ekranu, a zakładki
służą do przechodzenia do różnych kategorii ofert dnia z eBaya. Na rysunku 15.8
pokazano, że przycisk Podziel się pozwala „podzielić się” ofertą z innymi osobami
za pomocą aplikacji zainstalowanych w urządzeniu.
Jak może pamiętasz, w pierwszej wersji aplikacji DealDroid funkcja „dzie-
lenia się” była ukryta w menu. W wersji dla tabletów nawigacja jest wygodniejsza.
Pasek akcji nie tylko pozwala rozwiązać problem z menu, ale ma też inne funkcje.
Zakładki nawigacyjne omawiamy dalej. Teraz skupimy się na ikonach aplikacji
i funkcji „dzielenia się”. Na listingu 15.8 przedstawiono kod tych elementów.

Listing 15.8. Ikona aplikacji i funkcji „dzielenia się” z paska akcji


(plik DetailsActivity.java)

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.details_menu, menu);
return true;
}
652 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

Rysunek 15.8. „Dzielenie się” ofertą na tablecie

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, DealsMain.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
case R.id.share_action:
shareDealUsingChooser("text/*");
return true;
default:
return super.onOptionsItemSelected(item);
}
}

private void shareDealUsingChooser(final String type) {


// Z uwagi na zwięzłość pominięto. Kod jest taki sam jak w rozdziale 2.
}

private String createDealMessage() {


// Z uwagi na zwięzłość pominięto.
}

Na listingu 15.8 widać, że przodkiem paska akcji jest menu. Aby utworzyć pasek
akcji, należy zaimplementować wywoływaną zwrotnie metodę onCreateOptionsMenu
aktywności . Elementy paska akcji można tworzyć programowo. Jest to przy-
datne zwłaszcza wtedy, gdy wyświetlanie elementów zależy od stanu aktywności.
Inna możliwość to określenie zawartości paska akcji w XML-u. Oto kod w XML-u
tworzący pasek akcji z rozdziału 15.7:
0 TECHNIKA 90. Pasek akcji 653

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/share_action"
android:title="@string/deal_details_share_menu"
android:icon="@drawable/ic_menu_share"
android:showAsAction="ifRoom|withText" />
</menu>

Jak widać, określony jest tu jeden element z tytułem i ikoną. W przykładzie


tytuł i ikona to zewnętrzne zasoby, dlatego można utworzyć ich wersje dla innych
języków. Zwróć też uwagę na atrybut showAsAction. Pojawił się on w Androidzie 3.0
i służy do określenia, kiedy dana opcja menu ma być dostępna jako akcja i jak ma
wyglądać. Można ustawić ten atrybut na always, jednak jeśli na ekranie brakuje
miejsca, pasek wygląda nieelegancko.
Wróćmy do listingu 15.8. Aby zdefiniować działanie paska akcji (reakcję
na dotknięcie opcji przez użytkownika), należy zaimplementować metodę
onOptionsItemSelected aktywności. W ten sam sposób określana jest reakcja na
dotknięcie ikony aplikacji, widocznej po lewej stronie paska . Ikona ta jest
identyfikowana na podstawie predefiniowanego identyfikatora zasobu ( home).
Aplikacja w reakcji na wybranie tej ikony opróżnia stos aktywności i kieruje użyt-
kownika do głównego ekranu . Dotknięcie przycisku Podziel się można wykryć
przez dopasowanie identyfikatora zdefiniowanego w XML-owym kodzie menu do
identyfikatora wybranego elementu MenuItem . Wybranie wspomnianego przy-
cisku prowadzi do wywołania metody shareDealUsingChooser z aplikacji Deal-
Droid z rozdziału 2. Aplikacja wyświetla wtedy interfejs użytkownika widoczny
na rysunku 15.8.
Wiesz już, jak tworzyć ikony i określać ich działanie. Przyjrzyjmy się teraz,
jak tworzyć zakładki widoczne na rysunku 15.7. Potrzebny kod pokazano na
listingu 15.9.

Listing 15.9. Tworzenie zakładek paska akcji i zarządzanie nimi


(plik DetailsActivity.java)

public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.details);
app = (DealsApp) getApplication();
ActionBar bar = this.getActionBar();
TabListener listener = new TabListener(){
@Override
public void onTabReselected(Tab t, FragmentTransaction txn) {}
@Override
public void onTabSelected(Tab t, FragmentTransaction txn) {
if (active){
changeTab(t.getPosition());
}
}
@Override
public void onTabUnselected(Tab t, FragmentTransaction txn) {}
};
for (int i=0;i<Math.min(6, app.sectionList.size());i++){
654 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

final Section section = app.sectionList.get(i);


Tab tab = bar.newTab();
tab.setText(chomp(section.title));
tab.setTabListener(listener);
if (app.currentSection != null &&
app.currentSection.equals(section)){
bar.addTab(tab, true);
} else {
bar.addTab(tab);
}
}
bar.setDisplayShowTitleEnabled(false);
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
active = true;
}
private void changeTab(int position){
FragmentManager fm = getFragmentManager();
int orientation = getResources().getConfiguration().orientation;
if (orientation == ORIENTATION_LANDSCAPE){
SectionDetailsFragment fragment =
(SectionDetailsFragment) fm.findFragmentById(
R.id.section_list_fragment);
fragment.setSection(position);
} else {
FilmstripFragment fragment =
(FilmstripFragment) fm.findFragmentById(
R.id.section_filmstrip_fragment);
fragment.setSection(position);
}
}

Od wersji Android 3.0 każda aktywność może mieć pasek akcji. Jest on dostępny
poprzez metodę getActionBar aktywności . Pomysł polega na programowym
tworzeniu zakładek i dodawaniu ich do paska akcji. Każda zakładka wymaga
odbiornika TabListener, który będzie reagował na dotknięcie, dlatego tworzymy
jeden taki odbiornik do obsługi wszystkich zakładek. Dalej znajduje się imple-
mentacja metody onTabSelected i wywołanie metody changeTab na podstawie
pozycji wybranej zakładki. Działanie metody changeTab omawiamy dalej.
Kiedy egzemplarz odbiornika TabListener jest już gotowy, można utworzyć
zakładki i dodać je do paska akcji. Programowo tworzymy zakładkę , ustawiamy
jej tytuł i odbiornik TabListener . Zauważ, że na podstawie kategorii wybranej
przez użytkownika aplikacja określa obecnie zaznaczoną zakładkę. Nakazujemy
też paskowi akcji, aby nie wyświetlał nazwy aktywności, a w zamian pokazywał
zakładki nawigacyjne.
Przyjrzyjmy się teraz metodzie changeTab wywoływanej przez zwrotną metodę
onTabSelected odbiornika TabListener. Metoda changeTab najpierw sprawdza orien-
tację urządzenia . Jest to potrzebne, ponieważ układ tabletu wpływa na zawar-
tość aktywności. Metoda wykorzystuje informację o orientacji i obiekt klasy
FragmentManager z aktywności do uzyskania uchwytu do wyświetlanego fragmentu.
Następnie ustawiamy kategorię dla fragmentu , co pozwala określić, jakiego
rodzaju oferty dnia mają być widoczne.
0 TECHNIKA 91. Przeciąganie 655

OMÓWIENIE
Nawigacja z wykorzystaniem zakładek nie jest niczym nowym ani specjalnym
dla tabletów. Od lat jest powszechnie używana w aplikacjach sieciowych i wystę-
puje w Androidzie od wersji 1.0. Do tworzenia zakładek zawsze służyły klasy
TabHost i TabWidget. Pierwsza z nich umożliwia tworzenie zestawu zakładek,
z których każda powiązana jest z wyświetlaną aktywnością. Zakładki paska akcji
to rozwinięcie tej techniki, podobnie jak inne aspekty tego paska są rozwinięciem
menu.
Aby zbudować nawigację opartą na zakładkach z paska akcji, należy utworzyć
zakładki w podobny sposób jak w klasie TabHost. Jednak zamiast łączyć z każdą
zakładką odrębną aktywność, można pracować w ramach jednej aktywności
i stosować fragmenty. W przykładzie zmieniamy zawartość fragmentu. Zwróć
jednak uwagę na to, że do metody onTabSelected przekazywany jest obiekt klasy
FragmentTransaction. Dlatego w aktywności można wykonywać różne operacje na
fragmentach, na przykład usuwać je lub zastępować innymi. Pasek akcji nie tylko
jest ulepszeniem dawnego systemu menu, ale w połączeniu z fragmentami
sprawia też, że porządkowanie kodu aplikacji jest prostsze, i daje przy tym więcej
możliwości.
Ostatnia z podstawowych technik związanych z tabletami, przeciąganie,
pozwala usprawnić interakcję użytkowników z aplikacją.

0 TECHNIKA 91. Przeciąganie

Odkąd Douglas Engelbart wymyślił mysz komputerową, menedżerowie produktu


żądają od programistów dodawania funkcji przeciągania. W rozbudowanych
frameworkach do tworzenia aplikacji desktopowych mechanizm przeciągania
jest dostępny od wielu lat. W aplikacjach sieciowych przez długi czas występo-
wały znaczne problemy z jego obsługą. Pracę programistom ułatwiały frame-
worki JavaScriptu, aż w końcu mechanizm przeciągania stał się częścią specyfi-
kacji języka HTML5. W świecie urządzeń mobilnych do momentu pojawienia się
Androida 3.0 przeciąganie było w frameworkach pomijane. Oczywiście, można
było dodać jego obsługę za pomocą interfejsów API do obsługi dotknięć, jed-
nak technikę tę wykorzystywano głównie w grach. Od wersji Honeycomb prze-
ciąganie można stosunkowo łatwo dodać do aplikacji dowolnego rodzaju.
PROBLEM
Chcemy umożliwić użytkownikom bardziej intuicyjną interakcję z aplikacją
przez udostępnienie przeciągania różnych elementów.
ROZWIĄZANIE
Aby umożliwić przeciąganie w aplikacji, wystarczy zastosować kilka interfejsów
API wprowadzonych w Androidzie 3.0. W ramach przykładu przedstawiamy
prostą aplikację z funkcją przeciągania. Program wyświetla na ekranie widoki
656 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

StackView (jest to nowa kontrolka wprowadzona w wersji Honeycomb) i umożliwia


użytkownikom zmianę uporządkowania tych kontrolek przez ich przeciągnięcie.
Wygląd aplikacji przedstawiono na rysunku 15.9.

Rysunek 15.9. Aplikacja z funkcją przeciągania

Jak widać, aplikacja wyświetla prostą siatkę z kilkoma kontrolkami StackView.


Na listingu 15.10 znajduje się kod tego układu.

Listing 15.10. Plik XML z układem z siatką, używanym przez mechanizm


przeciągania

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


<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow>
<LinearLayout android:layout_width="640dp"
android:layout_height="345dp"
android:id="@+id/topLeft">
<StackView android:id="@+id/stack"
android:layout_width="250dp"
android:layout_height="250dp"
android:clickable="true"
android:loopViews="true"
android:longClickable="true"
/>
</LinearLayout>
<LinearLayout android:layout_width="640dp"
android:layout_height="345dp"
android:id="@+id/topRight"
/>
</TableRow>
<TableRow>
0 TECHNIKA 91. Przeciąganie 657

<LinearLayout android:layout_width="640dp"
android:layout_height="345dp"
android:id="@+id/bottomLeft"
/>
<LinearLayout android:layout_width="640dp"
android:layout_height="345dp"
android:id="@+id/bottomRight">
<StackView android:id="@+id/stack2"
android:layout_width="250dp"
android:layout_height="250dp"
android:clickable="true"
android:loopViews="true"
android:longClickable="true"
/>
</LinearLayout>
</TableRow>
</TableLayout>

W kodzie z listingu 15.10 używamy układu TableLayout . Nie ma w nim nic


wyjątkowego. Tu pozwala łatwo zidentyfikować różne części ekranu na potrzeby
pokazu przeciągania. Każda komórka tabeli obejmuje układ LinearLayout . Także
on nie ma żadnych specjalnych cech. Potrzebujemy tylko kontenera, do którego
można przeciągać widoki StackView . Również te widoki nie mają żadnych
specjalnych cech w kontekście przeciągania. Co więcej, prościej byłoby ich nie
używać, jednak są ciekawe wizualnie, dlatego warto z nich korzystać w aplika-
cjach na tablety.
Najważniejszą cechą aplikacji jest umożliwianie użytkownikom przeciągania
widoków StackView do różnych kontenerów (układów LinearLayout) na ekranie.
Aby rozpocząć przeciąganie, użytkownik musi dotknąć i przytrzymać stos (długie
kliknięcie), co powoduje podświetlenie miejsc, w których można upuścić ele-
menty. Na rysunku 15.10 pokazano takie miejsca.
Wszystkie pozostałe operacje w aplikacji wykonujemy programowo. Na lis-
tingu 15.11 znajduje się kod tworzący interfejs użytkownika.

Listing 15.11. Tworzenie interfejsu użytkownika

public class DndActivity extends Activity {


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

StackView stack = (StackView) findViewById(R.id.stack);


Bitmap[] bmps = new Bitmap[5];
Resources res = getResources();
bmps[0] = BitmapFactory.decodeResource(res, R.drawable.donut);
bmps[1] = BitmapFactory.decodeResource(res, R.drawable.eclair);
bmps[2] = BitmapFactory.decodeResource(res, R.drawable.froyo);
bmps[3] =
BitmapFactory.decodeResource(res, R.drawable.gingerbread);
bmps[4] =
BitmapFactory.decodeResource(res, R.drawable.honeycomb);
658 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

Rysunek 15.10. Aktywne obszary, w których można upuścić elementy

ImgAdapter adapter = new ImgAdapter(bmps, stack);


stack.setAdapter(adapter);

StackView stack2 = (StackView) findViewById(R.id.stack2);


stack2.setAdapter(new ImgAdapter(bmps, stack2));

findViewById(R.id.topLeft).setOnDragListener(
new BoxDragListener());
findViewById(R.id.bottomLeft).setOnDragListener(
new BoxDragListener());
findViewById(R.id.topRight).setOnDragListener(
new BoxDragListener());
findViewById(R.id.bottomRight).setOnDragListener(
new BoxDragListener());
}
}

Pierwszą rzeczą, jaką trzeba zrobić w celu utworzenia interfejsu użytkownika,


jest uzyskanie referencji do jednego z widoków StackView . Następnie aplikacja
wczytuje zasoby graficzne będące częścią aplikacji . Na podstawie rysun-
ków 15.9 i 15.10, a także kodu z listingu 15.11 można stwierdzić, że rysunki
używane w widokach StackView to ikony reprezentujące różne wersje Androida
(aż do wersji Honeycomb). Rysunki te są przekształcane na obiekty klasy Bitmap
i przekazywane do niestandardowego adaptera widoku StackView (kod tego
adaptera przedstawiono na listingu 15.12). StackView to oparta na adapterze kon-
trolka podobna do widoków ListView i GridView. Pobieramy uchwyty do kontene-
rów LinearLayout z listingu 15.10 i przekazujemy im obiekt typu OnDragListener .
0 TECHNIKA 91. Przeciąganie 659

OnDragListener to nowy interfejs wprowadzony w Androidzie 3.0. Jego imple-


mentację przedstawiono na listingu 15.13. Teraz przyjrzyjmy się niestandardo-
wemu adapterowi widoków StackView.

Listing 15.12. Adapter używany dla widoków StackView w aplikacji

class ImgAdapter extends BaseAdapter{


private Bitmap[] bmps;
private Context ctx = DndActivity.this;
private ViewGroup owner;
ImgAdapter(Bitmap[] bmps, ViewGroup owner){
this.bmps = bmps;
this.owner = owner;
}
@Override
public int getCount() {
return bmps.length;
}
@Override
public Object getItem(int index) {
return bmps[index];
}
@Override
public long getItemId(int index) {
return index;
}
@Override
public View getView(int index, View recycledView, ViewGroup parent) {
if (recycledView == null){
recycledView = new ImageView(ctx);
}
ImageView imgView = (ImageView) recycledView;
imgView.setOnLongClickListener(new OnLongClickListener(){
@Override
public boolean onLongClick(View view) {
ClipData data =
ClipData.newPlainText("foo","bar");
DragShadowBuilder sBuilder =
new DragShadowBuilder(owner);
owner.startDrag(data, sBuilder, owner, 0);
return true;
}
});
imgView.setImageBitmap(bmps[index]);
return imgView;
}
}

Wspomnieliśmy już, że widok StackView przypomina widoki ListView i GridView.


Wszystkie te widoki korzystają z adaptera tego samego rodzaju, dlatego tu two-
rzymy klasę pochodną od BaseAdapter . Jej kod jest podobny do kodu innych
adapterów. Adapter ImgAdapter z listingu 15.12 można zastosować także dla widoku
ListView lub GridView. Jest on przeznaczony nie tylko dla widoków StackView.
Jedyny wyjątkowy kod służy do obsługi przeciągania. Najpierw z widokiem
660 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

StackView wiążemy odbiornik OnLongClickListener . Przeciąganie chcemy inicjo-


wać w momencie długiego kliknięcia, wykrywanego właśnie przez ten odbiornik.
Nie chcemy przeciągać poszczególnych widoków ImageView z widoku StackView —
interesuje nas przeciąganie całego widoku StackView. To dlatego przechowujemy
referencję owner do tego widoku. Dalej wywołujemy metodę startDrag , dodaną
do klasy android.view.View w wersji Honeycomb Androida. Metoda ta początkuje
przeciąganie widoku StackView. Zauważ, że jednym z parametrów metody startDrag
jest egzemplarz klasy DragShadowBuilder. Klasa ta odpowiada za wyświetlanie
cienia widoku przeciąganego po ekranie. Tu używamy domyślnej wersji klasy
DragShadowBuilder. Wyświetla ona widok przekazany do konstruktora tej klasy.
Wyświetlanie takiego widoku może wymagać dużo zasobów. Jeśli tak jest, warto
utworzyć klasę pochodną od DragShadowBuilder i wyświetlać w niej niestandardowy
cień. Istnieje też inne rozwiązanie — można zastosować domyślną wersję klasy
DragShadowBuilder, jednak nie przekazywać widoku do jej konstruktora. Wtedy
klasa nie wyświetla żadnego cienia.

Do czego służy klasa ClipData?


Może zauważyłeś, że na listingu 15.13 przekazaliśmy do metody startDrag obiekt
klasy ClipData. Jeśli tak, prawdopodobnie zwróciłeś uwagę, że umieściliśmy w tym
obiekcie fikcyjne dane. Klasa ClipData jest przydatna, kiedy widok reprezentuje
skomplikowane dane przeciągane w aplikacji. Widok może na przykład wyświetlać
obiekt z danymi kontaktowymi, które aplikacja pomaga porządkować. Klasa ClipData
pozwala w wygodny sposób powiązać z przeciąganym widokiem wskaźnik do złożo-
nego obiektu. Ponadto klasa ta jest potrzebna do udostępniania niestandardowego
mechanizmu kopiowania i wklejania.

Po zainicjowaniu przeciągania widoku StackView w aplikacji trzeba dodać obsługę


upuszczania go w jednym z kontenerów LinearLayout. Na listingu 15.13 pokazano,
jak to zrobić za pomocą ustawionego wcześniej odbiornika OnDragListener.

Listing 15.13. Implementacja interfejsu OnDragListener dla kontenerów

class BoxDragListener implements OnDragListener{


boolean insideOfMe = false;
Drawable border = null;
Drawable redBorder = getResources().getDrawable(R.drawable.border3);
@Override
public boolean onDrag(View self, DragEvent event) {
if (event.getAction() == DragEvent.ACTION_DRAG_STARTED){
border = self.getBackground();
self.setBackgroundDrawable(redBorder);
} else if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED){
insideOfMe = true;
} else if (event.getAction() == DragEvent.ACTION_DRAG_EXITED){
insideOfMe = false;
} else if (event.getAction() == DragEvent.ACTION_DROP){
if (insideOfMe){
View view = (View) event.getLocalState();
0 TECHNIKA 91. Przeciąganie 661

ViewGroup owner = (ViewGroup) view.getParent();


owner.removeView(view);
LinearLayout container = (LinearLayout) self;
if (container.getChildCount() > 0){
container.addView(view,
container.getChildCount());
} else {
container.addView(view);
}
}
} else if (event.getAction() == DragEvent.ACTION_DRAG_ENDED){
self.setBackgroundDrawable(border);
}
return true;
}
}

W każdym kontenerze w aplikacji używamy egzemplarza klasy z implementacją


interfejsu OnDragListener (listing 15.13). Kontener musi śledzić, czy przeciągany
widok się w nim znajduje. Informacja ta jest przechowywana w zmiennej logicz-
nej . W momencie rozpoczęcia przeciągania obramowanie kontenera ustawiamy
na kolor czerwony . Jest to informacja, że w danym kontenerze można upuścić
przeciągany widok. Następnie śledzimy zdarzenia ENTERED i EXIT za pomocą
zmiennych logicznych. Po zgłoszeniu upuszczenia trzeba zrobić dwie rzeczy.
Najpierw należy usunąć przeciągany widok z poprzedniego kontenera . Później
trzeba dodać przeciągany widok do nowego kontenera . Po wykryciu zakończe-
nia przeciągania należy przywrócić domyślny wygląd obramowania . Warto
zauważyć, że nie ma tu znaczenia, jakiego rodzaju widok jest przeciągany do
kontenera. Ponadto jeśli dany kontener nie ma obsługiwać mechanizmu przecią-
gania, wystarczy nie ustawiać odbiornika OnDragListener. Kontener bez tego odbior-
nika nie ma czerwonego obramowania i nie przyjmuje upuszczonych widoków.
OMÓWIENIE
Nie bez powodu programiści nieustannie są proszeni o dodawanie obsługi prze-
ciągania do aplikacji. W programach dowolnego rodzaju przeciąganie pozwala
w bardziej intuicyjny sposób komunikować się z aplikacją. W przeszłości przecią-
ganie odbywało się za pomocą myszy w aplikacjach desktopowych oraz siecio-
wych — i nawet przy korzystaniu z tego urządzenia operacja ta była intuicyjna.
Obecnie nastała era urządzeń dotykowych, z których jako pierwsze pojawiły
się smartfony. Jednak przeciąganie rzadko było w nich stosowane, choć nie
z powodu braku potrzebnych mechanizmów w Androidzie. Po prostu operacja ta
na małych ekranach jest niewygodna. Miejsca, w których można dotknąć ekran,
aby rozpocząć przeciąganie, są zwykle małe. Jednak na tabletach sytuacja wygląda
zupełnie inaczej. Przeciąganie jest w nich dużo wygodniejsze niż w smartfonach.
Tablety mają duże wyświetlacze, dlatego można precyzyjnie wskazać przecią-
gany element. Ponadto przeciąganie za pomocą interfejsów dotykowych jest
662 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem

znacznie wygodniejsze niż posługiwanie się myszą. Przy przeciąganiu z wyko-


rzystaniem myszy pojawia się ikona dłoni, co ma imitować przenoszenie elemen-
tów w rzeczywistym świecie. Przeciąganie na tabletach nie wymaga żadnego
imitowania, ponieważ to prawdziwa, ludzka dłoń służy do interakcji z obiektami
na ekranie.
Możliwości tworzenia intuicyjnych interfejsów opartych na przeciąganiu
w aplikacjach na tablety są atrakcyjne i niemal nieskończone. Trzeba jednak
pokonać kilka przeszkód. Ponieważ przeciąganie nie było popularne w urządze-
niach dotykowych (smartfonach), nie istnieją powszechnie przyjęte sposoby
informowania użytkowników o tym, że aplikacja obsługuje tę technikę. W przy-
kładowej aplikacji początkiem przeciągania jest dotknięcie ekranu i przytrzy-
manie palca (czyli długie kliknięcie, jak określają to programiści aplikacji na
Android). Nie chcemy rozpoczynać przeciągania w reakcji na każde dotknięcie
widoku StackView. To uniemożliwiłoby użytkownikom przeglądanie rysunków
z widoku StackView. Ponadto irytująca byłaby sytuacja, gdyby każde dotknięcie
ekranu skutkowało wyświetlaniem czerwonego obramowania wokół kontenerów.
Nie twierdzimy jednak, że długie kliknięcie powinno być powszechnie przyjętym
sposobem początkowania przeciągania w aplikacjach na tablety. Inna możliwość
to dodanie przycisku Edycja (na przykład na pasku akcji) do włączania trybu
przeciągania, w którym dotknięcie początkuje przeciąganie obiektu.

15.3. Podsumowanie
Z pewnością wiesz, że Android nie był pierwszą platformą na smartfony, którą
przeniesiono na tablety. Sukces Androida w dotykowych smartfonach sprawił,
że zastosowanie go w tabletach było oczywiste. Jednak, jak zobaczyłeś w tym
rozdziale, twórcy tej platformy nie spoczęli na laurach i wprowadzili szereg istot-
nych usprawnień z myślą o tabletach. Najważniejszą nowinką było zastosowanie
fragmentów. Wspomnieliśmy już, że potrzeba tworzenia bardziej niezależnych
komponentów w Androidzie nie jest warunkowana tylko tym, że pojawiły się
tablety. Programiści odczuwali ją już wcześniej i wymyślali rozmaite rozwiązania
problemu. Jednak na tabletach chciano tworzyć interfejsy użytkownika lepiej
wykorzystujące dużą powierzchnię ekranu. Fragmenty to umożliwiły. Obecnie
fragmenty (a także inne ważne mechanizmy programowania aplikacji na tablety —
pasek akcji i przeciąganie) są dostępne także we wcześniejszych wersjach An-
droida poprzez pakiet Android Compatibility.
Warto zauważyć, że rozdział ten nie jest wyczerpującym omówieniem wszyst-
kich zmian wprowadzonych w wersji Honeycomb Androida. Większość mody-
fikacji ma pomagać programistom pisać aplikacje na tablety, dlatego pasuje do
tego rozdziału. Zdecydowaliśmy się jednak skoncentrować na najważniejszych
technikach, które powinni znać autorzy wszystkich aplikacji na Android. Nie
chcemy przy tym umniejszać znaczenia innych funkcji. Pominęliśmy tu na przy-
kład kilka nowych rozwiązań (m.in. środowisko Renderscript) bardzo przydat-
15.3. Podsumowanie 663

nych dla twórców gier. Nie omówiliśmy też usprawnień klasy RemoteView, ulep-
szających obsługę kontrolek i powiadomień na ekranie głównym. Wszystkie te
funkcje są istotne, a w niektórych sytuacjach niemal niezastąpione. Szczegółowy
opis ulepszeń w interfejsach API i działaniu mechanizmów znajdziesz, jak zawsze,
w dokumentacji Androida. W ten sposób doszliśmy do końca książki! Zacząłeś od
utworzenia aplikacji HelloAndroid w rozdziale 1., przebrnąłeś przez wiele zaawan-
sowanych zagadnień i ponad 80 technik, a na końcu zapoznałeś się z tabletami.
W tym momencie masz bardzo solidne podstawy do tworzenia aplikacji na Android.
Możesz sobie pogratulować!
664 ROZDZIAŁ 15. Pisanie aplikacji na tablety z Androidem
Dodatek A
Narzędzia do debugowania

W tym dodatku
Q Narzędzie ADB
Q Dostęp do dzienników systemowych
Q Klasa StrictMode

Omówiliśmy wiele zagadnień — mamy nadzieję, że na tyle szczegółowo, iż zna-


lazłeś odpowiedzi na większość nurtujących Cię pytań. Chcemy jednak poruszyć
także kilka innych tematów. W dodatkach przedstawiamy przydatne informacje
dotyczące kwestii, o których tylko napomknęliśmy w kontekście technik i dla
których nie znaleźliśmy miejsca w innych fragmentach książki.
Znajdziesz tu cztery dodatki (od A do D). Każdy jest pomyślany jako nieza-
leżny tekst, dlatego kolejność ich czytania nie ma znaczenia. Uważamy jednak, że
prezentujemy je w najbardziej naturalnym porządku.
Dodatek A zaczynamy od zaawansowanych porad i sztuczek z obszaru debu-
gowania. Wracamy do narzędzia ADB (ang. Android Debug Bridge), a także do
nowszego dodatku do pakietu SDK, klasy StrictMode. W dodatku B przedsta-
wiamy Android z zupełnie nowej perspektywy. Pokazujemy tu nowe podejście
do programowania (oparte na widokach WebView i JavaScripcie), a nawet całko-
wicie nowe języki, na przykład Scalę. W dodatku C znajdziesz informacje o przy-
datnym, choć niedocenianym narzędziu do optymalizacji — ProGuardzie.
W ostatnim dodatku omawiamy inną aplikację, która wciąż nie zyskała należnej jej
popularności — narzędzie Monkeyrunner, służące do automatyzacji zadań

665
666 DODATEK A Narzędzia do debugowania

w Androidzie za pomocą skryptów. Niniejszy dodatek zaczynamy od ponownego


przyjrzenia się programowi ADB.

A.1. Narzędzie ADB


W czasie pisania i debugowania aplikacji na Android programiści często korzy-
stają z narzędzia ADB. Jest to program typu klient-serwer, umożliwiający interak-
cje z urządzeniem (emulatorem) z Androidem (dalej używamy określenia urzą-
dzenie) z poziomu wiersza poleceń. Ujmijmy to dokładniej — ADB pozwala
wykryć i wymienić podłączone urządzenia oraz uruchamiać polecenia bezpo-
średnio z poziomu powłoki. W dalszych punktach przedstawiamy typowe scena-
riusze stosowania narzędzia ADB.

A.1.1. Interakcja z urządzeniami


Jeśli korzystasz z emulatora lub podłączyłeś do komputera urządzenie z wykorzy-
staniem portu USB, możesz zastosować narzędzie ADB do wykrywania urzą-
dzeń i łączenia się z nimi. Na przykład po podłączeniu telefonu Nexus One do
komputera, na którym działa też emulator Androida, za pośrednictwem instrukcji
adb devices można wyświetlić wszystkie dostępne urządzenia z Androidem. Poja-
wią się wtedy następujące informacje:
$ adb devices
List of devices attached
emulator-5554 device
HT019P801932 device

DEBUGOWANIE PRZEZ PORT USB. Jeśli urządzenie jest podłączone


do portu USB komputera i włączone, jednak nie pojawia się na liście dostęp-
nych urządzeń, prawdopodobnie w urządzeniu wyłączono debugowanie
przez port USB. Możesz je włączyć w ustawieniach urządzenia. Wybierz
Settings/Applications/Development i zaznacz opcję USB Debugging.
JEŚLI URZĄDZENIE JEST NIEWIDOCZNE… Jeżeli urządzenie nie
pojawia się nawet po włączeniu wspomnianej opcji, prawdopodobnie
wystąpił błąd w ADB. Czasem się to zdarza — nie jest to powód do zmar-
twień. W takiej sytuacji wywołaj najpierw instrukcję adb kill-server.
Następnie polecenie adb devices pozwala ponownie uruchomić serwer;
brakujące urządzenie powinno pojawić się na liście.
Narzędzie ADB identyfikuje urządzenia na podstawie numeru porządkowego.
Dla używanego tu telefonu numer to HT019P801932. Uruchamiane emulatory
automatycznie otrzymują numer porządkowy składający się z przedrostka emulator-
i numeru portu, pod którym odbierają instrukcje. Serwer ADB zajmuje porty
w parach. Numery parzyste to porty do przyjmowania instrukcji (z dalszego punktu
dowiesz się, jak działają), a nieparzyste służą do obsługi połączeń z ADB. Tak
więc drugi emulator otrzymałby numer porządkowy emulator-5556.
A.1. Narzędzie ADB 667

Jeśli chcesz komunikować się z konkretnym urządzeniem za pomocą narzędzia


ADB, możesz określić docelowe urządzenie przez podanie numeru porządko-
wego z opcją –s:
$adb -s HT019P801932 [dalsze opcje]

Jeśli podłączone są tylko jedno urządzenie i jeden emulator (tak jak w omawianej
sytuacji), można wskazać je w prostszy sposób, używając opcji –d i –e do wybrania
jedynego podłączonego fizycznego urządzenia (-d) lub emulatora (-e). Jeżeli
podłączone jest tylko jedno urządzenie (nieważne, czy jest nim emulator, czy
fizyczne urządzenie), nie trzeba go wskazywać. Narzędzie ADB łączy się wtedy
ze znalezionym urządzeniem.
Kiedy ADB zna już docelowe urządzenie, można wykonywać na nim różne
operacje, na przykład kopiować pliki, instalować i usuwać aplikacje, ponownie
uruchomić urządzenie lub otworzyć w nim powłokę poleceń. Aby zobaczyć pełną
listę dostępnych instrukcji i opcji, wpisz polecenie adb bez żadnych argumentów.
Warto zauważyć, że większość instrukcji bezpośrednio przekazywanych do ADB
to skróty wywołań, które normalnie uruchamiane są w powłoce poleceń urządze-
nia. Na przykład instrukcja adb install to skrót wywołania menedżera pakietów
z urządzenia, pm, i odpowiada poleceniu adb shell pm install. Dlatego w następ-
nym punkcie dokładnie wyjaśniamy, jak korzystać z powłoki urządzenia.

A.1.2. Korzystanie z powłoki urządzenia


Jedną z najwartościowszych cech narzędzia ADB jest to, że zapewnia dostęp
do powłoki poleceń Androida. W rozdziale 1. wyjaśniliśmy już, że Android wyko-
rzystuje zmodyfikowane jądro Linuksa. Nie trzeba chyba wspominać, że każdy
porządny Linux udostępnia powłokę poleceń. Niestety, Android nie obejmuje
bardzo popularnej i rozbudowanej powłoki Bourne-Again Shell (lepiej znanej
jako bash), która występuje w niemal każdym współczesnym komputerze z sys-
temem Linux lub Mac OS X. Zamiast tego dostępna jest minimalistyczna powłoka
z niewielkim podzbiorem aplikacji systemu operacyjnego używanych w typowym
środowisku GNU/Linux. Można używać na przykład instrukcji cd, ls i mv, jednak
inne popularne polecenia, takie jak cp, more, less i file, są niedostępne.
Dostęp do powłoki urządzenia można uzyskać z zastosowaniem instrukcji adb
shell. Korzystać z powłoki można na dwa sposoby — przy użyciu jednowierszo-
wych instrukcji i przez przejście w tryb interaktywny, w którym można wysyłać
wiele instrukcji jedna po drugiej. Jeśli chcesz na przykład wyświetlić wszystkie
pliki z bieżącego katalogu, możesz to zrobić w jednym wierszu (zakładamy, że
podłączone jest tylko jedno urządzenie):
$adb shell ls
config
cache
sdcard
...
668 DODATEK A Narzędzia do debugowania

Instrukcja ta oznacza: „Za pomocą ADB otwórz powłokę bieżącego urządzenia,


wywołaj instrukcję ls, a następnie wyświetl dane wyjściowe”. Powłokę można
też uruchomić bez przekazywania zdalnego polecenia:
$adb shell
# ls
config
cache
sdcard
...
# exit

Aby wyjść z powłoki urządzenia, należy użyć kombinacji Ctrl+D. Symbol # ozna-
cza, że powłoka jest uruchomiona dla głównego konta urządzenia (z dostępem
administracyjnym). W emulatorze zawsze uruchamiane jest takie konto, co daje
pełny dostęp do plików i katalogów. W telefonach i tabletach zwykle jest inaczej,
dlatego należy najpierw „zrootować” urządzenie, aby uzyskać dostęp do głównego
konta. Przed wykonaniem tej operacji do większości katalogów nie można nawet
zajrzeć.
Zainstalujmy przedstawioną w rozdziale 2. aplikację DealDroid na działającym
emulatorze, co pozwoli na późniejsze eksperymenty. Przejdź do katalogu z apli-
kacją i uruchom następujące polecenie:
$adb -e install -r bin/DealDroid.apk
663 KB/s (28308 bytes in 0.041s)
pkg: /data/local/tmp/DealDroid.apk
Success

Opcja –r wymusza ponowną instalację, jeśli pakiet już istnieje. Dobrze, aplikacja
znajduje się już w emulatorze. Zobaczmy, co można zrobić z wykorzystaniem
powłoki urządzenia.

A.1.3. Kontrolowanie środowiska wykonawczego w Androidzie


Kiedy aplikacja znajduje się już w urządzeniu, można uruchomić ją z poziomu
powłoki w zupełnie nowy sposób. Zazwyczaj należy przejść do ekranu z listą
aplikacji i wybrać ikonę odpowiedniego programu. Aplikacja jest wtedy urucha-
miana w zwykły sposób, co jest odpowiednie w standardowych sytuacjach. Co jed-
nak zrobić, jeśli chcemy zobaczyć, jak konkretna aktywność działa dla różnych
konfiguracji systemu lub po uruchomieniu za pomocą określonych parametrów
intencji? W pewnym stopniu można to sprawdzić programowo, używając testów
jednostkowych (zobacz rozdział 13.), jednak wymaga to pisania kodu, a czasem
programista chce tylko sprawdzić, czy dane rozwiązanie działa, lub wypróbować
funkcję. Przypominamy, że jeśli użytkownik nie wybrał na liście ofert żadnej
z nich, aktywność DealDetails wyświetla komunikat typu toast z informacjami
o błędzie (rysunek A.1).
A.1. Narzędzie ADB 669

Rysunek A.1. Tego komunikatu o błędzie


nie da się odtworzyć po uruchomieniu aplikacji
w normalny sposób. Jednak narzędzie Activity
Manager Androida pozwala uruchamiać konkretne
aktywności dla dowolnych danych przekazywanych
w intencjach

W standardowych warunkach trudno jest ręcznie przeprowadzać testy, ponieważ


zwykle do aktywności przekazywane są prawidłowe dane. Przydatna byłaby
możliwość uruchamiania poszczególnych aktywności z wykorzystaniem określo-
nych parametrów z intencji lub wykonywania aktywności bez wcześniejszego
wyświetlania innych ekranów, co pozwoliłoby testować scenariusze prowadzące
do problemów i odtwarzać je. Narzędzie Activity Manager to umożliwia
URUCHAMIANIE KOMPONENTÓW ZA POMOCĄ NARZĘDZIA ACTIVITY MANAGER
Narzędzie Activity Manager (am) można stosować do kontrolowanego urucha-
miania aplikacji, a nawet poszczególnych aktywności. Wróćmy do przykładowej
aktywności DealDetails. Możemy uruchomić ją bez wcześniejszego wyświetlania
aktywności DealList (i bez wybrania oferty):
$adb shell am start -n com.manning.aip.dealdroid/.DealDetails

Polecenie am start powoduje uruchomienie aktywności z podanymi danymi


z intencji. Tu bezpośrednio określamy uruchamiany komponent, do czego służy
opcja –n. Odpowiada to uruchomieniu aktywności w kodzie z użyciem nazwy
klasy, przez bezpośrednie określenie intencji. Za pomocą polecenia am start
można też pośrednio określić intencję poprzez zastosowanie różnych opcji (aby
wyświetlić ich listę, wywołaj polecenie am bez żadnych argumentów). Narzędzie
Activity Manager służy nie tylko do uruchamiania aktywności, ale też pozwala
uruchamiać usługi, rozsyłać intencje itd.
Jednym z wartych uwagi poleceń jest am instrument. W ten sposób można
korzystać z instrumentacji, na przykład z klasy InstrumentationTestCase, poza
środowiskiem Eclipse. Technikę tę stosuje się też w systemach budowania, takich
jak Ant i Maven (zobacz rozdział 14.). Daje ona duże możliwości w zakresie
670 DODATEK A Narzędzia do debugowania

wyboru testowanych elementów, czego wtyczka ADT nie zapewnia. Ujmijmy


to dokładniej — narzędzie Activity Manager pozwala uruchamiać testy o okre-
ślonych przypisach Javy, z konkretnego pakietu, a nawet z pojedynczej metody
testowej. Jeśli chcemy na przykład uruchomić tylko metodę testPreConditions
z klasy DealDetailsTest z rozdziału 13., możemy zrobić to w następujący sposób:
$adb shell am instrument -w -e class
com.manning.aip.dealdroid.test.DealDetailsTest#testPreConditions
com.manning.aip.dealdroid.test/android.test.InstrumentationTestRunner

com.manning.aip.dealdroid.test.DealDetailsTest:.
Test results for InstrumentationTestRunner=.
Time: 0.295

OK (1 test)

GDZIE JEST TO UDOKUMENTOWANE? Niestety, polecenie am instru


´ment nie wyświetla listy obsługiwanych parametrów. Jednak w komenta-
rzach JavaDoc klasy InstrumentationTestRunner znajduje się dobry przegląd
dostępnych opcji: http://mng.bz/s5r7.
Z uwagi na duże możliwości instrukcji am instrument systemy budowania w Andro-
idzie, na przykład androidowe wtyczki dla Mavena i Gradle’a, wykorzystują ją
do uruchamiania skomplikowanych testów.
MANIPULOWANIE WŁAŚCIWOŚCIAMI SYSTEMOWYMI
Zyskanie pełnej kontroli nad uruchamianiem aktywności i usług jest bardzo
przydatne, jednak działanie aplikacji zależy nie tylko od przekazanych w intencji
argumentów. Aplikacje czasem na przykład tłumaczy się na inne języki, dlatego
warto przetestować je w urządzeniu z ustawionym odpowiednim językiem. Nie-
stety, emulator udostępnia tylko kilka języków. Niedostępny jest na przykład
portugalski. Można jednak obejść ten problem. Android umożliwia kontrolowanie
wielu ustawień, w tym języka, poprzez listę globalnych właściwości systemowych.
Właściwości te są dostępne poprzez funkcję System.getProperty Javy. Właściwości
Javy to pary klucz-wartość, a właściwości systemowe Androida można bezpo-
średnio wczytywać i modyfikować za pomocą narzędzi getprop i setprop. Jeśli
programista chce zmienić język systemu na portugalski (pt_PT), może zrobić to tak:
$adb shell setprop persist.sys.language pt
$adb shell setprop persist.sys.country PT

Warto zauważyć, że właściwości systemowe są wczytywane tylko raz, w momen-


cie uruchamiania głównej maszyny wirtualnej Zygote Androida (zobacz roz-
dział 1.). Aby zmiany zostały wprowadzone, trzeba ponownie uruchomić emula-
tor (ręcznie lub z wykorzystaniem poleceń start i stop).
USTAWIANIE NIESTANDARDOWYCH JĘZYKÓW PRZY UŻYCIU
APLIKACJI DEV TOOLS. Istnieje też inny sposób na ustawienie języka
systemu. Służy do tego aplikacja Dev Tools dostępna w każdym emulatorze
A.1. Narzędzie ADB 671

Androida. Narzędzie to domyślnie nie jest dostępne w telefonach, dlatego


trzeba z wykorzystaniem instrukcji adb pull pobrać jego plik APK z emu-
latora i zainstalować aplikację w telefonie.
Listę aktualnych właściwości systemowych można wyświetlić przez wywołanie
instrukcji getprop bez argumentów. Za pomocą właściwości systemowych można
też kontrolować maszynę wirtualną Dalvik. Bardzo przydatna jest zwłaszcza
właściwość dalvik.vm.enableassertions. Jest to odpowiednik opcji –ea z Javy,
umożliwiający stosowanie słowa kluczowego assert w kodzie Javy. Wspomnianą
właściwość można włączyć tak:
$adb shell setprop dalvik.vm.enableassertions all

Jeśli uważamy, że przesłanie wartości null do aktywności DealDetails jest wyni-


kiem błędu programisty, a nie oczekiwaną sytuacją, którą należy obsłużyć, warto
dodać do tej aktywności asercję na etapie rozwijania i testowania programu (kod
oparty jest na listingu 2.11 z rozdziału 2.).
Item item = app.getCurrentItem();
assert( item != null );

Po włączeniu obsługi asercji uruchomienie aktywności bez wcześniejszego wybra-


nia elementu powoduje zgłoszenie błędu AssertionError przez środowisko uru-
chomieniowe i zatrzymanie aplikacji. W dzienniku można sprawdzić, który wiersz
kodu spowodował zgłoszenie błędu, i wprowadzić odpowiednie poprawki.
JEŚLI WŁAŚCIWOŚCI NIE SĄ ZACHOWYWANE… Android czasem
niepoprawnie wykonuje instrukcje setprop oraz start i stop. Jeżeli zauwa-
żysz, że Android „zapomina” ustawione w ten sposób właściwości, spróbuj
umieścić je w pliku /data/local.prop w urządzeniu. Każdą właściwość zapisz
w odrębnym wierszu w postaci <klucz> = <wartość> (na przykład dalvik.vm.
´enableassertions = all).
Wspomnieliśmy już, że błąd AssertionError, podobnie jak wszystkie nieprze-
chwycone wyjątki, jest rejestrowany w pliku dziennika systemowego. Do kwestii
tej wracamy dalej, jednak przed dokładniejszym omówieniem rejestrowania warto
zapoznać się z ostatnią techniką z tego punktu, dotyczącą manipulowania geolo-
kalizacją w urządzeniach z Androidem.
MANIPULOWANIE POŁOŻENIEM
Na początku stwierdziliśmy, że ADB to aplikacja typu klient-serwer działająca
w dwóch portach — porcie poleceń i porcie do obsługi połączenia z ADB. Pro-
gramistów interesuje głównie ten pierwszy, ponieważ można połączyć się z nim
przez telnet i wysyłać instrukcje bezpośrednio do urządzenia:
$telnet localhost 5554
Trying 127.0.0.1...
Connected to localhost.
672 DODATEK A Narzędzia do debugowania

Escape character is '^]'.


Android Console: type 'help' for a list of commands
OK

Jeśli emulator działa, instrukcja powoduje nawiązanie połączenia z portem pole-


ceń danego emulatora. Za pomocą telnetu można wprowadzić tekst do przesłania
i zakończyć go znakiem karetki (przez wciśnięcie klawisza Enter). Po wpisaniu
instrukcji help lub help <polecenie> pojawia się lista obsługiwanych instrukcji
lub opcje podanego polecenia. W omawianym kontekście najprzydatniejsza jest
instrukcja geo, umożliwiająca przesłanie do urządzenia dowolnych współrzęd-
nych GPS. Jeśli chcesz na przykład podać, że znajdujesz się na Mount Evereście,
możesz wpisać w oknie telnetu następujące polecenie:
geo fix 86.922 27.986 <ENTER>
OK

Przejdź do aplikacji Google Maps i zobacz, jakie są skutki wywołania tej instruk-
cji! Zwróć uwagę, że pierwszy argument to długość, a drugi — szerokość. Jeśli
aplikacja Google Maps poinformowała, że znajdujesz się gdzieś na oceanie, praw-
dopodobnie wprowadziłeś współrzędne w złej kolejności. Aby wrócić do połącze-
nia telnetowego, wybierz kombinację Ctrl+]. Instrukcja quit powoduje zamknięcie
połączenia.

A.1.4. Dostęp do dzienników systemowych


Jednym z najprzydatniejszych mechanizmów używanych przy debugowaniu jest
bez wątpienia plik dziennika aplikacji. Można w nim sprawdzić, co się stało.
W tym pliku zapisywany jest też zrzut stosu z nieprzechwyconymi wyjątkami,
dlatego warto zajrzeć do dziennika w trakcie debugowania.
Dziennik systemowy możesz śledzić w perspektywie DDMS w środowisku
Eclipse, jeśli jednak (tak jak i my) wolisz posługiwać się klawiaturą i konsolą,
możesz zastosować polecenie logcat Androida. Technikę tę dość szczegółowo
udokumentowano na stronie http://mng.bz/Tf31, tu jednak chcemy opisać jej naj-
ważniejsze aspekty i sposoby jej wykorzystywania.
Polecenie logcat, podobnie jak wszystkie wcześniej przedstawione instrukcje,
uruchamia aplikację działającą w urządzeniu, dostępną jednak poprzez program
ADB. Uruchomienie polecenia logcat bez podawania argumentów prowadzi do
wyświetlenia końcowej części głównego pliku dziennika (istnieją też dwa inne
pliki dziennika, dotyczące telefonii i zdarzeń, jednak nie omawiamy ich w tej
książce) i pojawiających się w nim nowych informacji. Podobnie działa instrukcja
tail –f z systemów GNU.
$adb logcat
I/DEBUG ( 31): debuggerd: Jun 30 2010 14:39:19
D/qemud ( 38): entering main loop
I/Vold ( 29): Vold 2.1 (the revenge) firing up
I/Netd ( 30): Netd 1.0 starting
...
A.1. Narzędzie ADB 673

Android rejestruje niemal wszystko — dane wyjściowe aplikacji, cykle mechani-


zmu przywracania pamięci, a także wywołania instrukcji System.out.println.
Warto zauważyć, że wyświetlany plik dziennika jest współużytkowany. Rejestrują
w nim dane wszystkie aplikacje korzystające z klasy Log Androida. Każdy wiersz
w pliku składa się z trzech części: poziom komunikatu, znacznika określającego
komponent, który zapisał dany wiersz, i komunikatu. Na przykład wywołanie
instrukcji Log.i("MyActivity", "To komunikat") prowadzi do zapisania w dzienniku
systemowym wiersza:
I/MyActivity ( 1824): To komunikat

Tu I (od ang. info) określa poziom komunikatu, a MyActivity to znacznik. Dalej


znajduje się identyfikator procesu aplikacji (1824) i sam komunikat. Instrukcja
logcat przyjmuje różne opcje. Jedną z najprzydatniejszych jest v, służąca do
kontrolowania formatu wpisów. Na przykład wywołanie logcat –v time powoduje
zapisywanie komunikatów z czasem ich dodania:
$adb logcat -v time
04-10 19:40:15.214 I/MyActivity ( 1824): To komunikat
...

Warto zapoznać się z wszystkimi opcjami. Znacznie upraszcza to pracę. Poziomy


komunikatów i znaczniki służą do porządkowania plików dziennika oraz umoż-
liwiają łatwe filtrowanie ich zawartości. Instrukcja logcat udostępnia wyrażenie
filtrujące, które pozwala wyświetlać elementy interesujące programistę. Takie
wyrażenia mają format znacznik:poziom. W ten sposób można wyświetlić komu-
nikaty o określonym znaczniku i z poziomu równego lub wyższego od podanego.
W technice tej wykorzystano fakt, że wszystkie poziomy komunikatów mają okre-
ślony priorytet. Informacje diagnostyczne mają na przykład niższy priorytet niż
komunikaty o błędach. Listę wszystkich poziomów przedstawiono w tabeli A.1.
Tabela A.1. Poziomy komunikatów z Androida w porządku rosnącym

Identyfikator Nazwa Priorytet

V Verbose (tryb pełny; wyświetlane są wszystkie informacje) 1 (najniższy)

D Debug (informacje diagnostyczne) 2

I Info (informacje) 3

W Warn (ostrzeżenia) 4

E Error (błędy) 5

F Fatal (błędy krytyczne) 6

S Silent (tryb „cichy”; nie są wyświetlane żadne informacje) 7 (najwyższy)

Jeżeli chcesz wyświetlić na przykład tylko błędy lub błędy krytyczne, możesz
wywołać polecenie logcat tak:
$adb logcat *:E
674 DODATEK A Narzędzia do debugowania

Oznacza to, że narzędzie ma wyświetlić tylko komunikaty typu E lub F wygene-


rowane przez dowolne komponenty (o dowolnych znacznikach). Zauważ, że
poziom S ma specjalne znaczenie. Sprawia, że nie są rejestrowane żadne infor-
macje, dlatego nie ma sensu mówić o zapisach z poziomu S.
Wyrażenia filtrujące można łączyć ze sobą. Aby zobaczyć komunikaty o błędach
z innych komponentów i informacje diagnostyczne z własnej aplikacji, można
użyć następującego wyrażenia:
$adb logcat MojaAplikacja:D *:E

Teraz pojawią się wpisy z dziennika zapisane przez aplikację z wykorzystaniem


znacznika MojaAplikacja. W specjalny sposób obsługiwane jest tu standardowe
wyjście. Jeśli w kodzie znajdują się wywołania System.out.println (nie zalecamy
stosowania tego podejścia; w zamian należy używać klasy Log), komunikaty
w pliku dziennika będą miały znacznik System.out. Niestety, w filtrach nie można
używać tego znacznika. W takiej sytuacji należy zastosować obejście w postaci
potokowego połączenia instrukcji logcat i grep (jeśli korzystasz z UNIX-a, praw-
dopodobnie znasz to rozwiązanie).
To już koniec omówienia programu ADB i powiązanych instrukcji. Wiesz, jak
z poziomu wiersza poleceń połączyć się z urządzeniami, jak zastosować powłokę
poleceń urządzenia do uzyskania większej kontroli nad uruchamianiem aplikacji,
a także jak korzystać z dziennika systemowego Androida. Możliwość manipulo-
wania aplikacją, śledzenia jej i kontrolowania z poziomu środowiska wykonawczego
jest bardzo przydatna, w jaki jednak sposób można diagnozować poszczególne
działania aplikacji, a zwłaszcza jej wydajność (lub problemy z nią)? Należy zasto-
sować klasę StrictMode!

A.2. Klasa StrictMode


Samo utworzenie aplikacji na Android to jedno, a zapewnienie jej szybkiego i płyn-
nego działania to zupełnie inna sprawa. Dlatego wraz z Androidem 2.3 w pakie-
cie Android SDK wprowadzono klasę StrictMode. Klasa ta pozwala wykryć wiele
problemów (na przykład obsługę sieci w wątku głównym) spowalniających pracę
aplikacji. Tryb ten uruchamiany jest programowo. Na listingu A.1 pokazano, jak
włączyć go w aplikacji DealDroid.

Listing A.1. Stosowanie klasy StrictMode w aplikacji DealDroid

public class DealDroidApp extends Application {


// Z uwagi na zwięzłość fragment kodu pominięto.
@Override
public void onCreate() {
/* Konfigurowanie zasad klasy StrictMode. */
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyDeath()
A.2. Klasa StrictMode 675

.build());
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyDeath()
.build());
super.onCreate();
this.cMgr = (ConnectivityManager)
this.getSystemService(Context.CONNECTIVITY_SERVICE);
this.parser = new DailyDealsXmlPullFeedParser();
this.sectionList = new ArrayList<Section>(6);
this.imageCache = new HashMap<Long, Bitmap>();
this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
}
}

Klasę StrictMode stosuje się programowo. W przykładzie robimy to w obiekcie


typu Application, jednak potrzebny kod można umieścić także w konkretnej
aktywności. Jeśli klasa używana jest w obiekcie typu Application, obowiązuje dla
wszystkich aktywności. W klasie StrictMode można skonfigurować dwa rodzaje
zasad. Pierwsza jest używana dla bieżącego wątku , którym tu jest główny wątek
interfejsu użytkownika. W kodzie ustawiamy opcję detectAll. Powoduje ona
sprawdzanie instrukcji odczytu i zapisu na dysku oraz operacji sieciowych. Dalej
konfigurujemy zasadę dla maszyny wirtualnej . Także dla niej ustawiamy opcję
detectAll. Pozwala to wykryć niektóre często popełniane błędy, które mogą pro-
wadzić do awarii aplikacji. Błędy te to na przykład niezamknięcie aktywności (co
może prowadzić do wyciekania dużej ilości pamięci), niezamknięcie baz danych i
kursorów lub niezwolnienie innych zasobów.
Po dodaniu nowego kodu do aplikacji można ją uruchomić. Natychmiast nastę-
puje wtedy awaria. Narzędzie logcat wyświetla następujące informacje:
StrictMode policy violation; ~duration=2689 ms:
android.os.StrictMode$StrictModeDiskReadViolation: policy=87 violation=2
...
at DealDroidApp.onCreate(DealDroidApp.java:93)

Komunikaty z klasy StrictMode początkowo wydają się niezrozumiałe, jednak


szybko można się do nich przyzwyczaić. Powyższy komunikat informuje, że apli-
kacja narusza zasadę dostępu do danych z dysku. Po przyjrzeniu się zrzutowi stosu
można stwierdzić, że problem występuje w wierszu 93. klasy DealDroidApp. Jest
to ostatni wiersz metody onCreate z listingu A.1, wczytujący domyślne ustawienia
współużytkowane aplikacji. Takie ustawienia są zwykle utrwalane w wewnętrz-
nej pamięci flash urządzenia, stąd naruszenie zasady. Możesz uznać, że problem
jest nieistotny. Jednak tu okazuje się, że operacja zajmuje 2689 milisekund, czyli
prawie 3 sekundy! Kod jest uruchamiany w emulatorze, dlatego działa wolniej
niż standardowo. Jeśli uznasz, że odczyt z dysku jest potrzebny, możesz skonfi-
gurować klasę StrictMode tak, aby ignorowała tę operację. Możesz też przepro-
wadzić refaktoryzację kodu. Tu ustawienia współużytkowane są potrzebne
676 DODATEK A Narzędzia do debugowania

tylko w usłudze, dlatego ich odczyt można przenieść z metody onCreate klasy
DealDroidApp, co pozwoli na szybsze uruchamianie programu. Warto też pamiętać
o usunięciu kodu związanego z klasą StrictMode przed udostępnieniem aplikacji.
Z klasy tej należy korzystać tylko w czasie programowania.

A.3. Podsumowanie
W pierwszym dodatku pokazaliśmy kilka sztuczek związanych z debugowa-
niem, które wykraczają poza stosowanie standardowego debugera ze środowiska
Eclipse. Wyjaśniliśmy, jak za pomocą programu ADB uzyskać większą kontrolę
nad urządzeniami z Androidem i środowiskiem wykonawczym, a także jak przy
użyciu klasy StrictMode sprawdzić wydajność aplikacji, aby wykryć błędy w rodzaju
uruchamiania kosztownych operacji w wątku interfejsu użytkownika.
Następny dodatek to gratka dla miłośników niestandardowych rozwiązań.
Wkraczamy w nim w świat programowania aplikacji sieciowych oraz nowych
języków programowania dla Androida.
Dodatek B
Niestandardowe techniki
tworzenia aplikacji
na Android

W tym dodatku
Q Korzystanie z widoków WebView
Q Stosowanie JavaScriptu
Q Języki programowania inne od Javy

Dla użytkowników smartfonów jedną z zalet Androida jest duży wybór urządzeń
z tym systemem. Możesz dobrać do własnych potrzeb wielkość ekranu, jego pro-
porcje itd. Podobnie sytuacja wygląda z perspektywy programistów. Android udo-
stępnia standardowy zestaw narzędzi programistycznych: pakiety SDK i NDK
Androida, wtyczkę ADT i inne rozwiązania. Pozwala to szybko zacząć pracę począt-
kującym programistom, a także ustalić standardowe podejście w dużych zespołach.
Dostępne są też jednak inne narzędzia. Możliwość wykonywania wszystkich
operacji z poziomu wiersza poleceń i stosowania otwartych systemów budowania,
takich jak Ant, pozwalają rozwijać aplikacje na Android także z wykorzystaniem
innych środowisk programistycznych, takich jak IntelliJ IDEA. Można pójść

677
678 DODATEK B Niestandardowe techniki tworzenia aplikacji na Android

jeszcze o krok dalej i całkowicie zrezygnować z Javy na rzecz technologii sie-


ciowych lub innych języków programowania. Przyjrzyjmy się obu tym podejściom.
Zaczynamy od przeglądu technologii sieciowych.

B.1. Używanie widoków WebView i JavaScriptu


Kilka razy napomknęliśmy o tym, że wiele aspektów programowania aplikacji na
Android przypomina rozwijanie aplikacji sieciowych. Dotyczy to zarówno prostych
kwestii (na przykład androidowa metoda findViewById jest podobna do metody
document.getElementById z JavaScriptu), jak i ważnych zagadnień (takich jak two-
rzenie stylów wizualnych i stosowanie ich do elementów interfejsu użytkownika).
Jeśli masz doświadczenie w programowaniu aplikacji sieciowych i wkraczasz
w świat tworzenia aplikacji na Android, wiele rozwiązań wyda Ci się znanych.
Jeżeli zamierzasz wykorzystać umiejętności z obszaru sieci do rozwijania progra-
mów na Android, warto zastanowić się nad zastosowaniem widoków WebView.
Android udostępnia klasę android.webkit.WebView, służącą do wyświetlania stron
internetowych. Jak wskazuje nazwa, klasa ta wyświetla materiały internetowe
za pomocą biblioteki WebKit. WebKit to biblioteka języka C++ o otwartym
dostępie do kodu źródłowego, a WebView i powiązane klasy można traktować jako
nakładki Javy na tę bibliotekę. Klasa WebView daje bardzo duże możliwości
w zakresie wyświetlania materiałów internetowych. WebView to widok, dlatego
można dowolnie określić jego wielkość i umieścić na ekranie wiele takich wido-
ków. Można też wczytać jeden widok WebView zajmujący cały ekran. Aplikacja
działa wtedy jak nakładka na stronę internetową.
Pojęcia strona internetowa używamy tu
w ogólnym znaczeniu. Można wczytać ją z inter-
netu lub z danego urządzenia. Można nawet
utworzyć widok WebView obejmujący wyświetlany
kod w HTML-u. Wszechstronne zastosowania
widoków WebView sprawiają, że są one wartościo-
wym narzędziem dla programistów aplikacji na
Android — także tych, którzy uważają, że pisa-
nie w JavaScripcie jest dla dzieci.
Widok WebView to standardowy komponent
Androida, a nie rozwiązanie naprędce opraco-
wane w celu wypełnienia luk we frameworku
do tworzenia aplikacji. Do strony internetowej
w widoku WebView można zastosować większość
interfejsów API Androida. Na rysunku B.1 przed-
stawiono przykładową aplikację z osadzonym
widokiem WebView.
Rysunek B.1. Osadzona
aplikacja sieciowa
B.1. Używanie widoków WebView i JavaScriptu 679

Aplikacja ta umożliwia użytkownikowi wybranie osoby z książki adresowej


i rysunku z telefonu. Rysunek ten jest następnie wyświetlany w widoku WebView.
Widać tu, że dzięki widokowi WebView możliwa jest interakcja między telefonem
a stroną internetową osadzoną w standardowej aplikacji. Na listingu B.1 poka-
zano, jak zapewnić tego rodzaju interaktywność.

Listing B.1. Używanie widoku WebView w aktywności

public class InterWebActivity extends Activity {

private static final int REQUEST_PIC = 5;


private static final int REQUEST_CONTACT = 4;
private static final String LOG_TAG = "InterWebActivity";
private WebView webView;
private InterWebInterface webInterface;
private static int onCreateCount = 0;
private int onResumeCount = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
webView = (WebView) findViewById(R.id.web);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url,
String message,JsResult result) {
Log.d(LOG_TAG, String.format(
"Komunikat JsAlert z widoku WebView = %s",
url, message));
return false;
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMsg) {
StringBuilder msg = new StringBuilder(consoleMsg
.messageLevel().name()).append('\t')
.append(consoleMsg.message()).append('\t')
.append(consoleMsg.sourceId()).append(" (")
.append(consoleMsg.lineNumber()).append(")\n");
if (consoleMsg.messageLevel() == ERROR) {
Log.e(LOG_TAG, msg.toString());
} else {
Log.d(LOG_TAG, msg.toString());
}
return true;
}
});
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view,
String url) {
Log.d(LOG_TAG, "Wczytywanie strony o adresie=" + url);
return false;
}
680 DODATEK B Niestandardowe techniki tworzenia aplikacji na Android

});
webInterface = new InterWebInterface();
webView.addJavascriptInterface(webInterface, "android");
webView.loadUrl("file:///android_asset/interweb.html");
onCreateCount++;
}

W kodzie z listingu B.1 najpierw włączamy obsługę JavaScriptu w osadzonym


w aktywności widoku WebView. Następnie tworzymy dla tego widoku obiekt klasy
WebChromeClient. Obiekt ten umożliwia przechwytywanie określonych zdarzeń
zachodzących w widoku. Tu przechwytujemy alerty JavaScriptu i operacje zapisu
do konsoli . Informacje te kierujemy do standardowego dziennika Androida.
Debugowanie osadzonych stron internetowych bywa trudne, ponieważ (inaczej
niż przy debugowaniu zwykłych aplikacji na Android) nie można korzystać ze
standardowego debugera Javy. Dlatego trzeba w dużym stopniu posiłkować się
rejestrowaniem danych w dzienniku, a najłatwiej robić to za pomocą obiektu
klasy WebChromeClient. Dalej tworzymy obiekt klasy WebViewClient. Działa on
podobnie jak obiekt klasy WebChromeClient, ponieważ także pozwala przechwy-
tywać zdarzenia z widoku WebView, w tym zdarzenia cyklu życia, na przykład
onPageFinished i onReceivedError. Tu przechwytujemy adres URL wczytywanej
strony . Można też przesłonić operację pobierania strony. Załóżmy, że chcemy
uruchamiać zewnętrzną przeglądarkę, zamiast wczytywać stronę do osadzonego
widoku WebView. Wymaga to użycia następującego kodu:
startActivity(new Intent(Intent.ACTION_GET, Uri.parse(url)));

Jednak w pierwotnej wersji rejestrujemy tylko adres URL wczytywanej strony.


Dalej znajduje się ciekawa operacja — tworzymy obiekt Javy i udostępniamy
go w środowisku uruchomieniowym JavaScriptu . Obiekt ten ma nazwę android,
dlatego w JavaScripcie można go używać, stosujące tę nazwę. Obiekt ten i sposób
korzystania z niego omawiamy dalej. W ostatnim kroku wczytujemy stronę .
W aplikacji jest to lokalna strona zapisana w katalogu /assets. Można też wczytać
zewnętrzną stronę, nie chcemy jednak pobierać danych z serwera WWW; zamiast
tego chcemy napisać kod aplikacji w językach HTML, CSS i JavaScript. Na
listingu B.2 znajdziesz kod wspomnianego obiektu Javy.

Listing B.2. Obiekt Javy dostępny w JavaScripcie

class InterWebInterface {
String callback;

public String getCreateCount() {


return String.valueOf(onCreateCount);
}

public String getResumeCount() {


return String.valueOf(onResumeCount);
}
B.1. Używanie widoków WebView i JavaScriptu 681

public String getUserName() {


AccountManager mgr = AccountManager.get(InterWebActivity.this);
Account gAccount = mgr.getAccountsByType("com.google")[0];
return gAccount.name;
}

public void selectContact(String callback) {


this.callback = callback;
Intent intentContact = new Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(intentContact, REQUEST_CONTACT);
}

public void selectPicture(String callback) {


this.callback = callback;
Intent intentPicture = new Intent(Intent.ACTION_GET_CONTENT);
intentPicture.setType("image/*");
startActivityForResult(intentPicture, REQUEST_PIC);
}

protected void executeContactCallback(Uri contact) {


String name = getContactDisplayName(contact);
webView.loadUrl(String.format("javascript:contactCallback('%s')",
name));
}

protected void executePicCallback(Uri picture) {


String filePath = getPictureData(picture);
File f = new File(filePath);
String uri = Uri.fromFile(f).toString();
webView.loadUrl(String.format("javascript:pictureCallback('%s')",
uri));
}
}

Listing B.2 jest dowodem na to, że w JavaScripcie można uzyskać dostęp tylko
do niektórych funkcji Androida. Tu za pomocą obiektu klasy AccountManager
udostępniamy nazwisko użytkownika . Następnie umożliwiamy wybranie da-
nych kontaktowych . Zauważ, że operacja ta wymaga uruchomienia drugiej
aktywności, dlatego żądanie jest asynchroniczne. Po otrzymaniu danych trzeba
przesłać je w wywołaniu zwrotnym do JavaScriptu . Używamy (nadużywamy?)
do tego metody loadURL klasy WebView, co pozwala bezpośrednio wykonywać kod
w JavaScripcie i uruchomić wywoływaną zwrotnie metodę, do której przekazu-
jemy dane otrzymane od zewnętrznej aktywności. Warto również zauważyć, że
istnieje też podobna metoda do przekazywania ścieżki do rysunku, a także dłuższe
metody do śledzenia stanu aktywności. Na listingu B.3 znajduje się kod w Java-
Scripcie, w którym wykorzystujemy opisany wcześniej obiekt.

Listing B.3. Aplikacja w JavaScripcie

<head>
<script type="text/javascript">
var initCount = 0;
682 DODATEK B Niestandardowe techniki tworzenia aplikacji na Android

function getContact(){
window.android.selectContact("contactCallback");
}
function contactCallback(contact){
document.getElementById("output").innerHTML = contact;
status();
}
function status(){
try{
var createCount = window.android.getCreateCount();
var resumeCount = window.android.getResumeCount();
document.getElementById("resume").innerHTML = resumeCount;
document.getElementById("create").innerHTML = createCount;
document.getElementById("init").innerHTML = initCount;
} catch (e) {
alert("Wyjątek w funkcji status: " + e.description)
}
}
function init(){
initCount++;
status();
}
function getPicture(){
window.android.selectPicture("pictureCallback");
}
function pictureCallback(url){
alert("Wczytywanie rysunku=" + url);
var img = document.getElementById("pic");
img.src = url;
img.height = "200";
img.width = "200";
}
</script>
</head>
<body onload="init()">
<div>
Zdarzenia onload: <span id="init"></span><br/>
Wznowienia: <span id="resume"></span><br/>
Operacje tworzenia: <span id="create"></span>
</div>
<input type="button" value="Wybierz kontakt" onclick="getContact()"/>
<br/>
<input type="button" value="Wybierz rysunek" onclick="getPicture()"/>
<div id="output">Identyfikator Uri</div>
<img id="pic" />
</body>
</html>

W kodzie z listingu B.3 wykorzystano wiele natywnych funkcji Androida, przy


czym zrobiono to na stronie internetowej napisanej w JavaScripcie. Strona ta
zawiera prosty przycisk Wybierz osobę. Jego dotknięcie powoduje wywołanie
metody selectContact klasy Javy przedstawionej na listingu B.2. Wyświetla
ona natywną aplikację do wybierania osób z listy kontaktów z urządzenia z Andro-
idem. Po wybraniu osoby jej nazwisko jest przekazywane z powrotem przez funk-
B.2. Języki programowania inne niż Java 683

cję contactCallback . W kodzie znajdują się też funkcje do korzystania z innych


metod zdefiniowanych w klasie Javy z listingu B.2.
W aplikacji sieciowej można wykonywać także wiele innych operacji. Taka
aplikacja może komunikować się ze zdalnym serwerem z wykorzystaniem obiektu
klasy XMLHttpRequest. Może też korzystać z mechanizmów języka HTML5, takich
jak element canvas, pamięć modelu DOM i geolokalizowanie. Cały interfejs
użytkownika można przy tym utworzyć za pomocą języków HTML, CSS i Java-
Script. Jednak w Androidzie można też napisać kod łączący poszczególne części
rozwiązania i umożliwić aplikacji sieciowej dostęp do wszystkich komponentów
z aplikacji natywnej.

B.2. Języki programowania inne niż Java


Kiedy po raz pierwszy poinformowano o udostępnieniu Androida, zastosowanie
w nim Javy spotkało się z różnym przyjęciem. Główną alternatywą w systemach
osadzonych jest kod natywny (na przykład w języku C, C++ lub Objective-C),
który można bezpośrednio skompilować na kod maszynowy. W porównaniu
z wymienionymi językami Java jest zaawansowana. Obsługuje na przykład przy-
wracanie pamięci (w pewnej postaci występuje ono także w języku Objective-C 2.0,
ale mechanizm ten nie jest jeszcze obsługiwany w świecie mobilnym). Wielu
programistów uważa Javę za zbyt rozwlekłą, narzucającą ograniczenia i prze-
starzałą. Istnieje wiele nowszych języków programowania o składni dającej więk-
sze możliwości. Na szczęście dla zwolenników takich rozwiązań Android nie
wymaga stosowania Javy. Niezbędny jest tylko kod bajtowy w Javie. Każdy język,
który można skompilować na kod bajtowy Javy, można następnie przekształcić na
kod bajtowy Dalvika i uruchomić na urządzeniu z Androidem. Przyjrzyjmy się
rysunkowi B.2 i zobaczmy, jak to działa.

Rysunek B.2. Kompilacja i pakowanie w Androidzie


684 DODATEK B Niestandardowe techniki tworzenia aplikacji na Android

Rysunek powinien wyglądać znajomo. Jest to rysunek 1.11 z początkowej części


książki. Ważny jest tu drugi prostokąt. Oznacza on pliki klas Javy przekształcane
na pliki .dex. Można więc uniknąć stosowania Javy, o ile napisany kod można
przekształcić na pliki klas Javy.
Okazuje się, że środowisko uruchomieniowe Javy (maszyna wirtualna Javy)
jest popularnym środowiskiem uruchomieniowym wielu nowych języków pro-
gramowania. Współczesne maszyny JVM są bardzo wydajne i zapewniają znako-
mite zarządzanie pamięcią. Z tego wynika ich popularność w dzisiejszych językach
programowania, takich jak Scala, Groovy, Clojure, Mirah i Fantom. Oprócz
tych nowych języków środowisko uruchomieniowe oparte na Javie (nazywane
JRuby) ma też Ruby. Dlatego możesz dodać także ten język do listy języków pro-
gramowania, które umożliwiają kompilację na kod bajtowy Javy. Dzięki tej kom-
pilacji wymienione języki można stosować do tworzenia aplikacji na Android.
Jak może domyśliłeś się na podstawie rysunku B.2, przy stosowaniu języka
programowania innego niż Java występuje pewna trudność. Związana jest ona
z procesem budowania. W kodzie aplikacji często potrzebne są komponenty
w rodzaju klasy R generowanej przez Android. Dlatego takie komponenty trzeba
wygenerować i skompilować przed kompilacją kodu aplikacji, w którym są
potrzebne. Następnie można przejść do procesu budowania przedstawionego
na rysunku B.2.
Zanim jednak zrezygnujesz z Javy na rzecz jednego z atrakcyjniejszych języ-
ków, pamiętaj, że ich stosowanie ma trzy istotne wady. Otóż wiele tych języków
obejmuje mechanizmy, które trzeba przekształcić na kod Javy przed etapem
kompilacji na kod bajtowy. Dotyczy to języków z rozwiązaniami, dla których nie
ma odpowiedników w Javie. Z języków tych często korzysta się właśnie z uwagi na
te rozwiązania. Ze względu na niezbędne przekształcenia kod w tych językach
z natury działa wolniej niż kod w Javie. Czasem nie ma to dużego znaczenia. Jeśli
w aplikacji na przykład większość czasu zajmuje oczekiwanie na dane z sieci,
wolniejsze działanie prawdopodobnie będzie niezauważalne. Długie operacje
i tak należy wykonywać poza głównym wątkiem interfejsu użytkownika.
Drugi poważny problem związany jest z tym, że inne języki często wymagają
więcej pamięci niż Java. Jest to odczuwalne zwłaszcza przy stosowaniu mecha-
nizmów, które trudno jest przekształcić na kod w Javie. Wiele języków udostęp-
nia na przykład domknięcia (ang. closure), czyli funkcje anonimowe, które można
przekazywać do innych funkcji i metod. Są one przydatne przy programowaniu
aplikacji na Android, ponieważ często trzeba w nich obsługiwać zdarzenia
w rodzaju dotknięć i gestów. Domknięcia często określane są funkcjami pierwszej
kategorii (ang. first-class function), natomiast Java ich nie obsługuje. Dlatego
w Javie trzeba zwykle przeprowadzić skomplikowane przekształcenia i utworzyć
klasę Javy, który działa jak kontener na funkcję. Tak więc zastosowanie domknię-
cia zwykle wymaga zdefiniowania klasy, jej skompilowania, a następnie utwo-
rzenia obiektu tej klasy. Prowadzi to do niejawnego zajęcia dużej ilości pamięci.
B.2. Języki programowania inne niż Java 685

Co gorsza, pamięć używaną dla klas (w odróżnieniu od pamięci dla ich egzempla-
rzy) znacznie trudniej jest odzyskać. Dlatego aplikacja nie tylko zajmuje więcej
pamięci — ponadto części z niej nie da się odzyskać. Pamięć jest jednym z ogra-
niczonych zasobów w środowisku mobilnym, dlatego sytuacja ta jest poważnym
problemem.
Ostatnią kwestią, o której należy pamiętać, jest to, że inne języki zwykle mają
własną bibliotekę uruchomieniową. Jest to standardowa biblioteka z klasami
i funkcjami, które zawsze powinny być dostępne. Zwykle obejmuje standardowe
struktury danych, biblioteki do obsługi wejścia-wyjścia, a nawet operacji siecio-
wych. Wszystkie standardowe biblioteki trzeba rozpowszechniać wraz z aplikacją.
Może to prowadzić do znacznego zwiększenia jej wielkości. Pomocny w tym
zakresie jest ProGuard, opisany w dodatku C. Ponadto wraz ze wzrostem zaawan-
sowania urządzeń z Androidem wielkość aplikacji staje się mniejszym problemem,
ponieważ użytkownikom rzadko brakuje miejsca na programy (w pierwszym
roku istnienia Androida był to standardowy problem). Jednak opisana sytuacja ma
też inne irytujące skutki uboczne. Biblioteka uruchomieniowa jest zwykle udostęp-
niana jako archiwum JAR z plikami klas. Wszystkie te pliki trzeba przekształcić na
format DEX przy każdym budowaniu aplikacji. Może to prowadzić do znacznego
wydłużenia czasu budowania. Na pozór jest to drobny problem, jednak może
znacznie utrudnić proces rozwijania aplikacji.
Z uwagi na opisane trudności ze stosowaniem nietypowych języków do two-
rzenia aplikacji na Android w wielu językach dodano mechanizmy ułatwiające
rozwijanie takich programów. Na przykład z projektem JRuby związany jest
projekt Ruboto, którego autorzy chcą ułatwić stosowanie języka Ruby do two-
rzenia aplikacji na Android. Innym popularnym językiem jest Scala. W poświę-
conej mu witrynie znajduje się wiele wskazówek dotyczących dostosowywania
procesu budowania w Ancie do możliwości tego języka. Ponadto w wersji Scala 2.9
usprawniono kompilator, aby ułatwić definiowanie statycznych pól z modyfi-
katorem final, takich jak pole CREATOR wymagane w implementacji interfejsu
Parcelable w Androidzie. W Scali nie ma pól statycznych, ale niektóre mechani-
zmy języka pozwalają utworzyć ich odpowiednik. Jednak w wersjach starszych
niż 2.9 generowany kod bajtowy nie był odpowiednikiem pól statycznych z Javy.
Na listingu B.4 pokazano, jak wygląda obiekt typu Parcelable w Scali.

Listing B.4. Obiekt typu Parcelable w Scali

import android.os.{Parcelable, Parcel}

class Stock (val symbol:String, var maxPrice:Double, var minPrice:Double,


val pricePaid:Double, var quantity:Int, var name:String,
var currentPrice:Double) extends Parcelable{
var id = 0

def this(in:Parcel) = this(in.readString, in.readDouble, in.readDouble,


in.readDouble, in.readInt,in.readString,
in.readDouble)
686 DODATEK B Niestandardowe techniki tworzenia aplikacji na Android

def describeContents = 0
def writeToParcel(parcel:Parcel, flags:Int){
parcel.writeString(symbol)
parcel.writeDouble(maxPrice)
parcel.writeDouble(minPrice)
parcel.writeDouble(pricePaid)
parcel.writeInt(quantity)
parcel.writeDouble(currentPrice)
parcel.writeString(name)
}
}

object Stock{
final val CREATOR = new Parcelable.Creator[Stock](){
def createFromParcel(in:Parcel) = new Stock(in)
def newArray(size:Int) = new Array[Stock](size)
}
}

Jak widać, w Scali można pominąć dużą część szablonowego kodu, który trzeba
pisać w Javie. Jednak brak zmiennych statycznych w Scali ma określone konse-
kwencje. Obiekt w Scali jest singletonem. Dlatego jego pola i metody mogą dzia-
łać w podobny sposób, jak pola statyczne i metody w Javie (dlatego pole CREATOR
znajduje się w obiekcie Stock, a nie w klasie o tej nazwie). Wspomnieliśmy już,
że rozwiązanie to nie działa w wersjach starszych niż Scala 2.9, gdzie obiektów
Scali nie można używać jako obiektów typu Parcelable. Na szczęście współ-
działanie między Javą i Scalą sprawia, że można pisać obiekty typu Parcelable
w Javie, a cały pozostały kod — w Scali (przy czym od Scali 2.9 kompilator tego
języka przekształca pole CREATOR ze Scali na kod bajtowy będący odpowiedni-
kiem pola statycznego z Javy, tak więc obiekty typu Parcelable można tworzyć
także w Scali).
To tylko jeden przykład stosowania niestandardowych języków w Androidzie.
Używanie ich do tworzenia aplikacji na Android jest coraz prostsze. Języki te
oparte są na różnych paradygmatach programowania, co programista może wyko-
rzystać. To następny dowód na to, że otwartość Androida zapewnia wiele moż-
liwości. Możesz stosować dowolne narzędzia, technologie sieciowe, a nawet języki
programowania.
Dodatek C
ProGuard

Nie trzeba wspominać, że wysoka wydajność jest ważna i trudna do uzyskania.


Nawet po spędzeniu wielu godzin na profilowaniu kodu można znaleźć proste
sposoby na optymalizację wydajności, a także na zwiększenie jego odporności
na ataki hakerów i inżynierię odwrotną. Osoby o dużym doświadczeniu w korzy-
staniu z Javy pewnie znają przydatne narzędzie, które to umożliwia. Poznaj
ProGuard.

C.1. Wprowadzenie
W rozdziale 14. kilkukrotnie wspomnieliśmy o ProGuardzie w kontekście zarzą-
dzania budowaniem i automatyzacji tego procesu. Nie wyjaśniliśmy jednak, do
czego służy ProGuard, a także kiedy i jak należy stosować to narzędzie. W tym
podrozdziale uzupełniamy te informacje. ProGuard przetwarza pliki klas Javy
i wykonuje przy tym dwa przydatne zadania:
Q sprawia, że aplikacja jest mniejsza i szybsza;
Q utrudnia zastosowanie inżynierii odwrotnej.
Warto wspomnieć, że ProGuard nie jest przeznaczony tylko dla Androida. Narzę-
dzie to jest znacznie starsze niż sam Android. Jest jednak dostępne w pakiecie
SDK (znajdziesz je w katalogu ANDROID_HOME/tools/proguard). Dokumentacja
ProGuarda znajduje się na stronie http://proguard.sourceforge.net/.
Nie trzeba wyjaśniać, że programiści chcą tworzyć małe i szybkie aplikacje.
ProGuard potrafi zmniejszyć i zoptymalizować klasy przez przetwarzanie kodu
bajtowego na wiele sposobów. Narzędzie to usuwa nieużywany kod, rozwija

687
688 DODATEK C ProGuard

metody w miejscu wywołania, scala hierarchie klas, dodaje modyfikatory final


i static wszędzie tam, gdzie to możliwe, a także wprowadza drobne optymalizacje
w rodzaju upraszczania operacji arytmetycznych lub sterowania przepływem.
Choć ochrona przed inżynierią odwrotną może być przydatna w każdej
aplikacji, warto dokładniej opisać tę kwestię. Jeśli nie obawiasz się, że inni pro-
gramiści poznają wewnętrzne mechanizmy Twojej aplikacji, nie musisz chronić
się przed inżynierią odwrotną, czyli przekształceniem programu z powrotem na
kod źródłowy (jest to możliwe przez przekształcenie kodu bajtowego Dalvika na
kod bajtowy Javy, a następnie na kod źródłowy Javy). Jednak w niektórych sytu-
acjach ochrona kodu jest ważna. Oto przykładowe sytuacje tego rodzaju:
Q w klasach Javy są poufne informacje zapisane na stałe, na przykład hasła;
Q kod źródłowy jest zastrzeżoną własnością intelektualną, której nie należy
udostępniać światu;
Q chcesz zapobiec obejściu sprawdzania licencji lub zabezpieczeń
przez programistów, którzy mogliby ponownie skompilować aplikację
po wyłączeniu mechanizmu sprawdzającego (jest to istotne na przykład
przy pobieraniu za pomocą usługi licencyjnej Androida opłat za ściągnięcie
aplikacji).
ProGuard pomaga w zaciemnianiu nazw klas, metod i pól, a także w usuwaniu
informacji strukturalnych, takich jak tabele nazw plików i numerów wierszy. To
sprawia, że modyfikowanie kodu z wykorzystaniem inżynierii odwrotnej jest
praktycznie niemożliwe. Narzędzie wygląda na ciekawe i przydatne, jednak
trzeba umieć je stosować. W tym dodatku pokazujemy, jak skonfigurować projekty
Androida, tak aby można było przetwarzać je przy użyciu ProGuarda. Wyjaśniamy
też, jak uruchamiać ProGuarda w procesie budowania, a także — co najważ-
niejsze — jak przygotować odpowiednie reguły tego narzędzia dla projektu i na
jakie pułapki uważać.

C.2. Stosowanie ProGuarda


Możliwe, że zastanawiałeś się, do czego służy plik proguard.cfg generowany
w każdym projekcie utworzonym za pomocą kreatora projektów wtyczki ADT
(plik ten jest zawarty w katalogu głównym projektu). To w tym pliku znajdują się
opcje i reguły określające, w jaki sposób ProGuard ma przetwarzać aplikację.
Kreator na szczęście nie tworzy pustego pliku, ale generuje sensowne informacje
domyślne, od których można rozpocząć pracę. W dalszych punktach dowiesz
się, jak działają reguły ProGuarda. Jedyną rzeczą, jaką trzeba zrobić, jest poinfor-
mowanie wtyczki ADT o tym, gdzie znajduje się konfiguracja narzędzia. W tym
celu należy umieścić w pliku default.properties poniższy kod:
proguard.config=proguard.cfg
C.2. Stosowanie ProGuarda 689

Jeśli plik z konfiguracją ProGuarda nie znajduje się w katalogu głównym pro-
jektu, trzeba odpowiednio zmodyfikować ścieżkę. To wystarczy, aby poinformo-
wać wtyczkę ADT, że przed utworzeniem archiwum APK pliki klas mają zostać
przetworzone przez ProGuard. Warto jednak zauważyć, że narzędzie to działa
tylko przy budowaniu wersji produkcyjnych, na przykład przy tworzeniu pliku
APK przez kliknięcie projektu prawym przyciskiem myszy i wybranie opcji
Android Tools/Export Signed Application Package. Ma to sens, ponieważ
w czasie rozwijania aplikacji ProGuard tylko utrudnia pracę. Debugowanie
metod o zaciemnionych nazwach jest jak szukanie w ciemnościach igły w sto-
gu siana. Niestety, problem ten dotyczy także analizowania raportów o błędach
zgłaszanych przez aplikacje zmodyfikowane z wykorzystaniem ProGuarda. W pod-
rozdziale C.4 opisujemy, jak radzić sobie z takimi sytuacjami.
W rozdziale 14. dokładnie wyjaśniliśmy, dlaczego warto zarządzać budowa-
niem z wykorzystaniem Anta lub Mavena. Do rozwiązania pozostaje kwestia tego,
jak uruchamiać ProGuard, jeśli wersje produkcyjne nie są budowane za pomocą
wtyczki ADT. Dla Mavena dostępna jest odpowiednia wtyczka; jej stosowanie
w połączeniu z androidową wtyczką Mavena opisano na stronie http://mng.bz/9uKq.
Obecnie ProGuard jest w pełni zintegrowany ze standardowym łańcuchem
androidowych narzędzi. W zadaniach Anta można zdefiniować prywatne zadanie
–obfuscate, które powoduje wywołanie ProGuarda przy tworzeniu wersji release.
Na listingu C.1 znajdziesz informacje wyświetlane po uruchomieniu instrukcji
ant release dla projektu HelloAnt z rozdziału 14. (dane wyjściowe skrócono).

Listing C.1. Dane wyjściowe z ProGuarda wywołanego dla aplikacji HelloAnt

matthias:[HelloAnt]$ ant clean release


...
-obfuscate:
[mkdir] Created dir: /Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard
[jar] Building jar: /Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard/original.jar
[proguard] ProGuard, version 4.4
...
[proguard] Reading input...
[proguard] Reading program jar [/Users/matthias/Projects/eclipse/HelloAnt/
bin/proguard/original.jar]
[proguard] Reading program jar [/Users/matthias/Projects/eclipse/HelloAnt/
libs/commons-lang-2.5.jar]
[proguard] Reading library jar [/Users/matthias/Library/Development/android-
sdk-mac_86/platforms/
android-8/android.jar]
[proguard] Reading library jar [/Users/matthias/Library/Development/android-
sdk-mac_86/
add-ons/addon_google_apis_google_inc_8/libs/maps.jar]
[proguard] Initializing...
[proguard] Note: the configuration refers to the unknown class
'com.android.vending.licensing.ILicensingService'
690 DODATEK C ProGuard

[proguard] Note: there were 1 references to unknown classes.


[proguard] You should check your configuration for typos.
[proguard] Ignoring unused library classes...
[proguard] Original number of library classes: 2696
[proguard] Final number of library classes: 230
[proguard] Printing kept classes, fields, and methods...
[proguard] Shrinking...
[proguard] Printing usage to [/Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard/usage.txt]...
[proguard] Removing unused program classes and class elements...
[proguard] Original number of program classes: 139
[proguard] Final number of program classes: 2
[proguard] Optimizing...
[proguard] Number of finalized classes: 1
[proguard] Number of vertically merged classes: 0 (disabled)
[proguard] Number of horizontally merged classes: 0 (disabled)
[proguard] Number of removed write-only fields: 0 (disabled)
[proguard] Number of privatized fields: 0 (disabled)
[proguard] Number of inlined constant fields: 0 (disabled)
[proguard] Number of privatized methods: 0
[proguard] Number of staticized methods: 0
[proguard] Number of finalized methods: 0
[proguard] Number of removed method parameters: 0
[proguard] Number of inlined constant parameters: 1
[proguard] Number of inlined constant return values: 0
[proguard] Number of inlined short method calls: 0
[proguard] Number of inlined unique method calls: 1
[proguard] Number of inlined tail recursion calls: 0
[proguard] Number of merged code blocks: 0
[proguard] Number of variable peephole optimizations: 2
[proguard] Number of arithmetic peephole optimizations: 0 (disabled)
[proguard] Number of cast peephole optimizations: 0
[proguard] Number of field peephole optimizations: 0
[proguard] Number of branch peephole optimizations: 0
[proguard] Number of simplified instructions: 12
[proguard] Number of removed instructions: 20
[proguard] Number of removed local variables: 0
[proguard] Number of removed exception blocks: 0
[proguard] Number of optimized local variable frames: 3
...
[proguard] Obfuscating...
[proguard] Printing mapping to [/Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard/mapping.txt]...
[proguard] Writing output...
[proguard] Preparing output jar [/Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard/obfuscated.jar]
[proguard] Copying resources from program jar [/Users/matthias/Projects/
eclipse/
HelloAnt/bin/proguard/original.jar]
[proguard] Copying resources from program jar [/Users/matthias/
Projects/eclipse/HelloAnt/libs/commons-lang-2.5.jar]
[proguard] Warning: can't write resource [META-INF/MANIFEST.MF]
(Duplicate zip entry [commons-lang-2.5.jar:META-INF/MANIFEST.MF])
[proguard] Printing classes to [/Users/matthias/Projects/eclipse/
HelloAnt/bin/proguard/dump.txt]...
C.2. Stosowanie ProGuarda 691

Kiedy ProGuard jest wywoływany w androidowych narzędziach (jako cel Anta lub
we wtyczce ADT), zapisuje cztery różne pliki dziennika . Ważne są przede
wszystkim trzy z nich:
Q Plik mapping.txt — obejmuje odwzorowanie z zaciemnionych nazw
na pierwotne nazwy. Archiwizuj ten plik dla każdej wersji produkcyjnej!
Będzie przydatny przy analizowaniu raportów o błędach zgłaszanych
przez aplikacje z zaciemnionym kodem (zobacz podrozdział C.4).
Q Plik seeds.txt. Lista punktów wejścia do aplikacji zidentyfikowanych przez
ProGuard. Dalej wyjaśniamy, dlaczego lista ta jest ważna.
Q Plik usage.txt. Lista klas, pól i metod, które ProGuard usunął, ponieważ
wykrył, że nie są używane. Plik ten pozwala zobaczyć, w jaki sposób
modyfikacje reguł ProGuarda wpływają na etap zmniejszania aplikacji.
Jeśli na liście znajdują się używane jednostki kodu, reguły te są zbyt
agresywne. Jeżeli na liście brakuje kodu, który nie jest wywoływany,
reguły są zbyt łagodne.
GDZIE ZNAJDUJĄ SIĘ PLIKI DZIENNIKA WYGENEROWANE
PRZEZ PROGUARD? Zauważ, że choć cel Anta zapisuje pliki w katalogu
bin/proguard/, to wtyczka ADT (po kliknięciu projektu prawym przyciskiem
myszy i wybraniu opcji Android Tools/Export Signed Application Package)
umieszcza je w katalogu proguard/.
SKĄD PROGUARD WIE, KTÓRE PLIKI MA PRZETWARZAĆ? W jaki
sposób narzędzie ustala, które pliki ma przetwarzać — czy ma na przykład
uwzględniać także plik android.jar i inne biblioteczne archiwa JAR?
Zwykle określa się to za pomocą opcji –injars i –libraryjars. Zarówno
wtyczka ADT, jak i zadania Anta ustawiają potrzebne pola automatycz-
nie na podstawie ścieżki klas projektu, a także katalogów na dane wyj-
ściowe i libs/.
Na listingu widać też, że ProGuard działa w trzech krokach. Skraca kod aplikacji ,
przeprowadza optymalizację (krok ten może być wielokrotnie powtarzany)
i zaciemnia kod . Wszystkie te etapy są opcjonalne i można je pominąć, uży-
wając opcji –dontshrink (wyłącza skracanie kodu), -dontoptimize (wyłącza optyma-
lizację) i –dontobfuscate (wyłącza zaciemnianie). Zwykle lepiej jest uruchomić
wszystkie trzy etapy i dostosować reguły do potrzeb. Na przykład optymalizacja
kodu to zwykle dobry pomysł, jednak nadmierna optymalizacja może prowadzić
do zakłócenia pracy aplikacji, dlatego warto zachować ostrożność.
Zobaczmy, jaką strukturę mają pliki konfiguracyjne ProGuarda, a także jak
pisać reguły i na co zwracać przy tym uwagę.
692 DODATEK C ProGuard

C.3. Reguły ProGuarda


ProGuard to rozbudowane narzędzie o wielu opcjach. Należy pamiętać, że nie
istnieje jedna recepta na sukces. Choć niektóre reguły i opcje są standardowe dla
wielu aplikacji na Android, zachęcamy do tego, aby nie stosować ich w automa-
tyczny sposób (dotyczy to także reguł opisanych w tym podrozdziale). Zamiast
tego należy dostosowywać konfigurację do wymagań aplikacji. Dany zestaw reguł
może doskonale nadawać się do modyfikowania kodu klienta Twittera, ale zupeł-
nie nie pasować do przekształcania kodu gry.
Ponieważ ProGuard ma tak wiele opcji, a ponadto stosuje zaawansowaną,
opartą na wzorcach składnię do wykrywania elementów kodu Javy (na przykład
nazw klas i metod), początkowo może przytłaczać. Dlatego tworzymy prostą przy-
kładową aplikację i przetwarzamy ją przy użyciu omawianego narzędzia. Apli-
kacja ta nie ma żadnego konkretnego przeznaczenia. Wykonuje tylko kilka ope-
racji, które trzeba odpowiednio potraktować w czasie przekształcania kodu za
pomocą ProGuarda. Zaczynamy od kilku prostych ustawień, które — co zaskaku-
jące — nie są właściwe i powodują awarię aplikacji, ponieważ ProGuard zbyt
agresywnie skraca i optymalizuje kod. Następnie dodajemy krok po kroku dalsze
reguły do momentu, w którym program znów zaczyna działać. Mamy nadzieję,
że przykład pozwoli Ci łatwiej zrozumieć i przyswoić reguły ProGuarda.
POBIERZ PROJEKT PROGUARDED. Kod źródłowy projektu do uru-
chamiania aplikacji znajdziesz w witrynie z kodem do książki Android
w praktyce. Ponieważ niektóre listingi skrócono, abyś mógł skoncentrować
się na konkretnych zagadnieniach, zalecamy pobranie kompletnego kodu
źródłowego i śledzenie go w Eclipse (lub innym środowisku IDE albo
edytorze tekstu).
Kod źródłowy: http://mng.bz/hxHs.
Aplikacja jest niemal identyczna z programem HelloAnt z rozdziału 14. Wprowa-
dziliśmy jednak kilka drobnych modyfikacji. Oto one:
Q widok tekstowy Hello jest zaimplementowany za pomocą niestandardowej
klasy kontrolki (MyButton), dziedziczącej po klasie Button;
Q metoda obsługi kliknięcia przycisku (myClickHandler) jest powiązana
z widokiem bezpośrednio w XML-owym kodzie układu, a nie w kodzie
Javy (rozwiązanie to można stosować od Androida 1.6).
Wygląd aplikacji przedstawiono na rysunku C.1, a XML-owy kod układu — na
listingu C.2.

Listing C.2. Układ prostej aplikacji, której kod skracamy i zaciemniamy

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


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
C.3. Reguły ProGuarda 693

android:layout_width="fill_parent"
android:layout_height="fill_parent">

<com.manning.aip.proguard.MyButton
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:onClick="myClickHandler"
/>

</LinearLayout>

Rysunek C.1.
Przykładowa
aplikacja po
uruchomieniu
wyświetla komunikat
typu toast. Kliknięcie
przycisku (opartego
na niestandardowej
klasie kontrolki)
powoduje
wyświetlenie
komunikatu
z wykorzystaniem
metody obsługi
kliknięcia podanej
w XML-u

W klasie MyButton nie ma nic niezwykłego. Dziedziczy ona po klasie Button i nie
ma żadnych nowych funkcji. Dziedziczenie stosujemy, aby przedstawić problem
z niestandardowymi klasami widoków i regułami skracania kodu używanymi
przez ProGuard. Także metoda obsługi kliknięcia nie wykonuje żadnych cieka-
wych operacji (wyświetla jedynie komunikat typu toast). Metoda ta, podobnie
jak klasa MyButton, ma jedynie ilustrować typowy problem związany z zaciem-
nianiem nazw metod przez ProGuard. W kodzie znajduje się też metoda, której
nigdzie nie wywołujemy. Narzędzie ma usunąć tę bezużyteczną metodę z pliku
APK. Na listingu C.3 znajduje się pełny kod głównej aktywności aplikacji.

Listing C.3. Główna aktywność aplikacji

public class MainActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
String toast = StringUtils.repeat("Witaj, ProGuardzie! ", 3);
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();
}
694 DODATEK C ProGuard

public void myClickHandler(View customView) {


Toast.makeText(this, "Kliknięcie", Toast.LENGTH_SHORT).show();
}

public void unusedMethod() {


System.out.println("Nikt mnie nie używa!");
}
}

Wyjaśnijmy pokrótce, jakie operacje ma wykonać ProGuard na aplikacji:


Q chcemy zachować klasę MainActivity, ponieważ stanowi punkt wejścia
do aplikacji;
Q chcemy zachować klasę StringUtils i metodę repeat;
Q chcemy zachować metodę myClickHandler;
Q chcemy zachować klasę MyButton;
Q chcemy pozbyć się metody unusedMethod;
Q nazwy prawie wszystkich klas mają być zaciemnione; wyjątki to nazwy
używanych w XML-u klas MainActivity i MyButton;
Q nazwy prawie wszystkich metod mają być zaciemnione; wyjątek to nazwa
używanej w XML-u metody myClickHandler;
Q chcemy wprowadzić standardowe optymalizacje (więcej na ten temat
dowiesz się dalej).
Zacznijmy od góry. Najpierw tworzymy bardzo prosty plik proguard.cfg, który
następnie krok po kroku rozszerzamy pod kątem podanych wymagań. Reguły
skracania i zaciemniania kodu oparte są na białych listach. Oznacza to, że Pro-
Guard pomija skracanie i zaciemnianie kodu bezpośrednio określonych klas. Trzeba
więc nakazać narzędziu zachowanie przynajmniej jednej klasy, MainActivity, sta-
nowiącej punkt wejścia do aplikacji. Klasa ta wymieniana jest w generowanym
przez narzędzie pliku seeds.txt. Aby zachować klasę MainActivity, należy dodać
do pliku proguard.cfg następującą regułę:
-keep public class * extends android.app.Activity

Jak widać, składnia reguł ProGuarda jest bardzo podobna do składni Javy, dla-
tego jest łatwa do zrozumienia. Najważniejsza różnica polega na tym, że Pro-
Guard pozwala stosować różne symbole wieloznaczne. Nie omawiamy ich
w tym miejscu, ponieważ są dobrze udokumentowane w witrynie narzędzia.
W podanej regule wykorzystaliśmy opcję –keep, która zapobiega usuwaniu i za-
ciemnianiu klas pochodnych od android.app.Activity. Należy wiedzieć, że zacho-
wanie aktywności i ich zaciemnienie jest niedopuszczalne. Wynika to z tego, że
aktywności zawsze deklarowane są w manifeście aplikacji (w formacie XML),
dlatego zmiana ich nazw w plikach klas prowadzi do awarii programu.
To było proste! Teraz wystarczy wyeksportować podpisany pakiet aplikacji
i uruchomić ją. Niestety, pojawia się wtedy następująca informacja:
C.3. Reguły ProGuarda 695

java.lang.RuntimeException: Unable to start activity ComponentInfo{


com.manning.aip.proguard/com.manning.aip.proguard.MainActivity}:
android.view.InflateException: Binary XML file line #6: Error inflating
class com.manning.aip.proguard.MyButton

Aplikacja uległa awarii i zgłosiła wyjątek. Problem wystąpił przy rozwijaniu do


klasy niestandardowego widoku przycisku. Co się nie powiodło? Wspomnieliśmy
już, że niestandardowa klasa widoku — podobnie jak klasa MainActivity — nie
jest używana w kodzie Javy, ale w XML-u. Dlatego ProGuard ustala, że klasa
widoku nie jest używana, i usuwa ją. Aby się o tym przekonać, należy zajrzeć do
pliku usage.txt. Wymieniono w nim klasę com.manning.aip.proguard.MyButton.
Można dodać regułę pozwalającą zachować tę konkretną klasę, jednak najwy-
raźniej jest to tylko jedno wystąpienie poważniejszego problemu związanego
z aplikacją na Android. Narzędzie powinno zachowywać niestandardowe klasy
widoków, a dokładniej — wszystkie klasy z konstruktorem, który może zostać
wywołany w czasie rozwijania układu do klasy. Wymóg ten można przekształcić
na następującą regułę ProGuarda:
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}

Te dwie reguły oznaczają, że nie należy usuwać ani zaciemniać klas z konstrukto-
rami (określonych symbolem wieloznacznym <init>), które mogą zostać wywołane
przez obiekt klasy LayoutInflater (zobacz też dotyczące takich konstruktorów
komentarze JavaDoc w klasie android.view.View). Może zauważyłeś, że użyliśmy
opcji –keepclasseswithmembers zamiast –keep. Użyta tu opcja sprawia, że zacho-
wywane są tylko klasy obejmujące wszystkie wymienione zmienne składowe. Jest
to przydatne, jeśli chcesz zachować klasy pochodne od różnych klas bazowych,
ale obejmujące konkretne pola lub metody. Gdyby zastosować w zamian opcję
–keep, ProGuard zachowałby wszystkie klasy i określone konstruktory (gdyby
takie znajdowały się w klasach).
Teraz można ponownie zbudować i włączyć aplikację. Tym razem program
uruchamia się poprawnie. Świetnie! Kliknijmy teraz niestandardowy przycisk.
java.lang.IllegalStateException: Could not find a method
myClickHandler(View) in the activity class
com.manning.aip.proguard.MainActivity for onClick handler on
view class com.manning.aip.proguard.MyButton

Najwyraźniej nadal występuje problem. Wystarczy zajrzeć do pliku usage.txt,


aby się przekonać, że ProGuard usunął metodę myClickHandler.
com.manning.aip.proguard.MainActivity:
22:23:public void myClickHandler(android.view.View)
696 DODATEK C ProGuard

Wiemy już, że metoda ta nie jest używana w kodzie Javy, tylko w XML-u. Jest
jednak częścią klasy MainActivity, którą zachowaliśmy ze względu na pierwszą
regułę. Dlaczego więc ProGuard usunął metodę? Aby to zrozumieć, należy dobrze
poznać działanie reguły –keep. W żądaniu zachowania wszystkich klas pochodnych
od Activity nie określiliśmy ciała klasy, ustalającego, które składowe chcemy
zachować wraz z klasami. Pominięcie ciała klasy sprawia, że ProGuard zachowuje
samą klasę i jej nazwę, jednak może usunąć i zoptymalizować wszystko wewnątrz
niej, a także zmienić nazwy składowych. Oto inna możliwa reguła:
-keep public class * extends android.app.Activity {
<methods>;
}

Ta reguła powoduje zachowanie wszystkich aktywności wraz z wszystkimi zde-


finiowanymi w nich metodami. To jednak przesada, a kłopoty z metodą obsługi
kliknięcia są ponownie oznaką ogólniejszego problemu, charakterystycznego dla
aplikacji na Android. Chodzi o możliwość wskazywania metod w XML-u. Lepszym
rozwiązaniem jest zastosowanie jeszcze innej reguły:
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

Oto jej znaczenie — jeśli aktywność nie jest usuwana na etapie skracania kodu,
należy zachować metody (i ich nazwy), które są publiczne, nie zwracają wartości i
przyjmują jeden parametr typu View. Dokładnie taka reguła jest potrzebna, aby
w XML-owym kodzie układu móc wskazywać metody obsługi kliknięcia za
pomocą atrybutu android:onClick. Po ponownym zbudowaniu aplikacji okazuje
się, że tym razem działa ona poprawnie.
Przedstawione tu reguły to minimum potrzebne do działania przykładowej
aplikacji. Istnieją też inne reguły i opcje, które bywają przydatne w niemal każ-
dej aplikacji na Android. Przyjrzyjmy się im.

C.4. Przydatne reguły i opcje


Zdefiniowane wcześniej reguły są odpowiednie dla przykładowej aplikacji. Jest
tak jednak tylko dlatego, że nie używamy w niej na przykład klas Service. Klasa
Service (podobnie jak Activity) to komponent Android wskazywany w XML-
owym kodzie manifestu. Jeśli więc używasz usług, powinieneś dodać do pliku
proguard.cfg podobne reguły jak dla aktywności. Dalej przedstawiamy reguły
przydatne w aplikacjach na Android.

C.4.1. Przydatne reguły


Zwykle warto zachować następujące klasy frameworku Androida:
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
C.4. Przydatne reguły i opcje 697

-keep public class * extends android.content.BroadcastReceiver


-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService

Nawet jeśli aplikacja nie korzysta z wszystkich wymienionych klas, warto zde-
finiować przedstawione reguły. Dzięki nim nie będziesz musiał zastanawiać się
nad przyczynami awarii, jeśli później dodasz któreś z tych klas, natomiast zapo-
mnisz zaktualizować reguły ProGuarda.
Następna reguła, która prawie zawsze jest przydatna, dotyczy zachowania
statycznego pola CREATOR, służącego w Androidzie do przekazywania obiektów
(zobacz rozdział 5.). Pole to jest wczytywane za pomocą mechanizmu introspekcji
w czasie wykonywania programu, dlatego ProGuard traktuje je jako nieużywane
i usuwa je. Możesz temu zapobiec, dodając następującą regułę:
-keepclassmembers class * implements android.os.Parcelable {
static android.os.Parcelable$Creator CREATOR;
}

Podobny problem powstaje przy wywoływaniu metod natywnych, zaimplemen-


towanych w językach kompilowanych, takich jak C. W kodzie Javy znajdują się
tylko sygnatury takich metod, które trzeba powiązać z kodem. Oznacza to, że
ProGuard nie może zaciemniać nazw takich metod, ponieważ nie da się wtedy
powiązać sygnatur z kodem. Zapobiec takiej sytuacji można przez dodanie nastę-
pującej reguły:
-keepclasseswithmembernames class * {
native <methods>;
}

Używamy tu opcji –keepclasseswithmembernames, ponieważ chcemy, aby ProGuard


usuwał metody, jeśli ich nie wywołujemy; jeżeli jednak metody są używane, ich
nazwy nie powinny się zmieniać.
Przedstawione wcześniej reguły są dość oczywiste. Zrozumieć następną
może być trochę trudniej. Przyjrzyjmy się jej:
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

Ta reguła sprawia, że ProGuard nie skraca ani nie zaciemnia metod values i valueOf
w wyliczeniach. Wymienione metody są wyjątkowe, ponieważ środowisko uru-
chomieniowe Javy wywołuje je za pomocą mechanizmu refleksji. Jest to jedna
z przyczyn, dla których Google sugeruje oszczędne stosowanie wyliczeń w Javie.
Działają one wolniej niż pola statyczne z modyfikatorem final. Jeśli nie uży-
wasz w kodzie wyliczeń Javy, podana reguła jest zbędna, jednak jej dodanie nie
powoduje żadnych problemów.
Omówiliśmy już potrzebne reguły — pora przejść do przydatnych opcji.
698 DODATEK C ProGuard

C.4.2. Przydatne opcje


Do tej pory omawialiśmy reguły informujące ProGuard o tym, które klasy lub
składowe klas ma zachować. Narzędzie to udostępnia też szereg opcji wpływa-
jących na jego działanie. Oto kilka ogólnych opcji, które zwykle warto ustawić:
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose

Uniemożliwienie stosowania nazw klas z literami o różnej wielkości w czasie


zaciemniania kodu sprawia, że narzędzie nie zapisze dwóch klas w pliku o tej
samej nazwie w systemach, w których wielkość znaków nie ma znaczenia (na
przykład w systemie Windows). W przeciwnym razie pliki A.class i a.class są
traktowane jak ten sam plik, co prowadzi do awarii aplikacji. Ponadto ProGuard
domyślnie pomija przy przetwarzaniu niepubliczne klasy biblioteczne (z poziomu
pakietu). Wynika to z założenia, że są one używane tylko w bibliotekach. Jednak
niektóre biblioteki udostępniają publiczne klasy pochodne od wewnętrznych,
niepublicznych klas, dlatego warto kosztem pewnego spadku szybkości działania
narzędzia przetworzyć większą część kodu. Pomijamy też cały etap wstępnego
sprawdzania kodu, ponieważ ma on sens tylko przy tworzeniu aplikacji na plat-
formę Java Micro-Edition lub dla Javy 6. Ostatnia opcja, -verbose, powoduje
wyświetlanie przez narzędzie bardziej szczegółowych informacji w czasie prze-
twarzania klas.
Wspomnieliśmy wcześniej, że ProGuard przeprowadza też optymalizację
kodu. Oprócz kilku wyjątkowych sytuacji ProGuard domyślnie stosuje wszystkie
znane optymalizacje. My nigdy nie mieliśmy z tym problemów, należy jednak
pamiętać, że niektóre optymalizacje są dość agresywne. ProGuard scala na przy-
kład hierarchie klas zarówno w pionie, jak i w poziomie, aby zmniejszyć liczbę
plików klas, a tym samym i wielkość pliku APK. Optymalizuje też pętle i operacje
arytmetyczne. Jeśli stwierdzisz, że kod przestał działać w oczekiwany sposób,
możesz krok po kroku wyłączać optymalizacje. W domyślnym pliku proguard.cfg
generowanym przez narzędzie ADT wyłączane są wszystkie optymalizacje aryt-
metyczne, związane z polami i polegające na scalaniu klas. Symbol ! przed opty-
malizacją powoduje jej wyłączenie. Oto instrukcja z domyślnego pliku:
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

Niestety, Google nie wyjaśnia, dlaczego dodał tę regułę. Zachęcamy do tego,


aby najpierw ją usunąć i sprawdzić, czy aplikacja działa w oczekiwany sposób.
Warto pamiętać, że konfiguracje ProGuarda nie są uniwersalne. Ustal, co spraw-
dza się najlepiej w Twoim przypadku, i trzymaj się tego. ProGuard wykonuje
kilka cykli optymalizacji. Można określić ich liczbę, jednak niezależnie od tego,
jak jest wysoka, ProGuard kończy pracę po wykryciu, że nie da się wprowadzić
dalszych optymalizacji. Liczbę cykli można określić tak:
C.5. Analizowanie raportów o błędach 699

-optimizationpasses 5

To już koniec omówienia konfigurowania oraz uruchamiania ProGuarda. Przed


podsumowaniem chcemy jednak pokrótce opisać jeszcze jedną kwestię — przy-
wracanie pierwotnej postaci zrzutom stosu z raportów o błędach dla zaciem-
nionego kodu.

C.5. Analizowanie raportów o błędach


Wspomnieliśmy już o pewnej związanej z zaciemnianiem kodu kwestii, która
prowadzi do problemów. Chodzi o analizowanie raportów o błędach dotyczących
aplikacji z zaciemnionym kodem. Pierwszą rzeczą, jaką programiści zwykle robią
po otrzymaniu raportu o błędach, jest przyjrzenie się zrzutowi stosu. Jeśli jednak
wszystkie klasy i metody mają niezrozumiałe nazwy, analizowanie zrzutu stosu
jest trudne, a nawet niemożliwe.
Aby zademonstrować problem, wywołujemy awarię aplikacji. Jej przyczyną
jest klasa Bomb:
public class Bomb {
public void explode() {
throw new RuntimeException("Bum!");
}
}

„Bomba” ta eksploduje w metodzie onCreate:


public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

String toast = StringUtils.repeat("Witaj, ProGuardzie! ", 3);


Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();

new Bomb().explode();
}

Teraz po uruchomieniu aplikacji w dziennikach urządzenia pojawia się następu-


jący zrzut stosu (przytoczony tu w skróconej wersji):
java.lang.RuntimeException: Unable to start activity ...MainActivity}:
java.lang.RuntimeException: Boom!
...
Caused by: java.lang.RuntimeException: Boom!
at com.manning.aip.proguard.MainActivity.onCreate(Unknown Source)
at android.app.Instrumentation.callActivityOnCreate(
Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(
ActivityThread.java:2627)
... 11 more

Jak widać, na zrzucie stosu obok wiersza dotyczącego błędu nie ma ani numeru
wiersza kodu z pliku źródłowego, ani nazwy tego pliku. Wynika to z tego, że
700 DODATEK C ProGuard

ProGuard domyślnie usuwa te informacje, aby dodatkowo utrudnić inżynierię


odwrotną. Można to zmienić przez umieszczenie w pliku proguard.cfg następu-
jącego wiersza:
-keepattributes SourceFile,LineNumberTable

Opcja ta sprawia, że na zrzutach stosu pojawiają się nazwy plików źródłowych


i numery wierszy kodu. Nie rozwiązuje to jednak problemu zaciemnionych nazw
metod. W przytoczonym tu bardzo prostym przykładzie nie stanowi to problemu,
ponieważ ProGuard nie zaciemnia nazwy metody onCreate. Wynika to z tego, że
metoda ta przesłania jej wersję z klasy bazowej. Ponieważ ProGuard nie prze-
twarza pliku android.jar (używa go tylko do analizy kodu; w końcu plik android.jar
nie jest częścią aplikacji, tylko platformy), nie może zmienić nazwy wspomnianej
metody. Jednak w bardziej skomplikowanych scenariuszach trzeba przywrócić
pierwotną formę zrzutu stosu. Służy do tego narzędzie retrace. Jest ono powią-
zane z ProGuardem i można je znaleźć w tym samym katalogu. Jedynym zada-
niem narzędzia retrace jest odczytywanie zaciemnionych zrzutów stosu, dopa-
sowywanie nazw na podstawie pliku z ich odwzorowaniami (wspomnianego
wcześniej pliku mapping.txt) i generowanie pierwotnego zrzutu stosu. Oto wywo-
łanie tego narzędzia:
$ retrace proguard/mapping.txt stacktrace.txt

Teraz staje się zrozumiałe, dlaczego tak ważne jest archiwizowanie plików odwzo-
rowania dla każdej budowanej wersji aplikacji. Bez tego pliku nie można odtwo-
rzyć zrzutów stosu.

C.6. Podsumowanie
To już koniec omówienia ProGuarda. Pokazaliśmy, że jest on wartościowym
dodatkiem, pozwalającym skrócić kod aplikacji, przyspieszyć jego działanie
i utrudnić inżynierię odwrotną. Jednak skonfigurowanie i dostosowanie tego
narzędzia do własnych potrzeb wymaga czasu. Szkoda, że ProGuard nie jest lepiej
opisany w dokumentacji Androida. Podobnie niedoceniane jest inne przydatne
narzędzie, monkeyrunner, które opisujemy w dodatku D.
Dodatek D
Monkeyrunner

W ostatnim dodatku omawiamy inne nowe narzędzie z pakietu SDK — mon-


keyrunner. Przedstawiony tu materiał jest uzupełnieniem rozdziałów 13. i 14.,
dotyczących testów, instrumentacji i automatyzacji procesu budowania. Jednak
monkeyrunner odgrywa specjalną rolę zarówno z przyczyn technicznych, jak
i z uwagi na wygodę jego używania, o czym się wkrótce przekonasz.

D.1 Przegląd możliwości


Monkeyrunner (nie należy mylić go z opisanym w rozdziale 13. narzędziem
monkey) to oparta na skryptach i rozszerzalna aplikacja, która umożliwia kon-
trolowanie emulatora i urządzenia w sposób programowy. Podobnie jak andro-
idowy framework do instrumentacji, pozwala sterować działaniem programu przez
uruchamianie aktywności i generowanie zdarzeń związanych z wprowadzaniem
danych. Odbywa się to jednak spoza frameworku Android, podobnie jak przy
korzystaniu z aplikacji przez użytkownika. Za pomocą monkeyrunnera można
instalować i odinstalowywać aplikacje, komunikować się z powłoką poleceń urzą-
dzenia, a nawet robić zrzuty ekranu. Możesz traktować monkeyrunner jak pilota
do Androida.
Sam monkeyrunner jest napisany w Javie i stanowi część pakietu SDK
Androida. Skrypt powłoki do uruchamiania tego narzędzia znajduje się w katalogu
ANDROID_HOME/tools. Monkeyrunner jest uruchamiany z wiersza poleceń
i nie ma graficznego interfejsu użytkownika. Narzędzie to można uruchamiać

701
702 DODATEK D Monkeyrunner

w dwóch trybach: domyślnym i interaktywnym. W trybie domyślnym monkey-


runner przyjmuje jako dane wejściowe program skryptowy i wykonuje go.
Wywołania wyglądają wtedy tak:
$ monkeyrunner my_script.py

Skrypty obejmują instrukcje, które monkeyrunner ma wykonać. Przykładowe


skrypty przedstawiamy dalej. Uruchomienie monkeyrunnera bez podania pliku
skryptu prowadzi do przejścia w tryb interaktywny. Dostępna staje się wtedy
powłoka, w której można bezpośrednio wprowadzać instrukcje.
$ monkeyrunner
Jython 2.5.0 (Release_2_5_0:6476, Jun 16 2009, 13:33:26)
[Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_24
>>> 5 == 5
True
>>>

W dalszych podrozdziałach pokrótce opisujemy komponenty składające się na


monkeyrunner i dostępne funkcje. Wyjaśniamy też, jak przy użyciu tych funkcji
pisać skrypty monkeyrunnera, a także jak wzbogacać możliwości tego narzędzia za
pomocą niestandardowych wtyczek.

D.2. Komponenty i funkcje


Monkeyrunner obejmuje trzy podstawowe klasy, które można stosować w skryp-
tach. Ogólnie klas tych jest więcej, jednak trzy opisane w tym miejscu (Monkey-
Runner, MonkeyDevice i MonkeyImage) pozwalają uzyskać dostęp do funkcji narzędzia.

KOD ŹRÓDŁOWY MONKEYRUNNERA. Monkeyrunner to część otwar-


tego pakietu SDK Android, dlatego w internecie dostępny jest kod narzę-
dzia. Ponieważ w dokumentacji internetowej (http://mng.bz/3jBo) opisane
są tylko trzy omawiane tu klasy, kod źródłowy monkeyrunnera stanowi bez-
cenne źródło informacji. Kod ten znajdziesz na stronie http://mng.bz/kqxE.
Przyjrzyjmy się teraz pokrótce każdej z wymienionych klas i zobaczmy, do czego
są przydatne.

D.2.1. MonkeyRunner
Ta klasa to wysokopoziomowy kontroler skryptów monkeyrunnera. W klasie
MonkeyRunner zdefiniowane są statyczne metody pomocnicze, służące na przykład
do otwierania okien dialogowych, usypiania skryptów na pewien czas (co pozwala
wykonać urządzeniu zadania) i — co najważniejsze — nawiązywania połączenia
z emulatorem lub urządzeniem.
D.3. Skrypty monkeyrunnera 703

D.2.2. MonkeyDevice
Klasa MonkeyDevice reprezentuje jedno urządzenie na Android podłączone za
pomocą metody MonkeyRunner.waitForConnection. Umożliwia kontrolowanie danego
urządzenia przez instalowanie i odinstalowywanie aplikacji oraz testów, urucha-
mianie testów aktywności lub testów z instrumentacją, wykonywanie poleceń
powłoki bezpośrednio w urządzeniu, rozsyłanie intencji, generowanie zdarzeń
dotknięcia ekranu i wciśnięcia klawiszy, a także wykonywanie zrzutów. Jest to
więc klasa do kontrolowania podłączonego urządzenia.

D.2.3. MonkeyImage
Zrzut wykonany za pomocą klasy MonkeyDevice jest zwracany jako obiekt klasy
MonkeyImage. Klasa MonkeyImage umożliwia zapisywanie zrzutów na dysku i porów-
nywanie ich w celu wykrycia rozbieżności względem zrzutów wzorcowych
w ramach kompleksowych testów. Zobaczmy, jak wykorzystać wszystkie klasy
w skrypcie monkeyrunnera.

D.3. Skrypty monkeyrunnera


Wspomnieliśmy wcześniej, że monkeyrunner to aplikacja oparta na skryptach.
Oznacza to, że można albo przekazać do niej skrypt do uruchomienia, albo
bezpośrednio wprowadzać polecenia w trybie interaktywnym. Firma Google nie
próbowała wymyślać koła od nowa i tworzyć zastrzeżonego języka skryptowego.
Zamiast tego wykorzystała w monkeyrunnerze popularny i dający duże możliwo-
ści język skryptowy Python.
Python to interpretowany język programowania zgodny z wieloma paradyg-
matami. Charakterystyczne dla niego są zwięzłość i czytelność kodu, dlatego
dobrze nadaje się do pisania skryptów. Python łączy elementy języków obiekto-
wych i funkcyjnych, jednak umożliwia też pisanie czysto proceduralnych pro-
gramów. Jednym z ciekawszych aspektów Pythona, zwłaszcza dla programi-
stów piszących w językach z rodziny C, jest brak nawiasów klamrowych. Zasięg
leksykalny jest określany na podstawie wcięć, dlatego Python jest jednym z nie-
licznych języków, w których odstępy rzeczywiście są istotne w składni. Nie
omawiamy tu szczegółowo Pythona, zamieszczamy jednak uwagi i wyjaśnienia
do fragmentów kodu źródłowego, które mogą być niezrozumiałe dla programi-
stów Javy.
Napiszmy teraz skrypt monkeyrunnera symulujący korzystanie z aplikacji
DealDroid. Kod z listingu D.1 możesz zapisać w pliku dealdroid.py (lub o innej
nazwie) albo wprowadzić ręcznie w trybie interaktywnym.

Listing D.1. Prosty skrypt monkeyrunnera

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice


import commands
import sys
704 DODATEK D Monkeyrunner

devices = commands.getoutput('adb devices').strip().split('\n')[1:]


if len(devices) == 0:
MonkeyRunner.alert("Nie znaleziono urządzeń. Uruchom emulator
lub podłącz urządzenie.", "Nie znaleziono urządzeń", "Zakończ")
sys.exit(1)
elif len(devices) == 1:
choice = 0
else:
choice = MonkeyRunner.choice("Znaleziono kilka urządzeń.
Wybierz docelowe urządzenie.", devices, "Wybierz docelowe urządzenie")

device_id = devices[choice].split('\t')[0]

device = MonkeyRunner.waitForConnection(5, device_id)

apk_path = device.shell('pm path com.manning.aip.dealdroid')


if apk_path.startswith('package:'):
print "Aplikacja DealDroid jest już zainstalowana."
else:
print "Aplikacja DealDroid nie jest zainstalowana. Instalowanie pliku APK."
device.installPackage('../DealDroid/bin/DealDroid.apk')

print "Uruchamianie aplikacji DealDroid..."


device.startActivity(component='com.manning.aip.dealdroid/.DealList')
MonkeyRunner.sleep(7)
device.touch(100, 450, 'DOWN_AND_UP')
MonkeyRunner.sleep(2)
device.touch(100, 250, 'DOWN_AND_UP')
MonkeyRunner.sleep(2)
device.touch(100, 150, 'DOWN_AND_UP')
MonkeyRunner.sleep(2)
device.press('KEYCODE_MENU', 'DOWN_AND_UP', None)
MonkeyRunner.sleep(1)
device.touch(280, 450, 'DOWN_AND_UP')
MonkeyRunner.sleep(2)
device.type("555-13456")
MonkeyRunner.sleep(2)
device.press('KEYCODE_BACK', 'DOWN_AND_UP', None)
MonkeyRunner.sleep(1)
device.press('KEYCODE_BACK', 'DOWN_AND_UP', None)
MonkeyRunner.sleep(1)
device.press('KEYCODE_BACK', 'DOWN_AND_UP', None)

Najpierw trzeba określić używane klasy. Służy do tego instrukcja import . Zauważ,
że zamiast listy klas można też użyć symbolu wieloznacznego *, jednak — podob-
nie jak w Javie — importowanie zbędnych klas jest uznawane za nieeleganckie
rozwiązanie.
Przed wykonaniem konkretnych operacji trzeba nawiązać połączenie z urzą-
dzeniem lub emulatorem Androida. Dlatego w skrypcie wyświetlamy wszystkie
podłączone urządzenia za pomocą metody getouput Pythona. Przy użyciu tej
metody wywołujemy polecenie powłoki (adb devices). Dane wyjściowe zwracane
są w formie łańcucha znaków. Ponieważ dane wyjściowe instrukcji adb devices
nie nadają się do umieszczenia na liście, usuwamy końcowe odstępy, dzielimy
tekst w miejscach przejścia do nowego wiersza i wybieramy dwa ostatnie wpisy za
D.3. Skrypty monkeyrunnera 705

pomocą składni używanej w Pythonie do określania przedziałów tablic . Frag-


ment [x:y] oznacza przedział od indeksu x do indeksu y. Pominięcie jednej
z wartości sprawia, że przedział jest nieograniczony w jednym z kierunków.
Następnie nawiązujemy połączenie z wybranym urządzeniem. Służy do tego
metoda MonkeyRunner.waitForConnection . Metoda ta zwraca egzemplarz klasy
MonkeyDevice, który można następnie wykorzystać w skrypcie. Zauważ, że argu-
menty wspomnianej metody (limit czasu oczekiwania i identyfikator urządzenia)
są opcjonalne. Metodę można wywołać bez argumentów. Wtedy próbuje ona
nawiązać połączenie z dowolnym dostępnym urządzeniem i oczekuje w nieskoń-
czoność, aż się ono pojawi.
Przed użyciem aplikacji DealDroid trzeba się upewnić, że jest zainstalowana.
Skrypt sprawdza to, a jeśli aplikacja nie jest zainstalowana, instaluje ją . Wyko-
rzystujemy do tego metodę shell, która umożliwia wywołanie w urządzeniu
dowolnego polecenia powłoki. Tu używamy menedżera pakietów (program pm)
do określenia ścieżki do pliku APK aplikacji DealDroid. Jeśli aplikacja jest zain-
stalowana, zwrócony łańcuch znaków ma format pakiet:/ścieżka/do/plikuAPK. Jeśli
ten łańcuch znaków nie jest pusty, skrypt ma pominąć instalowanie aplikacji.
UWAGA. Aby instalacja była możliwa, upewnij się, że aplikacja DealDroid
znajduje się na dysku twardym, a skrypt jest wywoływany z poziomu
katalogu z tą aplikacją. Jeśli korzystasz z innej konfiguracji, koniecznie
zmień ścieżkę do pliku APK.
Następnie uruchamiamy aplikację przez wywołanie początkowej aktywności
DealList . Możliwe, że zaciekawił Cię sposób przekazywania komponentu do
metody startActivity. Używamy tu argumentu nazwanego (ang. keyword argu-
ment). W Pythonie parametry można przekazywać nie tylko w zdefiniowanej
kolejności (tak jest w Javie), ale też w dowolnym porządku. Wymaga to podania
nazwy argumentu, znaku równości i wartości argumentu. Ponieważ metoda
startActivity przyjmuje osiem parametrów, a nie wszystkie z nich są zawsze
potrzebne, można skrócić wywołanie przez bezpośrednie przekazanie argumentu
component. Aplikacja DealDroid najpierw pobiera dane z usługi sieciowej eBaya,
dlatego usypiamy skrypt na siedem sekund w oczekiwaniu na zakończenie tej
operacji.
Na tym etapie aplikacja DealDroid powinna zakończyć wykonywanie zadań
i wyświetlić listę ofert. Zaczynamy więc symulowanie korzystania z aplikacji
przez wywoływanie metod touch, type i press. Odpowiadają one dotknięciu ekranu,
wprowadzeniu tekstu i wciśnięciu przycisku. Możliwe akcje związane z klawi-
szami i dotykiem to DOWN, UP i DOWN_AND_UP (wartości te są przekazywane jako
łańcuchy znaków), natomiast kody klawiszy są dokładnie takie same jak w klasie
android.view.KeyEvent. Efekt uruchomienia skryptu jest podobny jak w roz-
dziale 13., gdzie sterowaliśmy pracą aplikacji DealDroid programowo za pomocą
instrumentacji.
706 DODATEK D Monkeyrunner

Możesz się zastanawiać nad pewną kwestią. Na jakiej zasadzie działa rozwią-
zanie, skoro klasy monkeyrunnera są napisane w Javie, jednak wczytujemy je
i korzystamy z nich w skryptach Pythona? Jest to możliwe, ponieważ w monkey-
runnerze używany jest język Jython — implementacja Pythona napisana w Javie.
Jython potrafi wczytywać skrypty Pythona, manipulować nimi i uruchamiać je
w maszynie JVM. Potrafi też używać klas Pythona i Javy.
Skoro już jesteśmy przy klasach Javy, warto wspomnieć, że monkeyrunner
potrafi dołączać niestandardowe klasy Javy do skryptów. Służy do tego opisana
dalej architektura do dodawania wtyczek.

D.4. Pisanie wtyczek


Monkeyrunner umożliwia wzbogacanie funkcji za pomocą wtyczek. Wtyczka
monkeyrunnera to zwykły plik JAR, który można przekazać do narzędzia w wier-
szu poleceń:
$ monkeyrunner -plugin plugin.jar script.py

Aby to podejście zadziałało, trzeba samodzielnie utworzyć plik JAR (zaraz zoba-
czysz, jak to zrobić). We wtyczce ADT nie ma narzędzi do wykonywania tej ope-
racji. Wtyczka obejmuje jedną specjalną klasę (klasę główną wtyczki), która zapew-
nia dostęp do środowiska Pythona wykorzystywanego do uruchamiania skryptów
monkeyrunnera. Pozwala to dodawać do skryptu niestandardowe obiekty,
stałe i zmienne w czasie wczytywania wtyczki. Dodane elementy są dostępne
w skrypcie. Ponadto można dodać dowolną liczbę zwykłych klas Javy. Ważnym
ograniczeniem jest jednak to, że nie można uzyskać dostępu do klas frameworku
Androida. Dostępne są jedynie klasy wczytane przez monkeyrunner, w tym klasy
samego narzędzia (MonkeyRunner, MonkeyDevice, MonkeyImage i inne klasy pakietu
com.android.monkeyrunner). Wyjątkowo interesująca jest biblioteka ddmlib. Tej
biblioteki narzędzie DDMS (dostępne także jako dodawana przez wtyczkę ADT
perspektywa w środowisku Eclipse) używa do komunikowania się z urządze-
niami z Androidem. W bibliotece tej zdefiniowany jest interfejs API Javy, który
można wykorzystać do wyświetlania list urządzeń i bezpośredniej interakcji
z nimi. Nie trzeba przy tym tworzyć nowego procesu powłoki w celu wywoływania
instrukcji adb, co jest znacznie mniej wygodne. Monkeyrunner wymaga biblioteki
ddmlib, dlatego można z niej korzystać także we wtyczkach.
Dalej przedstawiamy prostą wtyczkę (podobną do aplikacji Hello World)
dla monkeyrunnera. W ten sposób chcemy pokazać, jak przygotować projekt
wtyczki, jak utworzyć plik JAR wtyczki, a także jak uzyskać dostęp do funkcji
wtyczki w skrypcie monkeyrunnera.
POBIERZ PROJEKT DEALDROIDMONKEYRUNNER. Kod źródłowy
projektu i pakiet APK do uruchamiania aplikacji znajdziesz w witrynie
z kodem do książki Android w praktyce. Ponieważ niektóre listingi
D.4. Pisanie wtyczek 707

skrócono, abyś mógł skoncentrować się na konkretnych zagadnieniach,


zalecamy pobranie kompletnego kodu źródłowego i śledzenie go w Eclipse
(lub innym środowisku IDE albo edytorze tekstu).
Kod źródłowy: http://mng.bz/Pufd.
Przed napisaniem wtyczki trzeba skonfigurować projekt. Ponieważ wtyczki piszemy
w Javie, należy utworzyć zwykły projekt Javy (używamy do tego środowiska IDE
Eclipse). Aby móc skompilować projekt wtyczki, trzeba dodać do ścieżki budo-
wania przynajmniej biblioteki Jython i Google Guava. Do ścieżki warto też dodać
pliki JAR monkeyrunnera i biblioteki ddmlib. Są one potrzebne tylko przy roz-
szerzaniu klas monkeyrunnera lub bezpośredniej interakcji z urządzeniami
(w omawianym prostym przykładzie tego nie robimy), jednak pliki te są przydatne
w bardziej zaawansowanych rozwiązaniach.
Wszystkie wymienione biblioteki są bezpłatnie dostępne w internecie, jednak
łatwiej i bezpieczniej jest korzystać z wersji dostępnych w pakiecie SDK Andro-
ida. Znajdują się one w katalogu ANDROID_HOME/tools/lib. Jeśli zauważysz, że
w pewnej sytuacji (na przykład przy pisaniu wtyczek dla monkeyrunnera) zwykle
dołączasz grupę plików JAR, warto umieścić je w bibliotece użytkownika z Eclipse.
W tym celu kliknij projekt wtyczki prawym przyciskiem myszy w Eclipse i wybierz
opcję Properties/Java Build Path/Libraries/Add Library…/User Library/User
Libraries…. Następnie kliknij przycisk New… i podaj nazwę nowej biblioteki (na
przykład Monkeyrunner). Zaakceptuj ustawienia i wybierz z listy nową bibliotekę
użytkownika, a także inne wspomniane biblioteki. Służy do tego opcja Add JARs….
Następnie możesz dodać nową bibliotekę użytkownika, Monkeyrunner, do ścieżki
budowania projektu przez wybranie pliku z listy dostępnych bibliotek użytkow-
nika. Struktura projektu powinna teraz wyglądać tak jak na rysunku D.1.

Rysunek D.1.
Struktura
przykładowego
projektu wtyczki

Wszystko jest już przygotowane, możemy więc przystąpić do pisania kodu


wtyczki. Chcemy uprościć wywoływanie metod touch i press w plikach skryptów.
Programiści prawie zawsze zgłaszają zdarzenie DOWN_AND_UP i odczekują krótki
czas przed uruchomieniem następnej operacji. Dlatego tworzymy klasę pomoc-
niczą MonkeyHelper, która automatycznie wykonuje te operacje. W klasie zaimple-
mentowane są metody tap i press, które upraszczają standardowe wywołania
metod touch i press, a także powodują uśpienie skryptu na dwie sekundy po
zgłoszeniu zdarzenia (listing D.2).
708 DODATEK D Monkeyrunner

UWAGA. Warto zauważyć, że w przykładowym projekcie nie używamy


biblioteki użytkownika z Eclipse. Zamiast tego korzystamy z narzędzi
z wersji 12. pakietu SDK, umieszczonych w repozytorium plików JAR.
Rozwiązanie to stosujemy jako udogodnienie dla Czytelników, ponieważ
pozwala od razu skompilować projekt. We własnych projektach zwykle nie
umieszcza się plików JAR w repozytorium.

Listing D.2. Klasa MonkeyHelper jest wczytywana we wtyczce i dostępna


w skryptach

package com.manning.aip.monkeyrunner;

import org.python.core.PyInteger;
import org.python.core.PyObject;
import org.python.core.PyString;

import com.android.monkeyrunner.MonkeyDevice;
import com.android.monkeyrunner.core.TouchPressType;

public class MonkeyHelper {

public static void tap(MonkeyDevice device, int x, int y) {


PyObject[] args = { new PyInteger(x), new PyInteger(y),
new PyString(TouchPressType.DOWN_AND_UP.name()) };
device.touch(args, null);
sleep(2000);
}

public static void press(MonkeyDevice device, String key) {


String keyCode = "KEYCODE_" + key.toUpperCase();
PyObject[] args = { new PyString(keyCode), new PyString(
TouchPressType.DOWN_AND_UP.name()),
new PyString("") };
device.press(args, null);
sleep(2000);
}

private static void sleep(long millis) {


try {
Thread.sleep(millis);
} catch (InterruptedException e) {
}
}
}

Potrzebna jest też główna klasa wtyczki. Tu nie wykonuje ona żadnych istotnych
operacji. Chcemy tylko powitać klasę MonkeyHelper, dlatego w skrypcie umiesz-
czamy zmienną z odpowiednim tekstem:
public class Plugin implements Predicate<PythonInterpreter> {

@Override
public boolean apply(PythonInterpreter python) {

python.set("hello", "Witaj, monkeyrunnerze!");


D.4. Pisanie wtyczek 709

return true;
}
}

W każdej głównej klasie wtyczki trzeba zaimplementować interfejs Predicate dla


typu PythonInterpreter. Wspomniany interfejs to części biblioteki Guava (jest
to opracowana w firmie Google biblioteka narzędziowa Javy). Implementujemy
metodę apply interfejsu, która jest punktem wejścia do wtyczki monkeyrunnera.
PythonInterpreter to klasa Jythona udostępniająca omawiany interfejs skryptom
Pythona. Klasa ta służy do wczytywania i zapisywania zmiennych. Tu jest to
zmienna hello o wartości Witaj, monkeyrunnerze!. Zmienna ta jest dostępna
w skryptach monkeyrunnera, o czym wkrótce się przekonasz. Nową klasę można
nazwać w dowolny sposób (nie trzeba używać nazwy Plugin).
To już cały kod wtyczki. Pozostało nam tylko umieścić klasy w pliku JAR, tak
aby można było przekazać go do narzędzia monkeyrunner. Rozwiązanie to działa
w następujący sposób: w czasie rozruchu monkeyrunner przeszukuje plik mani-
festu z archiwum JAR wtyczki i znajduje pole MonkeyRunnerStartupRunner. Pole
to musi obejmować pełną nazwę klasy z implementacją metody apply. Tu jest to
klasa Plugin. W archiwum JAR trzeba udostępnić niestandardowy manifest, dla-
tego tworzymy szablon manifest.txt z odpowiednim ustawieniem:
MonkeyRunnerStartupRunner: com.manning.aip.monkeyrunner.Plugin

Trzeba zwrócić uwagę na pewien szczegół — plik manifestu musi kończyć się
znakiem wysuwu wiersza (pustym wierszem). W przeciwnym razie wystąpi błąd.
Teraz można utworzyć plik JAR z klasami wtyczki z katalogu projektu. Służy do
tego następująca instrukcja:
$ jar cvfm bin/plugin.jar manifest.txt -C bin .

To powoduje utworzenie pliku plugin.jar w katalogu bin/. Uwzględniane w nim


są pola manifestu podane w pliku manifest.txt i wszystkie klasy z katalogu bin.
Przed wczytaniem nowej wtyczki wróćmy do skryptu z listingu D.1. Chcemy
wyświetlić w niej dodaną zmienną i zastosować nową klasę MonkeyHelper. W kodzie
z listingu D.3 pominęliśmy fragmenty, które są takie same jak na poprzednim
listingu.

Listing D.3. Poprawiony skrypt; wykorzystujemy w nim funkcje z niestandardowej


wtyczki

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice


from com.manning.aip.monkeyrunner import MonkeyHelper
import commands
import sys

print hello
...
710 DODATEK D Monkeyrunner

MonkeyHelper.tap(device, 100, 450)


MonkeyHelper.tap(device, 100, 250)
MonkeyHelper.tap(device, 100, 150)
MonkeyHelper.press(device, 'menu')
MonkeyHelper.tap(device, 280, 450)
device.type("555-13456")
MonkeyHelper.press(device, 'back')
MonkeyHelper.press(device, 'back')
MonkeyHelper.press(device, 'back')

Przed użyciem klasy pomocniczej trzeba zaimportować ją do skryptu w opisany


wcześniej sposób . Ponadto możemy korzystać ze zmiennej hello ustawionej
w klasie Plugin. Zmienna ta jest ustawiana przed rozpoczęciem wykonywania
skryptu, co jest wygodnym rozwiązaniem . W bloku symulującym poruszanie
się między ekranami używamy teraz nowych funkcji pomocniczych, co zwiększa
przejrzystość kodu. Na przykład klawisz „wstecz” ma teraz nazwę back zamiast
KEYCODE_BACK .
Skrypt można uruchomić za pomocą następującego polecenia:
$ monkeyrunner -plugin bin/plugin.jar dealdoid_with_plugin.py

Jeśli wszystko działa poprawnie, w standardowym wyjściu powinien pojawić się


komunikat Witaj, monkeyrunnerze!.

D.5. Podsumowanie
Monkeyrunnera można używać do nawiązania połączenia z urządzeniem, zain-
stalowania aplikacji na Android, a także zgłoszenia zapisanej w skrypcie sekwencji
zdarzeń (symulujących wprowadzanie danych przez użytkownika). W ten sposób
można zapisać w skrypcie operacje wykonywane przez użytkownika i zasymulo-
wać je. Jeśli potrzebne są dodatkowe możliwości, monkeyrunner można rozszerzyć
za pomocą wtyczek.
Monkeyrunner jest też wartościowym narzędziem do programowego symu-
lowania interakcji użytkownika z aplikacją. Dzięki skryptowemu charakterowi
tego narzędzia można wykorzystać je do wykonywania dowolnych zadań, ponie-
waż można wzbogacać je o nowe funkcje przy użyciu modułów Pythona i wty-
czek Javy.
Jednak w czasie stosowania monkeyrunnera szybko zwraca uwagę jego nie-
stabilność. Narzędzie często zgłasza komunikaty o błędach, po czym nie można
kontynuować pracy. Zdarza się to na przykład przy próbie zgłoszenia zdarzeń
związanych z wprowadzaniem danych przed wykonaniem wcześniejszych operacji
przez urządzenie. Wtedy wszystkie takie zdarzenia powodują błąd. W takiej sytu-
acji warto wydłużyć czas uśpienia między kolejnymi operacjami.
Następnym źródłem problemów jest konieczność częstego stosowania instruk-
cji sleep. Jeśli operacje są skomplikowane i trudno jest oszacować czas ich wyko-
nywania (dotyczy to m.in. wywołania usługi sieciowej w przykładzie), może nastą-
pić desynchronizacja pracy skryptu i urządzenia. Dlatego monkeyrunner nie
D.5. Podsumowanie 711

nadaje się do stosowania w środowiskach do przeprowadzania zautomatyzowa-


nych testów, na przykład w serwerach budowania (zobacz rozdział 14.). Testerzy
powinni korzystać z tego narzędzia ręcznie. Ponieważ monkeyrunner jest sto-
sunkowo nowym komponentem z pakietu SDK, na razie nie istnieją wtyczki
opracowane przez firmę Google lub niezależnych programistów. Mamy jednak
nadzieję, że społeczność skupiona wokół Androida wkrótce zacznie je tworzyć.
Oprogramowanie o otwartym dostępie do kodu źródłowego górą!
712 DODATEK D Monkeyrunner
Skorowidz

algorytm
A decyzyjny, 394
adapter, 91, 444, 459 najbliższego sąsiada, 517
MovieAdapter, 158 wyboru folderu, 194
widoków StackView, 659 Android, 30
adaptery niestandardowe, 92 Android Asset Processing Tool, 43
ADB, Android Debug Bridge, 665 Android Debug Bridge, 64
adres pocztowy, 425 Android Market, 30, 435, 476
ADT, Android Development Tools, 34, 608 animacja niestandardowa, 456
AIDL, Android IDL, 208 animacje, 454
Ajax, Asynchronous JavaScript and XML, 389 ANR, Activity Not Responding, 237
akcelerometr, 58 ANR, Application Not Responding, 572
akcja Ant, Another neat tool, 583–592
ACTION_SEND, 99 Apache Ant, 35
MAIN, 102 Apache Harmony, 46
akcje spółek, 218 aparaty VGA, 437
aktualizowanie aplikacja
informacji w tle, 217 Barcode Scanner, 59
lokalizacji, 416, 421 BrewMap, 422, 427
pamięci podręcznej, 216 Browser, 58
aktywności, 82 Bubble, 59
aplikacji LifecycleExplorer, 117 Camera, 467
główne, 131 Coin Flip, 59
oparte na listach, 84 Compass, 59
z pliku Main.java, 118 DealDroid, 70, 106, 529, 674, 705
aktywność, 39, 72 FileExplorer, 283, 287
BrewLocationDetails, 432 Gallery, 332
DealList, 548 GoodShares, 330
MapActivity, 427 HelloAndroid, 35
OpenGLGreenScreenActivity, 498 HelloAnt, 619, 689
ProviderDetail, 411 HelloMaven, 599
SlideshowActivity, 456, 460 Hoccer, 59
alarm, 226 ImageMash, 330, 333, 339
alarm systemowy, 225 LifecycleExplorer, 117, 120

713
714 Skorowidz

aplikacja Bionic, 51
Locale, 59 Calculon, 553
MediaMogul, 436, 447, 468 cglib, 560
MyMovies, 138, 395 ddmlib, 706
MyMoviesDatabase, 296, 300, 324 EasyMock, 560
OpenGLDemo, 497 glibc, 51
StockPortfolio, 208 GLUtils, 516
aplikacje ignition, 375
natywne, 683 java.io, 280, 285
otwarte, 33 JavaScript Object Notation, 48
typu klient-serwer, 671 Mockito, 566
w JavaScripcie, 681 OpenGL, 494, 500, 519
wykorzystujące czujniki, 59 OpenGL ES, 56, 477, 493, 495
APNS, Apple Push Notification Service, 230 Robotium, 551–553
archiwizacja, 580 SGL, 56
archiwum APK, 581 SQLite, 56
argument nazwany, keyword argument, 705 Swing, 47
argumenty określające typy, 257 WebKit, 58
artefakty, 597 biblioteki
artefakty Mavena, 613 natywne, 56
asynchroniczna praca, 256 uruchomieniowe, 685
asynchroniczne wywoływanie usługi, 338 blokada wzbudzająca, wake lock, 226
asynchroniczne zadania, 264 blokowanie bazy danych, 57
atrapy, 556, 559 błąd OutOfMemoryError, 604
atrybut błędy ANR, 572
android:color, 181 błędy w HttpURLConnection, 365
android:configChanges, 125 budowanie, 607, 615
android:listSelector, 177 aplikacji, 577, 589
cacheColorHint, 171 macierzowe, matrix build, 613, 617, 625–629
minSdkVersion, 189 z wykorzystaniem Hudsona, 617
supports-screens, 191 bufor wierzchołków, 512
windowBackground, 173
atrybuty
intencji, 102 C
układu, 144 C2DM, Cloud to Device Messaging, 230
widoków, 145 cel, target, 578
automatyczne dostosowywanie aplikacji, 186 clean, 602
automatyzacja budowania aplikacji, 631 distribution, 584
AVD, Android Virtual Devices, 35, 45 emulator-start, 603
awaria sieci, 393 install, 602, 604
azymut, 407 ceny akcji, 218
certyfikat bezpieczeństwa, 580
B ContentProvider, 75
CRUD, create, read, update, delete, 353
baza cykl życia, 108
filmów IMDB, 158 aktywności, 113, 460
kontaktów, 349 komponentów, 111
mymovies.db, 324 czas ustalenia lokalizacji, 419
bazy danych, 57, 299 czujnik, 58
biblioteka akcelerometr, 58
Apache Commons Lang, 585, 591 ciśnienia, 58
Apache HttpClient API, 47 GPS, 58
Apache HTTP Components, 366 pola magnetycznego, 58
AWT, 47 sztucznego światła, 58
Skorowidz 715

temperatury, 58
zbliżeniowy, 58
E
żyroskop, 58 Eclipse, 34, 577
czujniki geoprzestrzenne, 407 efekt FILL, 486
efekty
dwuwymiarowe, 490
D dźwiękowe, 462
Dalvik, 48, 50 graficzne, 490
Dalvik Debug Monitor, 65 tekstowe, 492
dane środowiskowe, 92 ekran, 189, 194
DAO, Data Access Object, 302 aktywności CanvasDemo, 479
debugowanie, 44, 665, 676 Main, 119
debugowanie przez port usb, 666 OLED, 357
definicja powitalny, 269, 270
motywu, 169 serwera budowania, 616
stylu, 168 właściwości projektu, 37
definiowanie ekrany aplikacji BrewMap, 422
zadań, 131 element
zasobów, 76 <supports-screens>, 188, 194
deklaracja <uses-permission>, 227
odbiornika, 206 scope, 598
układu, 80 uses-feature, 438
usługi, 203 uses-permissions, 438
DEX, Dalvik Executable, 579 elementy
diagram encja-związek, 304 obiektów graficznych, 178
długie kliknięcie, 662 stylu, 166
długość geograficzna, 405 emulator, 409
dodatek Google APIs Add-On, 423 emulator Androida, 45, 65, 619
dokumentacja, 32 etapy cyklu życia, 114
Javadoc, 332
ProGuarda, 687
dołączanie układów, 152, 154
F
DOM, Document Object Model, 47, 380 fabryka, 387
domknięcia, 684 FAT, File Allocation Table, 281
dostawca filtrowanie, 89
gps, 411 filtry intencji, 44, 96, 103, 452
kontaktów, 350 Firefoks
network, 411 rozszerzenie SQLiteManager, 325
położenia, 411, 414 format
treści, 73, 304, 349, 356 *.9.png, 183
treści niestandardowy, 353 DEX, 579
treści multimedialnych, 447 JSON, 375, 389
dostęp do OASIS XLIFF, 78
danych, 302, 303 wymiany danych, 375
interfejsu API instrumentacji, 540 XML, 375
zasobów, 79 fragmenty, 642, 648, 650
zdjęć, 444, 468 FrameLayout, 146
drzewo katalogów, 52 framework, 33
DSL, domain-specific languages, 525 Apache Turbine, 592
dzienniki budowania, 615 Google Guice, 560
dzienniki systemowe, 672 JUnit 3, 532
dźwięk, 458 JUnit 4, 565
Robolectric, 561
Robolectrica, 566
716 Skorowidz

framework instalowanie
Robotium, 549, 551 aplikacji, 65, 589
Spring, 560 wtyczek, 609
funkcja camera.front, 438 instrukcja ALTER TABLE, 57
funkcje pierwszej kategorii, 684 instrukcje HTTP
DELETE, 359
GET, 359
G HEAD, 359
geokodowanie, 425, 427 OPTIONS, 359
geolokalizowanie, 683 POST, 359
gęstość ekranu, 190 PUT, 359
gęstość pikseli, 188, 194 TRACE, 359
Google APIs Add-On, 423 instrumentacja, 539
Google Geocoding API, 427 integracja
Google Maps, 427 asynchroniczna, 338
Google Maps API Premier, 427 synchroniczna, 336
GPRS, General Packet Radio Service, 357 integracyjna baza danych, 347
GPS, 58, 408 IntelliJ, 677
GPU, graphics processing unit, 493 intencje, 73, 96–101, 329
gradient, 176 interfejs
grafika dwuwymiarowa, 478 API, 48, 60, 221
grafika trójwymiarowa, 493 API fragmentów, 642
ContentHandler, 381
DataManager, 308, 318
H do pobierania danych, 354
Handler.Callback, 246
Harmony, 46 narzędzia android, 64
HDPI, high dots per inch, 192 obiektów DAO, 312
hierarchia ustawień, 297 OnItemLongClickListener, 377
hierarchia widoków, 139, 140 Parcelable, 211
HTTP, HyperText Transfer Protocol, 358 protokołu HTTP, 359
Hudson, 617–622 renderscript, 495
SAX, 379
I sprzętowy, 34
użytkownika, 186, 190
I, info, 673 wygenerowany, 211
IDE, 34 interfejs wysokopoziomowy
IDEA, 677 JetPlayer, 462
identyfikator MediaPlayer, 462
artifactId, 597 SoundPool, 462
R.string.deal_details, 79 IoC, inversion of control, 560
URI, 350 izolowanie połączeń, 373
identyfikatory
rejestracyjne, 233
specjalne, 110
J
tekstur, 518 JavaScript, 680
użytkownika, 110 JAXB, Java API for XML Binding, 47
tekstur, 516 JDBC, 316
w układach, 151 JDT, Java Development Tools, 34
zarezerwowane zasobów, 86 Jenkins, 618
implementacja interfejsu OnDragListener, 660 język
inflating, 86 HTML5, 683
informacje o projekcie, 588 IDL, 208
inicjowanie testów, 543 Scala, 685
XPath, 380
Skorowidz 717

języki AsyncTask, 87, 203, 256–268


do testowania, 553 BasicHttpParams, 373
DSL, 525, 550 Bomb, 699
programowania, 683–686 BrewLocationOverlay, 431, 433
JRE, Java Runtime Environment, 45 Bundle, 122
JSON, JavaScript Object Notation, 48, 389–392 Camera, 60, 474
JVM, Java Virtual Machine, 45 Canvas, 478, 483, 519
CheckBoxPreference, 298
ClipData, 660
K ColouredPyramid, 510, 512
kanał RSS, 87 Configuration, 124
karta SD, 207, 281, 446 ConnectivityManager, 398
katalog ContentProvider, 75
anttasks, 587 Context, 91, 92
assets, 78, 443 CursorAdapter, 450
cache, 55 CustomButton, 490, 492
drawable-hdpi, 192 DataManager, 322
drawable-ldpi, 192 DataManagerImpl, 319–322
gen, 38 DealDetails.class, 101
główny, 53 DealDroidApp, 104, 535
raw, 76 DealExporterTest, 558
res, 39 DealList, 75, 84
src, 38 DealsAdapter, 88
tools, 61 DealsApp, 636
z narzędziami, 588 DefaultHandler, 381
kategoria DefaultHttpClient, 370, 372
Build, 624 DefaultHttpRequestRetryHandler, 394
Configuration Matrix, 626 FileDescriptor, 294
LAUNCHER, 102 FileOutputStream, 294
klasa FileUtil, 288
Activity3, 127 FragmentManager, 650
ActivityInstrumentationTestCase2, 546 FragmentTransaction, 655
ActivityUnitTestCase, 540, 542 Geocoder, 425, 426
Adapter, 86, 91, 93 GeoPoint, 432
AlarmManager, 208, 222, 226 GetMovieRatingTask, 378
AlarmReceiver, 224 getView, 94
android.app.AlarmManager, 222 GridAdapter, 445
android.app.Notification, 218 Handler, 272, 417
android.content.ContentProvider, 348 HashMap, 161
android.content.Intent, 332 HashSet, 459
android.content.pm.PackageManager, 438 HttpClient, 367, 370
android.content.res.AssetManager, 442 HttpContext, 367
android.media.SoundPool, 462 HttpURLConnection, 360, 364, 365
android.provider.MediaStore.Images, 450 IdleHandler, 276
android.provider.MediaStore.Video, 450 ImageHandler, 253
android.R.attr, 145, 167 InstrumentationTestRunner, 539
android.R.style, 167 Instrumentation, 544, 548
android.R.styleable, 167 InstrumentationTestCase, 534
android.view.View, 82 InstrumentationTestRunner, 670
AndroidHttpClient, 374 Intent, 96, 99
AndroidTestCase, 534 IntentFilter, 96, 102, 103
ApplicationTestCase, 535 ItemizedOverlay, 430
ArrayAdapter, 87, 91–94 java.lang.Thread, 241
AssetManager, 442 java.util.Timer, 203
JsonMovieParser, 391
718 Skorowidz

klasa Song, 449


JsonStringer, 612 SQLiteOpenHelper, 304, 306
LayoutInflater, 141 Stock, 210
LifecycleActivity, 122 StrictMode, 674, 675
ListActivity, 86 Stub, 212
ListView, 84 Surface, 472
LocationHelper, 416, 421 SurfaceHolder, 472
LocationListener, 420 TabActivity, 86
LocationManager, 407 TableLayout, 149
Looper, 273–276 TestCase, 532
Main, 39 TexturedPyramid, 514, 517
MapController, 429 Thread, 110, 269
MapResults, 428, 430 ThreadPoolExecutor, 250
MapView, 430 ThreadSafeClientConnManager, 371, 373
MediaPlayer, 459–465 Timer, 223, 271
MediaRecorder, 474 TimerTask, 223
MediaScannerConnection, 468 Triangle, 501, 503
MediaStore, 447, 450 Typeface, 487
Message, 246, 272 UpdateNoticeTask, 368
MockOutputStream, 558, 559 Uri, 450
ModelBase, 306 URL, 360
MonkeyDevice, 703 URLConnection, 364
MonkeyHelper, 708 VideoView, 464
MonkeyImage, 703 View, 41
MonkeyRunner, 702 ViewHolder, 160
Movie, 305 WakeLock, 228
MovieAdapter, 159 WebView, 678
MovieCategoryTable, 310 XMLHttpRequest, 683
MovieDao, 313–317 XmlPullMovieParser, 386
MovieTable, 308 klasy
MyOpenGLRenderer, 498 aktywności, 39, 84, 97
obsługi protokołów, 360 animacji, 454
OpenGLPyramidActivity, 508 anonimowe wewnętrzne, 87
OpenGLTexturedPyramidActivity, 514 pakietu com.google.android.maps, 428
OpenGLTriangleActivity, 503 pamięci, storage classes, 309
OpenHelper, 308 zastępcze, shadow class, 561
Paint, 491, 493 klucz API, 424, 429
Parcel, 211 klucze obce, 311
PendingIntent, 225 kod
PortfolioManagerService, 204, 212 5xx, 396
PortfolioStartupReceiver, 224 bajtowy, 48, 579
PreferenceActivity, 296, 298 kreskowy, 469
ProviderTestCase2, 534 obiektu graficznego, 179
Pyramid, 507 QR, 31, 469
R, 38 źródłowy, 579
RemoteViews, 221 kolejka komunikatów, 246, 275
RobolectricTestRunner, 562 kolejność testów, 537
SchemeRegistry, 372 kolor płótna, 480
Section, 92 kolory, 171
Service, 696 kompilacja, 683
ServiceTestCase, 534 kompilator
ShapesAndTextView, 485 dx, 62
ShareActivity, 332 JIT, 50
SharedPreferences, 295 komponent obsługi, handler, 245
Solo, 553
Skorowidz 719

komponenty
aplikacji, 72
M
platformy, 31 macierz
komunikacja konfiguracji, 627
międzyprocesowa, 205, 333 rzutowania, 509
z aplikacją, 218 manifest aplikacji, 73
z serwerem HTTP, 367 manifest BrewMap, 424
z usługą, 208 manipulowanie macierzami, 509
komunikat o błędzie, 669 mapy, 422
komunikaty, 246, 272 marginesy
HTTP, 369 wewnętrzne, padding, 145, 184
rozgłoszeniowe, 398 zewnętrzne, margin, 145
konfiguracja maska uprawnień, 344
ekranu, 189
maszyna
procesu budowania, 621
stanowa, 496
ProGuarda, 689
wirtualna, 33
sieci, 398
wirtualna Dalvik, 45, 48, 671
skryptu budowania, 587
wirtualna Zygote, 49, 670
wyświetlacza, 191
Maven, 592–607
konfiguracje
Maven Central, 594
testów, 555
zadań, 622 mechanizm przeciągania, 656
konfigurowanie mechanizm uruchamiania testów, 562
menu, 89 menedżer
obiektu klienta, 372 LocationManager, 407, 409, 411
projektów testowych, 563 połączeń, 371
rejestrowania, 473 układu, 143, 146
kontener, 661 FrameLayout, 146
kontener LinearLayout, 80 LinearLayout, 147
kontrolka, 82 RelativeLayout, 150
ListView, 81, 89, 141, 170 TableLayout, 149
Spinner, 81, 87 metoda
kształty, 176, 178 Activity.getApplication, 546
kursor, 449 addToPortfolio, 212
kwalifikatory zasobów, 194 AsyncTask.get, 547
bindService, 213
L Context.getCacheDir, 292
Context.startActivity, 102
LDPI, low dots per inch, 192 createLowPriceNotification, 220
liczby zmiennoprzecinkowe, 501 createPackageContent, 345
licznik egzemplarza, 128 DealDroidApp.onCreate, 536
LinearLayout, 147 DealList.onCreate, 570
Linux, 51 debugEvent, 122
lista, 84 doInBackground, 260
ofert, 645 drawArrays, 502
utworów, 458 find, 318
wtyczek, 621 finish, 123
lokalizacja, 411 getActionBar, 654
getApplication, 536
Ł getExternalStoragePublicDirectory, 445
getFrontFacingCamera, 439
łańcuchy znaków, 77 getGpsStatus, 421
łączenie getInstrumentation, 548
atrybutów widoków, 168 getLastNonConfigurationInstance, 130, 266
Eclipse z Mavenem, 607 getPortfolio, 216
elementów projektu, 43 getView, 250
720 Skorowidz

metoda testThatAllFieldsAreSetCorrectly, 543


glBindTexture, 518 texImage2D, 516
glClearColor, 499 Thread.sleep, 275
glDrawArrays, 512 Thread.start, 241
glRotatef, 508 toString, 342, 556
gluPerspective, 509 toString, 343
handleMessage, 254 TouchUtils.clickView, 549
HttpClient.execute, 239 updateStockData, 216, 223
Instrumentation.runOnMainSync, 549 waitAndUpdate, 547
invokeMenuActionSync, 543 waitForIdleSync, 549
ListActivity.onListItemClick, 161 metody
ListView.setChoiceMode, 161 cyklu życia, 39, 114, 120
Menu.add, 90 opt*, 392
Movie.imdbLookup, 382 testowe, 543
myClickHandler, 692 model
notifyDataSetChanged, 95 ACID, 57
obsługi kliknięcia, 693 DOM, 683
onActivityResult, 332 Movie, 305
onCreate, 83, 115, 205, 416 OSI, 358
onCreateOptionsMenu, 90, 98 POM, 593, 615
onCreateView, 646 model-widok-kontroler, 84, 95
onDestroy, 115, 205 modyfikator
onDraw, 492 in, 209
onDrawFrame, 499, 510, 517 inout, 209
onMessage, 233 out, 209
onOptionsItemSelected, 90 modyfikowanie
onPause, 83, 90, 115, 116 dźwięku, 462
onRestart, 115 wyglądu listy, 170
onRestoreInstanceState, 127 monkeyrunner, 710
onResume, 83, 115, 116, 414 kod źródłowy, 702
onRetainNonConfigurationInstance, 268 skrypty, 703
onServiceConnected, 213 uruchamianie, 701
onStart, 115, 205 motywy, 167, 168
onStartCommand, 225 multimedia, 436, 475
onStop, 115
onTabSelected, 654
openDealInBrowser, 100 N
ParseFeedTask.execute, 87 nagrywanie, Patrz rejestrowanie
populate, 432 nakładka itemizedoverlay, 432
query, 350 narożniki, 195
resetListItems, 88 narzędzia
retryRequest, 394 cURL, 374
saveImage, 334 dla platformy, 61
scheduleAtFixedRate, 272 GLU, 509
sendLocationToHandler, 419 pakietu SDK, 61, 579
setHighPriceNotification, 221 podstawowe, 61
setLastEventInfo, 221 uruchamiane z wiersza poleceń, 62
setLastFocusedIndex, 432 narzędzie
setListAdapter, 86 aapt, 63, 76, 579
setTextViewText, 221 Activity Manager, 669
shareDealUsingChooser, 99–102, 653 adb, 64, 65, 666
SimpleCursorAdapter.ViewBinder, 450 AIDL, 209
startActivity, 543 android, 63
startRecording, 473 Apache Ant, 582, 584
sync, 294
Skorowidz 721

ddms, 65, 110, 325, 706 HTML-u, 58


hierarchyviewer, 140 intencji, 339
logcat, 124 JavaScriptu, 58
maven-android-sdk-deployer, 610, 613 kluczy obcych, 311
Monkey, 568–572 map, 423
Monkeyrunner, 665, 701 multimediów, 56
ProGuard, 687 ponawiania żądań, 395
sqlite3, 324 protokołów, 360
zipalign, 581 rejestracji, 232
nawiasy klamrowe, 390 stanu egzemplarza, 129
nawigacja, 655 zarządzania zasobami, 638
nazwy w liczbie mnogiej, 77 odbiornik
NDK, Native Development Kit, 34 AlarmReceiver, 228
niepowodzenie testu, 537 LocationListener, 414, 416
notacja ?, 173 OnSharedPreferenceChangeListner, 296
notacja @, 173 TabListener, 654
typu BroadcastReceiver, 73, 75, 111
odczyt
O danych, 295
obiekt pliku, 284
IntentFilter, 75 odinstalowanie aplikacji, 33
POJO, 377 odświeżanie danych, 216
R.layout.main, 39 odtwarzanie
SQLiteDatabase, 312 dźwięku, 462
obiekty DAO, 312 filmów, 463
obiekty graficzne, 41, 172–185 multimediów, 453
dziewięciopolowe, 185 odwrotne geokodowanie, 425
elementy, 178 odwrócenie sterowania, 560
kształty, 176 odwzorowywanie UV, UV mapping, 514
predefiniowane, 176 określanie położenia, 150, 414, 612
selektory, 179 opcja
skalowanie, 182 anyDensity, 190
stany, 181 Create Activity, 39
obiekty klasy START_NOT_STICKY, 225
Application, 103 OpenGL, Open Graphics Library, 494, 500, 519
HttpClient, 371 OpenGL ES, 493, 495
MediaPlayer, 465 operacje na plikach, 288
SharedPreferences, 295 operacje wejścia-wyjścia, 440
obiekty modelu, 303 operator APN, 397
Movie, 305 opis celów, 605
obiekty typu orientacja, 640
DefaultHttpClient, 369 orientacja pionowa, 648
Parcelable, 333, 685
Runnable, 460 P
obiekty w komunikacji HTTP, 367
obrót ekranu, 124 pakiet
obsługa android.bluetooth, 60
animacji, 457 android.database, 301
awarii sieci, 393 android.database.sqlite, 301
CSS3, 58 android.graphics, 60
czujników, 58 android.hardware, 60
dotknięć, 432 android.location, 60
ekranów, 189, 194 android.media, 60
grafiki, 477 android.opengl, 60
722 Skorowidz

pakiet planowanie wykonania usługi, 223, 224


android.provider, 352 plik
android.telephony, 60 .nomedia, 291
android.widget, 82 Activity3.java, 127
APK, 70 AIDL, 338
com.google.android.maps, 428 android.jar, 529, 598, 691
GLUT, 509 android.R.styleable, 168
java.io, 47 AndroidManifest.xml, 39, 43, 74, 230, 270, 531,
java.lang, 47 639
java.net, 47 build.xml, 583–586
java.nio, 47 button_bar.xml, 153
java.sql, 47 classes.dex, 580
java.util, 47 colors.xml, 172
javax.sql, 47 DealDetails.java, 97
NDK, 60 DealDroidApp.java, 104
SDK, 30, 59 DealFragment.java, 647
pakiety DealList.java, 84, 88–90
dla programistów Javy, 34 deallist.xml, 80
najwyższego poziomu, 46 deals.txt, 557
pakowanie, 683 DealsAdapter.java, 93
pamięć DetailsActivity.java, 651, 653
podręczna, 214, 216 DownloadTask.java, 258
VRAM, 488 droid.gif, 41
wewnętrzna, 280 dziennika, 672
zewnętrzna, 280 FilmstripFragment.java, 649
parametry układu, 144, 148 ImageHandler.java, 254
parser InternalStorage.java, 284
JSON, 391 IStockService.aidl, 209
SAX, 381, 382, 388 LifecycleActivity.java, 120
SAXMovieParser, 383 list_selector.xml, 176, 179
StAX, 380 Main.java, 39, 118
typu pull, 47 main.xml, 40, 242
XmlPull, 380, 385, 388 main_rules.xml, 588
parsery, 380 mapping.txt, 691
strumieniowe, 380, 384, 392 maps.jar, 610, 612
typu pull, 380 modelu POM, 596, 599
typu push, 380 movies.xml, 158
partycja MoviesAdapter.java, 252
systemowa, 33 mymovies.db, 324
specjalna, 281 MyMovies.java, 157, 362
pary plugin.jar, 709
klucz-wartość, 670 plurals.xml, 78
nazwa-wartość, 234 pom.xml, 593
pasek akcji, Action Bar, 650–652 Preferences.java, 297
perspektywa preferences.xml, 297
DDMS, 346 proguard.cfg, 694, 698
Hierarchy View, 140 R.java, 38, 42
pętle komunikatów, 272 resources.arsc, 580
piksele, 196 SectionDetailsFragment.java, 645, 646
niezależne od gęstości, 195, 196 seeds.txt, 691
niezależne od skali, 196 settings.xml, 601
piramida, 505 ShareActivity.java, 331, 332
kolorowanie, 510 SimpleImageDownload.java, 241
tekstura, 513 Stock.aidl, 210
strings.xml, 41, 77
Skorowidz 723

styles.xml, 166, 270 ProGuard, 687, 694, 700


title.9.png, 185 dane wyjściowe, 689
usage.txt, 691 konfiguracja, 688
pliki opcje, 698
.aidl, 209 reguły, 691, 695, 696
.class, 45, 48, 62 projekt
.dax, 63 apache ivy, 592
.dex, 49, 63, 684 brewmap, 422
.jar, 74 canvasdemo, 478
.ttf, 487 dealdroidmonkeyrunner, 706
APK, 109 dealdroidrobolectrictest, 562
dziennika, 673, 691 dealdroidrobotiumtest, 551
dźwiękowe, 457 dealdroidtest, 529
graficzne, 489 dealdroidwithexport, 557
multimedialne, 440, 443 fileexplorer, 283
PNG, 183 handlingactivityinterruptions, 265
typu SharedPreferences, 344 helloant, 584
układu, 144 helloanttest, 619
z bazą, 326 hellomaven, 596
z kluczem, 581 hellomavenwithmaps, 614
imagedownloadwithmessagepassing, 247
z zasadami, 589
lifecycleexplorer, 118
zasobów, 77, 78
locationinfo, 408
płótno, 481
mediamogul, 441
pobieranie danych kontaktowych, 350
mymovies, 138
podgląd filmu, 472
mymoviesdatabase, 296
podpisywanie plików
mymovieswithhttpclient, 366
APK, 581
mymovieswithimages, 250
JAR, 581 mymovieswithimagesasynctask, 257
podwójne buforowanie, 488 mymovieswithsplash-screen, 269
podział układu, 155 mymovieswithupdatenotice, 361
POJO, plain old Java object, 377 opengldemo, 497
pokaz slajdów, 455 producerconsumerwithlooper, 273
pole widzenia, 509 proguarded, 692
polecenie ruboto, 685
logcat, 672 simpleimagedownload, 240
ls, 53 stockportfolio, 202
mount, 281 promień narożnika, 195
ps, 112 protokół
sqlite3, 324 FTP, 358
połączenie HttpURLConnection, 363 HTTP, 360
POM, Project Object Model, 593 HTTP/1.1, 369
pomiary widoków, 142 XVNC, 624
ponawianie żądań, 393, 395 przebieg
porządkowanie widoków, 143 pomiarowy, 141
powiadomienia, 217, 229 rozmieszczania, 141
powiadomienia typu toast, 217 przechowywanie
powłoka poleceń, 324 danych, 635
powłoka urządzenia, 667 ustawień, 294
poziomy komunikatów, 673 przechwytywanie wyjątków, 260
priorytety procesów, 113 przeciąganie, 655
proces budowania, 578, 589, 604 przeciąganie StackView, 657, 660
procesor graficzny, 493 przeglądanie pliku z bazą, 326
procesy, 110, 214 przekazywanie
program wyboru, chooser, 98, 100 informacji między wątkami, 249
programowaniem natywne, 60 komunikatów, 247, 329
724 Skorowidz

przekształcanie rysunek tytułowy, 182, 185


adresu pocztowego, 425 rzadkie macierze konfiguracji, 630
układów na klasy, 86 rzutowanie, 506
przekształcenia afiniczne, 330 ortogonalne, 506
przełącznik kształtów, 180 perspektywiczne, 506
przenośność, 186 rzutowanie perspektywiczne, 506
przestrzeń barw ARGB, 219
przesyłanie
danych, 331 S
komunikatów, 272
SAX, Simple API for XML, 47
przetwarzanie
scalanie układów, 152
dokumentu XML, 381
scena trójwymiarowa, 505
danych, 379
schemat architektury, 32
przezroczystość, 171
SD, Secure Digital, 281
pseudolosowość, 571
SDK, Software Development Kit, 29
pula wątków, 250, 259
selektor
punkt montowania, 53, 280
listy, 177, 180
punkty rozszerzeń, 584
obiektów graficznych, 179–181
przezroczysty, 177
R serializowanie danych, 375
Service, 75
RC, Remote Control, 553 serwer XVNC, 624
referencja do serwery budowania, 615, 616
aktywności, 263, 264 sieć
klucza obcego, 310 3G, 393
obiektu, 295 Wi-Fi, 393
widoku StackView, 658 silnik
zasobów, 79 SQLite, 57
reguły ProGuarda, 692 V8, 58
rejestracja, 231 WebKit, 58
rejestrowanie silniki
dźwięku, 470 bazodanowe, 56
filmów, 470, 473 motywów, 165
zdjęć, 465 skalowanie, 195
relacje, 57 skalowanie widoków, 182
relacyjne bazy danych, 299 sklep Android Market, 31, 435, 476
RelativeLayout, 150 skrypt monkeyrunnera, 703
renderowanie słowo kluczowe
obiektów, 506 synchronize, 244
obrazu, 499 volatile, 244
w odrębnym wątku, 499 sortowanie, 89
repozytorium Maven Central, 594, 610 specyfikacja
rodzaje SAX, 376
zdarzeń, 572 XmlPull, 376
połączeń, 371 sprawdzanie cen, 218
rozmieszczanie widoków, 142 SQL, Structured Query Language, 300
rozszerzalny obszar, 184 SQLite, 56, 299–301, 311
rozszerzenie SQLiteDatabase, 307
Google APIs Add-On, 423 SQLiteManager, 325
SQLiteManager, 325 stan egzemplarza, instance state, 108, 126, 129
rozwijanie układu do klasy, 141 aktywności, 125
RPC, remote procedure call, 336 niezwiązany z konfiguracją, 129
rysowanie, 519 stan
figur, 487 GPS-u, 421
kształtów, 481, 482 testowy, test fixture, 543
Skorowidz 725

trwały, 126 technologia


widoczności, 114 JDBC, 316
widoku, 181 stax, 380
StAX, Streaming API for XML, 47, 380 tekst, 172
sterowanie zdarzeniami, 572 tekstury, 494, 513
stopniowe wzbogacanie, 439 telefon wielofunkcyjny, 436
stos aktywności, 83, 123, 132, 133 test jednostkowy, 533
struktura testowanie
katalogu głównego, 53 aktywności, 539
obiektów graficznych, 174 aplikacji, 525, 534
projektu, 38 dostawców treści, 534
projektu wtyczki, 707 klas aplikacji, 536
układu, 144 scenariuszy, 527
strumień usług, 534
InputStream, 468 testy, 523
java.io.InputStream, 446 funkcjonalne, 526, 544
styl STROKE, 491 jednostkowe, 525, 539
styl tła, 170, 174 JUnit, 561
style, 166, 171 losowe, 554
symbol obciążeniowe, 567
#, 53 oparte na Javie, 528
$, 53 Robolectrica, 563
/, 53 z instrumentacją, 546, 554, 555
@, 79 tło okna, 173
symulowanie połączenia telefonicznego, 66 TMDb, The Movie Database, 377
synchroniczne wywoływanie usługi, 336 trójkąt, 500
system tryb
Apache Maven, 592 automatycznego skalowania, 190
DBMS, 57 letterbox, 190
ext4, 293 pełnoekranowy, 480
X11, 624 tworzenie
systemy adaptera, 92
budowania, 622 aktywności MapActivity, 427
operacyjne, 33 aplikacji, 35
plików, 281 aplikacji BrewMap, 424
plików z księgowaniem, 293 aplikacji na tablety, 635, 638
szerokość geograficzna, 404 baz danych, 303, 306
szybkość, 407 identyfikatora, 151
interfejsu użytkownika, 186, 458, 657
motywów, 165
Ś obiektów DAO, 312
obiektu trójwymiarowego, 507
ścieżka paska akcji, 652
bezwzględna, 52 piramidy, 504
względna, 52 plików multimedialnych, 465
środowisko pliku plugin.jar, 709
IDE, 37, 596 pokazu slajdów, 455
IDE Eclipse, 34, 577 powiadomień, 217–220
uruchomieniowe, 45 powiązania z aktywnością, 266
uruchomieniowe Dalvik, 33 projektu testowego, 530, 531
referencji współużytkowanej, 275
T stosu aktywności, 132
trójkąta, 502
tabele, 304 usługi, 201, 202
tablety, 641 wtyczki, 706
TDD, test-driven development, 523, 526 zakładek paska akcji, 653
726 Skorowidz

typ systemowe, 223


alarmu, 226 uruchamianie, 204, 206
serializowany, 210 wywoływanie, 213
SurfaceView, 82 znacznik, 204
typy ustawianie języka systemu, 670
animacji, 454 ustawienia supports-screens, 191
blokad, 227 usuwanie aktywności, 123, 130
intencji, 101 usypianie wątków, 275
rzutowania, 506
w AIDL-u, 209
zasobów, 79 W
wady
U parserów SAX, 385
Robolectrica, 566
układ, 73, 80, 143 środowiska Eclipse, 576
aplikacji MyMovies, 182 WAP, Wireless Access Protocol, 357
FrameLayout, 140, 455 warstwa
LinearLayout, 140 pośrednia, 33
LinearView, 139 prezentacji, 40
pokazu slajdów, 455 transportowa, 376
RelativeLayout, 94 wartość
układu @null, 173
atrybuty, 144 1e6, 419
dołączanie, 154 wątek, 240, 262
parametry, 145, 148 główny, 110
scalanie, 152 interfejsu użytkownika, 244, 273
struktura, 144 konsumenta, 273
wbudowane menedżery, 146 obsługi, 245
układy producenta, 273
niestandardowe, 89, 221 roboczy, 262
pionowe, 644 usługowy, daemon thread, 272
poziome, 643 z pętlą, 276
z siatką, 656 zegara, 272
uprawnienia, 54, 75, 281, 344 wątki
uruchamianie aplikacji, 110
aktywności, 669 robocze, 244
aplikacji, 44, 668 wczytywanie pliku PNG, 489
emulatora, 623, 624 WebKit, 58
Hudsona, 620 wiązanie aktywności z usługą, 212
komponentów, 669 widok, 73, 139, 143
usługi, 204, 206, 207 android.view.SurfaceView, 464
urządzenia dotykowe, 633, 661 CanvasView, 484
CheckBox, 181
usługa, 73
GLSurfaceView, 498
Apple Push Notification Service, 230
GridView, 446
Cloud to Device Messaging, 222, 229, 234
ImageView, 260, 454
Device Messaging, 208
ListView, 164
do zarządzania akcjami, 215
LogCat, 275
LAYOUT_INFLATER_SERVICE, 94
MapView, 429
PortfolioManagerService, 203
ShapesAndTextFontView, 488
TMDb, 381, 391
StackView, 657
typu IntentService, 233
SurfaceView, 464
usługi, 201, 202, 214
TextView, 41
komunikacja, 208
VideoView, 464
sieciowe, 375
WebView, 678
Skorowidz 727

widoki wyjątek
hierarchia, 140 ANR, 237, 250
listy, 170 IOException, 394
nagłówka, 161 NotFoundException, 608
pomiary, 142 RuntimeException, 570
porządkowanie, 143 SecurityException, 75, 410
rozmieszczanie, 142 wymagania
stopki, 162 aplikacji, 639
wyświetlanie, 139, 141 funkcjonalne, 524
złożone, 89 sprzętowe, 437
wielkość ekranu, 188 wynik testu, 533
wielozadaniowość, 55, 199 wysokość, 407
wiersz poleceń, 62 wyszukiwanie, 448
wierzchołki trójkąta, 502 wyszukiwanie numerów, 349
właściwości wyświetlacze, 187
Androida, 670 wyświetlanie
Javy, 670 bitmap, 489
nowego projektu, 37 celów, 588
projektu HelloAndroid, 37 cienia, 660
systemowe, 670 ekranów powitalnych, 268
właściwość elementów OverlayItems, 430
adb.device.arg, 628 filmów, 462
Build Target, 37 losowego koloru, 479
Create Activity, 38 oferty, 646
jar.libs.dir, 591 podłączonych urządzeń, 65
włączanie lampek LED, 219 procesów, 112
WML, Wireless Markup Language, 357 tekstu, 485
wprowadzanie zależności, 560 usług, 205
współbieżność, 238, 244 widoków, 139, 141
współczynnik proporcji, 509 zdjęć, 454
współdzielenie kontekstu, 341 wywołanie
współrzędne Anta, 629
geograficzne, 404 metody HttpClient.execute, 239
GPS, 672 zwrotne, 264
przestrzenne, 510 wywoływanie
znormalizowane, 514 asynchroniczne, 338
współużytkowanie danych, 327–329, 341, 637 synchroniczne, 336
wstępne skalowanie, prescaling, 191 zdalne, 336
wtyczka, 706 wywoływanie usługi, 213
ADT, 34, 39, 44, 608 wzorzec
Android Emulator Hudsona, 618, 619 DAO, 302
Google’a, 36 MVC, 96
Gradle, 606 ViewHolder, 160
Green Balls, 620
JDT, 608 X
m2eclipse, 607, 608, 610
m2eclipse--android-integration, 608 XML, Extensible Markup Language, 42, 376
Mavena, 596, 607, 689
maven-android-plugin, 601
SBT, 606
Z
wybieranie pliku, 451 zaciemnianie
wyciekanie pamięci, 263 kodu, 698
wydajność parserów, 388 nazw metod, 700
728 Skorowidz

zadania zmiana
asynchroniczne, 256 konfiguracji, 125
testowe, test cases, 532, 534, 550 konfiguracji sieci, 399
zalety tabletów, 639 orientacji, 125
zamknięcie procesu, 111 sieci, 396
zapis zmienna środowiskowa $PATH, 36, 61, 588
danych, 295 zmniejszanie zużycia energii, 59
pliku, 284, 287 znacznik
zarządzanie <merge>, 155
akcjami, 215 czasu, 571
bazami danych, 299 kierunkowy, directional tag, 209
testami, 529 usługi, 204
wątkami, 249 znak
wątkami roboczymi, 265 @, 41
zasilaniem, 226–228 +, 151
zasobami, 638 zachęty, 53
zasoby, 73, 76 znaki @+id, 151
DealDroid, 77 zrzut stosu, 699
multimedialne, 441
zasób układu, 40
zawieszanie się aplikacji, 239 Ż
zdalna usługa, 214
żądanie
zdalne wywołanie procedur, 335
GET, 368
zdarzenie
HTTP, 359
BOOT_COMPLETED, 207
rejestracji, 231
CONNECTIVITY_ACTION, 398
żyroskop, 58
parsera SAX, 382
zdjęcia, 466
zegar, 271
zgłaszanie zdarzeń, 571
zintegrowane środowisko programowania, 34
złączenia, 300

You might also like