כיצד לבחור פתרון אחסון נתונים מקומי (Room, Core Data, Realm, Isar)?
כולנו ראינו את התרחיש: האפליקציה מאבדת נתונים כשהרשת נופלת — וזה לא רק באג, זה כשל במקרה השימוש. המשתמש ממלא טופס, לוחץ על "שלח", מקבל timeout, ומאבד הכל. או גרוע מכך: הנתונים נשלחים פעמיים עקב לוגיקת retry שגויה. שכבת אחסון שנבחרה ומוגדרת כראוי פותרת את הבעיה הזו אחת ולתמיד. בחירה שגויה יכולה לעלות לצוותים בחודשים של שכתוב קוד ועד 70% מהזמן המושקע בסנכרון. הניסיון שלנו — 10+ שנים בפיתוח מובייל, מעל 50 פרויקטים עם אחסון אופליין — מאשר: בחירת האחסון קובעת 80% מהבעיות העתידיות בביצועים ובסנכרון.
בפועל, בחירת האחסון מונעת על ידי שני גורמים: סוג הנתונים ודרישות הסנכרון, לא על ידי פופולריות של ספרייה.
Room (אנדרואיד) — wrapper מעל SQLite עם אימות בזמן קומפילציה של שאילתות SQL. אם שאילתה לא תקינה, ה-build נכשל — טוב יותר מאשר SQLiteException בזמן ריצה. Room משתלב היטב עם Kotlin Flow ו-LiveData, מה שהופך עדכוני UI ראקטיביים לפשוטים. האתגר המרכזי הוא מיגרציות סכמה. @Database(version = N, exportSchema = true) עם קבצי מיגרציה ב-assets/databases/ הוא חובה; אחרת, fallbackToDestructiveMigration() פשוט ימחק את נתוני המשתמש בעדכון האפליקציה.
Core Data (iOS) — לא מסד נתונים, אלא framework לניהול גרף אובייקטים מעל SQLite (או XML, או בזיכרון). NSPersistentContainer עם viewContext לקריאה ב-thread הראשי ו-newBackgroundContext() לכתיבה הוא ההגדרה הבסיסית. הצרות מתחילות כשמפתח קורא ל-save() על viewContext מ-thread ברקע: EXC_BAD_ACCESS ברגע אקראי, קורה פעם בשבוע, עם כמעט שום דבר שימושי בלוג הקריסה. חייבים להשתמש ב-performAndWait או perform עבור כל context אך ורק על ה-thread שלו. Apple Core Data Programming Guide ממליץ על גישה זו.
Realm מנצח במקום שבו צריך מהירות עם קבוצות אובייקטים גדולות וראקטיביות מובנית דרך Results + observe(). Realm מאחסן אובייקטים ישירות ללא מיפוי ORM, כך שקריאות לא דורשות deserialization. לפי המדידות שלנו, Realm מעבד קריאות פי 2–3 מהר יותר מ-Core Data עבור נפחים של מעל 10,000 אובייקטים. ב-Flutter, ה-SDK של Realm (לשעבר MongoDB Realm) תומך ב-Device Sync — אבל זה שירות מנוהל עם תשתית נפרדת.
Hive ו-Isar הם פתרונות ספציפיים ל-Flutter. Hive הוא חנות key-value, מהיר, פשוט, מתאים להגדרות ו-caches. Isar הוא מסד נתונים מלא מונחה מסמכים עם אינדקסים, כתוב ב-Rust, מורכב לקוד native. עבור אפליקציות Flutter עם פונקציונליות אופליין, Isar הוא כיום הבחירה המועדפת: query builder מובנה עם פילטרים type-safe, טרנזקציות, watchObject/watchQuery לראקטיביות.
| פלטפורמה | פתרון | ראקטיביות | סנכרון |
|---|---|---|---|
| אנדרואיד | Room + Flow | LiveData/Flow | WorkManager |
| iOS | Core Data | NSFetchedResultsController | CloudKit |
| Flutter | Isar | Streams | מותאם אישית / Realm Sync |
| Cross-platform | Realm | RealmResults.observe | Device Sync |
| Flutter (פשוט) | Hive | ValueListenable | אין |
צרו קשר לקבלת ביקורת של האחסון הנוכחי שלכם והמלצות אופטימיזציה — זה יחסוך לכם מאות שעות פיתוח ועד 60% מתעבורת בקשות השרת.
למה סנכרון אופליין הוא החלק הקשה ביותר?
אחסון מקומי כשלעצמו אינו מסובך. המורכבות טמונה בסנכרון עם השרת בנוכחות קונפליקטים.
התבנית הנפוצה ביותר היא עדכונים אופטימיים עם rollback. המשתמש עורך רשומה, ה-UI משקף את השינוי מיידית, בקשה ברקע הולכת לשרת. אם השרת מחזיר שגיאה, אנחנו מחזירים את המצב המקומי. נשמע פשוט. בפועל: אם המשתמש עזב את המסך וחזר לפני ה-rollback (שיכול לקחת 3 שניות), ה-UX נשבר. צריך תור פעולות מפורש עם מצבים (PENDING, SYNCED, FAILED) בטבלה נפרדת.
באנדרואיד, לסנכרון ברקע אנחנו משתמשים ב-WorkManager עם Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). אל תשכחו את setInputMerger(ArrayCreatingInputMerger::class) בעת batching של משימות — אחרת, ריצות מקבילות ידרסו נתונים. יישום טיפוסי של תור פעולות:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val pendingOps = syncDao.getPendingOperations() for (op in pendingOps) { try { apiClient.send(op.payload) syncDao.markSynced(op.id) } catch (e: Exception) { syncDao.markFailed(op.id, e.message) return Result.retry() } } return Result.success() } } ב-iOS, המקבילה היא class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val pendingOps = syncDao.getPendingOperations() for (op in pendingOps) { try { apiClient.send(op.payload) syncDao.markSynced(op.id) } catch (e: Exception) { syncDao.markFailed(op.id, e.message) return Result.retry() } } return Result.success() } } עם BGTaskScheduler. המגבלות של iOS על זמן ריצה ברקע (~30 שניות למשימות רענון) אומרות שהסנכרון חייב להיות אינקרמנטלי: לא "לסנכרן הכל," אלא "לסנכרן את N הרשומות הבאות, לשמור את ה-cursor."
קונפליקטים בתרחישים מרובי-מכשירים נפתרים עם אחת משלוש גישות:
- Last-write-wins מבוסס על
BGProcessingTaskRequest(הפשוט ביותר, מאבד נתונים בעריכות מקבילות) - Server-wins (הלקוח תמיד מקבל את גרסת השרת)
- Three-way merge (מורכב, דורש אב קדמון משותף — מתאים למסמכים)
עבור רוב אפליקציות B2C, last-write-wins עם וקטור זמן ברמת משתמש מספיק, אבל לעריכה שיתופית, נדרשת גישת CRDTs — אז כדאי להסתכל על Automerge או Yjs עם bindings למובייל.
איך אנחנו בונים את שכבת האחסון
תבנית ה-repository היא לא אופציונלית — היא חובה. updated_at לא יודע מאיפה הנתונים מגיעים: Room, Realm, או רשת. ה-ViewModel קורא ל-UserRepository, מקבל Flow/Stream, ומציג נתונים. לוגיקת ה-caching נמצאת בתוך ה-repository.
עבור Flutter, ארכיטקטורה טיפוסית: Isar לאחסון, Riverpod לניהול מצב, repository.getUser(id) לסטטוס רשת, ו-ConnectivityPlus מותאם אישית עם תור פעולות. SyncService של Riverpod מכסה בנוחות את הלוגיקה של "הצג cache, עדכן מהרשת, הצג נתונים חדשים." דוגמה ל-repository עם caching:
class UserRepository { final Isar isar; final ApiClient api; Future<User> getUser(String id) async { // 1. попробовать из локального хранилища final cached = await isar.user.where().idEqualTo(id).findFirst(); if (cached != null) return cached; // 2. иначе из сети final remote = await api.fetchUser(id); // 3. сохранить локально await isar.writeTxn(() => isar.user.put(remote)); return remote; } } נושא חשוב נוסף הוא הצפנה. אם האפליקציה מאחסנת נתונים רפואיים, כרטיסי תשלום, או מסמכים ארגוניים, SQLCipher (אנדרואיד) ו-NSFileProtection (iOS) הם לא אופציונליים. Realm תומך בהצפנה באופן native דרך מפתח 64-בייט שחייב להישמר ב-Keychain/Keystore, לא ב-SharedPreferences. חיסכון באבטחה יכול להוביל לדליפות נתונים עם השלכות חמורות.
מה כוללת העבודה
אנחנו מבטיחים תהליך שקוף ומתעדים כל שלב:
| שלב | תוצאה |
|---|---|
| ביקורת דרישות | מסמך המנתח סוגי נתונים, נפחים, תרחישי סנכרון |
| עיצוב סכמה | דיאגרמת ER, קבצי מיגרציה, תוכנית פתרון קונפליקטים |
| פיתוח שכבת repository | קוד עם בדיקות unit (מסד נתונים בזיכרון + mocks לרשת) |
| אינטגרציית סנכרון | תור פעולות, טיפול בשגיאות, לוגיקת fallback |
| Profiling ואופטימיזציה | דוח מ-Android Profiler / Core Data SQLDebug, המלצות |
| פריסה ותיעוד | הוראות פריסה, תיאור API, גישה ל-repository |
רוצים להימנע מטעויות נפוצות בעיצוב אחסון? צרו קשר — נעזור לתכנן אחסון מקומי אמין מאפס או לשפר אחד קיים.
שלבי העבודה
אנחנו מתחילים בביקורת דרישות: אילו נתונים, איזה נפח, האם נדרש סנכרון, האם קונפליקטים אפשריים. בשלב זה, מתברר אם יש צורך ב-Core Data או בפתרון מבוסס SQLite, האם נדרש Realm Sync או ש-polling פשוט של REST יספיק.
לאחר מכן, אנחנו מתכננים את הסכמה עם מחשבה על מיגרציות. סכמות משתנות בכל פרויקט — השאלה היא לא "האם יהיו מיגרציות," אלא "כמה כואבות הן יהיו." אנחנו מייצאים את הסכמה כ-JSON, מאחסנים אותה ב-repository, וכותבים בדיקות למיגרציה של כל גרסה.
הפיתוח כולל כיסוי בדיקות unit לשכבת ה-repository: mocks לשכבת הרשת, מסד נתונים אמיתי בזיכרון לבדיקת שאילתות. לפני השחרור, אנחנו עושים profiling לשאילתות באמצעות Android Profiler (כרטיסיית Database Inspector) או דגלי debug של Core Data (AsyncNotifier).
ציר הזמן ליישום שכבת אחסון עם סנכרון אופליין בסיסי נע בין 2 ל-6 שבועות, תלוי במורכבות הסכמה ובדרישות פתרון הקונפליקטים. צרו קשר כדי לקבל ייעוץ לבחירת הסט האופטימלי ומיגרציות.







