В этом уроке рассмотрим, какие параметры мы можем задать для PagedList. Подробно разберем, какие значения необходимо передавать в callback.onResult в DataSource. Научимся использовать режим Placeholders.

 


Полный список уроков курса:


 

 

В прошлом уроке мы рассмотрели пример с использованием инструментов Paging Library. Напомню основные моменты кода:

 

Адаптер:

class EmployeeAdapter extends PagedListAdapter<Employee, EmployeeViewHolder> {
 
   protected EmployeeAdapter(DiffUtil.ItemCallback<Employee> diffUtilCallback) {
       super(diffUtilCallback);
   }
 
   @NonNull
   @Override
   public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
       View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.employee, parent, false);
       EmployeeViewHolder holder = new EmployeeViewHolder(view);
       return holder;
   }
 
   @Override
   public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) {
       holder.bind(getItem(position));
   }
 
}

 

DataSource:

class MyPositionalDataSource extends PositionalDataSource<Employee> {
 
    private final EmployeeStorage employeeStorage;
 
    public MyPositionalDataSource(EmployeeStorage employeeStorage) {
        this.employeeStorage = employeeStorage;
    }
 
    @Override
    public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) {
        Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition +
                ", requestedLoadSize = " + params.requestedLoadSize);
        List<Employee> result = employeeStorage.getData(params.requestedStartPosition, params.requestedLoadSize);
        callback.onResult(result, 0);
    }
 
    @Override
    public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Employee> callback) {
        Log.d(TAG, "loadRange, startPosition = " + params.startPosition + ", loadSize = " + params.loadSize);
        List<Employee> result = employeeStorage.getData(params.startPosition, params.loadSize);
        callback.onResult(result);
    }
 
}

Где, EmployeeStorage - это просто эмуляция какого-то внешнего источника данных, который содеержит 100 записей

 

Код в MainActivity, где мы все это собираем вместе:

// DataSource
MyPositionalDataSource dataSource = new MyPositionalDataSource(new EmployeeStorage());
 
 
// PagedList
PagedList.Config config = new PagedList.Config.Builder()
        .setEnablePlaceholders(false)
        .setPageSize(10)
        .build();
 
PagedList<Employee> pagedList = new PagedList.Builder<>(dataSource, config)
        .setMainThreadExecutor(new MainThreadExecutor())
        .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor())
        .build();
 
 
// Adapter
adapter = new EmployeeAdapter(diffUtilCallback);
adapter.submitList(pagedList);
 
 
// RecyclerView
recyclerView.setAdapter(adapter);

 

Мы уже знаем, как в целом работает схема. Теперь будем разбираться с нюансами. 

Давайте рассмотрим, какие параметры и настройки PagedList нам доступны

 

 

 

Initial Key

 

В нашем примере PagedList запрашивает первоначальную порцию данных с позиции 0. Но вполне может быть ситуация, когда необходимо запросить данные не с самого начала.

 

Например, в приложении открыт список и уже прокручен до какой-то позиции. Пользователь сворачивает приложение и занимается другими делами. Системе вдруг не хватает памяти, и она убивает приложение. Пользователь решает вернуться, открывает приложение и вот тут нам надо показать список на том же месте, где он был. Перед тем как приложение было убито, мы в onSaveInstanceState сохранили текущую позицию списка и теперь хотим показать данные в нем с этой же позиции. PagedList дает нам такую возможность.

 

В билдере PagedList есть параметр initialKey. Для примера зададим ему значение 50.

PagedList<Employee> pagedList = new PagedList.Builder<>(dataSource, config)
       .setMainThreadExecutor(new MainThreadExecutor())
       .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor())
       .setInitialKey(50)
       .build();

 

А в методе loadInitial в MyPositionalDataSource надо будет немного подправить параметры вызова callback.onResult.

@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) {
   Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition +
           ", requestedLoadSize = " + params.requestedLoadSize);
   List<Employee> result = employeeStorage.getData(params.requestedStartPosition, params.requestedLoadSize);
   callback.onResult(result, params.requestedStartPosition);
}

