feat: cache and poll the user profile page

This commit is contained in:
sBubshait 2025-07-28 10:02:10 +03:00
parent ff3ca9ab1f
commit e73bc88879
5 changed files with 236 additions and 38 deletions

View File

@ -45,10 +45,14 @@ class _ProfilePageState extends State<ProfilePage> {
print('Setting up user profile stream');
_userStreamSubscription = UserService.getUserStream().listen(
(updatedUserData) {
print('Cache change detected - triggering refresh like refresh button');
print('📱 User profile stream received data update');
if (mounted) {
// Force refresh like the refresh button
_loadUserData(forceRefresh: true);
// Update UI directly with stream data instead of making API call
setState(() {
userData = updatedUserData;
isLoadingUser = false;
});
// Only reload stats which are from posts service
_loadUserStats();
}
},
@ -56,6 +60,13 @@ class _ProfilePageState extends State<ProfilePage> {
print('User profile stream error: $error');
},
);
// Trigger stream with any existing cached data
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
UserService.notifyStreamWithCurrentData();
}
});
}
Future<void> _loadFCMToken() async {
@ -109,16 +120,18 @@ class _ProfilePageState extends State<ProfilePage> {
Future<void> _loadUserStats() async {
final result = await PostService.getUserPosts();
setState(() {
if (result['success'] == true) {
final posts = result['posts'] as List<Post>;
totalPosts = posts.length;
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
} else {
totalPosts = 0;
totalLikes = 0;
}
});
if (mounted) {
setState(() {
if (result['success'] == true) {
final posts = result['posts'] as List<Post>;
totalPosts = posts.length;
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
} else {
totalPosts = 0;
totalLikes = 0;
}
});
}
}
void _showErrorAlert(String message) {
@ -294,7 +307,7 @@ class _ProfilePageState extends State<ProfilePage> {
),
SizedBox(height: 16),
ProfilePostsList(
fetchPosts: PostService.getUserPosts,
fetchPosts: () => PostService.getUserPosts(),
onStatsUpdate: (posts, likes) {
setState(() {
totalPosts = posts;

View File

@ -28,12 +28,14 @@ class CacheService {
// Cache durations
static const Duration _postsCacheDuration = Duration(seconds: 30);
static const Duration _invitationsCacheDuration = Duration(seconds: 30);
static const Duration _userCacheDuration = Duration(hours: 3);
static const Duration _userCacheDuration = Duration(hours: 6);
static const Duration _userPostsCacheDuration = Duration(seconds: 30);
// Polling intervals
static const Duration _postsPollingInterval = Duration(seconds: 5);
static const Duration _invitationsPollingInterval = Duration(seconds: 5);
static const Duration _userPollingInterval = Duration(hours: 3);
static const Duration _userPostsPollingInterval = Duration(seconds: 5);
/// Get cached data if available and not expired, otherwise return null
static T? getCached<T>(String key) {
@ -212,6 +214,9 @@ class CacheService {
if (key == postsAllKey && data is List<Post>) {
print('📱 Notifying posts stream with ${data.length} posts');
notifyStreamListeners<List<Post>>(key, data);
} else if (key == userPostsKey && data is List<Post>) {
print('📱 Notifying user posts stream with ${data.length} posts');
notifyStreamListeners<List<Post>>(key, data);
} else if (key == invitationsAllKey && data is InvitationsData) {
print('📱 Notifying invitations stream');
notifyStreamListeners<InvitationsData>(key, data);
@ -291,13 +296,16 @@ class CacheService {
static const String postsAllKey = 'posts_all';
static const String invitationsAllKey = 'invitations_all';
static const String userProfileKey = 'user_profile';
static const String userPostsKey = 'user_posts';
// Helper methods for specific cache durations
static Duration get postsCacheDuration => _postsCacheDuration;
static Duration get invitationsCacheDuration => _invitationsCacheDuration;
static Duration get userCacheDuration => _userCacheDuration;
static Duration get userPostsCacheDuration => _userPostsCacheDuration;
static Duration get postsPollingInterval => _postsPollingInterval;
static Duration get invitationsPollingInterval => _invitationsPollingInterval;
static Duration get userPollingInterval => _userPollingInterval;
static Duration get userPostsPollingInterval => _userPostsPollingInterval;
}

View File

@ -7,6 +7,7 @@ import 'cache_service.dart';
class PostService {
static bool _pollingStarted = false;
static bool _userPostsPollingStarted = false;
/// Get all posts with caching - returns cached data if available and fresh
static Future<Map<String, dynamic>> getAllPosts({
@ -139,29 +140,126 @@ class PostService {
}
}
/// Start background polling for user posts
static void startUserPostsPolling() {
_userPostsPollingStarted = true;
CacheService.startPolling(
CacheService.userPostsKey,
() => _fetchAndCacheUserPosts(true), // Mark as from polling
CacheService.userPostsPollingInterval,
CacheService.userPostsCacheDuration,
);
}
/// Stop background polling for user posts
static void stopUserPostsPolling() {
CacheService.stopPolling(CacheService.userPostsKey);
_userPostsPollingStarted = false;
}
/// Get user posts stream for real-time updates
static Stream<List<Post>> getUserPostsStream() {
return CacheService.getStream<List<Post>>(CacheService.userPostsKey);
}
/// Force notify user posts stream with current cached data
static void notifyUserPostsStreamWithCurrentData() {
final cached = CacheService.getCachedStale<List<Post>>(
CacheService.userPostsKey,
);
if (cached != null) {
// Re-notify the stream with current data
CacheService.notifyStreamListeners<List<Post>>(
CacheService.userPostsKey,
cached,
);
}
}
/// Check if polling is active
static bool get isPollingActive => _pollingStarted;
/// Check if user posts polling is active
static bool get isUserPostsPollingActive => _userPostsPollingStarted;
/// Invalidate posts cache (useful after creating/updating posts)
static void invalidateCache() {
CacheService.clearCache(CacheService.postsAllKey);
}
static Future<Map<String, dynamic>> getUserPosts() async {
/// Invalidate user posts cache (useful after creating posts)
static void invalidateUserPostsCache() {
CacheService.clearCache(CacheService.userPostsKey);
}
/// Get user posts with caching - returns cached data if available and fresh
static Future<Map<String, dynamic>> getUserPosts({
bool forceRefresh = false,
}) async {
// Return cached data if available and not forcing refresh
if (!forceRefresh) {
final cached = CacheService.getCached<List<Post>>(
CacheService.userPostsKey,
);
if (cached != null &&
!CacheService.isCacheExpired(
CacheService.userPostsKey,
CacheService.userPostsCacheDuration,
)) {
return {
'success': true,
'message': 'Loaded from cache',
'posts': cached,
};
}
// Return stale data while fetching fresh data in background
final stale = CacheService.getCachedStale<List<Post>>(
CacheService.userPostsKey,
);
if (stale != null && !forceRefresh) {
// Trigger background refresh
_fetchAndCacheUserPosts();
return {
'success': true,
'message': 'Loaded from cache (refreshing in background)',
'posts': stale,
};
}
}
// Fetch fresh data
return await _fetchAndCacheUserPosts();
}
static Future<Map<String, dynamic>> _fetchAndCacheUserPosts([bool fromPolling = false]) async {
try {
final response = await HttpService.get('/posts/user');
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
final posts = (responseData['data'] as List?)
?.map((post) => Post.fromJson(post))
.toList() ??
[];
// Only cache when not called from polling to prevent conflicts
if (!fromPolling) {
CacheService.setCached(CacheService.userPostsKey, posts);
}
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'posts':
(responseData['data'] as List?)
?.map((post) => Post.fromJson(post))
.toList() ??
[],
'posts': posts,
};
} else if (response.statusCode == 401 || response.statusCode == 403) {
await AuthService.handleAuthenticationError();
return {
'success': false,
'message': 'Session expired. Please login again.',
'posts': <Post>[],
};
} else {
return {
@ -191,8 +289,9 @@ class PostService {
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
// Invalidate posts cache since we added a new post
// Invalidate both posts caches since we added a new post
invalidateCache();
invalidateUserPostsCache();
return {
'success': responseData['status'] ?? false,

View File

@ -110,6 +110,20 @@ class UserService {
return CacheService.getStream<Map<String, dynamic>>(CacheService.userProfileKey);
}
/// Force notify stream with current cached data (useful for initial stream setup)
static void notifyStreamWithCurrentData() {
final cached = CacheService.getCachedStale<Map<String, dynamic>>(
CacheService.userProfileKey,
);
if (cached != null) {
// Re-notify the stream with current data
CacheService.notifyStreamListeners<Map<String, dynamic>>(
CacheService.userProfileKey,
cached,
);
}
}
/// Check if polling is active
static bool get isPollingActive => _pollingStarted;

View File

@ -511,40 +511,104 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
bool _isLoading = true;
List<Post> _posts = [];
String _errorMessage = '';
StreamSubscription<List<Post>>? _userPostsStreamSubscription;
@override
void initState() {
super.initState();
_setupUserPostsStream();
_loadPosts();
// Start user posts polling when profile page is active
PostService.startUserPostsPolling();
}
Future<void> _loadPosts() async {
setState(() {
_isLoading = true;
_errorMessage = '';
});
@override
void dispose() {
_userPostsStreamSubscription?.cancel();
// Stop user posts polling when profile page is disposed
PostService.stopUserPostsPolling();
super.dispose();
}
try {
final result = await widget.fetchPosts();
setState(() {
if (result['success']) {
_posts = result['posts'];
void _setupUserPostsStream() {
print('Setting up user posts stream for profile');
_userPostsStreamSubscription?.cancel(); // Cancel any existing subscription
_userPostsStreamSubscription = PostService.getUserPostsStream().listen(
(updatedPosts) {
print('📱 User posts stream received data: ${updatedPosts.length} posts');
if (mounted) {
// Update the posts directly instead of fetching again
setState(() {
_posts = updatedPosts;
_isLoading = false;
_errorMessage = '';
});
// Update parent stats
if (widget.onStatsUpdate != null) {
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
widget.onStatsUpdate!(_posts.length, totalLikes);
}
print('🎯 User posts UI updated with ${updatedPosts.length} posts - setState called');
} else {
_errorMessage = result['message'] ?? 'Failed to load posts';
print('⚠️ Widget not mounted, skipping UI update');
}
_isLoading = false;
});
} catch (e) {
},
onError: (error) {
print('User posts stream error: $error');
if (mounted) {
setState(() {
_errorMessage = 'Stream error: $error';
});
}
},
);
// Trigger stream with any existing cached data
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
PostService.notifyUserPostsStreamWithCurrentData();
}
});
}
Future<void> _loadPosts() async {
// Don't show loading for cached data unless it's the initial load
final isInitialLoad = _posts.isEmpty;
if (isInitialLoad) {
setState(() {
_errorMessage = 'Network error: $e';
_isLoading = false;
_isLoading = true;
_errorMessage = '';
});
}
try {
final result = await widget.fetchPosts();
if (mounted) {
setState(() {
if (result['success']) {
_posts = result['posts'];
// Update parent stats
if (widget.onStatsUpdate != null) {
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
widget.onStatsUpdate!(_posts.length, totalLikes);
}
_errorMessage = '';
} else {
_errorMessage = result['message'] ?? 'Failed to load posts';
}
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Network error: $e';
_isLoading = false;
});
}
}
}
@override