در این صفحه، با چرخه عمر یک Composable آشنا میشوید و میآموزید که Compose چگونه تصمیم میگیرد آیا یک Composable نیاز به Recomposition دارد یا نه.
همانطور که در مستندات Managing state گفته شد، یک Composition رابط کاربری اپلیکیشن شما را توصیف میکند و با اجرای Composableها تولید میشود. Composition یک ساختار درختی از Composableهایی است که UI شما را توصیف میکنند.
وقتی Jetpack Compose برای اولین بار Composableهای شما را اجرا میکند، یعنی در مرحله Initial Composition، Composableهایی را که برای توصیف UI فراخوانی میکنید در یک Composition دنبال و ثبت میکند. سپس وقتی State اپلیکیشن شما تغییر میکند، Jetpack Compose یک Recomposition را زمانبندی میکند. Recomposition زمانی اتفاق میافتد که Jetpack Compose دوباره Composableهایی را که ممکن است در پاسخ به تغییرات State تغییر کرده باشند اجرا میکند و سپس Composition را برای نمایش تغییرات بهروزرسانی میکند.
یک Composition فقط میتواند با Initial Composition تولید شود و با Recomposition بهروزرسانی شود. تنها راه تغییر دادن یک Composition، Recomposition است.
نکته کلیدی: چرخه عمر یک Composable با رویدادهای زیر تعریف میشود: ورود به Composition، صفر بار یا چند بار Recomposition شدن، و خروج از Composition.
شکل ۱. چرخه عمر یک Composable در Composition. ابتدا وارد Composition میشود، سپس صفر بار یا چند بار Recomposition میشود و در نهایت از Composition خارج میشود.
Recomposition معمولاً با تغییر یک شیء State<T> فعال میشود. Compose این Stateها را دنبال میکند و همه Composableهایی را که آن State<T> خاص را خواندهاند، همراه با Composableهایی که آنها فراخوانی میکنند و قابل رد شدن نیستند، اجرا میکند.
نکته: چرخه عمر یک Composable سادهتر از چرخه عمر Viewها، Activityها و Fragmentها است. وقتی یک Composable نیاز دارد منابع خارجیای را مدیریت کند یا با آنها تعامل داشته باشد که چرخه عمر پیچیدهتری دارند، باید از Effects استفاده کنید.
اگر یک Composable چند بار فراخوانی شود، چند نمونه از آن در Composition قرار میگیرد. هر فراخوانی چرخه عمر مخصوص خودش را در Composition دارد.
@Composable
fun MyComposable() {
Column {
Text("Hello")
Text("World")
}
}
شکل ۲. نمایش MyComposable در Composition. اگر یک Composable چند بار فراخوانی شود، چند نمونه از آن در Composition قرار میگیرد. اگر یک عنصر رنگ متفاوتی داشته باشد، نشاندهنده این است که یک نمونه جداگانه است.
نمونه یک Composable در Composition با Call site آن شناسایی میشود. کامپایلر Compose هر Call site را متمایز در نظر میگیرد. فراخوانی Composableها از چند Call site مختلف باعث ایجاد چند نمونه از آن Composable در Composition میشود.
اصطلاح کلیدی: Call site محل قرارگیری فراخوانی یک Composable در سورسکد است. این موضوع روی جایگاه آن در Composition و در نتیجه روی درخت UI تأثیر میگذارد.
اگر در طول Recomposition، یک Composable نسبت به Composition قبلی Composableهای متفاوتی را فراخوانی کند، Compose تشخیص میدهد کدام Composableها فراخوانی شدهاند یا نشدهاند. برای Composableهایی که در هر دو Composition فراخوانی شدهاند، اگر ورودیهای آنها تغییر نکرده باشد، Compose از Recomposition آنها جلوگیری میکند.
حفظ هویت بسیار مهم است تا Side-effectها با Composable مربوط به خودشان مرتبط بمانند و بتوانند با موفقیت کامل شوند، نه اینکه در هر Recomposition دوباره از ابتدا شروع شوند.
مثال زیر را در نظر بگیرید:
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
@Composable
fun LoginInput() { /* ... */ }
@Composable
fun LoginError() { /* ... */ }
LoginScreen بهصورت شرطی Composable مربوط به LoginError را فراخوانی میکند و همیشه Composable مربوط به LoginInput را فراخوانی میکند. هر فراخوانی یک Call site و موقعیت سورس منحصربهفرد دارد که کامپایلر از آن برای شناسایی یکتای آن استفاده میکند.LoginScreen در Composition وقتی State تغییر میکند و Recomposition رخ میدهد. رنگ یکسان یعنی آن بخش Recomposition نشده است.حتی اگر LoginInput از حالت اولین فراخوانی به دومین فراخوانی تغییر کند، نمونه LoginInput در طول Recompositionها حفظ میشود. همچنین چون LoginInput هیچ پارامتری ندارد که در طول Recomposition تغییر کرده باشد، فراخوانی LoginInput توسط Compose رد میشود.
فراخوانی چندباره یک Composable باعث میشود آن Composable چند بار نیز به Composition اضافه شود. وقتی یک Composable چند بار از یک Call site یکسان فراخوانی میشود، Compose اطلاعاتی برای شناسایی یکتای هر فراخوانی آن Composable ندارد؛ بنابراین علاوه بر Call site، از ترتیب اجرا نیز برای متمایز نگه داشتن نمونهها استفاده میکند. این رفتار گاهی کافی است، اما در بعضی موارد میتواند باعث رفتار ناخواسته شود.
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
// MovieOverview composables are placed in Composition given its
// index position in the for loop
MovieOverview(movie)
}
}
}
شکل ۴. نمایش MoviesScreen در Composition وقتی یک عنصر جدید به انتهای لیست اضافه میشود. Composableهای MovieOverview موجود در Composition میتوانند دوباره استفاده شوند. رنگ یکسان در MovieOverview یعنی آن Composable دوباره Recomposition نشده است.
اما اگر لیست فیلمها با اضافه کردن آیتم به ابتدای لیست یا وسط لیست، حذف آیتمها یا تغییر ترتیب آنها تغییر کند، همه فراخوانیهای MovieOverview که پارامتر ورودیشان در لیست جابهجا شده است Recomposition میشوند. این موضوع بسیار مهم است؛ مثلاً اگر MovieOverview با استفاده از یک Side-effect تصویر فیلم را از شبکه دریافت کند. اگر هنگام انجام آن Effect، Recomposition رخ دهد، آن Effect لغو شده و دوباره از ابتدا شروع میشود.
@Composable
fun MovieOverview(movie: Movie) {
Column {
// Side effect explained later in the docs. If MovieOverview
// recomposes, while fetching the image is in progress,
// it is cancelled and restarted.
val image = loadNetworkImage(movie.url)
MovieHeader(image)
/* ... */
}
}
MoviesScreen در Composition وقتی یک عنصر جدید به ابتدای لیست اضافه میشود. همه آیتمهای دیگر در لیست جابهجا میشوند و باید Recomposition شوند. Composableهای MovieOverview نمیتوانند دوباره استفاده شوند و همه Side-effectها دوباره شروع میشوند. رنگ متفاوت در MovieOverview یعنی آن Composable دوباره Recomposition شده است.در حالت ایدهآل، باید هویت نمونه MovieOverview را به هویت فیلمی که به آن داده میشود مرتبط بدانیم. اگر لیست فیلمها را مرتبسازی مجدد کنیم، بهتر است نمونهها در درخت Composition نیز به همان شکل جابهجا شوند، نه اینکه هر Composable مربوط به MovieOverview با یک نمونه فیلم متفاوت Recomposition شود. Compose راهی فراهم میکند تا به Runtime بگویید از چه مقادیری برای شناسایی بخش مشخصی از درخت استفاده کند: Composable به نام key.
با قرار دادن یک بلوک کد داخل فراخوانی Composable به نام key و ارسال یک یا چند مقدار به آن، آن مقادیر با هم ترکیب میشوند و برای شناسایی آن نمونه در Composition استفاده میشوند. مقدار key لازم نیست در کل برنامه یکتا باشد؛ فقط باید بین فراخوانیهای Composable در همان Call site یکتا باشد. بنابراین در این مثال، هر فیلم باید یک key یکتا در میان فیلمها داشته باشد. اگر همان key در جای دیگری از اپلیکیشن برای Composable دیگری استفاده شود، مشکلی ندارد.
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
با کد بالا، حتی اگر عناصر لیست تغییر کنند، Compose فراخوانیهای جداگانه
MovieOverview را تشخیص میدهد و میتواند آنها را دوباره استفاده کند.MoviesScreen در Composition وقتی یک عنصر جدید به لیست اضافه میشود. چون Composableهای MovieOverview کلیدهای یکتا دارند، Compose تشخیص میدهد کدام نمونههای MovieOverview تغییر نکردهاند و میتواند آنها را دوباره استفاده کند؛ بنابراین Side-effectهای آنها به اجرای خود ادامه میدهند.نکته کلیدی: از Composable به نام key استفاده کنید تا به Compose کمک کنید نمونههای Composable را در Composition شناسایی کند. این موضوع زمانی مهم است که چند Composable از یک Call site یکسان فراخوانی میشوند و دارای Side-effect یا State داخلی هستند.
بعضی Composableها بهصورت داخلی از key پشتیبانی میکنند. برای مثال، LazyColumn امکان مشخص کردن یک key سفارشی را در DSL مربوط به items میپذیرد.
@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
LazyColumn {
items(movies, key = { movie -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
در طول Recomposition، بعضی توابع Composable واجد شرایط میتوانند در صورتی که ورودیهایشان نسبت به Composition قبلی تغییر نکرده باشد، بهطور کامل از اجرا شدن رد شوند.
یک تابع Composable واجد شرایط رد شدن است، مگر اینکه:
تابع مقدار بازگشتی غیر از Unit داشته باشد.
تابع با @NonRestartableComposable یا @NonSkippableComposable مشخص شده باشد.
یکی از پارامترهای الزامی آن از نوع غیر Stable باشد.
یک حالت آزمایشی کامپایلر به نام Strong Skipping وجود دارد که شرط آخر را سادهتر میکند.
برای اینکه یک نوع داده Stable در نظر گرفته شود، باید با قرارداد زیر مطابقت داشته باشد:
نتیجه equals برای دو نمونه مشخص همیشه برای همان دو نمونه یکسان باقی بماند.
اگر یکی از propertyهای public آن نوع تغییر کند، Composition از آن مطلع شود.
همه نوعهای propertyهای public آن نیز Stable باشند.
چند نوع رایج مهم وجود دارند که در این قرارداد قرار میگیرند و کامپایلر Compose آنها را Stable در نظر میگیرد، حتی اگر با annotation به نام @Stable بهصورت صریح Stable مشخص نشده باشند:
همه نوعهای primitive مانند Boolean، Int، Long، Float، Char و غیره
رشتهها یا String
همه نوعهای تابعی یا lambdaها
همه این نوعها میتوانند قرارداد Stable را رعایت کنند، چون immutable هستند. از آنجایی که نوعهای immutable هرگز تغییر نمیکنند، نیازی ندارند Composition را از تغییر مطلع کنند؛ بنابراین رعایت این قرارداد برای آنها بسیار آسانتر است.
نکته: همه نوعهایی که بهصورت عمیق immutable هستند، میتوانند با خیال راحت Stable در نظر گرفته شوند.
یک نوع مهم که Stable است اما mutable محسوب میشود، نوع MutableState در Compose است. اگر مقداری داخل یک MutableState نگهداری شود، کل شیء State بهعنوان Stable در نظر گرفته میشود، چون Compose از هر تغییری در property مربوط به .value در State مطلع خواهد شد.
وقتی همه نوعهایی که بهعنوان پارامتر به یک Composable داده میشوند Stable باشند، مقدار پارامترها بر اساس موقعیت Composable در درخت UI با استفاده از equality مقایسه میشوند. اگر همه مقادیر نسبت به فراخوانی قبلی بدون تغییر باشند، Recomposition رد میشود.
نکته کلیدی: Compose اگر همه ورودیها Stable باشند و تغییر نکرده باشند، Recomposition یک Composable را رد میکند. این مقایسه با استفاده از متد equals انجام میشود.
Compose فقط زمانی یک نوع را Stable در نظر میگیرد که بتواند آن را اثبات کند. برای مثال، یک interface معمولاً غیر Stable در نظر گرفته میشود. همچنین نوعهایی که propertyهای public قابل تغییر دارند، حتی اگر پیادهسازی آنها بتواند immutable باشد، Stable محسوب نمیشوند.
اگر Compose نتواند تشخیص دهد که یک نوع Stable است، اما شما میخواهید Compose را مجبور کنید آن را Stable در نظر بگیرد، آن نوع را با annotation به نام @Stable مشخص کنید.
// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
در قطعهکد بالا، چون UiState یک interface است، Compose معمولاً میتواند این نوع را غیر Stable در نظر بگیرد. با اضافه کردن annotation به نام @Stable، به Compose میگویید که این نوع Stable است و به Compose اجازه میدهید Recompositionهای هوشمندتری انجام دهد. این همچنین یعنی اگر interface بهعنوان نوع پارامتر استفاده شود، Compose همه پیادهسازیهای آن را Stable در نظر میگیرد.
نکته کلیدی: اگر Compose نتواند Stable بودن یک نوع را تشخیص دهد، آن نوع را با @Stable مشخص کنید تا Compose بتواند Recompositionهای هوشمندتری انجام دهد.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.android.com/develop/ui/compose/documentation