8、Compose界面架构

36 阅读5分钟

Jetpack Compose 架构指南

界面状态基础

在 Jetpack Compose 中,界面是不可变的,绘制后无法更新。开发者能控制的是界面的状态。当状态发生变化时,Compose 会重新创建界面树中已更改的部分。

可组合项通过接受状态并公开事件来工作。例如,TextField 接受值并公开 onValueChange 回调,用于请求更改值:

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

注意:采用 Jetpack Compose 不会影响应用的其他层(数据层和业务层)。如需详细了解如何为应用构建所有层,请参阅应用架构指南

单向数据流 (UDF)

单向数据流是一种设计模式,在该模式下状态向下流动,事件向上流动。通过采用单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分分离开来。

使用单向数据流的应用的界面更新循环如下所示:

  1. 事件:界面的某一部分生成一个事件,并将其向上传递(例如按钮点击);或者从应用的其他层传递事件(如用户会话过期)。
  2. 更新状态:事件处理脚本可能会更改状态。
  3. 显示状态:状态容器向下传递状态,界面显示此状态。

使用 Jetpack Compose 时遵循此模式可带来以下优势:

  • 可测试性:将状态与显示状态的界面分离开来,更方便单独对二者进行测试。
  • 状态封装:因为状态只能在一个位置进行更新,并且可组合项的状态只有一个可信来源,所以不太可能由于状态不一致而出现 bug。
  • 界面一致性:通过使用可观察的状态容器,所有状态更新都会立即反映在界面中。

Compose 将 State 对象定义为值容器,对状态值的更改会触发重组。您可以使用以下方式保存状态:

  • remember { mutableStateOf(value) } - 在组合中存储状态
  • rememberSaveable { mutableStateOf(value) } - 保留状态,使其在配置更改后仍保持不变

要点mutableStateOf(value) 会创建一个 MutableState,这是 Compose 中的可观察类型。如果其值有任何更改,系统会安排重组读取此值的所有可组合函数。

定义可组合项参数

在定义可组合项的状态参数时,应考虑以下问题:

  • 可组合项的可重用性或灵活性如何?
  • 状态参数如何影响此可组合项的性能?

保持可组合项精简

为了促进分离和重复使用,每个可组合项应包含尽可能少的信息。例如,构建显示新闻报道标题的可组合项时,最好只传递需要显示的信息,而非整篇新闻报道:

@Composable
fun Header(title: String, subtitle: String) {
    // 仅当 title 或 subtitle 更改时重组
}

@Composable
fun Header(news: News) {
    // 每当传入新的 News 实例时都会重组,即使 title 和 subtitle 未变化
}

使用不可变参数和事件处理

应用的每项输入都应表示为事件:点按、文本更改,甚至计时器或其他更新。当这些事件更改界面状态时,ViewModel 应负责处理这些事件并更新界面状态。

界面层绝不应更改事件处理脚本之外的状态,因为这样做可能会导致应用出现不一致和 bug。

最好为状态和事件处理脚本 lambda 传递不可变值。此方法具有以下优势:

  • 提升可重用性
  • 确保界面不会直接更改状态的值
  • 避免并发问题
  • 降低代码复杂性

例如,接受 String 和 lambda 作为参数的可组合项可以从多种上下文中调用,可重用性更高。考虑一个始终显示文本并包含返回按钮的顶部应用栏:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

在 Compose 中使用 ViewModel

借助 ViewModel 和 mutableStateOf,您可以在应用中引入单向数据流:

  • 界面的状态通过 StateFlowLiveData 等可观察的状态容器公开
  • ViewModel 处理来自应用界面或其他层的事件,并根据事件更新状态容器

密封类表示 UI 状态

例如,在实现登录屏幕时,可以将屏幕状态建模为密封类:

sealed class UiState {
    object SignedOut : UiState()
    object Loading : UiState()
    data class Error(val message: String) : UiState()
    object SignedIn : UiState()
}

ViewModel 实现

ViewModel 将状态公开为 State,设置初始状态,并根据需要更新状态。它还通过公开方法来处理事件:

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState> = _uiState
    
    fun onSignIn() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                // 执行登录网络请求
                _uiState.value = UiState.SignedIn
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

在可组合项中使用 ViewModel

在可组合项中,观察 ViewModel 公开的状态,并将事件处理委派给 ViewModel:

@Composable
fun LoginScreen(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState
    
    when (uiState) {
        is UiState.SignedOut -> {
            Button(onClick = { viewModel.onSignIn() }) {
                Text("Sign in")
            }
        }
        is UiState.Loading -> {
            CircularProgressIndicator()
        }
        is UiState.Error -> {
            val errorState = uiState as UiState.Error
            Text(text = errorState.message)
            Button(onClick = { viewModel.onSignIn() }) {
                Text("Try again")
            }
        }
        is UiState.SignedIn -> {
            Text("Welcome!")
        }
    }
}

其他状态容器

除了 mutableStateOf API 之外,Compose 还为其他可观察类型提供了扩展,可用于注册为监听器,并将值表示为状态:

LiveData

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState> = _uiState
    
    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.observeAsState()
    // ...
}

StateFlow

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.SignedOut)
    val uiState: StateFlow<UiState> = _uiState
    
    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    // ...
}

最佳实践总结

  1. 使用单向数据流:状态向下流动,事件向上流动
  2. 将界面元素与状态分离:可组合项应接受状态并公开事件
  3. 使用 ViewModel 管理复杂状态:尤其是需要在配置更改后保留的状态
  4. 选择合适的可观察类型:根据用例选择 mutableStateOfLiveDataStateFlow
  5. 保持可组合项参数精简:只传递必要信息,传递不可变值
  6. 在 ViewModel 中处理业务逻辑:界面层只负责显示状态和传递事件

遵循这些原则将帮助您构建可维护、可测试且性能良好的 Compose 应用。