fix: resolve critical state preservation bugs

P0 Fixes:
- [P0] Fix in-place list mutation preventing pagination UI updates
Change from _posts.addAll() to _posts = [..._posts, ...new] to create
new list instances, ensuring context.select rebuilds properly
- [P0] Preserve cursor/hasMore state during refresh failures
Don't reset pagination state until refresh succeeds, preventing users
from being stuck requesting the first page repeatedly

P1 Fixes:
- [P1] Keep existing posts visible during pull-to-refresh
Only show full-screen loader when posts.isEmpty, not on every refresh
RefreshIndicator now works without wiping the visible feed
- [P1] Prevent data loss on refresh failures
Don't clear _posts until new data arrives successfully

Bug Fixes:
- Users can now see new posts appear when scrolling (pagination works)
- Pull-to-refresh keeps existing posts visible during loading
- Failed refreshes no longer wipe the feed or pagination state
- Network failures during refresh show error but preserve content

Testing:
- All 44 tests passing
- 0 compilation errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+13 -10
lib
providers
screens
+7 -4
lib/providers/feed_provider.dart
···
try {
if (refresh) {
_isLoading = true;
-
_posts = [];
-
_cursor = null;
-
_hasMore = true;
_error = null;
} else {
_isLoadingMore = true;
···
final response = await fetcher();
if (refresh) {
_posts = response.feed;
} else {
-
_posts.addAll(response.feed);
}
_cursor = response.cursor;
···
try {
if (refresh) {
_isLoading = true;
+
// DON'T clear _posts, _cursor, or _hasMore yet
+
// Keep existing data visible until refresh succeeds
+
// This prevents transient failures from wiping the user's feed and pagination state
_error = null;
} else {
_isLoadingMore = true;
···
final response = await fetcher();
+
// Only update state after successful fetch
if (refresh) {
_posts = response.feed;
} else {
+
// Create new list instance to trigger context.select rebuilds
+
// Using spread operator instead of addAll to ensure reference changes
+
_posts = [..._posts, ...response.feed];
}
_cursor = response.cursor;
+6 -6
lib/screens/home/feed_screen.dart
···
final isLoading = context.select<FeedProvider, bool>((p) => p.isLoading);
final error = context.select<FeedProvider, String?>((p) => p.error);
-
// IMPORTANT: This works because FeedProvider replaces the list (_posts = ...)
-
// rather than mutating it in-place (_posts.addAll(...)).
-
// If you change FeedProvider to use in-place mutations, this will break
-
// because Lists use reference equality by default.
final posts = context.select<FeedProvider, List<FeedViewPost>>(
(p) => p.posts,
);
···
required bool isLoadingMore,
required bool isAuthenticated,
}) {
-
// Loading state
-
if (isLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFFFF6B35)),
);
···
final isLoading = context.select<FeedProvider, bool>((p) => p.isLoading);
final error = context.select<FeedProvider, String?>((p) => p.error);
+
// IMPORTANT: This relies on FeedProvider creating new list instances
+
// (_posts = [..._posts, ...response.feed]) rather than mutating in-place.
+
// context.select uses == for comparison, and Lists use reference equality,
+
// so in-place mutations (_posts.addAll(...)) would not trigger rebuilds.
final posts = context.select<FeedProvider, List<FeedViewPost>>(
(p) => p.posts,
);
···
required bool isLoadingMore,
required bool isAuthenticated,
}) {
+
// Loading state (only show full-screen loader for initial load, not refresh)
+
if (isLoading && posts.isEmpty) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFFFF6B35)),
);