رفتن به محتوای اصلی

کلاینت اندروید (KMP)

کلاینت اندروید (Kotlin Multiplatform) چهار موجودیت تقویمم — تقویم، رویداد، کار و یادآور — را کاملاً آفلاین ویرایش می‌کند؛ ساخت زیروظیفه هم در رابط کاربری هست. این صفحه نقشهٔ کد و رفتارهای خاص اندروید را توضیح می‌دهد؛ قرارداد شبکه (پروتکل ۲، وضعیت‌ها، reasonها و cursor) در همگام‌سازی است و اینجا تکرار نمی‌شود.

از کجا شروع کنم؟​

همهٔ مسیرها زیر apps/mobile/ هستند. بسته‌های قدیمی com.fanoos هنوز در کد مانده‌اند؛ این جدول نقطهٔ ورود هر بخش است:

مسیرنقطهٔ ورود
android/src/main/…/CalendarApplication (Koin و راه‌اندازی sync)؛ CalendarActivity (Compose و SAF)؛ AndroidSyncScheduler
feature/app/src/commonMain/…/CalendarApp: ناوبری ورود/خانه و بازیابی بازکلید
feature/home/src/commonMain/…/sync/RecordsRoute → RecordsVM → RecordEditor؛ ConflictsRoute
feature/account/src/commonMain/…/auth/ (ورود OTP)؛ AccountRoute/AccountVM (خروج)
core/data/src/commonMain/…/sync/ (repository، موتور و recovery)؛ auth/ (نشست و خروج)؛ wiring در DataModule
core/database/src/MyDatabase، SyncRows، dao/SyncDao؛ migrationها در androidMain
core/network/SDK تولیدشده؛ دست‌نویس ممنوع

تغییر آفلاین چطور به سرور می‌رسد؟​

مسیر داده: RecordsVM → OfflineRepository.edit. این فراخوانی ردیف محلی و نیت متناظرش در outbox را در یک تراکنش Room ثبت می‌کند و بعد به scheduler خبر می‌دهد. اگر کاربر همان رکورد را حین ارسال دوباره ویرایش کند، یک mutation تازه ساخته می‌شود و درخواست جاری دست‌نخورده می‌ماند.

چرخهٔ همگام‌سازی:

  • حساب تازه اول یک pull کامل می‌گیرد؛ دور عادی بعد از آن push → pull → refreshConflicts است.
  • push آیتم‌ها را به ترتیب وابستگی می‌فرستد: تقویم → رویداد → کار (والد قبل از فرزند) → یادآور؛ حذف‌ها برعکس. پاسخی که به آیتمی نمی‌خورد، آن را از صف مصرف نمی‌کند؛ خطای شبکه همان شناسه و محتوا را در صف نگه می‌دارد تا تکرار امن باشد.
  • pull صفحه و cursor را برای همان حساب اتمیک ذخیره می‌کند. اگر روی رکوردی پیش‌نویس تغییر باز دارید، پیش‌نویس حفظ و canonical جدا ذخیره می‌شود. cursorReset هیچ داده‌ای پاک نمی‌کند.
  • triggerهای شروع دور: آغاز نشست، آمدن برنامه به foreground، بازگشت اتصال و ویرایش کاربر. دورها هم‌زمان یکی اجرا می‌شوند.
  • timeout درخواست sync و socket برابر ۱۲۰ ثانیه و timeout اتصال ۱۵ ثانیه است (SyncHttpClientConfig).

محدودیت مهم: از WorkManager استفاده نمی‌شود؛ sync درون process برنامه اجرا می‌شود. اگر سیستم عملیات فرایند را بکشد، تا trigger بعدی چیزی ارسال نمی‌شود — داده‌ها پاک نمی‌شوند، فقط ارسال به تعویق می‌افتد.

ارتقا و هویت حساب​

Room روی نسخهٔ v4 است و builder سه migration دارد؛ fallback مخرب (drop-all) وجود ندارد:

  • v1→v2: شناسه‌های BLOB و فیلدهای تقویم، زیر مالکیت import حفظ می‌شوند.
  • v2→v3: کش تعارض.
  • Migration3To4: ستون nullable providerId برای تقویم/رویداد/یادآور.

داده‌های import‌شدهٔ خودکار ارسال نمی‌شوند؛ با associateImport یا ویرایش زیر حساب فعال، مالکیتشان منتقل و به صف ارسال می‌روند.

ورود اندروید فعلاً به شکل قدیمی کار می‌کند و این محدودیت‌ها را دارد:

  • OTP واقعی همراه با یک deviceId تصادفیِ نصب فرستاده می‌شود که در DataStore می‌ماند — حتی پس از خروج.
  • شیء device فرستاده نمی‌شود؛ نشست در slot قدیمی User است، نه نشست دستگاه‌محور. در فهرست دستگاه‌ها دیده نمی‌شود و ورود قدیمیِ دستگاه دیگری می‌تواند refresh آن را جایگزین کند.
  • حساب از user.id سرور شناسایی می‌شود.
  • refresh تک‌پروازی است: خطای شبکه/5xx داده را نگه می‌دارد؛ ابطال refresh (پاسخ 401/403) فقط «نیاز به ورود دوباره» را ثبت می‌کند، Room و outbox را پاک نمی‌کند.

تغییرم گیر کرده یا می‌خواهم خارج شوم​

وضعیتاقدام
تعارض محلی (stale)از منو «تعارض‌های همگام‌سازی» را باز کنید: پذیرش نسخهٔ سرور، ارسال دوبارهٔ نسخهٔ خودتان با base/شناسهٔ تازه، یا اصلاح نام/عنوان. رابطه‌های والد-فرزند در ویرایشگر رکورد تغییر می‌کنند.
حذف در سرورگزینهٔ «نسخهٔ من» یک restore نسخه‌دار است؛ زنده‌سازی خودکار وجود ندارد.
تعارض تاریخیرد یا بازگردانی با شبکه انجام می‌شود؛ شکست موقت مورد را نگه می‌دارد.
تغییر uid تقویم پیش‌فرضeffectiveUid سرور مبناست. ConflictRecovery ارجاع‌ها و outbox را بازکلید می‌کند و DataStore نیمه‌کاره را پس از restart ترمیم می‌کند.
خروجارسال باقی‌مانده‌ها؛ خروجی JSON-lines با SAF و بعد خروج؛ یا دورریز با تأیید دوم. لغو یا شکست خروجی، چیزی پاک نمی‌کند.

احتیاط: پیش از پاک‌سازی حساب، «همهٔ کار ذخیره شد» را فقط از صفر بودن pending و in_flight نفهمید — drain وضعیت failed و تعارض‌های باز را نمی‌سنجد. اول حل‌شان کنید یا خروجی بگیرید. خروج فقط دادهٔ همین حساب را پاک می‌کند (نه import و نه حساب دیگر)؛ fence و نسل نشست، پاسخ دیررس حساب قبلی را رد می‌کنند.

API محلی و debug​

تنها build نوع debug این override را می‌خواند. API و DB هدف باید آزمایشی باشند؛ میزبان emulator برای API روی کامپیوتر توسعه، 10.0.2.2 است:

adb shell setprop debug.taghvimam.sync.host http://10.0.2.2:5000
adb shell getprop debug.taghvimam.sync.host

خروجی دستور دوم باید دقیقاً http://10.0.2.2:5000 باشد:

  • پیش از هر instrumentation و پس از هر reboot دوباره چک کنید — boot سرد emulator این property را پاک می‌کند.
  • خالی بودن override یعنی هدف https://api.taghvimam.ir تولید است؛ در آن حالت برنامه را اجرا نکنید.
  • بعد از تنظیم، فرایند debug را دوباره راه‌اندازی کنید.

ساخت، تست و rig امن journey در تست‌ها توضیح داده شده است.

مرز قابلیت‌ها​

این‌ها را از وجود فیلد یا کد نتیجه نگیرید:

  • providerId در DTOهای تولیدشدهٔ Kotlin برای تقویم/رویداد/یادآور nullable Long است؛ مقدار روی wire باید عدد صحیح امن JSON در بازهٔ -9007199254740991..9007199254740991 باشد.
  • اتصال به Calendar Provider اندروید و اعلان‌های سیستم پیاده نشده‌اند.
  • وجود ErrorLogsApi در SDK به معنی ارسال خودکار خطاهای اندروید نیست.
  • هدف web/ داخل همین پروژهٔ KMP، خروجی آزمایشی Wasm و خارج از CI است؛ با کلاینت وب React اشتباه نگیرید.

خطِ UI ادغام‌شده (Fanoos dev)​

