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

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

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

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

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

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

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

بازترکیب معمولاً با تغییر در شیء State<T> راه‌اندازی می‌شود. Compose این موارد را ردیابی می‌کند و همه composable-های موجود در Composition را که آن State<T> خاص را می‌خوانند و هر composable-ای را که آن‌ها فراخوانی می‌کنند و نمی‌توان از آن رد شد اجرا می‌کند.

اگر یک composable چندین بار فراخوانی شود، چندین نمونه در Composition قرار می‌گیرد. هر فراخوانی چرخه حیات خودش را در «ترکیب‌بندی» دارد.

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

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

کالبدشکافی یک ترکیب‌شدنی در «ترکیب‌بندی»

نمونه یک composable در Composition با call site آن شناسایی می‌شود. کامپایلر 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 به true تغییر کند، کد قبلی چگونه بازترکیب می‌شود. ‫LoginError composable اضافه می‌شود، اما composableهای دیگر بازترکیب نمی‌شوند.
شکل ۳. نمایش LoginScreen در «ترکیب‌بندی» وقتی که وضعیت تغییر می‌کند و ترکیب‌بندی مجدد رخ می‌دهد. رنگ یکسان یعنی دوباره ترکیب‌بندی نشده است.

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

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

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

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

در مثال بالا، Compose علاوه‌بر سایت فراخوانی از ترتیب اجرا استفاده می‌کند تا نمونه را در «ترکیب‌بندی» متمایز نگه دارد. اگر movie جدیدی به پایین فهرست اضافه شود، Compose می‌تواند از نمونه‌هایی که قبلاً در Composition وجود دارند مجدداً استفاده کند، زیرا مکان آن‌ها در فهرست تغییر نکرده است و بنابراین، ورودی 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 را با نمونه فیلم متفاوتی بازترکیب کنیم. Compose راهی برای شما فراهم می‌کند تا به زمان اجرا بگویید از چه مقادیری می‌خواهید برای شناسایی بخش خاصی از درخت استفاده کنید: key composable.

با پیچیدن یک بلوک کد با فراخوانی به ترکیب‌پذیر کلید با یک یا چند مقدار ارسال‌شده، آن مقادیر ترکیب می‌شوند تا برای شناسایی آن نمونه در ترکیب‌بندی استفاده شوند. مقدار 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)
            }
        }
    }
}

با موارد بالا، حتی اگر عناصر موجود در فهرست تغییر کنند، Compose فراخوانی‌های تکی به 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 حاشیه‌نویسی شده است
  • پارامتر الزامی از نوع ناپایدار است

یک حالت کامپایلر به‌نام Strong Skipping وجود دارد که شرط آخر را آسان‌تر می‌کند.

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

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

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

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

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

یک نوع قابل‌توجه که پایدار است اما تغییرپذیر است، نوع MutableState در Compose است. If a value is held in a MutableState, the state object overall is considered to be stable as Compose will be notified of any changes to the .value property of State.

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

‫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، به Compose می‌گویید که این نوع پایدار است و به Compose اجازه می‌دهید تا ترکیب‌های مجدد هوشمند را ترجیح دهد. این همچنین به این معنی است که اگر از میانجی به‌عنوان نوع پارامتر استفاده شود، Compose همه پیاده‌سازی‌های آن را به‌عنوان پایدار درنظر می‌گیرد.