深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
可信指从数据源中获取的数据是 最新的
、完整的
、可靠的
,否则是不可信的,我们没有理由在编码中使用不可信的数据源。
单一是指这样的数据源仅一个。
在经典设计中,其内涵如下图:
- 按照视图的 所有的 内容状态,定义一个不可变的
ViewState
- 按照业务初始化 ViewState 实例
- Model业务生成驱动 ViewState变化的Result
- 计算出新状态,Reduce(Pre-ViewState,Result) -> New-ViewState
- 更新数据源
- View层消费ViewState
借助于数据绑定框架,可以很方便地解决视图更新的问题。
想象一下,此时页面UI非常复杂……
如果僵化的信奉这样的 单一
,情况会如何呢?
- 复杂(大量属性)的ViewState
- 复杂的UI更新计算,e.g. 100个属性变了2个,依然需要计算98个属性未变或者全量强制更新
在 APP-A和APP-B中,我分别使用了 DataBinding和Compose,但均无法避免该问题。
何为单一
从机器执行程序的原理上看,我们无法实现 多个内容一致的数据源 在 任意时刻 满足 最新的
、可靠的
。
将视图视为一个整体,规定它只拥有 一个 可信的数据源。在此基础上看局部的视图,它们也顺其自然地仅拥有一个可信的数据源。
反过来看,当任意的局部视图仅具有一个可信数据源时,整体视图也仅有一个逻辑上的可信数据源。
据此,我们可以对 经典MVI实现
进行一定程度的改造,将ViewState进行局部分解,使得UI绑定部分的业务逻辑更 清晰、干净。
请注意,复杂度不会凭空消失,我们为了让 “UI绑定的业务逻辑更清晰、干净”、“更新UI的计算量更少”,将复杂度转移到了ViewState的拆分。拆分后,将具有 多个视图部件的单一可信数据源,注意,为了不引起额外的麻烦、并且便于维护扩展,建议遵守以下条件:
- 基于业务需求,组合数据源形成新数据源
- 不在数据源的逻辑范围之外进行数据源组合操作
举个虚拟的例子:用户需要实名认证 且 关注博主 ,才在界面上显示某功能按钮。下面使用代码分别演示。
考虑到RxJava的广泛度依旧高于Kotlin-Coroutine+flow,数据流的实现采用RxJava
注意,考虑到读者可能会编写demo做UDF局部的验证,下文中的代码以示例目的为主,兼顾编写场景冒烟的方便性,流的类型不一定是构建完整UDF的最佳选择。
经典实现
在经典MVI实现中,需要先定义ViewState
data class ViewState(
/unique id of current login user/
val userId: Int,
/true if the current login user has complete real-name verified/
val realNameVerified: Boolean,
/true if the current login user has followed the author/
val hasFollowAuthor: Boolean
) {
}
并定义ViewModel,创建ViewState流,忽略掉其初始化和其他部分
class VM {
val viewState = BehaviorSubject.create()
//ignore
}
并定义View层,忽略掉其他部分,简单起见暂时不使用数据绑定框架
class View {
private val vm = VM()
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
vm.viewState.subscribe {
render(it)
}
}
private fun render(state: ViewState) {
imgRealNameVerified.isVisible = state.realNameVerified
cbHasFollowAuthor.isChecked = state.hasFollowAuthor
someButton.isVisible = state.realNameVerified && state.hasFollowAuthor
//ignore other
}
}
在JS中,JSON并不能附加逻辑,基本等价于Java中的POJO,故在数据源外部处理简单逻辑的情况较为常见。而在Java、Kotlin中可以进行适当的优化,适当封装,使得代码更加干净便于维护:
data class ViewState(
//ignore
) {
fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}
class View {
//ignore
private fun render(state: ViewState) {
//…
someButton.isVisible = state.isSomeFuncEnabled()
}
}
拆分实现
依旧先定义逻辑上完整的ViewState:
class ComposedViewState(
/unique id of current login user/
val userId: Int,
) {
/**
- real-name-verified observable subject,feed true if the current login user has complete real-name verified
- */
val realNameVerified = BehaviorSubject.create()
/**
- follow-author observable subject, feed true if the current login user has followed the author
- */
val hasFollowAuthor = BehaviorSubject.create()
val someFuncEnabled = BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }
}
定义ViewModel,子模块数据流均已定义,故而无需再定义全ViewState的流
class VM(val userId: Int) {
val viewState = ComposedViewState(userId)
//ignore
}
编写View层的UI绑定,同样简单起见,不使用数据绑定框架
class View {
private val vm = VM(1)
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
bindViewStateWithUI()
}
private fun bindViewStateWithUI() {
vm.viewState.realNameVerified.subscribe {
renderSection1(it)
}
vm.viewState.hasFollowAuthor.subscribe {
renderSection2(it)
}
vm.viewState.someFuncEnabled.subscribe {
renderSection3(it)
}
//…
}
private fun renderSection1(foo:Boolean) {
imgRealNameVerified.isVisible = foo
}
private fun renderSection2(foo:Boolean) {
cbHasFollowAuthor.isChecked = foo
}
private fun renderSection3(foo:Boolean) {
someButton.isVisible = foo
}
}
例子较为简单,在实际项目中,如果遇到复杂页面,则可以分块进行处理。
注意:实际情况中,并没有必要将每一个子数据源拆分到一个View级别的控件,那样过于啰嗦,例子因非常简单而无法丰满起来。 e.g. 针对每一块视图区,例如作者区域,定义子ViewState类,创建其数据流即可。
作者按:务必评估,在一次Model业务产生的Result中,会引起数据流下游的更新次数。 为避免产生不可预期的问题,可通过类似以下方式,使下游响应次数表现和经典实现的情况一致。
额外定义PartialChange流或者功能等价的流,它用于标识 reduce
计算的开始和结束,可以将此期间的数据流的变化延迟到最后发送终态
更加推荐定义功能上等价的流
class ComposedViewState(
/unique id of current login user/
val userId: Int,
) {
internal val changes = BehaviorSubject.create()
//ignore
val someFuncEnabled =
BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }.sync(PartialChange.Tag, changes)
}
inline fun <reified T, S> Observable.sync(tag: S, sync: BehaviorSubject): Observable {
return BehaviorSubject.combineLatest(this, sync) { source, syncItem ->
if (syncItem == tag) {
syncItem
} else {
source
}
}.filter { it is T }.cast(T::class.java)
}
修改PartialChange,为reduce函数添加边界:
PartialChange是Model产生的Result的表现物,封装了ViewState的reduce函数逻辑,即如何从 Pre-ViewState 生成 新 ViewState
sealed class PartialChange {
open fun reduce(state: ComposedViewState) {
}
/**
- 同步标记,从头开始到真实PartialChange之间,流的状态生效
- */
object Tag : PartialChange()
object None : PartialChange()
class Foo(val a: Boolean, val b: Boolean) : PartialChange() {
override fun reduce(state: ComposedViewState) {
state.changes.onNext(Tag)
state.realNameVerified.onNext(a)
state.hasFollowAuthor.onNext(b)
state.changes.onNext(this)
}
}
}
要想优雅,需要工具
采用响应式流,避免命令式编码
想来这一点已不需要多做解释。
在Android中,存在 LiveData
组件,它通过简单的方式封装了可观测的数据,但实现方式简单也限制了它的功能 不够强大 。因此,建议使用 RxJava
或者 Kotlin-Coroutine & flow
构建数据流。
本节便不再展开。
采用数据绑定框架
采用 jetpack-compose
或者 DataBinding
均可以移除枯燥的UI命令式逻辑,在APP-A中我使用了DataBinding,在APP-B中我使用了Compose。
在 ViewState的代码很棒时,均可以获得优秀的编程体验,从啰嗦的UI中解放出来。
作者的个人观点:
关于Compose。Compose依旧属于较新的事物,在商业项目中使用存在学习门槛和造轮工作。在目标用户具有较高容忍度的情况下,已然可以进行尝试。
关于DataBinding。一个近乎毁誉参半的工具,关于它的批判,大多集中于:xml中实现的逻辑难以阅读、维护,这实际上是对DataBinding设计的误解而带来的错误使用。
DataBinding本身具有生成VM层的功能,但这一功能并不足够强大,且没有完善的使用指导,而在官方Demo中过度宣传了它,导致大家认为DataBinding就该这样使用。
仅使用基础的数据绑定功能、和Resource或者Context有关的功能(例如字符串模板)、组件生命周期绑定等,适度自定义绑定。
何为状态、何为事件。最后的一公里
首先区别于上文提到的UI事件,这里的状态和事件均产生于数据流的末段,而UI事件处于数据流的首段。
UI事件属于:A possible action that the user can perform that is monitored by an application or the operating system (event listener). When an event occurs an event handler is called which performs a specific task
在展开之前,先用一张图回顾总结上文中对于 单向数据流
& 单一可信数据源
的知识
在 单向数据流动 章节中,提到了MVI的UDF设计:
- 系统捕获的UI事件、其他侦听事件(例如熄屏、应用生命周期事件),生成Intent,压入Intent流中
- ViewModel层中筛选、转换、处理Intent,实际是使用Model层业务,产生业务结果,即PartialChange
- PartialChange经过Reducer计算处理得到最新的ViewState,压入ViewState流
- View层(广义的表现层)响应并呈现最新的ViewState
在 单一可信数据源 章节中,提到View层应当采用 单一可信数据源
在这张图中,我们仅体现了 状态
即 ViewState。
关于GUI程序的认知
在展开前,先聊点理念上的内容。请读者诸君思考下自己对于GUI程序的认知。
作者的理解:
程序狭义上是计算机能识别和执行的一组指令集,编程工作是在程序世界对
客观实体
、业务逻辑
进行 建模和逻辑表达。而GUI程序拥有
用户图形界面
, 除了结合硬件接收用户交互输入外,可以将程序世界中的模型
以用户图形界面
等方式表现给用户。表现出来的内容代表着客观实体
其本质目的在于:通过 描述特征属性 、 描述变化过程 等方式让用户感知并理解
客观实体
而除了通过 程序语言描述 、 程序世界模拟展现 外,同样可以通过 自然语言描述 达到目的,这也是产品经理的工作。
当然,产品经理往往需要借助一些工具来提升自己的自然语言表达能力,但无奈的是能用数学公式和逻辑推演表达需求的产品经理太少见了。
写这段只是为了引入 他山之石
。
First-Order logic
在数学、哲学、语言学、计算机科学中,有一个概念 First-Order logic
,无论是产品需求还是计算机程序,都可以建立FOL表达。
当然,本篇不讨论FOL,那是一个很庞大且偏离主题的事情。我仅仅是想借用其中的概念。
FOL表达 Event或者State时:
- Event 体现的是特定的变化
- State 体现的是客观实体在任意时刻都适用的一组情况,即一段时间内无变化的条件或者特征
不难理解,变化是瞬时的,连续的变化是可分的。
但在人机交互中,瞬时意义很小,我们的目的在于让用户感知。
例如:“好友向你发送了一条消息的场景中”,消息抵达就是Event,它背后潜藏着 “消息数的变化”、“最新消息内容的变化” 等。 在常见的设计中:
- 应用需要弹出一个气泡通知用户这一事件
- 应用需要更新消息数,消息列表内容等,以呈现出最新的State
而为了让用户感知到,气泡呈现时长并不是瞬时的,但在产品交互设计中依旧将其定义为事件。
分离状态和事件,不是吃饱撑得
看山是山、看水是水
此时此刻,答案已经很明显。
在通用的产品设计中,状态和事件有不同的意义,如果程序中不分离出两者,则必然是自找麻烦,这是公然挑衅 面向对象编程
的行为。如果不明确定义不同的Class,则势必导致代码混乱不堪,毕竟这是违背编程原则的事情。
在大多MVVM设计中,状态和事件未分家,导致bug丛生,这一点便不再展开。
如何区分Event和State
State是一段时间内无变化的条件或者特征,它天然的 契合 了位于表现层的主体内容所对应的 数据模型特征。
Event是特定的变化,它在表现层体现,但与State的生命周期不一致,且并无一一对应的关系。
基于经验主义,我们可以机械地、笼统地认为:页面主体静态内容所需要的数据属于State范畴,气泡提醒等短暂的物体所需要的数据属于Event范畴。
从逻辑推演的角度出发,进行 等价逻辑推断 和 条件限定下的逻辑推断 ,一定序列的Event可以模型转换为State。
事件粘性导致重复?只是框架设计的bug
看山不是山,看水不是水
前面提到,State是一段时间内无变化的条件或者特征,所以在程序设计中State具有粘性的特征。
如果Event也设计出这样的粘性特征并造成重复消费,明显是违背需求的,无疑是框架设计的Bug。此问题在各大论坛中很常见。
注意,我们无法脱离实际需求去二元化的讨论事件本身该不该有粘性特征,只能结合实际讨论框架功能是否存在bug
如果要实现以力破法,在框架设计层面上 Event体系的设计要比State体系要复杂 。因为从交互设计上:
- State 只需要考虑呈现的准确性和及时性,除去美观、可理解性等等
- Event 需要考虑准确性、优先级、及时性、按条件丢弃等等,除去美观、可理解性等等
举个例子:网络连接问题导致的Web-API调用失败需要使用Toast提示网络连接失败
不难想象:
- 可能一瞬间的断开网络连接,会导致多个连接均返回失败
- 可能连接问题未修复,10秒前请求失败,当前请求又失败了
难道连续弹出吗?难道和上一次Event一致就不消费吗?…
或许您会使用一些 剑走偏锋的技巧
来解决问题,但技巧总是建立在特定条件下生效的,一旦条件发生变化,就会带来烦恼,您很难控制上游的PM和交互设计师。
所以在框架层面需要针对产品、交互设计的泛化理念,设计准确的、灵活的Event体系。
准确的、灵活的Event体系
看山还是山,看水还是水
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
前请求失败,当前请求又失败了
难道连续弹出吗?难道和上一次Event一致就不消费吗?…
或许您会使用一些 剑走偏锋的技巧
来解决问题,但技巧总是建立在特定条件下生效的,一旦条件发生变化,就会带来烦恼,您很难控制上游的PM和交互设计师。
所以在框架层面需要针对产品、交互设计的泛化理念,设计准确的、灵活的Event体系。
准确的、灵活的Event体系
看山还是山,看水还是水
[外链图片转存中…(img-A0D5bQSo-1715486148617)]
[外链图片转存中…(img-IE44v54z-1715486148618)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!