شاخهٔ dev فانوس (۲۱ کامیت: ویرایشگر رویداد، رخداددهای ماهِ تنبل، مجوزهای تقویم، رنگ‌ها، صفحههای احراز هویت) در ۲۰۲۶-10-10 روی موتورِ همین صفحه ادغام شد. اصل ادغام: موتور main معتبر است؛ UI فانوس روی آن نشسته است. سیستم sync خودنویسِ فانوس (SyncOrchestrator/PullChangesUC/SyncApi دستی + جدول موازی event) حذف شد و EventRepo روی مخزن موتور بازنویسی شد: خواندن از SyncDao.eventRowsFor(account) (displayJson = همان DTOهای wire) و نوشتن از OfflineRepository.edit — هر ذخیرهٔ ویرایشگر، ردیف نمایش + intent ارسال را در یک تراکنش می‌گذارد. کلاس‌های CalendarUid/EventUid لایهٔ UI نگه داشته شده‌اند؛ قرارداد موتور همان CalendarId بایتی + متنِ UUID قاعده‌مند روی wire است. گراف Koin با AuthTokensBus کامل شد (KtorApiImpl به آن وابسته است؛ DataSource احراز هویت خودش آن را پیاده می‌کند).

شواهد و محدودیت‌ها​

سفرِ ۱۴ سناریویی روی emulator (API 35) و روی دستگاه فیزیکی واقعی (Android 14 از طریق adb reverse) در برابر API واقعی و DB آزمایشی سبز شده است: ورود واقعی، ساخت آفلاینِ زنجیرهٔ تقویم→رویداد→وظیفه تو در تو→یادآور، force-stop و ادامه، حذف و tombstone، گم‌شدن پاسخ پس از commit بدون نوشتهٔ دوباره، تعارض ویرایش/حذف با انتخاب کاربر، جداسازی حساب B، ارتقای DB از v1 و برخورد uid پیش‌فرض با بازکلید کامل. سناریوها موتور تولیدی را از طریق گراف Koin اجرا می‌کنند؛ اتوماسیون ضربه‌به‌UI کامپوز و سفر مرورگر↔اندروید اجرا نشده است. یادآور و زیروظیفه از UI اندروید قابل ساخت‌اند (انتخاب «کار والد») — این امکان در وب نیست. زمان‌بندی اعلان سیستم (یادآورها) پیاده نشده.

مرز Google Calendar / Calendar Provider​

ورودِ تقویم‌های دستگاه (فاز ۱، یک‌طرفه دستگاه→اپ) فعال است: از کشوی کناری «تقویم‌های دستگاه» تقویم‌ها و رویدادهایشان انتخاب و وارد می‌شوند — هر ردیف provider با uid قطعیِ مشتق از providerId از همان OfflineRepository.edit می‌گذرد (source: ANDROID_PROVIDER روی wire)، پس outbox و LWW مثل هر ویرایش محلی اعمال می‌شود و ورودِ دوباره به‌جای تکرار، ادغام می‌شود. تست یکپارچهٔ ProviderImportJourneyTest کل مسیر را با CalendarContract واقعی و push تا API آزمون می‌کند (در rig اجرا می‌شود). فاز ۲ (نوشتن دوطرفه به سمت دستگاه، از جمله پل sync-adapter حساب خود اپ) هنوز پیاده نشده؛ پشتهٔ core/platform و DeviceCalendarSyncer خط فانوس برای آن موجود است و باید روی همان مسیر engine سیم‌کشی شود.

لبه‌های پوشش‌داده‌شده​

سناریوی «پیش‌فرضِ آفلاین موبایل در برابر پیش‌فرض وب»: تست یکپارچه offlineDefaultCalendarMergesIntoServerDefaultRekeysChildrenAndPreference (SyncEngineTest) — تقویم پیش‌فرض محلی + رویدادهایش آفلاین ساخته می‌شود؛ ادغام پیش‌فرض سرور با uid وب پاس می‌دهد؛ ردیف بازکلید می‌شود، هر دو رویداد زنده می‌مانند و ارجاع می‌گیرند، ترجیح پیش‌فرض کاربر دنبال می‌کند و چیزی از دست نمی‌رود. بازکلیدِ نیمه‌کاره (کرش بین Room و DataStore) در ConflictRecoveryTest با replay پوشش دارد. هدف Wasm این پروژهٔ KMP در ۲۰۲۶-10-10 به‌کلی حذف شد (تصمیم بهره‌بردار): کلاینت وبِ محصول، React (apps/calendar-web) است. یافته‌های تاریخ‌دارِ نسل پیش در ممیزی ۲۰۲۶-۰۹-۲۷ هستند.