# Knowledge View — Android App Design Spec > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace the Library tab with a typed Knowledge view and add a dedicated Projects tab, bringing the Android app to parity with the web Knowledge view while using the existing backend `/api/knowledge` and `/api/projects` endpoints. **Architecture:** Two-tier pagination — fetch IDs cheaply (50 at a time), hydrate visible items in batches of 12. Type tabs drive server-side filtering. A new Projects tab replaces the project section formerly in Library. All data access goes through the existing Riverpod/Dio/repository pattern. **Tech Stack:** Flutter 3, Riverpod 3, GoRouter 17, Dio 5, existing backend REST API (`/api/knowledge`, `/api/projects`, `/api/notes`) --- ## Manifest & Configuration Checklist These must be verified before any code is written. Failures here cause silent runtime errors. - [ ] `android/app/src/main/AndroidManifest.xml` — confirm `` is present - [ ] `android/app/src/main/AndroidManifest.xml` — confirm `android:usesCleartextTraffic="true"` is set on the `` tag (required for HTTP dev server connections; if already using HTTPS only, leave as-is but document the decision) - [ ] `pubspec.yaml` — no new dependencies required for Knowledge View - [ ] `android/app/build.gradle` — no changes required for Knowledge View --- ## File Map ### New files | Path | Responsibility | |---|---| | `lib/data/models/knowledge_item.dart` | Unified model for all knowledge types (note, person, place, list, task) | | `lib/data/api/knowledge_api.dart` | Two API methods: fetch IDs, batch-hydrate items | | `lib/data/repositories/knowledge_repository.dart` | Thin repo wrapping KnowledgeApi | | `lib/providers/knowledge_provider.dart` | StateNotifier with two-tier pagination state | | `lib/screens/knowledge/knowledge_screen.dart` | Main Knowledge screen with type tabs, tag chips, search, infinite scroll | | `lib/widgets/knowledge_item_card.dart` | Type-aware card widget (different icon/subtitle per type) | | `lib/screens/projects/projects_screen.dart` | Project list sorted by updated_at desc | | `lib/screens/projects/project_edit_screen.dart` | Create/edit project (title, description, goal, status) | ### Modified files | Path | Change | |---|---| | `lib/data/models/note.dart` | Add `noteType` field (String, default `'note'`) | | `lib/data/models/project.dart` | Add `status`, `goal`, `color`, `autoSummary`, `updatedAt` fields | | `lib/data/api/notes_api.dart` | Pass `note_type` in create and update request bodies | | `lib/data/api/projects_api.dart` | Add `sort=updated_at&order=desc` to list call; add `status`, `goal`, `color` to create/update | | `lib/screens/notes/note_edit_screen.dart` | Accept optional `noteType` parameter; show type badge in app bar; pass `note_type` to API | | `lib/screens/library/project_tasks_screen.dart` | Add edit button in app bar pushing to `ProjectEditScreen` | | `lib/app.dart` | Replace `Routes.library` with `Routes.knowledge` + `Routes.projects`; update shell to 4 tabs | | `lib/core/constants.dart` | Add `Routes.knowledge`, `Routes.projects`; remove `Routes.library` | ### Retired files | Path | Replacement | |---|---| | `lib/screens/library/library_screen.dart` | `KnowledgeScreen` + `ProjectsScreen` | | `lib/widgets/library_item_card.dart` | `KnowledgeItemCard` | --- ## Section 1: Navigation Four tabs replace the existing three: ``` Briefing | Knowledge | Chat | Projects ``` **`lib/core/constants.dart`:** - Remove `static const library = '/library'` - Add `static const knowledge = '/knowledge'` - `projects` already exists as `'/projects'` — no change needed - Add `static const projectEdit = '/projects/:id/edit'` **`lib/app.dart` — `_ShellState`:** ```dart static const _tabs = [ Routes.briefing, Routes.knowledge, Routes.conversations, Routes.projects, ]; ``` Shell `NavigationBar` / `NavigationRail` entries: ```dart // Bottom nav NavigationDestination(icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: 'Briefing'), NavigationDestination(icon: Icon(Icons.menu_book_outlined), selectedIcon: Icon(Icons.menu_book), label: 'Knowledge'), NavigationDestination(icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: 'Chat'), NavigationDestination(icon: Icon(Icons.folder_outlined), selectedIcon: Icon(Icons.folder), label: 'Projects'), ``` **`_QuickCaptureBar._hintForLocation` update:** ```dart String _hintForLocation(String location) { if (location.startsWith(Routes.knowledge)) return 'Capture a note…'; if (location.startsWith(Routes.projects)) return 'Capture a note…'; if (location.startsWith(Routes.conversations)) return 'Ask Fabled…'; return 'Capture a note…'; } ``` **GoRouter shell routes** — replace `Routes.library` route with: ```dart GoRoute(path: Routes.knowledge, builder: (_, _) => const KnowledgeScreen()), GoRoute(path: Routes.projects, builder: (_, _) => const ProjectsScreen()), ``` --- ## Section 2: Data Models ### `lib/data/models/knowledge_item.dart` ```dart class KnowledgeItem { final int id; final String noteType; // 'note' | 'person' | 'place' | 'list' | 'task' final String title; final String body; final List tags; final int? projectId; final int? milestoneId; final int? parentId; // Task-only fields (null for non-tasks) final String? status; // 'todo' | 'in_progress' | 'done' | 'cancelled' final String? priority; // 'low' | 'normal' | 'high' final String? dueDate; final DateTime createdAt; final DateTime updatedAt; const KnowledgeItem({...}); factory KnowledgeItem.fromJson(Map json) => KnowledgeItem( id: json['id'] as int, noteType: json['note_type'] as String? ?? 'note', title: json['title'] as String? ?? '', body: json['body'] as String? ?? '', tags: (json['tags'] as List?)?.map((e) => e as String).toList() ?? [], projectId: json['project_id'] as int?, milestoneId: json['milestone_id'] as int?, parentId: json['parent_id'] as int?, status: json['status'] as String?, priority: json['priority'] as String?, dueDate: json['due_date'] as String?, createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), ); } ``` ### `lib/data/models/note.dart` — add `noteType` ```dart final String noteType; // new field // In constructor: required this.noteType, // In fromJson: noteType: json['note_type'] as String? ?? 'note', // In toJson: 'note_type': noteType, // In copyWith: add noteType parameter ``` ### `lib/data/models/project.dart` — add missing fields ```dart final String status; // 'active' | 'completed' | 'archived' final String? goal; final String? color; final String? autoSummary; final DateTime updatedAt; // In fromJson: status: json['status'] as String? ?? 'active', goal: json['goal'] as String?, color: json['color'] as String?, autoSummary: json['auto_summary'] as String?, updatedAt: DateTime.parse(json['updated_at'] as String), ``` --- ## Section 3: API Layer ### `lib/data/api/knowledge_api.dart` ```dart class KnowledgeApi { final Dio _dio; const KnowledgeApi(this._dio); /// Fetch page of IDs. Returns (ids, total). Future<(List, int)> fetchIds({ String? noteType, List tags = const [], String sort = 'modified', String? q, int limit = 50, int offset = 0, }) async { final params = { 'limit': limit, 'offset': offset, 'sort': sort, if (noteType != null) 'type': noteType, if (tags.isNotEmpty) 'tags': tags.join(','), if (q != null && q.isNotEmpty) 'q': q, }; final response = await _dio.get('/api/knowledge/ids', queryParameters: params); final data = response.data as Map; final ids = (data['ids'] as List).map((e) => e as int).toList(); final total = data['total'] as int; return (ids, total); } /// Batch-hydrate up to 100 IDs into full items. Future> fetchBatch(List ids) async { if (ids.isEmpty) return []; final response = await _dio.get( '/api/knowledge/batch', queryParameters: {'ids': ids.join(',')}, ); final data = response.data as Map; return (data['items'] as List) .map((e) => KnowledgeItem.fromJson(e as Map)) .toList(); } /// Per-type counts for tab labels. Future> fetchCounts({List tags = const []}) async { final params = { if (tags.isNotEmpty) 'tags': tags.join(','), }; final response = await _dio.get('/api/knowledge/counts', queryParameters: params); return (response.data as Map).map( (k, v) => MapEntry(k, (v as num).toInt()), ); } /// All tags for the current type filter. Future> fetchTags({String? noteType}) async { final response = await _dio.get( '/api/knowledge/tags', queryParameters: {if (noteType != null) 'type': noteType}, ); return (response.data['tags'] as List).map((e) => e as String).toList(); } } ``` ### `lib/data/api/projects_api.dart` — add sort params + missing fields Add `sort: 'updated_at'` and `order: 'desc'` to the `getProjects` query parameters. Add `status`, `goal`, `color` to `createProject` and `updateProject` request bodies. ### `lib/data/api/notes_api.dart` — pass `note_type` ```dart // In createNote and updateNote bodies: if (noteType != null) 'note_type': noteType, ``` --- ## Section 4: Provider ### `lib/providers/knowledge_provider.dart` ```dart @immutable class KnowledgeState { final List ids; // all fetched IDs so far final Map items; // hydrated items final int totalIds; final bool isLoadingIds; final bool isLoadingBatch; final bool hasMore; final String? noteType; // active type filter final List activeTags; final String? searchQuery; final Map counts; // per-type counts bool get canLoadMore => hasMore && !isLoadingIds; // Items in ID order, only those hydrated List get orderedItems => ids.where(items.containsKey).map((id) => items[id]!).toList(); } class KnowledgeNotifier extends StateNotifier { // On filter change: reset state, fetch IDs from offset 0 void setTypeFilter(String? noteType) { ... } void toggleTag(String tag) { ... } void setSearch(String? q) { ... } // debounce handled in UI // Fetch next page of IDs (50 at a time) Future loadMoreIds() { ... } // Hydrate the next 12 un-hydrated IDs from the current list Future hydrateNext() { ... } Future refresh() { ... } // reset + reload } // Provider final knowledgeProvider = StateNotifierProvider(...); ``` **Scroll trigger:** `KnowledgeScreen` attaches a `ScrollController` listener. When `position.pixels >= maxScrollExtent - 300`: 1. Call `hydrateNext()` if there are un-hydrated IDs in the list 2. Call `loadMoreIds()` if all fetched IDs are hydrated and `hasMore` is true --- ## Section 5: Knowledge Screen ### `lib/screens/knowledge/knowledge_screen.dart` **Structure:** ``` Scaffold AppBar title: 'Knowledge' actions: [search icon → expand TextField] Column TabBar (All | Notes | People | Places | Lists | Tasks) — tab labels show counts: "Notes (12)" AnimatedContainer (tag filter chip row, hidden when empty) — horizontal SingleChildScrollView of FilterChip widgets Expanded ListView.builder — items from knowledgeState.orderedItems — trailing: loading indicator when isLoadingBatch FAB (pen icon) — Tasks tab → push TaskEditScreen — all other tabs → showModalBottomSheet (type picker) ``` **Pull-to-refresh:** `RefreshIndicator` wrapping the `ListView`. **Empty state:** Centered column with type-appropriate icon and "No [type] yet" text. **Error state:** Centered text with retry button calling `ref.invalidate(knowledgeProvider)`. ### `lib/widgets/knowledge_item_card.dart` `ListTile`-based card. Leading icon varies by type: - note → `Icons.description_outlined` - person → `Icons.person_outlined` - place → `Icons.place_outlined` - list → `Icons.checklist_outlined` - task → `Icons.task_alt_outlined` (with status colour on leading) Subtitle shows truncated body (max 2 lines) or due date for tasks. Trailing shows tag chips (up to 2, then "+N more"). Tap → `NoteDetailScreen(noteId: item.id)` for knowledge types; `TaskEditScreen(taskId: item.id)` for tasks. --- ## Section 6: Type Picker Bottom Sheet Shown when FAB is tapped on any non-Tasks tab. ```dart showModalBottomSheet( context: context, builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ _TypeRow(Icons.description_outlined, 'Note', 'General note or document', 'note'), _TypeRow(Icons.person_outlined, 'Person', 'Contact, colleague, or reference person', 'person'), _TypeRow(Icons.place_outlined, 'Place', 'Location, venue, or place of interest', 'place'), _TypeRow(Icons.checklist_outlined, 'List', 'Checklist or structured list', 'list'), ], ), ); ``` Each `_TypeRow` on tap: 1. `Navigator.pop(context)` 2. `context.push(Routes.noteNew, extra: {'noteType': selectedType})` ### `lib/screens/notes/note_edit_screen.dart` changes - Accept `noteType` from `GoRouterState.extra` or route parameter (default `'note'`) - Display a read-only type badge chip in the app bar subtitle - Pass `noteType` to `notes_api.createNote` / `notes_api.updateNote` --- ## Section 7: Projects Screen ### `lib/screens/projects/projects_screen.dart` ``` Scaffold AppBar: 'Projects' RefreshIndicator ListView.builder — projects from projectsProvider (sorted updated_at desc via API param) — each item: _ProjectCard FAB → push ProjectEditScreen() ``` `_ProjectCard` (inline widget): - Title + status badge (`active` = green, `completed` = blue, `archived` = grey) - Description (1 line, truncated) - Goal text if present (italic, muted) - Milestone progress: `"3 / 5 milestones done"` using milestone count from project data if available, otherwise omitted Tap → `context.push('/projects/${project.id}/tasks')` — `ProjectTasksScreen` reads `projectId` from the path parameter, unchanged except for the added edit button. ### `lib/screens/projects/project_edit_screen.dart` Fields: Title (required), Description, Goal, Status dropdown (`active` / `completed` / `archived`). Used for both create (no `projectId`) and edit (with `projectId`). On save: POST `/api/projects` or PATCH `/api/projects/:id` via `projectsApi`. On success: `ref.invalidate(projectsProvider)` then `Navigator.pop`. ### `lib/screens/library/project_tasks_screen.dart` change Add an edit `IconButton` in the `AppBar.actions`: ```dart IconButton( icon: const Icon(Icons.edit_outlined), onPressed: () => context.push('/projects/$projectId/edit'), ) ``` --- ## Section 8: Provider & API Wiring New providers to add to `lib/providers/`: ```dart // knowledge_api_provider.dart (or in api_client_provider.dart) final knowledgeApiProvider = Provider((ref) => KnowledgeApi(ref.watch(dioProvider))); final knowledgeRepositoryProvider = Provider((ref) => KnowledgeRepository(ref.watch(knowledgeApiProvider))); ``` `projectsProvider` — add query parameters to the underlying `getProjects` call: ```dart await api.getProjects(sort: 'updated_at', order: 'desc'); ``` --- ## What This Does NOT Include - Voice I/O (separate spec) - Backend parity pass (separate spec) - Editing knowledge item type after creation - Bulk delete / multi-select in Knowledge screen - Knowledge graph view - Milestone detail screen (projects show milestone count only)