syncslides: Basic SlideShow view.

-memory and syncbase APIs for current slide number.
-basic slideshow view with next/prev buttons.
-start of setup for syncing slidenum by allowing
two devices to run at the same time.

Change-Id: I1c5761fbddb726ebb7db13e76f485cd1f7c4894c
diff --git a/dart/Makefile b/dart/Makefile
index d8381d3..88eef82 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -1,3 +1,16 @@
+ifdef DEVICE_NUM
+
+ifneq ($(DEVICE_NUM), 1)
+	REUSE_FLAG := --reuse-server
+endif
+
+DEVICE_NUM_PLUS_ONE := $(shell echo $(DEVICE_NUM) \+ 1 | bc)
+DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
+DEVICE_FLAG := --target-device $(DEVICE_ID)
+
+endif
+
+
 
 default: run
 
@@ -16,8 +29,21 @@
 upgrade-packages:
 	pub upgrade
 
+# Usage example:
+# DEVICE_NUM=1 make run
+# DEVICE_NUM=2 make run
 run: packages
-	pub run sky_tools build && pub run sky_tools run_mojo --mojo-path $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --android --mojo-debug -- --enable-multiprocess --map-origin="https://syncslides.mojo.v.io/=$(PWD)" --args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo --v=1 --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" --no-config-file --free-host-ports
+	pub run sky_tools build && pub run sky_tools run_mojo --mojo-path $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --android --mojo-debug -- --enable-multiprocess --map-origin="https://syncslides.mojo.v.io/=$(PWD)" --args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo --v=1 --v23.namespace.root=/ns.dev.v.io:8101 $(NAME_FLAG) --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" $(DEVICE_FLAG) --no-config-file $(REUSE_FLAG)
+
+# Helper targets
+run1:
+	DEVICE_NUM=1 make run
+run2:
+	DEVICE_NUM=2 make run
+run3:
+	DEVICE_NUM=3 make run
+run4:
+	DEVICE_NUM=4 make run
 
 .PHONY: clean
 clean:
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 5ed43ab..410dad5 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -63,20 +63,15 @@
 // TODO(aghassemi): Is this approach okay? Check with Flutter team.
 // Building RawImage is expensive, so we cache.
 // Expando is a weak map so this does not effect GC.
-Expando<Widget> weakDeckItemCache = new Expando<Widget>();
+Expando<Widget> _weakDeckItemCache = new Expando<Widget>();
 Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
-  var cachedWidget = weakDeckItemCache[deckData];
+  var cachedWidget = _weakDeckItemCache[deckData];
   if (cachedWidget != null) {
     return cachedWidget;
   }
 
-  var thumbnail;
-  if (deckData.thumbnail != null) {
-    thumbnail = new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
-  } else {
-    // TODO(aghassemi): Replace with a proper default thumbnail.
-    thumbnail = new Text('No Thumbnail Image');
-  }
+  var thumbnail =
+      new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
 
   var title = new Text(deckData.name, style: style.Text.titleStyle);
   var titleAndActions =
@@ -91,6 +86,6 @@
         builder: (context) => new SlideListPage(deckData.key, deckData.name)));
   });
 
-  weakSlideCache[deckData] = gridItem;
+  _weakDeckItemCache[deckData] = gridItem;
   return gridItem;
 }
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 6cfbd68..49cb4c5 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -11,20 +11,22 @@
 
 import '../utils/keyvalue.dart';
 
+import 'slideshow.dart';
+
 // SlideListPage is the full page view of the list of slides for a deck.
 class SlideListPage extends StatelessComponent {
-  String _deckId;
-  String _title;
+  final String deckId;
+  final String title;
 
-  SlideListPage(this._deckId, this._title);
+  SlideListPage(this.deckId, this.title);
   Widget build(BuildContext context) {
     return new Scaffold(
         toolBar: new ToolBar(
             left: new IconButton(
                 icon: 'navigation/arrow_back',
                 onPressed: () => Navigator.of(context).pop()),
-            center: new Text(_title)),
-        body: new Material(child: new SlideList(_deckId)));
+            center: new Text(title)),
+        body: new Material(child: new SlideList(deckId)));
   }
 }
 
@@ -37,7 +39,6 @@
 }
 
 class _SlideListState extends State<SlideList> {
-  _SlideListState();
   Store _store = new Store.singleton();
   List<model.Slide> _slides = new List<model.Slide>();
 
@@ -47,21 +48,28 @@
     });
   }
 
+  @override
   void initState() {
     super.initState();
     _store.getAllSlides(config.deckId).then(updateSlides);
+    // TODO(aghassemi): Gracefully handle when deck is deleted while in this view.
   }
 
   Widget build(BuildContext context) {
     // Create a list of <SlideNumber, Slide> pairs.
-    List<KeyValue<String, model.Slide>> slidesWithPosition = [];
+    List<KeyValue<int, model.Slide>> slidesWithPosition = [];
     for (var i = 0; i < _slides.length; i++) {
-      slidesWithPosition.add(new KeyValue(i.toString(), _slides[i]));
+      slidesWithPosition.add(new KeyValue(i, _slides[i]));
     }
     return new ScrollableList(
         itemExtent: style.Size.listHeight,
         items: slidesWithPosition,
-        itemBuilder: (context, kv) => _buildSlide(context, kv.key, kv.value));
+        itemBuilder: (context, kv) =>
+            _buildSlide(context, kv.key.toString(), kv.value, onTap: () {
+              _store.setCurrSlideNum(config.deckId, kv.key);
+              Navigator.of(context).push(new PageRoute(
+                  builder: (context) => new SlideshowPage(config.deckId)));
+            }));
   }
 }
 
@@ -69,23 +77,19 @@
 // Builder gets called a lot by the ScrollableList and building RawImage
 // is expensive so we cache.
 // Expando is a weak map so this does not effect GC.
