یک عارضه جانبی تغییر در وضعیت برنامه است که خارج از محدوده یک تابع قابل ترکیب اتفاق می افتد. با توجه به چرخه عمر و ویژگیهای ترکیبپذیر مانند ترکیبهای مجدد غیرقابل پیشبینی، اجرای دوباره ترکیبهای ترکیبپذیر با ترتیبهای مختلف، یا ترکیبهایی که میتوان آنها را دور انداخت، ترکیبپذیرها در حالت ایدهآل باید بدون عارضه جانبی باشند .
با این حال، گاهی اوقات عوارض جانبی لازم است، به عنوان مثال، برای راه اندازی یک رویداد یکباره مانند نشان دادن نوار اسنک یا پیمایش به صفحه دیگری با توجه به شرایط خاص. این اقدامات باید از یک محیط کنترل شده که از چرخه حیات مواد ترکیبی آگاه است فراخوانی شود. در این صفحه، در مورد 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
0
3
، 4
، 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
که در بالا نشان داده شده است. با این حال، قبل از انجام این کار، دو بار فکر کنید و مطمئن شوید که این چیزی است که نیاز دارید.
برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- State و Jetpack Compose
- Kotlin برای Jetpack Compose
- استفاده از Views در Compose