13 KiB
13 KiB
Technical Documentation
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Expo App Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Tasks │ │ Calendar │ │ Settings │ │
│ │ Screen │ │ Screen │ │ Screen │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ React Hook Form + Zod │ │
│ │ (Form Validation) │ │
│ └─────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Database Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ WatermelonDB Database │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Categories│ │ Tasks │ │ Subtasks │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQLite Adapter (expo-sqlite) │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Sync Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Custom Sync Adapter │ │
│ │ • pullChanges() - GET /sync?since={timestamp} │ │
│ │ • pushChanges() - POST /sync/push │ │
│ │ • Conflict resolution (last-write-wins) │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend API │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Custom Postgres API │ │
│ │ • REST or GraphQL endpoints │ │
│ │ • Authentication (JWT/OAuth) │ │
│ │ • Conflict detection & resolution │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Database Schema
Tables
-- Categories table
CREATE TABLE categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Tasks table
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category_id TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'none',
completed INTEGER NOT NULL DEFAULT 0,
due_date INTEGER NOT NULL DEFAULT 0,
due_time TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
CREATE INDEX idx_tasks_category ON tasks(category_id);
CREATE INDEX idx_tasks_due_date ON tasks(due_date);
CREATE INDEX idx_tasks_completed ON tasks(completed);
-- Subtasks table
CREATE TABLE subtasks (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
"order" INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id)
);
CREATE INDEX idx_subtasks_task ON subtasks(task_id);
Key Components
DatabaseProvider (src/hooks/useDatabase.tsx)
Central context provider managing:
- Database initialization
- Default category seeding
- Collection access
- Ready state
interface DatabaseContextType {
database: Database;
collections: {
categories: Collection<Category>;
tasks: Collection<Task>;
subtasks: Collection<Subtask>;
};
isReady: boolean;
initializeDatabase: () => Promise<void>;
}
useTasks Hook (src/hooks/useTasks.tsx)
Reactive task queries with loading states:
function useTasks(categoryId?: string, showCompleted = false): {
tasks: Task[];
loading: boolean;
}
WatermelonDB Models
All models use decorators for field mapping:
@field('column_name') propertyName!: Type;
@date('column_name') dateProperty!: Date;
@children('table_name') relationName!: Query<Model>;
Sync Implementation
Pull Strategy
- Client sends
lastPulledAttimestamp - Server returns all changes since timestamp
- Client applies changes in transaction:
- Upsert categories (by id)
- Upsert tasks (by id)
- Upsert subtasks (by id)
Push Strategy
- Client collects local changes (created/updated/deleted)
- Sends batched changes with
lastPulledAt - Server applies and returns new timestamp
Conflict Resolution
Default: Last-write-wins based on updatedAt
Customizable in src/database/sync.ts
Navigation Structure
app/
├── _layout.tsx # Stack navigator + DatabaseProvider
│ ├── (tabs)/ # Tab group
│ │ ├── _layout.tsx # Tabs navigator
│ │ ├── index.tsx # / (Tasks)
│ │ ├── calendar.tsx # /calendar
│ │ └── settings.tsx # /settings
│ └── add-task.tsx # /add-task (modal push)
Navigation Patterns
- Tabs: Persistent bottom navigation
- Add Task: Push onto stack (modal on iOS)
- Edit Task: Push onto stack (future)
Form Handling
Add Task Form (app/add-task.tsx)
Uses react-hook-form with zod resolver:
const schema = z.object({
title: z.string().min(1).max(100),
description: z.string().max(1000).optional(),
categoryId: z.string().min(1),
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
dueDate: z.date().nullable().optional(),
dueTime: z.string().optional(),
subtasks: z.array(z.object({ title: z.string() })).optional(),
});
Field Components
Each field is a controlled component using Controller:
CategorySelector- Horizontal chip selectionTaskNameInput- Text input with validationSubtasksSection- FieldArray for dynamic subtasksDateTimePicker- Native date/time pickersPrioritySelector- Segmented controlDescriptionInput- Multiline text area
Animations
Reanimated 3 Patterns
- FAB Rotation (
FloatingActionButton.tsx)
const rotateAnim = useSharedValue(0);
rotateAnim.value = (rotateAnim.value + 0.25) % 1;
- Category Selection (
CategoryFilter.tsx)
const scaleAnim = useSharedValue(1);
scaleAnim.value = withTiming(selected ? 1.05 : 1);
- Checkbox Toggle (
TaskItem.tsx)
const checkedAnim = useSharedValue(0);
checkedAnim.value = withTiming(task.completed ? 1 : 0);
Platform-Specific Considerations
iOS
- DateTimePicker:
display="spinner" - Keyboard:
behavior="padding" - Safe areas: Automatic via
react-native-safe-area-context
Android
- DateTimePicker:
display="default" - Keyboard:
behavior="height" - Elevation shadows for Material Design
Web
- React Native Web for DOM rendering
- DateTimePicker falls back to HTML inputs
- No native SQLite (uses IndexedDB via expo-sqlite web polyfill)
Performance Optimizations
- Lazy Loading: Components loaded on demand
- Memoization:
React.memofor list items - Virtualized Lists: FlatList with
windowSize - Query Optimization: Indexed columns for common queries
- Batch Writes: Database transactions for multiple operations
Testing Strategy
Unit Tests
- Hooks:
useTasks,useCategories - Validators: Zod schemas
- Utilities: Date formatting, priority sorting
Integration Tests
- Database CRUD operations
- Sync adapter with mock server
- Form submission flow
E2E Tests
- Critical user flows (add task, complete, filter)
- Offline/online transitions
- Cross-platform consistency
Deployment Checklist
- Configure
app.jsonwith production values - Set
EXPO_PUBLIC_API_URLenvironment variable - Enable EAS Build for iOS/Android
- Configure code signing certificates
- Set up CI/CD pipeline
- Test offline sync scenarios
- Verify push notifications (if enabled)
- Performance profiling on device
Troubleshooting
Common Issues
Database not initializing
- Check
expo-sqliteinstallation - Verify
DatabaseProviderwraps app root - Check Metro bundler cache:
npx expo start -c
Sync failing
- Verify API endpoint accessibility
- Check CORS headers on server
- Inspect network requests in React Native Debugger
TypeScript errors with WatermelonDB
- Ensure
experimentalDecorators: truein tsconfig - Check
@nozbe/watermelondbversion compatibility - Run
npx expo-doctorfor dependency issues
Animation not working
- Verify
react-native-reanimatedbabel plugin - Check
useNativeDriver: truefor supported properties - Ensure worklet functions are properly extracted