-Expando<Widget> weakSlideCache = new Expando<Widget>();
-Widget _buildSlide(BuildContext context, String key, model.Slide slideData) {
-  var cachedWidget = weakSlideCache[slideData];
+Expando<Widget> _weakSlideCache = new Expando<Widget>();
+Widget _buildSlide(BuildContext context, String key, model.Slide slideData,
+    {Function onTap}) {
+  var cachedWidget = _weakSlideCache[slideData];
   if (cachedWidget != null) {
     return cachedWidget;
   }
 
-  var thumbnail;
-  if (slideData.image != null) {
-    thumbnail = new RawImage(
-        height: style.Size.listHeight,
-        bytes: new Uint8List.fromList(slideData.image),
-        fit: ImageFit.cover);
-  } else {
-    // TODO(aghassemi): Replace with a proper default thumbnail.
-    thumbnail = new Text('No Slide Image');
-  }
+  var thumbnail = new RawImage(
+      height: style.Size.listHeight,
+      bytes: new Uint8List.fromList(slideData.image),
+      fit: ImageFit.cover);
+
   thumbnail = new Flexible(child: thumbnail);
 
   var title = new Text('Slide $key', style: style.Text.subTitleStyle);
@@ -100,8 +104,8 @@
       child: new Card(child: new Row([thumbnail, titleAndNotes])),
       margin: style.Spacing.listItemMargin);
 
-  var listItem = new InkWell(key: new Key(key), child: card);
+  var listItem = new InkWell(key: new Key(key), child: card, onTap: onTap);
 
-  weakSlideCache[slideData] = listItem;
+  _weakSlideCache[slideData] = listItem;
   return listItem;
 }
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
new file mode 100644
index 0000000..1c48abc
--- /dev/null
+++ b/dart/lib/components/slideshow.dart
@@ -0,0 +1,92 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+
+class SlideshowPage extends StatelessComponent {
+  final String deckId;
+
+  SlideshowPage(this.deckId);
+
+  Widget build(BuildContext context) {
+    return new Scaffold(
+        toolBar: new ToolBar(
+            left: new IconButton(
+                icon: 'navigation/arrow_back',
+                onPressed: () => Navigator.of(context).pop())),
+        body: new Material(child: new SlideShow(deckId)));
+  }
+}
+
+class SlideShow extends StatefulComponent {
+  final String deckId;
+  SlideShow(this.deckId);
+
+  _SlideShowState createState() => new _SlideShowState();
+}
+
+class _SlideShowState extends State<SlideShow> {
+  Store _store = new Store.singleton();
+  List<model.Slide> _slides;
+  int _currSlideNum = 0;
+  StreamSubscription _onChangeSubscription;
+
+  void updateSlides(List<model.Slide> slides) {
+    setState(() {
+      _slides = slides;
+    });
+  }
+
+  void updateCurrSlideNum(int newCurr) {
+    setState(() {
+      _currSlideNum = newCurr;
+    });
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _store.getAllSlides(config.deckId).then(updateSlides);
+    _store.getCurrSlideNum(config.deckId).then(updateCurrSlideNum);
+    _onChangeSubscription =
+        _store.onCurrSlideNumChange(config.deckId).listen(updateCurrSlideNum);
+    // TODO(aghassemi): Gracefully handle when deck is deleted during Slideshow
+  }
+
+  @override
+  void dispose() {
+    // Stop listening to updates from store when component is disposed.
+    _onChangeSubscription.cancel();
+    super.dispose();
+  }
+
+  Widget build(BuildContext context) {
+    if (_slides == null) {
+      // TODO(aghassemi): Remove when store operations become sync.
+      return new Text('Loading');
+    }
+    var slideData = _slides[_currSlideNum];
+    var image = new RawImage(
+        bytes: new Uint8List.fromList(slideData.image), fit: ImageFit.contain);
+
+    return new Block([
+      image,
+      new Text(_currSlideNum.toString()),
+      new Row([
+        new FlatButton(child: new Text("Prev"), onPressed: () {
+          _store.setCurrSlideNum(config.deckId, _currSlideNum - 1);
+        }),
+        new FlatButton(child: new Text("Next"), onPressed: () {
+          _store.setCurrSlideNum(config.deckId, _currSlideNum + 1);
+        })
+      ])
+    ]);
+  }
+}
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index ad0a089..94d0243 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -27,10 +27,10 @@
   static const int numDeckSets = 2;
   Stream<model.Deck> _getSampleDecks() async* {
     for (var i = 1; i <= numDeckSets; i++) {
-      yield new model.Deck('pitch$i', 'Pitch Deck #$i',
-          await _getRawBytes('assets/images/sample_decks/pitch/thumb.png'));
       yield new model.Deck('baku$i', 'Baku Discovery Discussion #$i',
           await _getRawBytes('assets/images/sample_decks/baku/thumb.png'));
+      yield new model.Deck('pitch$i', 'Pitch Deck #$i',
+          await _getRawBytes('assets/images/sample_decks/pitch/thumb.png'));
       yield new model.Deck('vanadium$i', 'Vanadium #$i',
           await _getRawBytes('assets/images/sample_decks/vanadium/thumb.png'));
     }
@@ -58,7 +58,11 @@
       var removeDeck = _rand.nextBool();
 
       if (removeDeck && decks.length > 0) {
-        _store.removeDeck(decks[_rand.nextInt(decks.length)].key);
+        var rIndex = _rand.nextInt(decks.length);
+        // Never delete the first deck so we can safely use it for slideshow
+        if (rIndex >= 1) {
+          _store.removeDeck(decks[rIndex].key);
+        }
       } else {
         await for (var deck in _getSampleDecks()) {
           if (!deckKeys.contains(deck.key)) {
diff --git a/dart/lib/models/deck.dart b/dart/lib/models/deck.dart
index 6e389f1..133dc04 100644
--- a/dart/lib/models/deck.dart
+++ b/dart/lib/models/deck.dart
@@ -31,4 +31,6 @@
     map['thumbnail'] = thumbnail;
     return JSON.encode(map);
   }
+
+  // TODO(aghassemi): Override == and hash
 }
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
index ec24128..ebb665a 100644
--- a/dart/lib/models/slide.dart
+++ b/dart/lib/models/slide.dart
@@ -21,4 +21,6 @@
     map['image'] = image;
     return JSON.encode(map);
   }
+
+  // TODO(aghassemi): Override == and hash
 }
diff --git a/dart/lib/stores/keyutil.dart b/dart/lib/stores/keyutil.dart
index d2cd08b..eb543c8 100644
--- a/dart/lib/stores/keyutil.dart
+++ b/dart/lib/stores/keyutil.dart
@@ -7,12 +7,36 @@
   return '$deckId/slides/$slideIndex';
 }
 
