From 350f92f0a61ccc1fcff017edfb02960d862651f6 Mon Sep 17 00:00:00 2001 From: "Daniel.Tjuatja" Date: Fri, 7 Sep 2018 14:01:17 +0800 Subject: [PATCH] Clear issue #1, #2, #3 --- README.md | 31 ++++++-- example/README.md | 2 +- example/lib/main.dart | 45 +++++------- lib/s3_cache_image.dart | 132 +++++++++++++++++++--------------- lib/src/s3_cache_manager.dart | 86 ++++++++++++++++------ 5 files changed, 183 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index af8d426..91b38c5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # Cached network image +A flutter library to show image from S3 repository and keep them in the cache directory. +This package is based from [https://github.com/renefloor/flutter_cached_network_image]. - -A flutter library to show images from S3 repository and keep them in the cache directory. - -This package is based from [https://github.com/renefloor/flutter_cached_network_image] ## How to add Add this to your package's pubspec.yaml file: @@ -19,16 +17,39 @@ import 'package:s3_cache_image/s3_cache_image.dart'; ``` ## How to use -The S3ImageCache can be used directly or through the ImageProvider. +S3ImageCache can be used directly or through the ImageProvider. ``` S3CachedImage( fit: BoxFit.cover, width: width, height: width, onExpired: null, + onDebug: null, imageURL: 'INSERT S3 URL HERE', cacheId: 'INSERT CACHE ID HERE', errorWidget: Center(child: Text('ERROR')), placeholder: Center(child: Text('Loading'))) ``` + +Files stored in system temporary folder, so it can be cleared automatically by OS if necessary. + +Set directory path: +``` +setS3CachePath('/s3/cache/newImage/hello/'); +``` + +Get cache size will return future: +``` +getS3CacheSize(); +``` + + +Purging cache directory will return future: +``` +clearS3Cache(); +``` + + + + diff --git a/example/README.md b/example/README.md index 64a12f6..16c4986 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,6 @@ # example -A new Flutter project. +An example for how to use s3_cache_image pacakage ## Getting Started diff --git a/example/lib/main.dart b/example/lib/main.dart index 00529a9..7ce6fd0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,6 @@ import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:s3_cache_image/s3_cache_image.dart'; -import 'package:path_provider/path_provider.dart'; void main() => runApp(MyApp()); @@ -26,32 +23,20 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + + @override + void initState() { + setS3CachePath('/s3/cache/images/food/'); + super.initState(); + } + Future clearDiskCachedImages() async { - final tempDir = await getTemporaryDirectory(); - final cachePath = tempDir.path + '/images'; - final cacheDir = Directory(cachePath); - try { - await cacheDir.delete(recursive: true); - } catch (_) { - return false; - } - return true; + return clearS3Cache(); } /// Return the disk cache directory size. Future getDiskCachedImagesSize() async { - final tempDir = await getTemporaryDirectory(); - final cachePath = tempDir.path + '/images'; - final cacheDir = Directory(cachePath); - - print('${cacheDir.path}'); - var size = 0; - try { - cacheDir.listSync().forEach((var file) => size += file.statSync().size); - return size; - } catch (_) { - return null; - } + return getS3CacheSize(); } @override @@ -90,14 +75,16 @@ class _HomePageState extends State { height: width, onExpired: (id) { final completer = Completer() - ..complete('INSERT S3 URL'); + ..complete('INSERT S3 URL HERE'); return completer.future; }, -// onExpired: null, - imageURL: 'INSERT S3 URL', - cacheId: '123-456-789', + onDebug: (log) { + print('LOG $log'); + }, + imageURL: 'INSERT S3 URL HERE', + cacheId: 'INSERT CACHE ID HERE', errorWidget: Center(child: Text('ERROR')), - placeholder: Center(child: Text('Loading')))), + placeholder: Center(child: Text('LOADING')))), ); } } diff --git a/lib/s3_cache_image.dart b/lib/s3_cache_image.dart index 1c49e29..d3ad3af 100644 --- a/lib/s3_cache_image.dart +++ b/lib/s3_cache_image.dart @@ -6,17 +6,29 @@ import 'dart:typed_data'; import 'dart:ui' as ui show instantiateImageCodec, Codec; import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; import 'package:flutter/material.dart'; -import 'src/S3_cache_manager.dart'; +import 'src/s3_cache_manager.dart'; - -/// CachedNetworkImage for Flutter -/// Copyright (c) 2017 Rene Floor +/// s3_cache_image for Flutter +/// Copyright (c) 2018 Holmusk /// Released under MIT License. -class S3CachedImage extends StatefulWidget { +void setS3CachePath(String newPath) { + S3CacheManager().dirPath = newPath; +} +Future clearS3Cache() { + return S3CacheManager().clearCache(); +} +Future getS3CacheSize() { + return S3CacheManager().getCacheSize(); +} + +typedef void DebugCallback(LogRecord log); + +class S3CachedImage extends StatefulWidget { /// Creates a widget that displays a [placeholder] while an [S3ImageURL] is loading /// then cross-fades to display the [S3ImageURL]. /// The [imageUrl] and [cacheId] arguments must not be null. Arguments [width], @@ -27,6 +39,7 @@ class S3CachedImage extends StatefulWidget { @required this.imageURL, @required this.cacheId, this.onExpired, + this.onDebug, this.errorWidget, this.placeholder, this.width, @@ -38,14 +51,20 @@ class S3CachedImage extends StatefulWidget { /// The target image URL that is displayed. final String imageURL; + /// The target image Id that is displayed. final String cacheId; + /// Callback to refresh expired url final ExpiredURLCallback onExpired; - /// Widget displayed while the target [S3ImageURL] is loading. - final Widget errorWidget; + // Callback exposing log stream for debugging + final DebugCallback onDebug; + /// Widget displayed while the target [S3ImageURL] failed loading. + final Widget errorWidget; + + /// Widget displayed while the target [S3ImageURL] is loading. final Widget placeholder; /// If non-null, require the image to have this width. @@ -55,6 +74,7 @@ class S3CachedImage extends StatefulWidget { /// placeholder widget does not match that of the target image. The size is /// also affected by the scale factor. final double width; + /// If non-null, require the image to have this height. /// /// If null, the image will pick a size that best preserves its intrinsic @@ -62,6 +82,7 @@ class S3CachedImage extends StatefulWidget { /// placeholder widget does not match that of the target image. The size is /// also affected by the scale factor. final double height; + /// How to inscribe the image into the space allocated during layout. /// /// The default varies based on the other fields. See the discussion at @@ -72,22 +93,24 @@ class S3CachedImage extends StatefulWidget { _S3CachedImageState createState() => _S3CachedImageState(); } - - /// The phases a [CachedNetworkImage] goes through. @visibleForTesting enum ImagePhase { /// Initial state - START, + start, + /// Waiting for target image to load - WAITING, + waiting, + /// Fading out previous image. - FADEOUT, + fadeout, + /// Fading in new image. - FADEIN, - /// Fade-in complete. - COMPLETED } + fadein, + /// Fade-in complete. + completed +} typedef void _ImageProviderResolverListener(); @@ -119,7 +142,6 @@ class _ImageProviderResolver { } void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { -// print(' IMAGE CHANGED >>>>>>>> $listener'); _imageInfo = imageInfo; listener(); } @@ -137,10 +159,12 @@ class _S3CachedImageState extends State AnimationController _controller; Animation _animation; - ImagePhase _phase = ImagePhase.START; + ImagePhase _phase = ImagePhase.start; ImagePhase get state => _phase; + final _logger = Logger('S3CacheImage'); + bool _hasError; @override @@ -161,18 +185,24 @@ class _S3CachedImageState extends State ..addStatusListener((_) { _updatePhase(); }); - + + if (widget.onDebug != null) { + print('ONDEBUG != NULL'); + // hierarchicalLoggingEnabled = true; + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((log) { + print('LOG $log'); + }); + } + super.initState(); } @override void didChangeDependencies() { -// print('CHANGE DEPENDENCIES'); _imageProvider .obtainKey(createLocalImageConfiguration(context)) - .then((key) { -// setState(() => _hasError = true); - }); + .then((key) {}); _resolveImage(); super.didChangeDependencies(); @@ -180,17 +210,13 @@ class _S3CachedImageState extends State @override void didUpdateWidget(S3CachedImage oldWidget) { -// print('UPDATE WIDGET'); - super.didUpdateWidget(oldWidget); if (widget.cacheId != oldWidget.cacheId || widget.placeholder != widget.placeholder) { -// print('CHANGE WIDGET CALL RESOLVE AGAIN'); _imageProvider = S3CachedNetworkImageProvider( widget.imageURL, widget.cacheId, widget.onExpired, errorListener: _imageLoadingFailed); - _resolveImage(); } } @@ -202,24 +228,26 @@ class _S3CachedImageState extends State } void _resolveImage() { + _logger.finest('Resolve image'); _imageResolver.resolve(_imageProvider); - if (_phase == ImagePhase.START) { + if (_phase == ImagePhase.start) { _updatePhase(); } } void _updatePhase() { + _logger.finest('Update phase $_phase'); setState(() { switch (_phase) { - case ImagePhase.START: + case ImagePhase.start: if (_imageResolver._imageInfo != null || _hasError) - _phase = ImagePhase.COMPLETED; + _phase = ImagePhase.completed; else - _phase = ImagePhase.WAITING; + _phase = ImagePhase.waiting; break; - case ImagePhase.WAITING: + case ImagePhase.waiting: if (_hasError && widget.errorWidget == null) { - _phase = ImagePhase.COMPLETED; + _phase = ImagePhase.completed; return; } @@ -231,18 +259,18 @@ class _S3CachedImageState extends State } } break; - case ImagePhase.FADEOUT: + case ImagePhase.fadeout: if (_controller.status == AnimationStatus.dismissed) { _startFadeIn(); } break; - case ImagePhase.FADEIN: + case ImagePhase.fadein: if (_controller.status == AnimationStatus.completed) { // Done finding in new image. - _phase = ImagePhase.COMPLETED; + _phase = ImagePhase.completed; } break; - case ImagePhase.COMPLETED: + case ImagePhase.completed: _hasError = _imageResolver._imageInfo == null; // Nothing to do. break; @@ -251,16 +279,18 @@ class _S3CachedImageState extends State } void _startFadeOut() { + _logger.finest('Start fade out'); _controller.duration = const Duration(milliseconds: 300); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeOut); - _phase = ImagePhase.FADEOUT; + _phase = ImagePhase.fadeout; _controller.reverse(from: 1.0); } void _startFadeIn() { + _logger.finest('Start fade in'); _controller.duration = const Duration(milliseconds: 700); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn); - _phase = ImagePhase.FADEIN; + _phase = ImagePhase.fadein; _controller.forward(from: 0.0); } @@ -274,30 +304,26 @@ class _S3CachedImageState extends State bool get _isShowingPlaceholder { assert(_phase != null); switch (_phase) { - case ImagePhase.START: - case ImagePhase.WAITING: - case ImagePhase.FADEOUT: + case ImagePhase.start: + case ImagePhase.waiting: + case ImagePhase.fadeout: return true; - case ImagePhase.FADEIN: - case ImagePhase.COMPLETED: + case ImagePhase.fadein: + case ImagePhase.completed: return _hasError && widget.errorWidget == null; } return true; } void _imageLoadingFailed() { -// print('>>>>>>>>>>>>>>>>> Image LOADING FAILED'); + _logger.warning('Image loading failed'); _hasError = true; _updatePhase(); } @override Widget build(BuildContext context) { -// return widget.errorWidget; -// return widget.placeholder; - -// print('HAS ERROR >>>>> $_hasError'); - assert(_phase != ImagePhase.START); + assert(_phase != ImagePhase.start); if (_isShowingPlaceholder && widget.placeholder != null) { return _fadedWidget(widget.placeholder); } @@ -335,10 +361,6 @@ class _S3CachedImageState extends State } } -///============================================================= -/// IMAGE PROVIDER -///============================================================= - typedef void ErrorListener(); class S3CachedNetworkImageProvider @@ -380,11 +402,9 @@ class S3CachedNetworkImageProvider } Future _loadAsync(S3CachedNetworkImageProvider key) async { -// print('LOAD ASYNC'); var cacheManager = S3CacheManager(); var file = await cacheManager.getFile(url, cacheId, callback); if (file == null) { -// print('GET FILE RETURNED NULL'); if (errorListener != null) { errorListener(); } @@ -395,14 +415,12 @@ class S3CachedNetworkImageProvider Future _loadAsyncFromFile( S3CachedNetworkImageProvider key, File file) async { -// print('LOAD FROM FILE'); assert(key == this); final Uint8List bytes = await file.readAsBytes(); if (bytes.lengthInBytes == 0) { if (errorListener != null) { -// print('FILE IS EMPTY'); errorListener(); } return null; diff --git a/lib/src/s3_cache_manager.dart b/lib/src/s3_cache_manager.dart index 8af9aef..72e701d 100644 --- a/lib/src/s3_cache_manager.dart +++ b/lib/src/s3_cache_manager.dart @@ -1,7 +1,10 @@ +library s3_cache_image; + import 'dart:async'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; typedef Future ExpiredURLCallback(String id); @@ -14,39 +17,48 @@ class S3CacheManager { S3CacheManager._internal(); - Future getPath(String id) async { + final _logger = Logger.detached('S3CacheManager'); + var _path = '/s3/cache/images/'; + + set dirPath(String newPath) { + _logger.warning('PATH CHANGE FROM $_path to $newPath'); + _path = newPath; + print(_path); + } + + String get currentPath => _path; + + Future _getPath(String id) async { final dir = await getTemporaryDirectory(); - return dir.path + '/images/' + id; + print(currentPath); + return dir.path + _path + id; } Future getFile( String url, String id, ExpiredURLCallback callback) async { - // CHECK IF FILE EXIST - // IF FILE EXIST THEN RETURN FILE IMMEDIATELY - - final path = await getPath(id); - print('$path'); + final path = await _getPath(id); + _logger.finest('Start fetching file at path $path'); final file = File(path); if (file.existsSync()) { -// print('>>>>>>>>> FILE EXIST - RETURN'); + _logger.fine('File exist at path $path'); return file; } - // PARSE URL AND CHECK IF URL EXPIRED OR NOT - // IF URL IS EXPIRED CALL EXPIRED CALLBACK TO GET NEW URL var _downloadUrl = url; - if (isExpired(url)) { -// print('>>>>>>>>> URL IS EXPIRED - REFETCH'); + if (_isExpired(url)) { + _logger.fine('Url expired, refresh with expired callback'); if (callback == null) { -// print('>>>>>>>>> NO REFETCH CALLBACK RETURN NULL'); + _logger.warning('No refetch callback provided, return null'); return null; } _downloadUrl = await callback(id); } if (_downloadUrl == null) { + _logger.warning('No response from expired callback, return null'); return null; } -// print('>>>>>>>>> URL NOT EXPIRED DOWNLOAD'); + + _logger.fine('Valid Url, commencing download for $_downloadUrl'); return await _downloadFile(_downloadUrl, id); } @@ -55,32 +67,33 @@ class S3CacheManager { try { response = await http.get(url); } catch (e) { -// print('>>>>>>>>>>>>>>> ERROR DOWNLOAD IMAGE ${e.toString()}'); + _logger.severe('Failed to download image with error ${e.toString()}', e, + StackTrace.current); return null; } if (response != null) { -// print('>>>>>>>>> DOWNLOAD RESPONSE NOT NULL'); if (response.statusCode == 200) { -// print('>>>>>>>>> STATUS CODE 200'); - final path = await getPath(id); + final path = await _getPath(id); final folder = File(path).parent; if (!(await folder.exists())) { folder.createSync(recursive: true); } final file = await File(path).writeAsBytes(response.bodyBytes); + _logger.fine('Download success and file saved to path $path'); return file; } else { -// print('>>>>>>>>> STATUS CODE ${response.statusCode} >>>>>> RETURN NULL'); + _logger + .warning('Download failed with status code ${response.statusCode}'); return null; } } else { -// print('>>>>>>>>> RESPONSE NULL'); + _logger.warning('No response from server'); return null; } } - bool isExpired(String url) { + bool _isExpired(String url) { final uri = Uri.dataFromString(url); final queries = uri.queryParameters; final expiry = int.parse(queries['Expires']); @@ -90,4 +103,35 @@ class S3CacheManager { } return true; } + + + Future clearCache() async { + final tempDir = await getTemporaryDirectory(); + final cachePath = tempDir.path + _path; + final cacheDir = Directory(cachePath); + + try { + await cacheDir.delete(recursive: true); + } catch (e) { + _logger.severe('Failed to delete s3 cache ${e.toString()}', e, StackTrace.current); + return false; + } + return true; + } + + Future getCacheSize() async { + final tempDir = await getTemporaryDirectory(); + final cachePath = tempDir.path + _path; + final cacheDir = Directory(cachePath); + + var size = 0; + try { + cacheDir.listSync().forEach((var file) => size += file.statSync().size); + return size; + } catch (_) { + return null; + } + } + + }