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

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

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

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

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

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

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

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

نمونه تابع ترکیب‌پذیر

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

تلفنی که نوشتار Hello World و کد تابع ترکیب‌پذیر را که آن واسط کاربر را تولید می‌کند نشان می‌دهد.
شکل ۱. تابع ترکیب‌پذیری که داده‌ها به آن ارسال می‌شود و از آن برای ارائه ابزارک نوشتاری در صفحه استفاده می‌کند.

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

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

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

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

تغییر الگوی اعلانی

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

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

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

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

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

محتوای پویا

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

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

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

بازترکیب

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

بازترکیب خوش‌بینانه است

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

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

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

توابع ترکیب‌پذیر ممکن است به‌طور مکرر اجرا شوند

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

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

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

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

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

این بهینه‌سازی به این معنی است که یک تابع ترکیب‌پذیر ممکن است در یک استخر از رشته‌های پس‌زمینه اجرا شود. اگر تابع ترکیب‌پذیر تابعی را در 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}")
    }
}

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

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

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

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

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

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

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

بیشتر بدانید

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

ویدیوها