رویدادهای رابط کاربری

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

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

در حالی که منطق کسب و کار برای یک برنامه مشابه در پلتفرم‌های مختلف تلفن همراه یا فرم‌فاکتورها یکسان است، منطق رفتار رابط کاربری (UI) جزئیات پیاده‌سازی است که ممکن است بین این موارد متفاوت باشد. صفحه لایه رابط کاربری این نوع منطق را به شرح زیر تعریف می‌کند:

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

درخت تصمیم رویداد رابط کاربری

نمودار زیر یک درخت تصمیم را برای یافتن بهترین رویکرد برای مدیریت یک مورد استفاده خاص از رویداد نشان می‌دهد. ادامه این راهنما این رویکردها را به تفصیل توضیح می‌دهد.

اگر رویداد از ViewModel سرچشمه گرفته باشد، وضعیت رابط کاربری را به‌روزرسانی کنید. اگر رویداد از UI سرچشمه گرفته باشد و به منطق کسب‌وکار نیاز داشته باشد، منطق کسب‌وکار را به ViewModel واگذار کنید. اگر رویداد از UI سرچشمه گرفته باشد و به منطق رفتار رابط کاربری نیاز داشته باشد، وضعیت عنصر رابط کاربری را مستقیماً در UI تغییر دهید.
شکل ۱. درخت تصمیم برای مدیریت رویدادها.

مدیریت رویدادهای کاربر

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

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

@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {

    // State of whether more details should be shown
    var expanded by remember { mutableStateOf(false) }

    Column {
        Text("Some text")
        if (expanded) {
            Text("More details")
        }

        Button(
        // The expand details event is processed by the UI that
        // modifies this composable's internal state.
        onClick = { expanded = !expanded }
        ) {
        val expandText = if (expanded) "Collapse" else "Expand"
        Text("$expandText details")
        }

        // The refresh event is processed by the ViewModel that is in charge
        // of the UI's business logic.
        Button(onClick = { viewModel.refreshNews() }) {
            Text("Refresh data")
        }
    }
}

رویدادهای کاربر در لیست‌های تنبل

اگر این اکشن در مراحل پایین‌تر درخت رابط کاربری تولید شود، مانند یک آیتم LazyColumn ، ViewModel همچنان باید مسئول مدیریت رویدادهای کاربر باشد.

برای مثال، لیستی از آیتم‌های قابل کلیک را در نظر بگیرید. نمونه ViewModel را به لیست composable ( MyList ) ارسال نکنید، زیرا این کار کامپوننت UI را به جزئیات پیاده‌سازی متصل می‌کند.

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

data class MyItem(val id: Int)

@Composable
fun MyList(
    items: List<String>,
    onItemClick: (MyItem) -> Unit
) {
    Card {
        LazyColumn {
            itemsIndexed(items) { index, string ->
                ListItem(
                    modifier = Modifier.clickable {
                        onItemClick(MyItem(index))
                    },
                    headlineContent = {
                        Text(text = string)
                    }
                )
            }
        }
    }
}

در این رویکرد، MyList composable فقط با داده‌هایی که نمایش می‌دهد و رویدادهایی که ارائه می‌دهد کار می‌کند. به ViewModel دسترسی ندارد. رویداد در یک composable قبلی برداشته شده و به ViewModel ارسال می‌شود.

برای اطلاعات بیشتر در مورد مدیریت رویدادها، به رویدادها در Compose مراجعه کنید.

قراردادهای نامگذاری برای توابع رویداد کاربر و کنترل‌کننده‌های رویداد

در این راهنما، توابع ViewModel که رویدادهای کاربر را مدیریت می‌کنند، با یک فعل بر اساس عملی که مدیریت می‌کنند، نامگذاری شده‌اند - برای مثال: validateInput() یا login() .

کنترل‌کننده‌های رویداد در Compose از یک قرارداد نامگذاری استاندارد پیروی می‌کنند تا جریان داده‌ها را واضح نشان دهند:

  • نام پارامتر: on + Verb + Target (برای مثال، onExpandClicked یا onValueChange ).
  • عبارت لامبدا: هنگام فراخوانی composable، لامبدا اغلب فقط پیاده‌سازی آن رویداد است.

مدیریت رویدادهای ViewModel

اقدامات رابط کاربری که از ViewModel - رویدادهای ViewModel - سرچشمه می‌گیرند، همیشه باید منجر به به‌روزرسانی وضعیت رابط کاربری شوند. این امر با اصول جریان داده یک‌طرفه (Unidirectional Data Flow ) مطابقت دارد. این امر باعث می‌شود رویدادها پس از تغییرات پیکربندی قابل تکرار باشند و تضمین می‌کند که اقدامات رابط کاربری از بین نمی‌روند. به صورت اختیاری، در صورت استفاده از ماژول saved state ، می‌توانید رویدادها را پس از مرگ فرآیند نیز قابل تکرار کنید.

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

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

data class LoginUiState(
    val isLoginInProgress: Boolean = false,
    val errorMessage: String? = null,
    val isUserLoggedIn: Boolean = false
)

صفحه ورود به سیستم به تغییرات در وضعیت رابط کاربری واکنش نشان می‌دهد.

class LoginViewModel : ViewModel() {

    var uiState by mutableStateOf(LoginUiState())

    fun tryLogin(username: String, password: String) {
        viewModelScope.launch {
            // Emit a new state indicating that login is in progress
            uiState = uiState.copy(isLoginInProgress = true)

            uiState = if (login(username, password)) {
                // Emit a new state indicating that login was successful
                uiState.copy(isLoginInProgress = false, isUserLoggedIn = true)
            } else {
                // Emit a new state with the error message
                LoginUiState(isLoginInProgress = false, errorMessage = "Login failed")
            }
        }
    }

    private suspend fun login(username: String, password: String): Boolean {
        delay(1000)
        return (username == "Hello" && password == "World!")
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel, onSuccessfulLogin: () -> Unit) {

    val uiState = viewModel.uiState

    LaunchedEffect(uiState) {
        if (uiState.isUserLoggedIn) {
            onSuccessfulLogin()
        }
    }

    if (uiState.isLoginInProgress) {
        CircularProgressIndicator()
    } else {
        LoginForm(
            onLoginAttempt = { username, password ->
                viewModel.tryLogin(username, password)
            },
            errorMessage = uiState.errorMessage
        )
    }
}

رویدادهای مصرفی می‌توانند باعث به‌روزرسانی وضعیت شوند

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

// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
    val news: List<News> = emptyList(),
    val isLoading: Boolean = false,
    val userMessage: String? = null
)

وقتی منطق کسب‌وکار نیاز به نمایش یک پیام گذرای جدید به کاربر داشته باشد، ViewModel وضعیت رابط کاربری را به صورت زیر به‌روزرسانی می‌کند:

class LatestNewsViewModel(/* ... */) : ViewModel() {

    var uiState by mutableStateOf(LatestNewsUiState())
        private set

    fun refreshNews() {
        viewModelScope.launch {
            // If there isn't internet connection, show a new message on the screen.
            if (!internetConnection()) {
                uiState = uiState.copy(userMessage = "No Internet connection")
                return@launch
            }

            // Do something else.
        }
    }

    fun userMessageShown() {
        uiState = uiState.copy(userMessage = null)
    }
}

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

@Composable
fun LatestNewsScreen(
    snackbarHostState: SnackbarHostState,
    viewModel: LatestNewsViewModel = viewModel(),
) {
    // Rest of the UI content.

    // If there are user messages to show on the screen,
    // show it and notify the ViewModel.
    viewModel.uiState.userMessage?.let { userMessage ->
        LaunchedEffect(userMessage) {
            snackbarHostState.showSnackbar(userMessage)
            // Once the message is displayed and dismissed, notify the ViewModel.
            viewModel.userMessageShown()
        }
    }
}

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

رویدادهای Consuming می‌توانند به‌روزرسانی‌های وضعیت را فعال کنند و جزئیات نحوه استفاده از وضعیت رابط کاربری برای نمایش پیام‌های کاربر روی صفحه را شرح دهند. رویدادهای ناوبری نیز نوع رایجی از رویدادها در یک برنامه اندروید هستند.

اگر رویداد در رابط کاربری به دلیل لمس یک دکمه توسط کاربر فعال شود، رابط کاربری با نمایش رویداد به فراخواننده‌ی composable، این مشکل را برطرف می‌کند.

@Composable
fun LoginScreen(
    onHelp: () -> Unit, // Caller navigates to the help screen
    viewModel: LoginViewModel = viewModel()
) {
    // Rest of the UI
    Button(
        onClick = dropUnlessResumed { onHelp() }
    ) {
        Text("Get help")
    }
}

dropUnlessResumed بخشی از کتابخانه Lifecycle است و به شما امکان می‌دهد تابع onHelp را فقط زمانی اجرا کنید که چرخه حیات حداقل RESUMED باشد.

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

