تفکر در Compose

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

پارادایم برنامه نویسی اعلانی

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

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

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

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

یک تابع قابل ترکیب ساده

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

تصویری از یک گوشی که متن "Hello World" و کد آن را نشان می دهد تابع Composable ساده که آن را تولید می کند UI

شکل 1. یک تابع ساده قابل ترکیب که داده ها را ارسال می کند و از آن برای ارائه یک ویجت متنی روی صفحه استفاده می کند.

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

  • تابع با حاشیه نویسی @Composable حاشیه نویسی شده است. همه توابع Composable باید این حاشیه نویسی را داشته باشند. این حاشیه نویسی به کامپایلر Compose اطلاع می دهد که این تابع برای تبدیل داده ها به UI در نظر گرفته شده است.

  • تابع داده ها را می گیرد. توابع قابل ترکیب می توانند پارامترهایی را بپذیرند که به منطق برنامه اجازه می دهد تا رابط کاربری را توصیف کند. در این حالت، ویجت ما یک String می‌پذیرد تا بتواند با نام کاربر را احوالپرسی کند.

  • این تابع متن را در رابط کاربری نمایش می دهد. این کار را با فراخوانی تابع composable Text() انجام می دهد که در واقع عنصر UI متن را ایجاد می کند. توابع Composable سلسله مراتب UI را با فراخوانی سایر توابع قابل ترکیب منتشر می کنند.

  • تابع چیزی را بر نمی گرداند. توابعی که UI منتشر می کنند نیازی به برگرداندن چیزی ندارند، زیرا آنها به جای ساختن ویجت های UI وضعیت صفحه مورد نظر را توصیف می کنند.

  • این عملکرد سریع، ناتوان و بدون عوارض جانبی است.

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

    به طور کلی، به دلایلی که در Recomposition بحث شد، همه توابع قابل ترکیب باید با این خصوصیات نوشته شوند.

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

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

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

تصویری از جریان داده در یک UI Compose، از اشیاء سطح بالا به پایین به آنها کودکان

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

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

تصویری از چگونگی واکنش عناصر UI به تعامل، با ایجاد رویدادها که توسط برنامه مدیریت می شوند منطق

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

محتوای پویا

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

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

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

ترکیب مجدد

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

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

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

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

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

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

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

  • نوشتن به یک ویژگی یک شی مشترک
  • به روز رسانی یک قابل مشاهده در ViewModel
  • به روز رسانی تنظیمات برگزیده مشترک

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

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

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

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

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

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

ترکیب مجدد تا آنجا که ممکن است حذف می شود

وقتی بخش‌هایی از UI شما نامعتبر است، Compose تمام تلاش خود را می‌کند تا فقط بخش‌هایی را که باید به‌روزرسانی شوند، دوباره ترکیب کند. این بدان معنی است که ممکن است از اجرای مجدد یک Button's composable بدون اجرای هیچ یک از composable های بالا یا پایین آن در درخت UI صرفنظر کند.

هر تابع قابل ترکیب و لامبدا ممکن است به خودی خود دوباره ترکیب شود. در اینجا یک مثال آورده شده است که نشان می دهد چگونه ترکیب مجدد می تواند برخی از عناصر را هنگام ارائه یک لیست نادیده بگیرد:

/**
 * 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 ممکن است نوشتن بدون اجرای هیچ یک از والدین آن به Column لامبدا رد شود. و هنگام اجرای Column ، Compose ممکن است انتخاب کند که در صورت عدم تغییر names ، از موارد LazyColumn رد شود.

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

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

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

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

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

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

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

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

اگر تابع composable شما به داده نیاز دارد، باید پارامترهایی را برای داده ها تعریف کند. سپس می توانید کارهای گران قیمت را به رشته دیگری خارج از ترکیب بندی منتقل کنید و داده ها را با استفاده از mutableStateOf یا LiveData به Compose ارسال کنید.

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

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

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

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

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

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

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

این کد بدون عوارض جانبی است و لیست ورودی را به UI تبدیل می کند. این کد عالی برای نمایش یک لیست کوچک است. با این حال، اگر تابع روی یک متغیر محلی بنویسد، این کد از نظر موضوعی امن یا صحیح نخواهد بود:

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

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

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

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

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

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

بیشتر بدانید

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

ویدیوها

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}