درحال فکر کردن در «نوشتن»

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

الگوی برنامه‌نویسی بیانیه

ازنظر تاریخی، سلسله مراتب نمای Android به‌عنوان درختی از ابزارک‌های رابط کاربری قابل‌نمایش بوده است. ازآنجایی‌که وضعیت برنامه به‌دلیل مواردی مثل تعاملات کاربر تغییر می‌کند، سلسله‌مراتب میانای کاربر باید به‌روز شود تا داده‌های فعلی نمایش داده شود. رایج‌ترین روش به‌روزرسانی میانای کاربر، پیمایش درخت بااستفاده از توابعی مثل findViewById() و تغییر گره‌ها با فراخوانی روش‌هایی مثل button.setText(String)، container.addChild(View)، یا img.setImageBitmap(Bitmap) است. این روش‌ها وضعیت داخلی ابزاره را تغییر می‌دهند.

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

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

این تکنیک با بازسازی مفهومی کل صفحه از ابتدا کار می‌کند، سپس فقط تغییرات لازم را اعمال می‌کند. این رویکرد از پیچیدگی به‌روزرسانی دستی سلسله‌مراتب نمای حالت‌دار جلوگیری می‌کند. ‫Compose یک چارچوب واسط کاربر بیانیه است.

یکی از چالش‌های بازتولید کل صفحه این است که ازنظر زمان، قدرت محاسباتی، و مصرف باتری، احتمالاً گران است. برای کاهش این هزینه، Compose به‌طور هوشمند انتخاب می‌کند که کدام بخش‌های رابط کاربری باید در هر زمان معینی دوباره طراحی شوند. این موضوع همان‌طور که در ترکیب مجدد بحث شد، بر نحوه طراحی عناصر میانای کاربر شما تأثیر می‌گذارد.

مثالی از تابع ترکیب‌شدنی

بااستفاده از Compose، می‌توانید میانای کاربر خود را با تعریف مجموعه‌ای از توابع ترکیب‌پذیر بسازید که داده‌ها را دریافت می‌کنند و عناصر میانای کاربر را منتشر می‌کنند. برای مثال، ابزاره Greeting که String را می‌گیرد و ابزاره Text را منتشر می‌کند که پیام خوشامدگویی را نمایش می‌دهد.

تلفنی که نوشتار «سلام دنیا» و کد تابع ترکیب‌شدنی را که آن واسط کاربر را تولید می‌کند نشان می‌دهد.
شکل ۱. تابعی ترکیب‌شدنی که داده‌ها را دریافت می‌کند و از آن‌ها برای پرداخت ابزارک نوشتاری روی صفحه استفاده می‌کند.

چند نکته قابل‌توجه درباره این تابع:

  • گزارمان: تابع با گزارمان @Composable حاشیه‌نویسی شده است. همه توابع ترکیبی باید این شرح را داشته باشند. این گزارمان به ترجمان Compose اطلاع می‌دهد که این تابع برای تبدیل داده به میانای کاربر درنظر گرفته شده است.
  • ورودی داده: تابع داده‌ها را دریافت می‌کند. توابع ترکیبی می‌توانند پارامترهایی را بپذیرند که به منطق برنامه اجازه می‌دهند واسط کاربر را توصیف کند. در این مورد، ابزارک ما String را می‌پذیرد تا بتواند کاربر را با نامش خطاب کند.
  • نمایش واسط کاربر: تابع نوشتار را در واسط کاربر نمایش می‌دهد. این کار با فراخواندن تابع ترکیبی Text() انجام می‌شود که درواقع عنصر واسط کاربر نوشتاری را ایجاد می‌کند. توابع ترکیبی با فراخوانی توابع ترکیبی دیگر، سلسله‌مراتب واسط کاربر را منتشر می‌کنند.
  • بدون مقدار برگشتی: تابع چیزی برنمی‌گرداند. توابع Compose که میانای کاربر منتشر می‌کنند نیازی به برگرداندن چیزی ندارند، زیرا به‌جای ساختن ابزارک‌های میانای کاربر، وضعیت صفحه مقصد را توصیف می‌کنند.
  • خصوصیات: این تابع سریع است، خودتوان است، و اثرات جانبی ندارد.

    • وقتی تابع با آرگومان یکسان چندین بار فراخوانی می‌شود، عملکرد آن یکسان است و از مقادیر دیگر مثل متغیرهای سراسری یا فراخوانی‌های random() استفاده نمی‌کند.
    • این تابع واسط کاربر را بدون هیچ‌گونه اثر جانبی، مانند تغییر دادن ویژگی‌ها یا متغیرهای سراسری، توصیف می‌کند.

    به‌طورکلی، همه توابع ترکیب‌شدنی باید با این ویژگی‌ها نوشته شوند، به دلایلی که در ترکیب مجدد بحث شده است.

