عوارض جانبی در Compose

یک اثر جانبی ، تغییری در وضعیت برنامه است که خارج از محدوده یک تابع composable اتفاق می‌افتد. با توجه به چرخه عمر composableها و ویژگی‌هایی مانند ترکیب‌های غیرقابل پیش‌بینی، اجرای ترکیب‌های composableها با ترتیب‌های مختلف یا ترکیب‌هایی که می‌توانند دور ریخته شوند، composableها در حالت ایده‌آل باید بدون اثر جانبی باشند .

با این حال، گاهی اوقات عوارض جانبی ضروری هستند، به عنوان مثال، برای ایجاد یک رویداد یک‌باره مانند نمایش یک اسنک‌بار یا رفتن به صفحه دیگر با توجه به یک وضعیت خاص. این اقدامات باید از یک محیط کنترل‌شده که از چرخه حیات composable آگاه است، فراخوانی شوند. در این صفحه، در مورد APIهای عوارض جانبی مختلفی که Jetpack Compose ارائه می‌دهد، خواهید آموخت.

موارد استفاده از حالت و اثر

همانطور که در مستندات Thinking in Compose آمده است، composableها باید بدون عوارض جانبی باشند. هنگامی که نیاز به ایجاد تغییرات در وضعیت برنامه دارید (همانطور که در مستندات Managing state توضیح داده شده است)، باید از APIهای Effect استفاده کنید تا آن عوارض جانبی به شیوه‌ای قابل پیش‌بینی اجرا شوند .

با توجه به امکانات مختلفی که افکت‌ها در Compose ارائه می‌دهند، می‌توان به راحتی از آنها بیش از حد استفاده کرد. مطمئن شوید کاری که در آنها انجام می‌دهید مربوط به رابط کاربری است و جریان داده یک طرفه را همانطور که در مستندات مدیریت وضعیت توضیح داده شده است، مختل نمی‌کند.

LaunchedEffect : اجرای توابع suspend در محدوده یک composable

برای انجام کار در طول عمر یک composable و داشتن قابلیت فراخوانی توابع suspend، از LaunchedEffect composable استفاده کنید. وقتی LaunchedEffect وارد Composition می‌شود، یک کوروتین با بلوک کدی که به عنوان پارامتر ارسال می‌شود، اجرا می‌کند. اگر LaunchedEffect از composition خارج شود، کوروتین لغو خواهد شد. اگر LaunchedEffect با کلیدهای مختلف دوباره ترکیب شود (به بخش Restarting Effects در زیر مراجعه کنید)، کوروتین موجود لغو می‌شود و تابع suspend جدید در یک کوروتین جدید اجرا می‌شود.

برای مثال، در اینجا انیمیشنی را مشاهده می‌کنید که مقدار آلفا را با تأخیر قابل تنظیم پالس می‌کند:

// 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 { mutableLongStateOf(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 به صفر می‌رساند و دوباره برمی‌گرداند. این کار تا پایان عمر composable تکرار می‌شود.

rememberCoroutineScope : یک محدوده آگاه از ترکیب برای راه‌اندازی یک کوروتین خارج از یک ترکیب‌پذیر، به دست آورید.

از آنجایی که LaunchedEffect یک تابع composable است، فقط می‌توان آن را درون سایر توابع composable استفاده کرد. برای اینکه یک کوروتین را خارج از یک تابع composable اجرا کنید، اما scoped باشد تا پس از خروج از ترکیب، به طور خودکار لغو شود، از 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 : به مقداری در یک effect ارجاع می‌دهد که در صورت تغییر مقدار، نباید مجدداً راه‌اندازی شود.

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 همیشه حاوی آخرین مقداری است که LandingScreen با آن بازسازی شده است، onTimeout باید با تابع rememberUpdatedState پوشش داده شود. مقدار State برگردانده شده، currentOnTimeout در کد، باید در افکت استفاده شود.

DisposableEffect : افکت‌هایی که نیاز به پاکسازی دارند

برای عوارض جانبی که باید پس از تغییر کلیدها یا در صورت خروج composable از Composition پاک شوند ، از DisposableEffect استفاده کنید. اگر کلیدهای DisposableEffect تغییر کنند، composable باید اثر فعلی خود را پاک‌سازی کند (پاکسازی را انجام دهد) و با فراخوانی مجدد effect، آن را مجدداً تنظیم کند.

به عنوان مثال، ممکن است بخواهید رویدادهای تحلیلی را بر اساس رویدادهای 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 تضمین می‌کند که effect پس از هر recomposition موفق اجرا شود. از سوی دیگر، اجرای یک effect قبل از تضمین recomposition موفق، نادرست است، که این مورد هنگام نوشتن مستقیم effect در یک composable صادق است.

