• 6.Android应用架构指南:界面层界面事件


    界面层界面事件

    界面事件指的是应该在界面层中由界面或ViewModel处理的操作。最常见的事件类型是用户事件。用户通过与应用程序交互(例如,点击屏幕或手势)来生成用户事件。然后,界面会使用onClick()监听器等回调来使用这些事件。

    ViewModel通常负责处理特定用户事件的业务逻辑。例如,用户单击按钮以刷新某些数据。通常,ViewModel通过公开界面可以调用的函数来处理这个问题。用户事件也可能具有界面可以直接处理的界面行为逻辑,例如,导航到不同的屏幕或显示一个Snackbar。
    虽然同一应用的业务逻辑在不同移动平台或设备类型上保持不变,但界面的行为逻辑在实现方面可能有所不同。界面层概览定义了这些类型的逻辑,如下所示:

    • 业务逻辑是指如何处理状态更改,例如付款或存储用户的偏好设置。网域层和数据层通常负责处理此类逻辑。架构组件ViewModel通常用作处理业务逻辑类的特色解决方案。
    • 界面行为逻辑或者说界面逻辑是指如何显示状态更改,例如导航逻辑或如何向用户显示消息。界面会处理此逻辑。

    界面事件决策树

    下图这个决策树展示了如何寻找处理特定事件用例的最佳实践。其余部分将详细介绍这些方法:

    在这里插入图片描述

    处理用户事件

    如果用户事件与修改界面元素的状态(例如,可展开项的状态)有关,则界面可以直接处理用户事件。如果事件需要执行业务逻辑,例如刷新屏幕上的数据,则应该由ViewModel处理。
    以下示例展示了如何使用不同的按钮来展开界面元素(界面逻辑)和刷新屏幕上的数据(业务逻辑):

    class LatestNewsActivity : AppCompatActivity() {
        private lateinit var binding: ActivityLatestNewsBinding
        private val viewModel: LatestNewsViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            /* ... */
    
            // 展开详细信息的事件由修改视图内部状态的界面处理
            binding.expandButton.setOnClickListener {
                binding.expandedSection.visibility = View.VISIBLE
            }
    
            // 刷新事件由负责业务逻辑的ViewModel处理
            binding.refreshButton.setOnClickListener {
                viewModel.refreshNews()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    RecyclerView中的用户事件

    如果操作是在界面树中比较靠下一层生成的,例如在RecyclerView项或自定义的View中,ViewModel仍然应该是处理用户事件的一个。
    例如,假设所有来自NewsActivity的新闻条目都包含一个书签按钮。ViewModel需要知道书签新闻项的id。当用户收藏一个新闻条目时,RecyclerView适配器不会从ViewModel中调用公开的addBookmark(newsId)函数,这将需要依赖于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),
                // 业务逻辑作为lambda函数传递,界面在单击事件上调用该函数。
                onBookmark = {
                    repository.addBookmark(news.id)
                }
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这样,RecyclerView适配器只处理它需要的数据:NewsItemUiState对象列表。该适配器不能访问整个ViewModel,因此不太可能滥用ViewModel公开的功能。当你只允许Activity类与ViewModel一起工作时,即表示职责已经分开。这确保了特定于界面的对象(例如视图或RecyclerView适配器)不会直接与ViewModel交互。
    需要注意的是,ViewModel传递到RecyclerView适配器是不好的做法,因为这样会将适配器与ViewModel类紧密耦合在一起
    另一个常见的模式是RecyclerView适配器为用户操作提供一个回调接口。在这种情况下,ActivityFragment可以处理绑定,并直接从回调接口调用ViewModel的函数。

    处理ViewModel事件

    源自ViewModel的界面操作,即ViewModel事件,应该总是触发界面状态的更新。这遵循了单向数据流的原则。它使事件在配置更改后可以复现,并保证界面操作不会丢失。如果使用已保存的状态模块,还可以在进程终止后使事件重新复现(可选操作)。
    将界面操作映射到界面状态并不总是一个简单的过程,但确实可以简化逻辑。例如,不单单要想办法确定如何将界面导航到特定的屏幕,还需要进一步思考如何在界面状态中表示该用户流。换句话说:不需要考虑界面需要执行哪些操作,而是要思考这些操作会对界面状态造成什么影响

    例如,考虑在用户登录时从登录屏幕切换到主屏幕的情况。可以在界面状态中进行如下建模:

    data class LoginUiState(
        val isLoading: Boolean = false,
        val errorMessage: String? = null,
        val isUserLoggedIn: Boolean = false
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此界面会对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) {
                            // 导航到主屏幕
                        }
                        ...
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    使用事件可以触发状态更新

    在界面中使用某些ViewModel事件可能会导致其他界面状态的更新。例如,在屏幕上显示临时消息以让用户知道发生了什么事情,此时,当消息显示在屏幕上时,界面需要通知ViewModel触发另一个状态更新。用户处理消息(通过关闭消息或超时)后发生的事件可被视为用户输入,因此ViewModel应该知道这一点。在这种情况下,界面状态可按以下方式建模:

    // 为最新的新闻屏幕建模界面状态
    data class LatestNewsUiState(
        val news: List<News> = emptyList(),
        val isLoading: Boolean = false,
        val userMessage: String? = null
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当业务逻辑需要向用户显示一个新的临时消息时,ViewModel会按照如下方式更新界面状态:

    class LatestNewsViewModel(/* ... */) : ViewModel() {
    
        private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
        val uiState: StateFlow<LatestNewsUiState> = _uiState
    
        fun refreshNews() {
            viewModelScope.launch {
                // 如果没有网络连接,在屏幕上显示一条新的信息。
                if (!internetConnection()) {
                    _uiState.update { currentUiState ->
                        currentUiState.copy(userMessage = "No Internet connection")
                    }
                    return@launch
                }
    
                // 其他操作
            }
        }
    
        fun userMessageShown() {
            _uiState.update { currentUiState ->
                currentUiState.copy(userMessage = null)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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: 使用userMessage显示Snackbar。
    
                            // 一旦消息显示并关闭,通知ViewModel。
                            viewModel.userMessageShown()
                        }
                        ...
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    注意:如需了解更高级的用例,其中包含要在屏幕上显示的用户消息列表,请查看jetsnack compose示例

    其他用例

    如果你认为你的界面事件用例不能用界面状态更新来解决,此时可能需要重新考虑应用程序中的数据流。考虑以下原则:

    • 每个类都应各司其职,不能越界。界面负责屏幕专属的行为逻辑,例如导航调用、点击事件以及获取权限请求。ViewModel包含业务逻辑,并将较低层次结构的结果转换为界面状态。
    • 考虑事件的起源。参考本指南开头所展示的决策树,并让每个类处理它们负责的内容。例如,如果事件源自界面,并触发导航事件,则必须在界面中处理该事件。一些逻辑可能被委托给ViewModel,但是事件的处理不能完全委托给ViewModel
    • 如果有多个消费者,并且担心事件被多次使用,此时可能需要重新考虑应用程序架构。拥有多个并发的消费者会导致只交付一次的约定变得极其难以保证,因此复杂性和微妙行为的数量会激增。如果遇到了这一问题,考虑在界面树中将这些问题向上抛出:可能需要在层次结构的更高层次上定义不同的实体。
      考虑何时需要使用状态。在某些情况下,可能不想在应用处于后台时保留使用状态(例如显示Toast)。在这些情况下,请考虑在界面位于前台时使用状态。

    注意:在某些应用程序中,可能会看到ViewModel事件使用kotlin管道(Channel)或其他响应流暴露给界面。当生产者(ViewModel)的存在时间比消费者(界面,ComposeView)更长时,这些解决方案不能保证这些事件的交付和处理。这可能会导致开发者日后遇到问题,而且对于大多数应用来说,这也是一种不可接受的用户体验,因为可能会使应用处于不一致的状态,例如会引入错误,或者使用户错过关键信息。
    如果遇到上述某种情况,请重新考虑这种一次性ViewModel事件对于界面的实际意义。请立即处理这些情况并将其还原为界面状态。界面状态可以更好地表示给定时间点的界面,可以提供更多的交付和处理保证,通常更易于测试,并且可以与应用的其余部分保持统一。
    要了解关于为什么不应该在一些代码示例中使用上述API的更多信息,请阅读ViewModel:一次性事件反模式博客文章。

  • 相关阅读:
    Android组件模块间解耦及通信轻量级实现方案
    Matplotlib双轴教程
    C++ PrimerPlus 复习 第八章 函数探幽
    PMSM中常用的两种坐标变换——Clarke变换
    linux下安装maven(详细图文教程)
    linux特殊权限
    4.9-算法 4.10-伪代码 4.11复杂度 4.12排序
    ASEMI整流桥HD06参数,HD06图片,HD06应用
    每日一题——使用tkinter开发GUI程序习题
    《计算机基础与程序设计》之C语言复习—第一章C语言概述
  • 原文地址:https://blog.csdn.net/qq_39748549/article/details/126440315