Списки в Android

от
Android   listview, adapterview, recyclerview

device-2016-03-08-161242_sm.png
     Есть несколько способов решить эту задачу. Самый простой - сделать это с помощью ScrollView. Вы просто помещаете в контейнер нужные элементы интерфейса, привязываете к ним данные вручную и помещаете в ScrollView (HorizontalScrollView).
Этот способ целесообразно использовать если элементы в списке разные и их немного (максимум допустимая высота - несколько экранов, если больше, то стоит задуматься о другом решении).

     Второй способ - использовать ListView. Идеально подходит для небольших списков с одинаковыми элементами. Вы должны создать Adapter (либо использовать один из нескольких встроенных). Если список длинный, то нужно обязательно реализовывать паттерн ViewHolder, потому что если при каждом вызове метода адаптера getView будет происходить поиск элементов интерфейса (findViewById), то Вы можете заметить задержки при прокрутке списка. Но все же что бы вы не делали, на больших списках задержки в работе ListView неизбежны во многих слуяаях.

     Третий способ - это RecyclerView. Это очень мощная и гибкая реализация списка, к тому же оптимизирована лучше чем ListView.
Реализация доступна в appcompat библиотеке, так что можно не переживать насчет совместимости со старыми версиями ОС. В большинстве случаев это лучший выбор для рализации списка. Стоит всегда давать предпочтение RecyclerView перед ListView, даже если Вам кажется что в списке будет всего несколько элементов, вероятно когда-нибудь захочется расширить список и не прийдется ничего переделывать.
У этого решения есть ряд плюсов:
   - встроенная поддержка ViewHolder
   - поддержка разных типов элементов (переопределение getItemViewType)
   - поддержка декораторов для элементов (ItemDecoration)
   - поддержка LayoutManager, который управляет размещением и измерением элементов (например сеткой (grid) или обычным списком (linear)), можно написать свою имплементацию
   - поддержка анимации вставки/изменения/удаления одного элемента (причём если вызвать setHasStableIds(true) у адаптера и переопределить getItemId, то это будет происходить автоматически).
   - анимации элементов и жесты для сдвига влево/вправо реализуются без проблем

Из минусов можно отметить отсутствие встроенной реализации фильтрования списка (есть у ListView), нету headerView, emptyView, footerView (имхо самый большой минус, приходится выкручиваться - переделывать RecyclerView, либо выносить это в Adapter, либо вообще делать отдельными элементами интерфейса отдельно от списка).

Недостатки RecyclerView
     Вроде бы на этом можно заканчивать со списками, но когда доходит до практики, то появляется в приложении package adapters в котором лежат все наши адаптеры списков.
И чем далее, тем их больше. Потом мы хотим добавить категории в список, и в нем появляется несколько itemViewType, а потом мы хотим добавить еще один тип в список.
Позже нам может понадобится вот такой же список, но без категорий. Выход? +1 адаптер :)
А как же главный принцип ООП - переиспользование?

     А что если отображение конкретного элемента вынести из адаптера? Звучит интересно. Давайте же попробуем это реализовать.

AdapterDelegate
     Предоставим создание ViewHolder и его биндинг отдельным обьектам, назовем их AdapterDelegate. Это может выглядеть примерно так.
  1. /**
  2.  * @param T data items represented by delegates
  3.  */
  4. public interface AdapterDelegate<T> {
  5.  
  6.     int getItemViewType();
  7.  
  8.     boolean isForViewType(@NonNull T items, int position);
  9.  
  10.     @NonNull RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent);
  11.  
  12.     void onBindViewHolder(@NonNull T items, int position, @NonNull RecyclerView.ViewHolder holder);
  13.  
  14.     long getItemId(@NonNull T items, int position);
  15. }

     int getItemViewType(); - возвращает тип (просто некую константу, который буде асоциироваться с этим делегатом)
     boolean isForViewType(@NonNull T items, int position); - если вернет true, то этот делегат будет использован для отображения элемента данных на позиции position.
    
Остальные методы думаю понятны.

