Adding granular RecyclerView notifications
Change-Id: I55d8537d58b642b87375bb8368f0b258c4c78e7c
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
index 9a8c972..d11f93a 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
@@ -55,6 +55,7 @@
}
public abstract Observable<? extends ListAccumulator<T>> buildListAccumulator();
+ public abstract Observable<? extends ListDeltaAccumulator<T>> buildListDeltaAccumulator();
public RxListAdapter<T> buildListAdapter() {
return subscribeAdapter(new RxListAdapter<>(
@@ -63,7 +64,7 @@
public RxRecyclerAdapter<T, ?> buildRecyclerAdapter() {
return subscribeAdapter(new RxRecyclerAdapter<>(
- buildListAccumulator(), getViewAdapter(), mBase.mOnError));
+ buildListDeltaAccumulator(), getViewAdapter(), mBase.mOnError));
}
public RxListAdapter<T> bindTo(final ListView listView) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulator.java
new file mode 100644
index 0000000..b19a656
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulator.java
@@ -0,0 +1,286 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.baku.toolkit.bind;
+
+import android.support.v7.widget.RecyclerView;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import java8.util.function.Function;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+import rx.Observable;
+
+/**
+ * This {@link ListDeltaAccumulator} derives its deltas from {@link ListAccumulator} snapshots.
+ *
+ * @param <T>
+ */
+@RequiredArgsConstructor
+public class DerivedListDeltaAccumulator<T> implements ListDeltaAccumulator<T> {
+ @RequiredArgsConstructor
+ private static class DiffContext<T> {
+ final NumericIdMapper ids;
+ final Function<? super ImmutableList<T>, ? extends ListAccumulator<T>> stepFactory;
+
+ final List<DerivedListDeltaAccumulator<T>> steps = new ArrayList<>();
+
+ /**
+ * Algorithm:
+ * <ol>
+ * <li>Notify of all removals.</li>
+ * <li>Notify of moves to reorder intersection.</li>
+ * <li>Notify of all insertions.</li>
+ * </ol>
+ * This algorithm does not trigger any item change events, preferring remove/insert instead.
+ * Also, it fails if any items are not unique.
+ */
+ DiffContext<T> diff(final ListAccumulator<T> a, final ListAccumulator<T> b) {
+ if (a == null) {
+ steps.add(new DerivedListDeltaAccumulator<>(ids, b));
+ } else {
+ final ImmutableList<T>
+ aList = a.getListSnapshot(),
+ bList = b.getListSnapshot();
+ final ImmutableSet<T>
+ aItems = ImmutableSet.copyOf(aList),
+ bItems = ImmutableSet.copyOf(bList);
+
+ final List<T> mutable = processRemovals(aList, bItems);
+
+ final List<T> to = new ArrayList<>(bList);
+ to.retainAll(aItems);
+ processMoves(mutable, to);
+
+ processInsertions(mutable, bList);
+ }
+ return this;
+ }
+
+ ListAccumulator<T> produceStepSnapshot(final List<T> working) {
+ return stepFactory.apply(ImmutableList.copyOf(working));
+ }
+
+ /**
+ * @param start the mutable ordered start state
+ * @param goal the elements in the end state
+ * @return the result of retaining {@code goal} from {@code start}
+ */
+ List<T> processRemovals(final ImmutableList<T> start, final ImmutableSet<T> goal) {
+ final List<T> working = new ArrayList<>(start);
+ int removalCount = 0, i = 0;
+
+ for (final T item : start) {
+ if (goal.contains(item)) {
+ processRemovals(working, i, removalCount);
+ i++;
+ removalCount = 0;
+ } else {
+ removalCount++;
+ }
+ }
+ processRemovals(working, i, removalCount);
+ return working;
+ }
+
+ /**
+ * Helper to {@link #processRemovals(ImmutableList, ImmutableSet)} that performs multi-item
+ * removal, single-item removal, or no-ops based on contiguous scan results.
+ */
+ void processRemovals(final List<T> working, final int index, final int count) {
+ if (count > 0) {
+ working.subList(index, index + count).clear();
+ steps.add(new DerivedListDeltaAccumulator<T>(ids, produceStepSnapshot(working)) {
+ @Override
+ public void notifyDeltas(RecyclerView.Adapter<?> rva) {
+ rva.notifyItemRangeRemoved(index, count);
+ }
+ });
+ }
+ }
+
+ /**
+ * Transitions from {@code working} to {@code goal} by moving one item at a time. Moves are
+ * performed in clusters of like deltas, moving smaller clusters first in the hopes that
+ * larger clusters will fall into place as a result of the smaller moves. While this usually
+ * results in optimal plans, there are cases where it can result in terrible plan, e.g.
+ * <p>
+ * 3 4 2 1 -> 3 2 4 1 -> 2 4 3 1 -> 4 2 3 1 -> 2 3 1 4 -> 1 2 3 4
+ * <p>
+ * I believe we can prevent this pitfall by rejecting moves that would break clusters:
+ * <p>
+ * 3 4 2 1 -> 1 3 4 2 -> 1 2 3 4
+ * <p>
+ * TODO(rosswang): Do this (and add a test).
+ */
+ void processMoves(final List<T> working, final List<T> goal) {
+ final HashMap<T, Integer> goalIndices = new HashMap<>(goal.size());
+ for (int i = 0; i < goal.size(); i++) {
+ goalIndices.put(goal.get(i), i);
+ }
+
+ class MoveProcessor {
+ int start, end, clusterStart, clusterDelta,
+ minClusterStart, minClusterEnd, minClusterDelta;
+
+ boolean hasMovingCluster() {
+ return minClusterDelta != 0;
+ }
+
+ void startCluster(final int index, final int delta) {
+ clusterStart = index;
+ clusterDelta = delta;
+ }
+
+ void endCluster(final int indexAfter) {
+ if (clusterDelta == 0) {
+ if (!hasMovingCluster()) {
+ start = indexAfter;
+ } else if (indexAfter == end) {
+ end = clusterStart;
+ }
+ } else if (!hasMovingCluster() ||
+ indexAfter - clusterStart < minClusterEnd - minClusterStart) {
+ // smallest cluster; keep it as a prime candidate for moving
+ minClusterStart = clusterStart;
+ minClusterEnd = indexAfter;
+ minClusterDelta = clusterDelta;
+ }
+ }
+
+ MoveProcessor() {
+ this(0, working.size());
+ }
+
+ MoveProcessor(final int start, final int end) {
+ this.start = start;
+ this.end = end;
+ for (int i = start; i < end; i++) {
+ final int delta = goalIndices.get(working.get(i)) - i;
+ if (delta != clusterDelta) {
+ endCluster(i);
+ startCluster(i, delta);
+ }
+ }
+ endCluster(end);
+ }
+
+ DerivedListDeltaAccumulator<T> step(final int from, final int to) {
+ working.add(to, working.remove(from));
+ return new DerivedListDeltaAccumulator<T>(ids, produceStepSnapshot(working)) {
+ @Override
+ public void notifyDeltas(RecyclerView.Adapter<?> rva) {
+ rva.notifyItemMoved(from, to);
+ }
+ };
+ }
+
+ void addMinClusterSteps() {
+ if (minClusterDelta < 0) {
+ for (int i = minClusterStart; i < minClusterEnd; i++) {
+ steps.add(step(i, i + minClusterDelta));
+ }
+ } else {
+ for (int i = minClusterEnd - 1; i >= minClusterStart; i--) {
+ steps.add(step(i, i + minClusterDelta));
+ }
+ }
+ }
+ }
+
+ for (MoveProcessor mp = new MoveProcessor(); mp.hasMovingCluster();
+ mp = new MoveProcessor(mp.start, mp.end)) {
+ mp.addMinClusterSteps();
+ }
+ }
+
+ /**
+ * @param working the mutable ordered (intermediate) start state
+ * @param goal the ordered end state
+ */
+ void processInsertions(final List<T> working, final ImmutableList<T> goal) {
+ final List<T> insert = new ArrayList<>();
+
+ for (int i = 0; i < goal.size(); i++) {
+ final T item = goal.get(i);
+ final int insertAt = i - insert.size();
+ if (insertAt < working.size() && item.equals(working.get(insertAt))) {
+ processInsertions(working, insertAt, insert);
+ insert.clear();
+ } else {
+ insert.add(item);
+ }
+ }
+ if (!insert.isEmpty()) {
+ // Capture these ints so that we're not holding onto instances of the working lists
+ // and opening us to errors if they're modified.
+ final int count = insert.size(), index = goal.size() - count;
+ steps.add(new DerivedListDeltaAccumulator<T>(ids, stepFactory.apply(goal)) {
+ @Override
+ public void notifyDeltas(RecyclerView.Adapter<?> rva) {
+ rva.notifyItemRangeInserted(index, count);
+ }
+ });
+ }
+ }
+
+ /**
+ * Helper to {@link #processInsertions(List, ImmutableList)} that performs multi-item
+ * insertion, single-item insertion, or no-ops based on contiguous scan results.
+ */
+ void processInsertions(final List<T> working, final int index, final List<T> insert) {
+ if (!insert.isEmpty()) {
+ working.addAll(index, insert);
+ // Capture count so that we're not holding onto an instance of insert and opening us
+ // to errors if (and when) it's modified.
+ final int count = insert.size();
+ steps.add(new DerivedListDeltaAccumulator<T>(ids, produceStepSnapshot(working)) {
+ @Override
+ public void notifyDeltas(RecyclerView.Adapter<?> rva) {
+ rva.notifyItemRangeInserted(index, count);
+ }
+ });
+ }
+ }
+ }
+
+ private final NumericIdMapper mIds;
+ @Delegate
+ private final ListAccumulator<T> mSnapshot;
+
+ public static <T> Observable<DerivedListDeltaAccumulator<T>> scanFrom(
+ final Observable<? extends ListAccumulator<T>> snapshots,
+ final Function<? super ImmutableList<T>, ? extends ListAccumulator<T>> stepFactory) {
+ final NumericIdMapper ids = new NumericIdMapper();
+ return Observable.concat(Observable.zip(
+ // need to wrap null as an Observable (or Iterable) to capture generic wildcard
+ // without overload ambiguity
+ snapshots.startWith(Observable.just(null)),
+ snapshots,
+ (previous, next) -> Observable.from(new DiffContext<>(ids, stepFactory)
+ .diff(previous, next).steps)));
+ }
+
+ /**
+ * The default implementation simply calls {@link RecyclerView.Adapter#notifyDataSetChanged()}.
+ * However, it is fully expected that subclasses, such as the anonymous subclasses generated by
+ * {@link DiffContext#diff(ListAccumulator, ListAccumulator)}, override this for more specific
+ * notifications.
+ */
+ @Override
+ public void notifyDeltas(final RecyclerView.Adapter<?> rva) {
+ rva.notifyDataSetChanged();
+ }
+
+ @Override
+ public long getItemId(final int position) {
+ return mIds.assignNumericId(getRowAt(position));
+ }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListAccumulator.java
index 9ff9b85..b1ccbec 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListAccumulator.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListAccumulator.java
@@ -10,6 +10,10 @@
import lombok.RequiredArgsConstructor;
import rx.Observable;
+/**
+ * This accumulator is not a true accumulator, but rather a first-order transformation.
+ * TODO(rosswang): Rename these.
+ */
@RequiredArgsConstructor
public class IdListAccumulator implements ListAccumulator<String> {
private final ImmutableList<String> mIds;
@@ -20,7 +24,7 @@
public Observable<IdListAccumulator> scanFrom(
final Observable<SingleWatchEvent<ImmutableList<String>>> watch) {
- return watch.scan(this, (t, w) -> new IdListAccumulator(w.getValue()));
+ return watch.map(w -> new IdListAccumulator(w.getValue()));
}
@Override
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListBindingBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListBindingBuilder.java
index 988de7a..250069e 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListBindingBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListBindingBuilder.java
@@ -11,6 +11,7 @@
import io.v.rx.syncbase.SingleWatchEvent;
import rx.Observable;
+import rx.schedulers.Schedulers;
public class IdListBindingBuilder<A extends RangeAdapter>
extends CollectionAdapterBuilder<IdListBindingBuilder<A>, String, A> {
@@ -29,6 +30,9 @@
return this;
}
+ /**
+ * This assumes that no IDs are null.
+ */
public Observable<SingleWatchEvent<ImmutableList<String>>> buildIdListWatch() {
return mBase.mRxTable.watch(mIdListRowName, new TypeToken<List<String>>() {
}, ImmutableList.of()).map(w -> w.map(ImmutableList::copyOf));
@@ -39,4 +43,10 @@
return new IdListAccumulator()
.scanFrom(buildIdListWatch());
}
+
+ @Override
+ public Observable<? extends ListDeltaAccumulator<String>> buildListDeltaAccumulator() {
+ return DerivedListDeltaAccumulator.scanFrom(buildListAccumulator()
+ .observeOn(Schedulers.computation()), IdListAccumulator::new);
+ }
}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulator.java
index 2325e1b..24bd5d7 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulator.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulator.java
@@ -6,6 +6,17 @@
import com.google.common.collect.ImmutableList;
+import java8.util.function.Function;
+import rx.Observable;
+
+/**
+ * This interface tracks list updates for use with {@link android.widget.ListView}s via
+ * {@link RxListAdapter} and {@link android.support.v7.widget.RecyclerView}s via
+ * {@link RxRecyclerAdapter}. To support granular update notifications for {@code RecyclerView},
+ * the {@link ListDeltaAccumulator} subinterface is required. A {@code ListAccumulator} can be
+ * converted to a {@code ListDeltaAccumulator} by wrapping it with
+ * {@link DerivedListDeltaAccumulator#scanFrom(Observable, Function)}.
+ */
public interface ListAccumulator<T> {
boolean containsRow(String rowName);
int getCount();
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulators.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulators.java
index babf78f..95f7b03 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulators.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulators.java
@@ -13,8 +13,6 @@
@UtilityClass
public class ListAccumulators {
-
-
public static final ListAccumulator<Object> EMPTY = new ListAccumulator<Object>(){
@Override
public int getCount() {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListDeltaAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListDeltaAccumulator.java
new file mode 100644
index 0000000..2e55711
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListDeltaAccumulator.java
@@ -0,0 +1,16 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.baku.toolkit.bind;
+
+import android.support.v7.widget.RecyclerView;
+
+/**
+ * This interface adds delta notifications to {@link ListAccumulator} to allow granular updates to
+ * {@link RecyclerView}s via {@link RxRecyclerAdapter}.
+ */
+public interface ListDeltaAccumulator<T> extends ListAccumulator<T> {
+ void notifyDeltas(RecyclerView.Adapter<?> rva);
+ long getItemId(int position);
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/NumericIdMapper.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/NumericIdMapper.java
new file mode 100644
index 0000000..6c062fb
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/NumericIdMapper.java
@@ -0,0 +1,22 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.baku.toolkit.bind;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import java8.util.Maps;
+
+public class NumericIdMapper {
+ private final Map<Object, Integer> mMapping = new HashMap<>();
+
+ public int getNumericId(final Object key) {
+ return mMapping.get(key);
+ }
+
+ public int assignNumericId(final Object key) {
+ return Maps.computeIfAbsent(mMapping, key, k -> mMapping.size());
+ }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
index 57e8588..b1085f3 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
@@ -98,8 +98,14 @@
}
@Override
- public Observable<PrefixListAccumulator<T>> buildListAccumulator() {
+ public Observable<? extends PrefixListAccumulator<T>> buildListAccumulator() {
return new PrefixListAccumulator<>(getOrdering())
.scanFrom(buildPrefixWatch());
}
+
+ @Override
+ public Observable<? extends PrefixListDeltaAccumulator<T>> buildListDeltaAccumulator() {
+ return new PrefixListDeltaAccumulator<>(getOrdering())
+ .scanFrom(buildPrefixWatch());
+ }
}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
index c03eece..9bd3c57 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
@@ -42,7 +42,7 @@
mOrdering = ordering.compound(Ordering.natural().onResultOf(RxTable.Row::getRowName));
}
- public Observable<PrefixListAccumulator<T>> scanFrom(
+ public Observable<? extends PrefixListAccumulator<T>> scanFrom(
final Observable<RangeWatchBatch<T>> watch) {
return watch
.concatMap(RangeWatchBatch::collectChanges)
@@ -60,9 +60,10 @@
}
}
- private PrefixListAccumulator<T> withUpdates(final Collection<RangeWatchEvent<T>> events) {
+ protected PrefixListAccumulator<T> withUpdates(final Collection<RangeWatchEvent<T>> events) {
// TODO(rosswang): more efficient updates for larger batches
// TODO(rosswang): allow option to copy on add (immutable accumulator)
+ // If we copy on add, don't forget to override the clone in PrefixListDeltaAccumulator.
for (final RangeWatchEvent<T> e : events) {
if (e.getChangeType() == ChangeType.DELETE_CHANGE) {
removeOne(e.getRow());
@@ -73,10 +74,10 @@
return this;
}
- protected void removeOne(final RxTable.Row<T> entry) {
+ private void removeOne(final RxTable.Row<T> entry) {
final T old = mRows.remove(entry.getRowName());
if (old != null) {
- mSorted.remove(findRowForEdit(entry.getRowName(), old));
+ remove(findRowForEdit(entry.getRowName(), old));
}
}
@@ -85,28 +86,38 @@
return bs < 0 ? ~bs : bs;
}
- protected void updateOne(final RxTable.Row<T> entry) {
+ private void updateOne(final RxTable.Row<T> entry) {
final T old = mRows.put(entry.getRowName(), entry.getValue());
if (old == null) {
- mSorted.add(insertionIndex(entry), entry);
+ insert(insertionIndex(entry), entry);
} else {
final int oldIndex = findRowForEdit(entry.getRowName(), old);
- int newIndex = insertionIndex(entry);
-
- if (newIndex >= oldIndex) {
- newIndex--;
- for (int i = oldIndex; i < newIndex; i++) {
- mSorted.set(i, mSorted.get(i + 1));
- }
+ final int newIndex = insertionIndex(entry);
+ if (oldIndex == newIndex) {
+ change(newIndex, entry);
} else {
- for (int i = oldIndex; i > newIndex; i--) {
- mSorted.set(i, mSorted.get(i - 1));
- }
+ move(oldIndex, newIndex, entry);
}
- mSorted.set(newIndex, entry);
}
}
+ protected void remove(final int index) {
+ mSorted.remove(index);
+ }
+
+ protected void insert(final int index, final RxTable.Row<T> entry) {
+ mSorted.add(index, entry);
+ }
+
+ protected void move(final int from, final int to, final RxTable.Row<T> entry) {
+ mSorted.remove(from);
+ mSorted.add(to, entry);
+ }
+
+ protected void change(final int index, final RxTable.Row<T> entry) {
+ mSorted.set(index, entry);
+ }
+
@Override
public int getCount() {
return mRows.size();
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java
new file mode 100644
index 0000000..d804799
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListDeltaAccumulator.java
@@ -0,0 +1,75 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.baku.toolkit.bind;
+
+import android.support.v7.widget.RecyclerView;
+
+import com.google.common.collect.Ordering;
+
+import io.v.rx.syncbase.RangeWatchBatch;
+import io.v.rx.syncbase.RxTable;
+import java8.util.function.Consumer;
+import rx.Observable;
+
+/**
+ * This variant of {@link PrefixListAccumulator} notifies
+ * {@link android.support.v7.widget.RecyclerView.Adapter}s of granular data changes. For now, this
+ * implementation does not treat batches any differently, and derives reordering directly from
+ * one-by-one sorts. TODO(rosswang): Do we need to optimize this?
+ */
+public class PrefixListDeltaAccumulator<T> extends PrefixListAccumulator<T>
+ implements ListDeltaAccumulator<RxTable.Row<T>> {
+ private Consumer<RecyclerView.Adapter> mDeltas;
+ private final NumericIdMapper mIds = new NumericIdMapper();
+
+ public PrefixListDeltaAccumulator(final Ordering<? super RxTable.Row<T>> ordering) {
+ super(ordering);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Observable<? extends PrefixListDeltaAccumulator<T>> scanFrom(
+ final Observable<RangeWatchBatch<T>> watch) {
+ return (Observable<? extends PrefixListDeltaAccumulator<T>>)super.scanFrom(watch);
+ }
+
+ @Override
+ protected void remove(final int index) {
+ super.remove(index);
+ mDeltas = a -> a.notifyItemRemoved(index);
+ }
+
+ @Override
+ protected void insert(final int index, final RxTable.Row<T> entry) {
+ super.insert(index, entry);
+ mDeltas = a -> a.notifyItemInserted(index);
+ mIds.assignNumericId(entry.getRowName());
+ }
+
+ @Override
+ protected void move(final int from, final int to, final RxTable.Row<T> entry) {
+ super.move(from, to, entry);
+ mDeltas = a -> a.notifyItemMoved(from, to);
+ }
+
+ @Override
+ protected void change(final int index, final RxTable.Row<T> entry) {
+ super.change(index, entry);
+ // TODO(rosswang): Can we do anything with passing the optional payload here?
+ mDeltas = a -> a.notifyItemChanged(index);
+ }
+
+ @Override
+ public void notifyDeltas(final RecyclerView.Adapter<?> rva) {
+ if (mDeltas != null) {
+ mDeltas.accept(rva);
+ }
+ }
+
+ @Override
+ public long getItemId(final int position) {
+ return mIds.getNumericId(getRowAt(position).getRowName());
+ }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/RxRecyclerAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/RxRecyclerAdapter.java
index 34ee173..ea574b8 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/RxRecyclerAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/RxRecyclerAdapter.java
@@ -29,21 +29,39 @@
}
private final ViewAdapter<? super T, VH> mViewAdapter;
- @Delegate
- private ListAccumulator<T> mLatestState = ListAccumulators.empty();
+
+ /**
+ * The main purpose of this class is to capture the generic arg {@link T}. Otherwise, the
+ * {@link Delegate} annotation fails to capture it and cannot implement {@link ListAccumulator}
+ * with the right generic type.
+ * <p>
+ * While we're here, we might as well use it to override
+ * {@link android.support.v7.widget.RecyclerView.Adapter#getItemId(int)}.
+ */
+ private abstract class Delegated implements ListAccumulator<T> {
+ /**
+ * Overrides {@link android.support.v7.widget.RecyclerView.Adapter#getItemId(int)}.
+ */
+ public abstract long getItemId(int position);
+ }
+
+ @Delegate(types = Delegated.class)
+ private ListDeltaAccumulator<T> mLatestState =
+ new DerivedListDeltaAccumulator<>(null, ListAccumulators.empty());
+
@Getter
private final Subscription mSubscription;
- public RxRecyclerAdapter(final Observable<? extends ListAccumulator<T>> data,
+ public RxRecyclerAdapter(final Observable<? extends ListDeltaAccumulator<T>> data,
final ViewAdapter<? super T, VH> viewAdapter,
final Action1<Throwable> onError) {
+ setHasStableIds(true);
mViewAdapter = viewAdapter;
mSubscription = data
.observeOn(AndroidSchedulers.mainThread())
.subscribe(d -> {
mLatestState = d;
- notifyDataSetChanged();
- // TODO(rosswang): Use higher-fidelity update notifications.
+ d.notifyDeltas(this);
}, onError);
}
@@ -68,12 +86,4 @@
public int getItemCount() {
return getCount();
}
-
- /**
- * TODO(rosswang): If this can improve UX, allot numeric IDs to row keys.
- */
- @Override
- public long getItemId(int i) {
- return RecyclerView.NO_ID;
- }
}
diff --git a/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulatorTest.java b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulatorTest.java
new file mode 100644
index 0000000..c3aea71
--- /dev/null
+++ b/baku-toolkit/lib/src/test/java/io/v/baku/toolkit/bind/DerivedListDeltaAccumulatorTest.java
@@ -0,0 +1,71 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.baku.toolkit.bind;
+
+import android.support.v7.widget.RecyclerView;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.List;
+
+import java8.util.function.Consumer;
+import rx.Observable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.mock;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(RecyclerView.Adapter.class)
+public class DerivedListDeltaAccumulatorTest {
+ @SafeVarargs
+ private static List<DerivedListDeltaAccumulator<String>> deriveDeltas(
+ final ImmutableList<String>... idLists) {
+ return ImmutableList.copyOf(DerivedListDeltaAccumulator.scanFrom(
+ Observable.from(idLists)
+ .map(IdListAccumulator::new),
+ IdListAccumulator::new)
+ .toBlocking().toIterable());
+ }
+
+ private static void verifyDelta(final DerivedListDeltaAccumulator<String> delta,
+ final ImmutableList<String> expectedStep,
+ final Consumer<RecyclerView.Adapter<?>> expectedNotification) {
+ assertEquals(expectedStep, delta.getListSnapshot());
+ final RecyclerView.Adapter<?> rva = mock(RecyclerView.Adapter.class);
+ delta.notifyDeltas(rva);
+ expectedNotification.accept(verify(rva));
+ }
+
+ @Test
+ public void test() {
+ final List<DerivedListDeltaAccumulator<String>> deltas = deriveDeltas(
+ ImmutableList.of("a", "b", "c", "d", "e"),
+ ImmutableList.of("a", "c"),
+ ImmutableList.of("b", "l", "a", "c", "k"),
+ ImmutableList.of("k", "a", "c", "b", "l"));
+ verifyDelta(deltas.get(0), ImmutableList.of("a", "b", "c", "d", "e"),
+ RecyclerView.Adapter::notifyDataSetChanged);
+ verifyDelta(deltas.get(1), ImmutableList.of("a", "c", "d", "e"),
+ rva -> rva.notifyItemRangeRemoved(1, 1));
+ verifyDelta(deltas.get(2), ImmutableList.of("a", "c"),
+ rva -> rva.notifyItemRangeRemoved(2, 2));
+ verifyDelta(deltas.get(3), ImmutableList.of("b", "l", "a", "c"),
+ rva -> rva.notifyItemRangeInserted(0, 2));
+ verifyDelta(deltas.get(4), ImmutableList.of("b", "l", "a", "c", "k"),
+ rva -> rva.notifyItemRangeInserted(4, 1));
+ verifyDelta(deltas.get(5), ImmutableList.of("k", "b", "l", "a", "c"),
+ rva -> rva.notifyItemMoved(4, 0));
+ verifyDelta(deltas.get(6), ImmutableList.of("k", "b", "a", "c", "l"),
+ rva -> rva.notifyItemMoved(2, 4));
+ verifyDelta(deltas.get(7), ImmutableList.of("k", "a", "c", "b", "l"),
+ rva -> rva.notifyItemMoved(1, 3));
+ }
+}