رویدادهای UI اقداماتی هستند که باید در لایه UI، یا توسط UI یا ViewModel انجام شوند. رایج ترین نوع رویدادها رویدادهای کاربر هستند. کاربر با تعامل با برنامه، رویدادهای کاربر را تولید می کند - به عنوان مثال، با ضربه زدن روی صفحه یا با ایجاد حرکات. سپس UI این رویدادها را با استفاده از callbackهایی مانند شنوندگان onClick()
مصرف می کند.
ViewModel معمولاً مسئول مدیریت منطق تجاری یک رویداد کاربر خاص است - به عنوان مثال، کاربر روی یک دکمه کلیک می کند تا برخی از داده ها را تازه کند. معمولا، ViewModel با افشای توابعی که UI می تواند آنها را فراخوانی کند، این کار را انجام می دهد. رویدادهای کاربر همچنین ممکن است منطق رفتار رابط کاربری داشته باشند که رابط کاربری میتواند مستقیماً آن را مدیریت کند - به عنوان مثال، پیمایش به صفحهای دیگر یا نمایش Snackbar
.
در حالی که منطق کسبوکار برای یک برنامه مشابه در پلتفرمهای تلفن همراه یا عوامل شکل مختلف یکسان باقی میماند، منطق رفتار رابط کاربری یک جزئیات پیادهسازی است که ممکن است بین آن موارد متفاوت باشد. صفحه لایه UI این نوع منطق را به صورت زیر تعریف می کند:
- منطق کسب و کار به این اشاره دارد که با تغییرات وضعیت چه باید کرد - به عنوان مثال، پرداخت یا ذخیره تنظیمات برگزیده کاربر. دامنه و لایه های داده معمولاً این منطق را مدیریت می کنند. در سراسر این راهنما، کلاس Architecture Components ViewModel بهعنوان راهحلی برای کلاسهایی که منطق تجاری را مدیریت میکنند، استفاده میشود.
- منطق رفتار رابط کاربری یا منطق UI به نحوه نمایش تغییرات وضعیت اشاره دارد - به عنوان مثال، منطق ناوبری یا نحوه نمایش پیام ها به کاربر. رابط کاربری این منطق را مدیریت می کند.
درخت تصمیم رویداد UI
نمودار زیر یک درخت تصمیم را برای یافتن بهترین رویکرد برای رسیدگی به یک مورد استفاده رویداد خاص نشان می دهد. بقیه این راهنما این رویکردها را به تفصیل توضیح می دهد.
رویدادهای کاربر را مدیریت کنید
اگر این رویدادها مربوط به تغییر وضعیت یک عنصر UI باشد - به عنوان مثال، وضعیت یک آیتم قابل ارتقا، UI می تواند مستقیماً رویدادهای کاربر را مدیریت کند. اگر رویداد نیاز به انجام منطق تجاری، مانند تازه کردن داده های روی صفحه دارد، باید توسط ViewModel پردازش شود.
مثال زیر نشان می دهد که چگونه از دکمه های مختلف برای گسترش یک عنصر UI (منطق UI) و برای تازه کردن داده های روی صفحه (منطق تجاری) استفاده می شود:
بازدیدها
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()
}
}
}
نوشتن
@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")
}
}
}
رویدادهای کاربر در RecyclerViews
اگر این کنش در پایینتر درخت رابط کاربری ایجاد شود، مانند یک آیتم RecyclerView
یا یک View
سفارشی، ViewModel
همچنان باید رویدادهای کاربر را مدیریت کند.
به عنوان مثال، فرض کنید که همه اخبار از NewsActivity
دارای یک دکمه نشانک هستند. ViewModel
باید شناسه آیتم خبری نشانه گذاری شده را بداند. وقتی کاربر یک مورد خبری را نشانک میکند، آداپتور RecyclerView
تابع addBookmark(newsId)
در معرض نمایش را از ViewModel
فراخوانی نمیکند، که به وابستگی به ViewModel
نیاز دارد. در عوض، ViewModel
یک شیء حالت به نام 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 کار کند، مسئولیت ها را از هم جدا می کنید. این تضمین می کند که اشیاء خاص UI مانند view ها یا آداپتورهای RecyclerView
مستقیماً با ViewModel تعامل ندارند.
قراردادهای نامگذاری برای توابع رویداد کاربر
در این راهنما، توابع ViewModel که رویدادهای کاربر را مدیریت میکنند، بر اساس عملکردی که انجام میدهند، با یک فعل نامگذاری میشوند - به عنوان مثال: addBookmark(id)
یا logIn(username, password)
.
رویدادهای ViewModel را مدیریت کنید
اقدامات رابط کاربری که از ViewModel - رویدادهای ViewModel - سرچشمه میگیرند، همیشه باید منجر به بهروزرسانی وضعیت رابط کاربر شوند. این با اصول جریان داده یک جهته مطابقت دارد. رویدادها را پس از تغییرات پیکربندی قابل تکرار می کند و تضمین می کند که اقدامات UI از بین نخواهند رفت. در صورت استفاده از ماژول حالت ذخیره شده ، می توانید به صورت اختیاری، رویدادها را پس از مرگ فرآیند قابل تکرار کنید.
نگاشت اقدامات UI به وضعیت UI همیشه یک فرآیند ساده نیست، اما به منطق ساده تری منجر می شود. فرآیند فکر شما نباید با تعیین اینکه چگونه رابط کاربری را به یک صفحه خاص هدایت کنید، ختم شود. باید بیشتر فکر کنید و نحوه نمایش آن جریان کاربر را در وضعیت رابط کاربری خود در نظر بگیرید. به عبارت دیگر: به این فکر نکنید که UI باید چه اقداماتی انجام دهد. به این فکر کنید که این اقدامات چگونه بر وضعیت رابط کاربری تأثیر می گذارد.
به عنوان مثال، زمانی که کاربر در صفحه ورود به سیستم وارد شده است، به صفحه اصلی پیمایش کنید. می توانید این را در حالت UI به صورت زیر مدل کنید:
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.
}
...
}
}
}
}
}
نوشتن
class LoginViewModel : ViewModel() {
var uiState by mutableStateOf(LoginUiState())
private set
/* ... */
}
@Composable
fun LoginScreen(
viewModel: LoginViewModel = viewModel(),
onUserLogIn: () -> Unit
) {
val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
// Whenever the uiState changes, check if the user is logged in.
LaunchedEffect(viewModel.uiState) {
if (viewModel.uiState.isUserLoggedIn) {
currentOnUserLogIn()
}
}
// Rest of the UI for the login screen.
}
رویدادهای مصرف کننده می تواند به روز رسانی های ایالت را آغاز کند
مصرف برخی رویدادهای ViewModel در رابط کاربری ممکن است منجر به بهروزرسانیهای وضعیت رابط کاربر شود. به عنوان مثال، هنگام نمایش پیامهای گذرا بر روی صفحه برای اینکه کاربر بداند اتفاقی افتاده است، رابط کاربری باید ViewModel را مطلع کند تا هنگامی که پیام روی صفحه نمایش داده شد، بهروزرسانی وضعیت دیگری را آغاز کند. رویدادی که زمانی اتفاق میافتد که کاربر پیام را مصرف کرده است (با رد کردن آن یا پس از وقفه زمانی) میتواند به عنوان "ورودی کاربر" تلقی شود و به این ترتیب، ViewModel باید از آن آگاه باشد. در این شرایط، حالت UI را می توان به صورت زیر مدل کرد:
// 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 وضعیت UI را به صورت زیر به روز می کند:
بازدیدها
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)
}
}
}
نوشتن
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 نیازی ندارد که بداند رابط کاربری چگونه پیام را روی صفحه نمایش می دهد. فقط می داند که یک پیام کاربر وجود دارد که باید نشان داده شود. هنگامی که پیام گذرا نشان داده شد، UI باید ViewModel را در این مورد مطلع کند، که باعث میشود بهروزرسانی وضعیت UI دیگر ویژگی 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()
}
...
}
}
}
}
}
نوشتن
@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()
}
}
}
حتی اگر پیام گذرا است، وضعیت رابط کاربری نمایشی وفادار از آنچه در هر نقطه از زمان بر روی صفحه نمایش داده می شود است. یا پیام کاربر نمایش داده می شود یا نمایش داده نمی شود.
رویدادهای ناوبری
رویدادهای مصرفکننده میتوانند جزئیات بخش بهروزرسانیهای وضعیت را فعال کنند که چگونه از وضعیت رابط کاربری برای نمایش پیامهای کاربر روی صفحه استفاده میکنید. رویدادهای ناوبری نیز نوع رایجی از رویدادها در یک برنامه اندروید هستند.
اگر رویداد به دلیل ضربه زدن کاربر روی یک دکمه در رابط کاربری فعال شود، رابط کاربری با فراخوانی کنترلکننده پیمایش یا در معرض دید قرار دادن رویداد برای تماسگیرنده قابل تنظیم، به آن رسیدگی میکند.
بازدیدها
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
}
}
}
نوشتن
@Composable
fun LoginScreen(
onHelp: () -> Unit, // Caller navigates to the right screen
viewModel: LoginViewModel = viewModel()
) {
// Rest of the UI
Button(onClick = onHelp) {
Text("Get help")
}
}
اگر ورودی داده قبل از پیمایش به اعتبار منطقی تجاری نیاز دارد، ViewModel باید آن حالت را در معرض UI قرار دهد. رابط کاربری به این تغییر حالت واکنش نشان میدهد و بر این اساس حرکت میکند. بخش رویدادهای 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.
}
...
}
}
}
}
}
نوشتن
@Composable
fun LoginScreen(
onUserLogIn: () -> Unit, // Caller navigates to the right screen
viewModel: LoginViewModel = viewModel()
) {
Button(
onClick = {
// ViewModel validation is triggered
viewModel.login()
}
) {
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()
}
}
}
در مثال بالا، برنامه همانطور که انتظار می رود کار می کند زیرا مقصد فعلی، Login، در پشته باقی نمی ماند. اگر کاربران به عقب فشار دهند نمی توانند به آن برگردند. با این حال، در مواردی که ممکن است این اتفاق بیفتد، راه حل نیاز به منطق اضافی دارد.
رویدادهای ناوبری زمانی که مقصد در پشته نگه داشته می شود
وقتی یک ViewModel حالتی را تنظیم می کند که یک رویداد ناوبری از صفحه A به صفحه B ایجاد می کند و صفحه A در پشته ناوبری نگه داشته می شود، ممکن است به منطق اضافی نیاز داشته باشید تا به طور خودکار به B نشوید. برای اجرای این، باید موارد اضافی دیگری نیز داشته باشید. حالتی که نشان می دهد آیا رابط کاربری باید به صفحه دیگر پیمایش کند یا خیر. به طور معمول، این حالت در UI نگه داشته می شود زیرا منطق ناوبری مربوط به UI است، نه ViewModel. برای نشان دادن این موضوع، بیایید مورد استفاده زیر را در نظر بگیریم.
فرض کنید در جریان ثبت نام برنامه خود هستید. در صفحه اعتبارسنجی تاریخ تولد ، زمانی که کاربر یک تاریخ را وارد میکند، زمانی که کاربر روی دکمه «ادامه» ضربه میزند، تاریخ توسط ViewModel تأیید میشود. ViewModel منطق اعتبارسنجی را به لایه داده واگذار می کند. اگر تاریخ معتبر باشد، کاربر به صفحه بعدی می رود. به عنوان یک ویژگی اضافی، کاربران می توانند در صورتی که بخواهند برخی از داده ها را تغییر دهند، بین صفحات ثبت نام مختلف به عقب و جلو بروند. بنابراین، تمام مقصدها در جریان ثبت نام در پشته یکسانی نگهداری می شوند. با توجه به این الزامات، می توانید این صفحه را به صورت زیر پیاده سازی کنید:
بازدیدها
// 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)
}
}
نوشتن
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 این منطق را به لایه داده واگذار می کند. منطق هدایت کاربر به صفحه بعدی، منطق UI است زیرا این الزامات ممکن است بسته به پیکربندی UI تغییر کند. به عنوان مثال، اگر چندین مرحله ثبت نام را به طور همزمان نشان می دهید، ممکن است نخواهید به طور خودکار به صفحه دیگری در رایانه لوحی بروید. متغیر validationInProgress
در کد بالا این عملکرد را پیادهسازی میکند و بررسی میکند که هر زمان که تاریخ تولد معتبر است و کاربر میخواهد به مرحله ثبتنام زیر ادامه دهد، رابط کاربری باید بهطور خودکار پیمایش کند یا نه.
موارد استفاده دیگر
اگر فکر میکنید مورد استفاده از رویداد رابط کاربری شما با بهروزرسانیهای وضعیت رابط کاربری قابل حل نیست، ممکن است لازم باشد در نحوه جریان دادهها در برنامه خود تجدید نظر کنید. اصول زیر را در نظر بگیرید:
- هر کلاس باید آنچه را که مسئول است انجام دهد، نه بیشتر. UI مسئول منطق رفتاری خاص صفحه مانند تماسهای ناوبری، رویدادهای کلیک و دریافت درخواستهای مجوز است. ViewModel حاوی منطق تجاری است و نتایج را از لایه های پایین سلسله مراتب به حالت UI تبدیل می کند.
- به این فکر کنید که رویداد از کجا شروع می شود. درخت تصمیم ارائه شده در ابتدای این راهنما را دنبال کنید، و هر کلاس را وادار کنید تا مسئولیت خود را بر عهده بگیرد. به عنوان مثال، اگر رویداد از UI منشا می گیرد و منجر به یک رویداد ناوبری می شود، آن رویداد باید در UI مدیریت شود. ممکن است مقداری منطق به ViewModel واگذار شود، اما مدیریت رویداد را نمی توان به طور کامل به ViewModel واگذار کرد.
- اگر چندین مصرف کننده دارید و نگران این هستید که رویداد چندین بار مصرف شود، ممکن است لازم باشد در معماری برنامه خود تجدید نظر کنید. داشتن چندین مصرف کننده همزمان باعث می شود که تضمین قرارداد دقیقاً یک بار تحویل داده شود ، بنابراین میزان پیچیدگی و رفتار ظریف منفجر می شود. اگر این مشکل را دارید، این نگرانیها را در درخت UI خود به سمت بالا فشار دهید. ممکن است به یک موجودیت متفاوت نیاز داشته باشید که محدوده بالاتری در سلسله مراتب داشته باشد.
- به این فکر کنید که دولت چه زمانی نیاز به مصرف دارد. در شرایط خاص، ممکن است نخواهید زمانی که برنامه در پسزمینه است، به حالت مصرف ادامه دهید - برای مثال، نشان دادن یک
Toast
. در این موارد، زمانی که UI در پیش زمینه است، مصرف حالت را در نظر بگیرید.
نمونه ها
نمونههای Google زیر رویدادهای رابط کاربری را در لایه UI نشان میدهند. برای دیدن این راهنمایی در عمل، آنها را کاوش کنید:
برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- لایه رابط کاربری
- دارندگان ایالت و حالت رابط کاربری {:#mad-arch}
- راهنمای معماری اپلیکیشن