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

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

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

موارد استفاده را بیان و اثر کنید

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

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

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

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

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

// 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 تغییر کنند، composable باید اثر فعلی خود را از بین ببرد (پاکسازی را برای) انجام دهد و با فراخوانی مجدد افکت بازنشانی شود.

به عنوان مثال، ممکن است بخواهید با استفاده از LifecycleObserver رویدادهای تحلیلی را بر اساس رویدادهای Lifecycle ارسال کنید. برای گوش دادن به آن رویدادها در 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 مدیریت نمی‌شوند، از 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 : تبدیل حالت غیر Compose به حالت Compose

produceState یک کوروتین با محدوده Composition راه‌اندازی می‌کند که می‌تواند مقادیر را به State برگشتی منتقل کند. از آن برای تبدیل حالت غیر Compose به حالت Compose استفاده کنید، به عنوان مثال، حالت‌های مبتنی بر اشتراک خارجی مانند Flow ، LiveData یا RxJava را در ترکیب قرار دهید.

زمانی که produceState وارد Composition شود، تولیدکننده راه‌اندازی می‌شود و با خروج از Composition لغو می‌شود. 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 بیشتر از چیزی که نیاز به ترکیب مجدد دارید تغییر می کند. این اغلب زمانی اتفاق می‌افتد که چیزی به طور مکرر در حال تغییر است، مانند موقعیت اسکرول، اما سازنده فقط زمانی که از آستانه خاصی عبور می‌کند باید به آن واکنش نشان دهد. derivedStateOf یک شیء جدید Compose State ایجاد می کند که می توانید مشاهده کنید که فقط به اندازه نیاز شما به روز می شود. به این ترتیب، مشابه عملگر 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 0 5 و غیره می شود. این عدم تطابق در فرکانس به‌روزرسانی به این معنی است که این مورد استفاده خوبی برای 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 را به Flow تبدیل کنید

از 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 به یک Flow تبدیل شده است که می تواند از قدرت عملگرهای Flow بهره مند شود.

شروع مجدد جلوه ها

برخی از افکت‌ها در Compose، مانند LaunchedEffect ، produceState ، یا DisposableEffect ، تعداد متغیری از آرگومان‌ها، کلیدها را می‌گیرند که برای لغو افکت در حال اجرا و شروع اثر جدید با کلیدهای جدید استفاده می‌شوند.

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

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 که در بالا نشان داده شده است. با این حال، قبل از انجام این کار، دو بار فکر کنید و مطمئن شوید که این چیزی است که نیاز دارید.

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}