چرخه حیات عناصر ترکیب‌شدنی

در این صفحه، با چرخه حیات عنصر ترکیبی و نحوه تصمیم‌گیری Compose درباره نیاز عنصر ترکیبی به ترکیب مجدد آشنا می‌شوید.

نمای کلی چرخه حیات

همان‌طور که در مستندات مدیریت وضعیت ذکر شده است، «ترکیب» رابط کاربری برنامه‌تان را توصیف می‌کند و با اجرای عناصر ترکیبی تولید می‌شود. «ترکیب» ساختار درختی از عناصر ترکیبی است که رابط کاربری شما را توصیف می‌کند.

وقتی Jetpack Compose برای اولین‌بار عناصر ترکیبی شما را اجرا می‌کند، درطول ترکیب اولیه، عناصری ترکیبی را که برای توصیف میانای کاربرتان در «ترکیب» فراخوانی می‌کنید پیگیری می‌کند. سپس، وقتی وضعیت برنامه تغییر می‌کند، Jetpack Compose ترکیب مجدد را زمان‌بندی می‌کند. بازترکیب زمانی است که Jetpack Compose ترکیب‌شونده‌هایی را که ممکن است در پاسخ به تغییرات حالت تغییر کرده باشند دوباره اجرا می‌کند، و سپس «ترکیب» را به‌روزرسانی می‌کند تا هرگونه تغییر را منعکس کند.

«قطعه موسیقی» فقط می‌تواند ازطریق قطعه موسیقی اولیه تولید شود و ازطریق قطعه موسیقی مجدد به‌روز شود. تنها راه تغییر دادن «قطعه موسیقی» ازطریق «قطعه موسیقی مجدد» است.

نموداری که چرخه زندگی یک عنصر ترکیبی را نشان می‌دهد
شکل ۱. چرخه حیات یک عنصر ترکیبی در «ترکیب». وارد «ترکیب» می‌شود، ۰ یا چند بار بازترکیب می‌شود، و از «ترکیب» خارج می‌شود.

معمولاً تغییر در شیء State<T> باعث بازترکیب می‌شود. ‫Compose این‌ها را ردیابی می‌کند و همه عناصر ترکیبی را در «ترکیب» که آن State<T> خاص را می‌خوانند اجرا می‌کند، و همچنین همه عناصر ترکیبی که آن‌ها را فرا می‌خوانند و نمی‌توانند رد شوند.

اگر یک عنصر ترکیبی چندین بار فراخوانی شود، چندین نمونه در «ترکیب» قرار می‌گیرد. هر تماس در «ترکیب» چرخه عمر خودش را دارد.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

نموداری که ترتیب سلسله‌مراتبی عناصر را در تکه‌کد قبلی نشان می‌دهد
شکل ۲. نمایش MyComposable در «قطعه موسیقی». اگر یک ترکیب‌پذیر چندین بار فراخوانی شود، چندین نمونه در «ترکیب» قرار می‌گیرد. عنصری که رنگ متفاوتی دارد نشان می‌دهد که نمونه‌ای جداگانه است.

ساختار یک عنصر ترکیبی در «قطعه موسیقی»

نمونه‌ای از عنصر ترکیبی در «ترکیب» با سایت تماس آن شناسایی می‌شود. گردآورنده Compose هر سایت تماس را متمایز درنظر می‌گیرد. فراخوانی عناصر ترکیبی از چندین سایت فراخوانی باعث ایجاد چندین نمونه از عنصر ترکیبی در «ترکیب» می‌شود.

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

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

مثال زیر را درنظر بگیرید:

@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 به‌صورت شرطی LoginError عنصر ترکیبی را فراخوانی می‌کند و همیشه عنصر ترکیبی LoginInput را فراخوانی می‌کند. هر تماس دارای سایت تماس و موقعیت منبع منحصربه‌فردی است که کامپایلر از آن برای شناسایی منحصربه‌فرد آن استفاده می‌کند.

نموداری که نشان می‌دهد اگر پرچم showError به درست تغییر کند، کد قبلی چگونه بازسازی می‌شود. ترکیب‌پذیر LoginError اضافه می‌شود، اما ترکیب‌پذیرهای دیگر بازترکیب نمی‌شوند.
شکل ۳. نمایش LoginScreen در «ترکیب» وقتی وضعیت تغییر می‌کند و ترکیب مجددی رخ می‌دهد. رنگ یکسان یعنی دوباره ترکیب نشده است.

حتی اگر LoginInput از اینکه اول فراخوانده شود به اینکه دوم فراخوانده شود تغییر کند، نمونه LoginInput در ترکیب‌های مجدد حفظ خواهد شد. علاوه‌براین، چون LoginInput هیچ پارامتری ندارد که در ترکیب مجدد تغییر کرده باشد، Compose از تماس با LoginInput صرف‌نظر خواهد کرد.

