feat: Implement image cache eviction for S3 images and enhance image loading error handling.

This commit is contained in:
pradeepkumar
2026-03-14 15:26:08 +05:30
parent ad94db5620
commit 7b9c19ec67
4 changed files with 53 additions and 35 deletions

View File

@@ -1,4 +1,3 @@
import 'package:dio/dio.dart';
import 'package:real_estate_mobile/core/network/api_client.dart'; import 'package:real_estate_mobile/core/network/api_client.dart';
/// Resolves S3 keys to presigned download URLs via the backend API. /// Resolves S3 keys to presigned download URLs via the backend API.
@@ -54,10 +53,16 @@ class ImageUrlResolver {
return data['url'] as String?; return data['url'] as String?;
} }
return null; return null;
} on DioException { } catch (_) {
return null; return null;
} }
} }
void clearCache() => _cache.clear(); void clearCache() => _cache.clear();
/// Evict a single key from the in-memory cache so the next resolve
/// fetches a fresh presigned URL.
void evict(String? key) {
if (key != null) _cache.remove(key);
}
} }

View File

@@ -60,13 +60,17 @@ class _S3ImageState extends State<S3Image> {
return; return;
} }
final url = await ImageUrlResolver.instance.resolve(widget.imageUrl); try {
if (mounted) { final url = await ImageUrlResolver.instance.resolve(widget.imageUrl);
setState(() { if (mounted) {
_resolvedUrl = url; setState(() {
_loading = false; _resolvedUrl = url;
_failed = url == null; _loading = false;
}); _failed = url == null;
});
}
} catch (_) {
if (mounted) setState(() { _loading = false; _failed = true; });
} }
} }
@@ -102,12 +106,13 @@ class _S3ImageState extends State<S3Image> {
return CachedNetworkImage( return CachedNetworkImage(
imageUrl: _resolvedUrl!, imageUrl: _resolvedUrl!,
cacheKey: widget.imageUrl,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
fit: widget.fit, fit: widget.fit,
alignment: widget.alignment, alignment: widget.alignment,
placeholder: (ctx, _) => _buildPlaceholder(ctx), placeholder: (ctx, _) => _buildPlaceholder(ctx),
errorWidget: (ctx, _, __) => _buildError(ctx), errorWidget: (ctx, _, _) => _buildError(ctx),
); );
} }
} }

View File

@@ -126,33 +126,32 @@ class _ChangePasswordTabState extends ConsumerState<ChangePasswordTab> {
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
SizedBox( TextField(
height: 38, controller: controller,
child: TextField( obscureText: obscure,
controller: controller, textAlignVertical: TextAlignVertical.center,
obscureText: obscure, style: const TextStyle(
style: const TextStyle( fontFamily: 'SourceSerif4',
fontFamily: 'SourceSerif4', fontSize: 14,
fontSize: 14, fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, color: AppColors.primaryDark,
color: AppColors.primaryDark, ),
), decoration: InputDecoration(
decoration: InputDecoration( isDense: true,
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 26, vertical: 8), const EdgeInsets.fromLTRB(12, 12, 8, 12),
border: border, border: border,
enabledBorder: border, enabledBorder: border,
focusedBorder: border, focusedBorder: border,
suffixIcon: GestureDetector( suffixIcon: GestureDetector(
onTap: onToggle, onTap: onToggle,
child: Icon( child: Icon(
obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined,
size: 18, size: 18,
color: AppColors.primaryDark.withValues(alpha: 0.5), color: AppColors.primaryDark.withValues(alpha: 0.5),
),
), ),
suffixIconConstraints: const BoxConstraints(minWidth: 40),
), ),
suffixIconConstraints: const BoxConstraints(minWidth: 40),
), ),
), ),
], ],

View File

@@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/utils/image_url_resolver.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
@@ -99,6 +101,13 @@ class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
avatarUrl = user?['avatar'] as String?; avatarUrl = user?['avatar'] as String?;
} }
// Evict stale presigned URL cache and disk cache for the avatar
// so S3Image always resolves a fresh presigned URL on profile load.
if (avatarUrl != null && avatarUrl.isNotEmpty) {
ImageUrlResolver.instance.evict(avatarUrl);
CachedNetworkImage.evictFromCache(avatarUrl);
}
final descriptionText = isAgent final descriptionText = isAgent
? 'This information will be displayed on your public agent page.' ? 'This information will be displayed on your public agent page.'
: 'This information will be displayed on your profile.'; : 'This information will be displayed on your profile.';