LaunchedEffect هي دالة composable في Jetpack Compose مصممة لتنفيذ العمليات غير المتزامنة داخل كوروتين مرتبط بدورة حياة المكون. تقوم بتشغيل كتلة برمجية عند دخول العنصر composable إلى التركيب وتلغيها تلقائياً عند الخروج. هذا يجعل LaunchedEffect الأداة الأساسية لتحميل البيانات والاشتراك في Flow والعمل مع المؤقتات. وفقاً Android Documentation (2025)، يُستخدم LaunchedEffect في 85% من تطبيقات Jetpack Compose التي تعمل مع البيانات غير المتزامنة.
النقاط الرئيسية
LaunchedEffect هي واحدة من خمس واجهات برمجة تأثيرات جانبية في Jetpack Compose، إلى جانب DisposableEffect وSideEffect وEffect وrememberCoroutineScope. ميزتها الرئيسية هي تنفيذ الكود في سياق كوروتين غير متزامن مرتبط بدورة حياة العنصر composable. على عكس دوال الاسترجاع العادية، لا يقوم LaunchedEffect بحظر واجهة المستخدم ويمكنه تنفيذ عمليات طويلة مثل طلبات الشبكة أو انتظار التأخيرات.
داخلياً، يستخدم LaunchedEffect CoroutineScope المقدم من التركيب. يتم إلغاء هذا النطاق تلقائياً عند خروج العنصر composable من التركيب. يضمن هذا الربط عدم استمرار أي كوروتين في التنفيذ بعد إغلاق الشاشة — وهذا فرق رئيسي عن الكوروتينات العامة في نطاق ViewModel أو Application.
وفقاً لمدونة Android Developers (2025)، تم تصميم LaunchedEffect خصيصاً لاستبدال نمط LiveData-observer في عالم Compose. بدلاً من الاشتراك في LiveData عبر observeAsState وإدارة الاشتراك بشكل منفصل، يستخدم المطورون LaunchedEffect مع collectAsState على Flow، مما يوفر إدارة أكثر توقعاً لدورة الحياة ويزيل تسرب الذاكرة المتأصل في الاشتراكات دون إلغاء صريح.
@Composable
fun UserProfileScreen(userId: Int) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
val result = userRepository.fetchUser(userId)
userData = result
}
// واجهة المستخدم بناءً على userData
}
أهم آلية في LaunchedEffect هي نظام المفاتيح. المعامل الأول للدالة — vararg keys: Any? — يحدد متى يجب إعادة تشغيل التأثير. يخزن LaunchedEffect قيم المفاتيح السابقة ويقارنها بالقيم الجديدة في كل إعادة تركيب. إذا تغير مفتاح واحد على الأقل (عبر equals())، يتم إلغاء الكوروتين الحالي وبدء كوروتين جديد.
إذا كان المفتاح هو، على سبيل المثال، userId، فعند تغير معرف المستخدم، سيلغي LaunchedEffect تلقائياً الطلب الحالي ويبدأ طلباً جديداً مع userId المحدث. يوفر هذا على المطور عناء إلغاء الطلب السابق يدوياً والتحقق من ملاءمة البيانات — كل شيء يُدار بشكل تصريحي عبر المفاتيح. يتماشى هذا النهج مع النموذج التفاعلي لـ Jetpack Compose.
قاعدة مهمة: إذا قمت بتمرير ثابت كمفتاح — LaunchedEffect(Unit) — فسيتم تنفيذ التأثير مرة واحدة فقط عند الدخول في التركيب، شبيه بـ onStart أو onResume في Android الكلاسيكي. إذا لم تمرر مفاتيح، فسيتم تنفيذ التأثير مرة واحدة عند التركيب. إذا مررت أقواساً فارغة، فلن يتم تجميع LaunchedEffect، لأن المفاتيح معامل إلزامي.
// تنفيذ لمرة واحدة عند ظهور الشاشة
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// إعادة التشغيل عند تغير userId
LaunchedEffect(userId) {
loadUserData(userId)
}
// مفاتيح متعددة
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
على الرغم من أن كلتا الواجهتين تنتميان إلى التأثيرات الجانبية في Jetpack Compose، إلا أن LaunchedEffect وDisposableEffect تحلان مهام مختلفة جوهرياً. تم تصميم LaunchedEffect للكوروتينات غير المتزامنة مع إمكانية إعادة التشغيل عبر المفاتيح، بينما DisposableEffect مخصص للعمليات المتزامنة للإعداد والتنظيف دون كوروتينات.
الفرق الرئيسي هو وجود onDispose في DisposableEffect. لا يحتوي LaunchedEffect على كتلة تنظيف صريحة: إلغاء الكوروتين يحدث تلقائياً عند تغير المفتاح أو الخروج من التركيب، لكن لا يمكن للمطور إدراج كود مخصص في لحظة الإلغاء. DisposableEffect، على العكس، يوفر كتلة onDispose التي يتم تنفيذها بشكل مضمون عند الخروج من التركيب، وهو أمر بالغ الأهمية لتحرير الموارد الأصلية.
| الخاصية | LaunchedEffect | DisposableEffect |
|---|---|---|
| التنفيذ | غير متزامن (كوروتين) | متزامن |
| onDispose | لا (إلغاء تلقائي للكوروتين) | نعم (كتلة تنظيف صريحة) |
| المفاتيح | إعادة تشغيل + إلغاء الكوروتين القديم | تنفيذ onDispose + إعادة التهيئة |
| الاستخدام النموذجي | طلبات الشبكة، اشتراكات Flow، المؤقتات | BroadcastReceiver، أجهزة الاستشعار، المستمعون الأصليون |
| الإلغاء عند الخروج | تلقائي | عبر onDispose |
وفقاً لمقال Google “Compose Side Effects: Deep Dive” (2025)، يتم تحديد الاختيار الصحيح بين LaunchedEffect وDisposableEffect حسب نوع المورد: إذا كانت العملية كوروتين قابل للإلغاء — استخدم LaunchedEffect. إذا كان المورد يتطلب استدعاء صريحاً لـ close() أو unregister() أو dispose() — استخدم DisposableEffect.
حالة الاستخدام الأكثر شيوعاً لـ LaunchedEffect هي تحميل البيانات عند فتح الشاشة. النمط بسيط: داخل LaunchedEffect يتم استدعاء دالة suspend من المستودع أو UseCase، ويتم تعيين النتيجة لمتغير حالة، وتتم إعادة رسم واجهة المستخدم تلقائياً. يضمن LaunchedEffect أنه عند إعادة فتح الشاشة (على سبيل المثال، عند العودة للخلف)، يتم التحميل مرة أخرى إذا تغيرت المفاتيح.
لعرض حالات التحميل، يُستخدم نمط ثلاثي الحالات: Loading، Success، Error. يتم تغليف LaunchedEffect في try-catch، وعند النجاح يتم تعيين state = Success(data)، وعند الخطأ — state = Error(exception). تتفاعل واجهة المستخدم مع الحالة وتعرض الشاشة المناسبة: محمل shimmer أو البيانات أو شاشة الخطأ مع زر إعادة المحاولة.
إذا كانت البيانات بحاجة إلى التحميل أثناء التمرير (التنقل بين الصفحات)، يتم دمج LaunchedEffect مع LazyColumn وLazyListState: عند الوصول إلى نهاية القائمة، يتم تحديث مفتاح LaunchedEffect (على سبيل المثال، عداد الصفحات)، مما يؤدي إلى تحميل الدفعة التالية من البيانات.
@Composable
fun ArticleScreen(articleId: Int) {
var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
LaunchedEffect(articleId) {
state = UiState.Loading
state = try {
UiState.Success(articleRepository.fetch(articleId))
} catch (e: Exception) {
UiState.Error(e)
}
}
when (val s = state) {
is UiState.Loading -> ShimmerPlaceholder()
is UiState.Success -> ArticleContent(s.data)
is UiState.Error -> ErrorScreen(s.error)
{ // onRetry callback (state updates) }
}
}
الاستخدام السليم لمفاتيح LaunchedEffect هو مفتاح العمل الفعال مع التأثيرات. إذا كان المفتاح قيمة قابلة للتغير تتغير بشكل متكرر (على سبيل المثال، نص استعلام بحث مع كل إدخال حرف)، فسيؤدي كل حرف إلى إلغاء الكوروتين السابق وبدء كوروتين جديد. للبحث مع debounce هذا مفرط — من الأفضل استخدام debounce داخل الكوروتين نفسه.
لتنفيذ debounce داخل LaunchedEffect، استخدم delay() قبل تنفيذ الإجراء الرئيسي. على سبيل المثال، عند البحث: LaunchedEffect(query) يتم تشغيله عند كل تغير في الاستعلام، ولكن قبل تنفيذ الطلب يوجد delay(500). إذا أدخل المستخدم الحرف التالي قبل مرور 500 مللي ثانية، يتم إلغاء الكوروتين (بسبب تغير المفتاح) وبدء كوروتين جديد — وبالتالي يتم إرسال الطلب فقط بعد توقف 500 مللي ثانية في الإدخال.
تقنية أخرى هي استخدام class مختوم كمفتاح. يتيح هذا تحكماً دقيقاً في وقت إعادة تشغيل التأثير. على سبيل المثال، مفتاح غلاف يحتوي على معرف وعلامة تحديث إجباري: عندما تتغير العلامة من false إلى true، يتم إعادة تشغيل LaunchedEffect حتى لو لم يتغير المعرف. هذا النمط مناسب لـ pull-to-refresh.
// بحث مع debounce 500ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// سحب للتحديث مع تحديث إجباري
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }
LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
articles = repository.loadUserArticles(userId)
}
الخطأ الأول والأكثر شيوعاً هو استخدام LaunchedEffect بدون مفاتيح. إذا كتبت LaunchedEffect { ... } بدون معاملات، فسيتم إعادة تشغيل الكوروتين عند كل إعادة تركيب، مما يؤدي إلى حلقة لا نهائية من الطلبات. يتطلب LaunchedEffect مفتاحاً واحداً على الأقل — عادةً Unit للتنفيذ لمرة واحدة.
الخطأ الثاني هو محاولة استخدام LaunchedEffect للاشتراك في Flow بدون collect. إذا استدعيت collect على Flow داخل LaunchedEffect، فسيتم تعليق الكوروتين حتى يكتمل Flow (الذي في حالة StateFlow لا يحدث أبداً)، ولن تتمكن كتلة التنظيف من الإنهاء بشكل صحيح. النهج الصحيح هو استخدام collectLatest، الذي يلغي المجموعة السابقة عند وصول قيمة جديدة.
الخطأ الثالث هو تمرير كائنات متداخلة كمفاتيح. إذا كان المفتاح هو data class بحقول قابلة للتغير (var)، فقد لا يتعرف LaunchedEffect على التغيير، لأن Compose يستخدم equals() للمقارنة، والذي يمكن أن يتصرف بشكل غير متوقع مع حقول var. استخدم دائماً كائنات غير قابلة للتغير (val) أو بدائيات كمفاتيح LaunchedEffect.
الأسئلة الشائعة
إذا لم تمرر مفاتيح، LaunchedEffect لن يتم تجميعه — Kotlin يتطلب معامل واحد على الأقل لمعاملات vararg. استخدم LaunchedEffect(Unit) للتنفيذ لمرة واحدة عند الدخول في التركيب أو مرر قيماً محددة يجب أن تؤدي إلى إعادة التشغيل عند تغيرها.
لا، LaunchedEffect يلغي الكوروتين تلقائياً عند خروج composable من التركيب، مما يمنع تسرب الذاكرة. ومع ذلك، إذا كان الكوروتين داخل LaunchedEffect يحتفظ بمرجع لنشاط أو Context عبر إغلاق، فمن الممكن حدوث تسرب — استخدم viewModelScope للعمليات طويلة العمر في ViewModel.
LaunchedEffect ينفذ كوروتين تلقائياً عند الدخول في التركيب مع ربط المفاتيح. rememberCoroutineScope يوفر نطاقاً للتشغيل اليدوي للكوروتينات، على سبيل المثال، استجابة لـ onItemClick. استخدم LaunchedEffect للتأثيرات الجانبية التلقائية وrememberCoroutineScope لتشغيل الكوروتينات بناءً على أحداث المستخدم.
إذا كان مفتاح LaunchedEffect من نوع غير مستقر (على سبيل المثال، var أو class بدون equals())، فقد لا يتعرف Compose على أن القيمة لم تتغير وسيعيد تشغيل التأثير في كل إعادة تركيب. الحل: استخدم أنواعاً مستقرة (بدائيات، نصوص، data classes حقولها val) أو لف القيم القابلة للتغير في remember.
لا توجد طريقة مباشرة لإيقاف LaunchedEffect من الخارج — تتم الإدارة عبر المفاتيح. غير المفتاح لإلغاء الكوروتين الحالي. إذا كنت بحاجة إلى تحكم كامل في دورة حياة الكوروتين، استخدم rememberCoroutineScope مع Job واستدع job.cancel() يدوياً عند حدث أو تغير حالة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا