From 53442cbf1480811d6c28a94fba2bddfb9457b4ec Mon Sep 17 00:00:00 2001 From: Vishnu Mohandas Date: Sat, 28 Mar 2020 23:48:27 +0530 Subject: [PATCH] Add some amount of caching --- lib/core/lru_map.dart | 20 ++++----- lib/main.dart | 60 +++++++-------------------- lib/models/synced_photo.dart | 0 lib/photo_loader.dart | 22 +++++++++- lib/photo_sync_manager.dart | 3 ++ lib/ui/detail_page.dart | 11 +++-- lib/ui/gallery.dart | 68 ++++++++++++++++++++++++++++++ lib/ui/gallery_page.dart | 6 +-- lib/ui/image_item_widget.dart | 73 -------------------------------- lib/ui/image_widget.dart | 78 +++++++++++++++++++++++++++++++++++ 10 files changed, 203 insertions(+), 138 deletions(-) delete mode 100644 lib/models/synced_photo.dart create mode 100644 lib/ui/gallery.dart delete mode 100644 lib/ui/image_item_widget.dart create mode 100644 lib/ui/image_widget.dart diff --git a/lib/core/lru_map.dart b/lib/core/lru_map.dart index ceccb2237..999d73035 100644 --- a/lib/core/lru_map.dart +++ b/lib/core/lru_map.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:typed_data'; -import 'package:photo_manager/photo_manager.dart'; +import 'package:flutter/material.dart'; typedef EvictionHandler(K key, V value); @@ -38,31 +38,31 @@ class LRUMap { } class ImageLruCache { - static LRUMap<_ImageCacheEntity, Uint8List> _map = LRUMap(500); + static LRUMap<_ImageCacheEntity, Image> _map = LRUMap(500); - static Uint8List getData(AssetEntity entity, [int size = 64]) { - return _map.get(_ImageCacheEntity(entity, size)); + static Image getData(String path, [int size = 64]) { + return _map.get(_ImageCacheEntity(path, size)); } - static void setData(AssetEntity entity, int size, Uint8List list) { - _map.put(_ImageCacheEntity(entity, size), list); + static void setData(String path, int size, Image image) { + _map.put(_ImageCacheEntity(path, size), image); } } class _ImageCacheEntity { - AssetEntity entity; + String path; int size; - _ImageCacheEntity(this.entity, this.size); + _ImageCacheEntity(this.path, this.size); @override bool operator ==(Object other) => identical(this, other) || other is _ImageCacheEntity && runtimeType == other.runtimeType && - entity == other.entity && + path == other.path && size == other.size; @override - int get hashCode => entity.hashCode ^ size.hashCode; + int get hashCode => path.hashCode ^ size.hashCode; } diff --git a/lib/main.dart b/lib/main.dart index 88ec3a325..ace9a85f1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; @@ -6,7 +5,7 @@ import 'package:myapp/models/photo.dart'; import 'package:myapp/photo_loader.dart'; import 'package:myapp/photo_provider.dart'; import 'package:myapp/photo_sync_manager.dart'; -import 'package:myapp/ui/detail_page.dart'; +import 'package:myapp/ui/gallery.dart'; import 'package:myapp/ui/loading_widget.dart'; import 'package:provider/provider.dart'; import 'package:myapp/ui/gallery_page.dart'; @@ -19,7 +18,7 @@ void main() async { await provider.refreshGalleryList(); var assets = await provider.list[0].assetList; var photoSyncManager = PhotoSyncManager(assets); - await photoSyncManager.init(); + photoSyncManager.init(); runApp(MyApp2()); } @@ -50,55 +49,24 @@ class MyApp2 extends StatelessWidget { builder: (context, snapshot) { Widget body; if (snapshot.hasData) { - body = ChangeNotifierProvider.value( - value: photoLoader, - child: GridView.builder( - itemBuilder: _buildItem, - itemCount: snapshot.data.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4), - )); + body = Gallery(); } else if (snapshot.hasError) { body = Text("Error!"); } else { body = loadWidget; } - return MaterialApp( - title: title, - theme: ThemeData.dark(), - home: Scaffold( - appBar: AppBar( - title: Text(title), - ), - body: body), + return ChangeNotifierProvider.value( + value: photoLoader, + child: MaterialApp( + title: title, + theme: ThemeData.dark(), + home: Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: body), + ), ); }); } - - Widget _buildItem(BuildContext context, int index) { - logger.i("Building item"); - var file = File(photoLoader.getPhotos()[index].localPath); - return GestureDetector( - onTap: () async { - routeToDetailPage(file, context); - }, - child: Hero( - child: Image.file(file), - tag: 'photo_' + file.path, - ), - ); - } - - void routeToDetailPage(File file, BuildContext context) async { - final page = DetailPage( - file: file, - ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return page; - }, - ), - ); - } } diff --git a/lib/models/synced_photo.dart b/lib/models/synced_photo.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/photo_loader.dart b/lib/photo_loader.dart index ca0ab95a6..28de39084 100644 --- a/lib/photo_loader.dart +++ b/lib/photo_loader.dart @@ -1,11 +1,15 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; import 'package:myapp/db/db_helper.dart'; import 'package:myapp/models/photo.dart'; +import 'package:photo_manager/photo_manager.dart'; class PhotoLoader extends ChangeNotifier { final logger = Logger(); - List _photos; + final _photos = List(); + final _assetMap = Map(); PhotoLoader._privateConstructor(); static final PhotoLoader instance = PhotoLoader._privateConstructor(); @@ -16,7 +20,9 @@ class PhotoLoader extends ChangeNotifier { Future> loadPhotos() async { DatabaseHelper db = DatabaseHelper.instance; - _photos = await db.getAllPhotos(); + var photos = await db.getAllPhotos(); + _photos.clear(); + _photos.addAll(photos); logger.i("Imported photo size: " + _photos.length.toString()); return _photos; } @@ -26,4 +32,16 @@ class PhotoLoader extends ChangeNotifier { logger.i("Reloading..."); notifyListeners(); } + + void addAsset(String path, AssetEntity asset) { + _assetMap[path] = asset; + } + + Future getThumbnail(String path, int size) { + if (!_assetMap.containsKey(path)) { + logger.w("No thumbnail"); + return Future.value(null); + } + return _assetMap[path].thumbDataWithSize(size, size); + } } \ No newline at end of file diff --git a/lib/photo_sync_manager.dart b/lib/photo_sync_manager.dart index 9ef37e642..3ef7a4830 100644 --- a/lib/photo_sync_manager.dart +++ b/lib/photo_sync_manager.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:logger/logger.dart'; import 'package:myapp/db/db_helper.dart'; import 'package:myapp/photo_loader.dart'; @@ -39,6 +41,7 @@ class PhotoSyncManager { if (asset.createDateTime.millisecondsSinceEpoch > lastDBUpdateTimestamp) { await DatabaseHelper.instance.insertPhoto(await Photo.fromAsset(asset)); } + PhotoLoader.instance.addAsset((await asset.originFile).path, asset); } return await prefs.setInt( _lastDBUpdateTimestampKey, DateTime.now().millisecondsSinceEpoch); diff --git a/lib/ui/detail_page.dart b/lib/ui/detail_page.dart index 55aec494b..2506f747d 100644 --- a/lib/ui/detail_page.dart +++ b/lib/ui/detail_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:myapp/core/lru_map.dart'; class DetailPage extends StatefulWidget { final File file; @@ -30,10 +31,12 @@ class _DetailPageState extends State { }, child: Hero( tag: 'photo_' + widget.file.path, - child: Image.file( - widget.file, - filterQuality: FilterQuality.low, - ), + child: ImageLruCache.getData(widget.file.path) == null + ? Image.file( + widget.file, + filterQuality: FilterQuality.low, + ) + : ImageLruCache.getData(widget.file.path), ), ); } diff --git a/lib/ui/gallery.dart b/lib/ui/gallery.dart new file mode 100644 index 000000000..9b49a264b --- /dev/null +++ b/lib/ui/gallery.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import 'package:myapp/photo_loader.dart'; +import 'package:myapp/ui/image_widget.dart'; +import 'package:provider/provider.dart'; + +import 'change_notifier_builder.dart'; +import 'detail_page.dart'; + +class Gallery extends StatefulWidget { + @override + _GalleryState createState() { + return _GalleryState(); + } +} + +class _GalleryState extends State { + Logger _logger = Logger(); + + PhotoLoader get photoLoader => Provider.of(context); + + @override + Widget build(BuildContext context) { + _logger.i("Build"); + return ChangeNotifierBuilder( + value: photoLoader, + builder: (_, __) { + return GridView.builder( + itemBuilder: _buildItem, + itemCount: photoLoader.getPhotos().length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), + ); + }, + ); + } + + Widget _buildItem(BuildContext context, int index) { + var file = File(photoLoader.getPhotos()[index].localPath); + return GestureDetector( + onTap: () async { + routeToDetailPage(file, context); + }, + child: Hero( + child: Padding( + padding: const EdgeInsets.all(1.0), + child: ImageWidget(path: file.path), + ), + tag: 'photo_' + file.path, + ), + ); + } + + void routeToDetailPage(File file, BuildContext context) async { + final page = DetailPage( + file: file, + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return page; + }, + ), + ); + } +} diff --git a/lib/ui/gallery_page.dart b/lib/ui/gallery_page.dart index e06e095ba..604531249 100644 --- a/lib/ui/gallery_page.dart +++ b/lib/ui/gallery_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:myapp/photo_provider.dart'; +import 'package:myapp/ui/image_widget.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:myapp/ui/change_notifier_builder.dart'; import 'package:myapp/ui/loading_widget.dart'; -import 'package:myapp/ui/image_item_widget.dart'; import 'package:myapp/ui/detail_page.dart'; import 'package:provider/provider.dart'; @@ -80,9 +80,9 @@ class _GalleryPageState extends State { onTap: () async { routeToDetailPage(entity); }, - child: ImageItemWidget( + child: ImageWidget( key: ValueKey(entity), - entity: entity, + path: "", ), ); } diff --git a/lib/ui/image_item_widget.dart b/lib/ui/image_item_widget.dart deleted file mode 100644 index a1353de5e..000000000 --- a/lib/ui/image_item_widget.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:myapp/core/lru_map.dart'; -import 'package:myapp/ui/loading_widget.dart'; -import 'package:photo_manager/photo_manager.dart'; - -class ImageItemWidget extends StatefulWidget { - final AssetEntity entity; - - const ImageItemWidget({ - Key key, - this.entity, - }) : super(key: key); - @override - _ImageItemWidgetState createState() => _ImageItemWidgetState(); -} - -class _ImageItemWidgetState extends State { - @override - Widget build(BuildContext context) { - final item = widget.entity; - final size = 130; - final u8List = ImageLruCache.getData(item, size); - - Widget image; - - if (u8List != null) { - return _buildImageWidget(item, u8List, size); - } else { - image = FutureBuilder( - future: item.thumbDataWithSize(size, size), - builder: (context, snapshot) { - Widget w; - if (snapshot.hasError) { - w = Center( - child: Text("load error, error: ${snapshot.error}"), - ); - } - if (snapshot.hasData) { - ImageLruCache.setData(item, size, snapshot.data); - w = _buildImageWidget(item, snapshot.data, size); - } else { - w = Center( - child: loadWidget, - ); - } - - return w; - }, - ); - } - - return image; - } - - Widget _buildImageWidget(AssetEntity entity, Uint8List uint8list, num size) { - return Image.memory( - uint8list, - width: size.toDouble(), - height: size.toDouble(), - fit: BoxFit.cover, - ); - } - - @override - void didUpdateWidget(ImageItemWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.entity.id != oldWidget.entity.id) { - setState(() {}); - } - } -} diff --git a/lib/ui/image_widget.dart b/lib/ui/image_widget.dart new file mode 100644 index 000000000..3425d8e99 --- /dev/null +++ b/lib/ui/image_widget.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import 'package:myapp/core/lru_map.dart'; +import 'package:myapp/photo_loader.dart'; +import 'package:myapp/ui/loading_widget.dart'; +import 'package:provider/provider.dart'; + +class ImageWidget extends StatefulWidget { + final String path; + + const ImageWidget({ + Key key, + this.path, + }) : super(key: key); + @override + _ImageWidgetState createState() => _ImageWidgetState(); +} + +class _ImageWidgetState extends State { + var _logger = Logger(); + PhotoLoader get photoLoader => Provider.of(context); + + @override + Widget build(BuildContext context) { + final path = widget.path; + final size = 124; + final cachedImage = ImageLruCache.getData(path, size); + + Widget image; + + if (cachedImage != null) { + _logger.i("Cache hit for " + path); + image = cachedImage; + } else { + image = FutureBuilder( + future: _buildImageWidget(path, size), + builder: (context, snapshot) { + if (snapshot.hasData) { + ImageLruCache.setData(path, size, snapshot.data); + return snapshot.data; + } else { + return loadWidget; + } + }, + ); + } + + return image; + } + + Future _buildImageWidget(String path, num size) async { + var thumbnail = await photoLoader.getThumbnail(path, size); + if (thumbnail != null) { + return Image.memory( + thumbnail, + width: size.toDouble(), + height: size.toDouble(), + fit: BoxFit.cover, + ); + } else { + return Image.file( + File(path), + width: size.toDouble(), + height: size.toDouble(), + fit: BoxFit.cover); + } + } + + @override + void didUpdateWidget(ImageWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.path != oldWidget.path) { + setState(() {}); + } + } +}