以下是到目前为止的架构图,已经成功的将 ViewModel 层的全部商业逻辑移到了 Domain 层:
接下来,将在右边的 ContextMenu 也开一条从 View 层到 Domain 层的依赖,让 CoEditorViewModel
不需要认识 ContextMenu
,也就是说拿掉删除、改颜色、改文字的这几个功能,如此一来,也不需要将 CoEditorViewModel
的函式经过这麽多层传到 ContextMenuView
才能做事,ContextMenuView
会有独立的 ViewModel ,所有的事件交给这个 ViewModel 即可,所以,我们就先从建立 ViewModel 开始吧!
class ContextMenuViewModel(
private val contextMenu: ContextMenu
): ViewModel() {
val selectedColor = contextMenu.selectedColor
val colorOptions = contextMenu.colorOptions
fun onDeleteClicked() {
contextMenu.onDeleteClicked()
}
fun onColorSelected(color: YBColor) {
contextMenu.onColorSelected(color)
}
fun onEditTextClicked() {
contextMenu.onEditTextClicked()
}
}
这个 ViewModel 真是意料之外的简单,几乎就是个空壳,因为大部分的实作都已经在 Domain 层完成了。有了这个类别有助於我们可以更加的将职责分离,让 CoEditorViewModel
的职责更加单一,那我们该如何把它串接到 View 呢?让我们来看看原本的 MenuView
是长怎样的吧!
@Composable
fun MenuView(
modifier: Modifier = Modifier,
selectedColor: YBColor,
onDeleteClicked: () -> Unit,
onColorSelected: (YBColor) -> Unit,
onTextClicked: () -> Unit
) {
// in EditorScreen...
@Composable
fun EditorScreen(viewModel: EditorViewModel) {
// ..others
MenuView(
selectedColor = selectingColor,
onDeleteClicked = viewModel::onDeleteClicked,
onColorSelected = viewModel::onColorSelected,
onTextClicked = viewModel::onEditTextClicked
)
}
原本是由 EditorScreen
这个 Composable function 拥有 MenuView
,同时也会使用 EditorViewModel
当中的函式来与 MenuView
做互动,现在我们要脱离这样的相依关系,可以怎麽做呢?
还记得 Stateful 跟 Stateless 的观念吗?一个有 ViewModel 的 Composable function 就是一个 Stateful 的元件,换句话说,如果我们要让 ContextMenuView
有 ViewModel 的相依的话,ContextMenuView
****本身就要是一个 Stateful 元件,但同时我又不想破坏现在既有的函式,毕竟现在MenuView
是可以支援预览的,要快速修改很方便,一但这个函式跟 ViewModel 有连结的话将会无法预览,所以,我们为何不乾脆做出不同类型的 ContextMenuView 呢?
@Composable
fun StatefulContextMenuView(
modifier: Modifier = Modifier
) {
val contextMenuViewModel by LocalViewModelStoreOwner.current!!.viewModel<ContextMenuViewModel>() // [1]
val selectedColor by contextMenuViewModel.selectedColor.subscribeAsState(initial = YBColor.Aquamarine)
ContextMenuView(
modifier = modifier,
selectedColor = selectedColor,
allColors = contextMenuViewModel.colorOptions, // [2]
onDeleteClicked = contextMenuViewModel::onDeleteClicked,
onColorSelected = contextMenuViewModel::onColorSelected,
onTextClicked = contextMenuViewModel::onEditTextClicked
)
}
这里新增了一个新类别:StatefulContextMenuView
,可以用来跟 ContextMenuView
作区别,其最主要的职责是获取 ViewModel 以及掌控 ContextMenu 的状态。另外,在 [1] 这边出现了一个新的类别:LocalViewModelStoreOwner
,使用这个类别就可以获取当下的 ViewModelStoreOwner,还记得吗?在之前介绍 Navigation 的时候有出现过这个类别,以一般的情况下, ViewModelStoreOwner 会是 Activity 或是 Fragment ,但是有时候我们不希望拿到“全域”的 ViewModelStoreOwner ,而是能够随着当下的 Scope 一起生、一起死,这个 LocalViewModelStoreOwner
所做的正是这件事,会随着当下的情况去拿到当下所提供的 ViewModelStoreOwner ,而不是“全域”的那一个。
至於依我们的使用案例来说,这个 ViewModelStoreOwner 会是谁呢?答案就是 EditorScreen
这边的 backStackEntry :
ReactiveStickyNoteTheme {
NavHost(navController, startDestination = Screen.Board.route) {
composable(Screen.Board.route) { backStackEntry -> // 就是它
EditorScreen(
// ...
)
}
想了解更多关於 LocalViewModelStoreOwner
的资讯,可以去看看 Jetpack Compose 的 CompositionLocal 相关文章。像是这个官方介绍:https://developer.android.com/jetpack/compose/compositionlocal
另外在上方程序码 StatefulContextMenuView
中的 [2] 将 allColor 传进了 ContextMenuView
,这样做的目的是让更多的控制权放在 ViewModel ,不要让 View 层知道需要显示哪些颜色的选项,以下的程序码是 Stateless 的 ContextMenuView
:
@Composable
fun ContextMenuView(
modifier: Modifier = Modifier,
selectedColor: YBColor,
allColors: List<YBColor>,
onDeleteClicked: () -> Unit,
onColorSelected: (YBColor) -> Unit,
onTextClicked: () -> Unit
) {
var expended by remember {
mutableStateOf(false)
}
Surface(
modifier = modifier.fillMaxWidth(),
elevation = 4.dp,
color = MaterialTheme.colors.surface
) {
Row {
IconButton(onClick = onDeleteClicked ) {
val painter = painterResource(id = R.drawable.ic_delete)
Icon(painter = painter, contentDescription = "Delete")
}
IconButton(onClick = onTextClicked ) {
val painter = painterResource(id = R.drawable.ic_text)
Icon(painter = painter, contentDescription = "Edit text")
}
IconButton(onClick = { expended = true }) {
Box(modifier = Modifier
.size(24.dp)
.background(Color(selectedColor.color), shape = CircleShape))
DropdownMenu(expanded = expended, onDismissRequest = { expended = false }) {
for (color in allColors) {
DropdownMenuItem(onClick = {
onColorSelected(color)
expended = false
}) {
Box(modifier = Modifier
.size(24.dp)
.background(Color(color.color), shape = CircleShape))
}
}
}
}
}
}
}
运用 CompositionLocal 与 Stateful 概念的结合还有另一个好处,就是这样的做法可以任意的控制 Recomposition 所影响的范围。一般来说,如果使用参数传递的方式,任何有经过参数的 Composable function,只要参数的数值有修改过,不管改的范围有多少,所有的 Composable function 都会再重新 Recompose 一次,这样对於效能来说会是个负担:
以上图为例,A、B、C 都是 Immutable 的 class ,他们彼此之间是组合关系。现在可能在 ViewModel 中因为某个功能的关系,修改了 c 的值,照理来说,我只想要重新执行 ComposableC
就好了,但是由於他们都是 Immutable ,所以 ComposableA
跟 ComposableB
也会跟着受影响而一起被重新执行了。所以如果需要的话,使用 CompositionLocal 与 Stateful 会是一个不错的组合技。
在原本的 EditorScreen
中,控制 ContextMenu 还有 Adder Button 显示状态的方式是观察 selectingNote
这个变数,如果该值为空的,就显示 Adder Button,反之则显示 ContextMenu,但是这其实算是领域知识的一部分,交给 View 来判断其实不是很好,而且如果要写测试的话,测试很难描述出这边的行为变化,於是比较好的做法还是将这段逻辑放到 Domain 层:
class CoEditor(
private val noteRepository: NoteRepository
) {
private val _showContextMenu = BehaviorSubject.createDefault(false)
private val _showAddButton = BehaviorSubject.createDefault(true)
val showAdderButton: Observable<Boolean> = _showAddButton.hide()
val openEditTextScreen: Observable<String> = _openEditTextScreen.hide()
// 变更选择状态时也一起更新选单的显示状态
fun selectNote(noteId: String) {
if (selectedNoteId.value.isPresent && selectedNoteId.value.get() == noteId) {
clearSelection()
} else {
selectedNoteId.onNext(Optional.of(noteId))
_showAddButton.onNext(false)
_showContextMenu.onNext(true)
}
}
fun clearSelection() {
selectedNoteId.onNext(Optional.empty())
_showAddButton.onNext(true)
_showContextMenu.onNext(false)
}
// ..others
}
以下是更改过後的 View :
fun CoEditorScreen(
viewModel: EditorViewModel,
openEditTextScreen: (String) -> Unit
) {
...
// 预设值为 true,什麽都没做的情况下应该是未选择状态
val showAddButton by viewModel.showAddButton.subscribeAsState(initial = true)
val showContextMenu by viewModel.showContextMenu.subscribeAsState(initial = false)
// Adder button
AnimatedVisibility(
visible = showAddButton,
modifier = Modifier.align(Alignment.BottomEnd)
) { ... }
// ContextMenu
AnimatedVisibility(
visible = showContextMenu,
modifier = Modifier.align(Alignment.BottomCenter)
) { ... }
}
今天又踏出了改变结构的一小步,读者试着想像一下,如果没有经过之前的“小步快跑”,这样的结构改变会是多麽危险,凭空建立一个 ContextMenuViewModel
,同时又要将其商业逻辑做搬移,我们将很难确保这样巨大的改变不会改坏任何功能。
接着,好好运用 Jetpack Compose 的 Stateful 组件,可以让我们更方便的达到职责分离的这件事,从现在开始可以转换思维:不是只有像是 Activity 或是 Fragment 这种以“页面”为单位的元件才可以建立 ViewModel 的实例,只要是一个够独立的 UI 元件,都可以拥有属於自己的 ViewModel ,让 ViewModel 不再因为要认识所有 UI 细节而臃肿肥大。
最後,表达清晰的意图可以大大的增加程序码的可读性,有时为了贪图方便,让商业逻辑下放到了 View 层,这样做的结果会使得下一个看程序码的工程师需要再做一次的“脑内翻译”,让每个行为有适合的名称看似麻烦但是却是一个划算的交易。
加密前的资料在前几天我们都有拿到了!接着就是实作 AES-CBC 罗~ 流程如下图 关於 AES-C...
BrowsingHistoryView 好的,就从 NirSoft 官网的 Forensics 分类...
学习重点 一维阵列 多维阵列(阵列内含有阵列,形成多层结构) 列表(java.util.ArrayL...
实务上,因应不同的开发阶段,应用程序会运行在开发环境 (Develop Environment)、预...
-API 闸道器和服务网格(来源:Liran Katz) 实施 API 闸道器以促进跨境通信;他们...