Use paging library for non-manually sorted lists

pull/820/head
Alex Baker 5 years ago
parent ea33eed50a
commit 752f0a6661

@ -136,6 +136,7 @@ dependencies {
annotationProcessor "androidx.room:room-compiler:${ROOM_VERSION}"
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "androidx.paging:paging-runtime:2.1.0"
annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}"
implementation "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}"

@ -702,3 +702,15 @@
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/census-instrumentation/opencensus-java
- artifact: androidx.paging:paging-common:+
name: Android Paging-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.paging:paging-runtime:+
name: Android Paging-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html

@ -1887,6 +1887,38 @@
"group": "io.opencensus",
"version": "0.18.0"
}
},
{
"notice": null,
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"year": null,
"url": "https://developer.android.com/topic/libraries/architecture/index.html",
"libraryName": "Android Paging-Common",
"artifactId": {
"name": "paging-common",
"group": "androidx.paging",
"version": "2.1.0"
}
},
{
"notice": null,
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"year": null,
"url": "https://developer.android.com/topic/libraries/architecture/index.html",
"libraryName": "Android Paging-Runtime",
"artifactId": {
"name": "paging-runtime",
"group": "androidx.paging",
"version": "2.1.0"
}
}
]
}

@ -29,7 +29,6 @@ import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -83,6 +82,8 @@ import org.tasks.preferences.Device;
import org.tasks.preferences.Preferences;
import org.tasks.sync.SyncAdapters;
import org.tasks.tasklist.ActionModeProvider;
import org.tasks.tasklist.ManualSortRecyclerAdapter;
import org.tasks.tasklist.PagedListRecyclerAdapter;
import org.tasks.tasklist.TaskListRecyclerAdapter;
import org.tasks.tasklist.ViewHolderFactory;
import org.tasks.ui.CheckBoxes;
@ -237,12 +238,22 @@ public final class TaskListFragment extends InjectingFragment
taskListViewModel.setFilter(filter, taskAdapter.isManuallySorted());
recyclerAdapter =
new TaskListRecyclerAdapter(
taskAdapter, viewHolderFactory, this, actionModeProvider, taskListViewModel.getValue());
taskAdapter.isManuallySorted()
? new ManualSortRecyclerAdapter(
taskAdapter,
recyclerView,
viewHolderFactory,
this,
actionModeProvider,
taskListViewModel.getValue())
: new PagedListRecyclerAdapter(
taskAdapter,
viewHolderFactory,
this,
actionModeProvider,
taskListViewModel.getValue());
taskAdapter.setHelper(recyclerAdapter);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
new ItemTouchHelper(recyclerAdapter.getItemTouchHelperCallback())
.attachToRecyclerView(recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
taskListViewModel.observe(

@ -10,6 +10,7 @@ import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.DateUtilities.now;
import android.database.Cursor;
import androidx.paging.DataSource;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
@ -26,6 +27,9 @@ import com.todoroo.astrid.helper.UUIDHelper;
import java.util.ArrayList;
import java.util.List;
import org.tasks.BuildConfig;
import org.tasks.data.CaldavTask;
import org.tasks.data.GoogleTask;
import org.tasks.data.Tag;
import org.tasks.data.TaskContainer;
import org.tasks.jobs.WorkManager;
import timber.log.Timber;
@ -131,6 +135,9 @@ public abstract class TaskDao {
@RawQuery
public abstract List<TaskContainer> fetchTasks(SimpleSQLiteQuery query);
@RawQuery(observedEntities = {Task.class, GoogleTask.class, CaldavTask.class, Tag.class})
public abstract DataSource.Factory<Integer, TaskContainer> getTaskFactory(SimpleSQLiteQuery query);
/**
* Saves the given task to the database.getDatabase(). Task must already exist. Returns true on
* success.

@ -0,0 +1,19 @@
package org.tasks.tasklist;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import org.tasks.data.TaskContainer;
public class ItemCallback extends DiffUtil.ItemCallback<TaskContainer> {
@Override
public boolean areItemsTheSame(@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(
@NonNull TaskContainer oldItem, @NonNull TaskContainer newItem) {
return oldItem.equals(newItem);
}
}

@ -15,14 +15,14 @@ import org.tasks.data.TaskContainer;
public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final TaskAdapter adapter;
private final TaskListRecyclerAdapter recyclerAdapter;
private final ManualSortRecyclerAdapter recyclerAdapter;
private final Runnable onClear;
private int from = -1;
private int to = -1;
private boolean dragging;
ItemTouchHelperCallback(
TaskAdapter adapter, TaskListRecyclerAdapter recyclerAdapter, Runnable onClear) {
TaskAdapter adapter, ManualSortRecyclerAdapter recyclerAdapter, Runnable onClear) {
this.adapter = adapter;
this.recyclerAdapter = recyclerAdapter;
this.onClear = onClear;

@ -0,0 +1,109 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread;
import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.DiffResult;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.tasks.data.TaskContainer;
public class ManualSortRecyclerAdapter extends TaskListRecyclerAdapter {
private final ItemTouchHelperCallback itemTouchHelperCallback;
private List<TaskContainer> list;
private PublishSubject<List<TaskContainer>> publishSubject = PublishSubject.create();
private CompositeDisposable disposables = new CompositeDisposable();
private Queue<Pair<List<TaskContainer>, DiffResult>> updates = new LinkedList<>();
public ManualSortRecyclerAdapter(
TaskAdapter adapter,
RecyclerView recyclerView, ViewHolderFactory viewHolderFactory,
TaskListFragment taskList,
ActionModeProvider actionModeProvider,
List<TaskContainer> list) {
super(adapter, viewHolderFactory, taskList, actionModeProvider);
this.list = list;
itemTouchHelperCallback = new ItemTouchHelperCallback(adapter, this, this::drainQueue);
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView);
Pair<List<TaskContainer>, DiffResult> initial = Pair.create(list, null);
disposables.add(
publishSubject
.observeOn(Schedulers.computation())
.scan(initial, this::calculateDiff)
.skip(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::applyDiff));
}
private Pair<List<TaskContainer>, DiffResult> calculateDiff(
Pair<List<TaskContainer>, DiffResult> last, List<TaskContainer> next) {
assertNotMainThread();
DiffCallback cb = new DiffCallback(last.first, next, adapter);
DiffResult result = DiffUtil.calculateDiff(cb, true);
return Pair.create(next, result);
}
private void applyDiff(Pair<List<TaskContainer>, DiffResult> update) {
assertMainThread();
updates.add(update);
if (!itemTouchHelperCallback.isDragging()) {
drainQueue();
}
}
private void drainQueue() {
assertMainThread();
Pair<List<TaskContainer>, DiffResult> update = updates.poll();
while (update != null) {
list = update.first;
update.second.dispatchUpdatesTo((ListUpdateCallback) this);
update = updates.poll();
}
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
disposables.dispose();
}
@Override
public int getItemCount() {
return list.size();
}
@Override
public TaskContainer getItem(int position) {
return list.get(position);
}
@Override
public void submitList(List<TaskContainer> list) {
publishSubject.onNext(list);
}
void moved(int from, int to, int indent) {
adapter.moved(from, to, indent);
TaskContainer task = list.remove(from);
list.add(from < to ? to - 1 : to, task);
taskList.loadTaskListContent();
}
}

@ -0,0 +1,45 @@
package org.tasks.tasklist;
import androidx.paging.AsyncPagedListDiffer;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.AsyncDifferConfig;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import java.util.List;
import org.tasks.data.TaskContainer;
public class PagedListRecyclerAdapter extends TaskListRecyclerAdapter {
private AsyncPagedListDiffer<TaskContainer> differ;
public PagedListRecyclerAdapter(
TaskAdapter adapter,
ViewHolderFactory viewHolderFactory,
TaskListFragment taskList,
ActionModeProvider actionModeProvider,
List<TaskContainer> list) {
super(adapter, viewHolderFactory, taskList, actionModeProvider);
differ =
new AsyncPagedListDiffer<>(
this, new AsyncDifferConfig.Builder<>(new ItemCallback()).build());
if (list instanceof PagedList) {
differ.submitList((PagedList<TaskContainer>) list);
}
}
@Override
public int getItemCount() {
return differ.getItemCount();
}
@Override
public TaskContainer getItem(int position) {
return differ.getItem(position);
}
@Override
public void submitList(List<TaskContainer> list) {
differ.submitList((PagedList<TaskContainer>) list);
}
}

@ -1,17 +1,11 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread;
import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ActionMode;
import androidx.core.util.Pair;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.DiffResult;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.primitives.Longs;
@ -20,92 +14,32 @@ import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.utility.Flags;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.tasks.data.TaskContainer;
import org.tasks.intents.TaskIntents;
public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
public abstract class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
implements ViewHolder.ViewHolderCallbacks, ListUpdateCallback {
private static final String EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids";
private final TaskAdapter adapter;
protected final TaskAdapter adapter;
final TaskListFragment taskList;
private final ViewHolderFactory viewHolderFactory;
private final TaskListFragment taskList;
private final ActionModeProvider actionModeProvider;
private final ItemTouchHelperCallback itemTouchHelperCallback;
private final boolean isGoogleTaskList;
private ActionMode mode = null;
private List<TaskContainer> list;
private PublishSubject<List<TaskContainer>> publishSubject = PublishSubject.create();
private Queue<Pair<List<TaskContainer>, DiffResult>> updates = new LinkedList<>();
private CompositeDisposable disposables = new CompositeDisposable();
public TaskListRecyclerAdapter(
TaskListRecyclerAdapter(
TaskAdapter adapter,
ViewHolderFactory viewHolderFactory,
TaskListFragment taskList,
ActionModeProvider actionModeProvider,
List<TaskContainer> list) {
ActionModeProvider actionModeProvider) {
this.adapter = adapter;
this.viewHolderFactory = viewHolderFactory;
this.taskList = taskList;
this.actionModeProvider = actionModeProvider;
this.list = list;
itemTouchHelperCallback =
new ItemTouchHelperCallback(adapter, this, this::drainQueue);
isGoogleTaskList = taskList.getFilter() instanceof GtasksFilter;
Pair<List<TaskContainer>, DiffResult> initial = Pair.create(list, null);
disposables.add(
publishSubject
.observeOn(Schedulers.computation())
.scan(initial, this::calculateDiff)
.skip(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::applyDiff));
}
private Pair<List<TaskContainer>, DiffResult> calculateDiff(
Pair<List<TaskContainer>, DiffResult> last, List<TaskContainer> next) {
assertNotMainThread();
DiffCallback cb = new DiffCallback(last.first, next, adapter);
DiffResult result = DiffUtil.calculateDiff(cb, true);
return Pair.create(next, result);
}
private void drainQueue() {
assertMainThread();
Pair<List<TaskContainer>, DiffResult> update = updates.poll();
Bundle selections = getSaveState();
while (update != null) {
list = update.first;
update.second.dispatchUpdatesTo((ListUpdateCallback) this);
update = updates.poll();
}
restoreSaveState(selections);
}
private void applyDiff(Pair<List<TaskContainer>, DiffResult> update) {
assertMainThread();
updates.add(update);
if (!itemTouchHelperCallback.isDragging()) {
drainQueue();
}
}
public ItemTouchHelperCallback getItemTouchHelperCallback() {
return itemTouchHelperCallback;
}
public Bundle getSaveState() {
@ -125,13 +59,14 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
}
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return viewHolderFactory.newViewHolder(parent, this);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TaskContainer task = getItem(position);
if (task != null) {
holder.bindView(task, isGoogleTaskList);
@ -143,11 +78,6 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
}
}
@Override
public int getItemCount() {
return list.size();
}
@Override
public void onCompletedTask(TaskContainer task, boolean newState) {
adapter.onCompletedTask(task, newState);
@ -173,11 +103,6 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
}
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
disposables.dispose();
}
@Override
public boolean onLongPress(ViewHolder viewHolder) {
if (!adapter.isManuallySorted()) {
@ -230,13 +155,9 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
mode = null;
}
public TaskContainer getItem(int position) {
return list.get(position);
}
public abstract TaskContainer getItem(int position);
public void submitList(List<TaskContainer> list) {
publishSubject.onNext(list);
}
public abstract void submitList(List<TaskContainer> list);
@Override
public void onInserted(int position, int count) {
@ -257,11 +178,4 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
public void onChanged(int position, int count, @Nullable Object payload) {
notifyItemRangeChanged(position, count, payload);
}
void moved(int from, int to, int indent) {
adapter.moved(from, to, indent);
TaskContainer task = list.remove(from);
list.add(from < to ? to - 1 : to, task);
taskList.loadTaskListContent();
}
}

@ -7,9 +7,13 @@ import static com.todoroo.astrid.activity.TaskListFragment.TAGS_METADATA_JOIN;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import androidx.paging.DataSource.Factory;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import androidx.sqlite.db.SimpleSQLiteQuery;
import com.google.common.collect.Lists;
import com.todoroo.andlib.data.Property.StringProperty;
@ -40,7 +44,10 @@ import org.tasks.data.TaskContainer;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
public class TaskListViewModel extends ViewModel {
public class TaskListViewModel extends ViewModel implements Observer<PagedList<TaskContainer>> {
private static final PagedList.Config PAGED_LIST_CONFIG =
new PagedList.Config.Builder().setPageSize(20).build();
private static final Field TASKS = field("tasks.*");
private static final Field GTASK = field(GTASK_METADATA_JOIN + ".*");
@ -60,6 +67,7 @@ public class TaskListViewModel extends ViewModel {
private Filter filter;
private boolean manualSort;
private CompositeDisposable disposable = new CompositeDisposable();
private LiveData<PagedList<TaskContainer>> internal;
public void setFilter(@NonNull Filter filter, boolean manualSort) {
if (!filter.equals(this.filter)
@ -134,22 +142,49 @@ public class TaskListViewModel extends ViewModel {
}
public void invalidate() {
String query = getQuery(filter);
Timber.v(query);
disposable.add(
Single.fromCallable(() -> taskDao.fetchTasks(new SimpleSQLiteQuery(query)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tasks::setValue, Timber::e));
if (internal != null) {
internal.removeObserver(this);
}
SimpleSQLiteQuery query = new SimpleSQLiteQuery(getQuery(filter));
Timber.v(query.getSql());
if (manualSort) {
disposable.add(
Single.fromCallable(() -> taskDao.fetchTasks(query))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tasks::setValue, Timber::e));
} else {
Factory<Integer, TaskContainer> factory = taskDao.getTaskFactory(query);
LivePagedListBuilder<Integer, TaskContainer> builder = new LivePagedListBuilder<>(
factory, PAGED_LIST_CONFIG);
List<TaskContainer> current = tasks.getValue();
if (current instanceof PagedList) {
Object lastKey = ((PagedList<TaskContainer>) current).getLastKey();
if (lastKey instanceof Integer) {
builder.setInitialLoadKey((Integer) lastKey);
}
}
internal = builder.build();
internal.observeForever(this);
}
}
@Override
protected void onCleared() {
disposable.dispose();
if (internal != null) {
internal.removeObserver(this);
}
}
public List<TaskContainer> getValue() {
List<TaskContainer> value = tasks.getValue();
return value != null ? value : Collections.emptyList();
}
@Override
public void onChanged(PagedList<TaskContainer> taskContainers) {
tasks.setValue(taskContainers);
}
}

Loading…
Cancel
Save