Professional Documents
Culture Documents
Helion - Android w Praktyce
Helion - Android w Praktyce
Helion - Android w Praktyce
ISBN: 978-83-246-6611-9
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.
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.
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
3
4 Spis treści
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
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.
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
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
23
24 O ilustracji z okładki
Część I
Tło historyczne i podstawy
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
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
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).
Rysunek 1.3.
Aplikacja
HelloAndroid
uruchomiona
w emulatorze
i wyświetlająca
na ekranie
proste elementy
— tekst i grafikę
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).
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.
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
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;
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
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>
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.
package com.manning.aip.helloandroid;
</activity>
</application>
</manifest>
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?
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
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
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
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.
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ę.
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.
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ń.
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.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>
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
69
70 ROZDZIAŁ 2. Podstawy tworzenia aplikacji na Android
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
<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>
</manifest>
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
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.
<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>
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
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
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.deallist);
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.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
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.
@Override
protected void onListItemClick(ListView listView,
2.6. Aktywności 89
@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.
@Override
public void onPause() {
if (progressDialog.isShowing()) {
progressDialog.dismiss();
}
super.onPause();
}
2.7. Adaptery 91
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!
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.
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));
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
<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>
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dealdetails);
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);
}
}
...
Listing 2.12. Obsługiwane za pomocą obiektów klasy Intent akcje dzielenia się
informacjami w klasie aktywności 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.
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
@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>();
}
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">
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
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ń
107
108 ROZDZIAŁ 3. Zarządzanie cyklem życia i stanem
Można
Metoda Opis Następna
zamknąć?
@Override
3.2. Cykl życia aktywności 119
@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
public LifecycleActivity() {
super();
this.className = this.getClass().getName();
}
@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();
}
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
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.
Rysunek 3.8. Dane wyjściowe narzędzia logcat pozwalają zobaczyć, jakie metody
cyklu życia są wywoływane dla aktywności przy zmianie orientacji
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.
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.
@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
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);
. . .
@Override
public Object onRetainNonConfigurationInstance() {
return new Date();
}
}
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ąć.
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
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
137
138 ROZDZIAŁ 4. Precyzja co do piksela
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
140 ROZDZIAŁ 4. Precyzja co do piksela
android:layout_height="fill_parent"
/>
</LinearLayout>
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.
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>.
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.
Atrybut Działanie
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
android:layout_centerVertical="true"
android:background="@android:color/white"
/>
</RelativeLayout>
Aby następnie dołączyć fragment lub komponent układu do innego pliku, wystar-
czy użyć elementu <include>.
<include layout="@layout/button_bar" />
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
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.
setContentView(R.layout.main);
@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
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.
movieCollection.put(position, false);
}
}
@Override
public View getView(int position, View convertView,
ViewGroup parent) {
return listItem;
}
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"
/>
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
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.
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.
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ę.
...
</style>
</resources>
<style name="MyTextAppearance">
<item name="android:textColor">#F00</item>
<item name="android:textStyle">bold</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
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>
Nazwa
Opis Atrybuty
elementu
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
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>
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
Stan Opis
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).
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>
<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>
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)
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" ...>
<supports-screens
android:smallScreens="false"
android:normalScreens="true"
android:largeScreens="false"
android:xlargeScreens="false"
android:anyDensity="false"
/>
...
</manifest>
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.
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
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.
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.
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
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
W tym rozdziale
Q Usługi i wielozadaniowość
Q Tworzenie zadań wykonywanych w tle
Q Odtwarzanie usuniętych zadań
199
200 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>
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ć.
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.
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
package com.flexware.stocks.service;
import com.flexware.stocks.Stock;
interface IStockService{
void addToPortfolio(in Stock stock);
List<Stock> getPortfolio();
}
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.
quantity = parcel.readInt();
}
}
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.
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.
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
Listing 5.12. Sprawdzanie, czy ceny nie osiągnęły maksymalnego lub minimalnego
poziomu
<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>
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.
@Override
public void onReceive(Context context, Intent intent) {
Intent stockService =
new Intent(context, PortfolioManagerService.class);
context.startService(stockService);
}
}
}
}
@Override
public void onReceive(Context context, Intent intent) {
acquireLock(context);
Intent stockService =
new Intent(context, PortfolioManagerService.class);
context.startService(stockService);
}
}
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.
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.
<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>
@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);
}
}
@Override
protected void onHandleIntent(Intent intent) {
try{
String regId = intent.getStringExtra("regId");
// DO ZROBIENIA: wysyłanie identyfikatora regId na serwer.
} finally {
AlarmReceiver.releaseLock();
}
}
}
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
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
237
238 ROZDZIAŁ 6. Wątki i współbieżność
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.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
<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
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
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();
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
248 ROZDZIAŁ 6. Wątki i współbieżność
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).
Listing 6.4. Nowy układ elementu z filmem obejmuje miniaturę obok tytułu
<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>
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
@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);
}
}
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.
@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
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
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
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
@Override
protected void onDestroy() {
super.onDestroy();
worker.disconnectContext();
}
@Override
public Object onRetainNonConfigurationInstance() {
return worker;
}
}
@Override
0 TECHNIKA 25. Przygotowanie do zmian w konfiguracji 267
@Override
protected void onPostExecute(String result) {
if (context != null) {
Toast.makeText(context, result, Toast.LENGTH_LONG).show();
}
}
}
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.
...
<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>
</application>
…
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.splash_screen);
@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ść
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
@Override
public void run() {
Looper.prepare();
Looper.loop();
}
}
@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();
}
}
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
W tym rozdziale
Q Odczyt i zapis plików
Q Podawanie i zapamiętywanie współużytkowanych
ustawień
Q Korzystanie z baz SQLite
279
280 ROZDZIAŁ 7. Lokalne zapisywanie danych
Rysunek 7.1. Polecenie mount pozwala zobaczyć wybrane miejsca i typy używane
przez systemy plików z Androida
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.
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.
input.setText("");
output.setText("");
} 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
Rysunek 7.5. Powłoka adb pozwala sprawdzić plik zapisany w pamięci zewnętrznej
przez przykładową aplikację FileExplorer
private FileUtil() {
}
return true;
}
return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED_READ_ONLY);
}
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;
}
}
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ą.
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ć.
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
@Override
298 ROZDZIAŁ 7. Lokalne zapisywanie danych
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);
}
}
});
}
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.
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
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
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.
@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) {
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.
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
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.
@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();
}
@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).
@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;
}
if (!c.isClosed()) {
c.close();
}
return this.get(movieId);
}
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.
this.context = context;
SQLiteOpenHelper openHelper =
new OpenHelper(this.context);
db = openHelper.getWritableDatabase();
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.
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;
}
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
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
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
327
328 ROZDZIAŁ 8. Współużytkowanie danych między aplikacjami
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.
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.
imgView0.setImageURI(photoUri0);
}
}
}
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.
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>
}
}
}
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
@Override
0 TECHNIKA 37. Zdalne wywołania procedur 337
@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) {} }}});}}
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.
@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);
}
}
@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
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
Listing 8.7. Plik App1.java obejmuje kod metody toString() i zapisuje plik
ze współużytkowanymi ustawieniami
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
@Override
public String toString() {
return "Powitania z metody toString() aplikacji App1!";
}
}
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
@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);
}
}
}
<application android:process="com.manning.aip">
<activity … />
</application>
</manifest>
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.
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.
import android.provider.ContactsContract.CommonDataKinds;
public class ContactManager {
private final ContentResolver resolver;
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
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;
}
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.
@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);
}
}
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?
357
358 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe
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.
„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.
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;
}
}
@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();
}
}
}
@Override
protected void onPostExecute(String updateNotice) {
Message message = new Message();
364 ROZDZIAŁ 9. Protokół HTTP i usługi sieciowe
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”.
@Override
protected void onPostExecute(String updateNotice) {
...
}
}
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
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.
(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
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
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
@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 + "/";
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
TextView rating =
(TextView) dialog.findViewById(R.id.movie_dialog_rating);
rating.setText(movie.getRating());
dialog.show();
}
}
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
@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
case XmlPullParser.START_DOCUMENT:
doSetupWork();
break;
case XmlPullParser.START_TAG:
readElementText(parser.nextText());
break;
...
}
event = parser.next();
}
xpp = XmlPullParserFactory.newInstance().newPullParser();
xpp.setInput(xml, "UTF-8");
skipToTag("name");
movie.setTitle(xpp.nextText());
skipToTag("rating");
movie.setRating(xpp.nextText());
return movie;
}
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
[
{
"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",
...
}
]
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());
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.
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
static {
...
httpClient = new DefaultHttpClient(cm, clientParams);
retryHandler =
new DefaultHttpRequestRetryHandler(5, false) {
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));
}
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 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
403
404 ROZDZIAŁ 10. Najważniejsza jest lokalizacja
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.2.
Aplikacja LocationInfo
wyświetla dostępnych
dostawców położenia
(po lewej)
i szczegółowe
informacje
o wybranym dostawcy
(po prawej)
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.
@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());
@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
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.title_detail);
locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);
@Override
protected void onResume() {
super.onResume();
String providerName =
getIntent().getStringExtra("PROVIDER_NAME");
Location lastLocation =
locationMgr.getLastKnownLocation(providerName);
LocationProvider provider =
locationMgr.getProvider(providerName);
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.
Listing 10.3. Metoda onResume sprawdza, czy dostawca gps jest włączony
@Override
0 TECHNIKA 49. Sprawdzanie stanu dostawcy położenia 415
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);
}
}
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.
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);
locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);
this.handler = handler;
this.handlerCallback = new Thread() {
public void run() {
endListenForLocation(null);
}
};
this.logTag = logTag;
}
if (this.providerName == null) {
sendLocationToHandler(MESSAGE_CODE_PROVIDER_NOT_PRESENT, 0, 0);
return;
}
@Override
public void onLocationChanged(Location loc) {
if (loc == null) {
return;
}
0 TECHNIKA 50. Określanie aktualnego położenia za pomocą odbiornika LocationListener 421
@Override
public void onProviderDisabled(String provider) {
endListenForLocation(null);
}
@Override
public void onProviderEnabled(String provider) {
}
}
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.
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.
@Override
protected List<BrewLocation> doInBackground(List<BrewLocation>... args) {
List<BrewLocation> result = new ArrayList<BrewLocation>();
if (args == null) {
return result;
}
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;
}
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.
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
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.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.map_results);
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>" />
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.
@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();
}
}
Rysunek 10.7.
Używanie intencji
do otrzymania
dodatkowych
informacji na temat
wybranego lokalu
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
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
435
436 ROZDZIAŁ 11. Uatrakcyjnianie aplikacji za pomocą multimediów
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.
<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
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
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Dalszy kod pominięto.
playThemeSong();
}
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
Stała Opis
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
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.
@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;
}
}
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.
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ść.
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.
}
}
<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.
@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);
}
}
count++;
nextSlide();
}
}
});
rightSlide.startAnimation(animation);
currentImage = nextImage;
}
}
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.
@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;
}
}
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.
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.
}
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
@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.
}
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.
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);
}
}
}
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.
@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.
}
Listing 11.15. Kontrolowanie podglądu filmu w cyklu życia obiektu klasy Surface
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
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
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CanvasView(this));
}
@Override
480 ROZDZIAŁ 12. Grafika dwu- i trójwymiarowa
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);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CanvasView(this));
}
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
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.
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRGB(0, 0, 0);
drawShapes(canvas);
drawText(canvas);
}
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.
// Metody onDraw i drawShapes pominięto (są takie same jak na poprzednim listingu).
super(context);
bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.copter);
}
// Metody onDraw i drawShapes pominięto (są takie same jak na poprzednim listingu).
OMÓWIENIE
// ...
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle) {
borderPaint.setPathEffect(borderRadius);
borderPaint.setStyle(Style.STROKE);
borderPaint.setColor(Color.rgb(75, 75, 75));
borderPaint.setStrokeWidth(2F);
borderPaint.setAntiAlias(true);
// ...
}
// ...
}
// ...
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle) {
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.
@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);
}
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.
@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);
}
@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);
}
}
}
public Triangle() {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3 * 3 * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}
@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);
}
@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);
}
}
}
Rysunek 12.13.
Schemat sceny
trójwymiarowej
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).
Rysunek 12.14.
Schemat rzutowania
perspektywicznego
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.
public Pyramid() {
ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(vertices.length * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}
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.
@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);
}
@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);
}
}
}
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
public ColouredPyramid() {
ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(VERTEX_SIZE * 3 * 4);
byteBuffer.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuffer.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.flip();
}
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
@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);
}
@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);
}
}
}
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
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
523
524 ROZDZIAŁ 13. Testowanie i instrumentacja
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
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
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.
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
@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>();
}
...
}
public DealDroidAppTest() {
super(DealDroidApp.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
createApplication();
dealdroid = getApplication();
}
536 ROZDZIAŁ 13. Testowanie i instrumentacja
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
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
public DealDetailsTest() {
super(DealDetails.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
assertEquals("$" + testItem.getConvertedCurrentPrice(),
getViewText(R.id.details_price));
assertEquals(testItem.getTitle(), getViewText(R.id.details_title));
assertEquals(testItem.getLocation(),
getViewText(R.id.details_location));
}
getInstrumentation().invokeMenuActionSync(getActivity(),
DealDetails.MENU_BROWSE, 0);
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.
public DealListTest() {
super("com.manning.aip.dealdroid", DealList.class);
}
instr.waitForIdleSync();
instr.removeMonitor(monitor);
}
}
Listing 13.4. Biblioteka Robotium pozwala używać języka DSL do pisania testów
funkcjonalnych w formie scenariuszy
public DealListRobotiumTest() {
super("com.manning.aip.dealdroid", DealList.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
solo = new Solo(getInstrumentation(), getActivity());
}
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
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
@Override
public void write(byte[] buffer) throws IOException {
Item currentItem = deals.get(itemsWritten++);
assertEquals(currentItem.toString(), new String(buffer));
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
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!
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
@RunWith(RobolectricTestRunner.class)
public class DealDetailsRobolectricTest {
@Before
564 ROZDZIAŁ 13. Testowanie i instrumentacja
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());
}
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ć.
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
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
W tym rozdziale
Q Automatyczne budowanie
Q Zarządzanie budowaniem za pomocą narzędzia
Maven
Q Ciągłe budowanie
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ć.
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
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
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.
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.
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
<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>
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
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:
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
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.
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
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.
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
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>
Listing 14.5. Plik modelu POM prostej aplikacji na Android (część 2.)
<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>
Cel* Opis
[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
<jvmArguments>
<jvmArgument>-Xms256m</jvmArgument>
</jvmArguments>
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ć.
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
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.
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
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
<version>2.2</version>
<scope>provided</scope>
</dependency>
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
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] ------------------------------------------------------------
...
<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.
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.
public SampleTestCase() {
super(HelloAnt.class);
}
0 TECHNIKA 85. Ciągłe budowanie z wykorzystaniem Hudsona 619
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
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
en_US es_ES
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).
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"
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
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
Rysunek 15.1.
Aplikacja DealDroid
w wersji na tablety
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
<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.
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
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
this.currentPosition = position;
showDeal(position);
}
<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
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.
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.
@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
@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);
}
}
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>
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ą.
<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>
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());
}
}
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
665
666 DODATEK A Narzędzia do debugowania
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.
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.
com.manning.aip.dealdroid.test.DealDetailsTest:.
Test results for InstrumentationTestRunner=.
Time: 0.295
OK (1 test)
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.
I Info (informacje) 3
W Warn (ostrzeżenia) 4
E Error (błędy) 5
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
.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);
}
}
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
@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++;
}
class InterWebInterface {
String callback;
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.
<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>
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.
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
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
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).
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
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.
@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
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
-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
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>;
}
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.
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;
}
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
-optimizationpasses 5
new Bomb().explode();
}
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
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
701
702 DODATEK D Monkeyrunner
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.
device_id = devices[choice].split('\t')[0]
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
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.
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
Rysunek D.1.
Struktura
przykładowego
projektu wtyczki
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;
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) {
return true;
}
}
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 .
print hello
...
710 DODATEK D Monkeyrunner
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
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
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
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