Add some amount of caching

This commit is contained in:
Vishnu Mohandas 2020-03-28 23:48:27 +05:30
parent 22ca572532
commit 53442cbf14
10 changed files with 203 additions and 138 deletions

View file

@ -1,7 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:photo_manager/photo_manager.dart'; import 'package:flutter/material.dart';
typedef EvictionHandler<K, V>(K key, V value); typedef EvictionHandler<K, V>(K key, V value);
@ -38,31 +38,31 @@ class LRUMap<K, V> {
} }
class ImageLruCache { class ImageLruCache {
static LRUMap<_ImageCacheEntity, Uint8List> _map = LRUMap(500); static LRUMap<_ImageCacheEntity, Image> _map = LRUMap(500);
static Uint8List getData(AssetEntity entity, [int size = 64]) { static Image getData(String path, [int size = 64]) {
return _map.get(_ImageCacheEntity(entity, size)); return _map.get(_ImageCacheEntity(path, size));
} }
static void setData(AssetEntity entity, int size, Uint8List list) { static void setData(String path, int size, Image image) {
_map.put(_ImageCacheEntity(entity, size), list); _map.put(_ImageCacheEntity(path, size), image);
} }
} }
class _ImageCacheEntity { class _ImageCacheEntity {
AssetEntity entity; String path;
int size; int size;
_ImageCacheEntity(this.entity, this.size); _ImageCacheEntity(this.path, this.size);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is _ImageCacheEntity && other is _ImageCacheEntity &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
entity == other.entity && path == other.path &&
size == other.size; size == other.size;
@override @override
int get hashCode => entity.hashCode ^ size.hashCode; int get hashCode => path.hashCode ^ size.hashCode;
} }

View file

@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/logger.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_loader.dart';
import 'package:myapp/photo_provider.dart'; import 'package:myapp/photo_provider.dart';
import 'package:myapp/photo_sync_manager.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:myapp/ui/loading_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:myapp/ui/gallery_page.dart'; import 'package:myapp/ui/gallery_page.dart';
@ -19,7 +18,7 @@ void main() async {
await provider.refreshGalleryList(); await provider.refreshGalleryList();
var assets = await provider.list[0].assetList; var assets = await provider.list[0].assetList;
var photoSyncManager = PhotoSyncManager(assets); var photoSyncManager = PhotoSyncManager(assets);
await photoSyncManager.init(); photoSyncManager.init();
runApp(MyApp2()); runApp(MyApp2());
} }
@ -50,55 +49,24 @@ class MyApp2 extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
Widget body; Widget body;
if (snapshot.hasData) { if (snapshot.hasData) {
body = ChangeNotifierProvider<PhotoLoader>.value( body = Gallery();
value: photoLoader,
child: GridView.builder(
itemBuilder: _buildItem,
itemCount: snapshot.data.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4),
));
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
body = Text("Error!"); body = Text("Error!");
} else { } else {
body = loadWidget; body = loadWidget;
} }
return MaterialApp( return ChangeNotifierProvider<PhotoLoader>.value(
title: title, value: photoLoader,
theme: ThemeData.dark(), child: MaterialApp(
home: Scaffold( title: title,
appBar: AppBar( theme: ThemeData.dark(),
title: Text(title), home: Scaffold(
), appBar: AppBar(
body: body), 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;
},
),
);
}
} }

View file

