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

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

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

چون composableها حالت را می‌پذیرند و رویدادها را نمایش می‌دهند، الگوی جریان داده یک‌سویه به‌خوبی با Jetpack Compose سازگار است. این راهنما بر نحوه پیاده‌سازی الگوی جریان داده یک‌سویه در Compose، نحوه پیاده‌سازی رویدادها و نگه‌دارنده‌های حالت، و نحوه کار با ViewModels در Compose تمرکز دارد.

جریان داده یک‌سویه

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

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

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

دنبال کردن این الگو هنگام استفاده از Jetpack Compose چندین مزیت دارد:

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

جریان داده یک‌سویه در Jetpack Compose

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

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

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

تعریف پارامترهای قابل‌ترکیب

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

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

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

@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
                )
            }
        },
        // ...
    )
}

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

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

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

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

صفحه چهار حالت دارد:

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

می‌توانید این وضعیت‌ها را به‌عنوان کلاس مهروموم‌شده مدل‌سازی کنید. 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، به منابع زیر مراجعه کنید:

نمونه‌ها