Kurs Android (18)

Autor: Damian Chodorek • Opublikowany: 25 marca 2016 • Ostatnia aktualizacja: 3 września 2016 • Kategoria: android, kursy

Optymalizacja przewijania listy elementów. Omówienie wzorca ViewHolder.

W części 16 kursu pisałem o wyświetlaniu listy elementów oraz klasie RecyclerView, która posiada wiele przydatnych funkcjonalności. Pojawił się wtedy temat wzorca ViewHolder, z którego musieliśmy skorzystać przy użyciu RecyclerView. Dziś omówię dokładnie co to za wzorzec i dlaczego jego znajomość jest tak ważna.

Jak wygląda brak wzorca ViewHolder

Zakładamy, że nie znasz wzorca i chcesz zaimplementować prostą listę elementów. Utwórz więc projekt com.damianchodorek.kurs.android18. Będzie to tylko jedna prosta aktywność z listą elementów. Poniżej przedstawiam layout głównej aktywności.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

Nasza aplikacja będzie wyświetlać na ekranie prostą listę obiektów typu String. Jak już dowiedziałeś się w części 16, aby to było możliwe, musimy stworzyć layout dla pojedynczego elementu listy. Nazwiemy go list_item.xml.

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

<TextView
    android:id="@+id/list_item"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

Jak widzisz layout składa się tylko z jednego elementu, którym jest pole tekstowe TextView. Musimy teraz wygenerować sobie tablicę elementów do wyświetlenia, oraz podłączyć adapter pod naszą listę. Poniżej kod.

public class MainActivity extends AppCompatActivity
{
    private List stringsArray = new ArrayList<>();

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

        // generujemy tablicę
        for(int i = 0; i < 100; ++i)
        {
            stringsArray.add("number: " + i);
        }

        // tworzymy prosty adapter
        ArrayAdapter adapter = new ArrayAdapter(this, R.layout.list_item, stringsArray);
       
        // podpinamy go do listy
        ((ListView) findViewById(R.id.list_view)).setAdapter(adapter);
    }
}

Na tym etapie, uruchomiona aplikacja powinna wyświetlać listę wygenerowanych napisów. Do tego celu użyliśmy klasy ArrayAdater, która jak sama nazwa mówi, jest adapterem pozwalającym automatycznie wyświetlić wskazaną tablicę na danej liście.

Co gdy nasz layout pojedynczego elementu się skomplikuje? Zmieńmy nasz plik item_layout.xml zgodnie z poniższym kodem.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/list_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    
</LinearLayout>

Owinęliśmy nasze pole tekstowe elementem LinearLayout. W zasadzie to wygląd layoutu się nie zmieni, ale jego kod się skomplikował. Teraz nasz adapter ArrayAdapter nie będzie wiedział w jaki sposób wyświetlić listę elementów. Jeśli uruchomisz program to nastąpi crash. Aby to naprawić, musisz sam zaimplementować metodę adaptera, która odpowiada za generowanie elementów listy do wyświetlenia na podstawie tablicy obiektów.

Ta metoda nazywa się getView(). Jej zadaniem jest stworzenie obiektu listy na podstawie pliku list_item.xml, a następnie wypełnienie go i-tym obiektem z tablicy stringsArray. Żeby wszystko było jasne, znów przedstawiam poniżej kod całej aktywności.

public class MainActivity extends AppCompatActivity
{
    private List stringsArray = new ArrayList<>();

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

        for(int i = 0; i < 100; ++i)
        {
            stringsArray.add("number: " + i);
        }

        ArrayAdapter adapter = new ArrayAdapter(this, R.layout.list_item, stringsArray)
        {
            @Override
            public View getView(int position, View convertView, ViewGroup parent)
            {
                // pobieramy i-ty element tablicy
                String stringToShow = (String) getItem(position);

                // tworzymy element listy
                convertView = LayoutInflater.from(MainActivity.this)
                    .inflate(R.layout.list_item, parent, false);

                // uzupełniamy element listy danymi
                ((TextView) convertView.findViewById(R.id.list_item))
                    .setText(stringToShow);

                return convertView;
            }
        };


        ((ListView) findViewById(R.id.list_view)).setAdapter(adapter);
    }
}

Jak więc widzisz, metoda getView() pokazuje adapterowi jak ma stworzyć listę elementów do wyświetlenia. Kiedy elementem był zwykły TextView to adapter był na tyle inteligentny, że wiedział jak sobie z nim poradzić. Kiedy jednak layout stał się bardziej skomplikowany, adapter przestał dawać radę i wymagał naszej pomocy.

Jest w tym jednak mały haczyk. Metoda getView() posiada parametr convertView, pod który przypisujemy nowo stworzone elementy listy. Adapter tworzy tylko tyle elementów convertView ile będzie widocznych na ekranie. Jeśli mamy listę 100 elementów, ale widzimy tylko 27, to nie ma potrzeby tworzyć 100 elementów. Wystarczy stworzyć 27 i kiedy użytkownik zacznie przewijać listę, użyć tych, które się schowały jako tych, które się pojawią.

