K. Markowski, Przykłady zastosowań oraz optymalizacja działania

Transkrypt

K. Markowski, Przykłady zastosowań oraz optymalizacja działania
Przykłady zastosowań oraz optymalizacja działania
komponentu ListView w aplikacjach dla urządzeń
mobilnych z systemem Android
Kacper Markowski1
1 Wydział Inżynierii Mechanicznej i Informatyki
Politechnika Cz˛estochowska
Kierunek Informatyka, Rok III
[email protected]
Streszczenie
System operacyjny Android jest obecnie jednym z najbardziej dynamicznie rozwijających się systemów operacyjnych przeznaczonych dla urządzeń
mobilnych. Szacuje się, że liczba urządzeń mobilnych pracujących pod nadzorem systemu operacyjnego Android oscyluje w okolicach jednego miliarda, a
użytkownicy mogą wybierać spośród ponad 1 200 000 aplikacji dostępnych w
sklepie. W znacznej ilości z tych aplikacji, możemy spotkać jeden z najbardziej rozbudowanych komponentów dołączonych do SDK Androida - ListView.
Komponent ten jest ceniony ze względu na swoją uniwersalność, rozbudowaną
funkcjonalność oraz prostotę użycia. Niestety tak duża funkcjonalność niesie
ze sobą ryzyko, iż w przypadku niewłaściwego użycia tego komponentu aplikacja radykalnie straci na płynności interfejsu oraz znacznie zwiększy się zużycie
pamięci urządzenia. Dlatego tak ważne jest poznanie zasady jego funkcjonowania oraz sposobów optymalizacji działania.
1
Wstęp
Android jest obecnie najpopularniejszym na świecie systemem operacyjnym dla
urządzeń mobilnych. Szacuje się, że pod koniec 2013 roku Android posiadał 81%
udziału na rynku mobilnych systemów operacyjnych [1]. Popularność ta sprawia, że
coraz więcej programistów zaczyna tworzyć oprogramowanie na urządzenia działające pod kontrolą tego właśnie systemu. Aby ułatwić pracę nowym programistom,
Google udostępnia za darmo zbiór narzędzi oraz bibliotek niezbędnych do stworzenia oraz przetestowania aplikacji - Android SKD. Wraz z tym zestawem developerzy
otrzymują kilkadziesiąt podstawowych komponentów, dzięki którym można zbudować swoją pierwszą aplikacje. Jednym z najpopularniejszych, a zarazem najbardziej
złożonych, jest komponent ListView. Artykuł przedstawia zasadę działania, sposoby
optymalizacji oraz przygotowane przez autora przykładowe aplikacje prezentujące
funkcjonalność tego komponentu.
1
2
Zasada działania
Jak sama nazwa wskazuje, ListView używany jest do wyświetlania pewnego zbioru danych w postaci listy. Sposób wyświetlania tych danych, może być dowolnie modyfikowany przez programistę. Na rysunku 1 przedstawiono trzy przykłady użycia
ListView do wyświetlenia listy kontaktów. Wszystkie bazują na tym samym zbiorze
danych, jednak różnią się sposobem jego prezentacji.
Rys. 1: Różne sposoby wyświetlania tego samego zbioru danych za pomocą komponentu
ListView
Widać więc, że sposób wyświetlania danych może być różny, w zależności od
potrzeb. Skąd więc ListView ma wiedzieć w jaki sposób wyświetlić dane? Jak to
opisać? Z pomocą przychodzi tutaj klasa BaseAdapter. Jest to klasa znajdująca się
standardowo w zestawie SDK Androida. Jednak klasa ta nie definiuje od razu żadnego sposobu wyświetlania listy. Jest to klasa abstrakcyjna, tak więc trzeba stworzyć
własną klasę dziedziczącą po niej.
BaseAdapter posiada cztery metody abstrakcyjne:
1. getCount() - zwracającą liczbę elementów na liście,
2. getItem(position) - zwracającą element na podanej pozycji,
3. getItemId(position) - zwracającą identyfikator elementu na podanej pozycji,
4. getView(position, convertView, parent) - zwracającą widok (obiekt dziedziczący po klasie View) zawierający wygląd pojedynczego elementu listy.
To właśnie getView jest najważniejszą metodą adaptera. To ona określa nam
sposób wyświetlania każdego elementu listy. Najczęściej sposób wyświetlenia danych jest opisywany za pomocą pliku XML. Zawiera on dpowiednio ułożone oraz
opisane komponenty, które adapter musi tylko wypełnić danymi. W ten sposób,
ListView nie operuje na danych, a na tworzonych przez adapter widokach. Metoda
2
getView wywoływana jest dla każdego elementu listy osobno, tak więc nic nie stoi na
przeszkodzie temu, aby dla każdego elementu zdefiniować inny sposób wyświetlania
danych.
Po stworzeniu adaptera, wystarczy użyć metody setAdapter na obiekcie klasy
ListView, aby przypisać utworzony sposób prezentowania danych do listy.
Schemat działania adaptera prezentuje rysunek 2.
Rys. 2: Schemat działania adaptera listy
3
Nagłówki i stopki
Ciekawym elementem komponentu ListView jest także możliwość stosowania nagłówków oraz stopek. Nagłówek jest to widok, który komponent ListView wyświetli
przed widokami z główną zawartością utworzoną w adapterze. Analogicznie stopka
jest to widok wyświetlony na samym końcu, po głównej zawartości listy. Warto zaznaczyć, że zarówno nagłówek, jak i stopka nie są dodawane do listy przy użyciu adaptera, a przypisane bezpośrednio do listy za pomocą metod addHeaderView(View)
oraz addFooterView(View). Widoki te można usunąć za pomocą metod removeHeaderView(View) oraz removeFooterView(View). Do komponentu ListView można
dodać więcej niż jedną stopkę oraz więcej niż jeden nagłówek. Przy dodawaniu widoków stopki oraz nagłówka, należy pamiętać o bardzo ważnym ograniczeniu. We
wszystkich wersjach Androida przed wersją 4.4 (Android Kitkat), nagłówek oraz
stopka muszą być dodane przed ustawieniem dla listy adaptera [2].
Rysunek 3a pokazuje przykład zastosowania nagłówka listy, natomiast drugi rysunek 3b przedstawia zastosowanie stopki w przykładowej aplikacji do wysyłania
wiadomości tekstowych.
3
(a)
(b)
Rys. 3: Prezentacja przykładowego nagłówka listy (a) oraz stopki (b)
4
Optymalizacja działania
ListView jest komponentem bardzo złożonym. Może on być użyty do wyświetlenia bardzo dużego zbioru danych, w niekoniecznie prosty sposób. Dlatego bardzo
ważnym krokiem po użyciu tego komponentu w naszym programie jest jego optymalizacja. Poniżej zostaną przedstawione podstawowe metody optymalizacji działania
listy.
5
Mechanizm recyklingu widoków
Podczas wyświetlania danych, użytkownik najczęściej ma możliwość przesunięcia elementów listy, odsłaniając kolejne elementy. Jak już wcześniej wspomniano,
ListView operuje na widokach, a każdy element listy jest osobnym widokiem. Tak
więc jeżeli ma zostać wyświetlona lista zawierająca sto elementów, potrzebnych jest
sto wygenerowanych widoków. Twórcy komponentu ListView usprawnili proces wyświetlania, i generowane są jedynie widoki aktualnie widoczne na ekranie telefonu.
Pozostałe elementy tworzone są w razie konieczności, na przykład, gdy użytkownik
przesunie listę, odsłaniając kolejne jej elementy. Problem pojawia się jednak, gdy
użytkownik chce szybko przedostać się na drugi koniec długiej listy. Może wtedy
zajść potrzeba stworzenia, a następnie usunięcia dziesiątek widoków w ciągu jednej
lub kilku sekund. Generowanie widoków na podstawie jego opisu w kodzie XML jest
czasochłonne, tak więc podczas szybkiego przesuwania listy widoczny jest bardzo du-
4
ży spadek wydajności, a animacje interfejsu tracą płynność. Z pomocą przychodzi
tutaj mechanizm recyklingu widoków.
Mechanizm recyklingu widoków opiera się na zasadzie ponownego wykorzystania
wcześniej już użytego na liście widoku. Za każdym razem, gdy przesuwamy listę w
górę lub w dół, ListView zapamiętuje jeden niepotrzebny już widok każdego typu
użytego na liście. Zasadę działania mechanizmu recyklingu widoków opisuje poniższy
schemat.
Rys. 4: Zasada działania mechanizmu recyklingu widoków
ListView przekazuje do adaptera przechowywany w pamięci widok w chwili, gdy
wymagane jest wygenerowanie kolejnego elementu listy. Widok ten przekazywany
jest jako parametr metody getView. W chwili, gdy w pamięci nie ma żadnego widoku,
przekazywany jest zamiast niego null. Aby odpowiednio wykorzystać dostarczany
przez komponent widok, należy lekko zmodyfikować definicję naszej metody getView.
Listing 1 pokazuje przykładową metodę getView niewykorzystującej mechanizmu
recyklingu a listing 2 jej modyfikację, która uaktywnia mechanizm.
p u b l i c View g e t V i e w ( i n t p o s i t i o n , View c o n v e r t V i e w ,
ViewGroup p a r e n t ) {
c o n v e r t V i e w = i n f l a t e r . i n f l a t e (R . l a y o u t . row_1 , n u l l ) ;
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . i m i e ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . imie ) ;
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . n a z w i s k o ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . nazwisko ) ;
}
Listing 1: Metoda getView bez zastosowania mechanizmu recyklingu
p u b l i c View g e t V i e w ( i n t p o s i t i o n , View c o n v e r t V i e w ,
ViewGroup p a r e n t ) {
i f ( c o n v e r t V i e w == n u l l )
c o n v e r t V i e w = i n f l a t e r . i n f l a t e (R . l a y o u t . row_1 ,
null );
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . i m i e ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . imie ) ;
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . n a z w i s k o ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . nazwisko ) ;
}
Listing 2: Metoda getView z zastosowaniem mechanizmu recyklingu
5
Zastosowanie mechanizmu recyklingu widoków gwarantuje znaczną poprawę płynności działania interfejsu. Szybkość wyświetlania może się zwiększyć z 20 do nawet
50 klatek na sekundę [3].
6
Mechanizm recyklingu, a różne typy widoków
Nieraz zdarzyć się może, że na liście trzeba wyświetlić elementy różnego typu.
Mogą to być na przykład różne typy mediów, wiadomości od różnych osób. Zdarzyć
się może, że każdy typ posiada osobny wygląd. Jeżeli wykorzystywany jest z mechanizmu recyklingu, mogłoby się to wydawać problematyczne. Na szczęście twórcy
Androida przewidzieli taką sytuację.
Aby poinformować ListView, że wśród danych znajdują się dane wymagające
różnych typów widoków, trzeba nadpisać dwie kolejne metody klasy BaseAdapter:
1. getViewTypeCount() - zwraca liczbę typów widoku,
2. getItemViewType(position) - zwraca znacznik typu int widoku na danej pozycji.
Dodatkowo trzeba wprowadzić modyfikację w metodzie getView, tak aby dla
danego typu widoku, przypisywany był odpowiedni plik XML z wyglądem. Listing
3 pokazuje przykład metod getViewType, getItemViewType oraz getView.
p u b l i c i n t getItemViewType ( i n t p o s i t i o n ) {
r e t u r n o s o b y . g e t ( p o s i t i o n ) . isMe ? TYP_1 : TYP_2 ;
}
p u b l i c i n t getViewTypeCount ( ) {
return 2;
}
p u b l i c View g e t V i e w ( i n t p o s i t i o n , View c o n v e r t V i e w ,
ViewGroup p a r e n t ) {
i f ( c o n v e r t V i e w == n u l l )
c o n v e r t V i e w = i n f l a t e r . i n f l a t e (R . l a y o u t . row_1 ,
null );
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . i m i e ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . imie ) ;
( ( TextView ) c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . n a z w i s k o ) ) .
s e t t e x t ( osoby . g e t ( p o s i t i o n ) . nazwisko ) ;
}
Listing 3: Przykład użycia wbudowanego mechanizmu recyklingu widoków oraz przystosowania go do obsługi wielu typu widoków
Jak widać, w metodzie getView nadal sprawdzamy, czy ListView dostarczył dzięki mechanizmowi recyklingu widok wcześniej używany. Jeżeli nie, to tworzymy nowy
widok na podstawie kodu XML. Dzięki zmodyfikowanym metodom getItemViewType oraz getViewTypeCount, ListView będzie dostarczał nam widok tylko wtedy, gdy
w pamięci znajduje się niepotrzebny widok o takim samym typie, jak typ tworzony
aktualnie przez metodę getView.
7
Wzorzec ViewHolder
Jak można zauważyć w powyższych listingach, w metodzie getView adaptera bardzo często używana jest metoda findViewById. Służy ona do wyszukiwania, w widoku, na rzecz którego została wywołana, innego widoku o identyfikatorze podanym
jako parametr. Metoda findViewById jest metodą o dużej złożoności obliczeniowej.
6
Jest to metoda rekurencyjna, a szybkość otrzymania wyniku uzależniona jest od
ilości podwidoków, dla których kontenerem jest widok, w którym poszukiwany jest
dany identyfikator. Warto dodać, że każdy z podwidoków może być kontenerem dla
dowolnej liczby kolejnych widoków. Tak więc metoda ta może znacząco spowolnić
proces tworzenia elementu listy.
Aby uniknąć ciągłego wyszukiwania widoków, można posłużyć się wzorcem ViewHolder. Zasada jego działania jest bardzo prosta. W klasie adaptera należy stworzyć wewnętrzną klasę statyczną ViewHolder, która zawiera pola będące referencjami do używanych wewnątrz metody getView widoków [4]. Listing 4 przedstawia
przykład użycia wzorca ViewHolder.
p u b l i c View g e t V i e w ( i n t p o s i t i o n , View c o n v e r t V i e w ,
ViewGroup p a r e n t ) {
V ie wH ol de r h o l d e r = n u l l ;
i f ( c o n v e r t V i e w == n u l l ) {
c o n v e r t V i e w = i n f l a t e r . i n f l a t e (R . l a y o u t . row_1 ,
null );
h o l d e r = new Vi ew Ho ld er ( ) ;
h o l d e r . i m i e = ( TextView )
c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . i m i e ) ;
h o l d e r . n a z w i s k o = ( TextView )
c o n v e r t V i e w . f i n d V i e w B y I d (R . i d . t e x t ) ;
convertView . setTag ( h o l d e r ) ;
} else {
h o l d e r = ( Vi ew Ho ld er ) c o n v e r t V i e w . getTag ( ) ;
}
h o l d e r . imie . setText ( osoby . g e t ( p o s i t i o n ) . imie ) ;
h o l d e r . nazwisko . setText ( osoby . g e t ( p o s i t i o n ) . nazwisko ) ;
return convertView ;
}
s t a t i c c l a s s Vi ew Ho ld er {
TextView i m i e ;
TextView n a z w i s k o ;
}
Listing 4: Przykład zastosowania wzorca ViewHolder
Jak widać, metoda wyszukująca widok po identyfikatorze wywoływana jest jedynie podczas tworzenia widoku, a pomijana w przypadku widoku pochodzącego
z recyklingu. Zastosowanie tego wzorca może zwiększyć częstotliwość wyświetlania
widoku nawet o 10 klatek na sekundę [3]. Warto tutaj zaznaczyć, że wzorzec ViewHolder należy stosować razem z mechanizmem recyklingu widoków, gdyż tylko
wtedy przynosi widoczne rezultaty.
8
Pozostałe metody optymalizacji działania komponentu ListView
W artykule opisane zostały dwa najważniejsze oraz najczęściej spotykane sposoby optymalizacji komponentu ListView - mechanizm recyklingu widoków oraz wzorzec ViewHolder. Warto jednak pamiętać o innych sposobach, które mają wpływ na
szybkość działania opisywanego komponentu.
Pierwszym z nich jest przeźroczystość tła elementów listy. Duży wzrost szybkości
działania możemy uzyskać, zastępując przeźroczyste tło innym. Jak wiadomo, nie
zawsze jest to możliwe, jednak warto unikać przeźroczystego tła tam, gdzie nie jest
to konieczne.
Drugim ważnym czynnikiem wpływającym na szybkość ListView jest ilość widoków, które składają się na pojedynczy element listy. Zawsze warto unikać tworzenia
w plikach z kodem XML zbędnych widoków, gdyż każdy dodatkowy widok wymaga
większej mocy obliczeniowej przy tworzeniu elementu listy, co oczywiście zwiększa
czas potrzebny na jego wygenerowanie.
7
9
Podsumowanie
ListView jest komponentem o bardzo szerokim zastosowaniu. Jego podstawowym
zadaniem jest wyświetlanie listy elementów, na podstawie danego zbioru wejściowego. Sposób wyświetlania danych, może być praktycznie w dowolny sposób zmodyfikowany przez programistę, tak aby sprostać jego potrzebom. Dzięki wprowadzeniu
przez twórców kilku przydatnych funkcjonalności, komponent ten może znacząco
ułatwić pracę programistom.
ListView jest komponentem bardzo złożonym, a wyświetlanie dużych zbiorów
danych może skończyć się znacznym spadkiem szybkości działania aplikacji. Dlatego
każdy programista powinien zastosować opisane powyżej metody optymalizujące
ten komponent, gdyż zastosowanie ich może nawet kilkukrotnie zwiększyć szybkość
działania komponentu oraz całej aplikacji.
Literatura
[1] http://www.idc.com/getdoc.jsp?containerId=prUS24442013
[2] http://developer.android.com/reference/android/widget/ListView.html
[3] Romain Guy, Adam Powell, prezentacja The world of ListView - konferencja
Google I/O 2010
[4] http://developer.android.com/training/improving-layouts/smoothscrolling.html
[5] E. Burnette, Hello Android programowanie na platformę Google dla urządzeń
mobilnych, wydanie III, wyd. Helion, 2011
[6] C. Collins, M. Galpin, M. Kaeppler, Android w praktyce, wyd. Helion, 2012
[7] S. Komatineni, D. MacLean, S. Hashimi, Android 3 tworzenie aplikacji, wyd.
Helion, 2012
8