برای مثال، کتابخانه تحلیلی شما ممکن است به شما این امکان را بدهد که با پیوست کردن فراداده‌های سفارشی (در این مثال «ویژگی‌های کاربر») به تمام رویدادهای تحلیلی بعدی، جمعیت کاربران خود را بخش‌بندی کنید. برای ارتباط نوع کاربر فعلی با کتابخانه تحلیلی خود، 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 یک کوروتینِ محدود به کامپوزیشن (Composition) راه‌اندازی می‌کند که می‌تواند مقادیر را به یک State برگردانده شده ارسال کند. از آن برای تبدیل حالت غیرکامپوزیتی به حالت کامپوزیتی استفاده کنید، برای مثال، حالت‌های خارجیِ مبتنی بر اشتراک مانند Flow ، LiveData یا RxJava به کامپوزیشن بیاورید.

تولیدکننده زمانی اجرا می‌شود که produceState وارد ترکیب شود و زمانی که از ترکیب خارج شود، لغو خواهد شد. State بازگشتی با هم ترکیب می‌شود؛ تنظیم همان مقدار، ترکیب مجدد را آغاز نمی‌کند.

اگرچه produceState یک کوروتین ایجاد می‌کند، اما می‌توان از آن برای مشاهده منابع داده‌ای که در حالت تعلیق نیستند نیز استفاده کرد. برای حذف اشتراک در آن منبع، از تابع awaitDispose استفاده کنید.

مثال زیر نحوه استفاده از produceState برای بارگذاری یک تصویر از شبکه را نشان می‌دهد. تابع composable مربوط به loadNetworkImage State برمی‌گرداند که می‌تواند در composableهای دیگر استفاده شود.

@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 زمانی استفاده کنید که ورودی‌های شما به یک composable بیشتر از آنچه که برای recompose نیاز دارید، تغییر می‌کنند. این اغلب زمانی اتفاق می‌افتد که چیزی مرتباً تغییر می‌کند، مانند موقعیت اسکرول، اما composable فقط زمانی که از یک آستانه خاص عبور می‌کند، نیاز به واکنش نشان دادن به آن دارد. derivedStateOf یک شیء Compose state جدید ایجاد می‌کند که می‌توانید مشاهده کنید که فقط به اندازه نیاز شما به‌روزرسانی می‌شود. به این ترتیب، مشابه عملگر 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 است).

مثال زیر یک اثر جانبی را نشان می‌دهد که وقتی کاربر از اولین مورد در لیست به بخش تجزیه و تحلیل می‌رود، ثبت می‌شود:

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 ، تعداد متغیری آرگومان یا کلید دریافت می‌کنند که برای لغو افکت در حال اجرا و شروع یک افکت جدید با کلیدهای جدید استفاده می‌شوند.

فرم معمول برای این APIها به صورت زیر است:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

با توجه به ظرافت‌های این رفتار، اگر پارامترهای مورد استفاده برای راه‌اندازی مجدد افکت درست نباشند، ممکن است مشکلاتی رخ دهد:

  • راه‌اندازی مجدد افکت‌ها کمتر از حد لازم می‌تواند باعث ایجاد باگ در برنامه شما شود.
  • شروع مجدد اثرات بیش از آنچه که باید، می‌تواند ناکارآمد باشد.

به عنوان یک قاعده کلی، متغیرهای تغییرپذیر و تغییرناپذیر مورد استفاده در بلوک کد effect باید به عنوان پارامتر به effect composable اضافه شوند. جدا از این موارد، می‌توان پارامترهای بیشتری را برای راه‌اندازی مجدد اجباری effect اضافه کرد. اگر تغییر یک متغیر نباید باعث راه‌اندازی مجدد effect شود، متغیر باید در rememberUpdatedState قرار گیرد. اگر متغیر هرگز تغییر نمی‌کند زیرا در یک remember بدون کلید قرار گرفته است، نیازی نیست متغیر را به عنوان کلید به effect منتقل کنید.

در کد 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 هرگز در Composition تغییر نمی‌کند. اگر lifecycleOwner به عنوان پارامتر ارسال نکنید و تغییر کند، HomeScreen دوباره ترکیب می‌شود، اما DisposableEffect از بین نمی‌رود و مجدداً راه‌اندازی نمی‌شود. این امر باعث ایجاد مشکل می‌شود زیرا از آن نقطه به بعد از lifecycleOwner اشتباه استفاده می‌شود.

ثابت‌ها به عنوان کلید

می‌توانید از ثابتی مانند true به عنوان کلید اثر استفاده کنید تا آن را از چرخه حیات سایت فراخوانی پیروی کند . موارد استفاده معتبری برای آن وجود دارد، مانند مثال LaunchedEffect که در بالا نشان داده شده است. با این حال، قبل از انجام این کار، خوب فکر کنید و مطمئن شوید که این همان چیزی است که نیاز دارید.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}