Zastosowanie
ViewHolder
(ang. ViewHolder
) (wzorzec kreacyjny) ma za zadanie zoptymalizować proces renderowania widoku kolekcji elementów poprzez ponowne użycie obiektów istniejących widoków. W trakcie przewijania listy dla każdego elementu kolekcji wywoływana jest kosztowna operacja wiązania widoku findViewById
. W efekcie wzrasta zużycie zasobów, powstaje więcej obiektów widoku niż to w danej chwili potrzebne, a także przewijanie listy nie jest płynne. ViewHolder
likwiduje wspomniane problemy. Przechowuje referencję do związanych instancji widoku w postaci obiektu typu ViewHolder
i w razie potrzeby udostępnia je do ponownego użycia. Ilość stworzonych obiektów ViewHolder
jest zależna od wielkości ekranu urządzenia i wynosi mniej więcej tyle ile maksymalnie może zostać wyświetlonych elementów w widoku (naraz bez przewijania).
Ograniczenia
Wykorzystanie wzorca ViewHolder
nieznacznie zwiększa poziom skomplikowania.
Użycie
Wzorzec używany jest w widokach kolekcji w celu optymalizacji zużycia zasobów pamięci urządzenia. Dla niewielkich kolekcji (mieszczących się w całości na ekranie) różnica w wydajności może nie występować lub być niezauważalna, jednakże gdy nie mieszczą się one na ekranie i możliwe jest przewinięcie listy wówczas skok wydajności jest zauważalnym gołym okiem.
Implementacja
Wewnętrzna klasa ViewHolder
przechowuje referencję do obiektów widoku View
. Referencje są ustawiane w klasie adaptera bezpośrednio na obiekcie viewholder lub przekazywany jest cały widok do konstruktora ViewHolder
, który sam dba o związanie obiektów widoku. Klasa Adapter
wykorzystuje obiekt typu ViewHolder
do pobrania referencji i przekazania danych do widoków.
W celu uproszczenia idei wzorca ViewHolder
przedstawiona poniżej implementacja dedykowania jest dla widoku kolekcji typu ListView
oraz klasy adaptera rozszerzającego BaseAdapter
. Jednakże zalecane jest wykorzystanie widoku kolekcji RecyclerView
wraz implementacją klasy rozszerzającej RecyclerView.Adapter
.
public class ItemAdapter extends BaseAdapter {
private List<Item> items;
public ItemAdapter(List<Item> items) {
this.items = items;
}
class ViewHolder {
private TextView textView;
private ImageView imageView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
//check if received view has been inflated before
if(convertView == null) {
//get layout to inflate
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.item, parent, false);
viewHolder = viewHolder(convertView);
//inflate views into viewHolder
viewHolder.textView = convertView.findViewById(R.id.textView);
viewHolder.imageView = convertView.findViewById(R.id.imageView);
convertView.setTag(viewHolder);
}
else {
//receive inflated view from memory
viewHolder = (ViewHolder) convertView.getTag();
}
//set data into views
Item item = items.get(position);
viewHolder.textView.setText(item.getText());
viewHolder.imageView.seImageResource(item.getImage());
return convertView;
}
//here should be other adapter methods override from superclass
}
Aktywność lub fragment tworzą tworzą widok listy do którego przekazany jest adapter z danymi kolekcji wykorzystujący ViewHolder
.
//some activity or fragment lifecycle methods
//this work can be done in e.g. in onCreate or onCreateView
//create or get some collection
List<Item> items = new ArrayList<>();
items.add(new Item("Item1", R.drawable.image1));
items.add(new Item("Item2", R.drawable.image2));
items.add(new Item("Item3", R.drawable.image3));
//more and more items
//insert adapter with ViewHolder into ListView
ListView listView = findViewById(R.id.listView);
ItemAdapter adapter = new ItemAdapter(items);
listView.setAdapter(adapter);
Przykład
Aplikacja FoodDeliver
umożliwia użytkownikom dokonanie zamówienia dostawy jedzenia z wybranej restauracji. Wiele widoków aplikacji (w tym widok pozycji menu z danej restauracji) wykorzystuje mechanizm widoku przewijalnej listy. W celu optymalizacji wydajności działania listy do realizacji tego zadania użyto kontrolkę RecyclerView
wraz z implementacją ViewHolder
.
public class FoodAdapter extends RecyclerView.Adapter<FoodAdapter.ViewHolder> {
private Context context;
private List<Food> items;
public FoodAdapter(Context context, List<Food> items) {
this.context = context;
this.items = items;
}
class ViewHolder extends RecyclerView.ViewHolder {
private ImageView imageView;
private TextView nameView;
private TextView priceView;
private Button cartButton;
ViewHolder(View view) {
super(view);
imageView = view.findViewById(R.id.food_image);
nameView = view.findViewById(R.id.food_name);
priceView = view.findViewById(R.id.food_price);
cartButton = view.findViewById(R.id.food_cart);
}
public void setData(Food item) {
Picasso.with(context).load(item.getImageUrl()).into(imageView);
nameView.setText(item.getName());
priceView.setText(item.getPrice() + " PLN");
cartButton.setOnClickListener(v -> addToCart(item));
}
}
@Override
public FoodAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.food_item, parent, false);
return new FoodAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull FoodAdapter.ViewHolder holder, int position) {
Food item = items.get(position);
holder.setData(item);
}
@Override
public int getItemCount() {
return items.size();
}
private void addToCart(Food item) {
//add dish to cart action
}
}
Aktywność lub fragment pobierają dane z serwera o dostępnych pozycjach menu. Następnie tworzony jest adapter, który jest przekazywany do widoku kolekcji RecyclerView
.
//user chose restaurant and goes to dishes selection screen
//app receives data from server for choosen restaurant and load into items collection
List<Food> items = new ArrayList<>();
items.add("Pizza Margherita", 15.00, "http://example.com/margherita.jpg");
items.add("Pizza Pepperoni", 18.00, "http://example.com/pepperoni.jpg");
items.add("Pizza Hawaii", 20.00, "http://example.com/hawaii.jpg");
//and more more items
//inflate layout and other lifecycle stuff
RecyclerView recyclerView = findViewById(R.id.recyclerView);
//insert adapter into recyclerView
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
FoodAdapter adapter = new FoodAdapter(getActivity(), items);
recyclerView.setAdapter(adapter);
//scrolling the list is smooth and faster
Biblioteki
Użycie widoku typu RecyclerView
wymusza na programiście implementację wewnętrznej klasy RecyclerView.ViewHolder
. Realizacja wzorca w starszych widokach kolekcji np.: ListView
, GridView
jest opcjonalna.