# Tech Stack Research: Cross-Platform Mobile Framework ## Decision: Flutter **Recommendation: Flutter with Dart** ### Justification | Criterion | Flutter | React Native | Winner | |-----------|---------|-------------|--------| | UI consistency | Custom renderer, pixel-perfect on all devices | Native widgets, slight platform differences | Flutter | | Performance | 60/120fps via Impeller engine, compiled to native | Improved via New Architecture (Fabric), near-native | Flutter (slight edge) | | Offline data (SQLite) | sqflite package, excellent | better-sqlite3/op-sqlite, good | Tie | | Image handling + zoom | photo_view package, InteractiveViewer widget | react-native-image-zoom-viewer, good | Tie | | App Store IAP | in_app_purchase plugin (official Flutter team) | react-native-iap, community | Flutter (official plugin) | | Remote content download | dio + path_provider, well-documented | react-native-fs + axios, good | Tie | | Learning curve | Dart (2-3 week ramp for new devs) | JavaScript/React (huge talent pool) | React Native | | iOS-first development | Excellent, first-class Xcode integration | Excellent | Tie | | Educational app examples | Many quiz/flashcard apps | Many | Tie | | Bundle size | Larger (~10MB base) | Smaller (~5MB base) | React Native | | Hot reload speed | Fast | Fast | Tie | **Key deciding factors for Glidr:** 1. **Pixel-perfect UI** matters for zoomable aviation charts/diagrams 2. **Official IAP plugin** reduces risk for the one-time purchase model 3. **Flutter 46% market share** in 2025, strong momentum 4. **No JavaScript bridge** for image rendering means smoother zoom/pan experience 5. App is data-heavy but not UI-heavy — Flutter's widget library handles quiz UX cleanly --- ## Recommended Full Stack ### Mobile App - **Framework**: Flutter 3.x (Dart) - **State Management**: Riverpod 2.x (Provider successor, better testability) - **Local Database**: SQLite via `sqflite` + `sqflite_common_ffi` for tests - **ORM/Query builder**: `drift` (type-safe SQLite layer, formerly Moor) - **HTTP Client**: `dio` with interceptors for auth headers - **Image zoom**: `photo_view` package - **IAP**: `in_app_purchase` (official Flutter plugin, supports both App Store + Play Store) - **Secure storage**: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android) - **Internationalization**: Flutter's built-in `intl` + ARB files (en, fr) - **JSON serialization**: `json_serializable` + `freezed` for immutable data classes ### Backend (Content API) - **Hosting**: tekmidian.com — static file server or lightweight Node.js/PHP - **Language**: PHP 8.x or Node.js (whatever tekmidian.com already runs) - **Content delivery**: Pre-built JSON files served with auth header validation - **No database needed** for v1 — question bank is static JSON files per subject ### Content Update Mechanism 1. App checks `/api/v1/manifest.json` on launch (version hash per subject) 2. If local version hash differs from remote: download updated subject JSON 3. Questions stored in SQLite after first download 4. Images bundled with app for v1 (to avoid complex asset downloading); OR served from CDN with local cache --- ## Local Database Schema (SQLite via Drift) ### Tables ```sql -- Question bank (populated from downloaded JSON) CREATE TABLE questions ( id TEXT PRIMARY KEY, -- "air_law_q1" subject_id TEXT NOT NULL, -- "air_law" number INTEGER NOT NULL, text_en TEXT NOT NULL, text_fr TEXT NOT NULL, options_en TEXT NOT NULL, -- JSON: {"A":"...", "B":"...", "C":"...", "D":"..."} options_fr TEXT NOT NULL, correct TEXT NOT NULL, -- "A", "B", "C", or "D" explanation_en TEXT NOT NULL, explanation_fr TEXT NOT NULL, figures TEXT NOT NULL -- JSON array: [{"filename":"...", "type":"png"}] ); -- SM-2 progress per card CREATE TABLE card_progress ( question_id TEXT PRIMARY KEY, repetition_number INTEGER NOT NULL DEFAULT 0, easiness_factor REAL NOT NULL DEFAULT 2.5, interval_days INTEGER NOT NULL DEFAULT 1, next_review_date TEXT, -- ISO date "2026-03-16", NULL = new card last_review_date TEXT, total_reviews INTEGER NOT NULL DEFAULT 0, correct_reviews INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); -- Study session log (for stats/history) CREATE TABLE study_sessions ( id TEXT PRIMARY KEY, started_at TEXT NOT NULL, ended_at TEXT, mode TEXT NOT NULL, -- "spaced_repetition" | "cram" subject_id TEXT, -- NULL = mixed/all cards_reviewed INTEGER NOT NULL DEFAULT 0, cards_correct INTEGER NOT NULL DEFAULT 0 ); -- Per-review log (for detailed analytics) CREATE TABLE review_log ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, question_id TEXT NOT NULL, reviewed_at TEXT NOT NULL, quality INTEGER NOT NULL, -- 0-5 time_to_answer_ms INTEGER, -- optional: track hesitation time was_cram INTEGER NOT NULL DEFAULT 0 -- boolean ); -- Content manifest (version tracking) CREATE TABLE content_manifest ( subject_id TEXT PRIMARY KEY, version TEXT NOT NULL, downloaded_at TEXT NOT NULL, question_count INTEGER NOT NULL ); -- App settings CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); ``` --- ## Image Handling Strategy ### Option A: Bundle All Images with App (Recommended for v1) - All 58 figures (50 PNG + 8 SVG) = estimated ~5-10 MB total - Bundle in Flutter assets: `assets/figures/` - Display with `photo_view` for pinch-zoom - Pro: works offline immediately, no download complexity - Con: app binary is larger, updating images requires app update ### Option B: Download with Question Pack - Images served from tekmidian.com alongside JSON - Downloaded to local app documents directory - Cached permanently - Pro: images can be updated without app release - Con: more complex implementation, first-run requires download **Recommendation**: Bundle images in v1. Total size is small (<10MB). Move to Option B if the question bank grows significantly or if images need frequent correction. --- ## One-Time Purchase Model ### App Store (iOS) - IAP type: **Non-Consumable** (purchased once, restored across devices) - Product ID: `com.tekmidian.glidr.fullaccess` (example) - Free tier: first 10 questions of each subject (preview) - Paid tier: full question bank (unlock all 9 subjects) - Restore purchases button required by App Store guidelines ### Play Store (Android) - IAP type: **One-time product** (equivalent to non-consumable) - Same product unlock logic ### Implementation with `in_app_purchase` plugin ```dart // Check purchase status at app start final purchases = await InAppPurchase.instance.queryPastPurchases(); final isUnlocked = purchases.any((p) => p.productID == 'com.tekmidian.glidr.fullaccess'); ```