From 4762a7330fb5d409ef35d5b5bea497974b5d1d9c Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 7 Feb 2018 17:07:41 -0600 Subject: [PATCH] Use paging library --- app/build.gradle | 1 + .../astrid/activity/TaskListFragment.java | 80 ++++---------- .../todoroo/astrid/adapter/TaskAdapter.java | 41 ++----- .../java/com/todoroo/astrid/dao/TaskDao.java | 26 +++-- .../org/tasks/data/LimitOffsetDataSource.java | 97 +++++++++++++++++ .../org/tasks/data/TaskListDataProvider.java | 29 ++++- .../tasklist/TaskListRecyclerAdapter.java | 100 ++++++++++++++++-- .../main/res/layout/task_list_body_empty.xml | 1 + 8 files changed, 265 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/org/tasks/data/LimitOffsetDataSource.java diff --git a/app/build.gradle b/app/build.gradle index 4b0ff740a..cce1bec87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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}" diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java index f25cd08b2..5805c6137 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java @@ -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 oldTasks; - private final List newTasks; - - public DiffUtilCallback(List oldTasks, List 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 oldTasks = taskAdapter.getTasks(); - List 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() { diff --git a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java index 02f57a5bd..93c784385 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/TaskAdapter.java @@ -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 tasks = new ArrayList<>(); - - public List getTaskPositions(List longs) { - List 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 helper; - public void setTasks(List tasks) { - this.tasks.clear(); - this.tasks.addAll(tasks); + public int getCount() { + return helper.getItemCount(); } - public int getCount() { - return tasks.size(); + public void setHelper(PagedListAdapterHelper helper) { + this.helper = helper; } public interface OnCompletedTaskListener { @@ -87,20 +70,16 @@ public class TaskAdapter { } - public List 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) { diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java index 33549e2b2..a29161315 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -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 fetchFiltered(String query) { - return fetchFiltered(query, Task.PROPERTIES); - } - - public List fetchFiltered(String queryTemplate, Property... properties) { - Query query = Query.select(properties).withQueryTemplate(PermaSql.replacePlaceholders(queryTemplate)); + public List 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 convertRows(Cursor cursor) { + List result = new ArrayList<>(); + while (cursor.moveToNext()) { + result.add(new Task(cursor)); + } + return result; + } + }; + } } diff --git a/app/src/main/java/org/tasks/data/LimitOffsetDataSource.java b/app/src/main/java/org/tasks/data/LimitOffsetDataSource.java new file mode 100644 index 000000000..93e2e45a1 --- /dev/null +++ b/app/src/main/java/org/tasks/data/LimitOffsetDataSource.java @@ -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 { + + 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 convertRows(Cursor cursor) { + List result = new ArrayList<>(); + while (cursor.moveToNext()) { + result.add(new Task(cursor)); + } + return result; + } + + @Nullable + @WorkerThread + public List 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 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 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 callback) { + List list = loadRange(params.startPosition, params.loadSize); + if (list != null) { + callback.onResult(list); + } else { + invalidate(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/TaskListDataProvider.java b/app/src/main/java/org/tasks/data/TaskListDataProvider.java index f76db943d..72f432d17 100644 --- a/app/src/main/java/org/tasks/data/TaskListDataProvider.java +++ b/app/src/main/java/org/tasks/data/TaskListDataProvider.java @@ -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 toList(Filter filter, Property[] properties) { + public LiveData> getLiveData(Filter filter, Property[] properties) { + return new LivePagedListBuilder<>( + new LivePagedListProvider() { + @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(); } } diff --git a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java index 1b82ce610..985249e28 100644 --- a/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java +++ b/app/src/main/java/org/tasks/tasklist/TaskListRecyclerAdapter.java @@ -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 implements ViewHolder.ViewHolderCallbacks { +public class TaskListRecyclerAdapter extends RecyclerView.Adapter + implements ViewHolder.ViewHolderCallbacks, ListUpdateCallback { private static final String EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"; + private static final DiffCallback DIFF_CALLBACK = new DiffCallback() { + @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 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 im itemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback()); } + private PagedListAdapterHelper adapterHelper = new PagedListAdapterHelper<>(this, new ListAdapterConfig.Builder().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 im public Bundle getSaveState() { Bundle information = new Bundle(); - List selectedTaskIds = transform(multiSelector.getSelectedPositions(), adapter::getTaskId); + List selectedTasks = transform(multiSelector.getSelectedPositions(), adapterHelper::getItem); + List 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 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 im } } + private List getTaskPositions(List longs) { + List 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 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 im } } - private List getTasks() { - return newArrayList(transform(multiSelector.getSelectedPositions(), adapter::getTask)); + private List getSelectedTasks() { + return newArrayList(transform(multiSelector.getSelectedPositions(), adapterHelper::getItem)); } private void deleteSelectedItems() { tracker.reportEvent(Tracking.Events.MULTISELECT_DELETE); - List tasks = getTasks(); + List tasks = getSelectedTasks(); mode.finish(); int result = taskDeleter.markDeleted(tasks); taskList.onTaskDelete(tasks); @@ -173,7 +211,7 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter im private void copySelectedItems() { tracker.reportEvent(Tracking.Events.MULTISELECT_CLONE); - List tasks = getTasks(); + List tasks = getSelectedTasks(); mode.finish(); List duplicates = taskDuplicator.duplicate(tasks); taskList.onTaskCreated(duplicates); @@ -223,6 +261,46 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter 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 list) { + adapterHelper.setList(list); + } + + public void setAnimate(boolean animate) { + this.animate = animate; + } + + public PagedListAdapterHelper getHelper() { + return adapterHelper; + } + private class ItemTouchHelperCallback extends ItemTouchHelper.Callback { private int from = -1; diff --git a/app/src/main/res/layout/task_list_body_empty.xml b/app/src/main/res/layout/task_list_body_empty.xml index 24b80e07f..03b56ef02 100644 --- a/app/src/main/res/layout/task_list_body_empty.xml +++ b/app/src/main/res/layout/task_list_body_empty.xml @@ -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">