В этом уроке рассмотрим, какие параметры мы можем задать для 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

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, значит это заглушка и надо отобразить какой-то временный текст.


Language

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

 

Telegram канал



Android чат в Telegram



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



Страница в Facebook