precache slides

slides take a long time to decode, so we cache them
This commit is contained in:
2025-08-30 12:44:00 +02:00
parent 1241c76acd
commit b61ecb8f05
3 changed files with 220 additions and 124 deletions

View File

@@ -1,86 +0,0 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// Taken from https://gist.github.com/darmawan01/9be266df44594ea59f07032e325ffa3b
// and adapted to use assets
final _globalImageCache = <String, Uint8List>{};
Future<void> precacheAsset(AssetBundle bundle, String asset) async {
if (!_globalImageCache.containsKey(asset)) {
final data = await bundle.load(asset);
_globalImageCache[asset] = data.buffer.asUint8List();
}
}
Future<void> precacheAssets(AssetBundle bundle, List<String> assets) async {
for (final asset in assets) {
await precacheAsset(bundle, asset);
}
}
Future<Uint8List> _cacheLoad(AssetBundle bundle, String asset) async {
if (!_globalImageCache.containsKey(asset)) {
final data = await bundle.load(asset);
_globalImageCache[asset] = data.buffer.asUint8List();
}
return Future.value(_globalImageCache[asset]!);
}
class CacheMemoryImageProvider extends ImageProvider<CacheMemoryImageProvider> {
final AssetBundle bundle;
final String path;
Uint8List? _img;
CacheMemoryImageProvider(this.bundle, this.path);
@override
ImageStreamCompleter loadImage(
CacheMemoryImageProvider key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(decode),
scale: 1.0,
debugLabel: path,
informationCollector: () sync* {
yield ErrorDescription('Tag: $path');
},
);
}
Future<Codec> _loadAsync(ImageDecoderCallback decode) async {
_img ??= await _cacheLoad(bundle, path);
// the DefaultCacheManager() encapsulation, it get cache from local storage.
final Uint8List bytes = _img!;
if (bytes.lengthInBytes == 0) {
// The file may become available later.
PaintingBinding.instance.imageCache.evict(this);
throw StateError('$path is empty and cannot be loaded as an image.');
}
final buffer = await ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
}
@override
Future<CacheMemoryImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<CacheMemoryImageProvider>(this);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
bool res = other is CacheMemoryImageProvider && other.path == path;
return res;
}
@override
int get hashCode => path.hashCode;
@override
String toString() =>
'${objectRuntimeType(this, 'CacheImageProvider')}("$path")';
}

View File

@@ -0,0 +1,183 @@
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class ImagePreloader {
static final Map<String, ui.Image> _cache = {};
static final Map<String, Future<ui.Image>> _loadingFutures = {};
static Future<ui.Image> preloadAssetImage(
AssetBundle bundle, String assetPath) async {
if (_cache.containsKey(assetPath)) {
return _cache[assetPath]!;
}
if (_loadingFutures.containsKey(assetPath)) {
return _loadingFutures[assetPath]!;
}
final future = _loadAssetImage(bundle, assetPath);
_loadingFutures[assetPath] = future;
try {
final image = await future;
_cache[assetPath] = image;
return image;
} finally {
_loadingFutures.remove(assetPath);
}
}
static Future<ui.Image> preloadNetworkImage(String url) async {
if (_cache.containsKey(url)) {
return _cache[url]!;
}
if (_loadingFutures.containsKey(url)) {
return _loadingFutures[url]!;
}
final future = _loadNetworkImage(url);
_loadingFutures[url] = future;
try {
final image = await future;
_cache[url] = image;
return image;
} finally {
_loadingFutures.remove(url);
}
}
static Future<List<ui.Image>> preloadMultipleAssets(
AssetBundle bundle, List<String> assetPaths) async {
final futures =
assetPaths.map((path) => preloadAssetImage(bundle, path)).toList();
return await Future.wait(futures);
}
static Future<List<ui.Image>> preloadWithProgress(
AssetBundle bundle,
List<String> assetPaths,
Function(int loaded, int total)? onProgress,
) async {
final List<ui.Image> results = [];
for (int i = 0; i < assetPaths.length; i++) {
final image = await preloadAssetImage(bundle, assetPaths[i]);
results.add(image);
onProgress?.call(i + 1, assetPaths.length);
}
return results;
}
static ui.Image? getCachedImage(String key) {
return _cache[key];
}
static bool isCached(String key) {
return _cache.containsKey(key);
}
static int getCacheSize() {
return _cache.length;
}
static void clearImage(String key) {
_cache.remove(key);
}
static void clearCache() {
for (final image in _cache.values) {
image.dispose();
}
_cache.clear();
_loadingFutures.clear();
}
static void trimCache(int maxSize) {
if (_cache.length <= maxSize) return;
final keys = _cache.keys.toList();
final keysToRemove = keys.take(_cache.length - maxSize);
for (final key in keysToRemove) {
_cache[key]?.dispose();
_cache.remove(key);
}
}
static Future<ui.Image> _loadAssetImage(
AssetBundle bundle, String assetPath) async {
final ByteData data = await bundle.load(assetPath);
final Uint8List bytes = data.buffer.asUint8List();
return await _decodeImageFromBytes(bytes);
}
static Future<ui.Image> _loadNetworkImage(String url) async {
throw UnimplementedError(
'Network image loading not implemented in this example');
}
static Future<ui.Image> _decodeImageFromBytes(Uint8List bytes) async {
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
}
// NOTE: Only works on non animated images.
class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
final AssetBundle assetBundle;
final String assetPath;
const PreloadedImageProvider(this.assetBundle, this.assetPath);
@override
Future<PreloadedImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<PreloadedImageProvider>(this);
}
@override
ImageStreamCompleter loadImage(
PreloadedImageProvider key, ImageDecoderCallback decode) {
return OneFrameImageStreamCompleter(_loadAsync(key));
}
Future<ImageInfo> _loadAsync(PreloadedImageProvider key) async {
// clone the image before using it to prevent from the original image
// getting disposed
final cachedImage = ImagePreloader.getCachedImage(key.assetPath)?.clone();
if (cachedImage != null) {
return ImageInfo(image: cachedImage);
}
try {
final image =
await ImagePreloader.preloadAssetImage(assetBundle, key.assetPath);
return ImageInfo(image: image.clone());
} catch (e) {
final ByteData data = await assetBundle.load(key.assetPath);
final Uint8List bytes = data.buffer.asUint8List();
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frameInfo = await codec.getNextFrame();
return ImageInfo(image: frameInfo.image.clone());
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is PreloadedImageProvider && other.assetPath == assetPath;
}
@override
int get hashCode => assetPath.hashCode;
@override
String toString() => 'PreloadedImageProvider("$assetPath")';
}

