Use paging library

synthesis
Alex Baker 6 years ago
parent 3009004dcf
commit 4762a7330f

@ -113,6 +113,7 @@ dependencies {
compile "android.arch.persistence.room:rxjava2:${ROOM_VERSION}"
annotationProcessor "android.arch.persistence.room:compiler:${ROOM_VERSION}"
compile "io.reactivex.rxjava2:rxandroid:2.0.1"
compile "android.arch.paging:runtime:1.0.0-alpha5"
annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}"
compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}"

@ -16,7 +16,6 @@ import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -348,6 +347,25 @@ public class TaskListFragment extends InjectingFragment implements
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
taskListDataProvider.getLiveData(filter, taskProperties())
.observe(getActivity(), list -> {
if (list.isEmpty()) {
swipeRefreshLayout.setVisibility(View.GONE);
emptyRefreshLayout.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setVisibility(View.VISIBLE);
emptyRefreshLayout.setVisibility(View.GONE);
}
// stash selected items
Bundle saveState = recyclerAdapter.getSaveState();
recyclerAdapter.setList(list);
recyclerAdapter.restoreSaveState(saveState);
}
);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerAdapter.applyToRecyclerView(recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
@ -394,6 +412,7 @@ public class TaskListFragment extends InjectingFragment implements
* broadcast. Subclasses should override this.
*/
private void refresh() {
// TODO: compare indents in diff callback, then animate this
loadTaskListContent(!(this instanceof GtasksSubtaskListFragment));
setSyncOngoing(gtasksPreferenceService.isOngoing());
@ -405,68 +424,14 @@ public class TaskListFragment extends InjectingFragment implements
* ======================================================================
*/
private static class DiffUtilCallback extends DiffUtil.Callback {
private final List<Task> oldTasks;
private final List<Task> newTasks;
public DiffUtilCallback(List<Task> oldTasks, List<Task> newTasks) {
this.oldTasks = oldTasks;
this.newTasks = newTasks;
}
@Override
public int getOldListSize() {
return oldTasks.size();
}
@Override
public int getNewListSize() {
return newTasks.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldTasks.get(oldItemPosition).getId() == newTasks.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldTasks.get(oldItemPosition).equals(newTasks.get(newItemPosition));
}
}
public void loadTaskListContent() {
loadTaskListContent(true);
}
public void loadTaskListContent(boolean animate) {
if (taskAdapter == null || swipeRefreshLayout == null) {
return;
}
// stash selected items
Bundle saveState = recyclerAdapter.getSaveState();
recyclerAdapter.setAnimate(animate);
List<Task> oldTasks = taskAdapter.getTasks();
List<Task> newTasks = taskListDataProvider.toList(filter, taskProperties());
taskAdapter.setTasks(newTasks);
if (animate) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtilCallback(oldTasks, newTasks), true);
diffResult.dispatchUpdatesTo(recyclerAdapter);
} else {
recyclerAdapter.notifyDataSetChanged();
}
recyclerAdapter.restoreSaveState(saveState);
if (newTasks.isEmpty()) {
swipeRefreshLayout.setVisibility(View.GONE);
emptyRefreshLayout.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setVisibility(View.VISIBLE);
emptyRefreshLayout.setVisibility(View.GONE);
}
taskListDataProvider.invalidate();
}
protected TaskAdapter createTaskAdapter() {
@ -489,6 +454,7 @@ public class TaskListFragment extends InjectingFragment implements
taskAdapter = createTaskAdapter();
recyclerAdapter = new TaskListRecyclerAdapter(getActivity(), taskAdapter, viewHolderFactory,
this, taskDeleter, taskDuplicator, tracker, dialogBuilder);
taskAdapter.setHelper(recyclerAdapter.getHelper());
}
public Property<?>[] taskProperties() {

@ -5,6 +5,8 @@
*/
package com.todoroo.astrid.adapter;
import android.arch.paging.PagedListAdapterHelper;
import com.google.common.collect.ObjectArrays;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.LongProperty;
@ -14,14 +16,6 @@ import com.todoroo.astrid.data.Task;
import org.tasks.data.TaskAttachment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.astrid.data.Task.NO_ID;
import static com.todoroo.astrid.data.Task.NO_UUID;
/**
* Adapter for displaying a user's tasks as a list
*
@ -30,25 +24,14 @@ import static com.todoroo.astrid.data.Task.NO_UUID;
*/
public class TaskAdapter {
private final List<Task> tasks = new ArrayList<>();
public List<Integer> getTaskPositions(List<Long> longs) {
List<Integer> result = new ArrayList<>();
for (int i = 0 ; i < tasks.size() ; i++) {
if (longs.contains(tasks.get(i).getId())) {
result.add(i);
}
}
return result;
}
private PagedListAdapterHelper<Task> helper;
public void setTasks(List<Task> tasks) {
this.tasks.clear();
this.tasks.addAll(tasks);
public int getCount() {
return helper.getItemCount();
}
public int getCount() {
return tasks.size();
public void setHelper(PagedListAdapterHelper<Task> helper) {
this.helper = helper;
}
public interface OnCompletedTaskListener {
@ -87,20 +70,16 @@ public class TaskAdapter {
}
public List<Task> getTasks() {
return newArrayList(tasks);
}
public long getTaskId(int position) {
return position < tasks.size() ? tasks.get(position).getId() : NO_ID;
return getTask(position).getId();
}
public Task getTask(int position) {
return position < tasks.size() ? tasks.get(position) : null;
return helper.getItem(position);
}
protected String getItemUuid(int position) {
return position < tasks.size() ? tasks.get(position).getUuid() : NO_UUID;
return getTask(position).getUuid();
}
public void onCompletedTask(Task task, boolean newState) {

@ -21,6 +21,7 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.UUIDHelper;
import org.tasks.BuildConfig;
import org.tasks.data.LimitOffsetDataSource;
import org.tasks.jobs.AfterSaveIntentService;
import java.util.ArrayList;
@ -241,12 +242,8 @@ public abstract class TaskDao {
return fetchFiltered(filter.getSqlQuery());
}
public List<Task> fetchFiltered(String query) {
return fetchFiltered(query, Task.PROPERTIES);
}
public List<Task> fetchFiltered(String queryTemplate, Property<?>... properties) {
Query query = Query.select(properties).withQueryTemplate(PermaSql.replacePlaceholders(queryTemplate));
public List<Task> fetchFiltered(String queryTemplate) {
Query query = Query.select(Task.PROPERTIES).withQueryTemplate(PermaSql.replacePlaceholders(queryTemplate));
String queryString = query.from(Task.TABLE).toString();
if (BuildConfig.DEBUG) {
Timber.v(queryString);
@ -262,5 +259,22 @@ public abstract class TaskDao {
cursor.close();
}
}
public LimitOffsetDataSource getLimitOffsetDataSource(String queryTemplate, Property<?>... properties) {
String query = Query
.select(properties)
.withQueryTemplate(PermaSql.replacePlaceholders(queryTemplate))
.from(Task.TABLE).toString();
return new LimitOffsetDataSource(database, query) {
@Override
protected List<Task> convertRows(Cursor cursor) {
List<Task> result = new ArrayList<>();
while (cursor.moveToNext()) {
result.add(new Task(cursor));
}
return result;
}
};
}
}

@ -0,0 +1,97 @@
package org.tasks.data;
import android.arch.paging.PositionalDataSource;
import android.arch.persistence.room.RoomDatabase;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public abstract class LimitOffsetDataSource extends PositionalDataSource<Task> {
private final String mCountQuery;
private final String mLimitOffsetQuery;
private final RoomDatabase mDb;
protected LimitOffsetDataSource(RoomDatabase db, String query) {
mDb = db;
mCountQuery = "SELECT COUNT(*) FROM ( " + query + " )";
mLimitOffsetQuery = "SELECT * FROM ( " + query + " ) LIMIT ? OFFSET ?";
}
@WorkerThread
public int countItems() {
Cursor cursor = mDb.query(mCountQuery, null);
try {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
return 0;
} finally {
cursor.close();
}
}
@SuppressWarnings("WeakerAccess")
protected List<Task> convertRows(Cursor cursor) {
List<Task> result = new ArrayList<>();
while (cursor.moveToNext()) {
result.add(new Task(cursor));
}
return result;
}
@Nullable
@WorkerThread
public List<Task> loadRange(int startPosition, int loadCount) {
Cursor cursor = mDb.query(mLimitOffsetQuery, new Object[] { loadCount, startPosition });
//noinspection TryFinallyCanBeTryWithResources
try {
return convertRows(cursor);
} finally {
cursor.close();
}
}
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<Task> callback) {
int totalCount = countItems();
if (totalCount == 0) {
callback.onResult(Collections.emptyList(), 0, 0);
return;
}
// bound the size requested, based on known count
final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
// convert from legacy behavior
List<Task> list = loadRange(firstLoadPosition, firstLoadSize);
if (list != null && list.size() == firstLoadSize) {
callback.onResult(list, firstLoadPosition, totalCount);
} else {
// null list, or size doesn't match request
// The size check is a WAR for Room 1.0, subsequent versions do the check in Room
invalidate();
}
}
@WorkerThread
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<Task> callback) {
List<Task> list = loadRange(params.startPosition, params.loadSize);
if (list != null) {
callback.onResult(list);
} else {
invalidate();
}
}
}

@ -1,5 +1,10 @@
package org.tasks.data;
import android.arch.lifecycle.LiveData;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.LivePagedListProvider;
import android.arch.paging.PagedList;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Field;
@ -12,9 +17,6 @@ import com.todoroo.astrid.data.Task;
import org.tasks.preferences.Preferences;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import static com.todoroo.astrid.activity.TaskListFragment.FILE_METADATA_JOIN;
@ -24,6 +26,7 @@ public class TaskListDataProvider {
private final TaskDao taskDao;
private final Preferences preferences;
private LimitOffsetDataSource latest;
@Inject
public TaskListDataProvider(TaskDao taskDao, Preferences preferences) {
@ -31,7 +34,19 @@ public class TaskListDataProvider {
this.preferences = preferences;
}
public List<Task> toList(Filter filter, Property<?>[] properties) {
public LiveData<PagedList<Task>> getLiveData(Filter filter, Property<?>[] properties) {
return new LivePagedListBuilder<>(
new LivePagedListProvider<Integer, Task>() {
@Override
protected LimitOffsetDataSource createDataSource() {
latest = toDataSource(filter, properties);
return latest;
}
}, 20)
.build();
}
private LimitOffsetDataSource toDataSource(Filter filter, Property<?>[] properties) {
Criterion tagsJoinCriterion = Criterion.and(
Task.ID.eq(Field.field(TAGS_METADATA_JOIN + ".task")));
@ -60,6 +75,10 @@ public class TaskListDataProvider {
groupedQuery = query + " GROUP BY " + Task.ID;
}
return taskDao.fetchFiltered(groupedQuery, properties);
return taskDao.getLimitOffsetDataSource(groupedQuery, properties);
}
public void invalidate() {
latest.invalidate();
}
}

@ -1,8 +1,14 @@
package org.tasks.tasklist;
import android.app.Activity;
import android.arch.paging.PagedList;
import android.arch.paging.PagedListAdapterHelper;
import android.graphics.Canvas;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.recyclerview.extensions.DiffCallback;
import android.support.v7.recyclerview.extensions.ListAdapterConfig;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
@ -29,15 +35,29 @@ import org.tasks.analytics.Tracking;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.ui.MenuColorizer;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> implements ViewHolder.ViewHolderCallbacks {
public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
implements ViewHolder.ViewHolderCallbacks, ListUpdateCallback {
private static final String EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids";
private static final DiffCallback<Task> DIFF_CALLBACK = new DiffCallback<Task>() {
@Override
public boolean areItemsTheSame(@NonNull Task oldItem, @NonNull Task newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Task oldItem, @NonNull Task newItem) {
return oldItem.equals(newItem);
}
};
private final MultiSelector multiSelector = new MultiSelector();
private final Activity activity;
@ -52,6 +72,7 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
private ActionMode mode = null;
private boolean dragging;
private boolean animate;
public TaskListRecyclerAdapter(Activity activity, TaskAdapter adapter,
ViewHolderFactory viewHolderFactory,
@ -69,6 +90,8 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
itemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback());
}
private PagedListAdapterHelper<Task> adapterHelper = new PagedListAdapterHelper<>(this, new ListAdapterConfig.Builder<Task>().setDiffCallback(DIFF_CALLBACK).build());
public void applyToRecyclerView(RecyclerView recyclerView) {
recyclerView.setAdapter(this);
itemTouchHelper.attachToRecyclerView(recyclerView);
@ -76,7 +99,8 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
public Bundle getSaveState() {
Bundle information = new Bundle();
List<Long> selectedTaskIds = transform(multiSelector.getSelectedPositions(), adapter::getTaskId);
List<Task> selectedTasks = transform(multiSelector.getSelectedPositions(), adapterHelper::getItem);
List<Long> selectedTaskIds = transform(selectedTasks, Task::getId);
information.putLongArray(EXTRA_SELECTED_TASK_IDS, Longs.toArray(selectedTaskIds));
return information;
}
@ -87,7 +111,7 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
mode = ((TaskListActivity) activity).startSupportActionMode(actionModeCallback);
multiSelector.setSelectable(true);
for (int position : adapter.getTaskPositions(Longs.asList(longArray))) {
for (int position : getTaskPositions(Longs.asList(longArray))) {
multiSelector.setSelected(position, 0L, true);
}
@ -95,6 +119,17 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
}
}
private List<Integer> getTaskPositions(List<Long> longs) {
List<Integer> result = new ArrayList<>();
for (int i = 0 ; i < adapterHelper.getItemCount() ; i++) {
Task item = adapterHelper.getItem(i);
if (longs.contains(item.getId())) {
result.add(i);
}
}
return result;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewGroup view = (ViewGroup) LayoutInflater.from(activity)
@ -104,14 +139,17 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bindView(adapter.getTask(position));
holder.setMoving(false);
holder.setIndent(adapter.getIndent(holder.task));
Task task = adapterHelper.getItem(position);
if (task != null) {
holder.bindView(task);
holder.setMoving(false);
holder.setIndent(adapter.getIndent(task));
}
}
@Override
public int getItemCount() {
return adapter.getCount();
return adapterHelper.getItemCount();
}
@Override
@ -158,13 +196,13 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
}
}
private List<Task> getTasks() {
return newArrayList(transform(multiSelector.getSelectedPositions(), adapter::getTask));
private List<Task> getSelectedTasks() {
return newArrayList(transform(multiSelector.getSelectedPositions(), adapterHelper::getItem));
}
private void deleteSelectedItems() {
tracker.reportEvent(Tracking.Events.MULTISELECT_DELETE);
List<Task> tasks = getTasks();
List<Task> tasks = getSelectedTasks();
mode.finish();
int result = taskDeleter.markDeleted(tasks);
taskList.onTaskDelete(tasks);
@ -173,7 +211,7 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
private void copySelectedItems() {
tracker.reportEvent(Tracking.Events.MULTISELECT_CLONE);
List<Task> tasks = getTasks();
List<Task> tasks = getSelectedTasks();
mode.finish();
List<Task> duplicates = taskDuplicator.duplicate(tasks);
taskList.onTaskCreated(duplicates);
@ -223,6 +261,46 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> im
}
};
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
if (animate) {
notifyItemMoved(fromPosition, toPosition);
} else {
notifyDataSetChanged();
}
}
@Override
public void onChanged(int position, int count, Object payload) {
if (animate) {
notifyItemRangeChanged(position, count, payload);
} else {
notifyDataSetChanged();
}
}
public void setList(PagedList<Task> list) {
adapterHelper.setList(list);
}
public void setAnimate(boolean animate) {
this.animate = animate;
}
public PagedListAdapterHelper<Task> getHelper() {
return adapterHelper;
}
private class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
private int from = -1;

@ -4,6 +4,7 @@
style="@style/task_list_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
tools:ignore="UnusedAttribute">
<ScrollView

Loading…
Cancel
Save