Пример реализации делегата
  1. public class FriendDelegate implements AdapterDelegate<List<Model>> {
  2.  
  3.     private final int mViewType;
  4.  
  5.     public FriendDelegate(int viewType) {
  6.         mViewType = viewType;
  7.     }
  8.  
  9.     @Override
  10.     public int getItemViewType() {
  11.         return mViewType;
  12.     }
  13.  
  14.     @Override
  15.     public boolean isForViewType(@NonNull List<Model> items, int position) {
  16.         return items.get(position) instanceof Friend;
  17.     }
  18.  
  19.     @NonNull
  20.     @Override
  21.     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
  22.         return null;  // TODO: not implemented
  23.     }
  24.  
  25.     @Override
  26.     public void onBindViewHolder(@NonNull List<Model> items, int position, @NonNull RecyclerView.ViewHolder holder) {
  27.           // TODO: not implemented
  28.     }
  29.  
  30.     @Override
  31.     public long getItemId(@NonNull List<Model> items, int position) {
  32.         return items.get(position).getId();
  33.     }
  34.  
  35. }

     Тут нужно наверное сказать, что Model - это интерфейс, который реализуют все элементы списка, мы не можем строго типизировать делегат по типу Friend, т.к. в списке могут быть и другие обьекты (User, Header, etc).

DeledatesManager
Ok, делегат есть, но кто будет им управлять? Напишем же простой DelegatesManger
  1. public class DelegateManager<T> {
  2.  
  3.     private final SparseArray<AdapterDelegate<T>> mDelegateSparseArray = new SparseArray<>();
  4.  
  5.     public DelegateManager<T> addDelegate(@NonNull AdapterDelegate<T> delegate) {
  6.         int viewType = delegate.getItemViewType();
  7.         if (mDelegateSparseArray.get(viewType) != null) {
  8.             throw new IllegalArgumentException("AdapterDelegate viewType=" + viewType + " already used by other delegate!");
  9.         }
  10.  
  11.         mDelegateSparseArray.put(viewType, delegate);
  12.         return this;
  13.     }
  14.  
  15.     public void addDelegates(@NonNull Iterable<AdapterDelegate<T>> delegates) {
  16.         for (AdapterDelegate<T> delegate : delegates) {
  17.             addDelegate(delegate);
  18.         }
  19.     }
  20.  
  21.     public DelegateManager<T> removeDelegate(AdapterDelegate<T> delegate) {
  22.         mDelegateSparseArray.remove(delegate.getItemViewType());
  23.         return this;
  24.     }
  25.  
  26.     public int getItemViewType(@NonNull T items, int position) {
  27.         return findDelegateForPosition(items, position).getItemViewType();
  28.     }
  29.  
  30.     @NonNull
  31.     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  32.  
  33.         AdapterDelegate<T> delegate = mDelegateSparseArray.get(viewType);
  34.         return delegate.onCreateViewHolder(parent);
  35.     }
  36.  
  37.     public void onBindViewHolder(@NonNull T items, int position, @NonNull RecyclerView.ViewHolder viewHolder) {
  38.  
  39.         AdapterDelegate<T> delegate = mDelegateSparseArray.get(viewHolder.getItemViewType());
  40.         if (delegate == null) {
  41.             throw new NullPointerException("No AdapterDelegate added for ViewType " + viewHolder.getItemViewType());
  42.         }
  43.  
  44.         delegate.onBindViewHolder(items, position, viewHolder);
  45.     }
  46.  
  47.     public long getItemId(@NonNull T items, int position) {
  48.         return findDelegateForPosition(items, position).getItemId(items, position);
  49.     }
  50.  
  51.     @NonNull
  52.     private AdapterDelegate<T> findDelegateForPosition(@NonNull T items, int position) {
  53.         int delegatesCount = mDelegateSparseArray.size();
  54.         for (int i = 0; i < delegatesCount; i++) {
  55.             AdapterDelegate<T> delegate = mDelegateSparseArray.valueAt(i);
  56.             if (delegate.isForViewType(items, position)) {
  57.                 return delegate;
  58.             }
  59.         }
  60.         throw new IllegalArgumentException(
  61.                 "No AdapterDelegate added that matches position=" + position + " in data source");
  62.     }
  63.  
  64. }

Как видно из кода выше, мы просто держим коллекцию делегатов в SparseArray, позволяем их добавлять\удалять и делегируем методы из Adapter делегатам.

