Syncslides: MVP1 feature set and data flow refactoring

This CL includes the feature set for MVP1, polish, specially
around the Slideshow view, bug and performance fixes remain.

-Decks now sync between devices.
-Presentation can be drived by the presenter.
-Audience can follow a presentation or navigate
 on their own and sync back when they want.
-Toast messages for starting and joining a presentation.
-Proper back button navigation stack.
-Big overhaul of the UI rendering and data flow:
 +Introduction of an in-memory, publically deeply-immutable
  state object that rendering is purely based on.
 +Using watch to update the in-memory state after fetching
  the initial values from Syncbase.
 +Clear definitions for all the possible app actions and state.
 +This change makes the unidirectional data flow simpler to
  understand and maintain and removed many potential concurrency
  issues of the previous design.

Change-Id: I7ed07a75f56693931efa5fcdfc43ce4e6e544fa9
diff --git a/.gitignore b/.gitignore
index a752fc6..faa2bc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
 /dart/.packages
 /dart/snapshot_blob.bin
 /dart/app.flx
+/dart/build
+.atom
diff --git a/dart/FLUTTER_VERSION b/dart/FLUTTER_VERSION
index 8383605..7c3d0f3 100644
--- a/dart/FLUTTER_VERSION
+++ b/dart/FLUTTER_VERSION
@@ -1 +1 @@
-04dfa0bf87a37eaeef0d85aeff0c7ea7ca04782d
+ee8c0ad30d60ac965c243f7bf982527736d54cd8
diff --git a/dart/MOJO_VERSION b/dart/MOJO_VERSION
index df87133..a8fb06d 100644
--- a/dart/MOJO_VERSION
+++ b/dart/MOJO_VERSION
@@ -1 +1 @@
-7a690c5c0344946ed74402d61d473502bc03ad77
+08b1d8fc1e0296569b628cae2e7611988497b273
\ No newline at end of file
diff --git a/dart/Makefile b/dart/Makefile
index be5398d..5ae2d85 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -4,6 +4,10 @@
 
 ifneq ($(DEVICE_NUM), 1)
 	REUSE_FLAG := --reuse-servers
+else
+	# TODO(aghassemi): This needs to become $(DEVICE_ID) when we have a way to
+	# send the $(DEVICE_ID) to the Flutter app.
+	NAME_FLAG := --name=syncslides
 endif
 
 ifdef VLOG
@@ -15,6 +19,7 @@
 DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
 DEVICE_FLAG := --target-device $(DEVICE_ID)
 MOUNTTABLE_ADDR := /192.168.86.254:8101
+ANDROID_CREDS_DIR := /sdcard/v23creds
 
 default: run
 
@@ -45,6 +50,7 @@
 	--args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo \
 	--root-dir=$(SYNCBASE_DATA_DIR) \
 	--v23.namespace.root=$(MOUNTTABLE_ADDR) \
+	$(NAME_FLAG) \
 	$(VLOG_FLAGS)" \
 	$(DEVICE_FLAG) \
 	$(REUSE_FLAG) \
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 00911c6..47b433e 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -3,6 +3,7 @@
   - name: navigation/arrow_back
   - name: navigation/arrow_forward
   - name: content/add
+  - name: notification/sync
 assets:
   - assets/images/defaults/thumbnail.png
   - assets/images/sample_decks/vanadium/1.jpg
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 441db75..71771e3 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -2,137 +2,138 @@
 // 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/material.dart';
 import 'package:flutter/widgets.dart';
 
-import '../loaders/loader.dart';
 import '../models/all.dart' as model;
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
 import '../utils/image_provider.dart' as imageProvider;
-
 import 'slidelist.dart';
+import 'slideshow.dart';
+import 'syncslides_page.dart';
+import 'toast.dart' as toast;
+
+final GlobalKey _scaffoldKey = new GlobalKey();
 
 // DeckGridPage is the full page view of the list of decks.
-class DeckGridPage extends StatelessComponent {
-  Loader _loader = new Loader.singleton();
-  Widget build(BuildContext context) {
+class DeckGridPage extends SyncSlidesPage {
+  initState(AppState appState, AppActions appActions) {
+    appActions.stopAllPresentations();
+  }
+
+  Widget build(BuildContext context, AppState appState, AppActions appActions) {
+    List<model.Deck> decks = appState.decks.values
+        .where((DeckState d) => d.deck != null)
+        .map((d) => d.deck);
+    List<model.PresentationAdvertisement> presentations =
+        appState.presentationAdvertisements.values;
+
     return new Scaffold(
+        key: _scaffoldKey,
         toolBar: new ToolBar(center: new Text('SyncSlides')),
         floatingActionButton: new FloatingActionButton(
             child: new Icon(icon: 'content/add'), onPressed: () {
-          _loader.addDeck();
+          appActions.loadDemoDeck();
         }),
-        body: new Material(child: new DeckGrid()));
+        body: new Material(
+            child: new DeckGrid(decks, presentations, appActions)));
   }
 }
 
 // DeckGrid is scrollable grid view of decks.
-class DeckGrid extends StatefulComponent {
-  _DeckGridState createState() => new _DeckGridState();
-}
+class DeckGrid extends StatelessComponent {
+  AppActions _appActions;
+  List<model.Deck> _decks;
+  List<model.PresentationAdvertisement> _presentations;
 
-class _DeckGridState extends State<DeckGrid> {
-  Store _store = new Store.singleton();
-  List<model.Deck> _decks = new List<model.Deck>();
-  StreamSubscription _onDecksChangeSubscription;
-  StreamSubscription _onStateChangeSubscription;
-
-  void updateDecks(List<model.Deck> decks) {
-    setState(() {
-      _decks = decks;
-    });
-  }
-
-  void _rebuild(_) {
-    setState(() {});
-  }
-
-  @override
-  void initState() {
-    // Stop all active presentations when coming back to the decks grid page.
-    _store.stopAllPresentations();
-    _store.getAllDecks().then(updateDecks);
-    // Update the state whenever store tells us decks have changed.
-    _onDecksChangeSubscription = _store.onDecksChange.listen(updateDecks);
-    _onStateChangeSubscription = _store.onStateChange.listen(_rebuild);
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    // Stop listening to updates from store when component is disposed.
-    _onDecksChangeSubscription.cancel();
-    _onStateChangeSubscription.cancel();
-    super.dispose();
-  }
+  DeckGrid(this._decks, this._presentations, this._appActions);
 
   Widget build(BuildContext context) {
     List<Widget> deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck));
-    List<Widget> presentationBoxes = _store.state.livePresentations
+    List<Widget> presentationBoxes = _presentations
         .map((presentation) => _buildPresentationBox(context, presentation));
     var allBoxes = new List.from(presentationBoxes)..addAll(deckBoxes);
     var grid = new Grid(allBoxes, maxChildExtent: style.Size.thumbnailWidth);
     return new ScrollableViewport(child: grid);
   }
-}
 
-Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
-  var thumbnail =
-      new AsyncImage(provider: imageProvider.getDeckThumbnailImage(deckData));
-  // TODO(aghassemi): Add "Opened on" data.
-  var subtitleWidget =
-      new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
-  subtitleWidget = _stopWrapping(subtitleWidget);
-  var footer = _buildBoxFooter(deckData.name, subtitleWidget);
-  var box = _buildCard(deckData.key, [thumbnail, footer], () {
-    Navigator.of(context).push(new MaterialPageRoute(
-        builder: (context) => new SlideListPage(deckData.key, deckData.name)));
-  });
+  Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
+    var thumbnail =
+        new AsyncImage(provider: imageProvider.getDeckThumbnailImage(deckData));
+    // TODO(aghassemi): Add "Opened on" data.
+    var subtitleWidget =
+        new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
+    subtitleWidget = _stopWrapping(subtitleWidget);
+    var footer = _buildBoxFooter(deckData.name, subtitleWidget);
+    var box = _buildCard(deckData.key, [thumbnail, footer], () {
+      Navigator.of(context).push(new MaterialPageRoute(
+          builder: (context) => new SlideListPage(deckData.key)));
+    });
 
-  return box;
-}
+    return box;
+  }
 
