added image cache managing

This commit is contained in:
Mehul Ahal 2022-12-21 23:40:39 +01:00
parent b7045fc242
commit 2ff4d44d25
14 changed files with 188 additions and 80 deletions

View File

@ -0,0 +1,11 @@
import 'package:flutter/widgets.dart';
import 'const_colors.dart';
abstract class ConstText {
static const _imageOverlayTextStyle = TextStyle(
color: ConstColours.white,
fontSize: 28,
);
static TextStyle imageOverlayTextStyle(BuildContext context) => _imageOverlayTextStyle;
}

View File

@ -11,16 +11,13 @@ class OverlayService {
final LoggingService _loggingService;
final Map<int, AnimationController> _animationControllerMap = const {};
final Map<int, OverlayEntry> _overlayEntryMap = const {};
Future<void> playOverlayEntry({
required BuildContext context,
required AnimationController animationController,
required OverlayEntry overlayEntry,
}) async {
try {
_animationControllerMap[animationController.hashCode] = animationController;
_overlayEntryMap[overlayEntry.hashCode] = overlayEntry;
Overlay.of(
context,
@ -28,12 +25,9 @@ class OverlayService {
)!
.insert(overlayEntry);
await animationController.forward();
if (overlayEntry.mounted) overlayEntry.remove();
_overlayEntryMap.remove(overlayEntry.hashCode);
animationController.dispose();
_animationControllerMap.remove(animationController.hashCode);
} catch (error, stackTrace) {
_loggingService.handle(error, stackTrace);
}
@ -46,12 +40,6 @@ class OverlayService {
}
}
_overlayEntryMap.clear();
for (final animationController in _animationControllerMap.values) {
if (animationController.isAnimating) {
animationController.dispose();
}
}
_animationControllerMap.clear();
}
static OverlayService get locate => Locator.locate();

View File

@ -1,29 +1,37 @@
import 'dart:async';
import 'dart:math';
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/locator.dart';
import '/features/core/data/constants/const_values.dart';
import '/l10n/generated/l10n.dart';
import '../abstracts/images_api.dart';
import '../data/models/image_model.dart';
class UnsplashImagesApi implements ImagesApi {
class UnsplashImagesApi with LoggingService implements ImagesApi {
@override
FutureOr<Iterable<ImageModel>> fetchImageUri({required String token}) {
final random = Random();
return Iterable<int>.generate(ConstValues.numberOfImages).map((final imageIndex) {
// Drawing from a normal distribution
final imageSide = ConstValues.minImageSize +
random.nextInt((ConstValues.maxImageSize + 1) - ConstValues.minImageSize);
try {
return Iterable<int>.generate(ConstValues.numberOfImages).map((final imageIndex) {
// Drawing from a normal distribution
final imageSide = ConstValues.minImageSize +
random.nextInt((ConstValues.maxImageSize + 1) - ConstValues.minImageSize);
final imageUri = _imageUrlGenerator(imageSide: imageSide);
final imageUri = _imageUrlGenerator(imageSide: imageSide);
return ImageModel(
imageIndex: imageIndex,
uri: imageUri,
imageName: Strings.current.image,
);
});
return ImageModel(
imageIndex: imageIndex,
uri: imageUri,
imageName: '${Strings.current.image} $imageIndex: size=$imageSide',
);
});
} on Exception catch (ex, stackTrace) {
handleException(ex, stackTrace);
return const Iterable.empty();
}
}
Uri _imageUrlGenerator({required int imageSide}) => Uri(
@ -31,4 +39,6 @@ class UnsplashImagesApi implements ImagesApi {
host: ConstValues.backendHost,
pathSegments: [...ConstValues.backendUrlPathSegments, '${imageSide}x$imageSide'],
);
static UnsplashImagesApi get locate => Locator.locate();
}

View File

@ -0,0 +1,37 @@
import 'dart:ui';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart';
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/locator.dart';
class ImageCacheManagerService with LoggingService {
ImageCacheManagerService({
required AppLifecycleService appLifecycleService,
}) : _appLifecycleService = appLifecycleService {
_init();
}
final AppLifecycleService _appLifecycleService;
Future<void> emptyCache() async => await DefaultCacheManager().emptyCache();
Future<void> _init() async {
_appLifecycleService.addListener(
tag: runtimeType.toString(),
listener: (final appLifecycleState) async {
switch (appLifecycleState) {
case AppLifecycleState.resumed:
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
info('Discarding cached images');
await DefaultCacheManager().emptyCache();
}
},
);
}
static ImageCacheManagerService get locate => Locator.locate();
}

View File

@ -1,3 +1,4 @@
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/features/home/data/models/image_model.dart';
import 'package:mc_gallery/locator.dart';
@ -8,20 +9,33 @@ import '../abstracts/images_api.dart';
/// Since this is very simple use-case, this is the only interface. For complex (actual CRUD-based) I/O,
/// an additional Repository layer interface can be used between [ImagesService] and [ImagesApi].
class ImagesService {
ImagesService({required ImagesApi imagesApi}) : _imagesApi = imagesApi {
ImagesService({
required ImagesApi imagesApi,
required LoggingService loggingService,
}) : _imagesApi = imagesApi,
_loggingService = loggingService {
_init();
}
final ImagesApi _imagesApi;
final LoggingService _loggingService;
late final Iterable<ImageModel> _imageModels;
Iterable<ImageModel> get imageModels => _imageModels;
Future<void> _init() async {
_loggingService.info('Fetching and creating image models...');
_imageModels = await _imagesApi.fetchImageUri(token: '');
_imageModels.isNotEmpty
? _loggingService.good("Created ${_imageModels.length} images' models")
: _loggingService.warning('No images found');
Locator.instance().signalReady(this);
}
int get firstAvailableImageIndex => 0;
int get lastAvailableImageIndex => _imageModels.length - 1;
static ImagesService get locate => Locator.locate();
}

View File

@ -1,12 +1,12 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart';
import '/features/core/abstracts/base_view_model.dart';
import '/features/core/services/logging_service.dart';
import '/features/core/services/navigation_service.dart';
import '/features/home/data/models/image_model.dart';
import '/features/home/services/image_cache_manager_service.dart';
import '/features/home/services/images_service.dart';
import '/features/home/views/image_carousel/image_carousel_view.dart';
import '/locator.dart';
@ -15,16 +15,16 @@ class GalleryViewModel extends BaseViewModel {
GalleryViewModel({
required ImagesService imagesService,
required NavigationService navigationService,
required AppLifecycleService appLifecycleService,
required ImageCacheManagerService imageCacheManagerService,
required LoggingService loggingService,
}) : _imagesService = imagesService,
_navigationService = navigationService,
_appLifecycleService = appLifecycleService,
_imageCacheManagerService = imageCacheManagerService,
_loggingService = loggingService;
final ImagesService _imagesService;
final NavigationService _navigationService;
final AppLifecycleService _appLifecycleService;
final ImageCacheManagerService _imageCacheManagerService;
final LoggingService _loggingService;
final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true);
@ -32,27 +32,11 @@ class GalleryViewModel extends BaseViewModel {
@override
Future<void> initialise(bool Function() mounted, [arguments]) async {
_appLifecycleService.addListener(
tag: runtimeType.toString(),
listener: (final appLifecycleState) async {
switch (appLifecycleState) {
case AppLifecycleState.resumed:
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
await DefaultCacheManager().emptyCache();
}
},
);
super.initialise(mounted, arguments);
}
@override
Future<void> dispose() async {
await _appLifecycleService.removeListener(tag: runtimeType.toString());
super.dispose();
}

View File

@ -1,6 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:mc_gallery/features/core/data/constants/const_colors.dart';
import 'package:mc_gallery/features/core/data/constants/const_text.dart';
import '/features/core/widgets/gap.dart';
import '/features/core/widgets/mcg_scaffold.dart';
import '/features/core/widgets/view_model_builder.dart';
import '/features/home/views/image_carousel/image_carousel_view_model.dart';
@ -27,15 +31,56 @@ class ImageCarouselView extends StatelessWidget {
appBar: AppBar(
title: Text(model.strings.imageCarousel),
),
body: Hero(
tag: model.currentImageKey,
child: CachedNetworkImage(
imageUrl: model.currentImageUrl,
cacheKey: model.currentImageKey,
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
value: model.downloadProgressValue(progress: progress),
body: Column(
children: [
Expanded(
child: Card(
elevation: 8,
child: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: model.currentImageKey,
child: CachedNetworkImage(
imageUrl: model.currentImageUrl,
cacheKey: model.currentImageKey,
fit: BoxFit.fill,
progressIndicatorBuilder: (_, __, final progress) =>
CircularProgressIndicator(
value: model.downloadProgressValue(progress: progress),
),
),
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.chevron_left,
color: model.hasPreviousImage ? ConstColours.white : ConstColours.black,
),
Text(
model.currentImageName,
style: ConstText.imageOverlayTextStyle(context),
),
Icon(
Icons.chevron_right,
color: model.hasNextImage ? ConstColours.white : ConstColours.black,
),
],
),
),
],
),
),
),
),
const Gap(24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MarkdownBody(data: model.strings.imageDetails),
),
const Gap(16),
],
),
),
);