RecyclerAdapter
Ну вот теперь мы и добились того, что нам достаточно иметь один RecyclerAdapter на весь проект, и собирать его по частям добавляя\удаляя нужные делегаты. Напишем для примера простой адаптер, заодно увидим как будет использоваться эта вся система на практике.

  1. /**
  2.  * @param T data items
  3.  */
  4. public class DelegateRecyclerAdapter<T extends List> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  5.  
  6.     private final DelegateManager<T> mDelegateManager;
  7.     private T mData;
  8.  
  9.     public DelegateRecyclerAdapter(List<AdapterDelegate<T>> delegates, T data, boolean hasStableIds) {
  10.         mData = data;
  11.         mDelegateManager = new DelegateManager<>();
  12.         mDelegateManager.addDelegates(delegates);
  13.         setHasStableIds(hasStableIds);
  14.     }
  15.  
  16.     @Override
  17.     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  18.         return mDelegateManager.onCreateViewHolder(parent, viewType);
  19.     }
  20.  
  21.     @Override
  22.     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
  23.         mDelegateManager.onBindViewHolder(mData, position, holder);
  24.     }
  25.  
  26.     @Override
  27.     public int getItemViewType(int position) {
  28.         return mDelegateManager.getItemViewType(mData, position);
  29.     }
  30.  
  31.     @Override
  32.     public long getItemId(int position) {
  33.         return mDelegateManager.getItemId(mData, position);
  34.     }
  35.  
  36.     @Override
  37.     public int getItemCount() {
  38.         return mData.size();
  39.     }
  40. }

ViewHolder
   Остался еще один элемент, до которого мы не добрались :gg: Чтобы не создавать по множество по сути банального кода, мы напишем универсальный ViewHolder.

  1. public class SimpleViewHolder extends RecyclerView.ViewHolder {
  2.  
  3.     private final SparseArray<View> mViewSparseArray = new SparseArray<>();
  4.     private final View mRootView;
  5.  
  6.     public SimpleViewHolder(View itemView) {
  7.         super(itemView);
  8.         mRootView = itemView;
  9.     }
  10.  
  11.     public SimpleViewHolder useView(@IdRes int viewId) {
  12.         View v = mRootView.findViewById(viewId);
  13.         if (v == null) {
  14.             throw new IllegalArgumentException("View with id=" + viewId
  15.                     + " not found in this ViewHolder root view");
  16.         }
  17.         mViewSparseArray.put(viewId, v);
  18.         return this;
  19.     }
  20.  
  21.     public <T extends View> T getView(@IdRes int viewId) {
  22.         T view = (T) mViewSparseArray.get(viewId);
  23.         if (view == null) {
  24.             throw new IllegalArgumentException("View with id=" + viewId
  25.                     + " not found, try call useView(viewId) before");
  26.         }
  27.         return view;
  28.     }
  29. }

А заодно самое время дописать FriendDelegate:
  1. @NonNull
  2. @Override
  3. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
  4.     return new SimpleViewHolder(LayoutInflater.from(parent.getContext())
  5.             .inflate(R.layout.item_friend, parent, false))
  6.             .useView(R.id.friend_avatar)
  7.             .useView(R.id.friend_name);
  8. }
  9.  
  10. @Override
  11. public void onBindViewHolder(@NonNull List<? extends Model> items, int position, @NonNull RecyclerView.ViewHolder holder) {
  12.     SimpleViewHolder viewHolder = (SimpleViewHolder) holder;
  13.     Friend friend = (Friend) items.get(position);
  14.  
  15.     String firstLetter = friend.name.substring(0, 1);
  16.  
  17.     viewHolder.<TextView>getView(R.id.friend_name).setText(friend.name);
  18.     viewHolder.<ImageView>getView(R.id.friend_avatar).setImageDrawable(
  19.             TextDrawable.builder().buildRound(firstLetter,
  20.                     ColorGenerator.MATERIAL.getColor(firstLetter)).getCurrent()
  21.     );
  22. }

Конфигурирование списка
     Вот собственно пример того, как может использоваться вся эта система где-нибудь во фрагменте

  1. @Override
  2.     public View onCreateView(LayoutInflater inflater, ViewGroup container,
  3.                              Bundle savedInstanceState) {
  4.         View rootView = inflater.inflate(R.layout.fragment_main, container, false);
  5.  
  6.         RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerView);
  7.         recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
  8.         recyclerView.setAdapter(getListAdapter(Utils.generateFriends(30)));
  9.  
  10.         return rootView;
  11.     }
  12.  
  13.     private RecyclerView.Adapter getListAdapter(List<? extends Model> models) {
  14.         return new DelegateRecyclerAdapter<>(new ArrayList<AdapterDelegate<List<? extends Model>>>() {{
  15.             add(new FriendDelegate(1));
  16.         }}, models, true);
  17.     }

На этом этапе можно создать какой-нибудь ActionHandler и передать в нужные делегаты для обработки событий нажатия на элемент списка. Особенно хорошо это будет работать в связке с DataBinding. А пока что у нас уже есть вполне рабочий список.

DataBinding
     Наш универсальный SimpleViewHolder пока что мало функционален и не очень удобен. Исправить это можно с помощью DataBinding, Создадим же специальный ViewHolder с поддержкой биндинга.

     Итак для начала подключим DataBinding. Технология поддерживается начиная с Android API 7, но хорошо работает только с API 19, а лучше 21 :gg: Для подключения нужен gradle plugin не ниже версии 1.5.0-alpha1 (сейчас уже доступен 2.1.0-alpha1). Последнюю версию можно всегда посмотреть здесь https://bintray.com/android/an...ls.build.gradle/view
Теперь все что нужно, это включить биндинг в build.gradle
  1. android {
  2.     ....
  3.     dataBinding {
  4.         enabled = true
  5.     }
  6. }

И еще -- рекомендую использовать самую последнюю сборку AndroidStudio, т.к. в стабильной ветке студии поддержка биндинга хромает (да и в альфа сборках до сих пор не поддерживается подсветка кода в xml :gg: )

     Все, теперь можно написать вот такой BindableViewHolder по аналогии с SimpleViewHolder.
  1. public class BindableViewHolder <VB extends ViewDataBinding> extends RecyclerView.ViewHolder {
  2.  
  3.     private final VB mBinding;
  4.  
  5.     private BindableViewHolder(VB binding) {
  6.         super(binding.getRoot());
  7.         mBinding = binding;
  8.     }
  9.  
  10.     public static <VB extends ViewDataBinding> BindableViewHolder<VB> newInstance(@LayoutRes int layoutId, ViewGroup parent) {
  11.  
  12.         VB vb = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutId, parent, false);
  13.         return new BindableViewHolder<>(vb);
  14.     }
  15.  
  16.     public VB getBinding() {
  17.         return mBinding;
  18.     }
  19. }

Круто, правда? Кода меньше, а сам holder намного удобнее в использовании.

Перепишем во первых разметку элемента списка.

Было
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <android.support.v7.widget.CardView
  3.    xmlns:android="http://schemas.android.com/apk/res/android"
  4.    xmlns:app="http://schemas.android.com/apk/res-auto"
  5.    xmlns:tools="http://schemas.android.com/tools"
  6.    android:layout_width="match_parent"
  7.    android:layout_height="wrap_content"
  8.    style="@style/ListItemStyle">
  9.  
  10.     <LinearLayout
  11.        android:layout_width="match_parent"
  12.        android:layout_height="wrap_content"
  13.        android:orientation="horizontal"
  14.        android:gravity="center_vertical"
  15.        android:padding="@dimen/spacing_tiny"
  16.        android:layout_marginLeft="@dimen/spacing_tiny"
  17.        android:background="?selectableItemBackground"
  18.        android:clickable="true">
  19.  
  20.         <ImageView
  21.            android:id="@+id/friend_avatar"
  22.            android:layout_width="@dimen/avatar_size"
  23.            android:layout_height="@dimen/avatar_size"
  24.            android:layout_margin="@dimen/spacing_tiny"/>
  25.  
  26.         <TextView
  27.            android:id="@+id/friend_name"
  28.            style="@style/TextAppearance.AppCompat.Medium"
  29.            android:layout_marginLeft="@dimen/spacing_large"
  30.            android:layout_width="match_parent"
  31.            android:layout_height="wrap_content"
  32.            tools:text="Test label"/>
  33.  
  34.     </LinearLayout>
  35.  
  36. </android.support.v7.widget.CardView>

