تفکر در Compose

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

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

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

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

در طول چند سال گذشته، کل صنعت شروع به تغییر به سمت یک مدل رابط کاربری اعلانی (declarative UI model) کرده است. این مدل، مهندسی مرتبط با ساخت و به‌روزرسانی رابط‌های کاربری را ساده می‌کند. این تکنیک با بازسازی مفهومی کل صفحه از ابتدا و سپس اعمال تنها تغییرات لازم کار می‌کند. این رویکرد از پیچیدگی به‌روزرسانی دستی یک سلسله مراتب نمای حالت‌دار (stateful view hierarchy) جلوگیری می‌کند. Compose یک چارچوب رابط کاربری اعلانی (declarative UI framework) است.

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

یک مثال از تابع قابل ترکیب

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

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

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

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

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

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

تغییر پارادایم اعلانی

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

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

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

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

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

محتوای پویا

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

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

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

ترکیب مجدد

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

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

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

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

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

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

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

  • نوشتن در یک ویژگی از یک شیء مشترک
  • به‌روزرسانی یک observable در ViewModel
  • به‌روزرسانی تنظیمات برگزیده مشترک

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

به عنوان مثال، این کد یک composable برای به‌روزرسانی یک مقدار در SharedPreferences ایجاد می‌کند. composable نباید خودش از shared preferences بخواند یا بنویسد. در عوض، این کد read و write را به یک ViewModel در یک coroutine پس‌زمینه منتقل می‌کند. منطق برنامه مقدار فعلی را با یک callback برای راه‌اندازی به‌روزرسانی ارسال می‌کند.

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

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

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

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

ترکیب مجدد تا حد امکان پرش می‌کند

وقتی بخش‌هایی از رابط کاربری شما نامعتبر هستند، Compose تمام تلاش خود را می‌کند تا فقط بخش‌هایی را که نیاز به به‌روزرسانی دارند، دوباره ترکیب کند. این بدان معناست که ممکن است از اجرای مجدد یک ترکیب 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) }))
}

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

باز هم، همه توابع قابل ترکیب یا لامبداها باید بدون عوارض جانبی باشند. وقتی نیاز به انجام یک عارضه جانبی دارید، آن را از طریق یک فراخوانی مجدد فعال کنید.

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

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

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

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

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

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

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

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

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

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

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

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

وقتی یک تابع composable فراخوانی می‌شود، ممکن است فراخوانی در thread متفاوتی از caller رخ دهد. این بدان معناست که باید از کدی که متغیرها را در یک lambda composable تغییر می‌دهد، اجتناب شود - هم به این دلیل که چنین کدی thread-safe نیست و هم به این دلیل که یک عارضه جانبی غیرمجاز lambda composable است.

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

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

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

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

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

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

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

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

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

بیشتر بدانید

برای کسب اطلاعات بیشتر در مورد نحوه تفکر در Compose و توابع composable، به منابع اضافی زیر مراجعه کنید.

ویدیوها

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