Recommendation: Flutter with Dart
| 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 | inapppurchase 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
sqflite + sqflite_common_ffi for testsdrift (type-safe SQLite layer, formerly Moor)dio with interceptors for auth headersphoto_view packagein_app_purchase (official Flutter plugin, supports both App Store + Play Store)flutter_secure_storage (Keychain on iOS, EncryptedSharedPreferences on Android)intl + ARB files (en, fr)json_serializable + freezed for immutable data classes/api/v1/manifest.json on launch (version hash per subject)```sql -- Question bank (populated from downloaded JSON) CREATE TABLE questions ( id TEXT PRIMARY KEY, -- "airlawq1" 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, nextreviewdate TEXT, -- ISO date "2026-03-16", NULL = new card lastreviewdate 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 timetoanswer_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 ); ```
assets/figures/photo_view for pinch-zoomRecommendation: 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.
com.tekmidian.glidr.fullaccess (example)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'); ```