أحداث واجهة المستخدم

أحداث واجهة المستخدم هي إجراءات يجب معالجتها في طبقة واجهة المستخدم، إما من خلال واجهة المستخدم أو بواسطة ViewModel. إنّ نوع الأحداث الأكثر شيوعًا هو أحداث المستخدمين. ينشئ المستخدم أحداثًا للمستخدم من خلال التفاعل مع التطبيق، مثلاً من خلال النقر على الشاشة أو عن طريق إنشاء الإيماءات. بعد ذلك، تستهلك واجهة المستخدم هذه الأحداث من خلال عمليات استدعاء مثل أدوات معالجة طلبات onClick().

عادةً ما يكون نموذج العرض مسؤولًا عن معالجة منطق العمل لحدث معيّن خاص بالمستخدم، على سبيل المثال، عندما ينقر المستخدم على زر لتحديث بعض البيانات. عادةً ما يعالج ViewModel هذا من خلال إظهار الوظائف التي يمكن لواجهة المستخدم استدعاءها. قد تتضمّن أحداث المستخدِم أيضًا منطقًا لسلوك واجهة المستخدِم يمكن أن تتعامل معه واجهة المستخدِم بشكل مباشر، على سبيل المثال، عند الانتقال إلى شاشة مختلفة أو عرض Snackbar.

في حين يظل منطق النشاط التجاري كما هو للتطبيق نفسه على منصات أو أشكال الأجهزة المختلفة للأجهزة الجوّالة، فإن منطق سلوك واجهة المستخدم هو تفاصيل تنفيذ قد تختلف بين تلك الحالات. تحدّد صفحة طبقة واجهة المستخدم الأنواع التالية من المنطق على النحو التالي:

  • يشير منطق الأعمال إلى ما يجب فعله بشأن تغييرات الحالة، على سبيل المثال، تسديد دفعة أو تخزين إعدادات المستخدم المفضّلة. عادةً ما يتعامل النطاق وطبقات البيانات مع هذا المنطق. في هذا الدليل، يتم استخدام فئة مكونات البنية الأساسية ViewModel كحل نظري للصفوف التي تعالج منطق الأعمال.
  • يشير منطق سلوك واجهة المستخدم أو منطق واجهة المستخدم إلى كيفية عرض تغييرات الحالة، على سبيل المثال، منطق التنقل أو كيفية عرض الرسائل للمستخدم. تتعامل واجهة المستخدم مع هذا المنطق.

شجرة قرارات حدث واجهة المستخدم

يعرض المخطّط التالي شجرة قرارات للعثور على أفضل نهج للتعامل مع حالة استخدام حدث معيّن. يشرح الجزء المتبقي من هذا الدليل هذه الأساليب بالتفصيل.

إذا نشأ الحدث في ViewModel، عليك تعديل حالة واجهة المستخدم. إذا كان
 الحدث قد نشأ في واجهة المستخدم ويتطلب منطق العمل، يجب تفويض منطق النشاط التجاري إلى ViewModel. إذا نشأ الحدث في واجهة المستخدم ويتطلب منطقًا لسلوك واجهة المستخدم، عليك تعديل حالة عنصر واجهة المستخدم مباشرةً في
    
    واجهة المستخدم.
الشكل 1. شجرة القرار للتعامل مع الأحداث.

معالجة أحداث المستخدمين

يمكن أن تعالج واجهة المستخدم أحداث المستخدم مباشرةً إذا كانت تلك الأحداث تتعلق بتعديل حالة عنصر في واجهة المستخدم، على سبيل المثال، حالة عنصر قابل للتوسيع. إذا كان الحدث يتطلب تنفيذ منطق عمل، مثل إعادة تحميل البيانات على الشاشة، يجب أن تتم معالجته باستخدام 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 كائن حالة يُسمى 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. عندما تسمح لفئة النشاط فقط بالعمل باستخدام ViewModel، فإنك تفصل المسئوليات. ويضمن ذلك عدم تفاعل العناصر الخاصة بواجهة المستخدم مثل الملفات أو محوّلات RecyclerView مباشرةً مع نموذج ViewModel.

اصطلاحات التسمية لدوال أحداث المستخدم

في هذا الدليل، تتم تسمية دوال ViewModel التي تعالج أحداث المستخدم باستخدام فعل استنادًا إلى الإجراء الذي يعالجه، على سبيل المثال: addBookmark(id) أو logIn(username, password).

التعامل مع أحداث ViewModel

يجب أن تؤدي دائمًا إجراءات واجهة المستخدم التي تنشأ من أحداث ViewModel، أحداث ViewModel، إلى تحديث حالة واجهة المستخدم. وهذا يتوافق مع مبادئ تدفق البيانات الأحادي الاتجاه. كما أنه يجعل الأحداث قابلة لإعادة الظهور بعد تغييرات الإعدادات ويضمن عدم فقدان إجراءات واجهة المستخدم. اختياريًا، يمكنك أيضًا جعل الأحداث قابلة لإعادة التكرار بعد توقّف العملية إذا كنت تستخدم وحدة الحالة المحفوظة.

إن ربط إجراءات واجهة المستخدم بحالة واجهة المستخدم ليس دائمًا عملية بسيطة، ولكنه يؤدي إلى منطق أبسط. يجب ألا تنتهي عملية التفكير الخاصة بك بتحديد كيفية جعل واجهة المستخدم تنتقل إلى شاشة معينة، على سبيل المثال. تحتاج إلى التفكير أكثر والنظر في كيفية تمثيل تدفق المستخدم هذا في حالة واجهة المستخدم الخاصة بك. بعبارة أخرى: لا تفكر في الإجراءات التي يجب أن تتخذها واجهة المستخدم، بل فكّر في كيفية تأثير تلك الإجراءات على حالة واجهة المستخدم.