При запросе данных параметр params.requestedStartPosition будет равен 50, т.к. мы задали это в setInitialKey.

В callback.onResult мы возвращаем не только данные из Storage, но и позицию начального элемента этих данных. Т.е. мы сообщаем PagedList, что данные, которые мы ему передали, начинаются с позиции params.requestedStartPosition, т.е. 50.

Понимаю, что это выглядит как минимум странно. PagedList вроде как сам попросил нас предоставить ему данные с позиции 50. Т.е. он знает об этом. Зачем мы ему об этом дополнительно сообщаем в callback.onResult? Потому что он ждет от нас это значение. Даже если оно будет равно тому значению, которое он нам сам и прислал в params.requestedStartPosition. В дальнейшей работе он будет использовать именно то значение, которые мы вернули ему.

В простейших случаях два этих значения будут равны. И код в примере выше будет работать. Но в сложных случаях эти значения могут отличаться. Т.е. PagedList просил нас данные с позиции 50, а мы смогли предоставить только с позиции 40. Давайте рассмотрим такой нетривиальный сценарий.

 

Вспоминаем вышеописанный случай, когда приложение убивается системой, пока оно висит в фоне. Допустим, что перед тем, как быть убитым, оно подгрузило все 100 записей. Список был промотан до конца и показывал последние 10 записей: c 90 по 100. Значит при восстановлении приложения PagedList попросит у нас записи, начиная с позиции 90.

Небольшое отступление. PagedList попросит 30 записей, начиная с 90. Почему 30, ведь всего было 100 записей, и он должен попросить 10? Потому что он ничего не знает про то, сколько у нас данных в Storage. А 30 - это размер его порции начальных данных по умолчанию. И если в Storage есть только 10 записей, начиная с 90, то мы просто их и возвращаем в PagedList. Запрашиваемая и получаемая начальные порции данных необязательно должны совпадать по размеру.

Давайте представим, что, пока приложение было убито, количество данных в Storage уменьшилось до 70. А PagedList просит с 90. Наш Storage должен уметь обрабатывать такие ситуации. Т.е. когда мы у него в первоначальной загрузке попросим записи, начиная с 90, он должен понять, что таких записей больше нет, и вернуть ближайшие доступные записи.

Например, если в Storage всего 70 записей, и мы запрашиваем 30 записей, начиная с 90, то Storage может вернуть нам 30 последних записей, т.е. 30 записей с позиции 40. Мы передаем эти данные в callback.onResult, и обязательно указываем, что их позиция начинается с 40, а не с 90, как рассчитывал PagedList.

В итоге получается, что нас просили о данных с позиции 90 (params.requestedStartPosition), а в callback.onResult мы передаем список и сообщаем, что получилось добыть данные только с позиции 40. Таким образом мы скорректировали начальную позицию PagedList. Он знает, что у него есть записи с позиции 40 по позицию 70. И это позволит ему запрашивать у DataSource остальные данные, используя правильные позиции.

 

Если же мы в callback.onResult просто передадим params.requestedStartPosition (т.е. 90), то PagedList будет думать, что он получил начальные данные с позиции 90 по позицию 120. И дальше он будет запрашивать данные с 120 по 130, с 80 по 90 и т.д. Т.е. получится рассинхрон со Storage. Поэтому нам необходимо вернуть в PagedList корректное значение начальной позиции полученных данных. А чтобы мы могли это сделать, нам надо получить это значение от Storage.

Для этого я немного переписал MyPositionalDataSource:

 

class MyPositionalDataSource extends PositionalDataSource<Employee> {

   private final EmployeeStorage employeeStorage;

   public MyPositionalDataSource(EmployeeStorage employeeStorage) {
       this.employeeStorage = employeeStorage;
   }