View File

@@ -1,21 +1,14 @@
import 'dart:math' as math;
import 'package:carousel_slider/carousel_slider.dart';
import 'package:firka/helpers/api/client/kreta_client.dart';
import 'package:firka/helpers/api/consts.dart';
import 'package:firka/helpers/db/models/token_model.dart';
import 'package:firka/helpers/firka_bundle.dart';
import 'package:firka/main.dart';
import 'package:firka/ui/phone/widgets/login_webview.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../../helpers/api/token_grant.dart';
import '../../../../helpers/cache_memory_image_provider.dart';
import '../../../../helpers/image_preloader.dart';
import '../../../model/style.dart';
import '../home/home_screen.dart';
class LoginScreen extends StatefulWidget {
final AppInitialization data;
@@ -28,7 +21,6 @@ class LoginScreen extends StatefulWidget {
class _LoginScreenState extends State<LoginScreen> {
late LoginWebviewWidget _loginWebView;
bool _preloadDone = false;
@override
@@ -45,32 +37,41 @@ class _LoginScreenState extends State<LoginScreen> {
systemNavigationBarColor: Color(0xFFFAFFF0),
));
() async {
final firkaBundle = FirkaBundle();
_preloadImages();
}
await precacheAssets(firkaBundle, [
"assets/images/carousel/slide1.webp",
"assets/images/carousel/slide1_background.webp",
"assets/images/carousel/slide2.webp",
"assets/images/carousel/slide2_background.webp",
"assets/images/carousel/slide3.webp",
"assets/images/carousel/slide3_foreground.webp",
"assets/images/carousel/slide4.webp",
"assets/images/carousel/slide4_background.webp"
]);
Future<void> _preloadImages() async {
final imagePaths = [
"assets/images/carousel/slide1.webp",
"assets/images/carousel/slide2.webp",
"assets/images/carousel/slide3.webp",
"assets/images/carousel/slide4.webp",
"assets/images/logos/colored_logo.webp",
];
try {
// Preload with progress tracking
await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths);
// All images are now decoded and cached
setState(() {
_preloadDone = true;
});
}();
} catch (e) {
print('Error preloading images: $e');
// Fallback: continue anyway
setState(() {
_preloadDone = true;
});
}
}
@override
Widget build(BuildContext context) {
if (!_preloadDone) {
return MaterialApp(
home: SizedBox(),
);
home: SizedBox(),
);
}
final paddingWidthHorizontal = MediaQuery.of(context).size.width -
@@ -145,7 +146,7 @@ class _LoginScreenState extends State<LoginScreen> {
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: CacheMemoryImageProvider(
image: PreloadedImageProvider(
DefaultAssetBundle.of(context),
'assets/images/logos/colored_logo.webp'),
fit: BoxFit.cover,
@@ -217,12 +218,11 @@ class _LoginScreenState extends State<LoginScreen> {
child: Transform.scale(
scale: slides[index]['scale']
as double,
child: Image(
image: CacheMemoryImageProvider(
DefaultAssetBundle.of(
context),
slides[index]['background']!
as String),
child: Image.asset(
slides[index]['background']!
as String,
bundle: DefaultAssetBundle.of(
context),
fit: BoxFit.contain,
width: double.infinity,
)),
@@ -238,7 +238,7 @@ class _LoginScreenState extends State<LoginScreen> {
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Image(
image: CacheMemoryImageProvider(
image: PreloadedImageProvider(
DefaultAssetBundle.of(context),
slides[index]['picture']! as String),
fit: BoxFit.cover,
@@ -266,12 +266,11 @@ class _LoginScreenState extends State<LoginScreen> {
child: Transform.scale(
scale: slides[index]['scale']
as double,
child: Image(
image: CacheMemoryImageProvider(
DefaultAssetBundle.of(
context),
slides[index]['foreground']!
as String),
child: Image.asset(
slides[index]['foreground']!
as String,
bundle: DefaultAssetBundle.of(
context),
fit: BoxFit.cover,
width: MediaQuery.of(context)
.size