feat: Implement image cache eviction for S3 images and enhance image loading error handling.
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class _S3ImageState extends State<S3Image> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
final url = await ImageUrlResolver.instance.resolve(widget.imageUrl);
|
final url = await ImageUrlResolver.instance.resolve(widget.imageUrl);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -68,6 +69,9 @@ class _S3ImageState extends State<S3Image> {
|
|||||||
_failed = url == null;
|
_failed = url == null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() { _loading = false; _failed = true; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShimmerPlaceholder() {
|
Widget _buildShimmerPlaceholder() {
|
||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,11 +126,10 @@ class _ChangePasswordTabState extends ConsumerState<ChangePasswordTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
SizedBox(
|
TextField(
|
||||||
height: 38,
|
|
||||||
child: TextField(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
obscureText: obscure,
|
obscureText: obscure,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -138,8 +137,9 @@ class _ChangePasswordTabState extends ConsumerState<ChangePasswordTab> {
|
|||||||
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,
|
||||||
@@ -154,7 +154,6 @@ class _ChangePasswordTabState extends ConsumerState<ChangePasswordTab> {
|
|||||||
suffixIconConstraints: const BoxConstraints(minWidth: 40),
|
suffixIconConstraints: const BoxConstraints(minWidth: 40),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.';
|
||||||
|
|||||||
Reference in New Issue
Block a user