Re-architect - StickyNoteView

上一次我们完成了 ContextMenu 的部分,ContextMenu 也有了属於自己的 ViewModel,架构图如下:

417238B4-35BB-457B-8BCA-0E0E4E32F841.jpg

今天我们将要完成 Re-architect 的最後一哩路,StickyNoteView 的 ViewModel!

实做各别更新

在开始之前先复习一下接下来我将要做的事:ViewPort 将只会知道每一个 StickyNote 的 Id,不会有完整的 Model,所以 ViewPort 将会控制所有 StickyNoteView 的建立与删除,当然,在 Jetpack Compose 的世界中,这些事情都很简单,只要使用一个 For 回圈来建立所有应该建立的 StickyNoteView 就好了,剩下的新增以及删除机制交给内建的 Diff 与 Composition 机制去处理即可。接下来,拥有 id 的 StickyNoteView 就可以使用这个 id 去拿相对应的 ViewModel 就可以实现各别更新了,以下是 ViewPortView 的程序码:

@Composable
fun ViewPortView(
    noteIds: List<String>
) {
    Box(Modifier.fillMaxSize()) {
        noteIds.forEach { id ->
            key(id) { // [1]
                StatefulStickyNoteView(
                    modifier = Modifier.align(Alignment.Center),
                    id = id
                )
            }
        }
    }
}

超级简短,对吧?这个 ViewPortView 的参数只有 noteIds ,使用 For 回圈就可以一个一个拿到这些便利贴的 id ,接着使用这个 id 去建立 StatefulStickyNoteView ,至於 StickyNoteView 为什麽是 Stateful 的呢?请回忆昨天的内容,StickyNoteView 的情况跟 ContextMenuView 是一样的!

另外这个函式中还有一个需要解说的地方 [1]:key 可以使得这个 Composable function 在 Recompose 的时候记住之前呼叫过的内容,只要 key 里面的 id 是一样的,就不会重新执行 key{} 里面的内容,也就是说这样的操作等於是在执行两个 List 的 Diff ,当 noteIds 的内容改变时,只会针对有新增或是删除的 id 去做相对应的动作:

有 key

        noteIds      |       Recomposition 行为
["A", "B", "C", "D"] => 建立 "A", "B", "C", "D" 四个 StatefulStickyNoteView
["A", "B", "D"]      => 对 id 是 "C" 的 StatefulStickyNoteView 做回收
["A", "B", "D", "E"] => 建立 id 是 "E" 的 StatefulStickyNoteView

没 key

        noteIds      |       Recomposition 行为
["A", "B", "C", "D"] => 建立 "A", "B", "C", "D" 四个 StatefulStickyNoteView
["A", "B", "D"]      => 建立 "A", "B", "D" 三个 StatefulStickyNoteView,并回收之前所有的 StatefulStickyNoteView
["A", "B", "D", "E"] => 建立 "A", "B", "D", "E" 四个 StatefulStickyNoteView,并回收之前所有的 StatefulStickyNoteView

取代 allNotes

ViewPortView 这边完成之後,我们就可以拿掉之前在 Domain 层以及 ViewModel 层的 allNotes 了,使用 allVisibleNoteIds 来取代,於是 ViewPortView 就可以拿到这些 id 了:

@Composable
fun CoEditorScreen(viewModel: CoEditorViewModel) {
//  val allNotes by viewModel.allNotes().subscribeAsState(initial = listOf())
    val noteIds by viewModel.allVisibleNoteIds.subscribeAsState(initial = listOf())
    
    ...
    
   ViewPortView(noteIds)

    ...
}

class CoEditorViewModel(coEditor: CoEditor) {
//  val allVisibleNoteIds: Observable<List<Note>> = coEditor.allNotes
    val allVisibleNoteIds: Observable<List<String>> = coEditor.allVisibleNoteIds
}

class CoEditor() {
//  val allNotes: Observable<List<Note>> = noteRepository.getAllNotes()
    val allVisibleNoteIds: Observable<List<String>> = noteRepository.getAllVisibleNoteIds()
}

StatefulStickyNoteView

@Composable
fun StatefulStickyNoteView(
    id: String,
    modifier: Modifier = Modifier,
) {
    val stickyNoteViewModel by LocalViewModelStoreOwner.current!!.viewModel<StickyNoteViewModel>()
    val onPositionChanged: (Position) -> Unit = { delta ->
        stickyNoteViewModel.moveNote(id, delta)
    }
    val note by stickyNoteViewModel.getNoteById(id).subscribeAsState(initial = StickyNote.createEmptyNote(id))
    val selected: Boolean by stickyNoteViewModel.isSelected(id).subscribeAsState(false)

    StickyNoteView(
        modifier = modifier,
        onPositionChanged = onPositionChanged,
        onClick = stickyNoteViewModel::tapNote,
        stickyNote = note,
        selected = selected)
}

以上是 StatefuleStickyNoteView ,基本上没有什麽新东西,但如同之前说的,StickyNoteView 有一个属於自己的 ViewModel ,但是很可惜的每个便利贴都需要共用同一个 ViewModel ,这是因为现在的 ViewModelStoreOwner 永远都会是同一个,所以一但建立了第一个 ViewModel,之後将会共用同一个。因为这样的限制,所以 id 目前不会绑定在 ViewModel 里,而且这个 ViewModel 也因此不应该拥有自己的状态,不然会有状态管理上的问题。

StickyNoteViewModel

class StickyNoteViewModel(
    private val coEditor: CoEditor
): ViewModel() {

    fun moveNote(noteId: String, positionDelta: Position) {
        coEditor.moveNote(noteId, positionDelta)
    }

    fun tapNote(stickyNote: StickyNote) {
        coEditor.selectNote(stickyNote.id)
    }

    fun getNoteById(id: String) = coEditor.getNoteById(id)

    fun isSelected(id: String): Observable<Boolean> {
        return coEditor.selectedNote
            .map { optNote ->
                optNote.fold( // [1]
                    someFun = { note -> note.id == id},
                    emptyFun = { false }
                )
            }
    }
}

有了这个 ViewModel , CoEditorViewModel 的负担再次的减轻了,一些比较属於单个便利贴的行为放到了这个类别:moveNotetapNoteisSelected 。这边的实作也都是相对的简单,除了 [1] 之外,fold 是我自己替 Optional 写的 extension function,fold 操作在 functional programming 是很常见的,他的作用可以让我们一次写完所有可能的条件处理,如果对 functional programming 不是很熟的读者,你可以把它想像成是 streaming style 的 when ,该实做如下:

fun <T, R> Optional<T>.fold(someFun: (T) -> R, emptyFun: () -> R): R {
    return if (this.isPresent) {
        someFun(this.get())
    } else {
        emptyFun()
    }
}

FirebaseNoteRepository

到目前为止 view 层完成了,但是 Repository 还是使用以前的方式,必须要换成符合新架构的实作,这边其实就比较偏向是技术面的实作,所以我不打算多加解说,但要注意一点:因为动态的新增以及删除单张便利贴将导致相对应的 listener 以及 Observable 的生命周期会有变化, 所以要好好回收用不到的 listener 以及 Observable。

程序码连结:https://github.com/hungyanbin/ReactiveStickyNote/blob/DDD_implementation/app/src/main/java/com/yanbin/reactivestickynote/data/FirebaseNoteRepository.kt

Note → StickyNote

最後一步了!由於之前设计的 model 我们都称呼它为 Note ,但是为了名称的一致性,将它重新命名为 StickyNote 比较好。

小结

经过漫长的过程,到这部分我们终於真正的完成所有的 Re-architect 了,这边再次强调过去这几篇所说的“小步快跑”,所有的重新架构过程都是有经过规划的,并不是随意的下手,在这中间的过程中其实可以看到应用程序随时处於一个可以运行的状态,不会因为要“大规模的修改”,而让应用程序整个打掉重练,希望经由分享这样的案例,可以让各位读者对於架构要“大规模的修改”有着不一样的想像,作为一个开发人员,确保应用程序随时处於一个可执行的状态才是我们专业的表现!

到目前为止的所有专案程序码,可以在这个 github repository 中的 branch: DDD_implementation 中找到:https://github.com/hungyanbin/ReactiveStickyNote/tree/DDD_implementation


<<:  网页表格-30天学会HTML+CSS,制作精美网站

>>:  Day.26 Binary Search Tree IV

【第二十三天 - XSS Lab(2)-1】

Q1. XSS Lab(2)-1 建议也可以看 XSS Lab(1) 文章,alert() 与 pr...

[Day02] 变数

变数 变数是用来储存资料和进行基本运算的基本单位;在宣告时给资料一个名称,名称像一个盒子把资料装起来...

【Day 15】- 汇率什麽的。爬! (实战汇率爬虫 on chrome)

前情提要 前一篇带各位实作了爬取 Ubuntu ISO 映像档的爬虫,并存在 JSON 档。 开始之...

Day 06-Visual Studio 2019下载教学+初步建立chatbot专案

之前介绍了几天关於架设Bot的服务器,那接下来我们用程序写Bot并放上云端服务器呢? 目前我选择了先...

Day25 Redis架构实战-Sentinel选取Replica机制

Replica选择切换机制 先剔除不健康的Replica Replica与Master失去连线时间,...