التأثيرات الجانبية في Compose

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

ومع ذلك، تكون الآثار الجانبية ضرورية في بعض الأحيان، مثلاً لتشغيل حدث لمرة واحدة، مثل عرض شريط معلومات أو الانتقال إلى شاشة أخرى عند توفّر حالة معيّنة. يجب استدعاء هذه الإجراءات من بيئة خاضعة للتحكّم على دراية بدورة حياة العنصر القابل للإنشاء. في هذه الصفحة، ستتعرّف على واجهات برمجة التطبيقات المختلفة التي توفّرها 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).

يعرض المثال التالي تأثيرًا جانبيًا يسجّل في &quot;إحصاءات Google&quot; الوقت الذي يمر فيه المستخدم بجانب العنصر الأول في القائمة:

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 الموضّح أعلاه. ومع ذلك، قبل إجراء ذلك، يجب التفكير مليًا والتأكّد من أنّ هذا ما تحتاج إليه.