目录:
1. MVx架构通常为什么不起作用
2. 如何解决MVx架构的三个问题?
3. 适用于应用程序核心的跨平台架构。简单,线性,可扩展
4. T函数:像连接逻辑到数据库一样连接到UI界面
我是一名安卓开发者。通常,人们会来找我,说“我们有一个新功能,你能帮我们实现吗?”并带着设计草图,就像这样。
我看这些并发现:这里有静态数据的屏幕,这里有需要动态获取的数据,还有交互组件。与这些组件交互时,有时只是打开另一个屏幕或组件,有时则是执行一些逻辑操作。基于这些,我设计功能逻辑的样子。我在架构中描述它,将其分解为具体的任务,找出在哪里以及如何进行服务器交互。
隐藏案例但后来我注意到,不是所有的过渡都像设计中那样简单,比如授权案例中的过渡就不简单。还有,返回导航的过渡也不那么明确。另外,有些情况下,根本就没有加载屏幕或显示空白状态的界面。
听起来挺熟悉的?
观察一下我意识到,我一直是以错误的方式看待新特性和应用。
我说的是什么意思?
从用户的角度来看,一个应用程序就是屏幕、互动的地方和等待应用完成任务。你看,其实即使在这一描述中,还有一个另一个角色——应用程序。为什么不换个角度看所有流程呢?
另一种方法 例子举个例子,让我们使用编程教材中的典型任务——CRUD操作,它代表创建(Create)、读取(Read)、更新(Update)和删除(Delete)。实际上,我们会进一步简化,只做创建、删除和显示这三个操作。
最开始我原本想做一个象棋游戏,但后来我发现这样做太复杂了,我们会一直卡在这个问题上。
我们的应用程序将是一个简单的项目列表:点击某个项目时,该项目就会被删除。在列表下方,有一个输入框和一个按钮,用于添加新项目。
下面来看看这种应用的布局是怎样的。
问题是,我们从哪里开始呢?一般来说,我们可以从任何部分开始,但我认为从核心功能开始会更合逻辑:列出项目。
不过,为什么我要显示它呢?
这是一个非常好的问题,因为只要程序可以自主做出决策,它就无需与用户互动。比如,决定如何在列表中找到并删除一个项目。但是,程序无法确定哪个项目需要被删除,至少在这种情况中是这样。所以我们把这个决定留给用户。
看这里:这里有一个删除功能。它接受一个列表和一个元素作为输入,从列表中移除这个元素,并返回更新后的列表。通常,这是一般的标准库函数,所以我们不会详细描述它的实现。我们只需要把它当作一个已知条件。
我们现在仍然是应用程序,有两个疑问:我们需要从哪里获取列表数据,又需要从哪里获取要删除的项数据?
虽然这个列表是我们拥有的东西,我们让用户决定删除哪一项。现在这一切可以整合成一个新的选择和移除功能。
在这里,我想让你注意我们如何顺利地从移除一个元素扩展到选择和移除它。如果你觉得这只是巧合,让我们再进一步:让我们再添加创建新元素的部分。
扩展性:增加一个组件所以,作为一款应用,我们可以往列表里添加一个项目,但我们现在还不知道这个项目具体是什么样子的或者它里面有什么。我们先从已知的信息开始吧。
添加新元素的函数签名方式与删除元素的函数签名方式类似,只是名称不同。动作当然也不同。毕竟这两个函数是相反的操作。😃
然后我们又遇到了同样的问题:“我们从哪里获取要添加的元素?”因为我们解决不了这个问题,我们将这个任务交给用户处理,并将它封装成一个“创建并添加”的工具。
但感觉这一切好像以前都做过。那么,说好的可扩展性呢?关键在于我们现在可以并行运行这两个程序,将它们结合成一个,无需改动任何一个。
通过将所有这些打包成一个循环,将每一步的结果传递给下一步,我们就得到了完成任务的完整方案。
我们现在有一个应用逻辑的描述,不依赖于任何特定的用户界面。这只是理论上的……
实现过程 逻辑学现在我们要把这描述变成代码,怎么做?
每个块都被转换成了一个函数。在我的情况中,它是一个Kotlin中的挂起函数,但这并不是必须的。我会在另一个场合详细解释为什么。
按照我们图从上到下逐步进行,我们依次实现“应用示例”、“创建或移除”、“选择并移除”和其他功能(如)。这些过程。
suspend fun <Item> exampleApp(items: List<Item>): Nothing {
updateLoop(items) {
createOrRemove(it)
}
}
suspend fun <Item> createOrRemove(items: List<Item>): List<Item> {
return parallel(
{ selectAndRemoveItem(items) },
{ createAndAdd(items) }
)
}
suspend fun <Item> selectAndRemoveItem(items: List<Item>): List<Item> {
val item = selectItem(items)
return removeItem(items, item)
}
suspend fun <Item> removeItem(items: List<Item>, item: Item): List<Item> {
return items - item
}
suspend fun <Item> createAndAdd(items: List<Item>): List<Item> {
val item: Item = createItem()
return addItem(items, item)
}
suspend fun <Item> addItem(items: List<Item>, item: Item): List<Item> {
return items + item
}
在这里我们得开始和用户互动了。
挂起函数 selectItem(items: List<Item>): Item {
TODO("待实现:与用户交互")
}
挂起函数 createItem(): Item {
TODO("待实现:与用户交互")
}
这是相对于我们逻辑的外部依赖。“外部”意味着我们需要从外部得到它。那我们该怎么做到这一点呢?
依赖项我建议将这些函数的调用方式描述为接口。让想运行我们程序的外部系统自己搞定这些接口的实现。
定义了一个名为 selectItem 的挂起函数,它接受一个项目的列表,并返回一个项目。
定义了一个名为 SelectItemDependencies 的接口,它包含一个名为 select 的挂起函数,该函数接受一个项目的列表,并返回一个项目。
定义了一个名为 createItem 的挂起函数,它返回一个新的项目。
定义了一个名为 CreateItemDependencies 的接口,它包含一个名为 create 的挂起函数,该函数返回一个新的项目。
但是现在,“外部系统”变成了调用的函数本身。
从调用函数的视角来看,我们现在需要实现这个接口或者从其他地方获取它。但是这样做,我们将自己紧紧绑定到特定的函数和接口上,而这对于调用函数来说,我是不需要的。我只关心函数签名,而不关心具体的实现内容。因此,我们可以像处理 selectItem
和 createItem
这样:将依赖抽象成一个接口中。然后,我们可以递归地应用这种方法,一直应用到 exampleApp
。
懸掛 fun <Item> ExampleAppDependencies<Item>.exampleApp(items: List<Item>): Nothing {
updateLoop(items) {
createOrRemove(it)
}
}
接口 ExampleAppDependencies<Item> {
懸掛 fun createOrRemove(items: List<Item>): List<Item>
}
懸掛 fun <Item> CreateOrRemoveDependencies<Item>.createOrRemove(items: List<Item>): List<Item> {
返回 平行(
{ selectAndRemoveItem(items) },
{ createAndAdd(items) }
)
}
接口 CreateOrRemoveDependencies<Item> {
懸掛 fun selectAndRemoveItem(items: List<Item>): List<Item>
懸掛 fun createAndAdd(items: List<Item>): List<Item>
}
懸掛 fun <Item> SelectAndRemoveItemDependencies<Item>.selectAndRemoveItem(items: List<Item>): List<Item> {
val item = selectItem(items)
返回 移除Item(items, item)
}
接口 SelectAndRemoveItemDependencies<Item> {
懸掛 fun selectItem(items: List<Item>): Item
懸掛 fun 移除Item(items: List<Item>, item: Item): List<Item>
}
懸掛 fun <Item> 移除Item(items: List<Item>, item: Item): List<Item> {
返回 从项目列表中移除单个项目(items, item)
}
懸掛 fun <Item> CreateAndAddDependencies<Item>.createAndAdd(items: List<Item>): List<Item> {
val item = createItem()
返回 添加Item(items, item)
}
接口 CreateAndAddDependencies<Item> {
懸掛 fun createItem(): Item
懸掛 fun 添加Item(items: List<Item>, item: Item): List<Item>
}
懸掛 fun <Item> 添加Item(items: List<Item>, item: Item): List<Item> {
返回 将项目添加到列表中(items, item)
}
懸掛 fun <Item> SelectItemDependencies<Item>.selectItem(items: List<Item>): Item {
返回 選擇項目(items)
}
接口 SelectItemDependencies<Item> {
懸掛 fun 選擇項目(items: List<Item>): Item
}
懸掛 fun <Item> CreateItemDependencies<Item>.createItem(): Item {
返回 創建新項()
}
接口 CreateItemDependencies<Item> {
懸掛 fun 創建新項(): Item
}
现在我们的功能已经如此独立于彼此,我们需要把它们重新整合成一个完整的程序。
这很简单。我们只需要实现这些接口,并把它们正确地结合起来。
val selectAndRemoveContext = object : SelectAndRemoveItem依赖<String> {
override suspend fun selectItem(items: List<String>): String {
// 此处进行UI交互
}
override suspend fun removeItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.removeItem(items, item)
}
val createAndAddContext = object : CreateAndAdd依赖<String> {
override suspend fun createItem(): String {
// 此处进行UI交互
}
override suspend fun addItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.addItem(items, item)
}
val createOrRemoveContext = object : CreateOrRemove依赖<String> {
override suspend fun selectAndRemoveItem(items: List<String>): List<String> =
selectAndRemoveContext.selectAndRemoveItem(items)
override suspend fun createAndAdd(items: List<String>): List<String> =
createAndAddContext.createAndAdd(items)
}
val exampleAppContext = object : 示例应用程序依赖<String> {
override suspend fun createOrRemove(items: List<String>): List<String> =
createOrRemoveContext.createOrRemove(items)
}
在这里,我只是一步步地实现每个接口,并在需要的地方插入函数调用。当然,我们可以讨论这个解决方案以及为什么我没有为添加和移除元素抽象出依赖关系,但最好在注释里或者在Patreon()或Telegram(电报)中讨论这些内容。
控制台用户界面最后,我们需要使用构建的依赖图作为执行exampleApp
的上下文来运行它。我还添加了一个消息队列和响应队列,这样用户就可以与应用程序进行交互了。
fun main() {
val outputFlow = MutableSharedFlow<String>(1)
val inputFlow = MutableSharedFlow<OneOf<String, Int>>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val selectAndRemoveContext = object : SelectAndRemoveItemDependencies<String> {
override suspend fun selectItem(items: List<String>): String {
outputFlow.emit(
items.withIndex().joinToString("\n") { (index, item) -> "$index. $item" })
return inputFlow.filterIsInstance<OneOf.Second<Int>>()
.mapNotNull { items.getOrNull(it.second) }
.first()
}
override suspend fun removeItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.removeItem(items, item)
}
val createAndAddContext = object : CreateAndAddDependencies<String> {
override suspend fun createItem(): String {
return inputFlow.filterIsInstance<OneOf.First<String>>()
.map { it.first }
.first()
}
override suspend fun addItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.addItem(items, item)
}
val createOrRemoveContext = object : CreateOrRemoveDependencies<String> {
override suspend fun selectAndRemoveItem(items: List<String>): List<String> =
selectAndRemoveContext.selectAndRemoveItem(items)
override suspend fun createAndAdd(items: List<String>): List<String> =
createAndAddContext.createAndAdd(items)
}
val exampleAppContext = object : ExampleAppDependencies<String> {
override suspend fun createOrRemove(items: List<String>): List<String> =
createOrRemoveContext.createOrRemove(items)
}
runBlocking {
launch(Dispatchers.Default) { exampleAppContext.exampleApp(emptyList()) }
outputFlow.collectLatest { text ->
println("项目列表:")
println(text)
print("请输入要删除的编号或要添加的名称: ")
val input = readln()
inputFlow.emit(
input.toIntOrNull()?.let { OneOf.Second(it) } ?: OneOf.First(input)
)
}
}
}
因此,从应用的角度来看,我们可以这样理解逻辑,为我们的应用编写可扩展、可连接的和可测试的代码。
移动端UI
哦,我确实承诺过一个移动应用程序。所以,我们这样来处理:将与用户交互的部分换成更合适的内容或组件。我称之为T功能。我在我的文章最后提到了这个基本想法。
你现在需要知道的是:它会发送一个“请求”并附带一个回调函数到一个通道,并等待接收方调用这个回调函数。这类似于我们平时与后端交互的方式。Android 开发者可能熟悉 Handler.replyTo 这种做法。
对象类 Logic
@Composable
@Preview
fun App(
selectItemsFlow: MutableStateFlow<UiState<List<String>, String>?> = MutableStateFlow(null),
createItemsFlow: MutableStateFlow<UiState<Unit, String>?> = MutableStateFlow(null),
) {
MaterialTheme {
// 警告:不要这么干!!!
// 逻辑不应该放在组合中!
// 理想情况下应该在 App() 之外运行逻辑,并将 selectItemsFlow 和 createItemsFlow 作为参数提供
// 逻辑应该独立于 UI 之外存在更长的时间
LaunchedEffect(Logic) {
val selectAndRemoveContext = object : SelectAndRemoveItemDependencies<String> {
override suspend fun selectItem(items: List<String>): String {
return selectItemsFlow.showAndGetResult(items)
}
override suspend fun removeItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.removeItem(items, item)
}
val createAndAddContext = object : CreateAndAddDependencies<String> {
override suspend fun createItem(): String {
return createItemsFlow.showAndGetResult(Unit)
}
override suspend fun addItem(items: List<String>, item: String): List<String> =
com.genovich.cpa.addItem(items, item)
}
val createOrRemoveContext = object : CreateOrRemoveDependencies<String> {
override suspend fun selectAndRemoveItem(items: List<String>): List<String> =
selectAndRemoveContext.selectAndRemoveItem(items)
override suspend fun createAndAdd(items: List<String>): List<String> =
createAndAddContext.createAndAdd(items)
}
val exampleAppContext = object : ExampleAppDependencies<String> {
override suspend fun createOrRemove(items: List<String>): List<String> =
createOrRemoveContext.createOrRemove(items)
}
exampleAppContext.exampleApp(emptyList())
}
Column {
val selectItems by selectItemsFlow.collectAsState()
selectItems?.also { (items, select) ->
LazyColumn(
modifier = Modifier.weight(1f),
reverseLayout = true,
) {
items(items.asReversed()) { item ->
Text(
modifier = Modifier
.fillMaxWidth()
.clickable { select(item) }
.padding(16.dp),
text = item,
)
}
}
}
val createItem by createItemsFlow.collectAsState()
createItem?.also { (_, create) ->
Row(Modifier.fillMaxWidth()) {
var value by remember(create) { mutableStateOf("") }
TextField(
modifier = Modifier.weight(1f),
value = value,
onValueChange = { value = it },
)
Button(
onClick = { create(value) },
) {
Text(text = "创建")
}
}
}
}
}
值得分别讨论UI中事件流的规模和构成,但我已经说了很多了。
结束。我想强调的主要观点是从应用程序的角度而不是从用户界面的角度设计逻辑。这表明逻辑会变得多么紧密而有条理。此外,当每个功能在函数级别而不是对象级别与依赖项解耦时,这在灵活性、可测试性和可扩展性方面具有多么高的表现。
我认为我们可以更详细地讨论这些方面。目前感谢您的聆听。下回见!
附注:这是在这篇文章里做的项目的项目链接,还有包含的实用功能的项目的项目GitHub仓库。
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章