(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的解决方案。后续系列文章会介绍序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!

相关推荐
nashane8 小时前
HarmonyOS 6学习:CapsLock键失效诊断与长截图完整实现指南
学习·华为·harmonyos
richard_yuu10 小时前
鸿蒙心理测评模块实战|PHQ-9/GAD7双量表答题、实时计分与结果本地化存储
华为·harmonyos
不爱吃糖的程序媛13 小时前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane13 小时前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄666814 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教20 小时前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区1 天前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane1 天前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi002 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony