Covers four-tab navigation (Briefing/Knowledge/Chat/Projects), two-tier pagination (50 IDs / 12-item batches), type-aware cards, bottom sheet type picker, Projects screen with sorted project list, ProjectEditScreen, and manifest/config checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
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<uses-permission android:name="android.permission.INTERNET" />is presentandroid/app/src/main/AndroidManifest.xml— confirmandroid:usesCleartextTraffic="true"is set on the<application>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 Viewandroid/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' projectsalready exists as'/projects'— no change needed- Add
static const projectEdit = '/projects/:id/edit'
lib/app.dart — _ShellState:
static const _tabs = [
Routes.briefing,
Routes.knowledge,
Routes.conversations,
Routes.projects,
];
Shell NavigationBar / NavigationRail entries:
// 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:
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:
GoRoute(path: Routes.knowledge, builder: (_, _) => const KnowledgeScreen()),
GoRoute(path: Routes.projects, builder: (_, _) => const ProjectsScreen()),
Section 2: Data Models
lib/data/models/knowledge_item.dart
class KnowledgeItem {
final int id;
final String noteType; // 'note' | 'person' | 'place' | 'list' | 'task'
final String title;
final String body;
final List<String> 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<String, dynamic> 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<dynamic>?)?.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
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
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
class KnowledgeApi {
final Dio _dio;
const KnowledgeApi(this._dio);
/// Fetch page of IDs. Returns (ids, total).
Future<(List<int>, int)> fetchIds({
String? noteType,
List<String> tags = const [],
String sort = 'modified',
String? q,
int limit = 50,
int offset = 0,
}) async {
final params = <String, dynamic>{
'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<String, dynamic>;
final ids = (data['ids'] as List<dynamic>).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<List<KnowledgeItem>> fetchBatch(List<int> ids) async {
if (ids.isEmpty) return [];
final response = await _dio.get(
'/api/knowledge/batch',
queryParameters: {'ids': ids.join(',')},
);
final data = response.data as Map<String, dynamic>;
return (data['items'] as List<dynamic>)
.map((e) => KnowledgeItem.fromJson(e as Map<String, dynamic>))
.toList();
}
/// Per-type counts for tab labels.
Future<Map<String, int>> fetchCounts({List<String> tags = const []}) async {
final params = <String, dynamic>{
if (tags.isNotEmpty) 'tags': tags.join(','),
};
final response = await _dio.get('/api/knowledge/counts', queryParameters: params);
return (response.data as Map<String, dynamic>).map(
(k, v) => MapEntry(k, (v as num).toInt()),
);
}
/// All tags for the current type filter.
Future<List<String>> fetchTags({String? noteType}) async {
final response = await _dio.get(
'/api/knowledge/tags',
queryParameters: {if (noteType != null) 'type': noteType},
);
return (response.data['tags'] as List<dynamic>).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
// In createNote and updateNote bodies:
if (noteType != null) 'note_type': noteType,
Section 4: Provider
lib/providers/knowledge_provider.dart
@immutable
class KnowledgeState {
final List<int> ids; // all fetched IDs so far
final Map<int, KnowledgeItem> items; // hydrated items
final int totalIds;
final bool isLoadingIds;
final bool isLoadingBatch;
final bool hasMore;
final String? noteType; // active type filter
final List<String> activeTags;
final String? searchQuery;
final Map<String, int> counts; // per-type counts
bool get canLoadMore => hasMore && !isLoadingIds;
// Items in ID order, only those hydrated
List<KnowledgeItem> get orderedItems =>
ids.where(items.containsKey).map((id) => items[id]!).toList();
}
class KnowledgeNotifier extends StateNotifier<KnowledgeState> {
// 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<void> loadMoreIds() { ... }
// Hydrate the next 12 un-hydrated IDs from the current list
Future<void> hydrateNext() { ... }
Future<void> refresh() { ... } // reset + reload
}
// Provider
final knowledgeProvider =
StateNotifierProvider<KnowledgeNotifier, KnowledgeState>(...);
Scroll trigger: KnowledgeScreen attaches a ScrollController listener. When position.pixels >= maxScrollExtent - 300:
- Call
hydrateNext()if there are un-hydrated IDs in the list - Call
loadMoreIds()if all fetched IDs are hydrated andhasMoreis 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.
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:
Navigator.pop(context)context.push(Routes.noteNew, extra: {'noteType': selectedType})
lib/screens/notes/note_edit_screen.dart changes
- Accept
noteTypefromGoRouterState.extraor route parameter (default'note') - Display a read-only type badge chip in the app bar subtitle
- Pass
noteTypetonotes_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:
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/:
// 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:
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)