Feed Implementation - Coves Mobile App#
Date: October 28, 2025 Status: ✅ Complete Branch: main (uncommitted)
Overview#
This document details the implementation of the feed functionality for the Coves mobile app, including integration with the Coves backend API for authenticated timeline and public discovery feeds.
Features Implemented#
1. Backend API Integration#
- ✅ Connected Flutter app to Coves backend at
localhost:8081 - ✅ Implemented authenticated timeline feed (
/xrpc/social.coves.feed.getTimeline) - ✅ Implemented public discover feed (
/xrpc/social.coves.feed.getDiscover) - ✅ JWT Bearer token authentication from OAuth session
- ✅ Cursor-based pagination for infinite scroll
2. Data Models#
- ✅ Created comprehensive post models matching backend schema
- ✅ Support for external link embeds with preview images
- ✅ Community references, author info, and post stats
- ✅ Graceful handling of null/empty feed responses
3. Feed UI#
- ✅ Pull-to-refresh functionality
- ✅ Infinite scroll with pagination
- ✅ Loading states (initial, pagination, error)
- ✅ Empty state messaging
- ✅ Post cards with community badges, titles, and stats
- ✅ Link preview images with caching
- ✅ Error handling with retry capability
4. Network & Performance#
- ✅ ADB reverse port forwarding for local development
- ✅ Android network security config for HTTP localhost
- ✅ Cached image loading with retry logic
- ✅ Automatic token injection via Dio interceptors
Architecture#
File Structure#
lib/
├── models/
│ └── post.dart # Data models for posts, embeds, communities
├── services/
│ └── coves_api_service.dart # HTTP client for Coves backend API
├── providers/
│ ├── auth_provider.dart # OAuth session & token management (modified)
│ └── feed_provider.dart # Feed state management with ChangeNotifier
├── screens/home/
│ └── feed_screen.dart # Feed UI with post cards (rewritten)
└── config/
└── oauth_config.dart # API endpoint configuration (modified)
Implementation Details#
Data Models (lib/models/post.dart)#
Created comprehensive models:
TimelineResponse // Top-level feed response with cursor
└─ FeedViewPost[] // Individual feed items
├─ PostView // Post content and metadata
│ ├─ AuthorView
│ ├─ CommunityRef
│ ├─ PostStats
│ ├─ PostEmbed (optional)
│ │ └─ ExternalEmbed (for link previews)
│ └─ PostFacet[] (optional)
└─ FeedReason (optional)
Key features:
- All models use factory constructors for JSON deserialization
- Handles null feed arrays (backend returns
{"feed": null}for empty feeds) - External embeds parse thumbnail URLs, titles, descriptions
- Optional fields properly handled throughout
Example PostEmbed with ExternalEmbed:
class PostEmbed {
final String type; // e.g., "social.coves.embed.external"
final ExternalEmbed? external; // Parsed external link data
final Map<String, dynamic> data; // Raw embed data
}
class ExternalEmbed {
final String uri; // Link URL
final String? title; // Link title
final String? description; // Link description
final String? thumb; // Thumbnail image URL
final String? domain; // Domain name
}
API Service (lib/services/coves_api_service.dart)#
Purpose: HTTP client for Coves backend using Dio
Configuration:
Base URL: http://localhost:8081
Timeout: 10 seconds
Authentication: Bearer JWT tokens via interceptors
Key Methods:
-
getTimeline({String? cursor, int limit = 15})- Endpoint:
/xrpc/social.coves.feed.getTimeline - Authenticated: ✅ (requires Bearer token)
- Returns:
TimelineResponsewith personalized feed
- Endpoint:
-
getDiscover({String? cursor, int limit = 15})- Endpoint:
/xrpc/social.coves.feed.getDiscover - Authenticated: ❌ (public endpoint)
- Returns:
TimelineResponsewith public discover feed
- Endpoint:
Interceptor Architecture:
1. Auth Interceptor (adds Bearer token)
↓
2. Logging Interceptor (debug output)
↓
3. HTTP Request
Token Management:
- Token extracted from OAuth session via
AuthProvider.getAccessToken() - Automatically injected into all authenticated requests
- Token can be updated dynamically via
updateAccessToken()
Feed State Management (lib/providers/feed_provider.dart)#
Purpose: Manages feed data and loading states using ChangeNotifier pattern
State Properties:
List<FeedViewPost> posts // Current feed posts
bool isLoading // Initial load state
bool isLoadingMore // Pagination load state
String? error // Error message
String? _cursor // Pagination cursor
bool hasMore // More posts available
Key Methods:
-
fetchTimeline()- Loads authenticated user's timeline
- Clears existing posts
- Updates loading state
- Fetches access token from AuthProvider
-
fetchDiscover()- Loads public discover feed
- No authentication required
-
loadMore({required bool isAuthenticated})- Appends next page using cursor
- Prevents multiple simultaneous requests
- Updates
hasMorebased on response
-
retry({required bool isAuthenticated})- Retries failed requests
- Used by error state UI
Error Handling:
- Network errors (connection refused, timeouts)
- Authentication errors (401, token expiry)
- Empty/null responses
- User-friendly error messages
Feed UI (lib/screens/home/feed_screen.dart)#
Complete rewrite from StatelessWidget to StatefulWidget
Features:
-
Pull-to-Refresh
RefreshIndicator( onRefresh: _onRefresh, // Reloads appropriate feed (timeline/discover) ) -
Infinite Scroll
ScrollController with listener - Detects 80% scroll threshold - Triggers pagination automatically - Shows loading spinner at bottom -
UI States:
- Loading: Centered CircularProgressIndicator
- Error: Icon, message, and retry button
- Empty: Custom message based on auth status
- Content: ListView with post cards + pagination
-
Post Card Layout (
_PostCard):┌─────────────────────────────────────┐ │ [Avatar] community-name │ │ Posted by username │ │ │ │ Post Title (bold, 18px) │ │ │ │ [Link Preview Image - 180px] │ │ │ │ ↑ 42 💬 5 │ └─────────────────────────────────────┘ -
Link Preview Images (
_EmbedCard):- Uses
CachedNetworkImagefor performance - 180px height, full width, cover fit
- Loading placeholder with spinner
- Error fallback with broken image icon
- Rounded corners with border
- Uses
Lifecycle Management:
- ScrollController properly disposed
- Fetch triggered in
initState - Provider listeners cleaned up automatically
Authentication Updates (lib/providers/auth_provider.dart)#
Added method:
Future<String?> getAccessToken() async {
if (_session == null) return null;
try {
final session = await _session!.sessionGetter.get(_session!.sub);
return session.tokenSet.accessToken;
} catch (e) {
debugPrint('❌ Failed to get access token: $e');
return null;
}
}
Purpose: Extracts JWT access token from OAuth session for API authentication
Network Configuration#
Android Manifest (android/app/src/main/AndroidManifest.xml)#
Added:
<application
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
Purpose: Allows HTTP traffic to localhost for local development
Network Security Config (android/app/src/main/res/xml/network_security_config.xml)#
Created:
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.1.7</domain>
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
Purpose: Whitelists local development IPs for cleartext HTTP
Configuration Changes#
OAuth Config (lib/config/oauth_config.dart)#
Added:
// API Configuration
// Using adb reverse port forwarding, phone can access via localhost
// Setup: adb reverse tcp:8081 tcp:8081
static const String apiUrl = 'http://localhost:8081';
Main App (lib/main.dart)#
Changed from single provider to MultiProvider:
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: authProvider),
ChangeNotifierProvider(create: (_) => FeedProvider()),
],
child: const CovesApp(),
),
);
Dependencies (pubspec.yaml)#
Added:
dio: ^5.9.0 # HTTP client
cached_network_image: ^3.4.1 # Image caching with retry logic
Development Setup#
Local Backend Connection#
Problem: Android devices can't access localhost on the host machine directly.
Solution: ADB reverse port forwarding
# Create tunnel from phone's localhost:8081 -> computer's localhost:8081
adb reverse tcp:8081 tcp:8081
# Verify connection
adb reverse --list
Important Notes:
- Port forwarding persists until device disconnects or adb restarts
- Need to re-run after device reconnection
- Does not affect regular phone usage
Backend Configuration#
For local development, set in backend .env.dev:
# Skip JWT signature verification (trust any valid JWT format)
AUTH_SKIP_VERIFY=true
Then export and restart backend:
export AUTH_SKIP_VERIFY=true
# Restart backend service
⚠️ Security Warning: AUTH_SKIP_VERIFY=true is for Phase 1 local development only. Must be false in production.
Known Issues & Limitations#
1. Community Handles Not Included#
Issue: Backend CommunityRef only returns did, name, avatar - no handle field
Current Display: c/test-usnews (name only)
Desired Display: test-usnews@coves.social (full handle)
Solution: Backend needs to:
- Add
handlefield toCommunityRefstruct - Update feed SQL queries to fetch
c.handle - Populate handle in response
Status: 🔜 Backend work pending
2. Image Loading Errors#
Issue: Initial implementation with Image.network had "Connection reset by peer" errors from Kagi proxy
Solution: Switched to CachedNetworkImage which provides:
- Retry logic for flaky connections
- Disk caching for instant subsequent loads
- Better error handling
Status: ✅ Resolved
3. Post Text Body Removed#
Decision: Removed post text body from feed cards to keep UI clean
Current Display:
- Community & author
- Post title (if present)
- Link preview image (if present)
- Stats
Rationale: Text preview was redundant with title and made cards too busy
Testing Notes#
Manual Testing Performed#
✅ Feed Loading
- Authenticated timeline loads correctly
- Unauthenticated discover feed works
- Empty feed shows appropriate message
✅ Pagination
- Infinite scroll triggers at 80% threshold
- Cursor-based pagination works
- No duplicate posts loaded
✅ Pull to Refresh
- Clears and reloads feed
- Works on both timeline and discover
✅ Authentication
- Bearer tokens injected correctly
- 401 errors handled gracefully
- Token refresh tested
✅ Images
- Link preview images load successfully
- Caching works (instant load on scroll back)
- Error fallback displays for broken images
- Loading placeholder shows during fetch
✅ Error Handling
- Connection errors show retry button
- Network timeouts handled
- Null feed responses handled
✅ Performance
- Smooth 60fps scrolling
- Images don't block UI thread
- No memory leaks detected
Performance Optimizations#
-
Image Caching
CachedNetworkImageprovides disk cache- SQLite-based cache metadata
- Reduces network requests significantly
-
ListView.builder
- Only renders visible items
- Efficient for large feeds
-
Pagination
- Load 15 posts at a time
- Prevents loading entire feed upfront
-
State Management
- ChangeNotifier only rebuilds affected widgets
- No unnecessary full-screen rebuilds
Future Enhancements#
Short Term#
- Update UI to use community handles when backend provides them
- Add post detail view (tap to expand)
- Add comment counts and voting UI
- Implement user profile avatars (currently placeholder)
- Add community avatars (currently initials only)
Medium Term#
- Add post creation flow
- Implement voting (upvote/downvote)
- Add comment viewing
- Support image galleries (multiple images)
- Support video embeds
Long Term#
- Offline support with local cache
- Push notifications for feed updates
- Advanced feed filtering/sorting
- Search functionality
PR Review Fixes (October 28, 2025)#
After initial implementation, a comprehensive code review identified several critical issues that have been addressed:
🚨 Critical Issues Fixed#
1. P1: Access Token Caching Issue#
Problem: Access tokens were cached in CovesApiService, causing 401 errors after ~1 hour when atProto OAuth rotates tokens.
Fix: lib/services/coves_api_service.dart:19-75
- Changed from
setAccessToken(String?)to constructor-injectedtokenGetterfunction - Dio interceptor now fetches fresh token before every authenticated request
- Prevents stale credential issues entirely
Before:
void setAccessToken(String? token) {
_accessToken = token; // ❌ Cached, becomes stale
}
After:
CovesApiService({Future<String?> Function()? tokenGetter})
: _tokenGetter = tokenGetter;
onRequest: (options, handler) async {
final token = await _tokenGetter(); // ✅ Fresh every time
options.headers['Authorization'] = 'Bearer $token';
}
2. Business Logic in Widget Layer#
Problem: FeedScreen contained authentication decision logic, violating clean architecture.
Fix: lib/providers/feed_provider.dart:45-55
- Moved auth-based feed selection logic into
FeedProvider.loadFeed() - Widget layer now simply calls provider methods without business logic
Before (in FeedScreen):
void _loadFeed() async {
if (authProvider.isAuthenticated) {
final token = await authProvider.getAccessToken();
feedProvider.setAccessToken(token);
feedProvider.fetchTimeline(refresh: true); // ❌ Business logic in UI
} else {
feedProvider.fetchDiscover(refresh: true);
}
}
After (in FeedProvider):
Future<void> loadFeed({bool refresh = false}) async {
if (_authProvider.isAuthenticated) { // ✅ Logic in provider
await fetchTimeline(refresh: refresh);
} else {
await fetchDiscover(refresh: refresh);
}
}
After (in FeedScreen):
void _loadFeed() {
feedProvider.loadFeed(refresh: true); // ✅ No business logic
}
3. Production Security Risk#
Problem: Network security config allowed cleartext HTTP without warnings, risking production leak.
Fix: android/app/src/main/res/xml/network_security_config.xml:3-15
- Added prominent XML comments warning about development-only usage
- Added TODO items for production build flavors
- Clear documentation that cleartext is ONLY for localhost
4. Missing Test Coverage#
Problem: No tests for critical auth and feed functionality.
Fix: Created comprehensive test files with 200+ lines each
test/providers/auth_provider_test.dart- Unit tests for authenticationtest/providers/feed_provider_test.dart- Unit tests for feed statetest/widgets/feed_screen_test.dart- Widget tests for UI
Added dependencies:
mockito: ^5.4.4
build_runner: ^2.4.13
Test coverage includes:
- Sign in/out flows with error handling
- Token refresh failure → auto sign-out
- Feed loading (timeline/discover)
- Pagination and infinite scroll
- Error states and retry logic
- Widget lifecycle (mounted checks, dispose)
- Accessibility (Semantics widgets)
⚠️ Important Issues Fixed#
5. Code Duplication (DRY Violation)#
Problem: fetchTimeline() and fetchDiscover() had 90% identical code.
Fix: lib/providers/feed_provider.dart:57-117
- Extracted common logic into
_fetchFeed()method - Both methods now use shared implementation
After:
Future<void> _fetchFeed({
required bool refresh,
required Future<TimelineResponse> Function() fetcher,
required String feedName,
}) async {
// Common logic: loading states, error handling, pagination
}
Future<void> fetchTimeline({bool refresh = false}) => _fetchFeed(
refresh: refresh,
fetcher: () => _apiService.getTimeline(...),
feedName: 'Timeline',
);
6. Token Refresh Failure Handling#
Problem: If token refresh failed (e.g., revoked server-side), app stayed in "authenticated" state with broken tokens.
Fix: lib/providers/auth_provider.dart:47-65
- Added automatic sign-out when
getAccessToken()throws - Clears invalid session state immediately
After:
try {
final session = await _session!.sessionGetter.get(_session!.sub);
return session.tokenSet.accessToken;
} catch (e) {
debugPrint('🔄 Token refresh failed - signing out user');
await signOut(); // ✅ Clear broken session
return null;
}
7. No SafeArea Handling#
Problem: Content could be obscured by notches, home indicators, system UI.
Fix: lib/screens/home/feed_screen.dart:71-73
body: SafeArea(
child: _buildBody(feedProvider, isAuthenticated),
),
8. Inefficient Provider Listeners#
Problem: Widget rebuilt on every AuthProvider change, not just isAuthenticated.
Fix: lib/screens/home/feed_screen.dart:60
// Before
final authProvider = Provider.of<AuthProvider>(context); // ❌ Rebuilds on any change
// After
final isAuthenticated = context.select<AuthProvider, bool>(
(p) => p.isAuthenticated // ✅ Only rebuilds when this specific field changes
);
9. Missing Mounted Check#
Problem: addPostFrameCallback could execute after widget disposal.
Fix: lib/screens/home/feed_screen.dart:25-28
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { // ✅ Check before using context
_loadFeed();
}
});
10. Network Timeout Too Short#
Problem: 10-second timeouts fail on slow mobile networks (3G, poor signal).
Fix: lib/services/coves_api_service.dart:23-24
connectTimeout: const Duration(seconds: 30), // ✅ Was 10s
receiveTimeout: const Duration(seconds: 30),
11. Missing Accessibility#
Problem: No screen reader support for feed posts.
Fix: lib/screens/home/feed_screen.dart:191-195
return Semantics(
label: 'Feed post in ${post.post.community.name} by ${author}. ${title}',
button: true,
child: _PostCard(post: post),
);
💡 Suggestions Implemented#
12. Debug Prints Not Wrapped#
Fix: lib/screens/home/feed_screen.dart:367-370
if (kDebugMode) { // ✅ No logging overhead in production
debugPrint('❌ Image load error: $error');
}
Code Quality#
✅ Flutter Analyze: 0 errors, 0 warnings
flutter analyze lib/
# Result: No errors, 0 warnings (7 deprecation infos in unrelated file)
✅ Architecture Compliance:
- Clean separation: UI → Provider → Service
- No business logic in widgets
- Dependencies injected via constructors
- State management consistently applied
✅ Security:
- Fresh token retrieval prevents stale credentials
- Token refresh failures trigger sign-out
- Production warnings in network config
✅ Performance:
- Optimized widget rebuilds (context.select)
- 30-second timeouts for mobile networks
- SafeArea prevents UI obstruction
✅ Accessibility:
- Semantics labels for screen readers
- Proper focus management
✅ Testing:
- Comprehensive unit tests for providers
- Widget tests for UI components
- Mock implementations for services
- Error state coverage
✅ Best Practices Followed:
- Controllers properly disposed
- Const constructors used where possible
- Null safety throughout
- Error handling comprehensive
- Debug logging for troubleshooting
- Clean separation of concerns
- DRY principle (no code duplication)
Deployment Checklist#
Before deploying to production:
- Change backend URL from
localhost:8081to production endpoint - Remove cleartext traffic permissions from Android config
- Ensure
AUTH_SKIP_VERIFY=falsein backend production environment - Test with real OAuth tokens from production PDS
- Verify image caching works with production CDN
- Add analytics tracking for feed engagement
- Add error reporting (Sentry, Firebase Crashlytics)
- Test on both iOS and Android physical devices
- Performance testing with large feeds (100+ posts)
Resources#
Backend Endpoints#
- Timeline:
GET /xrpc/social.coves.feed.getTimeline?cursor={cursor}&limit={limit} - Discover:
GET /xrpc/social.coves.feed.getDiscover?cursor={cursor}&limit={limit}
Key Dependencies#
dio: ^5.9.0- HTTP clientcached_network_image: ^3.4.1- Image cachingprovider: ^6.1.5+1- State management
Related Documentation#
CLAUDE.md- Project instructions and guidelines- Backend PRD:
/home/bretton/Code/Coves/docs/PRD_POSTS.md - Backend Community Feeds:
/home/bretton/Code/Coves/docs/COMMUNITY_FEEDS.md
Contributors#
- Implementation: Claude (AI Assistant)
- Product Direction: @bretton
- Backend: Coves AppView API
This implementation document reflects the state of the codebase as of October 28, 2025.