@ -1,11 +1,15 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:myapp/db/db_helper.dart'; import 'package:myapp/db/db_helper.dart';
import 'package:myapp/models/photo.dart'; import 'package:myapp/models/photo.dart';
import 'package:photo_manager/photo_manager.dart';
class PhotoLoader extends ChangeNotifier { class PhotoLoader extends ChangeNotifier {
final logger = Logger(); final logger = Logger();
List<Photo> _photos; final _photos = List<Photo>();
final _assetMap = Map<String, AssetEntity>();
PhotoLoader._privateConstructor(); PhotoLoader._privateConstructor();
static final PhotoLoader instance = PhotoLoader._privateConstructor(); static final PhotoLoader instance = PhotoLoader._privateConstructor();
@ -16,7 +20,9 @@ class PhotoLoader extends ChangeNotifier {
Future<List<Photo>> loadPhotos() async { Future<List<Photo>> loadPhotos() async {
DatabaseHelper db = DatabaseHelper.instance; 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()); logger.i("Imported photo size: " + _photos.length.toString());
return _photos; return _photos;
} }
@ -26,4 +32,16 @@ class PhotoLoader extends ChangeNotifier {
logger.i("Reloading..."); logger.i("Reloading...");
notifyListeners(); notifyListeners();
} }
void addAsset(String path, AssetEntity asset) {
_assetMap[path] = asset;
}
Future<Uint8List> getThumbnail(String path, int size) {
if (!_assetMap.containsKey(path)) {
logger.w("No thumbnail");
return Future.value(null);
}
return _assetMap[path].thumbDataWithSize(size, size);
}
} }

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:myapp/db/db_helper.dart'; import 'package:myapp/db/db_helper.dart';
import 'package:myapp/photo_loader.dart'; import 'package:myapp/photo_loader.dart';
@ -39,6 +41,7 @@ class PhotoSyncManager {
if (asset.createDateTime.millisecondsSinceEpoch > lastDBUpdateTimestamp) { if (asset.createDateTime.millisecondsSinceEpoch > lastDBUpdateTimestamp) {
await DatabaseHelper.instance.insertPhoto(await Photo.fromAsset(asset)); await DatabaseHelper.instance.insertPhoto(await Photo.fromAsset(asset));
} }
PhotoLoader.instance.addAsset((await asset.originFile).path, asset);
} }
return await prefs.setInt( return await prefs.setInt(
_lastDBUpdateTimestampKey, DateTime.now().millisecondsSinceEpoch); _lastDBUpdateTimestampKey, DateTime.now().millisecondsSinceEpoch);

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:myapp/core/lru_map.dart';
class DetailPage extends StatefulWidget { class DetailPage extends StatefulWidget {
final File file; final File file;
@ -30,10 +31,12 @@ class _DetailPageState extends State<DetailPage> {
}, },
child: Hero( child: Hero(
tag: 'photo_' + widget.file.path, tag: 'photo_' + widget.file.path,
child: Image.file( child: ImageLruCache.getData(widget.file.path) == null
widget.file, ? Image.file(
filterQuality: FilterQuality.low, widget.file,
), filterQuality: FilterQuality.low,
)
: ImageLruCache.getData(widget.file.path),
), ),
); );
} }

68
lib/ui/gallery.dart Normal file
View file

@ -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<Gallery> {
Logger _logger = Logger();
PhotoLoader get photoLoader => Provider.of<PhotoLoader>(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;
},
),
);
}
}

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:myapp/photo_provider.dart'; import 'package:myapp/photo_provider.dart';
import 'package:myapp/ui/image_widget.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:myapp/ui/change_notifier_builder.dart'; import 'package:myapp/ui/change_notifier_builder.dart';
import 'package:myapp/ui/loading_widget.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:myapp/ui/detail_page.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -80,9 +80,9 @@ class _GalleryPageState extends State<GalleryPage> {
onTap: () async { onTap: () async {
routeToDetailPage(entity); routeToDetailPage(entity);
}, },
child: ImageItemWidget( child: ImageWidget(
key: ValueKey(entity), key: ValueKey(entity),
entity: entity, path: "",
), ),
); );
} }

View file

@ -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<ImageItemWidget> {
@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<Uint8List>(
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(() {});
}
}
}

78
lib/ui/image_widget.dart Normal file
View file

@ -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<ImageWidget> {
var _logger = Logger();
PhotoLoader get photoLoader => Provider.of<PhotoLoader>(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<Image>(
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<Image> _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(() {});
}
}
}