Przykłady zastosowań oraz optymalizacja działania komponentu ListView w aplikacjach dla urządzeń mobilnych z systemem Android Kacper Markowski 1 1 Wydział Inżynierii Mechanicznej i Informatyki Politechnika Częstochowska Kierunek Informatyka, Rok III kacper.markowski@student.pcz.pl 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. public View getview ( i n t position, View convertview, ViewGroup p a r e n t ) { convertview = 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 ) convertview. findviewbyid (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 ). i m i e ) ; ( ( TextView ) convertview. findviewbyid (R. i d. nazwisko ) ). 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 public View getview ( i n t position, View convertview, ViewGroup p a r e n t ) { i f ( convertview == n u l l ) convertview = 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 ) convertview. findviewbyid (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 ). i m i e ) ; ( ( TextView ) convertview. findviewbyid (R. i d. nazwisko ) ). 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 osoby. g e t ( p o s i t i o n ). isme? TYP_1 : TYP_2; public i n t getviewtypecount ( ) { r e t u r n 2 ; public View getview ( i n t position, View convertview, ViewGroup p a r e n t ) { i f ( convertview == n u l l ) convertview = 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 ) convertview. findviewbyid (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 ). i m i e ) ; ( ( TextView ) convertview. findviewbyid (R. i d. nazwisko ) ). 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. public View getview ( i n t position, View convertview, ViewGroup p a r e n t ) { ViewHolder h o l d e r = n u l l ; i f ( convertview == n u l l ) { convertview = 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 ) ; holder = new ViewHolder ( ) ; holder. imie = ( TextView ) convertview. findviewbyid (R. i d. i m i e ) ; holder. nazwisko = ( TextView ) convertview. findviewbyid (R. i d. t e x t ) ; convertview. settag ( holder ) ; e l s e { holder = ( ViewHolder ) convertview. gettag ( ) ; h o l d e r. i m i e. s e t T e x t ( osoby. g e t ( p o s i t i o n ). i m i e ) ; h o l d e r. nazwisko. s e t T e x t ( 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 ViewHolder { TextView imie ; TextView nazwisko ; 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