معماری واسط کاربر Compose

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

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

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

جریان داده یک‌طرفه

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

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

  1. رویداد: بخشی از «میانای کاربر» رویدادی تولید می‌کند و آن را به بالا ارسال می‌کند، مثلاً کلیک دکمه‌ای که به «نمای مدل» ارسال می‌شود تا مدیریت شود؛ یا رویدادی از لایه‌های دیگر برنامه شما ارسال می‌شود، مثلاً نشان می‌دهد که جلسه کاربر منقضی شده است.
  2. به‌روزرسانی وضعیت: مدیریت‌کننده رویداد ممکن است وضعیت را تغییر دهد.
  3. وضعیت نمایشگر: نگه‌دارنده وضعیت، وضعیت را به پایین منتقل می‌کند و واسط کاربر آن را نمایش می‌دهد.
جریان رویدادها از واسط کاربر به نگهدارنده حالت و جریان حالت از نگهدارنده حالت به واسط کاربر است.
شکل ۱. جریان داده یک‌طرفه.

پیروی از این الگو هنگام استفاده از Jetpack Compose مزایای متعددی دارد:

  • آزمون‌پذیری: جدا کردن وضعیت از میانای کاربری که آن را نمایش می‌دهد، آزمون هر دو را به‌صورت مجزا آسان‌تر می‌کند.
  • کپسوله کردن وضعیت: ازآنجایی‌که وضعیت فقط در یک مکان می‌تواند به‌روز شود و فقط یک منبع حقیقت برای وضعیت عنصر ترکیبی وجود دارد، احتمال اینکه به‌دلیل وضعیت‌های ناسازگار اشکال ایجاد کنید کمتر است.
  • ثبات میانای کاربر: همه به‌روزرسانی‌های وضعیت بااستفاده از نگهدارنده‌های وضعیت قابل‌مشاهده، مانند StateFlow یا LiveData، بلافاصله در میانای کاربر منعکس می‌شوند.

جریان داده یک‌طرفه در Jetpack Compose

عناصر ترکیبی براساس وضعیت و رویدادها کار می‌کنند. برای مثال، TextField فقط زمانی به‌روز می‌شود که پارامتر value آن به‌روز شود و یک onValueChange بازخوان—رویدادی که درخواست می‌کند مقدار به مقدار جدیدی تغییر کند—نمایان کند. ‫Compose شیء State را به‌عنوان نگهدارنده مقدار تعریف می‌کند، و تغییرات در مقدار وضعیت باعث بازآفرینی می‌شود. بسته به اینکه چه مدت باید مقدار را به‌خاطر بسپارید، می‌توانید وضعیت را در remember { mutableStateOf(value) } یا rememberSaveable { mutableStateOf(value) } نگه دارید.

نوع مقدار TextField قابل ترکیب String است، بنابراین این مقدار می‌تواند از هر جایی بیاید—از مقدار کدبندی‌شده، از «نمای مدل»، یا از قابل ترکیب والد منتقل شود. لازم نیست آن را در یک شیء State نگه دارید، اما باید مقدار را هنگام فراخوانی onValueChange به‌روز کنید.

  • ‫mutableStateOf(value) یک MutableState ایجاد می‌کند که نوعی قابل‌مشاهده در Compose است. هرگونه تغییر در برنامه زمانی مقدار آن باعث ترکیب مجدد هریک از توابع ترکیبی می‌شود که آن مقدار را می‌خوانند.
  • ‫remember اشیا را در ترکیب ذخیره می‌کند و وقتی عنصر ترکیبی که remember را فراخوانده است از ترکیب برداشته می‌شود، شیء را فراموش می‌کند.
  • rememberSaveable با ذخیره کردن وضعیت در Bundle، وضعیت را درطول تغییرات پیکربندی حفظ می‌کند.

تعریف پارامترهای ترکیب‌شدنی

هنگام تعریف پارامترهای وضعیت یک عنصر ترکیبی، سؤالات زیر را در ذهن داشته باشید:

  • آیا عنصر ترکیبی قابل‌استفاده مجدد یا انعطاف‌پذیر است؟
  • پارامترهای حالت چگونه بر عملکرد این عنصر ترکیبی تأثیر می‌گذارند؟

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

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

گاهی اوقات، استفاده از پارامترهای فردی نیز عملکرد را بهبود می‌بخشد—برای مثال، اگر News حاوی اطلاعات بیشتری نسبت‌به title و subtitle باشد، هرگاه نمونه جدیدی از News به Header(news) ارسال شود، ترکیب‌شونده دوباره ترکیب خواهد شد، حتی اگر title و subtitle تغییر نکرده باشند.

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

رویدادها در Compose

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

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

مقادیر تغییرناپذیر را برای وضعیت و لامبداهای مدیریت‌کننده رویداد ترجیح دهید. این رویکرد مزایای زیر را دارد:

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

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

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

‫ViewModels، وضعیت‌ها، و رویدادها: مثال

بااستفاده از ViewModel و mutableStateOf، اگر یکی از موارد زیر درست باشد، می‌توانید جریان داده یک‌طرفه را در برنامه‌تان معرفی کنید:

  • وضعیت واسط کاربر شما بااستفاده از نگهدارنده‌های وضعیت قابل‌مشاهده، مثل StateFlow یا LiveData، آشکار می‌شود.
  • ViewModel رویدادهای دریافتی از رابط کاربری یا لایه‌های دیگر برنامه را مدیریت می‌کند و دارنده وضعیت را براساس رویدادها به‌روز می‌کند.

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

صفحه‌نمایش چهار حالت دارد:

  • از سیستم خارج شده است: وقتی کاربر هنوز به سیستم وارد نشده است.
  • درحال انجام: وقتی برنامه شما درحال تلاش برای ورود کاربر به سیستم ازطریق انجام تماس شبکه است.
  • خطا: زمانی که هنگام ورود به سیستم خطایی رخ دهد.
  • وارد سیستم شده: زمانی که کاربر وارد سیستم شده است.

می‌توانید این وضعیت‌ها را به‌عنوان کلاس مهروموم‌شده مدل‌سازی کنید. ‫ViewModel وضعیت را به‌عنوان State آشکار می‌کند، وضعیت اولیه را تنظیم می‌کند، و وضعیت را درصورت نیاز به‌روزرسانی می‌کند. ViewModel همچنین رویداد ورود به سیستم را با آشکار کردن روش onSignIn() مدیریت می‌کند.

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

علاوه‌بر mutableStateOf API،‏ Compose افزونه‌هایی برای LiveData،‏ Flow، و Observable ارائه می‌دهد تا به‌عنوان شنونده ثبت شود و مقدار را به‌عنوان وضعیت نشان دهد.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

بیشتر بدانید

برای کسب اطلاعات بیشتر درباره معماری در Jetpack Compose، به منابع زیر مراجعه کنید:

نمونه‌ها