界面层 (View)

概念和 Jetpack Compose 实现

界面的作用是在屏幕上显示应用数据,并充当主要的用户互动点。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。实际上,界面是从数据层获取的应用状态的直观呈现。

不过,从数据层获取的应用数据的格式通常不同于您需要显示的信息的格式。例如,您可能只需要在界面中显示部分数据,或者可能需要合并两个不同的数据源,以便提供切合用户需求的信息。无论您应用的是什么逻辑,都需要向界面传递完全呈现界面所需的所有信息。界面层是一个流水线,负责将应用数据变化转换为界面可以呈现的形式,然后将其显示出来。

公开界面状态

定义界面状态并确定如何管理相应状态的提供后,下一步是将提供的状态发送给界面。由于您使用 UDF 管理状态的提供,因此您可以将提供的状态视为数据流,换句话说,随着时间的推移,将提供状态的多个版本。因此,您应在 LiveDataStateFlow 等可观察数据容器中公开界面状态。这样做是为了使界面可以对状态的任何变化做出反应,而无需直接从 ViewModel 手动拉取数据。这些类型还有一个好处是,始终缓存界面状态的最新版本,这对于在配置发生变化后快速恢复状态非常有用。

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = 
}

创建 UiState 流的一种常用方法是,将后备可变 数据流作为来自 ViewModel 的不可变数据流进行公开,例如将 MutableStateFlow<UiState> 作为 StateFlow<UiState> 进行公开。

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

这样一来,ViewModel 便可以公开在内部更改状态的方法,以便发布供界面使用的更新。以需要执行异步操作的情况为例,可以使用 viewModelScope启动协程,并且可以在操作完成时更新可变状态。

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

使用界面状态

在界面中使用可观察数据容器时,请务必考虑界面的生命周期。这非常重要,因为当未向用户显示视图时,界面不应观察界面状态。如需详细了解此主题,请参阅这篇博文。使用 LiveData 时,LifecycleOwner 会隐式处理生命周期问题。使用数据流时,最好通过适当的协程作用域和 repeatOnLifecycle API 来处理这一任务:

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

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

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

显示正在执行的操作

UiState 类中表示加载状态的一种简单方法是使用布尔值字段:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

此标记的值表示界面中是否存在进度条。

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

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

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

动画

为了提供流畅的顶级导航过渡,您可能需要等待第二个屏幕加载数据,然后再启动动画。Android 视图框架提供了一些钩子,以便通过 fragment 目的地之间的过渡。postponeEnterTransition()startPostponedEnterTransition()这些 API 提供了一种方法,可确保第二个屏幕上的界面元素(通常是从网络提取的图片)在界面将过渡动画化到该屏幕之前准备好显示。