برای کمک به ترکیب‌بندی‌های هوشمند، اطلاعات اضافی اضافه کنید

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

@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)
        }
    }
}

در مثال بالا، «نوشتن» علاوه‌بر سایت تماس از ترتیب اجرا نیز استفاده می‌کند تا نمونه را در «ترکیب» متمایز نگه دارد. اگر movie جدیدی به پایین فهرست اضافه شود، «نگارش» می‌تواند از نمونه‌های موجود در «نگارش» استفاده مجدد کند زیرا مکان آن‌ها در فهرست تغییر نکرده است و بنابراین، ورودی movie برای آن نمونه‌ها یکسان است.

نموداری که نشان می‌دهد اگر عنصر جدیدی به پایین فهرست اضافه شود، کد قبلی چگونه بازسازی می‌شود. موقعیت موارد دیگر در فهرست تغییر نکرده است و آن‌ها دوباره ترکیب نشده‌اند.
شکل ۴. نمایش MoviesScreen در «ترکیب» وقتی عنصر جدیدی به پایین فهرست اضافه می‌شود. می‌توان از MovieOverview عنصر ترکیبی در «ترکیب» دوباره استفاده کرد. رنگ یکسان در MovieOverview یعنی عنصر ترکیبی دوباره ترکیب نشده است.

بااین‌حال، اگر فهرست movies با افزودن به بالا یا وسط فهرست، برداشتن یا تغییر ترتیب موارد تغییر کند، باعث می‌شود در همه فراخوان‌های MovieOverview که پارامتر ورودی آن‌ها در فهرست تغییر موقعیت داده است، ترکیب مجدد انجام شود. این امر بسیار مهم است اگر، برای مثال، MovieOverview تصویر فیلمی را بااستفاده از اثر جانبی واکشی کند. اگر ترکیب مجدد درحالی‌که جلوه درحال انجام است اتفاق بیفتد، جلوه لغو می‌شود و دوباره شروع می‌شود.

@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 در «ترکیب» وقتی عنصر جدیدی به فهرست اضافه می‌شود. از MovieOverview عنصر ترکیبی نمی‌توان دوباره استفاده کرد و همه جلوه‌های جانبی بازراه‌اندازی خواهند شد. رنگ متفاوت در MovieOverview یعنی ترکیب‌شونده دوباره ترکیب شده است.

در حالت ایده‌آل، می‌خواهیم هویت نمونه MovieOverview را به‌عنوان پیوندخورده به هویت movie که به آن منتقل می‌شود درنظر بگیریم. اگر فهرست فیلم‌ها را تغییر دهیم، در حالت ایده‌آل، نمونه‌های موجود در درخت «ترکیب» را نیز به‌طور مشابه تغییر می‌دهیم، به‌جای اینکه هر MovieOverview قابل‌ساخت را با نمونه فیلم متفاوتی بازسازی کنیم. «ترکیب» روشی برای شما فراهم می‌کند تا به زمان اجرا بگویید از چه مقادیری می‌خواهید برای شناسایی بخش معینی از درخت استفاده کنید: عنصر key ترکیب‌شدنی.

با پیچیدن یک بلوک کد با فراخوانی به عنصر ترکیبی کلید با یک یا چند مقدار منتقل‌شده، آن مقادیر ترکیب می‌شوند تا برای شناسایی آن نمونه در ترکیب استفاده شوند. مقدار key لازم نیست در سطح جهانی یکتا باشد، فقط باید در میان فراخوانی‌های عناصر ترکیبی در سایت تماس یکتا باشد. بنابراین در این مثال، هر movie باید key داشته باشد که در میان movies منحصربه‌فرد باشد؛ اگر این key را با چند عنصر ترکیبی دیگر در جای دیگری از برنامه هم‌رسانی کند مشکلی ندارد.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

با موارد بالا، حتی اگر عناصر فهرست تغییر کند، «نگارش» تماس‌های فردی با MovieOverview را تشخیص می‌دهد و می‌تواند از آن‌ها مجدداً استفاده کند.

