Cache images using ImageProvider interface added to Flutter.
As key, use an identifier consisting of deckId and slideIndex
(for slides.)

Change-Id: I96c5541412f82dd49c564f8433ce6977078d9a5f
diff --git a/dart/FLUTTER_VERSION b/dart/FLUTTER_VERSION
index fb36760..8383605 100644
--- a/dart/FLUTTER_VERSION
+++ b/dart/FLUTTER_VERSION
@@ -1 +1 @@
-a35b214d891efaeddfb4b4270ebe222dfab16a8a
+04dfa0bf87a37eaeef0d85aeff0c7ea7ca04782d
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index f092be9..441db75 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -11,6 +11,7 @@
 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';
 
@@ -79,7 +80,8 @@
 }
 
 Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
-  var thumbnail = _buildThumbnail(deckData.thumbnail);
+  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);
@@ -95,7 +97,8 @@
 
 Widget _buildPresentationBox(
     BuildContext context, model.PresentationAdvertisement presentationData) {
-  var thumbnail = _buildThumbnail(presentationData.deck.thumbnail);
+  var thumbnail = new AsyncImage(
+      provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
   var liveBox = new Row([
     new Container(
         child: new Text("LIVE NOW", style: style.Text.liveNow),
@@ -108,20 +111,6 @@
   return box;
 }
 
-// TODO(aghassemi): Cache will be moved to Flutter and will become a map of encoded bytes to
-// decoded bytes. This is just a quick work-around for now.
-Map<int, RawImage> thumbnailCache = new Map();
-Widget _buildThumbnail(List<int> bytes) {
-  var key = bytes.hashCode;
-  if (thumbnailCache.containsKey(key)) {
-    return thumbnailCache[key];
-  }
-  var thumbnail = new RawImage(bytes: new Uint8List.fromList(bytes));
-
-  thumbnailCache[key] = thumbnail;
-  return thumbnail;
-}
-
 Widget _buildBoxFooter(String title, Widget subtitle) {
   var titleWidget = new Text(title, style: style.Text.titleStyle);
   titleWidget = _stopWrapping(titleWidget);
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index e27dbc7..49ead9b 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -8,6 +8,7 @@
 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';
 
@@ -70,7 +71,8 @@
         itemExtent: style.Size.listHeight,
         items: _slides,
         itemBuilder: (context, value, index) =>
-            _buildSlide(context, index.toString(), value, onTap: () {
+            _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)));
@@ -78,21 +80,12 @@
   }
 }
 
-// TODO(aghassemi): Is this approach okay? Check with Flutter team.
-// 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,
+Widget _buildSlide(BuildContext context, String deckId, int slideIndex,
+    String key, model.Slide slideData,
     {Function onTap}) {
-  var cachedWidget = _weakSlideCache[slideData];
-  if (cachedWidget != null) {
-    return cachedWidget;
-  }
-
-  var thumbnail = new RawImage(
+  var thumbnail = new AsyncImage(
+      provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
       height: style.Size.listHeight,
-      bytes: new Uint8List.fromList(slideData.image),
       fit: ImageFit.cover);
 
   thumbnail = new Flexible(child: thumbnail);
@@ -111,6 +104,5 @@
 
   var listItem = new InkWell(key: new Key(key), child: card, onTap: onTap);
 
-  _weakSlideCache[slideData] = listItem;
   return listItem;
 }
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index 284a6f8..700ef5b 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -10,6 +10,7 @@
 import '../models/all.dart' as model;
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
+import '../utils/image_provider.dart' as imageProvider;
 
 class SlideshowPage extends StatelessComponent {
   final String deckId;
@@ -74,8 +75,10 @@
       return new Text('Loading');
     }
     var slideData = _slides[_currSlideNum];
-    var image = new RawImage(
-        bytes: new Uint8List.fromList(slideData.image), fit: ImageFit.contain);
+    var image = new AsyncImage(
+        provider: imageProvider.getSlideImage(
+            config.deckId, _currSlideNum, slideData),
+        fit: ImageFit.contain);
     var navWidgets = [
       _buildSlideNav(_currSlideNum - 1),
       _buildSlideNav(_currSlideNum + 1)
@@ -89,7 +92,8 @@
     var card;
 
     if (slideNum >= 0 && slideNum < _slides.length) {
-      card = _buildThumbnailNav(_slides[slideNum], onTap: () {
+      card = _buildThumbnailNav(config.deckId, slideNum, _slides[slideNum],
+          onTap: () {
         _store.setCurrSlideNum(config.deckId, slideNum);
       });
     } else {
@@ -103,10 +107,11 @@
   }
 }
 
-Widget _buildThumbnailNav(model.Slide slideData, {Function onTap}) {
-  var thumbnail = new RawImage(
+Widget _buildThumbnailNav(String deckId, int slideIndex, model.Slide slideData,
+    {Function onTap}) {
+  var thumbnail = new AsyncImage(
+      provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
       height: style.Size.thumbnailNavHeight,
-      bytes: new Uint8List.fromList(slideData.image),
       fit: ImageFit.cover);
 
   return new InkWell(child: thumbnail, onTap: onTap);
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
new file mode 100644
index 0000000..abaeb9b
--- /dev/null
+++ b/dart/lib/utils/image_provider.dart
@@ -0,0 +1,35 @@
+// 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:typed_data' show Uint8List;
+import 'dart:ui' as ui;
+
+import 'package:flutter/services.dart';
+
+import '../models/all.dart' as model;
+
+ImageProvider getDeckThumbnailImage(model.Deck deck) {
+  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);
+}
+
+class _RawImageProvider implements ImageProvider {
+  final String imageKey;
+  final List<int> imageData;
+
+  _RawImageProvider(this.imageKey, this.imageData);
+
+  Future<ui.Image> loadImage() async {
+    return await decodeImageFromList(new Uint8List.fromList(imageData));
+  }
+
+  bool operator ==(other) =>
+      other is _RawImageProvider && imageKey == other.imageKey;
+  int get hashCode => imageKey.hashCode;
+  String toString() => imageKey;
+}
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index f3021f6..49d5397 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -186,11 +186,11 @@
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.56"
+    version: "0.0.57"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.56"
+    version: "0.0.57"
   source_map_stack_trace:
     description: source_map_stack_trace
     source: hosted