-// Constructs prefix key for a deck.
-String getDeckKeyPrefix(String deckKey) {
-  return deckKey + '/';
+// Constructs a key prefix for all slides of a deck.
+String getSlidesKeyPrefix(String deckId) {
+  return getDeckKeyPrefix(deckId) + 'slides/';
+}
+
+// Constructs a key prefix for a deck.
+String getDeckKeyPrefix(String deckId) {
+  return deckId + '/';
 }
 
 // Returns true if a key is for a deck.
 bool isDeckKey(String key) {
   return !key.contains('/');
 }
+
+// Constructs a current slide number key.
+String getCurrSlideNumKey(String deckId) {
+  return '$deckId/currslidenum';
+}
+
+// Gets the deck id given a current slide number key.
+String currSlideNumKeyToDeckId(String currSlideNumKey) {
+  if ((!isCurrSlideNumKey(currSlideNumKey))) {
+    throw new ArgumentError(
+        "$currSlideNumKey is not a valid current slide number key.");
+  }
+  return currSlideNumKey.substring(0, currSlideNumKey.indexOf('/currslidenum'));
+}
+
+// Returns true if a key is a current slide number key.
+bool isCurrSlideNumKey(String key) {
+  return key.endsWith('/currslidenum');
+}
diff --git a/dart/lib/stores/memory_store.dart b/dart/lib/stores/memory_store.dart
index 5c092c0..aa80f74 100644
--- a/dart/lib/stores/memory_store.dart
+++ b/dart/lib/stores/memory_store.dart
@@ -11,14 +11,21 @@
 
 // A memory-based implementation of Store.
 class MemoryStore implements Store {
-  StreamController _onDecksChangeController;
+  StreamController _onDecksChangeEmitter;
   Map<String, String> _decksMap;
   Map<String, String> _slidesMap;
+  Map<String, int> _currSlideNumMap;
+  Map<String, StreamController> _currSlideNumChangeEmitterMap;
 
   MemoryStore()
-      : _onDecksChangeController = new StreamController.broadcast(),
+      : _onDecksChangeEmitter = new StreamController.broadcast(),
         _decksMap = new Map(),
-        _slidesMap = new Map();
+        _slidesMap = new Map(),
+        _currSlideNumMap = new Map(),
+        _currSlideNumChangeEmitterMap = new Map();
+
+  //////////////////////////////////////
+  /// Decks
 
   Future<List<model.Deck>> getAllDecks() async {
     var decks = [];
@@ -32,7 +39,7 @@
   Future addDeck(model.Deck deck) async {
     var json = deck.toJson();
     _decksMap[deck.key] = json;
-    getAllDecks().then(_triggerDecksChangeEvent);
+    getAllDecks().then(_onDecksChangeEmitter.add);
   }
 
   Future removeDeck(String deckKey) async {
@@ -42,10 +49,13 @@
             slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
         .toList()
         .forEach(_slidesMap.remove);
-    getAllDecks().then(_triggerDecksChangeEvent);
+    getAllDecks().then(_onDecksChangeEmitter.add);
   }
 
-  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
+
+  //////////////////////////////////////
+  /// Slides
 
   Future<List<model.Slide>> getAllSlides(String deckKey) async {
     var slides = [];
@@ -65,7 +75,28 @@
     }
   }
 
-  _triggerDecksChangeEvent(List<model.Deck> decks) {
-    _onDecksChangeController.add(decks);
+  //////////////////////////////////////
+  // Slideshow
+
+  Future<int> getCurrSlideNum(String deckId) async {
+    return _currSlideNumMap[deckId] ?? 0;
+  }
+
+  Future setCurrSlideNum(String deckId, int slideNum) async {
+    var slides = await getAllSlides(deckId);
+    if (slideNum >= 0 && slideNum < slides.length) {
+      _currSlideNumMap[deckId] = slideNum;
+      _getCurrSlideNumChangeEmitter(deckId).add(slideNum);
+    }
+  }
+
+  Stream<int> onCurrSlideNumChange(String deckId) {
+    return _getCurrSlideNumChangeEmitter(deckId).stream;
+  }
+
+  StreamController _getCurrSlideNumChangeEmitter(String deckId) {
+    _currSlideNumChangeEmitterMap.putIfAbsent(
+        deckId, () => new StreamController.broadcast());
+    return _currSlideNumChangeEmitterMap[deckId];
   }
 }
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 4dec8b5..06eadfd 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -8,6 +8,13 @@
 
 import 'store_factory.dart' as storeFactory;
 
+// TODO(aghassemi): Make all store operation synchronous.
+// Current pattern of components needing to call async methods and keep and
+// update their own state is already becoming messy. When store becomes
+// synchronous, then these components can simply use _store.getSlides(),
+// _store.getCurrSlide(), etc.. directly in their renderer and do not need to
+// keep any state of their own.
+
 // Provides APIs for reading and writing app-related data.
 abstract class Store {
   static Store _singletonStore;
@@ -43,4 +50,13 @@
 
   // Sets the slides for a deck.
   Future setSlides(String deckKey, List<model.Slide> slides);
+
+  //////////////////////////////////////
+  // Slideshow
+
+  Future<int> getCurrSlideNum(String deckId);
+
+  Future setCurrSlideNum(String deckId, int slideNum);
+
+  Stream<int> onCurrSlideNumChange(String deckId);
 }
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
index 5777bec..2bfd564 100644
--- a/dart/lib/stores/syncbase_store.dart
+++ b/dart/lib/stores/syncbase_store.dart
@@ -7,6 +7,7 @@
 
 import '../models/all.dart' as model;
 import '../syncbase/client.dart' as sb;
+import '../utils/errors.dart' as errorsutil;
 
 import 'keyutil.dart' as keyutil;
 import 'store.dart';
@@ -15,14 +16,21 @@
 
 // Implementation of  using Syncbase (http://v.io/syncbase) storage system.
 class SyncbaseStore implements Store {
-  StreamController _onDecksChangeController;
+  StreamController _deckChangeEmitter;
+  Map<String, StreamController> _currSlideNumChangeEmitterMap;
+
   SyncbaseStore() {
-    _onDecksChangeController = new StreamController.broadcast();
-    _onDecksChangeController.onListen = () {
-      sb.getDatabase().then(_startDecksWatch);
-    };
+    _deckChangeEmitter = new StreamController.broadcast();
+    _currSlideNumChangeEmitterMap = new Map();
+    sb.getDatabase().then((db) {
+      _startDecksWatch(db);
+      _startSync(db);
+    });
   }
 
+  //////////////////////////////////////
+  /// Decks
+
   Future<List<model.Deck>> getAllDecks() async {
     // Key schema is:
     // <deckId> --> Deck
@@ -48,7 +56,19 @@
     tb.deleteRange(new sb.RowRange.prefix(deckKey));
   }
 
-  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+  Stream<List<model.Deck>> get onDecksChange => _deckChangeEmitter.stream;
+
+  model.Deck _toDeck(List<List<int>> row) {
+    // TODO(aghassemi): Keys return from queries seems to have double quotes
+    // around them.
+    // See https://github.com/vanadium/issues/issues/860
+    var key = UTF8.decode(row[0]).replaceAll('"', '');
+    var value = UTF8.decode(row[1]);
+    return new model.Deck.fromJson(key, value);
+  }
+
+  //////////////////////////////////////
+  /// Slides
 
   Future<List<model.Slide>> getAllSlides(String deckKey) async {
     // Key schema is:
@@ -57,7 +77,7 @@
     // So we scan for keys that start with $deckKey/
     // Ideally this would have been a query based on Type but that is not supported yet.
     sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
-    String prefix = keyutil.getDeckKeyPrefix(deckKey);
+    String prefix = keyutil.getSlidesKeyPrefix(deckKey);
     String query = 'SELECT k, v FROM $decksTableName WHERE k LIKE "$prefix%"';
     Stream results = sbDb.exec(query);
     return results.skip(1).map((result) => _toSlide(result.values)).toList();
@@ -74,13 +94,53 @@
     }
   }
 
+  model.Slide _toSlide(List<List<int>> row) {
+    var value = UTF8.decode(row[1]);
+    return new model.Slide.fromJson(value);
+  }
+
+  //////////////////////////////////////
+  // Slideshow
+
+  Future<int> getCurrSlideNum(String deckId) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    var v = await tb.get(keyutil.getCurrSlideNumKey(deckId));
+    if (v == null || v.isEmpty) {
+      return 0;
+    }
+    return v[0];
+  }
+
+  Future setCurrSlideNum(String deckId, int slideNum) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    var slides = await getAllSlides(deckId);
+    if (slideNum >= 0 && slideNum < slides.length) {
+      // TODO(aghassemi): Move outside of decks table and into a schema just for
+      // storing UI state.
+      await tb.put(keyutil.getCurrSlideNumKey(deckId), [slideNum]);
+    }
+  }
+
+  Stream<int> onCurrSlideNumChange(String deckId) {
+    return _getCurrSlideNumChangeEmitter(deckId).stream;
+  }
+
+  StreamController _getCurrSlideNumChangeEmitter(String deckId) {
+    _currSlideNumChangeEmitterMap.putIfAbsent(
+        deckId, () => new StreamController.broadcast());
+    return _currSlideNumChangeEmitterMap[deckId];
+  }
+
   Future<sb.SyncbaseTable> _getDecksTable() async {
     sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
     sb.SyncbaseTable tb = sbDb.table(decksTableName);
-    if (await tb.exists()) {
-      return tb;
+    try {
+      await tb.create(sb.createOpenPerms());
+    } catch (e) {
+      if (!errorsutil.isExistsError(e)) {
+        throw e;
+      }
     }
-    await tb.create(sb.createOpenPerms());
     return tb;
   }
 
@@ -88,36 +148,28 @@
     var resumeMarker = await sbDb.getResumeMarker();
     var stream = sbDb.watch(decksTableName, '', resumeMarker);
 
-    var streamListener = stream.listen((sb.WatchChange change) async {
+    stream.listen((sb.WatchChange change) async {
       if (keyutil.isDeckKey(change.rowKey)) {
         // TODO(aghassemi): Maybe manipulate an in-memory list based on watch
         // changes instead of getting the decks again from Syncbase.
-        var decks = await getAllDecks();
-        _onDecksChangeController.add(decks);
+        if (!_deckChangeEmitter.isPaused || !_deckChangeEmitter.isClosed) {
+          var decks = await getAllDecks();
+          _deckChangeEmitter.add(decks);
+        }
+      } else if (keyutil.isCurrSlideNumKey(change.rowKey)) {
+        var deckId = keyutil.currSlideNumKeyToDeckId(change.rowKey);
+        var emitter = _getCurrSlideNumChangeEmitter(deckId);
+        if (!emitter.isPaused || !emitter.isClosed) {
+          if (change.changeType == sb.WatchChangeTypes.put) {
+            int currSlideNum = change.valueBytes[0];
+            emitter.add(currSlideNum);
+          } else {
+            emitter.add(0);
+          }
+        }
       }
     });
-
-    // TODO(aghassemi): Currently we can not cancel a watch, only pause it.
-    // Since watch stream supports blocking flow control, it is not a big deal
-    // but ideally we can fully cancel a watch instead of enduing up with many
-    // paused watches.
-    // Also this issue becomes irrelevant if we do the TODO above regarding
-    // keeping and manipulating an in-memory list based on watch.
-    // https://github.com/vanadium/issues/issues/833
-    _onDecksChangeController.onCancel = () => streamListener.pause();
   }
 
-  model.Deck _toDeck(List<List<int>> row) {
-    // TODO(aghassemi): Keys return from queries seems to have double quotes
-    // around them.
-    // See https://github.com/vanadium/issues/issues/860
-    var key = UTF8.decode(row[0]).replaceAll('"', '');
-    var value = UTF8.decode(row[1]);
-    return new model.Deck.fromJson(key, value);
-  }
-
-  model.Slide _toSlide(List<List<int>> row) {
-    var value = UTF8.decode(row[1]);
-    return new model.Slide.fromJson(value);
-  }
+  Future _startSync(sb.SyncbaseNoSqlDatabase sbDb) async {}
 }
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index b64ad7f..4a25a22 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -7,6 +7,8 @@
 import 'package:flutter/services.dart' show shell;
 import 'package:syncbase/syncbase_client.dart';
 
+import '../utils/errors.dart' as errorsutil;
+
 export 'package:syncbase/syncbase_client.dart';
 
 const String syncbaseMojoUrl =
@@ -33,19 +35,27 @@
 
 Future<SyncbaseApp> _createApp(SyncbaseClient sbClient) async {
   var app = sbClient.app(appName);
-  if (await app.exists()) {
-    return app;
+  try {
+    await app.create(createOpenPerms());
+  } catch (e) {
+    if (!errorsutil.isExistsError(e)) {
+      throw e;
+    }
   }
-  await app.create(createOpenPerms());
+
   return app;
 }
 
 Future<SyncbaseNoSqlDatabase> _createDb(SyncbaseApp app) async {
   var db = app.noSqlDatabase(dbName);
-  if (await db.exists()) {
-    return db;
+  try {
+    await db.create(createOpenPerms());
+  } catch (e) {
+    if (!errorsutil.isExistsError(e)) {
+      throw e;
+    }
   }
-  await db.create(createOpenPerms());
+
   return db;
 }
 
diff --git a/dart/lib/utils/errors.dart b/dart/lib/utils/errors.dart
new file mode 100644
index 0000000..a9add77
--- /dev/null
+++ b/dart/lib/utils/errors.dart
@@ -0,0 +1,10 @@
+// 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.
+
+const String existsErrorId = 'v.io/v23/verror.Exist';
+
+// TODO(aghassemi): Export mojo.Error in Syncbase and use the type here.
+bool isExistsError(e) {
+  return e != null && (e.id == existsErrorId);
+}