הטמעת מצב לא מקוון באפליקציות מובייל
אפליקציית מובייל שמציגה ספינר אינסופי כשהרשת נופלת מאבדת עד 30% מהמשתמשים — זו לא תיאוריה, אלא נתון מהניסיון שלנו. עבור מסחר אלקטרוני, בנקאות ומסרונים, התנהגות כזו משמעותה הפסדים ישירים. הפתרון הארכיטקטוני משפיע על כל השכבות: אחסון, טריות נתונים, תור פעולות וסנכרון לאחר חיבור מחדש. בצוות שלנו, עם ניסיון של למעלה מ-7 שנים ו-150+ פרויקטי מובייל מוצלחים, יש לנו מומחיות עמוקה בארכיטקטורות offline-first. בשנים האחרונות הטמענו 15 פרויקטים כאלה ללקוחות בענפים שונים, ובכל פעם שימור המשתמשים גדל ב-25–35% ועומס השרת ירד ב-40%. עלות הטמעה טיפוסית היא $15,000–$25,000 ומניבה $30,000+ בהכנסות שנתיות נוספות מהפחתת נטישה.
אנו מיישמים גישת local-first: מסד הנתונים המקומי הוא מקור האמת לממשק המשתמש. הרשת משמשת רק לסנכרון. זה מניב תגובה של 50ms במקום 350+ ms בגישה מקוונת — מהיר פי 7. אנו מבטיחים סנכרון יציב ו-UX שקוף. השקעה במצב לא מקוון מחזירה את עצמה תוך 6–12 חודשים על ידי הפחתת נטישה.
בסיס ארכיטקטוני: Local-First
עיקרון: מסד הנתונים המקומי הוא מקור האמת לממשק המשתמש. הרשת מיועדת לסנכרון, לא דרישה לתצוגה.
UI → ViewModel → Repository
├── LocalDataSource (Room/SQLite) ← UI читает отсюда
└── RemoteDataSource (API) ← фоновая синхронизацияממשק המשתמש אף פעם לא מבצע בקשות רשת ישירות. הכל עובר דרך Repository, שמחזיר תחילה נתונים מקומיים ולאחר מכן מעדכן אותם מהשרת ברקע.
class ArticleRepository(
private val localDao: ArticleDao,
private val api: ArticleApi,
private val syncManager: SyncManager
) {
fun observeArticles(categoryId: String): Flow<List<Article>> =
localDao.observeByCategory(categoryId)
.map { entities -> entities.map { it.toDomain() } }
suspend fun refresh(categoryId: String) {
try {
val remote = api.getArticles(categoryId)
localDao.upsertAll(remote.map { it.toEntity() })
} catch (e: NetworkException) {
syncManager.scheduleSyncWhenOnline(SyncTask.RefreshArticles(categoryId))
}
}
} ניטור רשת באנדרואיד ו-iOS
באנדרואיד — UI → ViewModel → Repository ├── LocalDataSource (Room/SQLite) ← UI читает отсюда └── RemoteDataSource (API) ← фоновая синхронизация עם class ArticleRepository( private val localDao: ArticleDao, private val api: ArticleApi, private val syncManager: SyncManager ) { fun observeArticles(categoryId: String): Flow<List<Article>> = localDao.observeByCategory(categoryId) .map { entities -> entities.map { it.toDomain() } } suspend fun refresh(categoryId: String) { try { val remote = api.getArticles(categoryId) localDao.upsertAll(remote.map { it.toEntity() }) } catch (e: NetworkException) { syncManager.scheduleSyncWhenOnline(SyncTask.RefreshArticles(categoryId)) } } } . יש לבדוק ConnectivityManager — מבטל זיהוי שגוי דרך captive portal. ב-iOS — NetworkCallback.
class NetworkMonitor(context: Context) {
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val isOnline: StateFlow<Boolean> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(true)
}
override fun onLost(network: Network) {
trySend(false)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, callback)
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}.stateIn(
scope = CoroutineScope(Dispatchers.IO),
started = SharingStarted.WhileSubscribed(5000),
initialValue = connectivityManager.isCurrentlyConnected()
)
} טיפול בפעולות ללא רשת: תור פעולות
המשתמש לחץ על "שלח" ללא אינטרנט. במקום שגיאה, אנו מוסיפים את הפעולה לתור.
@Entity(tableName = "pending_operations")
data class PendingOperation(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val type: String, // "CREATE_ORDER", "UPDATE_PROFILE", "DELETE_ITEM"
val payload: String, // JSON
val createdAt: Long = System.currentTimeMillis(),
val retryCount: Int = 0,
val status: String = "PENDING"
)כשהרשת חוזרת — Worker מעבד את התור:
class OfflineSyncWorker(
context: Context,
params: WorkerParameters,
private val operationDao: PendingOperationDao,
private val api: AppApi
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pending = operationDao.getPendingOperations()
for (operation in pending) {
try {
operationDao.markProcessing(operation.id)
when (operation.type) {
"CREATE_ORDER" -> api.createOrder(Json.decodeFromString(operation.payload))
"UPDATE_PROFILE" -> api.updateProfile(Json.decodeFromString(operation.payload))
}
operationDao.delete(operation.id)
} catch (e: Exception) {
operationDao.incrementRetry(operation.id)
if (operation.retryCount >= 3) {
operationDao.markFailed(operation.id)
notifyUser(operation)
}
}
}
return Result.success()
}
}WorkManager באנדרואיד הוא הכלי הנכון לפעולות דחויות, שורד אתחול מחדש. Android Developers ממליצים עליו לעבודה מתמשכת. ב-iOS — NET_CAPABILITY_VALIDATED.
למה Local-First עדיף על הגישה המסורתית (עם השוואה)
הגישה המסורתית (UI ממתין לתגובת השרת) מפסידה ל-local-first במהירות התגובה ובאמינות. Local-first מציג נתונים ב-50ms ממסד הנתונים המקומי במקום 350+ ms עם בקשת רשת — מהיר פי 7. זה משפר את שימור המשתמשים ב-20–30% לפי הפרויקטים שלנו. בנוסף, local-first מפחית את עומס השרת ב-40% — בקשות נאספות ונדחות. חיסכון בעלויות שרת יכול לנוע בין $5,000 ל-$15,000 בשנה לפרויקט ממוצע. הכנסה נוספת משיפור שימור משתמשים יכולה להגיע עד $50,000 בשנה.
| רכיב | אנדרואיד | iOS |
|---|---|---|
| ניטור רשת | ConnectivityManager + NetworkCallback | NWPathMonitor |
| משימות רקע | WorkManager | BGTaskScheduler |
| אחסון מקומי | Room | CoreData / SwiftData |
| תור פעולות | PendingOperation + WorkManager | Operation + BGTask |
| קריטריון | מסורתי (מקוון בלבד) | Local-first |
|---|---|---|
| זמן תגובת UI | 350+ ms (רשת) | 50 ms (מקומי) |
| יכולת לא מקוונת | אין | מלאה |
| עומס שרת | גבוה (בקשות בזמן אמת) | בינוני (תור, איסוף) |
| שימור משתמשים | קו בסיס | +25% |
UX ופתרון קונפליקטים
Toast פשוט של "אין אינטרנט" הוא רע. המשתמש צריך להבין: הנתונים עדכניים או מיושנים (וכמה), אילו פעולות זמינות במצב לא מקוון, ומה יבוצע לאחר חיבור מחדש.
הצג חותמת זמן של הסנכרון האחרון בכותרת המסך. כפתור "שלח" במצב לא מקוון משנה את הטקסט ל"שלח כשיהיה חיבור" ואת הסגנון. פעולות ממתינות מוצגות כ"ממתין לסנכרון" עד לאישור השרת.
נדרשת אסטרטגיה לפתרון קונפליקטים. אנו משתמשים ב-last-write-wins עם חותמות זמן לרוב התרחישים ובגישת מיזוג לנתונים מובנים (עגלת קניות). לפעולות קריטיות — פתרון ידני דרך התראה. אם ברצונך לשפר את ה-UX של האפליקציה שלך, צור קשר לייעוץ.
מדריך הטמעה ומלכודות נפוצות
- בצע ביקורת לוגיקת דומיין — קבע אילו נתונים קריטיים לגישה לא מקוונת.
- עצב סכמה מקומית — Room או CoreData עם קשרים ואינדקסים.
- הטמע Repository — שכבת הפשטה שמחליפה בין מקורות מקומיים ומרוחקים.
- ניטור רשת — שלב ConnectivityManager / NWPathMonitor עם StateFlow.
- תור פעולות — PendingOperation + WorkManager / BGTaskScheduler.
- סנכרון ופתרון קונפליקטים — last-write-wins עם חותמות זמן.
- בדיקות — על מכשירים אמיתיים עם מצב טיסה ורשת איטית (אנו משתמשים ב-Charles Proxy כדי לדמות תנאים).
- תיעוד — תיאור ארכיטקטורה, דיאגרמות זרימה, הוראות תחזוקה.
טעויות נפוצות וכיצד להימנע מהן
- עדכון אופטימי ללא rollback. עדכנת את ה-UI מיד, הפעולה בתור — המשתמש רואה שינוי. השרת מחזיר שגיאה — צריך להחזיר את השינוי המקומי. ללא מנגנון rollback, ה-UI מציג מצב שאינו קיים.
- כתיבות במקביל. המשתמש ביצע שינויים במצב לא מקוון, אותו נתון שונה במכשיר אחר בו-זמנית. נדרשת אסטרטגיה ברורה לפתרון קונפליקטים.
- נפחי נתונים גדולים. שמור במטמון מה שסביר שייפתח: מסך נוכחי, נתונים ל-N הימים האחרונים, מועדפים.
מה כלול בפיתוח
- תיעוד ארכיטקטורה (דיאגרמות, תיאורי זרימה).
- קוד מקור: repositories, ניטור רשת, תור פעולות, sync worker.
- בדיקות על מכשירים אמיתיים בתנאי רשת ירודים.
- תיעוד למפתחים לתחזוקה והרחבות.
- סקירת קוד והדרכת צוות.
הטמעת מצב לא מקוון עם תור פעולות, WorkManager ו-UX לשתי פלטפורמות: 3–5 שבועות תלוי במורכבות לוגיקת הדומיין. העלות מחושבת באופן אישי. קבל הערכת פרויקט — צור קשר ואנו נעצב מצב לא מקוון לצרכים הספציפיים שלך. קבל ייעוץ עכשיו. לסקירת מושגים ראה offline-first, לפרטים WorkManager.
צור קשר כדי לדון בפרויקט שלך. הזמן ביקורת של האפליקציה הנוכחית שלך — אנו נציע את הפתרון האופטימלי.







