Add some amount of caching
This commit is contained in:
parent
22ca572532
commit
53442cbf14
10 changed files with 203 additions and 138 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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
68
lib/ui/gallery.dart
Normal 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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
78
lib/ui/image_widget.dart
Normal 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(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue