假设你的Android工程里有这样一个ViewModel
kotlin
class MyViewModel {
val name by mutableStateOf("")
}
要想支持ViewModel在鸿蒙端复用,我们需要实现两个目标:
- 在Compose中,
name的变化会自动触发UI 更新。 - 在鸿蒙中,
name的变化会自动触发ArkUI 更新。
为此,可以将 mutableStateOf 变为跨平台实现, 即抽象出来一个状态层。
提供 expect fun platformMutableStateOf<T>(initValue: T): MutableState<T>。
在Android上的实现:
kotlin
acutal fun platformMutableStateOf<T>(initValue: T) : MutableState<T> {
return mutableStateOf(initValue)
}
在鸿蒙上的实现:
kotlin
acutal fun platformMutableStateOf<T>(initValue: T) : MutableState<T> {
return HarmonyMutableState(initValue)
}
因此,想要实现跨平台复用ViewModel, 核心在于 HarmonyMutableState 的实现。
HarmonyMutableState
要想实现ArkUI 可感知的状态,那就要使用鸿蒙的那套状态组件(@Local, @ObservedV2, @Trace ...)。
直接用注解显然不方便,好在鸿蒙在@kit.ArkUI中提供UIUtils工具:
UIUtils.makeObserved能将普通对象转为状态对象UIUtils.getTarget能得到状态对象对应的原始对象
对UIUtils.makeObserved返回的状态对象的写入,可以触发ArkUI的更新,因此,HarmonyMutableState可以这样实现:
kotlin
class HarmonyState<T>(var data: T)
class PlatformMutableState<T> constructor(initValue: T) : MutableState<T> {
private lateinit var origin: HarmonyState<T>
private var rawData = initValue
override var value: T
get() {
if (!::origin.isInitialized) {
origin = ArkUI.UIUtils.makeObserved(HarmonyState(rawData))
}
if (origin.data == null) {
return origin.data
}
return return origin.data.let { ArkUI.UIUtils.getTarget(it as Any) as T }
}
set(value) {
if (::origin.isInitialized) {
origin.data = value
}
rawData = value
}
override fun component1(): T {
return value
}
override fun component2(): (T) -> Unit {
return { value = it }
}
override var jsValue: T
get() = value
set(value) {
this.value = value
}
}
这里需要注意一下 value get 方法的返回值其实是通过UIUtils.getTarget拿到的原始对象。为什么要这样做,有两个原因:
UIUtils.makeObserved返回的是包装对象,并不是原对象,如果参与比较,会出现比较时不相等的情况,这和Compose的State行为是不同的。
ini
class Obj
cal obj = Obj()
val objState = PlatformMutableState(obj)
print(obj == objState.value) // Android 上打印true, Harmony上打印false
UIUtils.makeObserved包装的结果,内部嵌套对象的属性也会引起ArkUI的更新,这和Compose State 的行为不一致,看个例子:
scss
class NestedObj(var text: String)
class Obj{
val nestedObj = NestedObj("Hello")
}
cal obj = Obj()
val objState = PlatformMutableState(obj)
//Compose UI
Text(objState.nestedObj.text) // Hello
.onClick({
objState.nestedObj.text = "World" // 点击不会触发UI更新
})
//ArkUI
Text(objState.nestedObj.text) // 先显示Hello,点击后变成 World
.onClick(() => {
objState.nestedObj.text = "World"
})
为了解决这个问题,我们需要用UIUtils.getTarget返回原始对象,保证Harmony State 和 Compose State的行为一致,这样ViewModel内的逻辑才能100%复用。
到这里,你以为就万事大吉搞定了嘛?No!并没有,当你有一天在异步线程读取PlatformMutableState的value, 你会发现crash了。。。原因是子线程不支持调用UIUtils.makeObserved方法。
为了解决它,你需要判断当前是否是子线程,是子线程就不去初始化State, 因此需要这样改get方法:
kotlin
get() {
+ if (Process.pid != Process.tid) {
+ return rawData
+ }
if (!::origin.isInitialized) {
origin = ArkUI.UIUtils.makeObserved(HarmonyState(rawData))
}
if (origin.data == null) {
return origin.data
}
return origin.data.let { ArkUI.UIUtils.getTarget(it as Any) as T }
}
OK! 至此,大功告成!!!
所以最终的版本,看起来是这样的:
kotlin
class HarmonyState<T>(var data: T)
class PlatformMutableState<T> constructor(initValue: T) : MutableState<T> {
private lateinit var origin: HarmonyState<T>
private var rawData = initValue
override var value: T
get() {
// pid != tid 说明不是主线程
if (Process.pid != Process.tid) {
return rawData
}
if (!::origin.isInitialized) {
origin = ArkUI.UIUtils.makeObserved(HarmonyState(rawData))
}
if (origin.data == null) {
return origin.data
}
return origin.data.let { ArkUI.UIUtils.getTarget(it as Any) as T }
}
set(value) {
if (::origin.isInitialized) {
origin.data = value
}
rawData = value
}
override fun component1(): T {
return value
}
override fun component2(): (T) -> Unit {
return { value = it }
}
override var jsValue: T
get() = value
set(value) {
this.value = value
}
}
我们在kotlin中使用了鸿蒙的API,因此需要导出:
kotlin
@JsModule("@ohos.arkui.StateManagement")
external class ArkUI {
class UIUtils {
companion object {
fun <T : Any> getTarget(source: T): T
fun <T : Any> makeObserved(source: T): T
}
}
}
@JsModule("@ohos.process")
external class Process {
companion object {
val pid: Int
val tid: Int
}
}
不知道细心的你有没有注意到,我们只在valueget的时候去初始化State。 这是因为逻辑上,"写"是为了触发"读"的地方的刷新,因此,需要先"读"过,写State才有意义。没"读"过,可以推断,这个State 100%不会触发UI刷新,因此也就没有必要向State里写。(State的读写在鸿蒙上涉及反射,需要尽量减少不必要的读写)
相同的道理,我们可以实现对应的MutableFloatState, MutableIntState, MutableLongState,这里就不再赘述了。
HarmonyMutableStateList
关于mutableStateListOf的适配,整体思路和mutableStateOf一致,但是需要特别注意,我们需要在适当的地方调用UIUtils.getTarget 以及重写迭代器,防止代码中 map, fliter 等操作后,出现对象比较不相等的问题。这列直接贴代码:
kotlin
class HarmonyMutableStateList<T>(val elements: Array<out T>) : MutableList<T> {
private lateinit var origin: ArrayList<T>
override val size: Int
get() {
initIfNeed()
return origin.size
}
private fun initIfNeed() {
if (!::origin.isInitialized) {
origin = ArkUI.UIUtils.makeObserved(ArrayList(elements.toMutableList()))
}
}
override fun contains(element: T): Boolean {
initIfNeed()
val elementUnWrap = unWrap(element)
return origin.find { unWrap(it) == elementUnWrap } != null
}
override fun add(element: T): Boolean {
initIfNeed()
return origin.add(element)
}
override fun add(index: Int, element: T) {
initIfNeed()
return origin.add(index, element)
}
override fun addAll(elements: Collection<T>): Boolean {
initIfNeed()
return origin.addAll(elements)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
initIfNeed()
return origin.addAll(index, elements)
}
override fun clear() {
initIfNeed()
origin.clear()
}
override fun get(index: Int): T {
initIfNeed()
return origin.get(index).let { unWrap(it) }
}
override fun isEmpty(): Boolean {
initIfNeed()
return origin.isEmpty()
}
override fun iterator(): MutableIterator<T> {
initIfNeed()
return HarmonyIteratorImpl()
}
override fun listIterator(): MutableListIterator<T> {
initIfNeed()
return HarmonyListIteratorImpl(0)
}
override fun listIterator(index: Int): MutableListIterator<T> {
initIfNeed()
return HarmonyListIteratorImpl(index)
}
override fun removeAt(index: Int): T {
initIfNeed()
return origin.removeAt(index).let { unWrap(it) }
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
initIfNeed()
return origin.subList(fromIndex, toIndex)
}
override fun set(index: Int, element: T): T {
initIfNeed()
return origin.set(index, element).let { unWrap(it) }
}
override fun retainAll(elements: Collection<T>): Boolean {
initIfNeed()
val list = ArrayList<T>()
elements.forEach {
if (!contains(it)) {
list.add(it)
}
}
return removeAll(list)
}
override fun removeAll(elements: Collection<T>): Boolean {
initIfNeed()
var removedAny = false
elements.forEach {
if (remove(it)) {
removedAny = true
}
}
return removedAny
}
override fun remove(element: T): Boolean {
initIfNeed()
val elementUnWrap = unWrap(element)
val index = origin.indexOfFirst { elementUnWrap == unWrap(it) }
if (index != -1) {
origin.removeAt(index)
}
return index != -1
}
override fun lastIndexOf(element: T): Int {
initIfNeed()
val elementUnWrap = unWrap(element)
return origin.indexOfLast { elementUnWrap == unWrap(it) }
}
override fun indexOf(element: T): Int {
initIfNeed()
val elementUnWrap = unWrap(element)
return origin.indexOfFirst { elementUnWrap == unWrap(it) }
}
override fun containsAll(elements: Collection<T>): Boolean {
initIfNeed()
var containsAll = true
elements.forEach { target ->
val found = origin.find { unWrap(it) == unWrap(target) }
if (found == null) {
containsAll = false
return@forEach
}
}
return containsAll
}
@ExperimentalJsCollectionsApi
override fun asJsArrayView(): JsArray<T> {
initIfNeed()
return origin.asJsArrayView()
}
@ExperimentalJsCollectionsApi
override fun asJsReadonlyArrayView(): JsReadonlyArray<T> {
initIfNeed()
return origin.asJsArrayView()
}
private fun unWrap(e: T): T {
return ArkUI.UIUtils.getTarget(e as Any) as T
}
private inner class HarmonyListIteratorImpl(index: Int) : HarmonyIteratorImpl(), MutableListIterator<T> {
init {
this.index = index
}
override fun hasPrevious(): Boolean = index > 0
override fun nextIndex(): Int = index
override fun previous(): T {
if (!hasPrevious()) throw NoSuchElementException()
last = --index
return get(last)
}
override fun previousIndex(): Int = index - 1
override fun add(element: T) {
add(index, element)
index++
last = -1
}
override fun set(element: T) {
check(last != -1) { "Call next() or previous() before updating element value with the iterator." }
set(last, element)
}
}
private open inner class HarmonyIteratorImpl : MutableIterator<T> {
/** the index of the item that will be returned on the next call to [next]`()` */
protected var index = 0
/** the index of the item that was returned on the previous call to [next]`()`
* or [ListIterator.previous]`()` (for `ListIterator`),
* -1 if no such item exists
*/
protected var last = -1
override fun hasNext(): Boolean = index < size
override fun next(): T {
if (!hasNext()) throw NoSuchElementException()
last = index++
return get(last)
}
override fun remove() {
check(last != -1) { "Call next() or previous() before removing element from the iterator." }
removeAt(last)
index = last
last = -1
}
}
}
关于「如何复用ViewModel」的介绍就告一段落了,如果大家在使用过程中有任何问题,欢迎留言讨论。 Android工程师的kmp(kotlin/js) for harmony开发指南 这一系列文章旨在系统性的提供一套完整的Kotlin/Js For Harmony的解决方案。后续系列文章会介绍序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!