Skip to content

Commit

Permalink
Improve-image-loading (#55)
Browse files Browse the repository at this point in the history
* Fix loading

* Reduce memory usage

* Reduce memory usage

* Reduce media load time

* Reduce media load time

* Reduce media load time
  • Loading branch information
cp-pratik-k authored Nov 15, 2024
1 parent 4a07169 commit 1b136d7
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 112 deletions.
8 changes: 8 additions & 0 deletions .idea/libraries/Dart_Packages.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 120 additions & 0 deletions app/lib/components/app_media_image_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:data/models/isolate/isolate_parameters.dart';
import 'package:data/models/media/media.dart';
import 'package:data/models/media/media_extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

final _providerLocks = <AppMediaImageProvider, Completer<ui.Codec>>{};

class AppMediaImageProvider extends ImageProvider<AppMediaImageProvider> {
final AppMedia media;
final Size thumbnailSize;

const AppMediaImageProvider({
required this.media,
this.thumbnailSize = const Size(500, 500),
});

@override
ImageStreamCompleter loadImage(
AppMediaImageProvider key,
ImageDecoderCallback decode,
) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
debugLabel: '${key.media.runtimeType}-'
'${key.media.id}-'
'${key.media.thumbnailLink ?? ''}'
'${key.media.sources.contains(AppMediaSource.local) ? 'local' : 'network'}'
'${key.thumbnailSize}',
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image Provider', this),
DiagnosticsProperty<AppMediaImageProvider>('Image Key', key),
];
},
);
}

@override
Future<AppMediaImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<AppMediaImageProvider>(this);
}

Future<ui.Codec> _loadAsync(
AppMediaImageProvider key,
ImageDecoderCallback decode,
) async {
if (_providerLocks.containsKey(key)) {
return _providerLocks[key]!.future;
}
final lock = Completer<ui.Codec>();
_providerLocks[key] = lock;
Future(() async {
try {
if (media.sources.contains(AppMediaSource.local)) {
final Uint8List? bytes =
await media.loadThumbnail(size: thumbnailSize);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes!);
return decode(buffer);
} else if (media.thumbnailLink != null &&
media.thumbnailLink?.isNotEmpty == true) {
final bytes = await compute(
_loadNetworkImageInBackground,
IsolateParameters<String>(data: media.thumbnailLink!),
);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return decode(buffer);
}
throw Exception('No image source found.');
} catch (e) {
Future<void>.microtask(
() => PaintingBinding.instance.imageCache.evict(key),
);
rethrow;
}
}).then((codec) {
lock.complete(codec);
}).catchError((e, s) {
lock.completeError(e, s);
}).whenComplete(() {
_providerLocks.remove(key);
});
return lock.future;
}

Future<Uint8List> _loadNetworkImageInBackground(
IsolateParameters<String> parameters,
) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(
parameters.rootIsolateToken!,
);
final Uri resolved = Uri.base.resolve(parameters.data);
final HttpClientRequest request = await HttpClient().getUrl(resolved);
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
throw NetworkImageLoadException(
statusCode: response.statusCode,
uri: resolved,
);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
return bytes;
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is AppMediaImageProvider &&
other.media.path == media.path &&
other.thumbnailSize == thumbnailSize);
}

