Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update paged list #421

Merged
merged 5 commits into from
Apr 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.airbnb.epoxy;

import android.arch.paging.PagedList;

import com.airbnb.epoxy.integrationtest.BuildConfig;
import com.airbnb.epoxy.integrationtest.Model_;
import com.airbnb.epoxy.paging.PagingEpoxyController;
Expand Down Expand Up @@ -47,9 +49,14 @@ public void setup() {

@Test
public void initialPageBind() {
controller.setConfig(
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(100)
.setInitialLoadSizeHint(100)
.build()
);
controller.setListWithSize(500);
controller.setPageSizeHint(10);
controller.setNumPagesToLoad(10);

List<EpoxyModel<?>> models = controller.getAdapter().getCopyOfModels();
assertEquals(100, models.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,14 @@ public void setup() {

@Test
public void initialPageBind() {
controller.setConfig(
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(100)
.setInitialLoadSizeHint(100)
.build()
);
controller.setPagedListWithSize(500);
controller.setPageSizeHint(10);
controller.setNumPagesToLoad(10);

List<EpoxyModel<?>> models = controller.getAdapter().getCopyOfModels();
assertEquals(100, models.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.arch.paging.PagedList;
import android.arch.paging.PagedList.Callback;
import android.arch.paging.PagedList.Config;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand Down Expand Up @@ -34,54 +35,45 @@
* @param <T> The type of item in the list
*/
public abstract class PagingEpoxyController<T> extends EpoxyController {
private static final int DEFAULT_PAGE_SIZE_HINT = 10;
private static final int DEFAULT_NUM_PAGES_T0_LOAD = 10;

private static final Config DEFAULT_CONFIG = new Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(100)
.setInitialLoadSizeHint(100)
.setPrefetchDistance(20)
.build();

@Nullable private PagedList<T> pagedList;
@NonNull private List<T> list = Collections.emptyList();

private int pageSizeHint = DEFAULT_PAGE_SIZE_HINT;
private int numPagesToLoad = DEFAULT_NUM_PAGES_T0_LOAD;

// TODO: (eli_hart 10/13/17) Save this in saved state and restore in constructor
private int lastBoundPositionWithinList = 0;
private boolean scrollingTowardsEnd = true;
private int numBoundModels;
private int lastBuiltLowerBound = 0;
private int lastBuiltUpperBound = 0;

/**
* Set an estimate of how many items will be shown on screen. This number will be used to
* calculate how many models should be built.
* <p>
* Setting this is optional - once the screen is fully populated Epoxy can track the number of
* bound items to determine the page size, so this is mostly useful for initial page load.
* <p>
* The default is {@link #DEFAULT_PAGE_SIZE_HINT}
*/
public void setPageSizeHint(int pageSizeHint) {
this.pageSizeHint = pageSizeHint;
}

/**
* Set how many pages of items in the list should be built as EpoxyModels at a time. The lower the
* number the faster the model build and diff times will be, but it will also require more calls
* to rebuild models as the user scrolls.
* <p>
* The default is {@link #DEFAULT_NUM_PAGES_T0_LOAD}
*/
public void setNumPagesToLoad(int numPagesToLoad) {
this.numPagesToLoad = numPagesToLoad;
}
@Nullable private Config customConfig = null;
private boolean isFirstBuildForList = true;
/** Prevent excessively throwing this exception. */
private boolean hasNotifiedInsufficientPageSize;

@Override
protected final void buildModels() {
int numListItemsToUse =
numBoundModels != 0 ? numBoundModels * numPagesToLoad : pageSizeHint * numPagesToLoad;
int numListItemsToUse = isFirstBuildForList ? config().initialLoadSizeHint : config().pageSize;
if (!list.isEmpty()) {
isFirstBuildForList = false;
}

int numBoundViews = getAdapter().getBoundViewHolders().size();
if (!hasNotifiedInsufficientPageSize && numBoundViews > numListItemsToUse) {
onExceptionSwallowed(new IllegalStateException(
"The page size specified in your PagedList config is smaller than the number of items "
+ "shown on screen. Increase your page size and/or initial load size."));
hasNotifiedInsufficientPageSize = true;
}

// If we are scrolling towards one end of the list we can build slightly more models in that
// If we are scrolling towards one end of the list we can build more models in that
// direction in anticipation of needing to show more there soon
float ratioOfEndItems = scrollingTowardsEnd ? .6f : .4f;
float ratioOfEndItems = scrollingTowardsEnd ? .7f : .3f;

int itemsToBuildTowardsEnd = (int) (numListItemsToUse * ratioOfEndItems);
int itemsToBuildTowardsStart = numListItemsToUse - itemsToBuildTowardsEnd;
Expand Down Expand Up @@ -123,28 +115,34 @@ protected void onModelBound(@NonNull EpoxyViewHolder holder, @NonNull EpoxyModel

int positionWithinList = positionWithinCurrentModels + lastBuiltLowerBound;

if (pagedList != null) {
if (pagedList != null && !pagedList.isEmpty()) {
pagedList.loadAround(positionWithinList);
}

scrollingTowardsEnd = lastBoundPositionWithinList < positionWithinCurrentModels;
scrollingTowardsEnd = lastBoundPositionWithinList < positionWithinList;
lastBoundPositionWithinList = positionWithinList;
numBoundModels++;

// TODO: (eli_hart 9/19/17) different prefetch depending on scroll direction?
// build again?
int prefetchDistance = numBoundModels;
int currentModelCount = getAdapter().getItemCount();
if (((currentModelCount - positionWithinCurrentModels - 1 < prefetchDistance)
|| (positionWithinCurrentModels < prefetchDistance && lastBuiltLowerBound != 0))) {

int prefetchDistance = config().prefetchDistance;
final int distanceToEndOfPage = getAdapter().getItemCount() - positionWithinCurrentModels;
final int distanceToStartOfPage = positionWithinCurrentModels;

if ((distanceToEndOfPage < prefetchDistance && !hasBuiltLastItem() && scrollingTowardsEnd)
|| (distanceToStartOfPage < prefetchDistance && !hasBuiltFirstItem()
&& !scrollingTowardsEnd)) {
requestModelBuild();
}
}

@CallSuper
@Override
protected void onModelUnbound(@NonNull EpoxyViewHolder holder, @NonNull EpoxyModel<?> model) {
numBoundModels--;
private boolean hasBuiltFirstItem() {
return lastBuiltLowerBound == 0;
}

private boolean hasBuiltLastItem() {
return lastBuiltUpperBound >= totalListSize();
}

public int totalListSize() {
return pagedList != null ? pagedList.size() : list.size();
}

public void setList(@Nullable List<T> list) {
Expand All @@ -157,9 +155,21 @@ public void setList(@Nullable List<T> list) {
}

this.list = list == null ? Collections.<T>emptyList() : list;
isFirstBuildForList = true;
requestModelBuild();
}

/**
* Set a PagedList that should be used to build models in this controller. A listener will be
* attached to the list so that models are rebuilt as new list items are loaded.
* <p>
* By default the Config setting on the PagedList will dictate how many models are built at once,
* and what prefetch thresholds should be used. This can be overridden with a separate Config via
* {@link #setConfig(Config)}.
* <p>
* See {@link #setConfig(Config)} for details on how the Config settings are used, and for
* recommended values.
*/
public void setList(@Nullable PagedList<T> list) {
if (list == this.pagedList) {
return;
Expand All @@ -176,9 +186,53 @@ public void setList(@Nullable PagedList<T> list) {
list.addWeakCallback(null, callback);
}

isFirstBuildForList = true;
updatePagedListSnapshot();
}

/**
* Set a Config value to specify how many models should be built at a time.
* <p>
* If not set, or set to null, the config value off of the currently set PagedList is used.
* <p>
* If no PagedList is set, {@link #DEFAULT_CONFIG} is used.
* <p>
* {@link Config#initialLoadSizeHint} dictates how many models are built on first load. This
* should be several times the number of items shown on screen, and is generally equal to or
* larger than pageSize.
* <p>
* {@link Config#pageSize} dictates how many models are built at a time after first load. This
* should be several times the number of items shown on screen (roughly 10x, and at least 5x). If
* this value is too small models will be rebuilt very often as the user scrolls, potentially
* hurting performance. In the worst case, if this value is too small, not enough models will be
* created to fill the whole screen and the controller will enter an infinite loop of rebuilding
* models.
* <p>
* {@link Config#prefetchDistance} defines how far from the edge of built models the user must
* scroll to trigger further model building. Should be significantly less than page size (roughly
* 1/4), and more than the number of items shown on screen. If this value is too big models will
* be rebuilt very often as the user scrolls, potentially hurting performance. If this number is
* too small then the user may have to wait while models are rebuilt as they scroll.
* <p>
* For example, if 5 items are shown on screen at once you might have a initialLoadSizeHint of 50,
* a pageSize of 50, and a prefetchDistance of 10.
*/
public void setConfig(@Nullable Config config) {
customConfig = config;
}

private Config config() {
if (customConfig != null) {
return customConfig;
}

if (pagedList != null) {
return pagedList.getConfig();
}

return DEFAULT_CONFIG;
}

/**
* @return The list currently being displayed by the EpoxyController. This is either the Java List
* set with {@link #setList(List)}, the latest snapshot if a PagedList is set, or an empty list if
Expand Down
4 changes: 2 additions & 2 deletions epoxy-pagingsample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ android {
dependencies {
compile rootProject.deps.kotlin

compile "org.jetbrains.anko:anko-coroutines:0.10.1"
compile "org.jetbrains.anko:anko-coroutines:0.10.4"
compile 'android.arch.persistence.room:runtime:1.0.0'
kapt 'android.arch.persistence.room:compiler:1.0.0'

compile project(':epoxy-adapter')
compile project(':epoxy-paging')
kapt project(':epoxy-processor')

implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:appcompat-v7:27.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
exclude group: 'com.android.support', module: 'support-annotations'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.airbnb.epoxy.paging.PagingEpoxyController
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import org.jetbrains.anko.coroutines.experimental.bg
import java.lang.RuntimeException
import java.util.concurrent.Executor

class PagingSampleActivity : AppCompatActivity() {
Expand All @@ -38,17 +39,16 @@ class PagingSampleActivity : AppCompatActivity() {
async(UI) {
val pagedList = bg {
db.userDao().delete(db.userDao().all)
for (i in 1..3000) {
db.userDao().insertAll(User(i))
}
(1..3000)
.map { User(it) }
.let { db.userDao().insertAll(*it.toTypedArray()) }

return@bg PagedList.Builder<Int, User>(
PagedList.Builder<Int, User>(
db.userDao().dataSource.create(),
PagedList.Config.Builder().run {
setEnablePlaceholders(false)
setPageSize(40)
setInitialLoadSizeHint(80)
setPrefetchDistance(50)
setPageSize(150)
setPrefetchDistance(30)
build()
}).run {
setNotifyExecutor(UiThreadExecutor)
Expand All @@ -60,15 +60,21 @@ class PagingSampleActivity : AppCompatActivity() {
pagingController.setList(pagedList.await())
}

pagingController.setList(emptyList())
}
}

class TestController : PagingEpoxyController<User>() {
init {
setDebugLoggingEnabled(true)
isDebugLoggingEnabled = true
}

override fun buildModels(users: List<User>) {
pagingView {
id("header")
name("Header")
}

users.forEach {
pagingView {
id(it.uid)
Expand All @@ -77,6 +83,10 @@ class TestController : PagingEpoxyController<User>() {
}
}

override fun onExceptionSwallowed(exception: RuntimeException) {
throw exception
}

}

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
Expand All @@ -90,9 +100,9 @@ class PagingView(context: Context) : AppCompatTextView(context) {
}

object UiThreadExecutor : Executor {
private val mHandler = Handler(Looper.getMainLooper())
private val handler = Handler(Looper.getMainLooper())

override fun execute(command: Runnable) {
mHandler.post(command)
handler.post(command)
}
}