التأثير الجانبي هو تغيير في حالة التطبيق يحدث خارج نطاق دالة قابلة للتجميع. بسبب دورة حياة العناصر القابلة للتجميع وخصائصها، مثل عمليات إعادة التركيب التي لا يمكن التنبؤ بها أو تنفيذ عمليات إعادة تركيب العناصر القابلة للتجميع بترتيبات مختلفة أو عمليات إعادة التركيب التي يمكن تجاهلها، يجب أن تكون العناصر القابلة للتجميع خالية من الآثار الجانبية.
ومع ذلك، تكون الآثار الجانبية ضرورية في بعض الأحيان، على سبيل المثال، لبدء حدث لمرة واحدة، مثل عرض شريط معلومات سريع أو الانتقال إلى شاشة أخرى استنادًا إلى حالة معيّنة. يجب استدعاء هذه الإجراءات من بيئة تتم إدارتها وتدرك دورة حياة العنصر القابل للتجميع. في هذه الصفحة، ستتعرّف على واجهات برمجة التطبيقات المختلفة التي توفّرها Jetpack Compose لعرض التأثيرات الجانبية.
حالات استخدام الحالة والتأثير
كما هو موضّح في مستندات التفكير في Compose، يجب أن تكون العناصر القابلة للتجميع خالية من الآثار الجانبية. عندما تحتاج إلى إجراء تغييرات على حالة التطبيق (كما هو موضّح في مستند مستندات إدارة الحالة)، يجب استخدام واجهات برمجة التطبيقات الخاصة بالتأثير لكي يتم تنفيذ هذه التأثيرات الجانبية بطريقة يمكن توقّعها.
بسبب الإمكانيات المختلفة التي توفّرها التأثيرات في ميزة "الإنشاء"، يمكن استخدامها بسهولة بشكل مفرط. تأكَّد من أنّ العمل الذي تُجريه في هذه العناصر مرتبط بواجهة المستخدم و لا يؤدي إلى إيقاف تدفق البيانات أحادي الاتجاه كما هو موضّح في مستندات إدارة الحالة.
LaunchedEffect
: تشغيل دوال التعليق في نطاق دالة مركّبة
لتنفيذ عمل على مدار عمر دالة مركّبة والقدرة على استدعاء الدوال التي تتضمّن ميزة "تعليق مؤقت"، استخدِم الدالة المركّبة
LaunchedEffect
. عندما يدخل LaunchedEffect
في التركيب، يتم تشغيل دالّة برمجية
متعدّدة المهام مع مجموعة الرموز البرمجية التي تم تمريرها كمَعلمة. سيتم
إلغاء دالة "الوقت المستغرَق في تنفيذ دالة برمجية" إذا غادر 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
مرتبطًا بنقطة التركيب التي يتمّ استدعاؤها فيها. سيتم إلغاء
النطاق عندما تغادر المكالمة التركيب.
استنادًا إلى المثال السابق، يمكنك استخدام هذا الرمز لعرض 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 */ }
لإنشاء تأثير يتطابق مع دورة حياة موقع الاتصال، يتم تمرير CONSTANT
لا يتغيّر أبدًا مثل Unit
أو true
كمَعلمة. في
الرمز البرمجي أعلاه، يتم استخدام LaunchedEffect(true)
. للتأكّد من أنّ دالة onTimeout
لامدا دائمًا تحتوي على أحدث قيمة تمت إعادة تركيب LandingScreen
بها، يجب لفّ onTimeout
بدالة rememberUpdatedState
.
يجب استخدام State
وcurrentOnTimeout
المعروضَين في الرمز في
التأثير.
DisposableEffect
: التأثيرات التي تتطلّب تنظيفًا
بالنسبة إلى التأثيرات الجانبية التي يجب تنظيفها بعد تغيير المفاتيح أو إذا خرج العنصر المكوّن من التركيب، استخدِم 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
كبيان أخير
في مجموعة التعليمات البرمجية. بخلاف ذلك، يعرض IDE خطأ في وقت الإنشاء.
SideEffect
: نشر حالة Compose إلى رمز غير Compose
لمشاركة حالة Compose مع عناصر لا تُدار من خلال Compose، استخدِم العنصر
SideEffect
composable. يضمن استخدام 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
: تحويل حالة "غير كتابة رسالة" إلى حالة "كتابة رسالة"
produceState
تُطلق دالة State
التي تُستخدم في وظائف معالجة المهام المتعدّدة (Coroutine) على مستوى التركيب، والتي يمكنها دفع القيم إلى State
المعروضة. استخدِمه لتحويل حالة غير "إنشاء" إلى حالة "إنشاء"، على سبيل المثال، إدخال حالة خارجية مستندة إلى الاشتراك، مثل Flow
أو LiveData
أو RxJava
، في التكوين.
يتم تشغيل المُنتج عندما يدخل 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 جديدًا
يمكنك مراقبته ولا يتم تعديله إلا بالقدر الذي تحتاج إليه. بهذه الطريقة، يعمل
بطريقة مشابهة لعامل Kotlin Flows
distinctUntilChanged()
.
الاستخدام الصحيح
يعرض المقتطف التالي حالة استخدام مناسبة لسمة 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 إلى Flows
استخدِم snapshotFlow
لتحويل عناصر State<T>
إلى مسار اتّجاه بارد. يُنفِّذ snapshotFlow
العنصر المكوّن له عند جمعه ويُرسِل
نتيجة عناصر State
التي تمت قراءتها فيه. عند حدوث تغيير في أحد عناصر State
التي يتم قراءتها داخل العنصر snapshotFlow
، ستُصدِر عملية Flow القيمة الجديدة
إلى مجمّعها إذا لم تكن القيمة الجديدة تساوي
القيمة السابقة التي تم بثّها (يشبه هذا السلوك سلوك
Flow.distinctUntilChanged
).
يعرض المثال التالي تأثيرًا جانبيًا يسجّل عندما ينتقل المستخدم بعيدًا عن العنصر الأول في القائمة إلى الإحصاءات:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
في الرمز البرمجي أعلاه، يتم تحويل listState.firstVisibleItemIndex
إلى عملية تدفق يمكنها
الاستفادة من فعالية عوامل التشغيل في "عملية تدفق".
إعادة تشغيل التأثيرات
تأخذ بعض التأثيرات في أداة "الإنشاء"، مثل LaunchedEffect
أو produceState
أو
DisposableEffect
، عددًا متغيرًا من الوسيطات والمفاتيح التي تُستخدَم ل canceled تأثير التشغيل وبدء تأثير جديد بالمفاتيح الجديدة.
التنسيق المعتاد لواجهات برمجة التطبيقات هذه هو:
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
، لأنّ قيمتهما لا تتغيّر أبدًا في "التركيب" بسبب استخدام
rememberUpdatedState
. إذا لم يتم تمرير lifecycleOwner
كمَعلمة وتغيّرت، تتم إعادة تركيب HomeScreen
، ولكن لا يتم التخلص من DisposableEffect
وإعادة تشغيله. ويؤدي ذلك إلى حدوث مشاكل لأنّه يتم استخدام lifecycleOwner
الخاطئ من تلك النقطة فصاعدًا.
استخدام الثوابت كمفاتيح
يمكنك استخدام ثابت مثل true
كمفتاح تأثير لجعله يتّبع دورة حياة موقع الاتصال. هناك حالات استخدام صالحة
له، مثل مثال LaunchedEffect
أعلاه. ومع ذلك، قبل إجراء ذلك،
عليك التفكير جيدًا والتأكّد من أنّ هذا هو ما تحتاجه.
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون لغة JavaScript غير مفعّلة.
- State وJetpack Compose
- Kotlin لـ Jetpack Compose
- استخدام "طرق العرض" في ميزة "الإنشاء"