Стало
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout>
  3.  
  4.     <data>
  5.         <variable
  6.            name="friend"
  7.            type="ua.naiksoftware.hidetabs.model.Friend"/>
  8.     </data>
  9.  
  10.     <android.support.v7.widget.CardView
  11.        xmlns:android="http://schemas.android.com/apk/res/android"
  12.        xmlns:app="http://schemas.android.com/apk/res-auto"
  13.        xmlns:tools="http://schemas.android.com/tools"
  14.        style="@style/ListItemStyle"
  15.        android:layout_width="match_parent"
  16.        android:layout_height="wrap_content">
  17.  
  18.         <LinearLayout
  19.            android:layout_width="match_parent"
  20.            android:layout_height="wrap_content"
  21.            android:layout_marginLeft="@dimen/spacing_tiny"
  22.            android:background="?selectableItemBackground"
  23.            android:clickable="true"
  24.            android:gravity="center_vertical"
  25.            android:orientation="horizontal"
  26.            android:padding="@dimen/spacing_tiny">
  27.  
  28.             <ImageView
  29.                android:id="@+id/friend_avatar"
  30.                android:layout_width="@dimen/avatar_size"
  31.                android:layout_height="@dimen/avatar_size"
  32.                android:layout_margin="@dimen/spacing_tiny"
  33.                app:letterDrawable='@{friend.name}'/>
  34.  
  35.             <TextView
  36.                android:id="@+id/friend_name"
  37.                style="@style/TextAppearance.AppCompat.Medium"
  38.                android:layout_width="match_parent"
  39.                android:layout_height="wrap_content"
  40.                android:layout_marginLeft="@dimen/spacing_large"
  41.                android:text='@{friend.name}'
  42.                tools:text="Test label"/>
  43.  
  44.         </LinearLayout>
  45.  
  46.     </android.support.v7.widget.CardView>
  47.  
  48. </layout>

Единственный обязательный здесь атрибут, для работы биндинга - это <layout> - вы обязаны обернуть в него разметку, если хотите пользоваться биндингом.
Терерь при компиляции будет автоматически создаваться класс, имя которого - название разметки преобразованное в camel case + binding (но есть и возможность самому задать это имя)
Например для разметки item_friend_bindable сгенерируется ItemFriendBindableBinding :gg: В этом случае стоит наверное переименовать разметку или задать свое имя биндинга.

Итак, что мы видим? Создали в разметке переменную Friend, в TextView взяли из нее имя друга, а вот что это за аттрибут app:letterDrawable='@{friend.name}' ?? Дело в том, что мы теперь можем создавать свои атрибуты. Для работы этого атрибута необходимо создать где-нибудь в проекте класс с любым именем (например Convrters) с таким методом
  1. @BindingAdapter("letterDrawable")
  2.     public static void setLetterDrawable(ImageView imageView, String letters) {
  3.         String firstLetter = letters.substring(0, 1);
  4.         imageView.setImageDrawable(TextDrawable.builder().buildRound(firstLetter,
  5.                 ColorGenerator.MATERIAL.getColor(firstLetter)).getCurrent());
  6.     }

Перепишем же теперь наш делегат
Открыть спойлер

Важно! Не забыть после бинда переменной вызвать executePendingBinding. Это нужно для того, чтобы элемент моментально перебиндил данные, иначе он может запустить этот процесс позже, а для списков это критично., могут появиться артефакты.

Если чем-то не подходит биндить Friend в xml, то можно достать из биндинга нужные View, у которых задан id. Для этого даже не нужно делать findViewById. В биндинге уже все есть. Например имя можно было бы задать и так
  1. viewHolder.getBinding().friendName.setText(friend.name);
Уже только ради этой фичи стоит присмотреться к биндингу =)

А вот теперь нам не нужно переделывать сам список\адаптер. Благодаря системе делегатов мы можем поменять всего одну строчку, сказав таким образом списку - импользуй этот делегат для обьекта Friend

  1. private RecyclerView.Adapter getListAdapter(List<? extends Model> models) {
  2.         return new DelegateRecyclerAdapter<>(new ArrayList<AdapterDelegate<List<? extends Model>>>() {{
  3.             //add(new FriendDelegate(1));
  4.             add(new BindingFriendDelegate(1));
  5.         }}, models, true);
  6.     }

На этом все, надеюсь мне удалось донести идею делегатов для списков. Если вас заинтересовала тема DataBinding в Android, то все возможности описаны здесь http://developer.android.com/i...ml#build_environment

Весь код доступен здесь https://github.com/NaikSoftwar...ImageAndTabs?files=1
+6   7   1
3639