   @Override
   public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) {
       Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition +
               ", requestedLoadSize = " + params.requestedLoadSize);
       EmployeeData result = employeeStorage.getInitialData(params.requestedStartPosition, params.requestedLoadSize);
       callback.onResult(result.data, result.position);
   }

   @Override
   public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Employee> callback) {
       Log.d(TAG, "loadRange, startPosition = " + params.startPosition + ", loadSize = " + params.loadSize);
       List<Employee> result = employeeStorage.getData(params.startPosition, params.loadSize);
       callback.onResult(result);
   }

}

В методе loadInitial я для получения данных использую employeeStorage.getInitialData. Этот метод вернет данные и позицию, с которой эти данные начинаются. Если вдруг запрашиваемых данных нет, то он сможет вернуть ближайшие данные. Это важно, потому что мы не можем передать пустой список в callback.onResult в методе loadInitial. Если мы в callback.onResult вернем пустой список и позицию 90, тем самым сообщая, что нам не удалось получить эти данные, то PagedList сообщит нам, что это наша проблема:

Initial result cannot be empty if items are present in data set

Поэтому нам надо обязательно получить от Storage какие-то начальные данные. В нашем примере я от employeeStorage.getInitialData получу 30 записей и позицию 40 и передам это в PagedList. 

 

Разумеется, может возникнуть ситуация, что данных в Storage нет совсем. Даже для первоначальной загрузки. Тогда мы передаем в callback.onResult пустой список и позицию 0. Только с позицией 0  PagedList примет от нас пустой список в методе loadInitial и поймет, что данных вообще никаких нет.

 

В методе loadRange мы для получения данных продолжаем использовать метод employeeStorage.getData. Он не должен ничего учитывать и определять. От него требуется просто вернуть данные, если они есть. А если их нет, то это будет сигналом для PagedList, что данные закончились.

 

 

Давайте посмотрим, как это выглядит в работе

PagedList просит 30 записей, начиная с 90.

loadInitial, requestedStartPosition = 90, requestedLoadSize = 30

DataSource возвращает ему 30 записей, начиная с 40, и список их отображает.

 

Сразу после первой загрузки PagedList просит у DataSource новую порцию данных: 10 штук, начиная с позиции 30.

loadRange, startPosition = 30, loadSize = 10

Т.е. он понял, что в начале списка должны быть еще данные и подгрузил одну порцию. Причем, если бы мы не скорректировали его позицию на 40 (в callback.onResult), то он считал бы, что отображает записи с 90 по 120, и запросил бы 10 записей с позиции 80. 

 

Последуюшие скроллы вверх, будут также подгружать предыдущие записи, пока не дойдем до нуля.

loadRange, startPosition = 20, loadSize = 10
loadRange, startPosition = 10, loadSize = 10
loadRange, startPosition = 0, loadSize = 10

 

При скролле в конец списка, PagedList попытается подгрузить следующие записи после 70.

loadRange, startPosition = 70, loadSize = 10

Но в Storage больше ничего нет. PagedList получит пустой список (в callback в методе loadRange) и успокоится.

 

 

 

pageSize

Параметр pageSize позволяет задать размер страницы

PagedList.Config config = new PagedList.Config.Builder()
       .setEnablePlaceholders(false)
       .setPageSize(15)
       .build();

Если мы зададим ему значение 15, то PagedList будет просить у DataSource по 15 записей.

loadInitial, requestedStartPosition = 0, requestedLoadSize = 45
loadRange, startPosition = 45, loadSize = 15
loadRange, startPosition = 60, loadSize = 15
loadRange, startPosition = 75, loadSize = 15
loadRange, startPosition = 90, loadSize = 15
loadRange, startPosition = 100, loadSize = 15

Параметр requestedLoadSize также изменился, он равен размер страницы * 3, т.е. 45

 

 

 

initialLoadSizeHint 

Этот параметр отвечает за количество данных, которое будет запрашивать PagedList при первоначальной загрузке. По умолчанию он равен pageSize * 3, но мы можем задать свое значение.

PagedList.Config config = configBuilder
       .setEnablePlaceholders(false)
       .setPageSize(10)
       .setInitialLoadSizeHint(50)
       .build();

Размер страницы задаем 10, а initialLoadSizeHint в 50.

 

Логи:

