
لما تضغط على زرار مرة، والتطبيق ينفذه مرتين
فيه حاجات في التطبيق لازم تفضل موجودة على الشاشة طول الوقت — زي بيانات صفحة أو تفاصيل عرض. وفيه حاجات تانية عمرها ما المفروض تتكرر — زي فتح ال Share Dialog لما المستخدم يضغط على زرار “دعوة صديق” مثلاً. المشكلة إن كتير من المطورين بيتعاملوا مع النوعين بنفس الطريقة، وده بالظبط اللي بيسبب bug انه المستخدم يضغط زرار مرة واحدة، والفعل يتنفذ أكتر من مرة.
تخيل معايا الكود ده: عندك شاشة فيها زرار “Invite”، لما تضغطه بيطلب من السيرفر رسالة دعوة جاهزة، وبعدين يفتحلك مباشرة شاشة الـ share (WhatsApp، رسائل، إيميل… إلخ). لو الفلو ده اتعمل بـ StateFlow:
val message = MutableStateFlow<ReferralMessageResponse?>(null)
inCoroutine {
viewModel.message.collectLatest {
shareAction(param = it.message)
}
}
المشكلة هنا إن StateFlow طبيعته إنه بيحتفظ بآخر قيمة، وأي collector جديد بياخد القيمة دي على طول من غير ما تتغير. يعني لو المستخدم ضغط “Invite”، الرسالة وصلت، وشاشة المشاركة فتحت… وبعدين حصل أي حاجة تخلي الـ collectLatest يشتغل تاني — زي تدوير الشاشة (screen rotation)، أو رجوع من background، أو حتى إعادة رسم الشاشة (recomposition) — الـ StateFlow هيرجع يدي نفس القيمة القديمة تاني، وشاشة المشاركة هتفتح تاني من غير ما المستخدم يضغط أي حاجة.
ده مش bug نادر. هو بالظبط النوع اللي بيحصل مع المستخدمين اللي بيلفوا الموبايل، أو اللي بيسيبوا التطبيق شوية ويرجعوله. المستخدم هيتفاجئ إن شاشة المشاركة بتفتح لوحدها من غير سبب واضح — وده تجربة مزعجة وبتدي انطباع إن التطبيق فيه مشكلة.
سبب المشكلة:
انه إحنا استخدمنا أداة اتصممت أصلاً عشان تحتفظ بحالة (state) في مكان محتاج حدث (event) بيحصل مرة واحدة وخلاص (one-shot event).يعني فعل لازم يتنفذ مرة واحدة بس لما المستخدم يطلبه فعلاً.
الحل:
إنك تفرق من الأول بين النوعين، وتستخدم SharedFlow من غير replay للأحداث اللي المفروض متتكررش:
val message = MutableSharedFlow<ReferralMessageResponse>()
بكده أي collector جديد أو أي إعادة تشغيل للـ coroutine مش هياخد حاجة لحد ما يحصل emit حقيقي جديد. يعني الحدث بيوصل لمرة واحدة بس، لللحظة اللي حصل فيها فعلاً، وخلاص.
في المقابل، بيانات تفاصيل الصفحة نفسها (زي عنوان ووصف العرض) لازم تفضل موجودة وتتعرض تاني لو الشاشة اتعاد رسمها — دي فعلاً “حالة” مش “حدث”، فمكانها الطبيعي هو StateFlow:
val referralDetails = MutableStateFlow<ReferralDetailsResponse?>(value = null)
النتيجة إن نفس الشاشة بتستخدم الأداتين مع بعض، كل واحدة في مكانها الصح: StateFlow للبيانات اللي المفروض تفضل ظاهرة، و SharedFlow للفعل اللي المفروض يحصل مرة واحدة بس.