التأثير الجانبي هو تغيير في حالة التطبيق يحدث خارج نطاق دالة قابلة للإنشاء. بسبب دورة حياة العناصر القابلة للإنشاء وخصائصها، مثل عمليات إعادة الإنشاء غير المتوقّعة، أو عمليات إعادة الإنشاء التي يمكن تجاهلها، أو عمليات إعادة الإنشاء التي يتم تنفيذها بترتيبات مختلفة، يجب أن تكون العناصر القابلة للإنشاء خالية من الآثار الجانبية.
ومع ذلك، تكون الآثار الجانبية ضرورية في بعض الأحيان، مثلاً لتشغيل حدث لمرة واحدة، مثل عرض شريط معلومات أو الانتقال إلى شاشة أخرى عند توفّر حالة معيّنة. يجب استدعاء هذه الإجراءات من بيئة خاضعة للتحكّم على دراية بدورة حياة العنصر القابل للإنشاء. في هذه الصفحة، ستتعرّف على واجهات برمجة التطبيقات المختلفة التي توفّرها Jetpack Compose للتأثيرات الجانبية.
حالات استخدام ميزة "الحالة والتأثير"
كما هو موضّح في مستندات التفكير في Compose، يجب أن تكون العناصر القابلة للإنشاء خالية من الآثار الجانبية. عندما تحتاج إلى إجراء تغييرات على حالة التطبيق (كما هو موضّح في مستند إدارة الحالة)، عليك استخدام واجهات برمجة التطبيقات الخاصة بالتأثيرات حتى يتم تنفيذ هذه الآثار الجانبية بطريقة يمكن توقّعها.
بسبب الإمكانات المختلفة التي تتيحها التأثيرات في Compose، يمكن الإفراط في استخدامها بسهولة. تأكَّد من أنّ العمل الذي تجريه في هذه الدوال مرتبط بواجهة المستخدم ولا يؤدي إلى إيقاف تدفّق البيانات أحادي الاتجاه كما هو موضّح في مستندات إدارة الحالة.
LaunchedEffect
: تشغيل دوال تعليق في نطاق عنصر قابل للإنشاء
لتنفيذ عمليات على مدار عمر عنصر قابل للإنشاء مع إمكانية استدعاء دوال معلّقة، استخدِم العنصر القابل للإنشاء LaunchedEffect
. عندما يدخل LaunchedEffect
إلى Composition، يتم تشغيل روتين فرعي باستخدام مجموعة الرموز البرمجية التي تم تمريرها كمَعلمة. سيتم إلغاء الروتين المشترك إذا غادر LaunchedEffect
التركيب. إذا تمت إعادة إنشاء LaunchedEffect
باستخدام مفاتيح مختلفة (راجِع القسم إعادة تشغيل التأثيرات أدناه)، سيتم إلغاء الروتين الفرعي الحالي وسيتم تشغيل وظيفة التعليق الجديدة في روتين فرعي جديد.
على سبيل المثال، إليك رسمًا متحركًا ينبض بقيمة ألفا مع تأخير قابل للضبط:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
في الرمز البرمجي أعلاه، يستخدم التأثير دالة تعليق
delay
لانتظار المدة الزمنية المحدّدة. بعد ذلك، يتم تحريك قيمة ألفا بشكل تسلسلي إلى الصفر ثم إلى القيمة الأصلية مرة أخرى باستخدام animateTo
.
سيتم تكرار هذا الإجراء طوال مدة بقاء العنصر القابل للإنشاء.
rememberCoroutineScope
: الحصول على نطاق مدرِك للتكوين لتشغيل روتين فرعي خارج عنصر قابل للإنشاء
بما أنّ LaunchedEffect
هي دالة قابلة للإنشاء، لا يمكن استخدامها إلا داخل دوال أخرى قابلة للإنشاء. لبدء روتين فرعي خارج عنصر قابل للإنشاء، ولكن ضمن نطاق يضمن إلغاءه تلقائيًا عند مغادرة التركيب، استخدِم rememberCoroutineScope
.
استخدِم rememberCoroutineScope
أيضًا عندما تحتاج إلى التحكّم يدويًا في دورة حياة روتين فرعي واحد أو أكثر، مثل إلغاء صورة متحركة عند وقوع حدث من المستخدم.
rememberCoroutineScope
هي دالة قابلة للإنشاء تعرض CoroutineScope
مرتبطًا بنقطة التركيب التي يتم استدعاؤها فيها. سيتم إلغاء النطاق عندما تغادر المكالمة Composition.
استنادًا إلى المثال السابق، يمكنك استخدام هذا الرمز لعرض Snackbar
عندما ينقر المستخدِم على Button
:
@Composable fun MoviesScreen(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
: تشير إلى قيمة في تأثير لا يجب إعادة تشغيله إذا تغيّرت القيمة
يتم إعادة تشغيل LaunchedEffect
عند تغيير إحدى المَعلمات الرئيسية. ومع ذلك، في بعض الحالات، قد تحتاج إلى تسجيل قيمة في التأثير لا تريد أن تتم إعادة تشغيلها إذا تغيّرت. لإجراء ذلك، يجب استخدام rememberUpdatedState
لإنشاء مرجع إلى هذه القيمة التي يمكن تسجيلها وتعديلها. ويفيد هذا الأسلوب في التأثيرات التي تتضمّن عمليات طويلة الأمد قد يكون من المكلف أو الصعب إعادة إنشائها وإعادة تشغيلها.
على سبيل المثال، لنفترض أنّ تطبيقك يتضمّن LandingScreen
يختفي بعد مرور بعض الوقت. حتى إذا تمت إعادة إنشاء LandingScreen
، يجب عدم إعادة تشغيل التأثير الذي ينتظر بعض الوقت ويُعلم بأنّ الوقت قد مرّ:
@Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
لإنشاء تأثير يتطابق مع مراحل نشاط الموقع الذي يتم استدعاؤه، يتم تمرير قيمة ثابتة لا تتغيّر أبدًا، مثل Unit
أو true
، كمَعلمة. في الرمز أعلاه، يتم استخدام LaunchedEffect(true)
. للتأكّد من أنّ onTimeout
lambda تحتوي دائمًا على أحدث قيمة تم إعادة تركيب LandingScreen
بها، يجب تضمين onTimeout
في الدالة rememberUpdatedState
.
يجب استخدام State
وcurrentOnTimeout
اللذين تم إرجاعهما في الرمز البرمجي في التأثير.
DisposableEffect
: التأثيرات التي تتطلّب تنظيفًا
بالنسبة إلى الآثار الجانبية التي يجب تنظيفها بعد تغيير المفاتيح أو إذا غادر العنصر القابل للإنشاء Composition، استخدِم DisposableEffect
.
في حال تغيّر مفاتيح DisposableEffect
، يجب أن تتخلص العناصر القابلة للإنشاء (أي أن تنفّذ عملية التنظيف) من التأثير الحالي، ثم تعيد ضبطه من خلال استدعاء التأثير مرة أخرى.
على سبيل المثال، قد تحتاج إلى إرسال أحداث إحصاءات استنادًا إلى
أحداث Lifecycle
باستخدام
LifecycleObserver
.
للاستماع إلى هذه الأحداث في Compose، استخدِم DisposableEffect
لتسجيل المراقب وإلغاء تسجيله عند الحاجة.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
في الرمز أعلاه، سيضيف التأثير observer
إلى lifecycleOwner
. إذا تغيّرت قيمة lifecycleOwner
، سيتم إلغاء التأثير وإعادة تشغيله باستخدام القيمة الجديدة lifecycleOwner
.
يجب أن يتضمّن DisposableEffect
عبارة onDispose
كالجملة الأخيرة في مجموعة الرموز البرمجية. وإلا، ستعرض بيئة التطوير المتكاملة خطأ في وقت الإنشاء.
SideEffect
: نشر حالة Compose إلى رمز غير Compose
لمشاركة حالة Compose مع عناصر لا تتم إدارتها من خلال Compose، استخدِم الدالة البرمجية القابلة للإنشاء
SideEffect
. يضمن استخدام SideEffect
تنفيذ التأثير بعد كل عملية إعادة إنشاء ناجحة. من ناحية أخرى، من غير الصحيح تنفيذ تأثير قبل ضمان إعادة التركيب بنجاح، وهو ما يحدث عند كتابة التأثير مباشرةً في عنصر قابل للإنشاء.
على سبيل المثال، قد تتيح لك مكتبة الإحصاءات تقسيم قاعدة المستخدمين
عن طريق إرفاق بيانات وصفية مخصّصة ("سمات المستخدم" في هذا المثال)
بجميع أحداث الإحصاءات اللاحقة. لتحديد نوع المستخدم الحالي في مكتبة الإحصاءات، استخدِم SideEffect
لتعديل قيمته.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: تحويل حالة غير Compose إلى حالة Compose
تُطلق الدالة produceState
روتينًا فرعيًا ضمن نطاق Composition يمكنه إرسال القيم إلى State
تم إرجاعه. يمكنك استخدامها لتحويل حالة غير متوافقة مع Compose إلى حالة متوافقة مع Compose، مثلاً، نقل حالة خارجية مستندة إلى الاشتراك، مثل Flow
أو LiveData
أو RxJava
، إلى Composition.
يتم تشغيل المنتج عندما يدخل produceState
إلى "المقطوعة الموسيقية"، وسيتم إلغاؤه عند الخروج من "المقطوعة الموسيقية". يؤدي State
الذي يتم عرضه إلى دمج البيانات،
ولن يؤدي ضبط القيمة نفسها إلى إعادة إنشاء التركيب.
على الرغم من أنّ produceState
تنشئ روتينًا فرعيًا، يمكن استخدامها أيضًا لمراقبة مصادر البيانات غير المعلقة. لإزالة الاشتراك في مصدر البيانات هذا، استخدِم الدالة awaitDispose
.
يوضّح المثال التالي كيفية استخدام produceState
لتحميل صورة من الشبكة. تعرض الدالة القابلة للإنشاء loadNetworkImage
عنصر State
يمكن استخدامه في عناصر أخرى قابلة للإنشاء.
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
: تحويل عنصر حالة واحد أو أكثر إلى حالة أخرى
في Compose، تحدث إعادة التركيب في كل مرة يتغير فيها كائن حالة مراقَب أو إدخال قابل للإنشاء. قد يتغيّر عنصر الحالة أو الإدخال بشكل متكرّر أكثر مما يحتاج إليه عنصر واجهة المستخدم فعليًا، ما يؤدي إلى إعادة إنشاء غير ضرورية.
يجب استخدام الدالة derivedStateOf
عندما تتغيّر المَعلمات المُدخَلة في عنصر قابل للإنشاء بشكل متكرّر أكثر من الحاجة إلى إعادة التركيب. يحدث ذلك غالبًا عندما يتغيّر شيء ما بشكل متكرر، مثل موضع التمرير، ولكن لا يحتاج العنصر القابل للإنشاء إلى التفاعل معه إلا بعد تجاوزه حدًا معيّنًا. تنشئ derivedStateOf
عنصر حالة جديدًا في Compose يمكنك مراقبته، ولا يتم تعديله إلا بالقدر الذي تحتاج إليه. وبهذه الطريقة، يعمل بشكل مشابه لعامل التشغيل
distinctUntilChanged()
في Kotlin Flows.
الاستخدام الصحيح
يعرض المقتطف التالي حالة استخدام مناسبة للسمة derivedStateOf
:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
في هذا المقتطف، يتغيّر firstVisibleItemIndex
في كل مرة يتغيّر فيها العنصر الأول المرئي. أثناء التمرير، تصبح القيمة 0
و1
و2
و3
و4
و5
وما إلى ذلك.
ومع ذلك، لا يلزم إعادة التركيب إلا إذا كانت القيمة أكبر من 0
.
يعني عدم التطابق في معدّل التحديث أنّ هذه حالة استخدام جيدة لـ
derivedStateOf
.
الاستخدام غير الصحيح
من الأخطاء الشائعة افتراض أنّه عند دمج عنصرَين من عناصر حالة Compose، يجب استخدام derivedStateOf
لأنّك "تستمدّ الحالة". ومع ذلك، فإنّ هذا الإجراء هو مجرد تكلفة إضافية وغير مطلوب، كما هو موضّح في المقتطف التالي:
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
في هذا المقتطف، يجب تعديل fullName
بالقدر نفسه الذي يتم فيه تعديل firstName
وlastName
. وبالتالي، لا يحدث أي إعادة تركيب مفرطة، ولا يلزم استخدام derivedStateOf
.
snapshotFlow
: تحويل حالة Compose إلى تدفقات
استخدِم snapshotFlow
لتحويل عناصر State<T>
إلى Flow بارد. تنفِّذ snapshotFlow
كتلتها عند جمعها، وتُصدر نتيجة عناصر State
التي تمت قراءتها فيها. عندما يتم تغيير أحد عناصر State
التي تم قراءتها داخل كتلة snapshotFlow
، سيصدر Flow القيمة الجديدة إلى أداة التجميع إذا كانت القيمة الجديدة لا تساوي القيمة السابقة التي تم إصدارها (هذا السلوك مشابه لسلوك Flow.distinctUntilChanged
).
يعرض المثال التالي تأثيرًا جانبيًا يسجّل في "إحصاءات Google" الوقت الذي يمر فيه المستخدم بجانب العنصر الأول في القائمة:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
في الرمز البرمجي أعلاه، يتم تحويل listState.firstVisibleItemIndex
إلى Flow يمكنه الاستفادة من قوة عوامل تشغيل Flow.
إعادة تشغيل التأثيرات
تتطلّب بعض التأثيرات في Compose، مثل LaunchedEffect
أو produceState
أو DisposableEffect
، عددًا متغيرًا من الوسيطات والمفاتيح التي تُستخدَم لإلغاء التأثير قيد التشغيل وبدء تأثير جديد باستخدام المفاتيح الجديدة.
يكون النموذج النموذجي لواجهات برمجة التطبيقات هذه على النحو التالي:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
بسبب دقة هذا السلوك، يمكن أن تحدث مشاكل إذا لم تكن المَعلمات المستخدَمة لإعادة تشغيل التأثير هي المَعلمات الصحيحة:
- قد يؤدي إعادة تشغيل التأثيرات بشكل أقل من اللازم إلى حدوث أخطاء في تطبيقك.
- قد يكون إعادة تشغيل التأثيرات أكثر من اللازم غير فعّال.
كقاعدة عامة، يجب إضافة المتغيّرات القابلة للتغيير وغير القابلة للتغيير المستخدَمة في كتلة التأثير للرمز البرمجي كمعلَمات إلى الدالة البرمجية القابلة للإنشاء الخاصة بالتأثير. بالإضافة إلى ذلك، يمكن إضافة المزيد من المَعلمات لإعادة تشغيل التأثير. إذا كان تغيير قيمة متغيّر لا يؤدي إلى إعادة تشغيل التأثير، يجب تضمين المتغيّر في rememberUpdatedState
. إذا لم يتغير المتغير مطلقًا لأنّه مضمّن في remember
بدون مفاتيح، لن تحتاج إلى تمرير المتغير كمفتاح إلى التأثير.
في الرمز DisposableEffect
الموضّح أعلاه، يأخذ التأثير lifecycleOwner
المستخدَم في كتلة الرمز كمعلَمة، لأنّ أي تغيير فيها يجب أن يؤدي إلى إعادة تشغيل التأثير.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
لا حاجة إلى currentOnStart
وcurrentOnStop
كمفاتيح DisposableEffect
، لأنّ قيمتها لا تتغيّر أبدًا في Composition بسبب استخدام rememberUpdatedState
. إذا لم يتم تمرير lifecycleOwner
كمعلَمة وتغيّرت، سيتم إعادة إنشاء HomeScreen
، ولكن لن يتم التخلص من DisposableEffect
وإعادة تشغيله. ويؤدي ذلك إلى حدوث مشاكل لأنّه يتم استخدام lifecycleOwner
خاطئ من تلك النقطة فصاعدًا.
الثوابت كمفاتيح
يمكنك استخدام قيمة ثابتة مثل true
كمفتاح تأثير لإتباع دورة حياة الموقع الذي تم استدعاء التأثير منه. وهناك حالات استخدام صالحة لهذا النوع من الروابط، مثل المثال LaunchedEffect
الموضّح أعلاه. ومع ذلك، قبل إجراء ذلك،
يجب التفكير مليًا والتأكّد من أنّ هذا ما تحتاج إليه.
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- الحالة وJetpack Compose
- Kotlin لـ Jetpack Compose
- استخدام طرق العرض في Compose