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

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

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

حالات استخدام الحالة والتأثير

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

بسبب الإمكانيات المختلفة التي تتيحها التأثيرات في ميزة "الإنشاء"، يمكن استخدامها بسهولة بشكل مفرط. تأكَّد من أنّ العمل الذي تؤديه في هذه التطبيقات مرتبط بواجهة المستخدم ولا يعطّل تدفق البيانات أحادي الاتجاه كما هو موضّح في مستندات إدارة الحالة.

LaunchedEffect: تشغيل دوال التعليق في نطاق دالة مركّبة

لتنفيذ عمل على مدار عمر دالة مركّبة والقدرة على استدعاء الدوال التي تتضمّن ميزة "تعليق مؤقت"، استخدِم الدالة المركّبة LaunchedEffect. عندما يدخل LaunchedEffect في التركيب، يتم تشغيل دالّة برمجية متعدّدة المهام مع مجموعة الرموز البرمجية التي تم تمريرها كمَعلمة. وسيتم إلغاء الكوروتين في حال ترك LaunchedEffect المقطوعة. إذا تم إعادة تكوين LaunchedEffect باستخدام مفاتيح مختلفة (راجِع قسم تأثيرات إعادة التشغيل أدناه)، سيتم إلغاء دالة coroutine الحالية وسيتم تشغيل دالة التعليق الجديدة في دالة coroutine جديدة.

على سبيل المثال، إليك صورة متحركة تُظهر قيمة شفافية متغيرة بفاصل زمني قابل للضبط:

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

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

إعادة تشغيل التأثيرات

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