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:typed_data';
|
||||
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef EvictionHandler<K, V>(K key, V value);
|
||||
|
||||
|
@ -38,31 +38,31 @@ class LRUMap<K, V> {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<PhotoLoader>.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<PhotoLoader>.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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Photo> _photos;
|
||||
final _photos = List<Photo>();
|
||||
final _assetMap = Map<String, AssetEntity>();
|
||||
|
||||
PhotoLoader._privateConstructor();
|
||||
static final PhotoLoader instance = PhotoLoader._privateConstructor();
|
||||
|
@ -16,7 +20,9 @@ class PhotoLoader extends ChangeNotifier {
|
|||
|
||||
Future<List<Photo>> 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<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: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);
|
||||
|
|
|
@ -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<DetailPage> {
|
|||
},
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
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: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<GalleryPage> {
|
|||
onTap: () async {
|
||||
routeToDetailPage(entity);
|
||||
},
|
||||
child: ImageItemWidget(
|
||||
child: ImageWidget(
|
||||
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