惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
量子位
宝玉的分享
宝玉的分享
人人都是产品经理
人人都是产品经理
博客园_首页
博客园 - 【当耐特】
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
腾讯CDC

DEV Community

I Built OpenKap — A Loom Alternative for Small Teams Who Just Want to Ship Gemma 4 is Here: The Dawn of Local Multimodal Reasoning Memory for Agents: When Vectors Meet Graphs, Bugs Drop 4 The Rise of Production-Grade AI Infrastructure I ran my idea-validation product through its own validator. The verdict was PIVOT. We Built an Agent Commerce API. Google I/O 2026 Changed Our 3-Month Roadmap in 24 Hours. "My Partner's Memory Was Full. I Didn't Know — Until We Tried to Talk." I’m a Front End Web Developer Learning Machine Learning From Scratch Laravel Waiting Request I Built a Chrome Extension to Track How Long You Actually Spend on Each Tab Why Google Can't See Your React Breadcrumbs (And the 4-Line Fix) AI Travel Assistant Powered by Gemma 4; With Streaming, Image Input, and Visual Recommendation Cards Microsoft tried to kill the printer driver. Healthcare said no. The Blueprint Beneath the Blueprint: Designing Data Model and Choosing Its Database REST APIs vs Webhooks in Telecom Billing - Which One Actually Makes Sense? Accounting Made Simple: AI-Powered Financial Insights of Japanese Companies with Gemma 4 The append-only AST trick that makes Flutter AI chat actually smooth Designing the Future of Payments — Why XML Still Matters in the Age of APIs From Legacy to Live — Reviving XMLPayments with GitHub Copilot Two Weeks Into Learning Solana XMLPayments — The Hidden Backbone of Modern Financial Orchestration AI Agents in Practice — Read from the beginning Reviving My Gemma Agentic Framework: From Prototype to Polished Repo Smart Contracts Demand Better Infrastructure: Building on contract.dev Self-Hosted LLM Tool Calling: Forge and the Build-vs-Buy Decision ORA-00072 오류 원인과 해결 방법 완벽 가이드 OpenWA for CTOs: Self-Hosted WhatsApp Gateway Trade-Offs NotebookLM Automation With notebooklm-py: Useful, But Classify Data First Docker v29.5.x Operator Upgrade Checklist Coding-Agent Instruction Design: The CLAUDE.md File That Prevents Rework When I Finally Realized My Runtime Was Holding Me Back GnokeOps: Host Your Own AI House Party The Death of Static Rate Limiters: Why Your Java Virtual Threads Need BBR-Style Adaptive Concurrency AI Agents in Practice — Part 2: What Makes Something an Agent Stop scattering LLM SDK/API calls across your codebase. Here is the 2-file rule that fixed mine Beyond Prompts: Structuring AI Workflows for Real Frontend Engineering From an Abandoned Hackathon Project to an AI Study Workspace 🚀 Terraform with AI: Build AWS Infra (Cursor + MCP) What If AI Didn’t Need the Internet? 750,000 Chips, 140 Trillion Tokens: The Math Behind DeepSeek's Permanent Price Cut You're Renting Someone Else's Compute — And It's Costing You More Than You Think CSS :has() Selector: The Layout Trick I Wish I Knew 5 Years Ago Five Clusters. Five Lessons. One Production System. Synaptic: A Local-First AI Dev Companion That Remembers How You Think Revolutionizing Edge MedTech: Building a Sovereign Sleep Apnea Companion ("XiHan Snore Coach") with Gemma 4 HDD Eksternal Tiba-Tiba Tidak Bisa Diakses di Windows? Ini Tiga Lapis Fix-nya DMARC p=none vs p=quarantine vs p=reject: what to use and when DSA Application in Real Life: How Git Diff Works: LCS Intuition, Myers Algorithm, and Real Code Changes I solo-built a reputation layer for AI agents on NEAR — and here's what I learned I built an AI faceless video generator in 2 months — here's the stack
离线优先的 Flutter:我们如何构建一个无需互联网即可管理 10 万+ 销售线索的 CRM 系统
Ahmed ElFirg · 2026-05-23 · via DEV Community

 大多数应用都默默假设网络一直可用。然后一个真实用户走进地下室、半建成的公寓楼或电梯——应用就崩溃了。

对房地产销售代理来说,那一刻不是故障。它是丢失的潜在客户,和丢失的佣金。

当我们在Aqarmap(埃及最大的房产平台)构建AM Live CRM时,我们的现场代理正在管理每月10万+潜在客户 — 而他们的大量时间恰恰发生在这些信号盲区:新建区域、地下停车场、信号仅剩一格的偏远地段.

单纯的"缓存最后响应"策略已不足够。我们需要让应用在零网络连接的情况下完全可用。 — 创建一个线索,将其通过管道,记录一个通话 — 并且在网络恢复的瞬间所有这些内容都能干净地同步。

这是我们最终确定的架构,以及值得避免的错误。

核心思想:本地数据库是事实来源