نموداری که نشان می‌دهد اگر عنصر جدیدی به بالای فهرست اضافه شود، کد قبلی چگونه بازسازی می‌شود. ازآنجایی‌که عناصر فهرست با کلیدها شناسایی می‌شوند، Compose می‌داند که آن‌ها را دوباره ترکیب نکند، حتی اگر موقعیتشان تغییر کرده باشد.
شکل ۶. نمایش MoviesScreen در «ترکیب» وقتی عنصر جدیدی به فهرست اضافه می‌شود. ازآنجایی‌که عناصر ترکیبی MovieOverview کلیدهای منحصربه‌فردی دارند، ‫Compose تشخیص می‌دهد کدام نمونه‌های MovieOverview تغییر نکرده‌اند و می‌تواند از آن‌ها مجدداً استفاده کند؛ جلوه‌های جانبی آن‌ها همچنان اجرا خواهد شد.

برخی‌از عناصر ترکیبی از عنصر ترکیبی key پشتیبانی داخلی دارند. برای مثال، LazyColumn مشخص کردن key سفارشی را در items DSL می‌پذیرد.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

اگر ورودی‌ها تغییر نکرده باشند، از آن صرف‌نظر می‌شود

درطول ترکیب مجدد، اگر ورودی‌های برخی‌از توابع ترکیبی واجدشرایط نسبت‌به ترکیب قبلی تغییری نکرده باشد، ممکن است اجرای آن‌ها به‌طور کامل رد شود.

یک تابع ترکیب‌شدنی واجدشرایط رد شدن است مگر اینکه:

  • تابع نوع برگشتی غیرUnit دارد
  • این تابع با @NonRestartableComposable یا @NonSkippableComposable حاشیه‌نویسی شده است
  • پارامتر الزامی از نوع غیرپایدار است

حالت مترجمی وجود دارد، پرش قوی، که آخرین الزام را آسان‌تر می‌کند.

برای اینکه نوعی پایدار درنظر گرفته شود، باید از قرارداد زیر پیروی کند:

  • نتیجه equals برای دو نمونه همیشه برای دو نمونه یکسان یکسان خواهد بود.
  • اگر دارایی عمومی از نوع تغییر کند، به «ترکیب» اطلاع داده می‌شود.
  • همه انواع دارایی عمومی نیز پایدار هستند.

چند نوع مشترک مهم وجود دارد که در این قرارداد قرار می‌گیرند و گردآورنده Compose آن‌ها را پایدار درنظر می‌گیرد، حتی اگر به‌طور صریح بااستفاده از حاشیه‌نویسی @Stable به‌عنوان پایدار علامت‌گذاری نشده باشند:

  • همه انواع مقدار اولیه: Boolean، Int، Long، Float، Char، و غیره.
  • رشته‌ها
  • همه انواع تابع (لامبدا)

همه این انواع می‌توانند از قرارداد پایدار پیروی کنند زیرا تغییرناپذیر هستند. ازآنجایی‌که انواع تغییرناپذیر هرگز تغییر نمی‌کنند، هرگز لازم نیست ترکیب تغییر را اعلام کنند، بنابراین دنبال کردن این قرارداد بسیار آسان‌تر است.

یکی از انواع قابل‌توجه که پایدار است اما قابل‌تغییر است نوع MutableState در Compose است. اگر مقداری در MutableState نگهداری شود، وضعیت کلی شیء پایدار درنظر گرفته می‌شود زیرا Compose از هرگونه تغییر در دارایی .value در State مطلع خواهد شد.

وقتی همه انواع منتقل‌شده به‌عنوان پارامتر به یک عنصر ترکیبی پایدار باشند، مقادیر پارامتر براساس موقعیت عنصر ترکیبی در درخت واسط کاربر برای برابری مقایسه می‌شوند. اگر همه مقادیر از زمان تماس قبلی بدون تغییر مانده باشند، از ترکیب مجدد صرف‌نظر می‌شود.

‫Compose نوعی را پایدار درنظر می‌گیرد که بتواند آن را اثبات کند. برای مثال، یک میانای کاربری معمولاً ناپایدار درنظر گرفته می‌شود، و انواع دارای دارایی‌های عمومی تغییرپذیر که پیاده‌سازی آن‌ها می‌تواند تغییرناپذیر باشد نیز ناپایدار هستند.

اگر Compose نتواند استنباط کند که نوعی پایدار است، اما می‌خواهید Compose را مجبور کنید آن را به‌عنوان پایدار درنظر بگیرد، آن را با @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 یک میانای است، Compose می‌تواند معمولاً این نوع را ناپایدار درنظر بگیرد. با افزودن @Stable حاشیه‌نویسی، به «ترکیب‌بندی» می‌گویید که این نوع پایدار است و به «ترکیب‌بندی» اجازه می‌دهد ترکیب‌بندی‌های هوشمند را ترجیح دهد. این همچنین به این معنی است که اگر از رابط به‌عنوان نوع پارامتر استفاده شود، «نوشتن» همه پیاده‌سازی‌های خود را پایدار درنظر می‌گیرد.