View File

@ -1,8 +1,5 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart';
import '/features/core/abstracts/base_view_model.dart';
import '/features/core/services/logging_service.dart';
@ -16,16 +13,13 @@ class ImageCarouselViewModel extends BaseViewModel {
ImageCarouselViewModel({
required ImagesService imagesService,
required NavigationService navigationService,
required AppLifecycleService appLifecycleService,
required LoggingService loggingService,
}) : _imagesService = imagesService,
_navigationService = navigationService,
_appLifecycleService = appLifecycleService,
_loggingService = loggingService;
final ImagesService _imagesService;
final NavigationService _navigationService;
final AppLifecycleService _appLifecycleService;
final LoggingService _loggingService;
late final ValueNotifier<ImageModel> _currentImageModel;
@ -33,19 +27,6 @@ class ImageCarouselViewModel extends BaseViewModel {
@override
Future<void> initialise(bool Function() mounted, [arguments]) async {
_appLifecycleService.addListener(
tag: runtimeType.toString(),
listener: (final appLifecycleState) async {
switch (appLifecycleState) {
case AppLifecycleState.resumed:
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
await DefaultCacheManager().emptyCache();
}
});
_currentImageModel = ValueNotifier(_imagesService.imageModels
.elementAt((arguments! as ImageCarouselViewArguments).imageIndexKey));
@ -54,15 +35,20 @@ class ImageCarouselViewModel extends BaseViewModel {
@override
Future<void> dispose() async {
await _appLifecycleService.removeListener(tag: runtimeType.toString());
super.dispose();
}
String get currentImageUrl => currentImageModel.value.uri.toString();
String get currentImageKey => currentImageModel.value.imageIndex.toString();
String get currentImageName => currentImageModel.value.imageName;
double? downloadProgressValue({required DownloadProgress progress}) =>
progress.totalSize != null ? progress.downloaded / progress.totalSize! : null;
bool get hasPreviousImage =>
currentImageModel.value.imageIndex > _imagesService.firstAvailableImageIndex;
bool get hasNextImage =>
currentImageModel.value.imageIndex < _imagesService.lastAvailableImageIndex;
static ImageCarouselViewModel get locate => Locator.locate();
}

View File

@ -25,6 +25,8 @@ class MessageLookup extends MessageLookupByLibrary {
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
"image": MessageLookupByLibrary.simpleMessage("Image"),
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
"imageDetails": MessageLookupByLibrary.simpleMessage(
"Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?"),
"somethingWentWrong":
MessageLookupByLibrary.simpleMessage("Something went wrong"),
"startLoadingPrompt":

View File

@ -99,6 +99,16 @@ class Strings {
args: [],
);
}
/// `Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?`
String get imageDetails {
return Intl.message(
'Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?',
name: 'imageDetails',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<Strings> {

View File

@ -7,5 +7,6 @@
"gallery": "Gallery",
"startLoadingPrompt": "Press me to start loading",
"imageCarousel": "Image carousel"
"imageCarousel": "Image carousel",
"imageDetails": "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?"
}

View File

@ -7,6 +7,7 @@ import 'package:mc_gallery/features/core/abstracts/router/app_router.dart';
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/features/core/services/navigation_service.dart';
import 'package:mc_gallery/features/home/api/unsplash_images_api.dart';
import 'package:mc_gallery/features/home/services/image_cache_manager_service.dart';
import 'package:mc_gallery/features/home/services/images_service.dart';
import 'package:mc_gallery/features/home/views/gallery/gallery_view_model.dart';
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view_model.dart';
@ -44,7 +45,7 @@ class Locator {
() => GalleryViewModel(
imagesService: ImagesService.locate,
navigationService: NavigationService.locate,
appLifecycleService: AppLifecycleService.locate,
imageCacheManagerService: ImageCacheManagerService.locate,
loggingService: LoggingService.locate,
),
);
@ -52,7 +53,6 @@ class Locator {
() => ImageCarouselViewModel(
imagesService: ImagesService.locate,
navigationService: NavigationService.locate,
appLifecycleService: AppLifecycleService.locate,
loggingService: LoggingService.locate,
),
);
@ -86,11 +86,14 @@ class Locator {
dispose: (param) async => await param.dispose(),
);
it.registerSingleton(
ImagesService(
imagesApi: UnsplashImagesApi(),
),
ImagesService(imagesApi: UnsplashImagesApi.locate, loggingService: LoggingService.locate),
signalsReady: true,
);
it.registerSingleton(
ImageCacheManagerService(
appLifecycleService: AppLifecycleService.locate,
),
);
}
static FutureOr<void> _registerRepos(GetIt locator) {}

View File

@ -195,6 +195,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.13"
flutter_svg:
dependency: "direct main"
description:
@ -289,6 +296,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
markdown:
dependency: transitive
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
matcher:
dependency: transitive
description:

View File

@ -30,6 +30,9 @@ dependencies:
connectivity_plus: ^3.0.2
internet_connection_checker: ^1.0.0+1
# Util frontend
flutter_markdown: ^0.6.13
# Logging
talker: ^2.1.0+1
talker_dio_logger: ^1.0.0