离线优先最大的思维转变是:界面从不直接与网络通信。 它与本地数据库通信。网络只是一个后台进程,用于保持本地存储和服务器最终的一致性。

UI / BLoC  ->  Repository  ->  Local DB (SQLite)  <->  Sync engine  <->  API

Enter fullscreen mode Exit fullscreen mode

每次读取都来自SQLite。每次写入首先发送到SQLite,然后排队等待服务器处理。用户永远不会等待请求,也永远不会看到依赖信号的加载动画。

— 本地写入,意图排队

当代理编辑线索时,我们通过一个事务处理两件事:更新本地行,并记录意图以同步它

class SyncOperation {
  final String id;          // uuid
  final String entity;      // 'lead', 'call_log', ...
  final String entityId;
  final OpType type;        // create | update | delete
  final Map<String, dynamic> payload;
  final int localVersion;   // bumped on every local edit
  final DateTime createdAt;

  const SyncOperation({ /* ... */ });
}

进入全屏模式 退出全屏模式

Future<void> updateLead(Lead lead) async {
  await db.transaction((txn) async {
    await txn.update('leads', lead.toMap(),
        where: 'id = ?', whereArgs: [lead.id]);

    await txn.insert('sync_queue',
        SyncOperation(
          id: uuid.v4(),
          entity: 'lead',
          entityId: lead.id,
          type: OpType.update,
          payload: lead.toMap(),
          localVersion: lead.version + 1,
          createdAt: DateTime.now(),
        ).toMap());
  });
}

进入全屏模式 退出全屏模式

因为行和队列条目是在写入同一笔交易,你永远不可能处于一个状态,即 UI 显示了永远不会同步的变化。

支柱2—当连接恢复时清空队列

一个监听器监控连接性并启动一个排水操作。排水操作处理操作为了,一次处理一个实体,并且只有在服务器确认后才会从队列中移除操作。

connectivity.onStatusChange
    .where((status) => status.isOnline)
    .listen((_) => _syncEngine.drain());

进入全屏模式 退出全屏模式

Future<void> drain() async {
  if (_isSyncing) return;          // never run two drains at once
  _isSyncing = true;
  try {
    final ops = await _queue.pending(limit: 50);
    for (final op in ops) {
      final result = await _push(op);
      if (result.isConflict) {
        await _resolveConflict(op, result.serverState);
      }
      await _queue.remove(op.id);   // only after success
    }
  } finally {
    _isSyncing = false;
  }
}

进入全屏模式 退出全屏模式

让我们少了很多痛苦的两个细节:

  • 一个守护标志_isSyncing) 这样,不稳定的连接在开启和关闭时不会产生重叠的同步,从而避免重复写入.
  • 确认后删除. 如果应用在同步过程中崩溃,该操作仍然在队列中,下次会简单地重新播放。以操作为键(id)的幂等服务器端点使重新播放是安全的.

第三支柱 — 有目的地解决冲突,而不是偶然解决

危险情况:一个代理离线编辑一个线索,而同事在同一时间在服务器上编辑同一个线索。如果你盲目地推送,你将无声地覆盖他们的工作.

我们版本化了每条记录。当服务器报告比我们操作所依据的版本更新时,我们不猜测——我们执行一个明确的策略:

Future<void> _resolveConflict(SyncOperation op, Lead serverState) async {
  // Field-level merge: keep the server's pipeline stage (authoritative,
  // it drives reporting), keep our locally-edited contact notes.
  final merged = serverState.copyWith(
    notes: op.payload['notes'],
    updatedAt: DateTime.now(),
  );
  await leadRepo.upsertLocal(merged);
  await _queue.enqueueUpdate(merged); // push the merged result back
}

进入全屏模式 退出全屏模式

对于某些字段来说,“最后写入者胜出”是没问题的。对于其他情况(比如用于管理报告的五阶段管道),服务器保持权威性。关键在于这个规则是你记录的一个决定,而不仅仅是你在生产中发现的紧急行为。

这给我们带来了什么

  • 代理商停止了因“没有互联网”而流失客户的情况。管道持续地在线或离线运行。
  • 界面变得更快,因为读取操作不再受网络阻塞的影响.
  • 同步失败变得不再重要:它们只是自动重试.

我想要分享的经验教训

  1. 从一开始就决定采用离线优先的策略.将这一策略添加到一个依赖网络的 App 上,那不是功能增强,而是需要重写.
  2. 让服务器操作幂等 在你信任录像之前。操作ID是你的朋友。
  3. 在真实的蜂窝网络上进行测试,而不是办公室的WiFi。 在你办公桌上运行正常的演示并不是最终产品。
  4. 把冲突规则记下来。 将来的你不会记得为什么舞台变化与笔记行为不同。

我是Ahmed (Saqr),一名资深的Flutter工程师——开发过22+生产级应用,服务过200K+用户。我专注于撰写关于实际交付和扩展的移动应用开发内容.

如果这对你有帮助,请在这里和GitHub上关注我,我在那里维护一个开源的Flutter Enterprise Template,已有100+开发者使用。

你在Flutter中的离线同步方法是什么?我很想听听你在评论区的看法。