@override
int get hashCode => media.hashCode ^ thumbnailSize.hashCode;
}
2 changes: 1 addition & 1 deletion app/lib/components/app_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class AppPage extends StatelessWidget {
: AppBar(
backgroundColor: barBackgroundColor,
title: titleWidget ?? _title(context),
actions: actions,
actions: [...?actions, const SizedBox(width: 16)],
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
),
Expand Down
23 changes: 2 additions & 21 deletions app/lib/components/thumbnail_builder.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:data/models/media/media.dart';
import 'package:data/models/media/media_extension.dart';
import 'package:flutter/cupertino.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
import 'package:style/extensions/context_extensions.dart';
import 'package:style/indicators/circular_progress_indicator.dart';
import 'app_media_image_provider.dart';

class AppMediaImage extends StatelessWidget {
final Object? heroTag;
Expand All @@ -29,7 +25,7 @@ class AppMediaImage extends StatelessWidget {
child: Hero(
tag: heroTag ?? '',
child: Image(
image: _imageProvider(),
image: AppMediaImageProvider(media: media, thumbnailSize: size),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress != null) {
return AppMediaPlaceHolder(
Expand All @@ -52,21 +48,6 @@ class AppMediaImage extends StatelessWidget {
),
);
}

ImageProvider _imageProvider() {
if (media.sources.contains(AppMediaSource.local) && media.type.isImage) {
return FileImage(File(media.path));
} else if (media.sources.contains(AppMediaSource.local) &&
media.type.isVideo) {
return AssetEntityImageProvider(
media.assetEntity,
thumbnailSize: ThumbnailSize(size.width.toInt(), size.height.toInt()),
thumbnailFormat: ThumbnailFormat.png,
);
} else {
return CachedNetworkImageProvider(media.thumbnailLink ?? '');
}
}
}

class AppMediaPlaceHolder extends StatelessWidget {
Expand Down
44 changes: 16 additions & 28 deletions app/lib/ui/flow/home/components/app_media_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'package:style/text/app_text_style.dart';
import '../../../../domain/assets/assets_paths.dart';
import 'package:style/animations/item_selector.dart';

class AppMediaItem extends StatefulWidget {
class AppMediaItem extends StatelessWidget {
final AppMedia media;
final void Function()? onTap;
final void Function()? onLongTap;
Expand All @@ -26,31 +26,24 @@ class AppMediaItem extends StatefulWidget {
this.process,
});

@override
State<AppMediaItem> createState() => _AppMediaItemState();
}

class _AppMediaItemState extends State<AppMediaItem>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return LayoutBuilder(
builder: (context, constraints) => ItemSelector(
onTap: widget.onTap,
onLongTap: widget.onLongTap,
isSelected: widget.isSelected,
onTap: onTap,
onLongTap: onLongTap,
isSelected: isSelected,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Stack(
alignment: Alignment.bottomLeft,
children: [
AppMediaImage(
size: constraints.biggest,
media: widget.media,
heroTag: widget.media,
media: media,
heroTag: media,
),
if (widget.media.type.isVideo) _videoDuration(context),
if (media.type.isVideo) _videoDuration(context),
_sourceIndicators(context: context),
],
),
Expand All @@ -72,7 +65,7 @@ class _AppMediaItemState extends State<AppMediaItem>
),
const SizedBox(width: 2),
Text(
(widget.media.videoDuration ?? Duration.zero).format,
(media.videoDuration ?? Duration.zero).format,
style: AppTextStyles.caption.copyWith(
color: context.colorScheme.surfaceInverse,
),
Expand All @@ -85,12 +78,12 @@ class _AppMediaItemState extends State<AppMediaItem>
Widget _sourceIndicators({required BuildContext context}) {
return Row(
children: [
if (widget.media.sources.contains(AppMediaSource.googleDrive))
if (media.sources.contains(AppMediaSource.googleDrive))
_BackgroundContainer(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.media.sources.contains(AppMediaSource.googleDrive))
if (media.sources.contains(AppMediaSource.googleDrive))
SvgPicture.asset(
Assets.images.icons.googleDrive,
height: 14,
Expand All @@ -99,27 +92,25 @@ class _AppMediaItemState extends State<AppMediaItem>
],
),
),
if (widget.process?.status.isProcessing ?? false)
if (process?.status.isProcessing ?? false)
_BackgroundContainer(
margin: EdgeInsets.symmetric(
vertical: 4,
horizontal:
widget.media.sources.contains(AppMediaSource.googleDrive)
? 0
: 4,
media.sources.contains(AppMediaSource.googleDrive) ? 0 : 4,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppCircularProgressIndicator(
size: 16,
value: widget.process?.progress?.percentageInPoint,
value: process?.progress?.percentageInPoint,
color: context.colorScheme.surfaceInverse,
),
if (widget.process?.progress != null) ...[
if (process?.progress != null) ...[
const SizedBox(width: 4),
Text(
'${widget.process?.progress?.percentage.toStringAsFixed(0)}%',
'${process?.progress?.percentage.toStringAsFixed(0)}%',
style: AppTextStyles.caption.copyWith(
color: context.colorScheme.surfaceInverse,
),
Expand All @@ -128,7 +119,7 @@ class _AppMediaItemState extends State<AppMediaItem>
],
),
),
if (widget.process?.status.isWaiting ?? false)
if (process?.status.isWaiting ?? false)
_BackgroundContainer(
child: Icon(
CupertinoIcons.time,
Expand All @@ -139,9 +130,6 @@ class _AppMediaItemState extends State<AppMediaItem>
],
);
}

@override
bool get wantKeepAlive => true;
}

class _BackgroundContainer extends StatelessWidget {
Expand Down
Loading

0 comments on commit 1b136d7

Please sign in to comment.