چرخه حیات مواد ترکیب پذیر

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

مرور کلی چرخه عمر

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

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

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

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

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

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

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

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

آناتومی یک ترکیب‌پذیر در کامپوزیشن

نمونه یک composable در Composition توسط محل فراخوانی آن شناسایی می‌شود. کامپایلر Compose هر محل فراخوانی را مجزا در نظر می‌گیرد. فراخوانی composableها از چندین محل فراخوانی، چندین نمونه از composable را در Composition ایجاد می‌کند.

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

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

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

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

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

با قرار دادن یک بلوک کد با فراخوانی کلید composable به همراه یک یا چند مقدار ارسالی، آن مقادیر با هم ترکیب می‌شوند تا برای شناسایی آن نمونه در ترکیب استفاده شوند. مقدار یک key نیازی نیست که به صورت سراسری منحصر به فرد باشد، بلکه فقط باید در بین فراخوانی‌های composableها در محل فراخوانی منحصر به فرد باشد. بنابراین در این مثال، هر movie باید key داشته باشد که در بین movies منحصر به فرد باشد. اگر آن 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 را تشخیص می‌دهد و می‌تواند از آنها دوباره استفاده کند.

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

برخی از composableها از key composable به صورت داخلی پشتیبانی می‌کنند. برای مثال، 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 برای دو نمونه، برای همیشه برای همان دو نمونه یکسان خواهد بود.
  • اگر یک ویژگی عمومی از این نوع تغییر کند، به Composition اطلاع داده می‌شود.
  • همه انواع اموال عمومی نیز پایدار هستند.

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

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

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

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

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

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 با تمام پیاده‌سازی‌های خود به عنوان پایدار رفتار خواهد کرد.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}