على سبيل المثال، ضع في اعتبارك حالة الانتقال إلى الشاشة الرئيسية عند تسجيل دخول المستخدم في شاشة تسجيل الدخول. يمكنك نمذجة هذا في حالة واجهة المستخدم على النحو التالي:

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 بذلك، ما يؤدي إلى محو سمة 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()
        }
    }
}

على الرغم من أن الرسالة مؤقتة، فإن حالة واجهة المستخدم هي تمثيل دقيق لما يتم عرضه على الشاشة في كل نقطة زمنية. إما أن رسالة المستخدم معروضة أو لا تظهر.

يفصّل قسم يمكن أن تؤدي الأحداث الاستهلاكية إلى تشغيل تعديلات الحالة وتوضّح تفاصيل كيفية استخدام حالة واجهة المستخدم لعرض رسائل المستخدمين على الشاشة. أحداث التنقل هي أيضًا نوع شائع من الأحداث في تطبيق Android.

إذا تم تشغيل الحدث في واجهة المستخدم لأن المستخدم قد نقر على أحد الأزرار، ستتولى واجهة المستخدم ذلك من خلال استدعاء وحدة التحكم في التنقل أو عرض الحدث على المتصل القابل للتعديل حسبما تقتضي الحاجة.

المشاهدات

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 إلى إظهار هذه الحالة لواجهة المستخدم. ستتفاعل واجهة المستخدم مع تغيير الحالة وتنتقل وفقًا لذلك. يتناول قسم أحداث التعامل مع نموذج العرض حالة الاستخدام هذه. إليك رمز مشابه:

المشاهدات

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. لتوضيح ذلك، سنأخذ في الاعتبار حالة الاستخدام التالية.

لنفترض أنّك في مرحلة التسجيل لتطبيقك. في شاشة التحقق من تاريخ الميلاد، عندما يُدخل المستخدم تاريخًا، يتم التحقق من صحة التاريخ بواسطة 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 هذا المنطق إلى طبقة البيانات. منطق انتقال المستخدم إلى الشاشة التالية هو منطق واجهة المستخدم لأن هذه المتطلبات يمكن أن تتغير وفق إعداد واجهة المستخدم. على سبيل المثال، قد لا تريد الانتقال تلقائيًا إلى شاشة أخرى في جهاز لوحي إذا كنت تعرض خطوات تسجيل متعددة في نفس الوقت. ينفِّذ المتغيّر validationInProgress في الرمز أعلاه هذه الوظيفة ويعالج ما إذا كان يجب الانتقال من واجهة المستخدم تلقائيًا أم لا عندما يكون تاريخ الميلاد صالحًا وأراد المستخدم المتابعة إلى خطوة التسجيل التالية.

حالات الاستخدام الأخرى

إذا كنت تعتقد أنّه لا يمكن حل حالة الاستخدام لحدث واجهة المستخدم من خلال تحديثات حالة واجهة المستخدم، قد تحتاج إلى إعادة النظر في كيفية تدفق البيانات في تطبيقك. ننصحك بمراعاة المبادئ التالية:

  • يجب أن ينجز كل صف المهام المنوطة به، وليس أكثر من ذلك. تتولى واجهة المستخدم مسؤولية منطق السلوك الخاص بالشاشة مثل استدعاءات التنقّل، وأحداث النقر، والحصول على طلبات الأذونات. يحتوي ViewModel على منطق العمل ويحول النتائج من الطبقات السفلى من التسلسل الهرمي إلى حالة واجهة المستخدم.
  • فكِّر في الموقع الجغرافي الذي نشأ فيه هذا الحدث. اتّبِع شجرة القرارات الواردة في بداية هذا الدليل، واجعل كل صف مسؤولاً عن المهام التي تقع على عاتقه. على سبيل المثال، إذا نشأ الحدث من واجهة المستخدم ونتج عنه حدث تنقّل، يجب التعامل مع هذا الحدث في واجهة المستخدم. قد يتم تفويض بعض المنطق إلى ViewModel، ولكن لا يمكن تفويض معالجة الحدث بالكامل إلى ViewModel.
  • إذا كان لديك العديد من المستهلكين وكنت قلقًا بشأن مشاهدة الفعالية عدة مرات، قد تحتاج إلى إعادة النظر في بنية تطبيقك. يؤدي توفُّر العديد من المستهلكين المتزامنين إلى أن يصبح من الصعب جدًا ضمان التسليم مرة واحدة فقط، ما يؤدي إلى حدوث مضاعفة كبيرة وسرعة كبيرة في السلوك الدقيق. إذا كنت تواجه هذه المشكلة، فكر في دفع هذه المخاوف إلى أعلى في شجرة واجهة المستخدم لديك؛ قد تحتاج إلى كيان مختلف على مستوى أعلى في التسلسل الهرمي.
  • فكِّر في الأوقات التي تحتاج فيها الدولة إلى استهلاك الطاقة. في بعض الحالات، قد لا تريد مواصلة استخدام التطبيق عندما يكون التطبيق في الخلفية، على سبيل المثال عرض علامة Toast. وفي تلك الحالات، ضع في اعتبارك استخدام الحالة عندما تكون واجهة المستخدم في المقدمة.

عيّنات

توضّح نماذج Google التالية أحداث واجهة المستخدم في طبقة واجهة المستخدم. يمكنك الانتقال إلى هذه الصفحة للاطّلاع على هذه الإرشادات عمليًا: