رویدادهای رابط کاربری (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

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

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

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

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

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

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

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

class LatestNewsActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLatestNewsBinding
    private val viewModel: LatestNewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */

        // The expand details event is processed by the UI that
        // modifies a View's internal state.
        binding.expandButton.setOnClickListener {
            binding.expandedSection.visibility = View.VISIBLE
        }

        // The refresh event is processed by the ViewModel that is in charge
        // of the business logic.
        binding.refreshButton.setOnClickListener {
            viewModel.refreshNews()
        }
    }
}

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

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

برای مثال، فرض کنید که تمام آیتم‌های خبری از NewsActivity حاوی یک دکمه‌ی بوک‌مارک هستند. ViewModel باید شناسه‌ی آیتم خبری بوک‌مارک‌شده را بداند. وقتی کاربر یک آیتم خبری را بوک‌مارک می‌کند، آداپتور RecyclerView تابع addBookmark(newsId) را از ViewModel فراخوانی نمی‌کند، که این امر نیاز به وابستگی به ViewModel دارد. در عوض، ViewModel یک شیء state به نام NewsItemUiState را در معرض نمایش قرار می‌دهد که شامل پیاده‌سازی برای مدیریت رویداد است:

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    val publicationDate: String,
    val onBookmark: () -> Unit
)

class LatestNewsViewModel(
    private val formatDateUseCase: FormatDateUseCase,
    private val repository: NewsRepository
)
    val newsListUiItems = repository.latestNews.map { news ->
        NewsItemUiState(
            title = news.title,
            body = news.body,
            bookmarked = news.bookmarked,
            publicationDate = formatDateUseCase(news.publicationDate),
            // Business logic is passed as a lambda function that the
            // UI calls on click events.
            onBookmark = {
                repository.addBookmark(news.id)
            }
        )
    }
}

به این ترتیب، آداپتور RecyclerView فقط با داده‌هایی که نیاز دارد کار می‌کند: لیست اشیاء NewsItemUiState . آداپتور به کل ViewModel دسترسی ندارد و این باعث می‌شود که احتمال سوءاستفاده از قابلیت‌های ViewModel کمتر شود. وقتی فقط به کلاس activity اجازه می‌دهید با ViewModel کار کند، مسئولیت‌ها را از هم جدا می‌کنید. این تضمین می‌کند که اشیاء مخصوص رابط کاربری مانند نماها یا آداپتورهای RecyclerView مستقیماً با ViewModel تعامل ندارند.

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

در این راهنما، توابع ViewModel که رویدادهای کاربر را مدیریت می‌کنند، با یک فعل بر اساس عملی که مدیریت می‌کنند، نامگذاری شده‌اند - برای مثال: addBookmark(id) یا logIn(username, password) .

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

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

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

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

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

این رابط کاربری به تغییرات در وضعیت isUserLoggedIn واکنش نشان می‌دهد و در صورت نیاز به مقصد صحیح هدایت می‌شود:

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
    /* ... */
}

class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    if (uiState.isUserLoggedIn) {
                        // Navigate to the Home screen.
                    }
                    ...
                }
            }
        }
    }
}

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

مصرف برخی از رویدادهای 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() {

    private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
    val uiState: StateFlow<LatestNewsUiState> = _uiState

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

            // Do something else.
        }
    }

    fun userMessageShown() {
        _uiState.update { currentUiState ->
            currentUiState.copy(userMessage = null)
        }
    }
}

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

class LatestNewsActivity : AppCompatActivity() {
    private val viewModel: LatestNewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    uiState.userMessage?.let {
                        // TODO: Show Snackbar with userMessage.

                        // Once the message is displayed and
                        // dismissed, notify the ViewModel.
                        viewModel.userMessageShown()
                    }
                    ...
                }
            }
        }
    }
}

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

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

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

class LoginActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLoginBinding
    private val viewModel: LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */

        binding.helpButton.setOnClickListener {
            navController.navigate(...) // Open help screen
        }
    }
}

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

class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    if (uiState.isUserLoggedIn) {
                        // Navigate to the Home screen.
                    }
                    ...
                }
            }
        }
    }
}

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

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

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

// Key that identifies the `validationInProgress` state in the Bundle
private const val DOB_VALIDATION_KEY = "dobValidationKey"

class DobValidationFragment : Fragment() {

    private var validationInProgress: Boolean = false
    private val viewModel: DobValidationViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = // ...
        validationInProgress = savedInstanceState?.getBoolean(DOB_VALIDATION_KEY) ?: false

        binding.continueButton.setOnClickListener {
            viewModel.validateDob()
            validationInProgress = true
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState
                .flowWithLifecycle(viewLifecycleOwner.lifecycle)
                .collect { uiState ->
                    // Update other parts of the UI ...

                    // If the input is valid and the user wants
                    // to navigate, navigate to the next screen
                    // and reset `validationInProgress` flag
                    if (uiState.isDobValid && validationInProgress) {
                        validationInProgress = false
                        navController.navigate(...) // Navigate to next screen
                    }
                }
        }

        return binding
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBoolean(DOB_VALIDATION_KEY, validationInProgress)
    }
}

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

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