# Android News Screen Design > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build the Android News screen — a paginated, filterable list of RSS news items with reactions and a Discuss action that opens a new general chat conversation. **Architecture:** A `NewsNotifier` (`AsyncNotifier`) holds the full accumulated item list, pagination state, and selected feed ID. The screen is a `ConsumerStatefulWidget` accessed via the More bottom sheet at `/news`. Discuss uses `POST /api/chat/from-article/{id}` (same endpoint as the web) so any backend behaviour change is automatically reflected. Reactions reuse the existing `briefingApiProvider` methods. The existing `NewsCard` widget is used without modification. **Tech Stack:** Flutter, Riverpod AsyncNotifier, Dio, GoRouter, existing `NewsCard` widget --- ## Files ### New | File | Responsibility | |------|---------------| | `lib/data/models/news_item.dart` | `NewsItem` model + `fromJson` | | `lib/data/models/briefing_feed.dart` | `BriefingFeed` model + `fromJson` | | `lib/data/api/news_api.dart` | `getNewsItems(...)` and `getFeeds()` API calls | | `lib/providers/news_provider.dart` | `NewsNotifier`, `NewsState`, `newsProvider`, `feedsProvider` | | `lib/screens/news/news_screen.dart` | News screen UI | ### Modified | File | Change | |------|--------| | `lib/data/api/chat_api.dart` | Add `openArticleInChat(int itemId) → Future` | | `lib/providers/api_client_provider.dart` | Add `newsApiProvider` | --- ## Data Models ### `NewsItem` ```dart class NewsItem { final int id; final String title; final String url; final String snippet; final String source; final DateTime? publishedAt; final List topics; final String? reaction; // 'up' | 'down' | null } ``` `fromJson` maps: `id`, `title`, `url`, `snippet`, `source`, `published_at` (nullable ISO string → `DateTime.tryParse`), `topics` (cast `List` → `List`), `reaction` (nullable string). ### `BriefingFeed` ```dart class BriefingFeed { final int id; final String title; final String url; final String? category; } ``` --- ## API Layer ### `news_api.dart` ```dart class NewsApi { final Dio _dio; const NewsApi(this._dio); // GET /api/briefing/news Future getNewsItems({ int days = 90, int limit = 40, int offset = 0, int? feedId, }) async { ... } // GET /api/briefing/feeds Future> getFeeds() async { ... } } class NewsItemsResponse { final List items; final int offset; final int limit; } ``` ### `chat_api.dart` addition ```dart // POST /api/chat/from-article/{itemId} // Returns conversation_id Future openArticleInChat(int itemId) async { ... } ``` ### `api_client_provider.dart` addition ```dart final newsApiProvider = Provider((ref) => NewsApi(ref.watch(dioProvider))); ``` --- ## State Management ### `NewsState` ```dart class NewsState { final List items; final int offset; final bool hasMore; final bool loadingMore; final int? selectedFeedId; final Map reactions; // item id → 'up'|'down'|null } ``` ### `NewsNotifier extends AsyncNotifier` - `build()`: fetches first page (offset=0, no feed filter); initialises `reactions` from `item.reaction` on each item - `loadMore()`: appends next page; no-op if `loadingMore` or `!hasMore`; sets `loadingMore = true` optimistically; on error shows snackbar (error returned to caller, not thrown into AsyncError) - `setFeed(int? feedId)`: resets `items`, `offset`, `hasMore`, `reactions`; sets `selectedFeedId`; triggers `build()`-equivalent reload via `state = AsyncLoading()` + fetch - `toggleReaction(int itemId, String reaction)`: optimistic toggle in `reactions` map; calls `briefingApi.postRssReaction` or `deleteRssReaction`; reverts on error ### `feedsProvider extends AsyncNotifier>` - `build()`: fetches once; cached for the session (no invalidation needed — feeds rarely change) --- ## Screen Behaviour ### `NewsScreen` (`ConsumerStatefulWidget`) **AppBar**: title "News", subtitle "Last 90 days" **Feed filter row** (below app bar, above list): `DropdownButton` with "All feeds" option + one entry per feed. On change calls `ref.read(newsProvider.notifier).setFeed(id)`. **List**: `ListView.builder` of `NewsCard` widgets. Each `NewsCard` receives: - `item`: `RssItemMeta.fromNewsItem(item)` — a thin adapter since `NewsCard` already uses `RssItemMeta` - `reaction`: `newsState.reactions[item.id]` - `onReaction`: calls `notifier.toggleReaction` - `onDiscuss`: calls `_handleDiscuss(item.id)` **Load more**: `ListTile` / `FilledButton.tonal` at the bottom — shows "Load more" when `hasMore && !loadingMore`, spinner when `loadingMore`, hidden when `!hasMore`. **Initial loading**: `CircularProgressIndicator` centered. Error state shows message + "Retry" button that calls `ref.invalidate(newsProvider)`. **Discuss flow** (`_handleDiscuss`): 1. Track `_openingChat = {itemId}` in local `setState` (disables that card's button while in flight) 2. Call `chatApi.openArticleInChat(itemId)` 3. On success: `context.push(Routes.chat.replaceFirst(':id', '$conversationId'))` 4. On error: show snackbar "Failed to open article in chat." 5. Always: remove from `_openingChat` --- ## `RssItemMeta` Adapter `NewsCard` currently consumes `RssItemMeta` (from `news_card.dart`). Add a factory on `RssItemMeta`: ```dart factory RssItemMeta.fromNewsItem(NewsItem item) => RssItemMeta( id: item.id, title: item.title, url: item.url, source: item.source, snippet: item.snippet, publishedAt: item.publishedAt, ); ``` This keeps `NewsCard` unchanged and avoids coupling the widget to a second model type. --- ## What This Does NOT Include - Feed management (add/remove/refresh feeds) — that is a Settings concern - Offline caching - Pull-to-refresh (load-more button is sufficient for the initial pass)