Zamiast więc ciągle przypisywać nowe elementy do convertView, możemy sprawdzić, czy ten element już jest stworzony. To znacznie przyspieszy działanie naszej listy.

  @Override
public View getView(int position, View convertView, ViewGroup parent)
{
    // tworzymy element listy
    if(convertView == null) {
        convertView = LayoutInflater.from(MainActivity.this)
            .inflate(R.layout.list_item, parent, false);
    }

    // uzupełniamy element listy danymi
    ((TextView) convertView.findViewById(R.id.list_item))
        .setText((String) getItem(position));

    return convertView;
}

Poznajemy ViewHolder

Optymalizacja numer 1 gotowa. Pozostała nam jeszcze jedna. Zauważ, że za każdym razem korzystamy z metody convertView.findViewById(), aby znaleźć pole tekstowe w naszym skomplikowanym layoucie, a następnie uzupełnić je tekstem. Jak to zoptymalizować? Korzystając właśnie ze wzorca ViewHolder. Najpierw kod, potem wyjaśnienia.

public class MainActivity extends AppCompatActivity
{
    private List stringsArray = new ArrayList<>();

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

        for(int i = 0; i < 100; ++i)
        {
            stringsArray.add("number: " + i);
        }

        ArrayAdapter adapter = new ArrayAdapter(this, R.layout.list_item, stringsArray)
        {
            class ViewHolder
            {
                TextView textViewItem;
            }

            @Override
            public View getView(int position, View convertView, ViewGroup parent)
            {
                ViewHolder viewHolder = null;

                if(convertView == null)
                {
                    // jeśli convertView jest nullem, tworzymy go,
                    convertView = LayoutInflater.from(MainActivity.this)
                        .inflate(R.layout.list_item, parent, false);

                    // następnie tworzymy nowy obiekt ViewHolder i umieszczamy w nim referencję
                    // do znalezionego metodą findViewById() pola tekstowego
                    viewHolder = new ViewHolder();
                    viewHolder.textViewItem = ((TextView) convertView
                        .findViewById(R.id.list_item));

                    // na końcu przypisujemy obiekt ViewHolder do widoku,
                    // aby wykorzystać go w przyszłości i nie korzystać już z findViewById()
                    convertView.setTag(viewHolder);
                }
                else
                {
                    // skoro convertView istnieje (nie jest nullem), to możemy wykorzystać
                    // utworzony wcześniej ViewHolder z gotowym polem tekstowm
                    viewHolder = (ViewHolder) convertView.getTag();
                }

                viewHolder.textViewItem.setText((String) getItem(position));

                return convertView;
            }
        };


        ((ListView) findViewById(R.id.list_view)).setAdapter(adapter);
    }
}

Utworzyliśmy więc klasę ViewHolder, której zadaniem jest przechowanie referencji do raz znalezionego pola tekstowego. Dzięki niej nie korzystamy już z co chwilę z findViewById(), a jedynie przy pierwszym utworzeniu elementu listy. Potem tylko re-używamy.

Warto w tym momencie podkreślić wykorzystaną metodę setTag(), którą posiadają wszystkie klasy widoków (dziedziczące po View). Ta metoda przypisuje do obiektu widoku, dowolny obiekt. To takie pudełko, w które wolno wsadzić nam cokolwiek chcemy. W naszym przykładzie umieszczamy tam obiekt trzymacza widoków czyli ViewHoldera. Podkreślam jeszcze raz - po to, aby nie szukać ciągle pola tekstowego TextView metodą findViewById(), a jedynie raz.

W części 16 kursu też utworzyliśmy listę elementów, ale zamiast ListView skorzystaliśmy z nowszego RecyclerView, który wymusza na nas skorzystanie z klasy RecyclerView.ViewHolder.

część 19

1 komentarz

  • Marcin napisał(a):

    Warto rozwinąć kurs o 2 rzeczy aby pomóc początkującym:
    1) podpowiedź, że należy dodać następujący wpis w gradle.build app:
    compile ‚com.android.support:design:XX.Y.Z (XX.Y.Z to wersja biblioteki)
    inaczej wpisy:
    app:layout_behavior=”@string/appbar_scrolling_view_behavior”
    app:layout_scrollFlags=”scroll|enterAlways”
    będą generować błąd.

    2) dodać wpis dotyczący pliku menu_main.xml (wiem, że to jest opisane w kursie 8, ale jak kogoś interesuje tylko ten kurs to natrafi na problem)
    Swoją drogą jak sprawić aby wpisy z powyższego pliku pojawiały się w kolorze toolbara (colorPrimary)? U mnie są czarne.

    Twoje kursy z androida śledzę i uważam za rewelacyjne. Dobra robota!

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.