docs: add Android nav restructure implementation plan
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
# Android Nav Restructure & Projects Staleness Fix Implementation Plan
|
||||
|
||||
> **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:** Replace the Projects bottom-nav tab with a "More" bottom sheet giving access to Projects, News, and Calendar; fix stale task data when returning from task edit.
|
||||
|
||||
**Architecture:** The shell's `_tabs` list shrinks from 4 to 3 entries; the 4th nav destination ("More") is intercepted in `onDestinationSelected` to show a `showModalBottomSheet` instead of navigating. Projects moves from a ShellRoute to a top-level push route. News and Calendar are added as stub push-routes. `_tabIndex` maps the three overflow paths to index 3 so "More" highlights correctly. The staleness fix moves the task-tap callback out of the stateless `_TaskRow` widget into the parent `ConsumerState` where `ref` is available, using `.then()` to invalidate after pop.
|
||||
|
||||
**Tech Stack:** Flutter, GoRouter, Riverpod, Material 3
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | Action | What changes |
|
||||
|------|--------|-------------|
|
||||
| `lib/core/constants.dart` | Modify | Add `news` and `calendar` route constants |
|
||||
| `lib/app.dart` | Modify | Remove Projects from ShellRoute, add 3 push routes, shrink `_tabs`, add `_showMoreSheet`, update `_tabIndex`, update both nav widgets |
|
||||
| `lib/screens/library/project_tasks_screen.dart` | Modify | Add `onTap: VoidCallback` to `_TaskRow`; add `_openTask` method to state; pass callback at both call sites |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add route constants
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/core/constants.dart`
|
||||
|
||||
- [ ] **Step 1: Add `news` and `calendar` to `Routes`**
|
||||
|
||||
Open `lib/core/constants.dart`. The file currently ends with:
|
||||
```dart
|
||||
abstract class Routes {
|
||||
static const splash = '/';
|
||||
static const setup = '/setup';
|
||||
static const login = '/login';
|
||||
static const notes = '/notes';
|
||||
static const noteDetail = '/notes/:id';
|
||||
static const noteEdit = '/notes/:id/edit';
|
||||
static const noteNew = '/notes/new';
|
||||
static const tasks = '/tasks';
|
||||
static const taskNew = '/tasks/new';
|
||||
static const taskEdit = '/tasks/:id/edit';
|
||||
static const knowledge = '/knowledge';
|
||||
static const projects = '/projects';
|
||||
static const projectEdit = '/projects/:id/edit';
|
||||
static const conversations = '/chat';
|
||||
static const chat = '/chat/:id';
|
||||
static const quickCapture = '/quick-capture';
|
||||
static const settings = '/settings';
|
||||
static const briefing = '/briefing';
|
||||
static const projectTasks = '/projects/:id/tasks';
|
||||
}
|
||||
```
|
||||
|
||||
Add two constants after `briefing`:
|
||||
```dart
|
||||
abstract class Routes {
|
||||
static const splash = '/';
|
||||
static const setup = '/setup';
|
||||
static const login = '/login';
|
||||
static const notes = '/notes';
|
||||
static const noteDetail = '/notes/:id';
|
||||
static const noteEdit = '/notes/:id/edit';
|
||||
static const noteNew = '/notes/new';
|
||||
static const tasks = '/tasks';
|
||||
static const taskNew = '/tasks/new';
|
||||
static const taskEdit = '/tasks/:id/edit';
|
||||
static const knowledge = '/knowledge';
|
||||
static const projects = '/projects';
|
||||
static const projectEdit = '/projects/:id/edit';
|
||||
static const conversations = '/chat';
|
||||
static const chat = '/chat/:id';
|
||||
static const quickCapture = '/quick-capture';
|
||||
static const settings = '/settings';
|
||||
static const briefing = '/briefing';
|
||||
static const news = '/news';
|
||||
static const calendar = '/calendar';
|
||||
static const projectTasks = '/projects/:id/tasks';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify no analysis errors**
|
||||
|
||||
```bash
|
||||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||||
flutter analyze lib/core/constants.dart
|
||||
```
|
||||
Expected: `No issues found!`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Restructure shell and add routes in `app.dart`
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/app.dart`
|
||||
|
||||
This task has several sub-steps. Make them all before running analyze.
|
||||
|
||||
- [ ] **Step 1: Move Projects out of ShellRoute, add three push routes**
|
||||
|
||||
Find the `ShellRoute` block (currently lines ~142–162):
|
||||
```dart
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _Shell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.briefing,
|
||||
builder: (_, _) => const BriefingScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.knowledge,
|
||||
builder: (_, _) => const KnowledgeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.conversations,
|
||||
builder: (_, _) => const ConversationsTabScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.projects,
|
||||
builder: (_, _) => const ProjectsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
Replace with (Projects removed from shell; three new top-level routes added after the closing `],` of the ShellRoute):
|
||||
```dart
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _Shell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.briefing,
|
||||
builder: (_, _) => const BriefingScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.knowledge,
|
||||
builder: (_, _) => const KnowledgeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.conversations,
|
||||
builder: (_, _) => const ConversationsTabScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.projects,
|
||||
builder: (_, _) => const ProjectsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.news,
|
||||
builder: (_, _) => Scaffold(
|
||||
appBar: AppBar(title: const Text('News')),
|
||||
body: const Center(child: Text('News — coming soon')),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.calendar,
|
||||
builder: (_, _) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Calendar')),
|
||||
body: const Center(child: Text('Calendar — coming soon')),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Shrink `_tabs` from 4 to 3 entries**
|
||||
|
||||
Find in `_ShellState`:
|
||||
```dart
|
||||
static const _tabs = [
|
||||
Routes.briefing,
|
||||
Routes.knowledge,
|
||||
Routes.conversations,
|
||||
Routes.projects,
|
||||
];
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
static const _tabs = [
|
||||
Routes.briefing,
|
||||
Routes.knowledge,
|
||||
Routes.conversations,
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `_tabIndex` to map overflow routes to index 3**
|
||||
|
||||
Find:
|
||||
```dart
|
||||
int _tabIndex(String location) {
|
||||
for (var i = 0; i < _tabs.length; i++) {
|
||||
if (location.startsWith(_tabs[i])) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
int _tabIndex(String location) {
|
||||
for (var i = 0; i < _tabs.length; i++) {
|
||||
if (location.startsWith(_tabs[i])) return i;
|
||||
}
|
||||
if (location.startsWith(Routes.projects) ||
|
||||
location.startsWith(Routes.news) ||
|
||||
location.startsWith(Routes.calendar)) return 3;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add `_showMoreSheet` method to `_ShellState`**
|
||||
|
||||
Add this method anywhere in `_ShellState`, e.g. just before `build`:
|
||||
```dart
|
||||
void _showMoreSheet(BuildContext context) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (_) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: const Text('Projects'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push(Routes.projects);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.newspaper_outlined),
|
||||
title: const Text('News'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push(Routes.news);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.calendar_month_outlined),
|
||||
title: const Text('Calendar'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push(Routes.calendar);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update `NavigationRail` — intercept index 3 and swap destination**
|
||||
|
||||
Find in the wide-layout branch:
|
||||
```dart
|
||||
NavigationRail(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i]),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: const [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.wb_sunny_outlined),
|
||||
selectedIcon: Icon(Icons.wb_sunny),
|
||||
label: Text('Briefing'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
selectedIcon: Icon(Icons.menu_book),
|
||||
label: Text('Knowledge'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.chat_bubble_outline),
|
||||
selectedIcon: Icon(Icons.chat_bubble),
|
||||
label: Text('Chat'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.folder_outlined),
|
||||
selectedIcon: Icon(Icons.folder),
|
||||
label: Text('Projects'),
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
NavigationRail(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (i) {
|
||||
if (i == 3) {
|
||||
_showMoreSheet(context);
|
||||
} else {
|
||||
context.go(_tabs[i]);
|
||||
}
|
||||
},
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: const [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.wb_sunny_outlined),
|
||||
selectedIcon: Icon(Icons.wb_sunny),
|
||||
label: Text('Briefing'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
selectedIcon: Icon(Icons.menu_book),
|
||||
label: Text('Knowledge'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.chat_bubble_outline),
|
||||
selectedIcon: Icon(Icons.chat_bubble),
|
||||
label: Text('Chat'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.more_horiz_outlined),
|
||||
selectedIcon: Icon(Icons.more_horiz),
|
||||
label: Text('More'),
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update `NavigationBar` — intercept index 3 and swap destination**
|
||||
|
||||
Find in the narrow-layout branch:
|
||||
```dart
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i]),
|
||||
destinations: const [
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (i) {
|
||||
if (i == 3) {
|
||||
_showMoreSheet(context);
|
||||
} else {
|
||||
context.go(_tabs[i]);
|
||||
}
|
||||
},
|
||||
destinations: const [
|
||||
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.more_horiz_outlined),
|
||||
selectedIcon: Icon(Icons.more_horiz),
|
||||
label: 'More',
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify no analysis errors**
|
||||
|
||||
```bash
|
||||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||||
flutter analyze lib/app.dart lib/core/constants.dart
|
||||
```
|
||||
Expected: `No issues found!`
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/core/constants.dart lib/app.dart
|
||||
git commit -m "feat: replace Projects tab with More bottom sheet (news/calendar stubs)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Fix stale tasks after returning from task edit
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/screens/library/project_tasks_screen.dart`
|
||||
|
||||
- [ ] **Step 1: Add `_openTask` method to `_ProjectTasksScreenState`**
|
||||
|
||||
`_ProjectTasksScreenState` is a `ConsumerState` — it has `ref` and `context`. Find the class body (look for `_cycleStatus` method as a landmark) and add `_openTask` as a sibling method:
|
||||
|
||||
```dart
|
||||
void _openTask(int taskId) {
|
||||
context
|
||||
.push(Routes.taskEdit.replaceFirst(':id', '$taskId'))
|
||||
.then((_) => ref.invalidate(projectTasksProvider(widget.projectId)));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `onTap` parameter to `_TaskRow`**
|
||||
|
||||
Find the `_TaskRow` class definition:
|
||||
```dart
|
||||
class _TaskRow extends StatelessWidget {
|
||||
final Task task;
|
||||
final TaskStatus effectiveStatus;
|
||||
final VoidCallback onStatusTap;
|
||||
|
||||
const _TaskRow({
|
||||
required this.task,
|
||||
required this.effectiveStatus,
|
||||
required this.onStatusTap,
|
||||
});
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
class _TaskRow extends StatelessWidget {
|
||||
final Task task;
|
||||
final TaskStatus effectiveStatus;
|
||||
final VoidCallback onStatusTap;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TaskRow({
|
||||
required this.task,
|
||||
required this.effectiveStatus,
|
||||
required this.onStatusTap,
|
||||
required this.onTap,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Use the `onTap` callback in `InkWell`**
|
||||
|
||||
Find inside `_TaskRow.build`:
|
||||
```dart
|
||||
child: InkWell(
|
||||
onTap: () => context
|
||||
.push(Routes.taskEdit.replaceFirst(':id', '${task.id}')),
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```dart
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Pass `onTap` at both `_TaskRow` call sites**
|
||||
|
||||
There are two places in `_buildBody` where `_TaskRow` is instantiated. Update both:
|
||||
|
||||
**First call site (milestone tasks, around line 170):**
|
||||
```dart
|
||||
return _TaskRow(
|
||||
task: task,
|
||||
effectiveStatus: _effectiveStatus(task),
|
||||
onStatusTap: () => _cycleStatus(task),
|
||||
onTap: () => _openTask(task.id),
|
||||
);
|
||||
```
|
||||
|
||||
**Second call site (unassigned tasks, around line 197):**
|
||||
```dart
|
||||
return _TaskRow(
|
||||
task: task,
|
||||
effectiveStatus: _effectiveStatus(task),
|
||||
onStatusTap: () => _cycleStatus(task),
|
||||
onTap: () => _openTask(task.id),
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify no analysis errors**
|
||||
|
||||
```bash
|
||||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||||
flutter analyze lib/screens/library/project_tasks_screen.dart
|
||||
```
|
||||
Expected: `No issues found!`
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/screens/library/project_tasks_screen.dart
|
||||
git commit -m "fix: invalidate project tasks on return from task edit screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Final check
|
||||
|
||||
- [ ] **Step 1: Full analyze**
|
||||
|
||||
```bash
|
||||
cd /home/bvandeusen/Nextcloud/Projects/fabled_app
|
||||
flutter analyze
|
||||
```
|
||||
Expected: `No issues found!`
|
||||
Reference in New Issue
Block a user