تغییر پارادایم بیانیه

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

در رویکرد بیانیه Compose، ابزاره‌ها نسبتاً بدون حالت هستند و توابع تنظیم‌کننده یا دریافت‌کننده را آشکار نمی‌کنند. درواقع، ابزاره‌ها به‌عنوان شیء نمایان نمی‌شوند. با فراخوانی همان تابع ترکیب‌پذیر با آرگومان‌های متفاوت، واسط کاربر را به‌روز می‌کنید. این کار ارائه وضعیت به الگوهای معماری مثل ViewModel را، همان‌طور که در راهنمای معماری برنامه توضیح داده شده است، ساده می‌کند. سپس، عناصر ترکیبی شما مسئول تبدیل وضعیت فعلی برنامه به رابط کاربری در هر بار به‌روزرسانی داده‌های قابل‌مشاهده هستند.

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

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

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

محتوای پویا

ازآنجایی‌که توابع ترکیب‌پذیر به‌جای XML در Kotlin نوشته می‌شوند، می‌توانند به پویایی هر کد Kotlin دیگری باشند. برای مثال، فرض کنید می‌خواهید واسط کاربری‌ای بسازید که به فهرست کاربران خوشامد بگوید:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

این تابع فهرستی از نام‌ها را می‌گیرد و برای هر کاربر یک پیام خوشامدگویی تولید می‌کند. توابع ترکیبی می‌توانند بسیار پیچیده باشند. می‌توانید از دستورات if برای تصمیم‌گیری درباره نمایش یا عدم نمایش عنصر خاصی از میانای کاربر استفاده کنید. می‌توانید از حلقه‌ها استفاده کنید. می‌توانید توابع کمکی را فراخوانی کنید. شما انعطاف‌پذیری کامل زبان زیرین را دارید. این قدرت و انعطاف‌پذیری یکی از مزایای کلیدی Jetpack Compose است.

بازترکیب

در مدل واسط کاربر امری، برای تغییر دادن ابزاره، یک تنظیم‌کننده را در ابزاره فراخوانی می‌کنید تا وضعیت داخلی آن را تغییر دهید. در «نوشتن»، تابع ترکیب‌شدنی را دوباره با داده‌های جدید فراخوانی می‌کنید. انجام این کار باعث می‌شود تابع بازترکیب شود--ابزارک‌های منتشرشده توسط تابع درصورت نیاز با داده‌های جدید دوباره رسم می‌شوند. چارچوب «نوشتن» می‌تواند به‌طور هوشمند فقط مؤلفه‌هایی را که تغییر کرده‌اند بازآرایی کند.

برای مثال، این تابع ترکیب‌شدنی را که دکمه‌ای را نمایش می‌دهد درنظر بگیرید:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

هر بار که روی دکمه کلیک می‌شود، تماس‌گیرنده مقدار clicks را به‌روز می‌کند. «ترکیب» دوباره تابع لامبدا را با تابع Text فرا می‌خواند تا مقدار جدید را نشان دهد؛ این فرایند ترکیب مجدد نامیده می‌شود. توابع دیگری که به مقدار وابسته نیستند دوباره ترکیب نمی‌شوند.

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

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

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

  • نوشتن در دارایی یک شیء مشترک
  • درحال به‌روزرسانی یک مشاهده‌پذیر در ViewModel
  • درحال به‌روزرسانی اولویت‌های هم‌رسانی‌شده

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

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

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

این سند درباره تعدادی از مواردی که باید هنگام استفاده از «نوشتن» از آن‌ها آگاه باشید بحث می‌کند:

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

بخش‌های زیر نحوه ساختن توابع ترکیبی برای پشتیبانی از ترکیب مجدد را پوشش می‌دهد. در هر مورد، بهترین روال این است که توابع ترکیبی خود را سریع، خودتوان، و بدون اثر جانبی نگه دارید.

ترکیب مجدد تا حد امکان از موارد صرف‌نظر می‌کند

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

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

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.bodyLarge)
        HorizontalDivider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

هریک از این محدوده‌ها ممکن است تنها چیزی باشد که درطول یک ترکیب مجدد اجرا می‌شود. وقتی header تغییر می‌کند، Compose ممکن است بدون اجرای هیچ‌یک از والدین آن به Column لامبدا پرش کند. و هنگام اجرای Column، ممکن است Compose تصمیم بگیرد اگر names تغییر نکرده باشد، از موارد LazyColumn صرف‌نظر کند.

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

ترکیب مجدد خوش‌بینانه است

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

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

تأیید کنید که همه توابع ترکیبی و لامبداها برای مدیریت ترکیب مجدد خوش‌بینانه، خودتوان و بدون اثر جانبی هستند.

توابع ترکیبی ممکن است خیلی مکرر اجرا شوند

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

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

اگر یک تابع ترکیب‌پذیر به داده نیاز دارد، پارامترهایی برای آن داده تعریف کنید. سپس می‌توانید کار پرهزینه را به رشته‌ای دیگر، خارج از ترکیب، منتقل کنید و مقدار حاصل را بااستفاده از mutableStateOf یا LiveData به‌عنوان پارامتر به تابع ترکیب‌پذیر ارسال کنید.

توابع ترکیبی می‌توانند به‌صورت موازی اجرا شوند

«ترکیب» می‌تواند با اجرای توابع ترکیبی به‌صورت موازی، ترکیب مجدد را بهینه‌سازی کند. این کار به «نوشتن» امکان می‌دهد از چند هسته استفاده کند و عملکردهای ترکیبی را که در صفحه نیستند با اولویت پایین‌تر اجرا کند.

این بهینه‌سازی به این معنی است که یک تابع ترکیبی ممکن است در یک استخر از رشته‌های پس‌زمینه اجرا شود. اگر یک تابع ترکیب‌پذیر تابعی را در ViewModel فراخوانی کند، ممکن است Compose آن تابع را از چندین رشته به‌طور هم‌زمان فراخوانی کند.

برای اینکه مطمئن شوید برنامه شما عملکرد درستی دارد، همه توابع ترکیب‌شدنی باید بدون اثر جانبی باشند. درعوض، عوارض جانبی را از پاسخ‌های تماسی مثل onClick که همیشه در رشته UI اجرا می‌شوند راه‌اندازی کنید.

وقتی یک تابع ترکیب‌شدنی فراخوانی می‌شود، فراخوانی ممکن است در یک رشته متفاوت از فراخواننده رخ دهد. این یعنی باید از کدی که متغیرها را در لامبدای ترکیب‌پذیر تغییر می‌دهد اجتناب شود–هم به این دلیل که چنین کدی ایمن برای رشته نیست، و هم به این دلیل که این کد اثر جانبی غیرمجاز لامبدای ترکیب‌پذیر است.

در اینجا مثالی از یک عنصر ترکیبی نشان داده شده است که فهرستی و تعداد آن را نمایش می‌دهد:

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

این کد بدون اثر جانبی است و فهرست ورودی را به واسط کاربر تبدیل می‌کند. این کد برای نمایش فهرست کوچک عالی است. بااین‌حال، اگر تابع در متغیر محلی بنویسد، این کد ایمن یا صحیح نخواهد بود:

@Composable
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Card {
                    Text("Item: $item")
                    items++ // Avoid! Side-effect of the column recomposing.
                }
            }
        }
        Text("Count: $items")
    }
}

در این مثال، items با هر ترکیب مجدد اصلاح می‌شود. این می‌تواند هر قاب پویانمایی یا زمانی باشد که فهرست به‌روز می‌شود. درهرصورت، رابط کاربری تعداد اشتباه را نمایش خواهد داد. به‌همین دلیل، نوشتن‌هایی ازاین‌دست در «نوشتن» پشتیبانی نمی‌شود؛ با ممنوع کردن این نوشتن‌ها، به چارچوب اجازه می‌دهیم تا رشته‌ها را برای اجرای لامبداهای ترکیب‌پذیر تغییر دهد.

توابع ترکیبی می‌توانند به هر ترتیبی اجرا شوند

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

برای مثال، فرض کنید کد شما برای رسم سه صفحه در چیدمان برگه به این صورت است:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

تماس با StartScreen، MiddleScreen، و EndScreen ممکن است به هر ترتیبی برقرار شود. این یعنی نمی‌توانید، برای مثال، StartScreen() متغیری جهانی (اثر جانبی) را تنظیم کند و MiddleScreen() از آن تغییر بهره ببرد. درعوض، هریک از این توابع باید خوداتکا باشد.

بیشتر بدانید

برای کسب اطلاعات بیشتر درباره نحوه فکر کردن در Compose و توابع ترکیب‌پذیر، منابع تکمیلی زیر را ببینید.

ویدیوها