Merge changes from topic 'baku'

* changes:
  Making APIs consistent
  Baku - Factoring out sync error handlers
  Baku - improving docs and easing composition
  Baku Toolkit - Adding ID-list bindings
  Baku - Splitting collection adapter components
diff --git a/baku-toolkit/gradle/wrapper/gradle-wrapper.properties b/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
index de00d04..89af0bd 100644
--- a/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
+++ b/baku-toolkit/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip
diff --git a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapterTest.java b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
similarity index 69%
rename from baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapterTest.java
rename to baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
index 9b916bd..2b17d53 100644
--- a/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapterTest.java
+++ b/baku-toolkit/lib/src/androidTest/java/io/v/baku/toolkit/bind/CollectionBindingTest.java
@@ -12,7 +12,7 @@
 import io.v.rx.syncbase.RxSyncbase;
 import io.v.rx.syncbase.RxTable;
 
-public class SyncbaseRangeAdapterTest extends VAndroidTestCase {
+public class CollectionBindingTest extends VAndroidTestCase {
     private RxSyncbase mRxSyncbase;
     private RxTable mTable;
 
@@ -37,34 +37,33 @@
                 mTable.put("Good morning", "starshine")));
 
         final ListView listView = new ListView(getContext());
-        try (final SyncbaseListAdapter<String> adapter = SyncbaseRangeAdapter.builder()
+        try (final SyncbaseListAdapter<RxTable.Row<String>> adapter = CollectionBinding.builder()
                 .onError(t -> fail(Throwables.getStackTraceAsString(t)))
                 .viewAdapterContext(getContext())
                 .rxTable(mTable)
-                .prefix("Good")
+                .onPrefix("Good")
                 .type(String.class)
-                .bindTo(listView)
-                .getAdapter()) {
+                .bindTo(listView)) {
 
             pause();
 
             assertEquals(2, listView.getCount());
-            assertEquals("Goodnight", adapter.getRowAt(0).getRowName());
-            assertEquals("moon", adapter.getItem(0));
-            assertEquals("Good morning", adapter.getRowAt(1).getRowName());
-            assertEquals("starshine", adapter.getItem(1));
+            assertEquals("Goodnight", adapter.getItem(0).getRowName());
+            assertEquals("moon", adapter.getItem(0).getValue());
+            assertEquals("Good morning", adapter.getItem(1).getRowName());
+            assertEquals("starshine", adapter.getItem(1).getValue());
 
             start(mTable.put("Goodbye", "Mr. Bond"));
             pause();
 
-            assertEquals("Goodbye", adapter.getRowAt(0).getRowName());
-            assertEquals("Mr. Bond", adapter.getItem(0));
+            assertEquals("Goodbye", adapter.getItem(0).getRowName());
+            assertEquals("Mr. Bond", adapter.getItem(0).getValue());
 
             start(mTable.delete("Good morning"));
             pause();
 
             assertEquals(1, adapter.getRowIndex("Goodnight"));
-            assertEquals("moon", adapter.getItem(1));
+            assertEquals("moon", adapter.getItem(1).getValue());
         }
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
index e506464..3e424ea 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivity.java
@@ -12,16 +12,16 @@
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * A default application of {@link BakuActivityTrait} extending {@link android.app.Activity}. Most
+ * A default integration with {@link BakuActivityTrait} extending {@link android.app.Activity}. Most
  * activities with distributed state should inherit from this.
  */
 @Slf4j
 public abstract class BakuActivity extends VActivity implements BakuActivityTrait<Activity> {
     @Delegate
-    private BakuActivityTrait mBakuActivityTrait;
+    private BakuActivityTrait<Activity> mBakuActivityTrait;
 
-    protected BakuActivityTrait createBakuActivityTrait() {
-        return new BakuActivityMixin(this);
+    protected BakuActivityTrait<Activity> createBakuActivityTrait() {
+        return new BakuActivityMixin<>(this);
     }
 
     @Override
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
index 8172987..79975d6 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
@@ -5,10 +5,10 @@
 package io.v.baku.toolkit;
 
 import android.app.Activity;
+import android.os.Bundle;
 
-import io.v.baku.toolkit.bind.RangeAdapter;
 import io.v.baku.toolkit.bind.SyncbaseBinding;
-import io.v.baku.toolkit.bind.SyncbaseRangeAdapter;
+import io.v.baku.toolkit.bind.CollectionBinding;
 import io.v.baku.toolkit.syncbase.BakuDb;
 import io.v.baku.toolkit.syncbase.BakuSyncbase;
 import io.v.baku.toolkit.syncbase.BakuTable;
@@ -27,6 +27,7 @@
  * <li>{@link BakuActivity} (extends {@link Activity})</li>
  * <li>{@link BakuAppCompatActivity} (extends {@link android.support.v7.app.AppCompatActivity})</li>
  * </ul>
+ * <p>
  * Since Java doesn't actually support multiple inheritance, clients requiring custom inheritance
  * hierarchies will need to wire in manually, like any of the examples above.
  */
@@ -61,6 +62,33 @@
         joinInitialSyncGroup();
     }
 
+    /**
+     * Convenience constructor for compositional integration. Example usage:
+     *
+     * <pre><code>
+     * public class SampleCompositionActivity extends Activity {
+     *     private BakuActivityTrait<SampleCompositionActivity> mBaku;
+     *
+     *     &#64;Override
+     *     protected void onCreate(Bundle savedInstanceState) {
+     *         super.onCreate(savedInstanceState);
+     *         setContentView(R.layout.activity_hello);
+     *
+     *         mBaku = new BakuActivityMixin<>(this, savedInstanceState);
+     *     }
+     *
+     *     &#64;Override
+     *     protected void onDestroy() {
+     *         mBaku.close();
+     *         super.onDestroy();
+     *     }
+     * }
+     * </code></pre>
+     */
+    public BakuActivityMixin(final T context, final Bundle savedInstanceState) {
+        this(VAndroidContextMixin.withDefaults(context, savedInstanceState));
+    }
+
     @Override
     public void close() {
         mSubscriptions.unsubscribe();
@@ -84,16 +112,16 @@
     }
 
     public void onSyncError(final Throwable t) {
-        mVAndroidContextTrait.getErrorReporter().onError(R.string.err_sync, t);
+        ErrorReporters.getDefaultSyncErrorReporter(mVAndroidContextTrait);
     }
 
     public <U> SyncbaseBinding.Builder<U> binder() {
         return SyncbaseBinding.<U>builder()
-                .bakuActivity(this);
+                .activity(this);
     }
 
-    public <U> SyncbaseRangeAdapter.Builder<U, ?> collectionBinder() {
-        return SyncbaseRangeAdapter.<U, RangeAdapter>builder()
-                .bakuActivity(this);
+    public CollectionBinding.Builder collectionBinder() {
+        return CollectionBinding.builder()
+                .activity(this);
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
index 6ba0d0f..9fbd4ec 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
@@ -7,7 +7,7 @@
 import android.app.Activity;
 
 import io.v.baku.toolkit.bind.SyncbaseBinding;
-import io.v.baku.toolkit.bind.SyncbaseRangeAdapter;
+import io.v.baku.toolkit.bind.CollectionBinding;
 import io.v.baku.toolkit.syncbase.BakuDb;
 import io.v.baku.toolkit.syncbase.BakuSyncbase;
 import io.v.baku.toolkit.syncbase.BakuTable;
@@ -22,6 +22,6 @@
     String getSyncbaseTableName();
     void onSyncError(Throwable t);
     <U> SyncbaseBinding.Builder<U> binder();
-    <U> SyncbaseRangeAdapter.Builder<U, ?> collectionBinder();
+    CollectionBinding.Builder collectionBinder();
     void close();
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
index e2c5ae0..14220dd 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
@@ -12,7 +12,7 @@
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * A default application of {@link BakuActivityTrait} extending
+ * A default integration with {@link BakuActivityTrait} extending
  * {@link android.support.v7.app.AppCompatActivity}.
  */
 @Slf4j
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/ErrorReporters.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/ErrorReporters.java
index b9df48b..2ade39d 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/ErrorReporters.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/ErrorReporters.java
@@ -8,6 +8,7 @@
 
 import lombok.experimental.UtilityClass;
 import lombok.extern.slf4j.Slf4j;
+import rx.functions.Action1;
 
 @Slf4j
 @UtilityClass
@@ -23,4 +24,22 @@
             }
         };
     }
+
+    /**
+     * Derives a default sync error reporting function from a {@link VAndroidContextTrait}. The
+     * error message is {@link io.v.baku.toolkit.R.string#err_sync}.
+     *
+     * @see #getDefaultSyncErrorReporter(ErrorReporter)
+     */
+    public static Action1<Throwable> getDefaultSyncErrorReporter(final VAndroidContextTrait<?> v) {
+        return getDefaultSyncErrorReporter(v.getErrorReporter());
+    }
+
+    /**
+     * Derives a default sync error reporting function from an {@link ErrorReporter}. The error
+     * message is {@link io.v.baku.toolkit.R.string#err_sync}.
+     */
+    public static Action1<Throwable> getDefaultSyncErrorReporter(final ErrorReporter r) {
+        return t -> r.onError(R.string.err_sync, t);
+    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
index 97218a6..8ebd442 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VActivity.java
@@ -11,7 +11,7 @@
 import lombok.experimental.Delegate;
 
 /**
- * A default application of {@link VAndroidContextTrait} extending {@link Activity}.
+ * A default integration with {@link VAndroidContextTrait} extending {@link Activity}.
  */
 public abstract class VActivity extends Activity implements VAndroidContextTrait<Activity> {
     @Delegate
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
index 08dad20..c3ca36c 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
@@ -12,7 +12,7 @@
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * A default application of {@link VAndroidContextTrait} extending
+ * A default integration with {@link VAndroidContextTrait} extending
  * {@link android.support.v7.app.AppCompatActivity}.
  */
 @Slf4j
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/AbstractViewAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/AbstractViewAdapter.java
index 6fdece9..3a51515 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/AbstractViewAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/AbstractViewAdapter.java
@@ -10,7 +10,8 @@
 import java8.util.function.Function;
 
 public abstract class AbstractViewAdapter<T, VH extends ViewHolder> implements ViewAdapter<T, VH> {
-    public <U> AbstractViewAdapter<U, VH> map(final Function<U, T> fn) {
+    @Override
+    public <U> ViewAdapter<U, VH> map(final Function<U, ? extends T> fn) {
         return new TransformingViewAdapter<>(this, fn);
     }
 
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseBuilder.java
index 36e0be8..64ba08a 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseBuilder.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseBuilder.java
@@ -6,10 +6,10 @@
 
 
 import android.app.Activity;
-import android.support.annotation.IdRes;
-import android.view.View;
 
 import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.baku.toolkit.ErrorReporters;
+import io.v.baku.toolkit.VAndroidContextTrait;
 import io.v.rx.syncbase.RxTable;
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
@@ -19,15 +19,14 @@
 
 @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
 public abstract class BaseBuilder<T extends BaseBuilder<T>> {
+    @SuppressWarnings("unchecked")
+    protected final T mSelf = (T)this;
+
     protected Activity mActivity;
     protected RxTable mRxTable;
     protected CompositeSubscription mSubscriptionParent;
     protected Action1<Throwable> mOnError;
 
-    @SuppressWarnings("unchecked")
-    protected final T mSelf = (T)this;
-
-
     public T activity(final Activity activity) {
         mActivity = activity;
         return mSelf;
@@ -38,13 +37,34 @@
         return mSelf;
     }
 
-    public T bakuActivity(final BakuActivityTrait<?> trait) {
+    /**
+     * Sets the following properties from the given {@link BakuActivityTrait}:
+     * <ul>
+     *     <li>{@link #activity(Activity)}</li>
+     *     <li>{@link #rxTable(RxTable)}</li>
+     *     <li>{@link #subscriptionParent(CompositeSubscription)}</li>
+     *     <li>{@link #onError(Action1)}</li>
+     * </ul>
+     */
+    public T activity(final BakuActivityTrait<?> trait) {
         return activity(trait.getVAndroidContextTrait().getAndroidContext())
                 .rxTable(trait.getSyncbaseTable())
                 .subscriptionParent(trait.getSubscriptions())
                 .onError(trait::onSyncError);
     }
 
+    /**
+     * Sets the following properties from the given {@link VAndroidContextTrait}:
+     * <ul>
+     *     <li>{@link #activity(Activity)}</li>
+     *     <li>{@link #onError(Action1)}</li>
+     * </ul>
+     */
+    public T activity(final VAndroidContextTrait<? extends Activity> trait) {
+        return activity(trait.getAndroidContext())
+                .onError(ErrorReporters.getDefaultSyncErrorReporter(trait));
+    }
+
     public T subscriptionParent(final CompositeSubscription subscriptionParent) {
         mSubscriptionParent = subscriptionParent;
         return mSelf;
@@ -66,10 +86,4 @@
         mOnError = onError;
         return mSelf;
     }
-
-    public abstract T bindTo(final View view);
-
-    public T bindTo(final @IdRes int viewId) {
-        return bindTo(mActivity.findViewById(viewId));
-    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseCollectionBindingBuilder.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseCollectionBindingBuilder.java
new file mode 100644
index 0000000..4433363
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/BaseCollectionBindingBuilder.java
@@ -0,0 +1,29 @@
+// 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.content.Context;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Encapsulates the common logic for building the Syncbase side of a collection binding.
+ *
+ * @see CollectionAdapterBuilder
+ */
+@RequiredArgsConstructor
+public abstract class BaseCollectionBindingBuilder<B extends BaseCollectionBindingBuilder<B>>
+        extends BaseBuilder<B> {
+    private Context mViewAdapterContext;
+
+    public B viewAdapterContext(final Context context) {
+        mViewAdapterContext = context;
+        return mSelf;
+    }
+
+    public Context getDefaultViewAdapterContext() {
+        return mViewAdapterContext == null ? mActivity : mViewAdapterContext;
+    }
+}
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
new file mode 100644
index 0000000..bbaf946
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionAdapterBuilder.java
@@ -0,0 +1,90 @@
+// 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 android.view.View;
+import android.widget.ListView;
+
+import java8.util.function.Function;
+import lombok.RequiredArgsConstructor;
+import rx.Observable;
+
+/**
+ * Encapsulates the common logic for building the widget side of a collection binding.
+ *
+ * @see BaseCollectionBindingBuilder
+ */
+@RequiredArgsConstructor
+public abstract class CollectionAdapterBuilder<B extends CollectionAdapterBuilder<B, T, A>,
+        T, A extends RangeAdapter> {
+    @SuppressWarnings("unchecked")
+    protected final B mSelf = (B)this;
+
+    protected final CollectionBinding.Builder mBase;
+
+    private ViewAdapter<? super T, ?> mViewAdapter;
+
+    public B viewAdapter(
+            final ViewAdapter<? super T, ?> viewAdapter) {
+        mViewAdapter = viewAdapter;
+        return mSelf;
+    }
+
+    public B textViewAdapter() {
+        return viewAdapter(new TextViewAdapter(mBase.getDefaultViewAdapterContext()));
+    }
+
+    public B textViewAdapter(final Function<T, ?> fn) {
+        return viewAdapter(new TextViewAdapter(mBase.getDefaultViewAdapterContext()).map(fn));
+    }
+
+    protected ViewAdapter<? super T, ?> getDefaultViewAdapter() {
+        return new TextViewAdapter(mBase.getDefaultViewAdapterContext());
+    }
+
+    private ViewAdapter<? super T, ?> getViewAdapter() {
+        return mViewAdapter == null ? getDefaultViewAdapter() : mViewAdapter;
+    }
+
+    private <U extends RangeAdapter> U subscribeAdapter(final U adapter) {
+        mBase.subscribe(adapter.getSubscription());
+        return adapter;
+    }
+
+    public abstract Observable<? extends ListAccumulator<T>> buildListAccumulator();
+
+    public SyncbaseListAdapter<T> buildListAdapter() {
+        return subscribeAdapter(new SyncbaseListAdapter<>(
+                buildListAccumulator(), getViewAdapter(), mBase.mOnError));
+    }
+
+    public SyncbaseRecyclerAdapter<T, ?> buildRecyclerAdapter() {
+        return subscribeAdapter(new SyncbaseRecyclerAdapter<>(
+                buildListAccumulator(), getViewAdapter(), mBase.mOnError));
+    }
+
+    public SyncbaseListAdapter<T> bindTo(final ListView listView) {
+        final SyncbaseListAdapter<T> adapter = buildListAdapter();
+        listView.setAdapter(adapter);
+        return adapter;
+    }
+
+    public SyncbaseRecyclerAdapter<T, ?> bindTo(final RecyclerView recyclerView) {
+        final SyncbaseRecyclerAdapter<T, ?> adapter = buildRecyclerAdapter();
+        recyclerView.setAdapter(adapter);
+        return adapter;
+    }
+
+    public RangeAdapter bindTo(final View view) {
+        if (view instanceof ListView) {
+            return bindTo((ListView) view);
+        } else if (view instanceof RecyclerView) {
+            return bindTo((RecyclerView) view);
+        } else {
+            throw new IllegalArgumentException("No default binding for view " + view);
+        }
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionBinding.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionBinding.java
new file mode 100644
index 0000000..ac8abeb
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/CollectionBinding.java
@@ -0,0 +1,28 @@
+// 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 io.v.v23.syncbase.nosql.PrefixRange;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class CollectionBinding {
+    public static class Builder extends BaseCollectionBindingBuilder<Builder> {
+        public <T, A extends RangeAdapter> PrefixBindingBuilder<T, A> onPrefix(final String prefix) {
+            return new PrefixBindingBuilder<T, A>(this).prefix(prefix);
+        }
+        public <T, A extends RangeAdapter> PrefixBindingBuilder<T, A> onPrefix(final PrefixRange prefix) {
+            return new PrefixBindingBuilder<T, A>(this).prefix(prefix);
+        }
+
+        public <A extends RangeAdapter> IdListBindingBuilder<A> onIdList(final String idListRowName) {
+            return new IdListBindingBuilder<A>(this).idListRowName(idListRowName);
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+}
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
new file mode 100644
index 0000000..b75997e
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListAccumulator.java
@@ -0,0 +1,51 @@
+// 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 com.google.common.collect.ImmutableList;
+
+import io.v.rx.syncbase.SingleWatchEvent;
+import lombok.RequiredArgsConstructor;
+import rx.Observable;
+
+@RequiredArgsConstructor
+public class IdListAccumulator implements ListAccumulator<String> {
+    private final ImmutableList<String> mIds;
+
+    public IdListAccumulator() {
+        this(ImmutableList.of());
+    }
+
+    public Observable<IdListAccumulator> scanFrom(
+            final Observable<SingleWatchEvent<ImmutableList<String>>> watch) {
+        return watch.scan(this, (t, w) -> new IdListAccumulator(w.getValue()));
+    }
+
+    @Override
+    public int getCount() {
+        return mIds.size();
+    }
+
+    @Override
+    public String getRowAt(final int position) {
+        return mIds.get(position);
+    }
+
+    @Override
+    public boolean containsRow(String rowName) {
+        // TODO(rosswang): possibly index
+        // Since contains and indexOf are O(n) on a list, these don't scale too well. If we ever
+        // have to deal with large lists and performance becomes an issue, it might be indicated to
+        // index these into a map. On the other hand, that's premature right now and would probably
+        // end up being even slower for most cases, so I'm punting on that until there's evidence.
+        return mIds.contains(rowName);
+    }
+
+    @Override
+    public int getRowIndex(final String rowName) {
+        // TODO(rosswang): possibly index
+        return mIds.indexOf(rowName);
+    }
+}
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
new file mode 100644
index 0000000..988de7a
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/IdListBindingBuilder.java
@@ -0,0 +1,42 @@
+// 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 com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
+
+import java.util.List;
+
+import io.v.rx.syncbase.SingleWatchEvent;
+import rx.Observable;
+
+public class IdListBindingBuilder<A extends RangeAdapter>
+        extends CollectionAdapterBuilder<IdListBindingBuilder<A>, String, A> {
+    private String mIdListRowName;
+
+    public IdListBindingBuilder(final CollectionBinding.Builder base) {
+        super(base);
+    }
+
+    /**
+     * This binding will produce lists of row name strings, which the item {@link ViewAdapter} will
+     * need to bind to Syncbase rows with scalar {@link SyncbaseBinding}s.
+     */
+    public IdListBindingBuilder<A> idListRowName(final String idListRowName) {
+        mIdListRowName = idListRowName;
+        return this;
+    }
+
+    public Observable<SingleWatchEvent<ImmutableList<String>>> buildIdListWatch() {
+        return mBase.mRxTable.watch(mIdListRowName, new TypeToken<List<String>>() {
+        }, ImmutableList.of()).map(w -> w.map(ImmutableList::copyOf));
+    }
+
+    @Override
+    public Observable<IdListAccumulator> buildListAccumulator() {
+        return new IdListAccumulator()
+                .scanFrom(buildIdListWatch());
+    }
+}
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
new file mode 100644
index 0000000..b7d2f9c
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulator.java
@@ -0,0 +1,17 @@
+// 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;
+
+public interface ListAccumulator<T> {
+    boolean containsRow(String rowName);
+    int getCount();
+    T getRowAt(int position);
+
+    /**
+     * @return the row index, or a negative index if not present. The negative value (and whether or
+     *  not it has meaning) may vary by implementation.
+     */
+    int getRowIndex(String rowName);
+}
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
new file mode 100644
index 0000000..f6737a4
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ListAccumulators.java
@@ -0,0 +1,42 @@
+// 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.NoSuchElementException;
+
+import io.v.rx.syncbase.RxTable;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class ListAccumulators {
+
+
+    public static final ListAccumulator<Object> EMPTY = new ListAccumulator<Object>(){
+        @Override
+        public int getCount() {
+            return 0;
+        }
+
+        @Override
+        public boolean containsRow(final String rowName) {
+            return false;
+        }
+
+        @Override
+        public RxTable.Row<Object> getRowAt(final int position) {
+            throw new NoSuchElementException("No elements in empty ListAccumulator.");
+        }
+
+        @Override
+        public int getRowIndex(final String rowName) {
+            return -1;
+        }
+    };
+
+    @SuppressWarnings("unchecked")
+    public static <T> ListAccumulator<T> empty() {
+        return (ListAccumulator<T>)EMPTY;
+    }
+}
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
new file mode 100644
index 0000000..57e8588
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixBindingBuilder.java
@@ -0,0 +1,105 @@
+// 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 com.google.common.collect.Ordering;
+
+import io.v.rx.syncbase.RangeWatchBatch;
+import io.v.rx.syncbase.RxTable;
+import io.v.v23.syncbase.nosql.PrefixRange;
+import io.v.v23.syncbase.nosql.RowRange;
+import rx.Observable;
+import rx.functions.Func1;
+
+/**
+ * If {@code T} is {@link Comparable}, the default row ordering is natural ordering on row
+ * values. Otherwise, the default is natural ordering on row names.
+ */
+public class PrefixBindingBuilder<T, A extends RangeAdapter>
+        extends CollectionAdapterBuilder<PrefixBindingBuilder<T, A>, RxTable.Row<T>, A> {
+    private Class<T> mType;
+    private PrefixRange mPrefix;
+    private Ordering<? super RxTable.Row<T>> mOrdering;
+    private Func1<String, Boolean> mKeyFilter;
+
+    public PrefixBindingBuilder(final CollectionBinding.Builder base) {
+        super(base);
+    }
+
+    public PrefixBindingBuilder<T, A> prefix(final PrefixRange prefix) {
+        mPrefix = prefix;
+        return this;
+    }
+
+    public PrefixBindingBuilder<T, A> prefix(final String prefix) {
+        return prefix(RowRange.prefix(prefix));
+    }
+
+    /**
+     * This setter is minimally typesafe; after setting the {@code type}, clients should
+     * probably also update {@code ordering} and {@code viewAdapter}. If intending to use a
+     * collection binding that requires a
+     */
+    public <U> PrefixBindingBuilder<U, A> type(final Class<U> type) {
+        @SuppressWarnings("unchecked")
+        final PrefixBindingBuilder<U, A> casted = (PrefixBindingBuilder<U, A>) this;
+        casted.mType = type;
+        return casted;
+    }
+
+    public PrefixBindingBuilder<T, A> ordering(
+            final Ordering<? super RxTable.Row<? extends T>> ordering) {
+        mOrdering = ordering;
+        return this;
+    }
+
+    public PrefixBindingBuilder<T, A> valueOrdering(final Ordering<? super T> ordering) {
+        return ordering(ordering.onResultOf(RxTable.Row::getValue));
+    }
+
+    public PrefixBindingBuilder<T, A> valueAdapter(final ViewAdapter<? super T, ?> viewAdapter) {
+        return viewAdapter(new TransformingViewAdapter<>(viewAdapter, RxTable.Row::getValue));
+    }
+
+    @Override
+    protected ViewAdapter<RxTable.Row<T>, ?> getDefaultViewAdapter() {
+        return new TextViewAdapter(mBase.getDefaultViewAdapterContext()).map(RxTable.Row::getValue);
+    }
+
+    /**
+     * For comparable {@code T}, default to natural ordering on values. Otherwise, default to
+     * natural ordering on row names.
+     */
+    private Ordering<? super RxTable.Row<? extends T>> getDefaultOrdering() {
+        if (mOrdering == null && Comparable.class.isAssignableFrom(mType)) {
+            return Ordering.natural().onResultOf(r -> (Comparable) r.getValue());
+        } else {
+            return Ordering.natural().onResultOf(RxTable.Row::getRowName);
+        }
+    }
+
+    public PrefixBindingBuilder<T, A> keyFilter(final Func1<String, Boolean> keyFilter) {
+        mKeyFilter = keyFilter;
+        return this;
+    }
+
+    public Observable<RangeWatchBatch<T>> buildPrefixWatch() {
+        if (mType == null) {
+            throw new IllegalStateException("Missing required type property");
+        }
+        return mBase.mRxTable.watch(mPrefix == null? RowRange.prefix("") : mPrefix,
+                mKeyFilter, mType);
+    }
+
+    private Ordering<? super RxTable.Row<T>> getOrdering() {
+        return mOrdering == null ? getDefaultOrdering() : mOrdering;
+    }
+
+    @Override
+    public Observable<PrefixListAccumulator<T>> buildListAccumulator() {
+        return new PrefixListAccumulator<>(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
new file mode 100644
index 0000000..af5bd6c
--- /dev/null
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/PrefixListAccumulator.java
@@ -0,0 +1,133 @@
+// 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 com.google.common.collect.Ordering;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.v.rx.syncbase.RangeWatchBatch;
+import io.v.rx.syncbase.RangeWatchEvent;
+import io.v.rx.syncbase.RxTable;
+import io.v.v23.syncbase.nosql.ChangeType;
+import rx.Observable;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Func2;
+
+/**
+ * This class accumulates prefix watch streams into observable lists. It is meant to be used in
+ * conjunction with {@link Observable#scan(Object, Func2)}:
+ *
+ * <p>{@code .scan(new PrefixListAccumulator<>(...), PrefixListAccumulator::add)}
+ * @param <T>
+ */
+public class PrefixListAccumulator<T> implements ListAccumulator<RxTable.Row<T>> {
+    private static final String ERR_INCONSISTENT = "Sorted data are inconsistent with map data";
+
+    private final Map<String, T> mRows = new HashMap<>();
+    private final List<RxTable.Row<T>> mSorted = new ArrayList<>();
+    private final Ordering<? super RxTable.Row<T>> mOrdering;
+
+    public PrefixListAccumulator(final Ordering<? super RxTable.Row<T>> ordering) {
+        // ensure deterministic ordering by always applying secondary order on row name
+        mOrdering = ordering.compound(Ordering.natural().onResultOf(RxTable.Row::getRowName));
+    }
+
+    public Observable<PrefixListAccumulator<T>> scanFrom(
+            final Observable<RangeWatchBatch<T>> watch) {
+        return watch
+                .concatMap(RangeWatchBatch::collectChanges)
+                .observeOn(AndroidSchedulers.mainThread()) // required unless we copy
+                .scan(this, PrefixListAccumulator::withUpdates);
+    }
+
+    private int findRowForEdit(final String rowName, final T oldValue) {
+        final int oldIndex = Collections.binarySearch(mSorted,
+                new RxTable.Row<>(rowName, oldValue), mOrdering);
+        if (oldIndex < 0) {
+            throw new ConcurrentModificationException(ERR_INCONSISTENT);
+        } else {
+            return oldIndex;
+        }
+    }
+
+    private 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)
+        for (final RangeWatchEvent<T> e : events) {
+            if (e.getChangeType() == ChangeType.DELETE_CHANGE) {
+                removeOne(e.getRow());
+            } else {
+                updateOne(e.getRow());
+            }
+        }
+        return this;
+    }
+
+    protected void removeOne(final RxTable.Row<T> entry) {
+        final T old = mRows.remove(entry.getRowName());
+        if (old != null) {
+            mSorted.remove(findRowForEdit(entry.getRowName(), old));
+        }
+    }
+
+    private int insertionIndex(final RxTable.Row<T> entry) {
+        final int bs = Collections.binarySearch(mSorted, entry, mOrdering);
+        return bs < 0 ? ~bs : bs;
+    }
+
+    protected void updateOne(final RxTable.Row<T> entry) {
+        final T old = mRows.put(entry.getRowName(), entry.getValue());
+        if (old == null) {
+            mSorted.add(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));
+                }
+            } else {
+                for (int i = oldIndex; i > newIndex; i--) {
+                    mSorted.set(i, mSorted.get(i - 1));
+                }
+            }
+            mSorted.set(newIndex, entry);
+        }
+    }
+
+    @Override
+    public int getCount() {
+        return mRows.size();
+    }
+
+    @Override
+    public RxTable.Row<T> getRowAt(final int position) {
+        return mSorted.get(position);
+    }
+
+    public T getValue(final String rowName) {
+        return mRows.get(rowName);
+    }
+
+    @Override
+    public int getRowIndex(final String rowName) {
+        return Collections.binarySearch(mSorted, new RxTable.Row<>(rowName, mRows.get(rowName)),
+                mOrdering);
+    }
+
+    @Override
+    public boolean containsRow(final String rowName) {
+        return mRows.containsKey(rowName);
+    }
+}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java
index 052e000..48dbca0 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java
@@ -4,6 +4,7 @@
 
 package io.v.baku.toolkit.bind;
 
+import android.support.annotation.IdRes;
 import android.view.View;
 import android.widget.EditText;
 import android.widget.TextView;
@@ -164,7 +165,6 @@
             return bindTwoWay(editText);
         }
 
-        @Override
         public Builder<T> bindTo(final View view) {
             if (view instanceof TextView) {
                 return bindTo((TextView) view);
@@ -172,6 +172,14 @@
                 throw new IllegalArgumentException("No default binding for view " + view);
             }
         }
+
+        /**
+         * Binds to the view identified by {@code viewId}.
+         * @see #bindTo(View)
+         */
+        public Builder<T> bindTo(final @IdRes int viewId) {
+            return bindTo(mActivity.findViewById(viewId));
+        }
     }
 
     public static <T> Builder<T> builder() {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseListAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseListAdapter.java
index 150e699..7877239 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseListAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseListAdapter.java
@@ -8,51 +8,53 @@
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
 
-import com.google.common.collect.Ordering;
-
-import java.util.Collection;
-
-import io.v.rx.syncbase.RangeWatchBatch;
-import io.v.rx.syncbase.RangeWatchEvent;
-import io.v.rx.syncbase.RxTable;
+import lombok.Getter;
 import lombok.experimental.Accessors;
 import lombok.experimental.Delegate;
 import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
 import rx.functions.Action1;
 
 @Accessors(prefix = "m")
-public class SyncbaseListAdapter<T> extends BaseAdapter implements RangeAdapter {
+public class SyncbaseListAdapter<T> extends BaseAdapter
+        implements RangeAdapter, ListAccumulator<T> {
+    private final ViewAdapter<? super T, ?> mViewAdapter;
     @Delegate
-    private final SyncbaseRangeAdapter<T> mAdapter;
-    private final ViewAdapter<? super RxTable.Row<T>, ?> mViewAdapter;
+    private ListAccumulator<T> mLatestState = ListAccumulators.empty();
+    @Getter
+    private final Subscription mSubscription;
 
-    public SyncbaseListAdapter(final Observable<RangeWatchBatch<T>> watch,
-                               final Ordering<? super RxTable.Row<T>> ordering,
-                               final ViewAdapter<? super RxTable.Row<T>, ?> viewAdapter,
+    public SyncbaseListAdapter(final Observable<? extends ListAccumulator<T>> data,
+                               final ViewAdapter<? super T, ?> viewAdapter,
                                final Action1<Throwable> onError) {
-        mAdapter = new SyncbaseRangeAdapter<T>(watch, ordering, onError) {
-            @Override
-            protected void processEvents(Collection<RangeWatchEvent<T>> rangeWatchEvents) {
-                super.processEvents(rangeWatchEvents);
-                notifyDataSetChanged();
-            }
-        };
         mViewAdapter = viewAdapter;
+        mSubscription = data
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(d -> {
+                    mLatestState = d;
+                    notifyDataSetChanged();
+                }, onError);
+    }
+
+    @Override
+    public void close() throws Exception {
+        mSubscription.unsubscribe();
     }
 
     @Override
     public View getView(final int position, View view, final ViewGroup parent) {
-        final RxTable.Row<T> entry = mAdapter.getRowAt(position);
+        final T row = mLatestState.getRowAt(position);
         if (view == null) {
             view = mViewAdapter.createView(parent);
         }
-        mViewAdapter.bindView(view, position, entry);
+        mViewAdapter.bindView(view, position, row);
         return view;
     }
 
     @Override
     public T getItem(int position) {
-        return mAdapter.getRowAt(position).getValue();
+        return getRowAt(position);
     }
 
     /**
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapter.java
deleted file mode 100644
index 8b40617..0000000
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRangeAdapter.java
+++ /dev/null
@@ -1,308 +0,0 @@
-// 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.content.Context;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.widget.ListView;
-
-import com.google.common.collect.Ordering;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.ConcurrentModificationException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.v.rx.syncbase.RangeWatchBatch;
-import io.v.rx.syncbase.RangeWatchEvent;
-import io.v.rx.syncbase.RxTable;
-import io.v.v23.syncbase.nosql.ChangeType;
-import io.v.v23.syncbase.nosql.PrefixRange;
-import io.v.v23.syncbase.nosql.RowRange;
-import java8.util.function.Function;
-import lombok.Getter;
-import lombok.experimental.Accessors;
-import rx.Observable;
-import rx.Subscription;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action1;
-import rx.functions.Func1;
-
-@Accessors(prefix = "m")
-public class SyncbaseRangeAdapter<T> implements RangeAdapter {
-    private static final String ERR_INCONSISTENT = "Sorted data are inconsistent with map data";
-
-    /**
-     * If {@code T} is {@link Comparable}, the default row ordering is natural ordering on row
-     * values. Otherwise, the default is natural ordering on row names.
-     */
-    @Accessors(prefix = "m")
-    public static class Builder<T, A extends RangeAdapter>
-            extends BaseBuilder<Builder<T, ? extends RangeAdapter>> {
-        private PrefixRange mPrefix = RowRange.prefix("");
-        private Class<T> mType;
-        private Ordering<? super RxTable.Row<T>> mOrdering;
-        private Func1<String, Boolean> mKeyFilter;
-        private ViewAdapter<? super RxTable.Row<T>, ?> mViewAdapter;
-        private Context mViewAdapterContext;
-
-        @Getter
-        private A mAdapter;
-
-        public Builder<T, A> prefix(final PrefixRange prefix) {
-            mPrefix = prefix;
-            return this;
-        }
-
-        public Builder<T, A> prefix(final String prefix) {
-            return prefix(RowRange.prefix(prefix));
-        }
-
-        /**
-         * This setter is minimally typesafe; after setting the {@code type}, clients should
-         * probably also update {@code ordering} and {@code viewAdapter}.
-         */
-        public <U> Builder<U, SyncbaseRangeAdapter<U>> type(final Class<U> type) {
-            @SuppressWarnings("unchecked")
-            final Builder<U, SyncbaseRangeAdapter<U>> casted =
-                    (Builder<U, SyncbaseRangeAdapter<U>>) this;
-            casted.mType = type;
-            return casted;
-        }
-
-        public Builder<T, A> ordering(final Ordering<? super RxTable.Row<? extends T>> ordering) {
-            mOrdering = ordering;
-            return this;
-        }
-
-        public Builder<T, A> valueOrdering(final Ordering<? super T> ordering) {
-            return ordering(ordering.onResultOf(RxTable.Row::getValue));
-        }
-
-        /**
-         * For comparable {@code T}, default to natural ordering on values. Otherwise, default to
-         * natural ordering on row names.
-         */
-        private Ordering<? super RxTable.Row<? extends T>> getDefaultOrdering() {
-            if (mOrdering == null && Comparable.class.isAssignableFrom(mType)) {
-                return Ordering.natural().onResultOf(r -> (Comparable) r.getValue());
-            } else {
-                return Ordering.natural().onResultOf(RxTable.Row::getRowName);
-            }
-        }
-
-        public Builder<T, A> keyFilter(final Func1<String, Boolean> keyFilter) {
-            mKeyFilter = keyFilter;
-            return this;
-        }
-
-        public Builder<T, A> viewAdapter(final ViewAdapter<? super RxTable.Row<T>, ?> viewAdapter) {
-            mViewAdapter = viewAdapter;
-            return this;
-        }
-
-        public <U> Builder<T, A> textViewAdapter(final Function<RxTable.Row<T>, U> fn) {
-            return viewAdapter(getDefaultViewAdapter(fn));
-        }
-
-        public Builder<T, A> valueAdapter(final ViewAdapter<T, ?> viewAdapter) {
-            return viewAdapter(new TransformingViewAdapter<>(viewAdapter, RxTable.Row::getValue));
-        }
-
-        private <U> ViewAdapter<? super RxTable.Row<T>, ?> getDefaultViewAdapter(
-                final Function<RxTable.Row<T>, U> fn) {
-            return new TextViewAdapter<U>(getDefaultViewAdapterContext()).map(fn);
-        }
-
-        /**
-         * The default view adapter stringizes values.
-         */
-        private ViewAdapter<? super RxTable.Row<T>, ?> getDefaultViewAdapter() {
-            return getDefaultViewAdapter(RxTable.Row::getValue);
-        }
-
-        public Builder<T, A> viewAdapterContext(final Context context) {
-            mViewAdapterContext = context;
-            return this;
-        }
-
-        public Context getDefaultViewAdapterContext() {
-            return mViewAdapterContext == null ? mActivity : mViewAdapterContext;
-        }
-
-        public Observable<RangeWatchBatch<T>> buildWatch() {
-            if (mType == null) {
-                throw new IllegalStateException("Missing required type property");
-            }
-            return mRxTable.watch(mPrefix, mKeyFilter, mType);
-        }
-
-        private Ordering<? super RxTable.Row<T>> getOrdering() {
-            return mOrdering == null ? getDefaultOrdering() : mOrdering;
-        }
-
-        private ViewAdapter<? super RxTable.Row<T>, ?> getViewAdapter() {
-            return mViewAdapter == null ? getDefaultViewAdapter() : mViewAdapter;
-        }
-
-        private <U extends RangeAdapter> U subscribeAdapter(final U adapter) {
-            subscribe(adapter.getSubscription());
-            mAdapter = null;
-            return adapter;
-        }
-
-        public SyncbaseListAdapter<T> buildListAdapter() {
-            return subscribeAdapter(new SyncbaseListAdapter<>(
-                    buildWatch(), getOrdering(), getViewAdapter(), mOnError));
-        }
-
-        public SyncbaseRecyclerAdapter<T, ?> buildRecyclerAdapter() {
-            return subscribeAdapter(new SyncbaseRecyclerAdapter<>(
-                    buildWatch(), getOrdering(), getViewAdapter(), mOnError));
-        }
-
-        public Builder<T, SyncbaseListAdapter<T>> bindTo(final ListView listView) {
-            @SuppressWarnings("unchecked")
-            final Builder<T, SyncbaseListAdapter<T>> casted =
-                    (Builder<T, SyncbaseListAdapter<T>>) this;
-            casted.mAdapter = buildListAdapter();
-            listView.setAdapter(casted.mAdapter);
-            return casted;
-        }
-
-        public Builder<T, SyncbaseRecyclerAdapter<T, ?>> bindTo(final RecyclerView recyclerView) {
-            @SuppressWarnings("unchecked")
-            final Builder<T, SyncbaseRecyclerAdapter<T, ?>> casted =
-                    (Builder<T, SyncbaseRecyclerAdapter<T, ?>>) this;
-            casted.mAdapter = buildRecyclerAdapter();
-            recyclerView.setAdapter(casted.mAdapter);
-            return casted;
-        }
-
-        @Override
-        public Builder<T, ?> bindTo(final View view) {
-            if (view instanceof ListView) {
-                return bindTo((ListView) view);
-            } else if (view instanceof RecyclerView) {
-                return bindTo((RecyclerView) view);
-            } else {
-                throw new IllegalArgumentException("No default binding for view " + view);
-            }
-        }
-    }
-
-    public static <T, A extends RangeAdapter> Builder<T, A> builder() {
-        return new Builder<>();
-    }
-
-    private final Map<String, T> mRows = new HashMap<>();
-    private List<RxTable.Row<T>> mSorted = new ArrayList<>();
-    private final Ordering<? super RxTable.Row<T>> mOrdering;
-    private final Action1<Throwable> mOnError;
-    @Getter
-    private final Subscription mSubscription;
-
-    public SyncbaseRangeAdapter(final Observable<RangeWatchBatch<T>> watch,
-                                final Ordering<? super RxTable.Row<T>> ordering,
-                                final Action1<Throwable> onError) {
-        // ensure deterministic ordering by always applying secondary order on row name
-        mOrdering = ordering.compound(Ordering.natural().onResultOf(RxTable.Row::getRowName));
-        mOnError = onError;
-
-        mSubscription = subscribeTo(watch);
-    }
-
-    @Override
-    public void close() {
-        mSubscription.unsubscribe();
-    }
-
-    private Subscription subscribeTo(final Observable<RangeWatchBatch<T>> watch) {
-        return watch
-                .concatMap(RangeWatchBatch::collectChanges)
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(this::processEvents, mOnError);
-    }
-
-    private int findRowForEdit(final String rowName, final T oldValue) {
-        final int oldIndex = Collections.binarySearch(mSorted,
-                new RxTable.Row<>(rowName, oldValue), mOrdering);
-        if (oldIndex < 0) {
-            throw new ConcurrentModificationException(ERR_INCONSISTENT);
-        } else {
-            return oldIndex;
-        }
-    }
-
-    protected void processEvents(final Collection<RangeWatchEvent<T>> events) {
-        // TODO(rosswang): more efficient updates for larger batches
-        for (final RangeWatchEvent<T> e : events) {
-            if (e.getChangeType() == ChangeType.DELETE_CHANGE) {
-                removeOne(e.getRow());
-            } else {
-                updateOne(e.getRow());
-            }
-        }
-    }
-
-    protected void removeOne(final RxTable.Row<T> entry) {
-        final T old = mRows.remove(entry.getRowName());
-        if (old != null) {
-            mSorted.remove(findRowForEdit(entry.getRowName(), old));
-        }
-    }
-
-    private int insertionIndex(final RxTable.Row<T> entry) {
-        final int bs = Collections.binarySearch(mSorted, entry, mOrdering);
-        return bs < 0 ? ~bs : bs;
-    }
-
-    protected void updateOne(final RxTable.Row<T> entry) {
-        final T old = mRows.put(entry.getRowName(), entry.getValue());
-        if (old == null) {
-            mSorted.add(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));
-                }
-            } else {
-                for (int i = oldIndex; i > newIndex; i--) {
-                    mSorted.set(i, mSorted.get(i - 1));
-                }
-            }
-            mSorted.set(newIndex, entry);
-        }
-    }
-
-    public int getCount() {
-        return mRows.size();
-    }
-
-    public RxTable.Row<T> getRowAt(final int position) {
-        return mSorted.get(position);
-    }
-
-    public T getValue(final String rowName) {
-        return mRows.get(rowName);
-    }
-
-    public int getRowIndex(final String rowName) {
-        return Collections.binarySearch(mSorted, new RxTable.Row<>(rowName, mRows.get(rowName)),
-                mOrdering);
-    }
-
-    public boolean containsRow(final String rowName) {
-        return mRows.containsKey(rowName);
-    }
-}
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRecyclerAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRecyclerAdapter.java
index 6260631..1bc4d83 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRecyclerAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/SyncbaseRecyclerAdapter.java
@@ -7,22 +7,18 @@
 import android.support.v7.widget.RecyclerView;
 import android.view.ViewGroup;
 
-import com.google.common.collect.Ordering;
-
-import java.util.Collection;
-
-import io.v.rx.syncbase.RangeWatchBatch;
-import io.v.rx.syncbase.RangeWatchEvent;
-import io.v.rx.syncbase.RxTable;
+import lombok.Getter;
 import lombok.experimental.Accessors;
 import lombok.experimental.Delegate;
 import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
 import rx.functions.Action1;
 
 @Accessors(prefix = "m")
 public class SyncbaseRecyclerAdapter<T, VH extends ViewHolder>
         extends RecyclerView.Adapter<SyncbaseRecyclerAdapter.ViewHolderAdapter<VH>>
-        implements RangeAdapter {
+        implements RangeAdapter, ListAccumulator<T> {
 
     public static class ViewHolderAdapter<B extends ViewHolder> extends RecyclerView.ViewHolder {
         public final B bakuViewHolder;
@@ -32,27 +28,28 @@
         }
     }
 
-    private interface SimilarButDifferent {
-        int getCount();
+    private final ViewAdapter<? super T, VH> mViewAdapter;
+    @Delegate
+    private ListAccumulator<T> mLatestState = ListAccumulators.empty();
+    @Getter
+    private final Subscription mSubscription;
+
+    public SyncbaseRecyclerAdapter(final Observable<? extends ListAccumulator<T>> data,
+                                   final ViewAdapter<? super T, VH> viewAdapter,
+                                   final Action1<Throwable> onError) {
+        mViewAdapter = viewAdapter;
+        mSubscription = data
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(d -> {
+                    mLatestState = d;
+                    notifyDataSetChanged();
+                    // TODO(rosswang): Use higher-fidelity update notifications.
+                }, onError);
     }
 
-    @Delegate(excludes = SimilarButDifferent.class)
-    private final SyncbaseRangeAdapter<T> mAdapter;
-    private final ViewAdapter<? super RxTable.Row<T>, VH> mViewAdapter;
-
-    public SyncbaseRecyclerAdapter(final Observable<RangeWatchBatch<T>> watch,
-                                   final Ordering<? super RxTable.Row<T>> ordering,
-                                   final ViewAdapter<? super RxTable.Row<T>, VH> viewAdapter,
-                                   final Action1<Throwable> onError) {
-        mAdapter = new SyncbaseRangeAdapter<T>(watch, ordering, onError) {
-            @Override
-            protected void processEvents(Collection<RangeWatchEvent<T>> rangeWatchEvents) {
-                super.processEvents(rangeWatchEvents);
-                notifyDataSetChanged();
-                // TODO(rosswang): Use higher-fidelity update notifications.
-            }
-        };
-        mViewAdapter = viewAdapter;
+    @Override
+    public void close() throws Exception {
+        mSubscription.unsubscribe();
     }
 
     @Override
@@ -63,7 +60,13 @@
 
     @Override
     public void onBindViewHolder(final ViewHolderAdapter<VH> holder, final int position) {
-        mViewAdapter.bindViewHolder(holder.bakuViewHolder, position, mAdapter.getRowAt(position));
+        mViewAdapter.bindViewHolder(
+                holder.bakuViewHolder, position, mLatestState.getRowAt(position));
+    }
+
+    @Override
+    public int getItemCount() {
+        return getCount();
     }
 
     /**
@@ -73,9 +76,4 @@
     public long getItemId(int i) {
         return RecyclerView.NO_ID;
     }
-
-    @Override
-    public int getItemCount() {
-        return mAdapter.getCount();
-    }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TextViewAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TextViewAdapter.java
index 9482adc..a233704 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TextViewAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TextViewAdapter.java
@@ -21,7 +21,7 @@
 
 @RequiredArgsConstructor
 @Slf4j
-public class TextViewAdapter<T> extends AbstractViewAdapter<T, TextViewAdapter.ViewHolder> {
+public class TextViewAdapter extends AbstractViewAdapter<Object, TextViewAdapter.ViewHolder> {
     @Accessors(prefix = "m")
     @Getter
     @RequiredArgsConstructor
@@ -87,7 +87,8 @@
     }
 
     @Override
-    public void bindViewHolder(final ViewHolder viewHolder, final int position, final T value) {
+    public void bindViewHolder(final ViewHolder viewHolder, final int position,
+                               final Object value) {
         viewHolder.setText(format(position, value));
     }
 
@@ -96,7 +97,7 @@
      * or stringizes otherwise. We avoid agnostic stringization to preserve any Android formatting
      * that might be present, like with {@link android.text.SpannableString}.
      */
-    protected CharSequence format(final int position, final T value) {
+    protected CharSequence format(final int position, final Object value) {
         return value instanceof CharSequence ? (CharSequence) value : Objects.toString(value);
     }
 }
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TransformingViewAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TransformingViewAdapter.java
index 334b2bd..71325b6 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TransformingViewAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/TransformingViewAdapter.java
@@ -14,7 +14,7 @@
 public class TransformingViewAdapter<T, U, VH extends ViewHolder>
         extends AbstractViewAdapter<T, VH> {
     private final ViewAdapter<U, VH> mBase;
-    private final Function<T, U> mFn;
+    private final Function<T, ? extends U> mFn;
 
     @Override
     public View createView(final ViewGroup parent) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ViewAdapter.java b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ViewAdapter.java
index 6a0de8f..7883265 100644
--- a/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ViewAdapter.java
+++ b/baku-toolkit/lib/src/main/java/io/v/baku/toolkit/bind/ViewAdapter.java
@@ -8,12 +8,12 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import java8.lang.FunctionalInterface;
+import java8.util.function.Function;
 
-@FunctionalInterface
 public interface ViewAdapter<T, VH extends ViewHolder> {
     View createView(ViewGroup parent);
     VH createViewHolder(View view);
     void bindViewHolder(VH viewHolder, int position, T value);
     void bindView(View view, int position, T value);
+    <U> ViewAdapter<U, VH> map(final Function<U, ? extends T> fn);
 }
\ No newline at end of file
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
index 17e594a..7bafee7 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
@@ -52,12 +52,6 @@
         return builder().activity(t).build();
     }
 
-    /*
-    As of Lombok IntelliJ 0.9.6, @Builder exhibits a few bugs interacting with @Accessors (Gradle
-    build is fine).
-
-    https://github.com/mplushnikov/lombok-intellij-plugin/issues/151
-     */
     public static class Builder {
         private String sgSuffix = DEFAULT_SYNCGROUP_SUFFIX;
         private Function<String, String> descriptionForUsername = u -> "User syncgroup for " + u;
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RangeWatchEvent.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RangeWatchEvent.java
index 6810f50..5de69b4 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RangeWatchEvent.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RangeWatchEvent.java
@@ -4,6 +4,8 @@
 
 package io.v.rx.syncbase;
 
+import com.google.common.reflect.TypeToken;
+
 import java.util.Map;
 
 import io.v.v23.syncbase.nosql.ChangeType;
@@ -21,19 +23,25 @@
     boolean mFromSync;
 
     @SuppressWarnings("unchecked")
-    private static <T> T getWatchValue(final WatchChange change, final Class<T> type)
+    private static <T> T getWatchValue(final WatchChange change, final TypeToken<T> tt)
             throws VException {
         if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
             return null;
         } else {
-            return (T) VomUtil.decode(change.getVomValue(), type);
+            return (T) VomUtil.decode(change.getVomValue(),
+                    tt == null? Object.class : tt.getType());
         }
     }
 
+    public static <T> RangeWatchEvent<T> fromWatchChange(final WatchChange c, final TypeToken<T> tt)
+            throws VException {
+        return new RangeWatchEvent<>(new RxTable.Row<>(c.getRowName(), getWatchValue(c, tt)),
+                c.getChangeType(), c.isFromSync());
+    }
+
     public static <T> RangeWatchEvent<T> fromWatchChange(final WatchChange c, final Class<T> type)
             throws VException {
-        return new RangeWatchEvent<>(new RxTable.Row<>(c.getRowName(), getWatchValue(c, type)),
-                c.getChangeType(), c.isFromSync());
+        return fromWatchChange(c, TypeToken.of(type));
     }
 
     public void applyTo(final Map<String, T> accumulator) {
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
index 15ba1c0..ead7b5f 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/RxTable.java
@@ -7,6 +7,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import com.google.common.reflect.TypeToken;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -92,11 +93,11 @@
     }
 
     private <T> Observable<T> getInitial(
-            final BatchDatabase db, final String tableName, final String key, final Class<T> type,
+            final BatchDatabase db, final String tableName, final String key, final TypeToken<T> tt,
             final T defaultValue) {
         @SuppressWarnings("unchecked")
         final ListenableFuture<T> fromGet = (ListenableFuture<T>) db.getTable(tableName).get(
-                mVContext, key, type);
+                mVContext, key, tt == null ? Object.class : tt.getType());
         return toObservable(Futures.withFallback(fromGet, t -> t instanceof NoExistException ?
                 Futures.immediateFuture(defaultValue) : Futures.immediateFailedFuture(t)));
     }
@@ -104,14 +105,14 @@
     @SuppressWarnings("unchecked")
     private <T> Observable<Row<T>> getInitial(
             final BatchDatabase db, final String tableName, final RowRange keys,
-            @Nullable final Func1<String, Boolean> keyFilter, final Class<T> type) {
+            @Nullable final Func1<String, Boolean> keyFilter, final TypeToken<T> tt) {
         Observable<KeyValue> untyped = RxInputChannel.wrap(
                 db.getTable(tableName).scan(mVContext, keys)).autoConnect();
         if (keyFilter != null) {
             untyped = untyped.filter(kv -> keyFilter.call(kv.getKey()));
         }
         return untyped.concatMap(VFn.wrap(kv -> new Row<>(kv.getKey(),
-                (T) VomUtil.decode(kv.getValue(), type))));
+                (T) VomUtil.decode(kv.getValue(), tt == null ? Object.class : tt.getType()))));
     }
 
     /**
@@ -126,14 +127,15 @@
      * once, as we can only iterate over the underlying stream once.
      */
     private static <T> Observable<SingleWatchEvent<T>> observeWatchStream(
-            final InputChannel<WatchChange> s, final String key, final T defaultValue) {
+            final InputChannel<WatchChange> s, final String key, final TypeToken<T> tt,
+            final T defaultValue) {
         return RxInputChannel.wrap(s)
                 .autoConnect()
                 .filter(c -> c.getRowName().equals(key))
                         // About the Vfn.wrap, on error, the wrapping replay will disconnect,
                         // calling cancellation (see cancelOnDisconnect). The possible source of
                         // VException here is VOM decoding.
-                .concatMap(VFn.wrap(c -> SingleWatchEvent.fromWatchChange(c, defaultValue)))
+                .concatMap(VFn.wrap(c -> SingleWatchEvent.fromWatchChange(c, tt, defaultValue)))
                 .distinctUntilChanged();
     }
 
@@ -181,7 +183,7 @@
      */
     private static <T> Observable<RangeWatchBatch<T>> observeWatchStream(
             final InputChannel<WatchChange> s, @Nullable final Func1<String, Boolean> prefixFilter,
-            final Class<T> type) {
+            final TypeToken<T> tt) {
         // TODO(rosswang): support other RowRange types
         final Observable<WatchChange> raw = RxInputChannel.wrap(s).autoConnect();
 
@@ -193,7 +195,7 @@
                                 if (prefixFilter == null || prefixFilter.call(c.getRowName())) {
                                     try {
                                         windower.onNext(c.getResumeMarker(),
-                                                RangeWatchEvent.fromWatchChange(c, type));
+                                                RangeWatchEvent.fromWatchChange(c, tt));
                                     } catch (final VException e) {
                                         windower.onError(c.getResumeMarker(), e);
                                     }
@@ -246,11 +248,11 @@
     }
 
     private <T> void subscribeWatch(final Subscriber<? super SingleWatchEvent<T>> subscriber,
-                                    final Database db, final String key, final Class<T> type,
+                                    final Database db, final String key, final TypeToken<T> tt,
                                     final T defaultValue) {
         subscribeWatch(subscriber, db, key,
-                b -> getInitial(b, mName, key, type, defaultValue),
-                s -> observeWatchStream(s, key, defaultValue),
+                b -> getInitial(b, mName, key, tt, defaultValue),
+                s -> observeWatchStream(s, key, tt, defaultValue),
                 (i, s) -> s.startWith(i.initial.map(iv ->
                         new SingleWatchEvent<>(iv, i.resumeMarker, false))));
     }
@@ -258,10 +260,10 @@
     private <T> void subscribeWatch(
             final Subscriber<? super RangeWatchBatch<T>> subscriber, final Database db,
             final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
-            final Class<T> type) {
+            final TypeToken<T> tt) {
         subscribeWatch(subscriber, db, prefix.getPrefix(),
-                b -> getInitial(b, mName, prefix, keyFilter, type),
-                s -> RxTable.observeWatchStream(s, keyFilter, type),
+                b -> getInitial(b, mName, prefix, keyFilter, tt),
+                s -> RxTable.observeWatchStream(s, keyFilter, tt),
                 (i, s) -> s.startWith(new RangeWatchBatch<>(i.resumeMarker, i.initial.map(r ->
                         new RangeWatchEvent<>(r, ChangeType.PUT_CHANGE, false)))));
     }
@@ -277,10 +279,10 @@
     /**
      * Watches a specific Syncbase row for changes.
      */
-    public <T> Observable<SingleWatchEvent<T>> watch(final String key, final Class<T> type,
+    public <T> Observable<SingleWatchEvent<T>> watch(final String key, final TypeToken<T> tt,
                                                      final T defaultValue) {
         return this.<SingleWatchEvent<T>>watch((db, s) ->
-                subscribeWatch(s, db, key, type, defaultValue))
+                subscribeWatch(s, db, key, tt, defaultValue))
                 // Don't create new watch streams for subsequent subscribers, but do cancel the
                 // stream if no subscribers are listening (and restart if new subscriptions happen).
                 .replay(1)
@@ -288,12 +290,29 @@
     }
 
     /**
+     * Watches a specific Syncbase row for changes.
+     */
+    public <T> Observable<SingleWatchEvent<T>> watch(final String key, final Class<T> type,
+                                                     final T defaultValue) {
+        return watch(key, TypeToken.of(type), defaultValue);
+    }
+
+    /**
+     * Watches a Syncbase prefix for changes.
+     */
+    public <T> Observable<RangeWatchBatch<T>> watch(
+            final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
+            final TypeToken<T> tt) {
+        return watch((db, s) -> subscribeWatch(s, db, prefix, keyFilter, tt));
+    }
+
+    /**
      * Watches a Syncbase prefix for changes.
      */
     public <T> Observable<RangeWatchBatch<T>> watch(
             final PrefixRange prefix, @Nullable final Func1<String, Boolean> keyFilter,
             final Class<T> type) {
-        return watch((db, s) -> subscribeWatch(s, db, prefix, keyFilter, type));
+        return watch(prefix, keyFilter, TypeToken.of(type));
     }
 
     /**
diff --git a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SingleWatchEvent.java b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SingleWatchEvent.java
index 2f76ae8..69f1741 100644
--- a/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SingleWatchEvent.java
+++ b/baku-toolkit/lib/src/main/java/io/v/rx/syncbase/SingleWatchEvent.java
@@ -4,11 +4,14 @@
 
 package io.v.rx.syncbase;
 
+import com.google.common.reflect.TypeToken;
+
 import io.v.v23.services.watch.ResumeMarker;
 import io.v.v23.syncbase.nosql.ChangeType;
 import io.v.v23.syncbase.nosql.WatchChange;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
+import java8.util.function.Function;
 import lombok.Value;
 import lombok.experimental.Accessors;
 
@@ -20,18 +23,28 @@
     boolean mFromSync;
 
     @SuppressWarnings("unchecked")
-    private static <T> T getWatchValue(final WatchChange change, final T defaultValue)
-            throws VException {
+    private static <T> T getWatchValue(final WatchChange change, final TypeToken<T> tt,
+                                       final T defaultValue) throws VException {
         if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
             return defaultValue;
         } else {
-            return (T) VomUtil.decode(change.getVomValue());
+            return (T) VomUtil.decode(change.getVomValue(),
+                    tt == null? Object.class : tt.getType());
         }
     }
 
-    public static <T> SingleWatchEvent<T> fromWatchChange(final WatchChange c, final T defaultValue)
-            throws VException {
-        return new SingleWatchEvent<>(getWatchValue(c, defaultValue),
+    public static <T> SingleWatchEvent<T> fromWatchChange(
+            final WatchChange c, final TypeToken<T> tt, final T defaultValue) throws VException {
+        return new SingleWatchEvent<>(getWatchValue(c, tt, defaultValue),
                 c.getResumeMarker(), c.isFromSync());
     }
+
+    public static <T> SingleWatchEvent<T> fromWatchChange(
+            final WatchChange c, final Class<T> type, final T defaultValue) throws VException {
+        return fromWatchChange(c, TypeToken.of(type), defaultValue);
+    }
+
+    public <U> SingleWatchEvent<U> map(final Function<T, U> fn) {
+        return new SingleWatchEvent<>(fn.apply(mValue), mResumeMarker, mFromSync);
+    }
 }