loadInitial, requestedStartPosition = 0, requestedLoadSize = 50
loadRange, startPosition = 50, loadSize = 10
loadRange, startPosition = 60, loadSize = 10
loadRange, startPosition = 70, loadSize = 10

При первой загрузке PagedList получил 50 записей. И дальше подгружает порциями по 10.

 

 

 

prefetchDistance

PagedList использует этот параметр, чтобы определить, когда надо подгружать следующую порцию данных. По умолчанию этот параметр равен pageSize. Можно задать свое значение.

PagedList.Config config = configBuilder
       .setEnablePlaceholders(false)
       .setPageSize(10)
       .setPrefetchDistance(20)
       .build();

Теперь PagedList будет подгружать новые данные, когда при прокрутке остается 20 записей до конца списка.

 

 

 

Placeholders

Презентация с Google IO.

PagedList обычно не знает сколько всего будет данных. Он по мере прокрутки списка подгружает данные и добавляет их в список. Он будет так делать, пока в Storage не закончатся данные.

Но есть и другой режим. Когда PagedList выполняет первоначальную загрузку данных, мы можем сразу сообщить ему, что у нас ожидается, например, 100 записей. PagedList сообщит об этом адаптеру, и список сразу покажет 100 записей. Реальными из них будут только те, которые были получены при первоначальной загрузке. Остальные будут фейковыми, вместо реальных данных там будут null-заглушки.

Т.е. для таких записей метод адаптера getItem(position) будет возвращать null. Соответственно, нам надо будет научить Holder адекватно реагировать на null данные, которые мы ему передаем, и отображать какую-то визуальную заглушку, показывающую пользователю, что данные пока не доступны. 

Далее, по мере прокрутки списка, PagedList будет подгружать реальные данные и отображать их вместо заглушек. 

 

Я сделал пример с размером страницы = 5 

Список содержит 100 строк. Именно это значение я передал в PagedList во время загрузки первоначальной загрузки данных. Видно, что весь список кроме начальных данных заполнен заглушками. По мере прокрутки списка выполняется подгрузка данных и заглушки заменяются полученными реальными данными. Новых записей в список добавляться уже не будет.

 

Реализовать это несложно.

 

В конфиге PagedList надо включить placeholders.

PagedList.Config config = configBuilder
       .setEnablePlaceholders(true)
       .setPageSize(5)
       .build();

 

Код метода loadInitial надо будет немного переписать.

@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) {
   Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition +
           ", requestedLoadSize = " + params.requestedLoadSize);
   EmployeeData result = employeeStorage.getInitialData(params.requestedStartPosition, params.requestedLoadSize);
   if (params.placeholdersEnabled) {
       callback.onResult(result.data, result.position, result.count);
   } else {
       callback.onResult(result.data, result.position);
   }
}

EmployeeStorage должен уметь возвращать нам количество записей, которые он содержит. Он может возвращать это значение вместе с данными в методе getInitialData. Либо можно получать это значение вызовом отдельного метода. Тут все зависит от реализации Storage.

Полученное значение нам следует передать в качестве третьего параметра в callback.onResult. Тем самым PagedList будет знать, сколько записей надо отобразить в списке.

 

Обратите внимание, в коде я делаю проверку параметра placeholdersEnabled. Если он включен, то я передаю количество записей в колбэк, иначе - не передаю. Если у вас будет включен параметр placeholdersEnabled и вы не передадите количество записей, то будет ошибка:

Placeholders requested, but totalCount not provided. Please call the three-parameter onResult method, or disable placeholders in the PagedList.Config

PagedList не сможет заполнить список заглушками, потому что он не знает, сколько записей в нем будет.

 

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

public void bind(Employee employee) {
   if (employee == null) {
       textViewName.setText(R.string.wait);
       textViewSalary.setText(R.string.wait);
   } else {
       textViewName.setText(employee.name);
       textViewSalary.setText(employee.salary);
   }
}

Если адаптер вместо реальных данных дает null, значит это заглушка и надо отобразить какой-то временный текст.


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal