Оптимізація адаптера та View Holder

У минулій темі було створено кастомний адаптер, який давав змогу працювати зі складними списками об'єктів:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
import java.util.List;
 
public class StateAdapter extends ArrayAdapter<State> {
 
    private LayoutInflater inflater;
    private int layout;
    private List<State> states;
 
    public StateAdapter(Context context, int resource, List<State> states) {
        super(context, resource, states);
        this.states = states;
        this.layout = resource;
        this.inflater = LayoutInflater.from(context);
    }
    public View getView(int position, View convertView, ViewGroup parent) {
 
        View view=inflater.inflate(this.layout, parent, false);
 
        ImageView flagView = view.findViewById(R.id.flag);
        TextView nameView = view.findViewById(R.id.name);
        TextView capitalView = view.findViewById(R.id.capital);
 
        State state = states.get(position);
 
        flagView.setImageResource(state.getFlagResource());
        nameView.setText(state.getName());
        capitalView.setText(state.getCapital());
 
        return view;
    }
}

Цей адаптер має один великий мінус — при прокручуванні в ListView, якщо в списку дуже багато об'єктів, то для кожного елемента, коли він потрапить в зону видимості, буде повторно викликатися метод getView, в якому буде знову створюватися новий об'єкт View. Це збільшуватиме споживання пам'яті та знижуватиме продуктивність. Тому оптимізуємо код методу getView:

public View getView(int position, View convertView, ViewGroup parent) {
 
    if (convertView == null) {
        convertView = inflater.inflate(this.layout, parent, false);
    }
     
    ImageView flagView = convertView.findViewById(R.id.flag);
    TextView nameView = convertView.findViewById(R.id.name);
    TextView capitalView = convertView.findViewById(R.id.capital);
 
    State state = states.get(position);
 
    flagView.setImageResource(state.getFlagResource());
    nameView.setText(state.getName());
    capitalView.setText(state.getCapital());
 
    return convertView;
}

Параметр convertView вказує на елемент View, який використовується для об'єкта в списку по позиції position. Якщо раніше вже був створений View для цього об'єкта, то параметр convertView вже містить певне значення, яке ми можемо використовувати.

В цьому випадку ми будемо повторно використовувати вже створені об'єкти і підвищимо продуктивність, однак цей код можна ще більше оптимізувати. Справа в тому, що отримання елементів за id також є відносно затратною операцією. Тому далі оптимізуємо код StateAdapter, змінивши його наступним чином:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
import java.util.List;
 
public class StateAdapter extends ArrayAdapter<State> {
 
    private LayoutInflater inflater;
    private int layout;
    private List<State> states;
 
    public StateAdapter(Context context, int resource, List<State> states) {
        super(context, resource, states);
        this.states = states;
        this.layout = resource;
        this.inflater = LayoutInflater.from(context);
    }

    public View getView(int position, View convertView, ViewGroup parent) {
 
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = inflater.inflate(this.layout, parent, false);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        State state = states.get(position);
 
        viewHolder.imageView.setImageResource(state.getFlagResource());
        viewHolder.nameView.setText(state.getName());
        viewHolder.capitalView.setText(state.getCapital());
 
        return convertView;
    }

    private class ViewHolder {
        final ImageView imageView;
        final TextView nameView, capitalView;

        ViewHolder(View view) {
            imageView = view.findViewById(R.id.flag);
            nameView = view.findViewById(R.id.name);
            capitalView = view.findViewById(R.id.capital);
        }
    }
}

Для зберігання посилань на використані елементи ImageView і TextView визначений внутрішній приватний клас ViewHolder, який в конструкторі отримує об'єкт View, що містить ImageView та TextView.

У методі getView, якщо convertView рівний null (тобто якщо раніше для об'єкта не створена розмітка), створюємо об'єкт ViewHolder, який зберігаємо в тегу convertView:

convertView.setTag(viewHolder);

Якщо ж розмітка для об'єкта в ListView вже була створена, то назад отримуємо ViewHolder з тегу:

viewHolder = (ViewHolder) convertView.getTag();

Потім для ImageView та TextView в ViewHolder встановлюються значення з об'єкта State:

viewHolder.imageView.setImageResource(state.getFlagResource());
viewHolder.nameView.setText(state.getName());
viewHolder.capitalView.setText(state.getCapital());

Тепер ListView, особливо при великих списках, буде працювати плавніше та продуктивніше, ніж у попередній версії.