W pierwszej części tutoriala stworzyliśmy widoki elementów listy oraz ich model danych. Przewijanie listy zawierającej dużą liczbę elementów wiąże się z częstym wywoływaniem metody findViewById
, co może znacznie obniżyć wydajność a w rezultacie spowodować, że lista nie będzie przewijała się płynnie. Sposobem na obejście tego problemu jest wzorzec projektowy View Holder
. Idea wzorca polega na stworzeniu obiektu, który będzie przechowywał w swoich polach referencje do kontrolek widoku raz pobranych z layoutu metodą findViewById
. Kiedy będzie potrzeba odwołania się do kontrolki, robimy to po prostu po przez pole tego obiektu.
5. ViewHoldery dla elementów listy.
Tworzymy po jednej klasie ViewHolder
dla każdego typu elementu listy. Polom klasy przypisujemy wyciągnięte z layoutu kontrolki. W konstruktorach przekazywany jest widok danego elementu.
public class BookItemViewHolder { TextView bookTitleText; SimpleDraweeView bookCoverDrawee; TextView bookAuthorText; TextView bookDescriptionText; public BookViewHolder(View itemView) { bookTitleText = (TextView) itemView.findViewById(R.id.book_title_text); bookCoverDrawee = (SimpleDraweeView) itemView.findViewById(R.id.book_cover_drawee); bookAuthorText = (TextView) itemView.findViewById(R.id.book_author_text); bookDescriptionText = (TextView) itemView.findViewById(R.id.book_description_text); } }
public class HeaderViewHolder { TextView titleTextView; TextView subtitleTextView; public HeaderViewHolder(View itemView) { titleTextView = (TextView) itemView.findViewById(R.id.header_title_text); subtitleTextView = (TextView) itemView.findViewById(R.id.header_subtitle_text); } }
public class DVDViewHolder { TextView dvdTitleText; SimpleDraweeView dvdCoverDrawee; TextView dvdDescriptionText; public DVDViewHolder(View itemView) { dvdTitleText = (TextView) itemView.findViewById(R.id.dvd_title_text); dvdCoverDrawee = (SimpleDraweeView) itemView.findViewById(R.id.dvd_cover_drawee); dvdDescriptionText = (TextView) itemView.findViewById(R.id.dvd_description_text); } }
6. Abstrakcyjny ViewHolder
Tworzymy teraz abstrakcyjny ViewHolder
dziedziczący po klasie RecyclerView.ViewHolder
. Dzięki temu stworzone poprzednio holdery elementów listy będą mogły zostać użyte z kontrolką RecyclerView
.
AbstractViewHolder.java
public abstract class AbstractViewHolder<I> extends RecyclerView.ViewHolder { public AbstractViewHolder(View itemView) { super(itemView); } public abstract void setValues(I item, int position); }
Zauważmy, że klasa przyjmuje generyczny parametr I
. Dzięki temu każdy z naszych ViewHolderów będzie mógł zaimplementować metodę setValues
przyjmującą taką klasę modelu elementu jaką potrzebuje. Metoda ta zostanie użyta do przypisania wartości z modelu do kontrolek widoku.
Rozszerzmy teraz nasze klasy aby dziedziczyły po klasie AbstractViewHolder
. Oczywiście każda będzie musiała implementować własną wersję metody setValues
, gdyż jest ona zdefiniowana jako abstrakcyjna w klasie bazowej. W ich konstruktorach nie zapomnijmy też o wywołaniu konstruktora rodzica, przekazując mu widok po przez super
.
HeaderViewHolder.java
public class HeaderViewHolder extends AbstractViewHolder<Header> { // ... public HeaderViewHolder(View itemView) { super(itemView); // ... } @Override public void setValues(Header item, int position) { titleTextView.setText(item.getTitle()); subtitleTextView.setText(item.getSubTitle()); } }
BookViewHolder.java
public class BookViewHolder extends AbstractViewHolder<Book> { // ... public BookViewHolder(View itemView) { super(itemView); // ... } @Override public void setValues(Book book, int position) { bookTitleText.setText(book.getTitle()); bookCoverDrawee.setImageURI(book.getCoverUrl()); bookAuthorText.setText(book.getAuthor()); bookDescriptionText.setText(book.getDescription()); } }
DVDViewHolder.java
public class DVDViewHolder extends AbstractViewHolder<Dvd> { // ... public DVDViewHolder(View itemView) { super(itemView); // ... } @Override public void setValues(Dvd dvd, int position) { dvdTitleText.setText(dvd.getTitle()); dvdCoverDrawee.setImageURI(dvd.getCoverUrl()); dvdDescriptionText.setText(dvd.getDescription()); } }
7. Adapter dla RecyclerView
Kontrolka klasy RecyclerView
wymaga adaptera dziedziczącego po klasie RecyclerView.Adapter
. Adapter umożliwia połączenie (binding) danych aplikacji z widokami wyświetlanymi przez recycler. Najpierw stworzymy abstrakcyjny adapter, po którym będą dziedziczyć inne, konkretne adaptery w naszej aplikacji. Będzie zawierał metody które byłyby takie same dla nich wszystkich, oszczędzimy więc sobie kodu w przyszłości. Definiuje też kolekcję w której będą przechowywane dane i sposób ich ustawiania i pobierania. W minimalnej implementacji wystarczą metody na przekazanie kolekcji danych (zastępujące poprzednie), pobierającą element z konkretnej pozycji listy, oraz pobierającą całkowitą liczbę elementów.
Definicja klasy naszego adaptera będzie nieco skomplikowana. Jako parametry generyczne przyjmuje on:
– I
– klasa modelu elementu listy, czyli jakiego typu obiekty będziemy przechowywać i pobierać z kolekcji.
– VH
– klasa view holdera potrzebna recyclerowi, musi ona dziedziczyć po klasie AbstractViewHolder
.
Następnie nasz abstrakcyjny adapter rozszerza klasę RecyclerView.Adapter
, przekazując mu, z jakiego typu (VH
) view holdera będzie korzystać.
AbstarctAdapter.java
public abstract class AbstractAdapter<I, VH extends AbstractViewHolder> extends RecyclerView.Adapter<VH> { protected List<I> items; public AbstractAdapter() { this.items = new ArrayList<>(); } public void setItems(Collection<I> items) { this.items.clear(); this.items.addAll(items); notifyDataSetChanged(); } public I getItem(int position) { return items.get(position); } @Override public int getItemCount() { return items.size(); } protected View inflateView(ViewGroup parent, int layoutId) { return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); } }
Komentarza wymaga jeszcze metoda setItems
. Przekazanie nowej kolekcji z danymi do adaptera spowoduje wyczyszczenie starych danych, dodanie wszystkich elementów oraz powiadomienie adaptera, że dane zostały zmienione.
Mając adapter abstrakcyjny, stworzymy teraz dziedziczący po nim adapter konkretny. Typem przechowywanych elementów będzie klasa RecycledListItem
, jak pamiętamy z poprzedniej części – kontener dla różnych elementów listy. Typem view holdera będzie klasa bazowa dla holderów naszych widoków, czyli AbstractViewHolder
. Należy pamiętać o nadpisaniu metody getItemViewType
, która zwróci typ elementu na podanej pozycji.
public class RecycledListAdapter extends AbstractAdapter<RecycledListItem, AbstractViewHolder { @Override public AbstractViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { AbstractRecyclerViewHolder viewHolder = null; switch (RecycledListItem.ItemType.values()[viewType]) { case HEADER: viewHolder = new HeaderViewHolder(inflateView(parent, R.layout.header_item_layout)); break; case DVD: viewHolder = new DVDViewHolder(inflateView(parent, R.layout.dvd_item_layout)); break; case BOOK: viewHolder = new BookViewHolder(inflateView(parent, R.layout.book_item_layout)); break; } return viewHolder; } @Override public int getItemViewType(int position) { return items.get(position).getType().ordinal(); } @Override public void onBindViewHolder(AbstractRecyclerViewHolder holder, int position) { holder.setValues(getItem(position).getValue(), position); } }
8. Łączymy wszystko w całość
Mamy już wszystko czego potrzebujemy. W klasie MainActivity
stwórzmy recycler, adapter i zainicjujmy go przykładowymi danymi.
MainActivity.java
public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private RecycledListAdapter adapter; private List<RecycledListItem> items = new ArrayList<RecycledListItem>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fresco.initialize(this); setContentView(R.layout.activity_main); initViews(); } protected void initViews() { recyclerView = (RecyclerView) findViewById(R.id.list_recycler); adapter = new RecycledListAdapter(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(adapter); items = MockAPI.retrieveData(); adapter.setItems(items); } }
Jeszcze tylko stworzymy przykładowe dane do wyrenderowania. Będzie to lista 100 książek i 100 płyt DVD podzielona na sekcje mające swoje nagłówki:
api/MockAPI.java
public class MockAPI { public static List<RecycledListItem> retrieveData() { List<RecycledListItem> data = new ArrayList<RecycledListItem>(); RecycledListItem header1 = new RecycledListItem(new Header("Section header 1", "This is section subheader 1")); data.add(header1); for (int i = 0; i < 100; i++) { RecycledListItem book = new RecycledListItem(new Book("Book Title " + i, "Author Anonymous", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "")); data.add(book); } RecycledListItem header2 = new RecycledListItem(new Header("Section header 2", "This is section subheader 2")); data.add(header2); for (int i = 0; i < 100; i++) { RecycledListItem dvd = new RecycledListItem(new Dvd("DVD Title " + i, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "")); data.add(dvd); } return data; } }
W zasadzie to wszystko. Otrzymaliśmy uniwersalny mechanizm pozwalający na tworzenie wydajnych, przewijalnych list o dowolnej liczbie i typie elementów. Pozostały jeszcze listenery na kliknięcia w element listy, czy też w przycisk znajdujący się na elemencie, ale o tym w następnej części.
Cały kod źródłowy dostępny jest na GitHubie.