رویدادهای رابط کاربری، اقداماتی هستند که باید در لایه رابط کاربری، یا توسط رابط کاربری یا توسط 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()
}
}
}
نوشتن
@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 یک شیء 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.
}
...
}
}
}
}
}
نوشتن
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 باید از آن آگاه باشد. در این شرایط، وضعیت رابط کاربری را میتوان به صورت زیر مدلسازی کرد:
// 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)
}
}
}
نوشتن
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 پاک کند:
بازدیدها
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()
}
}
}
اگرچه پیام گذرا است، اما وضعیت رابط کاربری نمایش دقیقی از آنچه در هر لحظه روی صفحه نمایش داده میشود، ارائه میدهد. یا پیام کاربر نمایش داده میشود یا نمیشود.
رویدادهای ناوبری
رویدادهای Consuming میتوانند باعث بهروزرسانی وضعیت شوند که جزئیات نحوه استفاده از وضعیت رابط کاربری برای نمایش پیامهای کاربر روی صفحه را شرح میدهد. رویدادهای ناوبری نیز نوع رایجی از رویدادها در یک برنامه اندروید هستند.
اگر رویداد در رابط کاربری به دلیل لمس یک دکمه توسط کاربر فعال شود، رابط کاربری با فراخوانی کنترلر ناوبری یا نمایش رویداد به فراخوانندهی قابل ترکیب (caller composable) مربوطه، این مشکل را برطرف میکند.
بازدیدها
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 باید آن وضعیت را در اختیار رابط کاربری قرار دهد. رابط کاربری به آن تغییر وضعیت واکنش نشان میدهد و بر اساس آن پیمایش میکند. بخش رویدادهای 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()
}
}
}
در مثال بالا، برنامه همانطور که انتظار میرود کار میکند زیرا مقصد فعلی، یعنی ورود، در پشته پشتی نگه داشته نمیشود. کاربران در صورت فشردن دکمه بازگشت نمیتوانند به آن بازگردند. با این حال، در مواردی که ممکن است این اتفاق بیفتد، راهحل به منطق اضافی نیاز دارد.
رویدادهای ناوبری زمانی که مقصد در پشته پشتی نگه داشته میشود
وقتی یک 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)
}
}
نوشتن
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 را زمانی که رابط کاربری در پیشزمینه است در نظر بگیرید.
نمونهها
نمونههای گوگل زیر، رویدادهای رابط کاربری را در لایه رابط کاربری نشان میدهند. برای مشاهده این راهنمایی در عمل، به آنها مراجعه کنید:
برای شما توصیه میشود
- توجه: متن لینک زمانی نمایش داده میشود که جاوا اسکریپت غیرفعال باشد.
- لایه رابط کاربری
- دارندگان وضعیت و وضعیت رابط کاربری {:#mad-arch}
- راهنمای معماری برنامه