-Widget _buildPresentationBox(
-    BuildContext context, model.PresentationAdvertisement presentationData) {
-  var thumbnail = new AsyncImage(
-      provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
-  var liveBox = new Row([
-    new Container(
-        child: new Text("LIVE NOW", style: style.Text.liveNow),
-        decoration: style.Box.liveNow,
-        margin: style.Spacing.normalMargin,
-        padding: style.Spacing.extraSmallPadding)
-  ]);
-  var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
-  var box = _buildCard(presentationData.key, [thumbnail, footer], () {});
-  return box;
-}
+  Widget _buildPresentationBox(
+      BuildContext context, model.PresentationAdvertisement presentationData) {
+    var thumbnail = new AsyncImage(
+        provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
+    var liveBox = new Row([
+      new Container(
+          child: new Text("LIVE NOW", style: style.Text.liveNow),
+          decoration: style.Box.liveNow,
+          margin: style.Spacing.normalMargin,
+          padding: style.Spacing.extraSmallPadding)
+    ]);
+    var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
+    var box = _buildCard(presentationData.key, [thumbnail, footer], () async {
+      toast.info(
+          _scaffoldKey, 'Joining presentation ${presentationData.deck.name}...',
+          duration: toast.Durations.permanent);
+      try {
+        await _appActions.joinPresentation(presentationData);
 
-Widget _buildBoxFooter(String title, Widget subtitle) {
-  var titleWidget = new Text(title, style: style.Text.titleStyle);
-  titleWidget = _stopWrapping(titleWidget);
+        toast.info(
+            _scaffoldKey, 'Joined presentation ${presentationData.deck.name}.');
 
-  var titleAndSubtitle = new Block([titleWidget, subtitle]);
-  return new Container(
-      child: titleAndSubtitle, padding: style.Spacing.normalPadding);
-}
+        // Push slides list page first before navigating to the slideshow.
+        Navigator.of(context).push(new MaterialPageRoute(
+            builder: (context) => new SlideListPage(presentationData.deck.key,
+                presentationId: presentationData.key)));
+        Navigator.of(context).push(new MaterialPageRoute(
+            builder: (context) => new SlideshowPage(presentationData.deck.key,
+                presentationId: presentationData.key)));
+      } catch (e) {
+        toast.error(_scaffoldKey,
+            'Failed to start presentation ${presentationData.deck.name}.', e);
+      }
+    });
+    return box;
+  }
 
-Widget _buildCard(String key, List<Widget> children, Function onTap) {
-  var content = new Container(
-      child: new Card(child: new Block(children)),
-      margin: style.Spacing.normalMargin);
+  Widget _buildBoxFooter(String title, Widget subtitle) {
+    var titleWidget = new Text(title, style: style.Text.titleStyle);
+    titleWidget = _stopWrapping(titleWidget);
 
-  return new InkWell(key: new Key(key), child: content, onTap: onTap);
-}
+    var titleAndSubtitle = new Block([titleWidget, subtitle]);
+    return new Container(
+        child: titleAndSubtitle, padding: style.Spacing.normalPadding);
+  }
 
-Widget _stopWrapping(Text child) {
-  // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
-  // overflow: hidden or text-overflow: ellipsis in Flutter yet.
-  // This workaround simulates white-space: nowrap and overflow: hidden.
-  // See https://github.com/flutter/flutter/issues/417
-  return new Viewport(
-      child: child, scrollDirection: ScrollDirection.horizontal);
+  Widget _buildCard(String key, List<Widget> children, Function onTap) {
+    var content = new Container(
+        child: new Card(child: new Block(children)),
+        margin: style.Spacing.normalMargin);
+
+    return new InkWell(key: new Key(key), child: content, onTap: onTap);
+  }
+
+  Widget _stopWrapping(Text child) {
+    // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
+    // overflow: hidden or text-overflow: ellipsis in Flutter yet.
+    // This workaround simulates white-space: nowrap and overflow: hidden.
+    // See https://github.com/flutter/flutter/issues/417
+    return new Viewport(
+        child: child, scrollDirection: ScrollDirection.horizontal);
+  }
 }
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 49ead9b..a680b35 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -2,97 +2,117 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'package:flutter/widgets.dart';
 import 'package:flutter/material.dart';
 
 import '../models/all.dart' as model;
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
 import '../utils/image_provider.dart' as imageProvider;
-
 import 'slideshow.dart';
+import 'syncslides_page.dart';
+import 'toast.dart' as toast;
 
-// SlideListPage is the full page view of the list of slides for a deck.
-class SlideListPage extends StatelessComponent {
-  final String deckId;
-  final String title;
-  Store _store = new Store.singleton();
+final GlobalKey _scaffoldKey = new GlobalKey();
 
-  SlideListPage(this.deckId, this.title);
-  Widget build(BuildContext context) {
+class SlideListPage extends SyncSlidesPage {
+  final String _deckId;
+  final String _presentationId;
+
+  SlideListPage(this._deckId, {String presentationId})
+      : _presentationId = presentationId;
+
+  Widget build(BuildContext context, AppState appState, AppActions appActions) {
+    if (!appState.decks.containsKey(_deckId)) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Deck no longer exists.');
+    }
+
+    var deckState = appState.decks[_deckId];
+    var slides = deckState.slides;
     return new Scaffold(
+        key: _scaffoldKey,
         toolBar: new ToolBar(
             left: new IconButton(
                 icon: 'navigation/arrow_back',
                 onPressed: () => Navigator.of(context).pop()),
-            center: new Text(title)),
-        floatingActionButton: _buildPresentFab(context),
-        body: new Material(child: new SlideList(deckId)));
+            center: new Text(deckState.deck.name)),
+        floatingActionButton: _buildPresentFab(context, appState, appActions),
+        body: new Material(child: new SlideList(_deckId, slides, appActions)));
   }
 
-  _buildPresentFab(BuildContext context) {
+  _buildPresentFab(
+      BuildContext context, AppState appState, AppActions appActions) {
+    bool inPresentation = _presentationId != null &&
+        appState.presentations.containsKey(_presentationId);
+    if (inPresentation) {
+      // Can't present when viewing a presentation.
+      return null;
+    }
+
+    bool alreadyAdvertised = appState.advertisedPresentations
+        .any((model.PresentationAdvertisement p) => p.deck.key == _deckId);
+    if (alreadyAdvertised) {
+      return null;
+    }
+
     return new FloatingActionButton(
         child: new Icon(icon: 'navigation/arrow_forward'), onPressed: () async {
-      model.PresentationAdvertisement presentation =
-          await _store.startPresentation(deckId);
-      Navigator.of(context).push(new MaterialPageRoute(
-          builder: (context) => new SlideshowPage(deckId)));
+      toast.info(_scaffoldKey, 'Starting presentation...',
+          duration: toast.Durations.permanent);
+
+      try {
+        var presentation = await appActions.startPresentation(_deckId);
+        toast.info(_scaffoldKey, 'Presentation started.');
+
+        Navigator.of(context).push(new MaterialPageRoute(
+            builder: (context) =>
+                new SlideshowPage(_deckId, presentationId: presentation.key)));
+      } catch (e) {
+        toast.error(_scaffoldKey, 'Failed to start presentation.', e);
+      }
     });
   }
 }
 
-// SlideList is scrollable list view of slides for a deck.
-class SlideList extends StatefulComponent {
-  final String deckId;
-  SlideList(this.deckId);
-
-  _SlideListState createState() => new _SlideListState();
-}
-
-class _SlideListState extends State<SlideList> {
-  Store _store = new Store.singleton();
+class SlideList extends StatelessComponent {
+  String _deckId;
+  String _presentationId;
   List<model.Slide> _slides = new List<model.Slide>();
-
-  void updateSlides(List<model.Slide> slides) {
-    setState(() {
-      _slides = slides;
-    });
-  }
-
-  @override
-  void initState() {
-    super.initState();
-    _store.getAllSlides(config.deckId).then(updateSlides);
-    // TODO(aghassemi): Gracefully handle when deck is deleted while in this view.
-  }
+  AppActions _appActions;
+  SlideList(this._deckId, this._slides, this._appActions,
+      {String presentationId})
+      : _presentationId = presentationId;
 
   Widget build(BuildContext context) {
+    NavigatorState navigator = Navigator.of(context);
     return new ScrollableList(
         itemExtent: style.Size.listHeight,
         items: _slides,
         itemBuilder: (context, value, index) =>
-            _buildSlide(context, config.deckId, index, index.toString(), value,
-                onTap: () {
-              _store.setCurrSlideNum(config.deckId, index);
-              Navigator.of(context).push(new MaterialPageRoute(
-                  builder: (context) => new SlideshowPage(config.deckId)));
+            _buildSlide(context, _deckId, index, value, onTap: () {
+              _appActions.setCurrSlideNum(_deckId, index,
+                  presentationId: _presentationId);
+
+              navigator.push(new MaterialPageRoute(
+                  builder: (context) => new SlideshowPage(_deckId,
+                      presentationId: _presentationId)));
             }));
   }
 }
 
-Widget _buildSlide(BuildContext context, String deckId, int slideIndex,
-    String key, model.Slide slideData,
+Widget _buildSlide(
+    BuildContext context, String deckId, int slideIndex, model.Slide slideData,
     {Function onTap}) {
   var thumbnail = new AsyncImage(
-      provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
+      provider: imageProvider.getSlideImage(deckId, slideData),
       height: style.Size.listHeight,
       fit: ImageFit.cover);
 
   thumbnail = new Flexible(child: thumbnail);
 
-  var title = new Text('Slide $key', style: style.Text.subtitleStyle);
+  var title = new Text('Slide $slideIndex', style: style.Text.subtitleStyle);
   var notes = new Text(
-      'This is the teaser slide. It should be memorable and descriptive');
+      'This is the teaser slide. It should be memorable and descriptive.');
   var titleAndNotes = new Flexible(
       child: new Container(
           child: new Column([title, notes], alignItems: FlexAlignItems.start),
@@ -102,7 +122,8 @@
       child: new Card(child: new Row([thumbnail, titleAndNotes])),
       margin: style.Spacing.listItemMargin);
 
-  var listItem = new InkWell(key: new Key(key), child: card, onTap: onTap);
+  var listItem = new InkWell(
+      key: new Key(slideIndex.toString()), child: card, onTap: onTap);
 
   return listItem;
 }
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index 700ef5b..cf301f9 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -2,100 +2,129 @@
 // 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 'package:flutter/widgets.dart';
+import 'package:logging/logging.dart';
 
 import '../models/all.dart' as model;
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
 import '../utils/image_provider.dart' as imageProvider;
+import 'syncslides_page.dart';
 
-class SlideshowPage extends StatelessComponent {
-  final String deckId;
+final Logger log = new Logger('store/syncbase_store');
 
-  SlideshowPage(this.deckId);
+class SlideshowPage extends SyncSlidesPage {
+  final String _deckId;
+  final String _presentationId;
 
-  Widget build(BuildContext context) {
+  SlideshowPage(this._deckId, {String presentationId})
+      : _presentationId = presentationId;
+
+  Widget build(BuildContext context, AppState appState, AppActions appActions) {
+    if (!appState.decks.containsKey(_deckId)) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Deck no longer exists.');
+    }
+
+    var deckState = appState.decks[_deckId];
+
     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)));
+        floatingActionButton:
+            _buildSyncUpNavigationFab(context, appState, appActions),
+        body: new Material(
+            child: new SlideShow(appActions, deckState,
+                appState.presentations[_presentationId])));
+  }
+
+  _buildSyncUpNavigationFab(
+      BuildContext context, AppState appState, AppActions appActions) {
+    if (_presentationId == null) {
+      // Not in a presentation.
+      return null;
+    }
+
+    PresentationState presentationState =
+        appState.presentations[_presentationId];
+
+    // If not navigating out of sync, do not show the sync icon.
+    if (presentationState == null || !presentationState.isNavigationOutOfSync) {
+      return null;
+    }
+
+    return new FloatingActionButton(child: new Icon(icon: 'notification/sync'),
+        onPressed: () async {
+      appActions.syncUpNavigationWithPresentation(_deckId, _presentationId);
+    });
   }
 }
 
-class SlideShow extends StatefulComponent {
-  final String deckId;
-  SlideShow(this.deckId);
+class SlideShow extends StatelessComponent {
+  AppActions _appActions;
+  DeckState _deckState;
+  PresentationState _presentationState;
 
-  _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();
-  }
+  SlideShow(this._appActions, this._deckState, this._presentationState);
 
   Widget build(BuildContext context) {
-    if (_slides == null) {
-      // TODO(aghassemi): Remove when store operations become sync.
-      return new Text('Loading');
+    if (_deckState.slides.length == 0) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('No slide to show.');
     }
-    var slideData = _slides[_currSlideNum];
+
+    int currSlideNum;
+
+    bool isFollowingPresenter =
+        _presentationState != null && !_presentationState.isNavigationOutOfSync;
+
+    if (isFollowingPresenter) {
+      currSlideNum = _presentationState.currSlideNum;
+    } else {
+      currSlideNum = _deckState.currSlideNum;
+    }
+
+    if (currSlideNum >= _deckState.slides.length) {
+      // TODO(aghassemi): Can this ever happen?
+      //  -What if slide number set by another peer is synced before the actual slides?
+      //  -What if we have navigated to a particular slide on our own and peer deletes that slide?
+      // I think without careful batching and consuming watch events as batches, this could happen
+      // maybe for a split second until rest of data syncs up.
+      // UI needs to be bullet-roof, a flicker in the UI is better than an exception and crash.
+      log.shout(
+          'Current slide number $currSlideNum is greater than the number of slides ${_deckState.slides.length}');
+
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Slide does not exist.');
+    }
+
+    var slideData = _deckState.slides[currSlideNum];
     var image = new AsyncImage(
-        provider: imageProvider.getSlideImage(
-            config.deckId, _currSlideNum, slideData),
+        provider: imageProvider.getSlideImage(_deckState.deck.key, slideData),
         fit: ImageFit.contain);
     var navWidgets = [
-      _buildSlideNav(_currSlideNum - 1),
-      _buildSlideNav(_currSlideNum + 1)
+      _buildSlideNav(currSlideNum - 1),
+      _buildSlideNav(currSlideNum + 1)
     ];
 
     return new Block(
-        [image, new Text(_currSlideNum.toString()), new Row(navWidgets)]);
+        [image, new Text(currSlideNum.toString()), new Row(navWidgets)]);
   }
 
   Widget _buildSlideNav(int slideNum) {
     var card;
 
-    if (slideNum >= 0 && slideNum < _slides.length) {
-      card = _buildThumbnailNav(config.deckId, slideNum, _slides[slideNum],
-          onTap: () {
-        _store.setCurrSlideNum(config.deckId, slideNum);
-      });
+    if (slideNum >= 0 && slideNum < _deckState.slides.length) {
+      var onTap = () => _appActions.setCurrSlideNum(
+          _deckState.deck.key, slideNum,
+          presentationId: _presentationState?.key);
+
+      card = _buildThumbnailNav(
+          _deckState.deck.key, slideNum, _deckState.slides[slideNum],
+          onTap: onTap);
     } else {
       card = new Container(
           width: style.Size.thumbnailNavWidth,
@@ -110,7 +139,7 @@
 Widget _buildThumbnailNav(String deckId, int slideIndex, model.Slide slideData,
     {Function onTap}) {
   var thumbnail = new AsyncImage(
-      provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
+      provider: imageProvider.getSlideImage(deckId, slideData),
       height: style.Size.thumbnailNavHeight,
       fit: ImageFit.cover);
 
diff --git a/dart/lib/components/syncslides_page.dart b/dart/lib/components/syncslides_page.dart
new file mode 100644
index 0000000..c6c3a25
--- /dev/null
+++ b/dart/lib/components/syncslides_page.dart
@@ -0,0 +1,50 @@
+// 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/material.dart';
+
+import '../stores/store.dart';
+
+typedef Widget SyncSlidesPageBuilder(
+    BuildContext context, AppState appState, AppActions appActions);
+
+// The base class for every page.
+// Responsible for watching state changes from the store and passing
+// the state and actions to the build function of descendant components.
+abstract class SyncSlidesPage extends StatefulComponent {
+  build(BuildContext context, AppState appState, AppActions appActions);
+  initState(AppState appState, AppActions appActions) {}
+
+  _SyncSlidesPage createState() => new _SyncSlidesPage();
+}
+
+class _SyncSlidesPage extends State<SyncSlidesPage> {
+  Store _store = new Store.singleton();
+  AppState _state;
+  StreamSubscription _onStateChangeSubscription;
+
+  void _updateState(AppState newState) {
+    setState(() => _state = newState);
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    config.initState(_store.state, _store.actions);
+    _state = _store.state;
+    _onStateChangeSubscription = _store.onStateChange.listen(_updateState);
+  }
+
+  @override
+  void dispose() {
+    _onStateChangeSubscription.cancel();
+    super.dispose();
+  }
+
+  Widget build(BuildContext context) {
+    return config.build(context, _state, _store.actions);
+  }
+}
diff --git a/dart/lib/components/toast.dart b/dart/lib/components/toast.dart
new file mode 100644
index 0000000..488601d
--- /dev/null
+++ b/dart/lib/components/toast.dart
@@ -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.
+
+import 'dart:core';
+
+import 'package:flutter/material.dart';
+
+import '../styles/common.dart' as style;
+
+class Durations {
+  static const Duration permanent = const Duration(days: 100);
+  static const Duration long = const Duration(seconds: 5);
+  static const Duration medium = kSnackBarMediumDisplayDuration;
+  static const Duration short = kSnackBarShortDisplayDuration;
+}
+
+ScaffoldFeatureController _currSnackBar;
+
+void info(GlobalKey scaffoldKey, String text,
+    {Duration duration: Durations.short}) {
+  _closePrevious();
+  _currSnackBar = scaffoldKey.currentState
+      .showSnackBar(new SnackBar(content: new Text(text), duration: duration));
+}
+
+void error(GlobalKey scaffoldKey, String text, Error err,
+    {Duration duration: Durations.long}) {
+  _closePrevious();
+  _currSnackBar = scaffoldKey.currentState.showSnackBar(new SnackBar(
+      // TODO(aghassemi): Add "Details" action to error toasts and move error text there.
+      content: new Text(text + ' - ERROR: $err', style: style.Text.error),
+      duration: duration));
+}
+
+void _closePrevious() {
+  // TODO(aghassemi): Fix this in Flutter. Currently close() throws exception
+  // if snackbar is already closed.
+  try {
+    _currSnackBar?.close();
+  } catch (e) {}
+}
diff --git a/dart/lib/config.dart b/dart/lib/config.dart
index fe5bca2..1ba0489 100644
--- a/dart/lib/config.dart
+++ b/dart/lib/config.dart
@@ -3,5 +3,6 @@
 // license that can be found in the LICENSE file.
 
 // TODO(aghassemi): Make these configurable from command line and/or UI.
-bool SyncbaseEnabled = true;
 bool DemoEnabled = true;
+
+String mounttableAddr = '/192.168.86.254:8101';
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index 52b4257..781dc6a 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -7,20 +7,15 @@
 
 import '../models/all.dart' as model;
 import '../stores/store.dart';
-import '../utils/uuid.dart' as uuidutil;
 import '../utils/asset.dart' as assetutil;
-
+import '../utils/uuid.dart' as uuidutil;
 import 'loader.dart';
 
 // DemoLoader loads some sample decks and slides and randomly adds/removes
 // decks based on a timer.
 class DemoLoader implements Loader {
-  final Store _store;
-  final Random _rand;
-
-  DemoLoader()
-      : _store = new Store.singleton(),
-        _rand = new Random();
+  final Store _store = new Store.singleton();
+  final Random _rand = new Random();
 
   static final List<String> thumbnails = [
     'assets/images/sample_decks/baku/thumb.png',
@@ -67,15 +62,17 @@
     var numSlides = _rand.nextInt(maxNumSlides);
     for (var i = 0; i < numSlides; i++) {
       var slideIndex = i % slides.length;
-      yield new model.Slide(await assetutil.getRawBytes(
-          'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
+      yield new model.Slide(
+          i,
+          await assetutil.getRawBytes(
+              'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
     }
   }
 
-  Future addDeck() async {
+  Future loadDeck() async {
     var deck = await _getRandomDeck();
     List<model.Slide> slides = await _getRandomSlides().toList();
-    await _store.addDeck(deck);
-    await _store.setSlides(deck.key, slides);
+    await _store.actions.addDeck(deck);
+    await _store.actions.setSlides(deck.key, slides);
   }
 }
diff --git a/dart/lib/loaders/loader_factory.dart b/dart/lib/loaders/factory.dart
similarity index 66%
rename from dart/lib/loaders/loader_factory.dart
rename to dart/lib/loaders/factory.dart
index 7b006f1..354dd93 100644
--- a/dart/lib/loaders/loader_factory.dart
+++ b/dart/lib/loaders/factory.dart
@@ -2,17 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import '../config.dart' as config;
-
-import 'loader.dart';
 import 'demo_loader.dart';
+import 'loader.dart';
 import 'sdcard_loader.dart';
 
 // Factory method to create a concrete loader instance.
-Loader create() {
-  if (config.DemoEnabled) {
-    return new DemoLoader();
-  } else {
-    return new SdCardLoader();
-  }
+Loader createDemoLoader() {
+  return new DemoLoader();
+}
+
+Loader createSdCardLoader() {
+  return new SdCardLoader();
 }
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
index c206b36..8113924 100644
--- a/dart/lib/loaders/loader.dart
+++ b/dart/lib/loaders/loader.dart
@@ -4,15 +4,17 @@
 
 import 'dart:async';
 
-import 'loader_factory.dart' as loaderFactory;
+import 'factory.dart' as factory;
 
 // Loader is responsible for importing existing decks and slides into the store.
 abstract class Loader {
-  static Loader _singletonLoader = loaderFactory.create();
-
-  factory Loader.singleton() {
-    return _singletonLoader;
+  factory Loader.demo() {
+    return factory.createDemoLoader();
   }
 
-  Future addDeck();
+  factory Loader.sdcard() {
+    return factory.createSdCardLoader();
+  }
+
+  Future loadDeck();
 }
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
index a2b0102..60aa269 100644
--- a/dart/lib/loaders/sdcard_loader.dart
+++ b/dart/lib/loaders/sdcard_loader.dart
@@ -7,7 +7,7 @@
 import 'loader.dart';
 
 class SdCardLoader implements Loader {
-  Future addDeck() {
+  Future loadDeck() {
     throw new UnimplementedError();
   }
 }
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
index ebb665a..e93b03a 100644
--- a/dart/lib/models/slide.dart
+++ b/dart/lib/models/slide.dart
@@ -4,20 +4,25 @@
 
 import 'dart:convert';
 
-// Slide represents an independent slide without ties to a specific deck.
+// Slide represents a slide within a deck.
 class Slide {
+  int _num;
+  int get num => _num;
+
   List<int> _image;
   List<int> get image => _image;
 
-  Slide(this._image) {}
+  Slide(this._num, this._image) {}
 
   Slide.fromJson(String json) {
     Map map = JSON.decode(json);
+    _num = map['num'];
     _image = map['image'];
   }
 
   String toJson() {
     Map map = new Map();
+    map['num'] = _num;
     map['image'] = image;
     return JSON.encode(map);
   }
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
new file mode 100644
index 0000000..093bab8
--- /dev/null
+++ b/dart/lib/stores/actions.dart
@@ -0,0 +1,48 @@
+// 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.
+
+part of store;
+
+// Defines all possible actions that the application can perform.
+abstract class AppActions {
+  //////////////////////////////////////
+  // Decks
+
+  // Adds a new deck.
+  Future addDeck(model.Deck deck);
+
+  // Removes a deck given its key.
+  Future removeDeck(String key);
+
+  // Loads a demo deck.
+  Future loadDemoDeck();
+
+  // Loads a deck from SdCard.
+  Future loadDeckFromSdCard();
+
+  // Sets the slides for a deck.
+  Future setSlides(String deckKey, List<model.Slide> slides);
+
+  // Sets the current slide number for a deck.
+  Future setCurrSlideNum(String deckId, int slideNum, {String presentationId});
+
+  //////////////////////////////////////
+  // Presentation
+
+  // Joins an advertised presentation.
+  Future joinPresentation(model.PresentationAdvertisement presentation);
+
+  // Starts a presentation.
+  Future<model.PresentationAdvertisement> startPresentation(String deckId);
+
+  // Stops the given presentation.
+  Future stopPresentation(String presentationId);
+
+  // Stops all presentations.
+  Future stopAllPresentations();
+
+  // If viewer has started navigating on their own, this will sync the navigation
+  // back up with the presentation.
+  Future syncUpNavigationWithPresentation(String deckId, String presentationId);
+}
diff --git a/dart/lib/stores/keyutil.dart b/dart/lib/stores/keyutil.dart
deleted file mode 100644
index eb543c8..0000000
--- a/dart/lib/stores/keyutil.dart
+++ /dev/null
@@ -1,42 +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.
-
-// Constructs a slide key.
-String getSlideKey(String deckId, int slideIndex) {
-  return '$deckId/slides/$slideIndex';
-}
-
-// 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
deleted file mode 100644
index bfee15f..0000000
--- a/dart/lib/stores/memory_store.dart
+++ /dev/null
@@ -1,135 +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.
-
-import 'dart:async';
-
-import '../models/all.dart' as model;
-
-import 'keyutil.dart' as keyutil;
-import 'state.dart';
-import 'store.dart';
-
-// A memory-based implementation of Store.
-class MemoryStore implements Store {
-  StreamController _onDecksChangeEmitter;
-  Map<String, String> _decksMap;
-  Map<String, String> _slidesMap;
-  Map<String, int> _currSlideNumMap;
-  Map<String, StreamController> _currSlideNumChangeEmitterMap;
-  State _state = new State();
-  StreamController _stateChangeEmitter = new StreamController.broadcast();
-
-  MemoryStore()
-      : _onDecksChangeEmitter = new StreamController.broadcast(),
-        _decksMap = new Map(),
-        _slidesMap = new Map(),
-        _currSlideNumMap = new Map(),
-        _currSlideNumChangeEmitterMap = new Map(),
-        _state = new State(),
-        _stateChangeEmitter = new StreamController.broadcast();
-
-  //////////////////////////////////////
-  // State
-
-  State get state => _state;
-  Stream get onStateChange => _stateChangeEmitter.stream;
-
-  //////////////////////////////////////
-  // Decks
-
-  Future<List<model.Deck>> getAllDecks() async {
-    var decks = [];
-    _decksMap.forEach((String key, String value) {
-      decks.add(new model.Deck.fromJson(key, value));
-    });
-
-    return decks;
-  }
-
-  Future<model.Deck> getDeck(String key) async {
-    return new model.Deck.fromJson(key, _decksMap[key]);
-  }
-
-  Future addDeck(model.Deck deck) async {
-    var json = deck.toJson();
-    _decksMap[deck.key] = json;
-    getAllDecks().then(_onDecksChangeEmitter.add);
-  }
-
-  Future removeDeck(String deckKey) async {
-    _decksMap.remove(deckKey);
-    _slidesMap.keys
-        .where((slideKey) =>
-            slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
-        .toList()
-        .forEach(_slidesMap.remove);
-    getAllDecks().then(_onDecksChangeEmitter.add);
-  }
-
-  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
-
-  //////////////////////////////////////
-  // Slides
-
-  Future<List<model.Slide>> getAllSlides(String deckKey) async {
-    var slides = [];
-    _slidesMap.keys
-        .where((slideKey) =>
-            slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
-        .forEach((String key) {
-      slides.add(new model.Slide.fromJson(_slidesMap[key]));
-    });
-    return slides;
-  }
-
-  Future setSlides(String deckKey, List<model.Slide> slides) async {
-    List<String> jsonSlides = slides.map((slide) => slide.toJson()).toList();
-    for (int i = 0; i < jsonSlides.length; i++) {
-      _slidesMap[keyutil.getSlideKey(deckKey, i)] = jsonSlides[i];
-    }
-  }
-
-  //////////////////////////////////////
-  // 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];
-  }
-
-  //////////////////////////////////////
-  // Presentation
-
-  static final Error noPresentationSupportError =
-      new UnsupportedError('MemoryStore does not support presentation.');
-
-  Future<model.PresentationAdvertisement> startPresentation(String deckId) {
-    throw noPresentationSupportError;
-  }
-
-  Future stopPresentation(String presentationId) {
-    throw noPresentationSupportError;
-  }
-
-  Future stopAllPresentations() {
-    throw noPresentationSupportError;
-  }
-}
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
index 3ecf6bd..a3afb59 100644
--- a/dart/lib/stores/state.dart
+++ b/dart/lib/stores/state.dart
@@ -2,18 +2,29 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import '../models/all.dart' as model;
+part of store;
 
-// Represents current state of the application.
-class State {
-  // TODO(aghassemi): The new store model is to have one state object
-  // and a change event instead of async getters.
-  // This model has not been implemented for decks and slides yet but
-  // we are using the new model for presentation advertisements.
+// Represents the current state of the data that the application holds.
+// Application is rendered purely based on this state.
+// State is deeply-immutable outside of store code.
+abstract class AppState {
+  UnmodifiableMapView<String, DeckState> get decks;
+  UnmodifiableMapView<String, PresentationState> get presentations;
+  UnmodifiableListView<
+      model.PresentationAdvertisement> get advertisedPresentations;
+  UnmodifiableMapView<String,
+      model.PresentationAdvertisement> get presentationAdvertisements;
+}
 
-  // TODO(aghassemi): State needs to be deeply immutable.
-  // Maybe https://github.com/google/built_value.dart can help?
-  List<model.PresentationAdvertisement> livePresentations;
+abstract class DeckState {
+  model.Deck get deck;
+  UnmodifiableListView<model.Slide> get slides;
+  int get currSlideNum;
+}
 
-  State() : livePresentations = new List();
+abstract class PresentationState {
+  String get key;
+  int get currSlideNum;
+  bool get isDriving;
+  bool get isNavigationOutOfSync;
 }
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 6c817c8..da994d5 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -2,70 +2,26 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+library store;
+
 import 'dart:async';
+import 'dart:collection';
 
 import '../models/all.dart' as model;
+import 'utils/factory.dart' as factory;
 
-import 'state.dart';
-import 'store_factory.dart' as storeFactory;
+part 'actions.dart';
+part 'state.dart';
 
-// Provides APIs for reading and writing app-related data.
+// Provides the state, actions and state change event to the application.
 abstract class Store {
-  static Store _singletonStore = storeFactory.create();
+  static Store _singletonStore = factory.create();
 
   factory Store.singleton() {
     return _singletonStore;
   }
 
-  //////////////////////////////////////
-  // State
-
-  State get state;
+  AppActions get actions;
+  AppState get state;
   Stream get onStateChange;
-
-  //////////////////////////////////////
-  // Decks
-
-  // Returns all the existing decks.
-  Future<List<model.Deck>> getAllDecks();
-
-  // Returns the deck for the given key.
-  Future<model.Deck> getDeck(String key);
-
-  // Adds a new deck.
-  Future addDeck(model.Deck deck);
-
-  // Removes a deck given its key.
-  Future removeDeck(String key);
-
-  // Event that fires when deck are added or removed.
-  // The up-to-date list of decks with be sent to listeners.
-  Stream<List<model.Deck>> get onDecksChange;
-
-  //////////////////////////////////////
-  // Slides
-
-  // Returns the list of all slides for a deck.
-  Future<List<model.Slide>> getAllSlides(String deckKey);
-
-  // 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);
-
-  //////////////////////////////////////
-  // Presentation
-
-  Future<model.PresentationAdvertisement> startPresentation(String deckId);
-
-  Future stopPresentation(String presentationId);
-
-  Future stopAllPresentations();
 }
diff --git a/dart/lib/stores/store_factory.dart b/dart/lib/stores/store_factory.dart
deleted file mode 100644
index c8cc53e..0000000
--- a/dart/lib/stores/store_factory.dart
+++ /dev/null
@@ -1,18 +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.
-
-import '../config.dart' as config;
-
-import 'store.dart';
-import 'memory_store.dart';
-import 'syncbase_store.dart';
-
-// Factory method to create a concrete store instance.
-Store create() {
-  if (config.SyncbaseEnabled) {
-    return new SyncbaseStore();
-  } else {
-    return new MemoryStore();
-  }
-}
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
new file mode 100644
index 0000000..2fe3428
--- /dev/null
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -0,0 +1,205 @@
+// 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.
+
+part of syncbase_store;
+
+class _AppActions extends AppActions {
+  _AppState _state;
+  Function _emitChange;
+  _AppActions(this._state, this._emitChange);
+
+  //////////////////////////////////////
+  // Decks
+
+  Future addDeck(model.Deck deck) async {
+    log.info("Adding deck ${deck.name}...");
+    sb.SyncbaseTable tb = await _getDecksTable();
+    await tb.put(deck.key, UTF8.encode(deck.toJson()));
+    log.info("Deck ${deck.name} added.");
+  }
+
+  Future removeDeck(String deckKey) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    tb.deleteRange(new sb.RowRange.prefix(deckKey));
+  }
+
+  Future setSlides(String deckKey, List<model.Slide> slides) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+
+    slides.forEach((slide) async {
+      // TODO(aghassemi): Use batching.
+      await tb.put(
+          keyutil.getSlideKey(deckKey, slide.num), UTF8.encode(slide.toJson()));
+    });
+  }
+
+  Future setCurrSlideNum(String deckId, int slideNum,
+      {String presentationId}) async {
+    var deckState = _state._getOrCreateDeckState(deckId);
+    if (slideNum < 0 || slideNum >= deckState.slides.length) {
+      throw new ArgumentError.value(slideNum,
+          "Slide number out of range. Only ${deckState.slides.length} slides exist.");
+    }
+
+    // TODO(aghassemi): Have a session table to persist UI state such as
+    // local slide number within a deck, whether user is navigating a presentation
+    // on their own, and similar UI state that is desirable to be persisted.
+    deckState._currSlideNum = slideNum;
+
+    // Is slide number change happening within a presentation?
+    if (presentationId != null) {
+      if (!_state.presentations.containsKey(presentationId)) {
+        throw new ArgumentError.value(
+            presentationId, "Presentation does not exist.");
+      }
+
+      _PresentationState presentationState =
+          _state.presentations[presentationId];
+
+      // Is the current user driving the presentation?
+      if (presentationState.isDriving) {
+        // Update the common slide number for the presentation.
+        sb.SyncbaseTable tb = await _getPresentationsTable();
+        await tb.put(
+            keyutil.getPresentationCurrSlideNumKey(deckId, presentationId),
+            [slideNum]);
+      } else {
+        // User is not driving the presentation so they are navigating on their own.
+        presentationState._isNavigationOutOfSync = true;
+      }
+    }
+    _emitChange();
+  }
+
+  Future loadDemoDeck() {
+    return new Loader.demo().loadDeck();
+  }
+
+  Future loadDeckFromSdCard() {
+    return new Loader.demo().loadDeck();
+  }
+
+  //////////////////////////////////////
+  // Presentation
+
+  Future<model.PresentationAdvertisement> startPresentation(
+      String deckId) async {
+    var alreadyAdvertised = _state._advertisedPresentations
+        .any((model.PresentationAdvertisement p) => p.deck.key == deckId);
+    if (alreadyAdvertised) {
+      throw new ArgumentError.value(deckId,
+          'Cannot simultaneously present the same deck. Presentation already in progress.');
+    }
+
+    if (!_state._decks.containsKey(deckId)) {
+      throw new ArgumentError.value(deckId, 'Deck no longer exists.');
+    }
+
+    String uuid = uuidutil.createUuid();
+    String syncgroupName = keyutil.getPresentationSyncgroupName(uuid);
+
+    model.Deck deck = _state._getOrCreateDeckState(deckId)._deck;
+    var presentation =
+        new model.PresentationAdvertisement(uuid, deck, syncgroupName);
+
+    await sb.createSyncgroup(syncgroupName, [
+      sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId),
+      sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId)
+    ]);
+
+    await discovery.advertise(presentation);
+    _state._advertisedPresentations.add(presentation);
+
+    await joinPresentation(presentation);
+
+    return presentation;
+  }
+
+  Future joinPresentation(model.PresentationAdvertisement presentation) async {
+    bool isMyOwnPresentation =
+        _state._advertisedPresentations.any((p) => p.key == presentation.key);
+
+    if (!isMyOwnPresentation) {
+      await sb.joinSyncgroup(presentation.syncgroupName);
+      String deckId = presentation.deck.key;
+      Completer completer = new Completer();
+
+      // Wait until at least the slide for current page number is synced.
+      new Timer.periodic(new Duration(milliseconds: 30), (Timer timer) {
+        if (_state._decks.containsKey(deckId) &&
+            _state._decks[deckId].deck != null &&
+            _state._decks[deckId].slides.length >
+                _state._decks[deckId].currSlideNum &&
+            !completer.isCompleted) {
+          timer.cancel();
+          completer.complete();
+        }
+      });
+      await completer.future.timeout(new Duration(seconds: 20));
+    }
+
+    _PresentationState presentationState =
+        _state._getOrCreatePresentationState(presentation.key);
+
+    // TODO(aghassemi): For now, only the presenter can drive. Later when we have
+    // identity and delegation support, this will change to: if "driver == me".
+    presentationState._isDriving = isMyOwnPresentation;
+
+    log.info('Joined presentation ${presentation.key}');
+  }
+
+  Future stopPresentation(String presentationId) async {
+    await discovery.stopAdvertising(presentationId);
+    _state._advertisedPresentations.removeWhere((p) => p.key == presentationId);
+    _state._presentations.remove(presentationId);
+    log.info('Presentation $presentationId stopped');
+  }
+
+  Future stopAllPresentations() async {
+    // Stop all presentations in parallel.
+    return Future.wait(_state._advertisedPresentations
+        .map((model.PresentationAdvertisement p) {
+      return stopPresentation(p.key);
+    }));
+  }
+
+  Future syncUpNavigationWithPresentation(
+      String deckId, String presentationId) async {
+    if (!_state.presentations.containsKey(presentationId)) {
+      throw new ArgumentError.value(
+          presentationId, "Presentation does not exist.");
+    }
+
+    _PresentationState presentationState = _state.presentations[presentationId];
+
+    // Set the current slide number to the presentation's current slide number.
+    await setCurrSlideNum(deckId, presentationState.currSlideNum);
+
+    presentationState._isNavigationOutOfSync = false;
+  }
+}
+
+//////////////////////////////////////
+// Utilities
+
+Future<sb.SyncbaseTable> _getTable(String tableName) async {
+  sb.SyncbaseDatabase sbDb = await sb.getDatabase();
+  sb.SyncbaseTable tb = sbDb.table(tableName);
+  try {
+    await tb.create(sb.createOpenPerms());
+  } catch (e) {
+    if (!errorsutil.isExistsError(e)) {
+      throw e;
+    }
+  }
+  return tb;
+}
+
+Future<sb.SyncbaseTable> _getDecksTable() {
+  return _getTable(decksTableName);
+}
+
+Future<sb.SyncbaseTable> _getPresentationsTable() {
+  return _getTable(presentationsTableName);
+}
diff --git a/dart/lib/stores/syncbase/consts.dart b/dart/lib/stores/syncbase/consts.dart
new file mode 100644
index 0000000..0e4955a
--- /dev/null
+++ b/dart/lib/stores/syncbase/consts.dart
@@ -0,0 +1,9 @@
+// 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.
+
+part of syncbase_store;
+
+const String decksTableName = 'decks';
+const String presentationsTableName = 'presentations';
+final Logger log = new Logger('store/syncbase_store');
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
new file mode 100644
index 0000000..f2206b6
--- /dev/null
+++ b/dart/lib/stores/syncbase/state.dart
@@ -0,0 +1,69 @@
+// 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.
+
+part of syncbase_store;
+
+class _AppState implements AppState {
+  List<model.PresentationAdvertisement> advertisedPresentations;
+  Map<String, model.PresentationAdvertisement> presentationAdvertisements;
+  Map<String, DeckState> decks;
+  Map<String, PresentationState> presentations;
+
+  _AppState() {
+    presentationAdvertisements =
+        new UnmodifiableMapView(_presentationsAdvertisements);
+    decks = new UnmodifiableMapView(_decks);
+    presentations = new UnmodifiableMapView(_presentations);
+    advertisedPresentations =
+        new UnmodifiableListView(_advertisedPresentations);
+  }
+
+  Map<String, model.PresentationAdvertisement> _presentationsAdvertisements =
+      new Map();
+  Map<String, _DeckState> _decks = new Map();
+  Map<String, _PresentationState> _presentations = new Map();
+  List<model.PresentationAdvertisement> _advertisedPresentations = new List();
+
+  _DeckState _getOrCreateDeckState(String deckId) {
+    return _decks.putIfAbsent(deckId, () {
+      return new _DeckState();
+    });
+  }
+
+  _PresentationState _getOrCreatePresentationState(String presentationId) {
+    return _presentations.putIfAbsent(presentationId, () {
+      return new _PresentationState(presentationId);
+    });
+  }
+}
+
+class _DeckState implements DeckState {
+  model.Deck _deck;
+  model.Deck get deck => _deck;
+
+  List<model.Slide> _slides = new List();
+  List<model.Slide> slides;
+
+  int _currSlideNum = 0;
+  int get currSlideNum => _currSlideNum;
+
+  _DeckState() {
+    slides = new UnmodifiableListView(_slides);
+  }
+}
+
+class _PresentationState implements PresentationState {
+  final String key;
+
+  int _currSlideNum = 0;
+  int get currSlideNum => _currSlideNum;
+
+  bool _isDriving = false;
+  bool get isDriving => _isDriving;
+
+  bool _isNavigationOutOfSync = false;
+  bool get isNavigationOutOfSync => _isNavigationOutOfSync;
+
+  _PresentationState(this.key);
+}
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
new file mode 100644
index 0000000..4095383
--- /dev/null
+++ b/dart/lib/stores/syncbase/store.dart
@@ -0,0 +1,168 @@
+// 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.
+
+library syncbase_store;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:logging/logging.dart';
+
+import '../../discovery/client.dart' as discovery;
+import '../../loaders/loader.dart';
+import '../../models/all.dart' as model;
+import '../../syncbase/client.dart' as sb;
+import '../../utils/errors.dart' as errorsutil;
+import '../../utils/uuid.dart' as uuidutil;
+import '../store.dart';
+import '../utils/key.dart' as keyutil;
+
+part 'actions.dart';
+part 'state.dart';
+part 'consts.dart';
+
+// Implementation of Store using Syncbase (http://v.io/syncbase) storage system.
+class SyncbaseStore implements Store {
+  _AppState _state;
+  _AppActions _actions;
+  StreamController _stateChangeEmitter = new StreamController.broadcast();
+
+  AppState get state => _state;
+  Stream get onStateChange => _stateChangeEmitter.stream;
+  AppActions get actions => _actions;
+
+  SyncbaseStore() {
+    _state = new _AppState();
+    _actions = new _AppActions(_state, _triggerStateChange);
+
+    sb.getDatabase().then((sb.SyncbaseDatabase db) async {
+      // Make sure all table exists.
+      await _ensureTablesExist();
+
+      // TODO(aghassemi): Use the multi-table scan and watch API when ready.
+      // See https://github.com/vanadium/issues/issues/923
+      for (String table in [decksTableName, presentationsTableName]) {
+        _getInitialValuesAndStartWatching(db, table);
+      }
+      _startScanningForPresentations();
+    });
+  }
+
+  // Note(aghassemi): We could have copied the state to provide a snapshot at the time of
+  // change, however for this application we did not choose to do so because:
+  //  1- The state is already publically deeply-immutable, so only store code can mutate it.
+  //  2- Although there is a chance by the time UI rerenders due to a change event, the state
+  //     may have changed causing UI to skip rendering the intermediate states, it is not an
+  //     undesirable behaviour for SyncSlides. This behaviour may not be acceptable for different
+  //     categories of apps that require continuity of rendering, such as games.
+  void _triggerStateChange() => _stateChangeEmitter.add(_state);
+
+  Future _startScanningForPresentations() async {
+    discovery.onFound.listen((model.PresentationAdvertisement newP) {
+      _state._presentationsAdvertisements[newP.key] = newP;
+      _triggerStateChange();
+    });
+
+    discovery.onLost.listen((String pId) {
+      _state._presentationsAdvertisements.remove(pId);
+      _triggerStateChange();
+    });
+
+    discovery.startScan();
+  }
+
+  Future _getInitialValuesAndStartWatching(
+      sb.SyncbaseDatabase sbDb, String table) async {
+    // TODO(aghassemi): Ideally we wouldn't need an initial query and can configure
+    // watch to give both initial values and future changes.
+    // See https://github.com/vanadium/issues/issues/917
+    var batchDb =
+        await sbDb.beginBatch(sb.SyncbaseClient.batchOptions(readOnly: true));
+    var resumeMarker = await batchDb.getResumeMarker();
+
+    // Get initial values in a batch.
+    String query = 'SELECT k, v FROM $table';
+    Stream<sb.Result> results = batchDb.exec(query);
+    // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
+    results.skip(1).forEach((sb.Result result) => _onChange(
+        table,
+        sb.WatchChangeTypes.put,
+        UTF8.decode(result.values[0]),
+        result.values[1]));
+
+    await batchDb.abort();
+
+    // Start watching from batch's resume marker.
+    var stream = sbDb.watch(table, '', resumeMarker);
+    stream.listen((sb.WatchChange change) =>
+        _onChange(table, change.changeType, change.rowKey, change.valueBytes));
+  }
+
+  _onChange(String table, int changeType, String rowKey, List<int> value) {
+    log.finest('Change in $table for $rowKey of the type $changeType.');
+
+    keyutil.KeyType keyType = keyutil.getKeyType(rowKey);
+    switch (keyType) {
+      case keyutil.KeyType.Deck:
+        _onDeckChange(changeType, rowKey, value);
+        break;
+      case keyutil.KeyType.Slide:
+        _onSlideChange(changeType, rowKey, value);
+        break;
+      case keyutil.KeyType.PresentationCurrSlideNum:
+        _onPresentationSlideNumChange(changeType, rowKey, value);
+        break;
+      case keyutil.KeyType.Unknown:
+        log.severe('Got change for $rowKey with an unknown key type.');
+    }
+    _triggerStateChange();
+  }
+
+  _onDeckChange(int changeType, String rowKey, List<int> value) {
+    var deckId = rowKey;
+    if (changeType == sb.WatchChangeTypes.put) {
+      _state._getOrCreateDeckState(deckId)._deck =
+          new model.Deck.fromJson(deckId, UTF8.decode(value));
+    } else if (changeType == sb.WatchChangeTypes.delete) {
+      _state.decks.remove(deckId);
+    }
+  }
+
+  _onSlideChange(int changeType, String rowKey, List<int> value) {
+    var deckId = keyutil.currSlideKeyToDeckId(rowKey);
+    var index = keyutil.currSlideKeyToIndex(rowKey);
+    var slides = _state._getOrCreateDeckState(deckId)._slides;
+    if (changeType == sb.WatchChangeTypes.put) {
+      var slide = new model.Slide.fromJson(UTF8.decode(value));
+      slides.add(slide);
+    } else if (changeType == sb.WatchChangeTypes.delete) {
+      slides.removeWhere((s) => s.num == index);
+    }
+
+    // Keep the slides sorted by number.
+    slides.sort((model.Slide a, model.Slide b) {
+      return a.num.compareTo(b.num);
+    });
+  }
+
+  _onPresentationSlideNumChange(
+      int changeType, String rowKey, List<int> value) {
+    var presentationId =
+        keyutil.presentationCurrSlideNumKeyToPresentationId(rowKey);
+    var presentationState =
+        _state._getOrCreatePresentationState(presentationId);
+    if (changeType == sb.WatchChangeTypes.put) {
+      int currSlideNum = value[0];
+      presentationState._currSlideNum = currSlideNum;
+    } else {
+      presentationState._currSlideNum = 0;
+    }
+  }
+
+  Future _ensureTablesExist() async {
+    await _getDecksTable();
+    await _getPresentationsTable();
+  }
+}
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
deleted file mode 100644
index e6d6ddf..0000000
--- a/dart/lib/stores/syncbase_store.dart
+++ /dev/null
@@ -1,247 +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.
-
-import 'dart:async';
-import 'dart:convert';
-
-import '../discovery/client.dart' as discovery;
-import '../models/all.dart' as model;
-import '../syncbase/client.dart' as sb;
-import '../utils/errors.dart' as errorsutil;
-import '../utils/uuid.dart' as uuidutil;
-
-import 'keyutil.dart' as keyutil;
-import 'state.dart';
-import 'store.dart';
-
-const String decksTableName = 'Decks';
-
-// Implementation of  using Syncbase (http://v.io/syncbase) storage system.
-class SyncbaseStore implements Store {
-  StreamController _deckChangeEmitter;
-  Map<String, StreamController> _currSlideNumChangeEmitterMap;
-  List<model.PresentationAdvertisement> _advertisedPresentations;
-  State _state = new State();
-  StreamController _stateChangeEmitter = new StreamController.broadcast();
-
-  SyncbaseStore() {
-    _deckChangeEmitter = new StreamController.broadcast();
-    _currSlideNumChangeEmitterMap = new Map();
-    _advertisedPresentations = new List();
-    _state = new State();
-    _stateChangeEmitter = new StreamController.broadcast();
-    sb.getDatabase().then((db) {
-      _startDecksWatch(db);
-      _startScanningForPresentations();
-    });
-  }
-
-  //////////////////////////////////////
-  // State
-
-  State get state => _state;
-  Stream get onStateChange => _stateChangeEmitter.stream;
-  void _triggerStateChange() => _stateChangeEmitter.add(_state);
-
-  //////////////////////////////////////
-  // Decks
-
-  Future<List<model.Deck>> getAllDecks() async {
-    // Key schema is:
-    // <deckId> --> Deck
-    // <deckId>/slides/1 --> Slide
-    // So we scan for keys that don't have /
-    // Ideally this would become a query based on Type when there is VOM/VDL
-    // support in Dart and we store typed objects instead of JSON bytes.
-    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
-    String query = 'SELECT k, v FROM $decksTableName WHERE k NOT LIKE "%/%"';
-    Stream<sb.Result> results = sbDb.exec(query);
-    // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
-
-    return results.skip(1).map((result) => _toDeck(result.values)).toList();
-  }
-
-  // Return the deck for the given key or null if it does not exist.
-  Future<model.Deck> getDeck(String key) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
-    var value = await tb.get(key);
-    return new model.Deck.fromJson(key, UTF8.decode(value));
-  }
-
-  Future addDeck(model.Deck deck) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
-    tb.put(deck.key, UTF8.encode(deck.toJson()));
-  }
-
-  Future removeDeck(String deckKey) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
-    // Delete deck and all of its slides.
-    tb.deleteRange(new sb.RowRange.prefix(deckKey));
-  }
-
-  Stream<List<model.Deck>> get onDecksChange => _deckChangeEmitter.stream;
-
-  model.Deck _toDeck(List<List<int>> row) {
-    var key = UTF8.decode(row[0]);
-    var value = UTF8.decode(row[1]);
-    return new model.Deck.fromJson(key, value);
-  }
-
-  Future<sb.SyncbaseTable> _getDecksTable() async {
-    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
-    sb.SyncbaseTable tb = sbDb.table(decksTableName);
-    try {
-      await tb.create(sb.createOpenPerms());
-    } catch (e) {
-      if (!errorsutil.isExistsError(e)) {
-        throw e;
-      }
-    }
-    return tb;
-  }
-
-  Future _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
-    var resumeMarker = await sbDb.getResumeMarker();
-    var stream = sbDb.watch(decksTableName, '', resumeMarker);
-
-    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.
-        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);
-          }
-        }
-      }
-    });
-  }
-
-  //////////////////////////////////////
-  // Slides
-
-  Future<List<model.Slide>> getAllSlides(String deckKey) async {
-    // Key schema is:
-    // <deckId> --> Deck
-    // <deckId>/slides/1 --> Slide
-    // 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.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();
-  }
-
-  Future setSlides(String deckKey, List<model.Slide> slides) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
-
-    for (var i = 0; i < slides.length; i++) {
-      var slide = slides[i];
-      // TODO(aghassemi): Use batching when support is added.
-      await tb.put(
-          keyutil.getSlideKey(deckKey, i), UTF8.encode(slide.toJson()));
-    }
-  }
-
-  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();
-    String key = keyutil.getCurrSlideNumKey(deckId);
-    // TODO(aghassemi): Run exist and get in a batch.
-    if (await tb.row(key).exists()) {
-      return 0;
-    }
-    var v = await tb.get(key);
-    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];
-  }
-
-  //////////////////////////////////////
-  // Presentation
-
-  Future<model.PresentationAdvertisement> startPresentation(
-      String deckId) async {
-    var alreadyAdvertised =
-        _advertisedPresentations.any((p) => p.deck.key == deckId);
-    if (alreadyAdvertised) {
-      throw new ArgumentError(
-          'Cannot simultaneously present the same deck. Presentation already in progress for $deckId.');
-    }
-
-    model.Deck deck = await this.getDeck(deckId);
-    String uuid = uuidutil.createUuid();
-    String syncgroupName = '';
-    var presentation =
-        new model.PresentationAdvertisement(uuid, deck, syncgroupName);
-
-    await discovery.advertise(presentation);
-    _advertisedPresentations.add(presentation);
-
-    return presentation;
-  }
-
-  Future stopPresentation(String presentationId) async {
-    await discovery.stopAdvertising(presentationId);
-    _advertisedPresentations.removeWhere((p) => p.key == presentationId);
-  }
-
-  Future stopAllPresentations() async {
-    // Stop all presentations in parallel.
-    return Future.wait(
-        _advertisedPresentations.map((model.PresentationAdvertisement p) {
-      return stopPresentation(p.key);
-    }));
-  }
-
-  Future _startScanningForPresentations() async {
-    discovery.onFound.listen((model.PresentationAdvertisement newP) {
-      state.livePresentations.add(newP);
-      _triggerStateChange();
-    });
-
-    discovery.onLost.listen((String pId) {
-      state.livePresentations.removeWhere((p) => p.key == pId);
-      _triggerStateChange();
-    });
-
-    discovery.startScan();
-  }
-}
diff --git a/dart/lib/stores/utils/factory.dart b/dart/lib/stores/utils/factory.dart
new file mode 100644
index 0000000..b9d4122
--- /dev/null
+++ b/dart/lib/stores/utils/factory.dart
@@ -0,0 +1,11 @@
+// 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 '../store.dart';
+import '../syncbase/store.dart';
+
+// Factory method to create a concrete store instance.
+Store create() {
+  return new SyncbaseStore();
+}
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
new file mode 100644
index 0000000..033368d
--- /dev/null
+++ b/dart/lib/stores/utils/key.dart
@@ -0,0 +1,95 @@
+// 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 '../../config.dart' as config;
+
+enum KeyType { Deck, Slide, PresentationCurrSlideNum, Unknown }
+
+KeyType getKeyType(String key) {
+  if (isDeckKey(key)) {
+    return KeyType.Deck;
+  } else if (isSlideKey(key)) {
+    return KeyType.Slide;
+  } else if (isPresentationCurrSlideNumKey(key)) {
+    return KeyType.PresentationCurrSlideNum;
+  } else {
+    return KeyType.Unknown;
+  }
+}
+
+// Constructs a slide key.
+String getSlideKey(String deckId, int slideIndex) {
+  return '$deckId/slides/$slideIndex';
+}
+
+// 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('/');
+}
+
+// Returns true if a key is for a slide.
+bool isSlideKey(String key) {
+  return key.contains('/slides/');
+}
+
+// Gets the deck id given a slide key.
+String currSlideKeyToDeckId(String key) {
+  if ((!isSlideKey(key))) {
+    throw new ArgumentError("$key is not a valid slide key.");
+  }
+  return key.substring(0, key.indexOf('/slides/'));
+}
+
+// Gets the slide index given a slide key.
+int currSlideKeyToIndex(String key) {
+  if ((!isSlideKey(key))) {
+    throw new ArgumentError("$key is not a valid slide key.");
+  }
+  var indexStr = key.substring(key.lastIndexOf('/') + 1);
+  return int.parse(indexStr);
+}
+
+// TODO(aghassemi): Don't use regex, just regular split should be fine.
+const String _uuidPattern = '[a-zA-Z0-9-]+';
+final RegExp _currPresentationSlideNumPattern =
+    new RegExp('(?:$_uuidPattern/)($_uuidPattern)(?:/currentslide)');
+
+// Constructs a current slide number key.
+String getPresentationCurrSlideNumKey(String deckId, String presentationId) {
+  return '$deckId/$presentationId/currentslide';
+}
+
+// Gets the presentation id given a current slide number key.
+String presentationCurrSlideNumKeyToPresentationId(String currSlideNumKey) {
+  if ((!isPresentationCurrSlideNumKey(currSlideNumKey))) {
+    throw new ArgumentError(
+        "$currSlideNumKey is not a valid presentation current slide number key.");
+  }
+  return _currPresentationSlideNumPattern.firstMatch(currSlideNumKey).group(1);
+}
+
+// Returns true if a key is a current slide number key.
+bool isPresentationCurrSlideNumKey(String key) {
+  return _currPresentationSlideNumPattern.hasMatch(key);
+}
+
+// Constructs the Syncgroup name for a presentation.
+String getPresentationSyncgroupName(String presentationId) {
+  // TODO(aghassemi): Currently we are assuming the first device
+  // mounts itself under the name 'syncslides' and then every other device
+  // creates the Syncgroup on the first device.
+  // We should have each device mount itself under a unique name and
+  // to create the Syncgroup on their own instance.
+  return '${config.mounttableAddr}/syncslides/%%sync/$presentationId';
+}
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index d275af0..ccbef24 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -6,11 +6,13 @@
 
 class Text {
   static final Color secondaryTextColor = Colors.grey[500];
+  static final Color errorTextColor = Colors.red[500];
   static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
   static final TextStyle subtitleStyle =
       new TextStyle(fontSize: 12.0, color: secondaryTextColor);
   static final TextStyle liveNow =
       new TextStyle(fontSize: 12.0, color: theme.accentColor);
+  static final TextStyle error = new TextStyle(color: errorTextColor);
 }
 
 class Size {
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index 4a25a22..9424099 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -5,21 +5,24 @@
 import 'dart:async';
 
 import 'package:flutter/services.dart' show shell;
+import 'package:logging/logging.dart';
 import 'package:syncbase/syncbase_client.dart';
 
+import '../config.dart' as config;
 import '../utils/errors.dart' as errorsutil;
 
 export 'package:syncbase/syncbase_client.dart';
 
+final Logger log = new Logger('syncbase/client');
+
 const String syncbaseMojoUrl =
     'https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo';
 const appName = 'syncslides';
 const dbName = 'syncslides';
 
-SyncbaseNoSqlDatabase _db;
-
+SyncbaseDatabase _db;
 // Returns the database handle for the SyncSlides app.
-Future<SyncbaseNoSqlDatabase> getDatabase() async {
+Future<SyncbaseDatabase> getDatabase() async {
   if (_db != null) {
     return _db;
   }
@@ -33,6 +36,26 @@
   return _db;
 }
 
+Future createSyncgroup(String syncgroupName, prefixes) async {
+  SyncbaseDatabase sbDb = await getDatabase();
+  SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+  var sgSpec = SyncbaseClient.syncgroupSpec(prefixes,
+      perms: createOpenPerms(), mountTables: [config.mounttableAddr]);
+  var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
+
+  await sg.create(sgSpec, myInfo);
+  log.info('Created syncgroup $syncgroupName');
+}
+
+Future joinSyncgroup(String syncgroupName) async {
+  SyncbaseDatabase sbDb = await getDatabase();
+  SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+  var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
+
+  await sg.join(myInfo);
+  log.info('Joined syncgroup $syncgroupName');
+}
+
 Future<SyncbaseApp> _createApp(SyncbaseClient sbClient) async {
   var app = sbClient.app(appName);
   try {
@@ -46,7 +69,7 @@
   return app;
 }
 
-Future<SyncbaseNoSqlDatabase> _createDb(SyncbaseApp app) async {
+Future<SyncbaseDatabase> _createDb(SyncbaseApp app) async {
   var db = app.noSqlDatabase(dbName);
   try {
     await db.create(createOpenPerms());
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
index abaeb9b..a716b07 100644
--- a/dart/lib/utils/image_provider.dart
+++ b/dart/lib/utils/image_provider.dart
@@ -14,8 +14,8 @@
   return new _RawImageProvider('thumbnail_${deck.key}', deck.thumbnail);
 }
 
-ImageProvider getSlideImage(String deckId, int slideIndex, model.Slide slide) {
-  return new _RawImageProvider('slide_${deckId}_$slideIndex', slide.image);
+ImageProvider getSlideImage(String deckId, model.Slide slide) {
+  return new _RawImageProvider('slide_${deckId}_$slide.num', slide.image);
 }
 
 class _RawImageProvider implements ImageProvider {
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 49d5397..cfed1e8 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -186,11 +186,11 @@
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.57"
+    version: "0.0.58"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.57"
+    version: "0.0.58"
   source_map_stack_trace:
     description: source_map_stack_trace
     source: hosted
@@ -214,7 +214,7 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.14"
+    version: "0.0.15"
   test:
     description: test
     source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 86d198c..9a6892e 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -5,7 +5,7 @@
     path: "../../../../../flutter/packages/flutter"
   logging: ">=0.11.2 <0.12.0"
   mojo_services: ">=0.4.5 <0.5.0"
-  syncbase: ">=0.0.9 <0.1.0"
+  syncbase: ">=0.0.15 <0.1.0"
   v23discovery: ">=0.0.4 < 0.1.0"
   uuid: ">=0.5.0 <0.6.0"
 dev_dependencies: