相信各位也看了 N 个介绍 MVVM 的文章了吧,不知道你们有没有觉得大家所描述的 MVVM 是不是有点不太一样呢?或是套用在你的专案後,实作中所遇到的问题跟网路教学差了十万八千里呢?或是在专案的初期,你看着你完美、设计好的 MVVM 架构,期待它永远都可以这麽完美下去,结果半年後被 PM 的需求搞的乱七八糟,自己都不认识它了,不再是你熟悉的那个 MVVM?没关系,遇到这些事情都很正常,请不要慌张,今天我介绍的 MVVM 也可能是一个新的 MVVM,所以,万一你正在思考什麽才是最“正确”的 MVVM 的话,我会建议你放下这个想法,因为本来就没有一个最“正确”的架构存在的必要,有解决到所遇到的问题才是重点,今天的 MVVM 将会针对这专案目前的范围去做设计,而在更之後的篇幅中,你也会看到因为需求而做的架构调整。
现在 Android 最主流的架构模式,就属於 MVVM 了,今天会从多层式架构为起点,来说明多层式架构的核心诉求,再带到 MVVM 这样的架构模式,以及为什麽这个架构模式非常适合本专案,最後,示范如何套用 MVVM 到便利贴这个专案上。
多层式架构的英文为 Multilayer architecture,又称 Multitier architecture。最主要的用途是将程序模组化,不同层级有着不同的责任,也有着不同的存取权限。在多层式架构中,最常被来使用的就属於三层式架构了,当然多层式架构可以不只三层,只是以一般的应用程序来说,这样的分层方式能完成大部分的功能,而且好管理。
如上图所示,三层式架构从上到下分别为:显示层、商业逻辑层、资料层
一般来说,显示层能直接对商业逻辑层进行操作,但是不能对资料层进行操作,也完全不知道资料层的存在。商业逻辑层不知道显示层的存在,但是能直接操作资料层,而资料层呢?就只能等待被别人使用了。所以三层式架构有一个从上而下的相依关系,上层使用下层,下层无法知道上层。
在某些使用案例中,可能不只有三层,也有可能因爲需求而需要跨层存取,第一层可以使用第三层或是第四层的物件,所以每一层到下面几层之间的存取并不是绝对的,如果因采用严格的限制而导致开发速度缓慢,或是没有一个合理的原因去做这样的限制的话,就不会是一个好的架构,就像前面所说的,好的架构应该要最大化工程师的生产力。
MVVM 总共是由三个部分组成 View, ViewModel, Model ,那这三个部分是不是就刚好可以跟三层式架构一一对应呢?很可惜的是这部分我觉得是有争议的。
上图撷取自 Wiki(https://en.wikipedia.org/wiki/Model–view–viewmodel) ,请注意右半边的 Model 部分,下面的说明是 BusinessLogic and Data,也就是说三层式架构的底下两层 - 商业逻辑层、资料层都是属於 MVVM 的 Model,左半边 View 跟 ViewModel 的部分则是强调资料绑定,藉由资料绑定,ViewModel 的任何资料更新都会自动的在 View 上面产生相对应的变化,因此任何对 ViewModel 的逻辑操作,也就是等於在对 View 进行操作,进而使得在这样的架构模式下写单元测试变得非常容易,因为 ViewModel 就是纯粹的资料类别,没有平台的相依性,同时 View 跟 ViewModel 之间也有一些显示的逻辑在这边处理。
以上这是最原始的定义,那我们在 Android 上采用时需要把这些定义原封不动的照搬过来吗?这边有几个问题我想跟大家讨论一下:
其实以上的问题都没有标准的答案,最後还是那一句“It depends”。但对我来说,我希望整个 App 的架构是越简单越好,在没有复杂的问题需要被解决的情况下,采用“过於严格”的定义往往没什麽好处,最後反而会花费太多时间在讨论一些对使用者或是对公司产品没有价值的事情。以现在我要做的 App 来说好了,便利贴应用程序就是一个充满 UI 互动的软件,对於这个 App 来说,什麽是商业逻辑,什麽是显示逻辑,似乎没有这麽容易的可以分清楚,那没有分清楚是一个大问题吗?目前也看不太出来,那就也不用花太多时间纠结在这上面。相对的,如果最後我要开发一个可以用来跑 Sprint 流程的看板应用程序, “Sprint 流程”本身就已经隐含了非常多规则在里面,所以在这情况下,分出商业逻辑层跟显示逻辑层是一件再正常也不过的事情。
以目前这个 App 的规模来说,分成三大组件就非常够用了:
那这三个组件组在一起就是完整的 MVVM 吗?ViewModel 跟 Model 中间的那条分层的线在哪里?我想,这个问题会依据不同的定义会有不同的答案,但是现在有没有回答这个问题也不是那麽的重要了,更好的问题会是:我们已经知道要在哪个地方写怎样的程序码了吗?这几个元件的职责已经够清楚了吗?
不知道各位心中是不是已经有答案了呢?没有也没关系,下面直接用程序来做示范吧!由於相依的顺序是从上到下,最下层是被别人使用的,所以就从最下层来介绍起吧!
今天需要的 gradle dependency:
implementation 'io.reactivex.rxjava3:rxjava:3.0.12'
implementation "androidx.compose.runtime:runtime-rxjava3:$compose_version"
目前只需要拿既有资料就好,所以该层的实作将会非常的简单。
interface NoteRepository {
fun getAll(): Observable<List<Note>>
}
class InMemoryNoteRepository(): NoteRepository {
private val allNotes = listOf(
Note.createRandomNote()
)
override fun getAll(): Observable<List<Note>> {
return Observable.just(allNotes)
}
}
在还没有资料的情况底下,我们暂时用随机的假资料来做替代,请注意这边回传的型别是 Observable
,所以我们已经预期这些资料将会随着时间而有所变动,一但资料产生改变,这里的 Observable
就会送出最新的资料给下游订阅的人。
由於目前没有任何的逻辑,也不需要转换资料格式,所以 ViewModel 这边也非常的简单,下一篇整合手势操作後才会有比较大的用途,不过我们已经很明确的知道这一个元件是需要的,就算看起来很笨也无妨。
class BoardViewModel(
private val noteRepository: NoteRepository
): ViewModel() {
val allNotes = noteRepository.getAll()
}
很单纯的呼叫 Repository 的 API 给外面的人使用,对 Android 开发已经很熟悉的你可能会有疑问,怎麽不用 LiveData 呢?答案将会在下面揭晓:
View 的部分其实在之前就已经完成的差不多了,让我们再复习一下:
@Composable
fun StickyNote(note: Note) {
Surface(
Modifier
.offset(x = note.position.x.dp, y = note.position.y.dp)
.size(108.dp, 108.dp),
color = Color(note.color.color),
elevation = 8.dp
) {
Column(modifier = Modifier
.padding(16.dp)
) {
Text(text = note.text, style = MaterialTheme.typography.h5)
}
}
}
我们已经有办法显示单张便利贴,但是从资料层传来的资料是一个 List ,所以我们需要做另外一个 View ,来显示多张便利贴。由於便利贴通常是贴在白板上,这边就将他命名为 BoardView
吧!
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rxjava3.subscribeAsState
@Composable
fun BoardView(boardViewModel: BoardViewModel) {
val notes by boardViewModel.allNotes.subscribeAsState(initial = emptyList())
Box(Modifier.fillMaxSize()) {
notes.forEach { note ->
StickyNote(note = note)
}
}
}
很自然而然的,把刚刚的 ViewModel 当作这个函式的参数传进来,还有为了要显示所有的便利贴,在下面,对 notes
这个 list 使用了一个 forEach
回圈来将他们全部画出来,这也正是 Jetpack Compose 令人着迷的地方之一,使用起来非常直觉友善。
剩下来的问题就是,notes
是怎麽来的了,这边有用到一个委派的语法: by 以及 subscribeAsState
这两个不认识的新东西,在 View 层的资料绑定就是由这两个元素组合而成的。首先,subscribeAsState
会将 Observable 转成另一种物件: State
。我们可以把他想像成这是一种 Observer 的实作方式,而且这边State
中的状态永远都会是最新的,最基本的使用方式是可以透过 State.value
来拿到现在状态中的值:
但是要是每次取值时都要使用State.value
也是挺麻烦的,这时候 by 就派出用场了,使用这个委派的语法,其实就是等於我在使用当下这个变数的时候 - 以目前的例子来说就是 notes
,也是等於对这个 State
取值,所以也可以暂时忽略这个型别的存在,在程序码的使用上比较简洁。
那为什麽不用 LiveData 转成 State 而是 Observable 呢?让我们再看一下 LiveData 要解决的问题是什麽:
依据官方文件的定义:LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
我们现在有看到任何 Activity、 Fragment 还是 Service 吗?没有,对吧?现在使用的全部都是 Composable function,那是不是代表我们没有生命周期的问题了?这也不对,因为 Composable function 跟 Android framework 之间还是有一些生命周期的事件要处理。但是这些我们已经不需要去烦恼了(至少已现在的需求来说),Jetpack Compose 自动帮我们解决这些问题,不用烦恼 Memory leak,不用自己控制资源回收的时机,一切交给 Jetpack Compose 的 API 吧!subscribeAsState
已经处理好生命周期的问题了,所以这时候如果在 ViewModel 硬要多一个转换到 LiveDate 的动作的话,就只是在浪费时间。除非...这个 ViewModel 除了 Composable function之外,还要跟其他的 Fragment 一起共用,那这时候使用 LiveData 就比较说得过去。
除了这三个组件之外,还有什麽其他的物件吗?当然有!像是 Activity ,以及建立这些组件的物件。一般来说都会认为 Activty 是属於 View 的一部分,但是有了 Jetpack Compose,Activity 就可以完全的抛弃这个职责,全部交给 Composable function 来处理就好,在这里,Activity 就只要做一件事就好,就是把所有必要相依的物件准备好,然後把这些相依的物件丢给 View ,剩下的一率不处理,在 Clean architecture 的书中的第 26 章: Main component
,就是我现在对 Activity 的定位。在 Main component
中,通常我还会使用 Dependency Injection 函式库帮忙做相依性管理,这边就随大家的喜好,用 Dagger2 或是 Koin 都可以,由於我对 Koin 比较熟悉,在之後的章节我会预设使用 Koin 来当作Dependency Injection 函式库。
今天会说明一下,实务上如何将 Open-Match svc endpoints,从 kubernet...
设定档 昨日说明关於使用者身分验证以及权限设定的部分加以说明,并且透过第三方插件的方式展现如何在do...
如标题,如果你突然想不开,想将Ubuntu下载成英文版的,那你一定会头疼该如何输入中文 因为英文介面...
SQL的资料类型转换分为隐性和显性转换,隐性转换即不必使用指定的转换函数,语句执行时资料库管理系统会...
这篇的Switch button与前几篇的ToggleButton很类似, 但ToggleButto...