@Composable
fun LoginScreen(
    onUserLogIn: () -> Unit, // Caller navigates to the right screen
    viewModel: LoginViewModel = viewModel()
) {
    Button(
        onClick = {
            // ViewModel validation is triggered
            viewModel.tryLogin()
        }
    ) {
        Text("Log in")
    }
    // Rest of the UI

    val lifecycle = LocalLifecycleOwner.current.lifecycle
    val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
    LaunchedEffect(viewModel, lifecycle)  {
        // Whenever the uiState changes, check if the user is logged in and
        // call the `onUserLogin` event when `lifecycle` is at least STARTED
        snapshotFlow { viewModel.uiState }
            .filter { it.isUserLoggedIn }
            .flowWithLifecycle(lifecycle)
            .collect {
                currentOnUserLogIn()
            }
    }
}

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

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

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

class DobValidationViewModel(/* ... */) : ViewModel() {
    var uiState by mutableStateOf(DobValidationUiState())
        private set
}

@Composable
fun DobValidationScreen(
    onNavigateToNextScreen: () -> Unit, // Caller navigates to the right screen
    viewModel: DobValidationViewModel = viewModel()
) {
    // TextField that updates the ViewModel when a date of birth is selected

    var validationInProgress by rememberSaveable { mutableStateOf(false) }

    Button(
        onClick = {
            viewModel.validateInput()
            validationInProgress = true
        }
    ) {
        Text("Continue")
    }
    // Rest of the UI

    /*
        * The following code implements the requirement of advancing automatically
        * to the next screen when a valid date of birth has been introduced
        * and the user wanted to continue with the registration process.
        */

    if (validationInProgress) {
        val lifecycle = LocalLifecycleOwner.current.lifecycle
        val currentNavigateToNextScreen by rememberUpdatedState(onNavigateToNextScreen)
        LaunchedEffect(viewModel, lifecycle) {
            // If the date of birth is valid and the validation is in progress,
            // navigate to the next screen when `lifecycle` is at least STARTED,
            // which is the default Lifecycle.State for the `flowWithLifecycle` operator.
            snapshotFlow { viewModel.uiState }
                .filter { it.isDobValid }
                .flowWithLifecycle(lifecycle)
                .collect {
                    validationInProgress = false
                    currentNavigateToNextScreen()
                }
        }
    }
}

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

موارد استفاده دیگر

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

  • هر کلاس باید کاری را که مسئولیت آن را بر عهده دارد انجام دهد، نه بیشتر. رابط کاربری (UI) مسئول منطق رفتار خاص صفحه نمایش مانند فراخوانی‌های ناوبری، رویدادهای کلیک و دریافت درخواست‌های مجوز است. ViewModel شامل منطق کسب و کار است و نتایج لایه‌های پایین‌تر سلسله مراتب را به حالت رابط کاربری تبدیل می‌کند.
  • به این فکر کنید که رویداد از کجا سرچشمه می‌گیرد. درخت تصمیم ارائه شده در ابتدای این راهنما را دنبال کنید و هر کلاس را وادار کنید تا مسئولیت خود را بر عهده بگیرد. برای مثال، اگر رویداد از رابط کاربری (UI) سرچشمه می‌گیرد و منجر به یک رویداد ناوبری می‌شود، آن رویداد باید در رابط کاربری (UI) مدیریت شود. ممکن است برخی از منطق‌ها به ViewModel واگذار شوند، اما مدیریت رویداد را نمی‌توان به طور کامل به ViewModel واگذار کرد.
  • اگر چندین مصرف‌کننده دارید و نگران این هستید که یک رویداد چندین بار مصرف شود، ممکن است لازم باشد معماری برنامه خود را مجدداً بررسی کنید. داشتن چندین مصرف‌کننده همزمان باعث می‌شود که تضمین تحویل دقیقاً یک بار قرارداد بسیار دشوار شود، بنابراین میزان پیچیدگی و رفتار ظریف به شدت افزایش می‌یابد. اگر با این مشکل مواجه هستید، در نظر بگیرید که این نگرانی‌ها را در درخت رابط کاربری خود به سمت بالا هدایت کنید. ممکن است به یک موجودیت متفاوت در سلسله مراتب بالاتر نیاز داشته باشید.
  • به این فکر کنید که چه زمانی باید از state استفاده شود. در شرایط خاص، ممکن است نخواهید وقتی برنامه در پس‌زمینه است، به مصرف state ادامه دهید - مثلاً هنگام نمایش یک Toast . در این موارد، مصرف state را زمانی که رابط کاربری در پیش‌زمینه است در نظر بگیرید.

نمونه‌ها

نمونه‌های گوگل زیر، رویدادهای رابط کاربری را در لایه رابط کاربری نشان می‌دهند. برای مشاهده این راهنمایی در عمل، به آنها مراجعه کنید:

منابع اضافی

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

کدلبز

مستندات

محتوا را مشاهده می‌کند

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