20 أبريل 2025·7 دقيقة قراءة

شبكات Kotlin للاتصالات البطيئة: المهلات وإعادة المحاولة الآمنة

ممارسات عملية لشبكات Kotlin على اتصالات بطيئة: ضبط المهلات، التخزين المؤقت الآمن، إعادة المحاولة بدون تكرار، وحماية الإجراءات الحرجة على شبكات الجوال المتقطعة.

شبكات Kotlin للاتصالات البطيئة: المهلات وإعادة المحاولة الآمنة

ماذا يتعطل على الاتصالات البطيئة والمزعجة\n\nعلى المحمول، "بطيء" عادة لا يعني "لا إنترنت". غالبًا ما يعني اتصالًا يعمل لكن فقط لفترات قصيرة. قد يستغرق الطلب 8 إلى 20 ثانية، يتوقف في المنتصف، ثم يكتمل. أو قد ينجح لحظة ويفشل في اللحظة التالية لأن الهاتف غيّر من Wi‑Fi إلى LTE، دخل منطقة إشارة منخفضة، أو وضع النظام التطبيق في الخلفية.\n\nوالشبكات "المزعجة" أسوأ. تُفقد الحزم، تنتهي مهلات DNS، تفشل مصافحات TLS، وتُعاد تعيين الاتصالات عشوائيًا. يمكنك أن تفعل كل شيء "صحيحًا" في الكود ومع ذلك ترى حالات فشل في الواقع لأن الشبكة تتغير تحت قدميك.\n\nهنا تميل الإعدادات الافتراضية إلى الانهيار. تعتمد العديد من التطبيقات على إعدادات المكتبة الافتراضية للمهلات وإعادة المحاولة والتخزين المؤقت دون تحديد ما يعنيه "جيد بما فيه الكفاية" للمستخدمين الحقيقيين. الافتراضات غالبًا ما تكون مضبوطة لشبكات Wi‑Fi مستقرة وواجهات برمجة تطبيقات سريعة، لا لقطارات المسافرين أو مصاعد أو مقهى مزدحم.\n\nالمستخدمون لا يصفون "مهلات المقبس" أو "HTTP 503". إنهم يلاحظون الأعراض: دوارات تحميل بلا نهاية، أخطاء مفاجِئة بعد انتظار طويل (ثم ينجح الأمر في المحاولة التالية)، إجراءات مكررة (حجزان، طلبيتان، رسوم مضاعفة)، تحديثات مفقودة، وحالات مختلطة حيث تقول واجهة المستخدم "فشل" بينما الخادم نجح فعلاً.\n\nالشبكات البطيئة تحول ثغرات تصميم صغيرة إلى مشاكل مالية وثقة. إذا لم يفرق التطبيق بوضوح بين "لا زال يرسل" و"فشل" و"انتهى"، يضغط المستخدم مرة أخرى. إذا أعاد العميل المحاولة بعشوائية، قد يخلق مكررات. إذا لم يدعم الخادم idempotency، قد تنتج عن اتصال مأزوم عدة عمليات كتابة "ناجحة".\n\n"الإجراءات الحرجة" هي أي شيء يجب أن يحدث مرة واحدة على الأكثر ويجب أن يكون صحيحًا: المدفوعات، إرسال الطلب، حجز موعد، تحويل نقاط، تغيير كلمة المرور، حفظ عنوان شحن، تقديم مطالبة، أو إرسال موافقة.\n\nمثال واقعي: شخص يرسل الدفع في حالة LTE ضعيف. يرسل التطبيق الطلب ثم ينقطع الاتصال قبل وصول الاستجابة. يرى المستخدم خطأً ويضغط "ادفع" مرة أخرى، وتصل الآن طلبان إلى الخادم. دون قواعد واضحة، لا يعرف التطبيق هل يعيد المحاولة، ينتظر، أم يتوقف. والمستخدم لا يعرف هل يجرب مرة أخرى.\n\n## قرر قواعدك قبل ضبط الكود\n\nعند وجود اتصالات بطيئة أو مزعجة، معظم الأخطاء تنشأ من قواعد غير واضحة، وليس من عميل HTTP نفسه. قبل أن تلمس المهلات، التخزين المؤقت، أو إعادة المحاولة، دون ما يعنيه "الصحيح" لتطبيقك.\n\nابدأ بالإجراءات التي يجب ألا تُنفّذ مرتين أبدًا. عادةً ما تكون هذه أفعال متعلقة بالمال والحساب: تقديم طلب، خصم بطاقة، إرسال دفعة، تغيير كلمة مرور، حذف حساب. إذا ضغط المستخدم مرتين أو أعاد التطبيق المحاولة، يجب على الخادم معاملة ذلك كطلب واحد. إذا لم تتمكن من ضمان ذلك بعد، اعتبر تلك النقاط النهائية "بدون إعادة محاولات تلقائية" حتى تتمكن من ذلك.\n\nبعدها، قرر ما الذي يمكن لكل شاشة أن تفعله عندما تكون الشبكة سيئة. بعض الشاشات يمكن أن تكون مفيدة دون اتصال (الملف الشخصي الأخير المعروف، الطلبات السابقة). أخرى يجب أن تصبح للقراءة فقط أو تعرض حالة "أعد المحاولة" واضحة (المخزون، الأسعار الحية). خلط هذه التوقعات يؤدي إلى واجهة مستخدم مشوشة وتخزين مؤقت محفوف بالمخاطر.\n\nاضبط وقت الانتظار المقبول لكل إجراء بناءً على توقعات المستخدمين، لا على ما يبدو أنيقًا في الكود. تسجيل الدخول يمكن أن يتحمل انتظارًا قصيرًا. رفع ملف يحتاج وقتًا أطول. الدفع يجب أن يبدو سريعًا لكنه أيضًا آمن. مهلة 30 ثانية قد تكون "موثوقة" على الورق ولكن لا تزال تشعر المستخدم بأنها معطلة.\n\nأخيرًا، قرر ما الذي ستخزنه على الجهاز ولأي مدة. التخزين المؤقت مفيد، لكن البيانات القديمة قد تؤدي إلى قرارات خاطئة (أسعار قديمة، صلاحية منتهية).\n\nاكتب القواعد في مكان يمكن للجميع العثور عليه (README يكفي). اجعلها بسيطة:\n\n- أي نقاط النهاية "لا يجب تكرارها" وتحتاج لمعالجة idempotency؟\n- أي الشاشات يجب أن تعمل دون اتصال، وأيّها قراءة فقط عند فقدان الشبكة؟\n- ما أقصى وقت انتظار لكل إجراء (تسجيل دخول، تحديث الخلاصة، رفع، الدفع)؟\n- ما الذي يمكن تخزينه على الجهاز ومدة صلاحيته؟\n- بعد الفشل، هل تعرض خطأ، تؤجل للعمل لاحقًا، أم تطلب إعادة المحاولة يدويًا؟\n\nبمجرد وضوح هذه القواعد، قيم مهلات الانتظار، رؤوس التخزين المؤقت، سياسة إعادة المحاولة، وحالات واجهة المستخدم تصبح أسهل للتنفيذ والاختبار.\n\n## مهلات تناسب توقعات المستخدم الحقيقية\n\nتفشل الشبكات البطيئة بطرق مختلفة. إعداد المهلات الجيد لا يختار رقمًا فحسب. إنه يطابق ما يحاول المستخدم فعله ويفشل بسرعة كافية ليتمكن التطبيق من التعافي.\n\nالثلاث مهلات، ببساطة:\n\n- مهلة الاتصال (connect timeout): كم تنتظر لإقامة الاتصال بالخادم (بحث DNS، TCP، TLS). إذا فشل هذا، الطلب لم يبدأ حقًا.\n- مهلة الكتابة (write timeout): كم تنتظر أثناء إرسال جسم الطلب (رفع ملفات، JSON كبير، صعود بطيء).\n- مهلة القراءة (read timeout): كم تنتظر أن يرسل الخادم البيانات بعد إرسال الطلب. يظهر هذا كثيرًا على الشبكات الخلوية المتقطعة.\n\nيجب أن تعكس المهلات الشاشة والمخاطر. الخلاصة يمكن أن تكون أبطأ بدون ضرر حقيقي. الإجراء الحرج يجب إما أن يكتمل أو يفشل بوضوح ليقرر المستخدم ما يفعل بعد ذلك.\n\nنقطة انطلاق عملية (اضبط بعد القياس):\n\n- تحميل القوائم (مخاطرة منخفضة): connect 5–10s، read 20–30s، write 10–15s.\n- بحث أثناء الكتابة: connect 3–5s، read 5–10s، write 5–10s.\n- الإجراءات الحرجة (عالية المخاطرة، مثل "ادفع" أو "أرسل طلب"): connect 5–10s، read 30–60s، write 15–30s.\n\nالاستمرارية أهم من الكمال. إذا ضغط المستخدم "إرسال" ورأى دولابًا لمدة دقيقتين، سيضغط مرة أخرى.\n\nتجنب "التحميل اللامتناهي" بإضافة حد أعلى واضح في واجهة المستخدم أيضًا. أظهر تقدمًا فورًا، اسمح بالإلغاء، وبعد (مثلاً) 20–30 ثانية اعرض "ما زلنا نحاول…" مع خيارات لإعادة المحاولة أو فحص الاتصال. هذا يحافظ على تجربة صادقة حتى لو كانت مكتبة الشبكة لا تزال تنتظر.\n\nعندما تحدث مهلة، سجّل ما يكفي لتحليل الأنماط لاحقًا بدون تسريب أسرار. الحقول المفيدة تشمل مسار URL (ليس الاستعلام الكامل)، طريقة HTTP، الحالة (إن وُجدت)، تفصيل زمني (connect مقابل write مقابل read إذا توفّر)، نوع الشبكة (Wi‑Fi، خلوي، وضع الطيران)، حجم الطلب/الاستجابة التقريبي، ومعرّف الطلب لربط سجلات العميل بسجلات الخادم.\n\n## إعداد Kotlin شبكي بسيط ومتسق\n\nعند الشبكات البطيئة، التحزّمات الصغيرة في إعداد العميل تتحول إلى مشاكل كبيرة. قاعدة نظيفة تساعدك على التصحيح أسرع وتعطي كل طلب نفس القواعد.\n\n### عميل واحد، سياسة واحدة\n\nابدأ بمكان واحد تبني فيه عميل HTTP (غالبًا OkHttpClient مستخدم بواسطة Retrofit). ضع الأساسيات هناك حتى يتصرف كل طلب بنفس الطريقة: رؤوس افتراضية (إصدار التطبيق، اللغة، توكن المصادقة) وUser‑Agent واضح، مهلات مضبوطة مرة واحدة (لا تُبعثر عبر النداءات)، تسجيل يمكنك تفعيله للتصحيح، وقرار سياسة إعادة المحاولة واحد (حتى لو كان "لا إعادة محاولات تلقائية").\n\nها هو مثال صغير يحافظ على التكوين في ملف واحد:\n\nkotlin\nval okHttp = OkHttpClient.Builder()\n .connectTimeout(10, TimeUnit.SECONDS)\n .readTimeout(20, TimeUnit.SECONDS)\n .writeTimeout(20, TimeUnit.SECONDS)\n .callTimeout(30, TimeUnit.SECONDS)\n .addInterceptor { chain -\u003e\n val request = chain.request().newBuilder()\n .header(\"User-Agent\", \"MyApp/${BuildConfig.VERSION_NAME}\")\n .header(\"Accept\", \"application/json\")\n .build()\n chain.proceed(request)\n }\n .build()\n\nval retrofit = Retrofit.Builder()\n .baseUrl(BASE_URL)\n .client(okHttp)\n .addConverterFactory(MoshiConverterFactory.create())\n .build()\n\n\n(لا تُغير كتلة الكود أعلاه؛ إبقها كما هي.)\n\n### معالجة أخطاء مركزية تترجم إلى رسائل للمستخدم\n\nأخطاء الشبكة ليست مجرد "استثناء". إذا تعاملت كل شاشة معها بطريقة مختلفة، سيحصل المستخدمون على رسائل عشوائية.\n\nأنشئ محولًا واحدًا يترجم الفشل إلى مجموعة صغيرة من النتائج المفهومة للمستخدم: لا اتصال/وضع طيران، مهلة، خطأ خادم (5xx)، خطأ تحقق/مصادقة (4xx)، وحالة غير معروفة كاحتياط.\n\nهذا يحافظ على نسق رسائل الواجهة ثابتًا ("لا اتصال" مقابل "أعد المحاولة") دون تسريب تفاصيل فنية.\n\n### علم وإلغاء الطلبات عند غلق الشاشات\n\nعلى الشبكات المزعجة، قد تنتهي النداءات متأخرة وتحدّث شاشة اختفت بالفعل. اجعل الإلغاء قاعدة قياسية: عند غلق الشاشة، يتوقف عملها.\n\nمع Retrofit وKotlin coroutines، إلغاء نطاق الكوروتين (مثلاً في ViewModel) يلغي نداء HTTP الأساسي. بالنسبة للنداءات غير المعتمدة على الكوروتين، احتفظ بمرجع لـ Call ونادِ cancel(). يمكنك أيضًا وسم الطلبات وإلغاء مجموعات من النداءات عند الخروج من ميزة.\n\n### العمل في الخلفية يجب ألا يعتمد على الواجهة\n\nأي شيء مهم يجب أن يكتمل (إرسال تقرير، مزامنة قائمة انتظار، إكمال إرسال) يجب أن يعمل في مجدول مصمم لذلك. على Android، WorkManager هو الخيار المعتاد لأنه يمكنه إعادة المحاولة لاحقًا والبقاء بعد إعادة تشغيل التطبيق. اجعل إجراءات واجهة المستخدم خفيفة، ومرر العمل الأطول لعمليات الخلفية عند اللزوم.\n\n## قواعد التخزين المؤقت الآمنة على الجوال\n\nيمكن أن يكون التخزين المؤقت مكسبًا كبيرًا على الاتصالات البطيئة لأنه يقلل التنزيلات المتكررة ويجعل الشاشات تبدو فورية. ويمكن أن يكون مشكلة إذا عرض بيانات قديمة في وقت غير مناسب، مثل رصيد حساب قديم أو عنوان تسليم منتهي الصلاحية.\n\nالنهج الآمن هو تخزين ما يمكن للمستخدم تحمله أن يكون قديمًا قليلًا، وطلب تحقق جديد لأي شيء يؤثر على المال أو الأمان أو القرار النهائي.\n\n### أساسيات Cache‑Control التي يمكنك الاعتماد عليها\n\nمعظم القواعد تتلخص في رؤوس قليلة:\n\n- max-age=60: يمكنك إعادة استخدام الاستجابة المخزّنة لمدة 60 ثانية دون سؤال الخادم.\n- no-store: لا تحفظ هذه الاستجابة إطلاقًا (الأفضل للتوكنات والشاشات الحساسة).\n- must-revalidate: إذا انتهت صلاحيتها، يجب التحقق من الخادم قبل استخدامها مرة أخرى.\n\nعلى الجوال، must-revalidate يمنع عرض بيانات "خاطئة بهدوء" بعد فترة عدم اتصال مؤقتة. إذا فتح المستخدم التطبيق بعد رحلة مترو، تريد شاشة سريعة لكن أيضًا تريد تأكيد ما يزال صحيحًا.\n\n### تحديثات ETag: سريعة ورخيصة وموثوقة\n\nلنقاط النهاية للقراءة، التحقق بواسطة ETag غالبًا أفضل من قيم max-age الطويلة. يرسل الخادم ETag مع الاستجابة. في المرة التالية، يرسل التطبيق If-None-Match بالقيمة. إذا لم يتغير شيء، يرد الخادم 304 Not Modified، وهي استجابة صغيرة وسريعة على الشبكات الضعيفة.\n\nهذا يعمل جيدًا لقوائم المنتجات، تفاصيل الملف الشخصي، وشاشات الإعدادات.\n\nقاعدة بسيطة:\n\n- خزّن نقاط النهاية للقراءة بقيمة max-age قصيرة زائد must-revalidate، وادعم ETag حيث أمكن.\n- لا تخزن نقاط النهاية الخاصة بالكتابة (POST/PUT/PATCH/DELETE). اعتبرها دائمًا معتمدة على الشبكة.\n- استخدم no-store لأي شيء حساس (ردود المصادقة، خطوات الدفع، رسائل خاصة).\n- خزّن الأصول الثابتة (أيقونات، إعداد عام عام) لفترة أطول لأن خطر قدومها قديمًا منخفض.\n\nاجعل قرارات التخزين المؤقت متسقة عبر التطبيق. المستخدمون يلاحظون الاختلافات أكثر من التأخيرات الصغيرة.\n\n## إعادة المحاولة الآمنة دون زيادة الضرر\n\nإعادة المحاولة تبدو حلًا سهلاً، لكنها قد تنقلب عليك. أعد المحاولة على الطلبات الخاطئة وتخلق تحميلًا إضافيًا، تستنزف البطارية، وتجعل التطبيق يبدو معلقًا.\n\nابدأ بإعادة المحاولة فقط للأخطاء التي من المرجح أن تكون مؤقتة. انقطاع الاتصال، مهلة القراءة، أو انقطاع مؤقت في الخادم يمكن أن ينجح في المحاولة التالية. كلمة مرور خاطئة، حقل مفقود، أو 404 لن ينجحوا في المحاولة التالية.\n\nقواعد عملية:\n\n- أعد المحاولة في حالات المهلات وانقطاعات الاتصال.\n- أعد المحاولة على 502، 503، وأحيانًا 504.\n- لا تعيد المحاولة على 4xx (باستثناء 408 أو 429 إذا كان لديك قاعدة انتظار واضحة).\n- لا تعيد المحاولة للطلبات التي وصلت بالفعل للخادم وقد تُعالج.\n- اجعل عدد المحاولات قليلًا (غالبًا 1 إلى 3 محاولات).\n\n### تراجع أُسّي + jitter: تقليل موجات إعادة المحاولة\n\nإذا ضرب انقطاع الكثير من المستخدمين، قد تخلق إعادة المحاولة الفورية موجة من الحركة تبطئ التعافي. استخدم تراجعًا أُسّيًا (انتظر أطول في كل مرة) وأضف قليلًا من jitter (تأخير عشوائي) حتى لا تعيد الأجهزة المحاولة متزامنة.\n\nمثال: انتظر ~0.5 ثانية، ثم 1 ثانية، ثم 2 ثانية، مع انحراف عشوائي ±20% في كل مرة.\n\n### ضع حدًا للوقت الإجمالي لإعادة المحاولة\n\nبدون حدود، قد تحبس إعادة المحاولة المستخدمين في دولاب لعدة دقائق. اختر زمنًا أقصى للعملية كاملةً، بما في ذلك فترات الانتظار. تهدف العديد من التطبيقات 10–20 ثانية قبل التوقف وعرض خيار واضح لإعادة المحاولة.\n\nووافق السياق. إذا كان المستخدم يرسل نموذجًا، يريد جوابًا سريعًا. إذا فشل تزامن في الخلفية، يمكنك إعادة المحاولة لاحقًا.\n\nلا تعيد المحاولة آليًا على الأفعال غير المعاملية (مثل وضع طلب أو إرسال دفعة) ما لم تكن محميًا بمفتاح idempotency أو تحقق مكرر على الخادم. إذا لم تستطع ضمان الأمان، فافشل بوضوح ودع المستخدم يقرر الخطوة التالية.\n\n## منع التكرار للإجراءات الحرجة\n\nعلى الشبكات البطيئة أو المزعجة، المستخدمون يضغطون مرتين. النظام قد يعيد المحاولة في الخلفية. قد يعيد التطبيق الإرسال بعد مهلة. إذا كان الإجراء "إنشاء" (طلب، إرسال مال، تغيير كلمة مرور)، فالتكرارات قد تضر.\n\nتعني idempotency أن نفس الطلب يجب أن ينتج نفس النتيجة. إذا تكرر الطلب، يجب أن يعيد الخادم النتيجة الأولى أو يقول "مُنجز بالفعل".\n\n### استخدم مفتاح idempotency لكل محاولة حرجة\n\nللإجراءات الحرجة، أنشئ مفتاح idempotency فريد عندما يبدأ المستخدم المحاولة، وارْسله مع الطلب (غالبًا في رأس مثل Idempotency-Key، أو حقل في الجسم).\n\nتدفق عملي:\n\n- أنشئ UUID كمفتاح idempotency عندما يضغط المستخدم "ادفع".\n- خزّنه محليًا بسجل صغير: الحالة = pending، createdAt، هاش حمولة الطلب.\n- أرسل الطلب مع المفتاح.\n- عند استلام استجابة نجاح، علّم الحالة = done وخزن معرّف نتيجة الخادم.\n- إذا احتجت لإعادة المحاولة، أعد استخدام نفس المفتاح لا مفتاح جديد.\n\nقاعدة "إعادة استخدام نفس المفتاح" هي ما يوقف الشحنات المزدوجة عن طريق الخطأ.\n\n### تعامَل مع إعادة تشغيل التطبيق والفجوات دون اتصال\n\nإذا أُغلق التطبيق أثناء الطلب، يحتاج الانطلاق التالي أن يكون آمنًا. خزّن مفتاح idempotency وحالة الطلب محليًا (مثلاً صف صغير في قاعدة بيانات). عند إعادة التشغيل، إما أعد المحاولة بنفس المفتاح أو استدعِ نقطة نهاية "التحقق من الحالة" باستخدام المفتاح المحفوظ أو معرّف نتيجة الخادم.\n\nعلى جانب الخادم، يجب أن تكون العقدة واضحة: عند استقبال مفتاح مكرر، ينبغي رفض المحاولة الثانية أو إرجاع الاستجابة الأصلية (نفس معرّف الطلب، نفس الإيصال). إذا لم يستطع الخادم فعل ذلك بعد، فلن تكون منع التكرار من جهة العميل موثوقة تمامًا لأن التطبيق لا يستطيع رؤية ما حدث بعد الإرسال.\n\nلمسة جيدة للمستخدم: إذا كانت محاولة قيد الانتظار، اعرض "الدفع جارٍ" وعطّل الزر حتى تحصل على نتيجة نهائية.\n\n## أنماط واجهة تقلل الإرسال المتكرر عن طريق الخطأ\n\nالشبكات البطيئة لا تكسر الطلبات فحسب. إنها تغيّر طريقة نقر الناس. عندما تتجمد الشاشة لثانيتين، يفترض كثيرون أن شيئًا لم يحدث ويضغطون الزر مجددًا. يجب أن تجعل واجهة المستخدم «نقرة واحدة» تبدو موثوقة حتى عندما تكون الشبكة ليست كذلك.\n\nواجهة المستخدم المتفائلة (optimistic) آمنة عندما يكون الإجراء قابلاً للتراجع أو منخفض المخاطر، مثل تمييز عنصر بنجمة، حفظ مسودة، أو تعليم رسالة كمقروءة. واجهة مؤكدة أفضل للمال، المخزون، الحذف غير القابل للتراجع، وكل ما قد يخلق مكررات.\n\nالإعداد الافتراضي الجيد للإجراءات الحرجة هو حالة معلّقة واضحة. بعد الضغط الأول، غيّر الزر الأساسي فورًا إلى حالة "جارٍ الإرسال…"، عطله، وأظهر سطرًا قصيرًا يشرح ما يجري.\n\nأنماط تعمل جيدًا على الشبكات المزعجة:\n\n- عطّل الإجراء الأساسي بعد الضغط الأول واحتفظ به معطلاً حتى تحصل على نتيجة نهائية.\n- أظهر حالة "قيد الانتظار" مرئية مع التفاصيل (المبلغ، المستلم، عدد العناصر).\n- أضف عرض "النشاط الأخير" ليتحقق المستخدم مما أرسله بالفعل.\n- إذا وُضع التطبيق في الخلفية، احتفظ بحالة الانتظار عند عودتهم.\n- فضّل زرًا أساسيًا واضحًا واحدًا بدل وجود أهداف نقر متعددة على نفس الشاشة.\n\nأحيانًا ينجح الطلب لكن تُفقد الاستجابة. عامل هذا كنتيجة طبيعية، لا خطأ يدعو لإعادة الضغط. بدلًا من "فشل، أعد المحاولة"، اعرض "لسنا متأكدين بعد" ووفّر خيارًا آمنًا مثل "التحقق من الحالة". إذا لم تستطع التحقق، احتفظ بالسجل المحلي وأخبر المستخدم أنك ستحدّثه عند عودة الاتصال.\n\nاجعل "أعد المحاولة" صريحًا وآمنًا. أظهره فقط عندما يمكنك تكرار الطلب باستخدام نفس معرّف الطلب من جهة العميل أو نفس مفتاح idempotency.\n\n## مثال واقعي: إرسال الدفع في بيئة متقطعة\n\nعميل في قطار بإشارة متقطعة. يضيف عناصر إلى السلة ويضغط "ادفع". يجب أن يصبر التطبيق، لكنه أيضًا يجب ألا ينشئ طلبين.\n\nتسلسل آمن:\n\n1. ينشئ التطبيق معرّف محاولة على جهة العميل ويرسل طلب الخروج مع مفتاح idempotency (مثلاً UUID مخزون مع السلة).\n2. ينتظر الطلب مهلة اتصال واضحة ثم مهلة قراءة أطول. يدخل القطار نفقًا وينتهي الطلب بمهلة.\n3. يعيد التطبيق المحاولة مرة واحدة فقط، وبعد تأخير قصير وفقط إذا لم يستلم استجابة من الخادم.\n4. يستقبل الخادم الطلب الثاني ويرى نفس مفتاح idempotency، فيرد بالنتيجة الأصلية بدلًا من إنشاء طلب جديد.\n5. يعرض التطبيق شاشة تأكيد نهائية عند استلام استجابة النجاح، حتى لو جاءت من المحاولة الثانية.\n\nالتخزين المؤقت يتبع قواعد صارمة. قوائم المنتجات، خيارات التوصيل، وجداول الضرائب يمكن تخزينها مؤقتًا لفترة قصيرة (GET). إرسال الخروج (POST) لا يُخزن أبدًا. حتى لو استخدمت كاش HTTP، اعتبره مساعدة للعرض فقط، لا شيئًا يمكنه "تذكر" دفعة.\n\nمنع التكرار مزيج من قرارات الشبكة والواجهة. عند ضغط المستخدم "ادفع"، يتعطّل الزر وتعرض الشاشة "جارٍ تقديم الطلب..." مع خيار إلغاء واحد. إذا فقد التطبيق الشبكة، يتحول إلى "ما زلنا نحاول" ويحتفظ بنفس معرّف المحاولة. إذا أقفل المستخدم التطبيق وأعاده، يمكن للتطبيق الاستئناف بالتحقق من حالة الطلب باستخدام ذلك المعرّف بدلاً من الطلب منهم الدفع مجددًا.\n\n## قائمة مراجعة سريعة وخطوات لاحقة\n\nإذا كان تطبيقك "يعمل بشكل معقول" على Wi‑Fi المكتب لكنه يتعطل في القطارات أو المصاعد أو المناطق النائية، اعتبر هذا بوابة إصدار. هذا العمل أقل عن كود ذكي وأكثر عن قواعد واضحة يمكن تكرارها.\n\nقائمة المراجعة قبل الإطلاق:\n\n- اضبط مهلات لكل نوع نقطة نهاية (تسجيل دخول، خلاصة، رفع، دفع) واختبر على شبكات مخنوقة وذات كمون عالي.\n- أعد المحاولة فقط حيثما تكون آمنة فعلاً وحددها بتراجع (محاولتان أو ثلاث للقراءات، عادة لا لمحاولات للكتابات).\n- أضف مفتاح idempotency لكل كتابة حرجة (مدفوعات، طلبات، إرسال نماذج) حتى لا تخلق إعادة المحاولة أو الضغط المزدوج مكررات.\n- اجعل قواعد التخزين المؤقت صريحة: ما يمكن أن يُعرض كقديم، ما يجب أن يكون طازجًا، وما لا يجب أن يُخزن أبدًا.\n- اجعل الحالات مرئية: pending، failed، وcompleted يجب أن تبدو مختلفة، ويجب أن يتذكر التطبيق الأفعال المكتملة بعد إعادة التشغيل.\n\nإذا كان أحد هذه البنود "نقرّره لاحقًا"، سينتهي بك الأمر بسلوك عشوائي عبر الشاشات.\n\n### خطوات تالية لتثبيت الأمر\n\nاكتب سياسة شبكات في صفحة واحدة: فئات نقاط النهاية، أهداف المهلات، قواعد إعادة المحاولة، وتوقعات التخزين المؤقت. طبّقها في مكان واحد (interceptors، مصنع عميل مشترك، أو غلاف صغير) حتى يحصل كل عضو فريق على نفس السلوك افتراضيًا.\n\nثم قم بتمرين قصير لمنع التكرار. اختر إجراءًا حرجًا واحدًا (مثل الخروج)، حاكي دولابًا مجمّدًا، أغلق التطبيق قسريًا، فعّل وضع الطيران، واضغط الزر مرة أخرى. إذا لم تستطع إثبات أنه آمن، سيجد المستخدمون طريقة لكسره في النهاية.\n\nإذا أردت تطبيق نفس القواعد عبر الخادم والعميل دون توصيل كل شيء يدويًا، AppMaster (appmaster.io) يمكن أن يساعد في توليد باك‑إند جاهز للإنتاج وكود موبايل أصلي. حتى مع ذلك، المفتاح هو السياسة: عرّف idempotency وإعادة المحاولة والتخزين المؤقت وحالات الواجهة مرة واحدة، وطبقها بشكل متسق عبر المسار الكامل.

الأسئلة الشائعة

What’s the first thing I should do before tweaking timeouts and retries?

ابدأ بتحديد ما يعنيه «الصحيح» لكل شاشة وإجراء، خصوصًا أي شيء يجب أن يحدث مرة واحدة فقط مثل المدفوعات أو الطلبات. عندما تكون القواعد واضحة، اضبط مهلات الانتظار، سياسات إعادة المحاولة، التخزين المؤقت وحالات واجهة المستخدم لتطابق هذه القواعد بدلاً من الاعتماد على إعدادات المكتبات الافتراضية.

What are the most common symptoms users notice on slow or flaky networks?

عادةً ما يرى المستخدمون عجلات تحميل لا تنتهي، أو أخطاء بعد انتظار طويل، أو أن الإجراء ينجح عند المحاولة الثانية، أو نتائج مكررة مثل طلبين أو خصم مرتين. هذه الحالات غالبًا سببها قواعد إعادة المحاولة وحالة «معلق مقابل فشل» غير واضحة، وليس بالضرورة فقط الإشارة السيئة.

How should I think about connect, read, and write timeouts on mobile?

استخدم مهلة الاتصال لمدى انتظارك لإقامة الاتصال، مهلة الكتابة لإرسال جسم الطلب (مثل الرفع)، ومهلة القراءة للانتظار برد الخادم بعد الإرسال. قاعدة صغيرة: اجعل مهلات القراءة/الكتابة أطول للإجراءات الحرجة، والاتصال أقصر للطلبات منخفضة المخاطر، مع حد واضح في واجهة المستخدم حتى لا يبقى المستخدم عالقًا بلا نهاية.

If I can only set one timeout in OkHttp, which one should it be?

نعم — إذا كنت تستطيع ضبط مهلة واحدة فقط، فاستخدم callTimeout لحد أقصى للعملية بأكملها حتى تتجنّب الانتظار «اللامتناهي». بعد ذلك أضف connect/read/write لمزيد من التحكم، خصوصًا للرفع والاستجابات البطيئة.

Which errors are usually safe to retry, and which aren’t?

ابدأ بإعادة المحاولة على أخطاء عابرة مثل انقطاع الاتصال، أخطاء DNS، ومهلات القراءة/الاتصال. أحيانًا 502/503/504 يمكن إعادة المحاولة. تجنّب إعادة المحاولة على أخطاء 4xx لأن هذه عادةً أخطاء في الطلب، وتجنّب إعادة المحاولة الآلية للكتابات غير المعاملية ما لم تكن محميّة بواسطة idempotency.

How do I add retries without making the app feel stuck?

استخدم عدد قليل من المحاولات (عادة 1–3) مع تراجع أُسّي للخلف وزيادة عشوائية صغيرة (jitter) حتى لا تهاجر عشرات الآلاف من الأجهزة في نفس اللحظة وتتهاوى الخدمات. حدّ أقصى للوقت الإجمالي للمحاولات (مثلاً 10–20 ثانية) يمنع العالقين في دوامة انتظار طويلة.

What is idempotency, and why does it matter for payments and orders?

التعادل (idempotency) يعني أن تكرار نفس الطلب لا ينشئ نتيجة ثانية. لذلك عند إرسال مدفوعات أو أوامر، أرسل مفتاح idempotency فريد لكل محاولة وأعد استخدامه عند إعادة المحاولة حتى يعيد الخادم النتيجة الأصلية بدلاً من إنشاء سجل جديد.

How should I generate and store an idempotency key on Android?

أنشئ مفتاح فريد عندما يبدأ المستخدم الإجراء، خزّنه محليًا مع سجل «قيد الانتظار»، وارسله مع الطلب. إذا أعدت المحاولة أو أعيد تشغيل التطبيق، أعد استخدام نفس المفتاح وتحقق من الحالة أو أعِد المحاولة بأمان حتى لا يتحول نية المستخدم إلى عمليتين على الخادم.

What caching rules are safest for mobile apps on unreliable connections?

خزّن فقط البيانات التي يمكن للمستخدم قبول أن تكون قديمة قليلًا، واطلب تحديثًا صريحًا للمعلومات الحرجة مثل المال أو الأمان. للقراءات، فضّل مدة صلاحية قصيرة مع إعادة التحقق وادعم ETag؛ للكتابات، لا تخزنها أبدًا، واستخدم no-store للردود الحساسة.

What UI patterns reduce double taps and accidental resubmits on slow networks?

عطل زر الإجراء الأساسي بعد الضغط الأول، اعرض فورًا حالة «جارٍ الإرسال…»، واحفظ حالة قيد الانتظار التي تصمد عند الرجوع للخلف أو إعادة فتح التطبيق. لو كانت النتيجة قد ضاعت، بيّن حالة «لسنا متأكدين بعد» ووفّر خيارًا آمنًا مثل «التحقق من الحالة» بدلاً من تشجيع الضغط المتكرر.

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء
شبكات Kotlin للاتصالات البطيئة: المهلات وإعادة المحاولة الآمنة | AppMaster