(2)Kotlin/Js For Harmony——如何复用ViewModel

假设你的Android工程里有这样一个ViewModel

kotlin 复制代码
class MyViewModel {
	val name by mutableStateOf("")
}

要想支持ViewModel在鸿蒙端复用,我们需要实现两个目标:

  1. 在Compose中,name的变化会自动触发UI 更新。
  2. 在鸿蒙中,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拿到的原始对象。为什么要这样做,有两个原因:

  1. UIUtils.makeObserved 返回的是包装对象,并不是原对象,如果参与比较,会出现比较时不相等的情况,这和Compose的State行为是不同的。
ini 复制代码
class Obj
cal obj = Obj()
val objState = PlatformMutableState(obj)

print(obj == objState.value) // Android 上打印true, Harmony上打印false
  1. 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!并没有,当你有一天在异步线程读取PlatformMutableStatevalue, 你会发现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的解决方案。后续系列文章会介绍序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!

相关推荐
ifeng091816 分钟前
鸿蒙应用开发常见Crash场景解析:线程安全与异常边界处理
安全·cocoa·harmonyos
大雷神1 小时前
HarmonyOS 横竖屏切换与响应式布局实战指南
python·深度学习·harmonyos
爱笑的眼睛112 小时前
深入解析HarmonyOS应用包管理Bundle:从原理到实践
华为·harmonyos
爱笑的眼睛113 小时前
HarmonyOS网络状态深度监听与智能响应机制解析
华为·harmonyos
不爱吃糖的程序媛4 小时前
Cordova 开发鸿蒙PC应用翻译应用实现技术博客
华为·harmonyos
大师兄66685 小时前
Qt-for-鸿蒙PC-Electron应用鸿蒙平台白屏问题修复实战
qt·electron·harmonyos
国服第二切图仔5 小时前
Electron 鸿蒙pc开发环境搭建完整保姆级教程(window)
javascript·electron·harmonyos
啃火龙果的兔子6 小时前
如何控制kotlin项目back的时候,只回退webview的路由
开发语言·kotlin·harmonyos
lqj_本人8 小时前
HarmonyOS + Cordova:在线资源加载与拦截缓存问题排查
harmonyos
7***37458 小时前
HarmonyOS分布式能力的核心技术
分布式·华为·harmonyos