You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android SharedPreferences可空Int存储读取方案探讨及KMM跨平台适配需求

解决KMM中可空Int类型的跨平台存储问题

嘿,我来帮你梳理下这个问题~你现在要在KMM核心模块里实现一个无平台依赖的SettingsStorage接口,其中包含可空Int类型的timerDuration,Android端用SharedPreferences实现,还纠结现有两个方案的弊端,对吧?

先分析下你现有方案的问题

方案一:值为null时移除对应键

这个思路的问题确实在于SharedPreferences的异步特性:如果用apply()(KTX的edit { ... }默认是异步),频繁快速调用set(null)再set(某个值),可能会因为异步移除操作还没完成,导致后续的put操作被覆盖,出现数据不一致的情况。另外,contains(key)的判断也不是绝对原子的,和异步编辑的操作可能存在竞态,导致读取到错误的结果。

方案二:统一用字符串存储

其实你担心的.toIntOrNull()开销完全是多虑的——对于Int来说,字符串转换是非常轻量的操作,几乎不会影响性能。但这个方案的问题在于:存储类型是非原生的Int,Android端如果之前有存量的Int数据会不兼容;iOS端UserDefaults存Int和String是不同的类型,跨平台调试或者和原生代码交互时会有点别扭。

更优方案:用“标记值映射null”

其实你完全不需要重复造轮子,这个场景下最稳妥高效的方式是选一个业务中永远不会用到的Int值作为null的“替身”,在核心模块里统一处理这个映射,平台端只需要实现原生的Int读写逻辑即可。

核心模块的统一逻辑

先在核心模块里定义一个抽象基类,把null和标记值的转换逻辑封装起来,避免平台端重复编写:

interface SettingsStorage {
    var timerDuration: Int?
    // 其他设置项...
}

abstract class BaseSettingsStorage : SettingsStorage {
    // 选一个业务中绝对不会用到的Int值作为null标记,比如Int最小值
    private val NULL_INT_MARKER = Int.MIN_VALUE

    override var timerDuration: Int?
        get() {
            val rawValue = getRawInt(TIMER_DURATION, NULL_INT_MARKER)
            return if (rawValue == NULL_INT_MARKER) null else rawValue
        }
        set(value) {
            val rawValue = value ?: NULL_INT_MARKER
            putRawInt(TIMER_DURATION, rawValue)
        }

    // 让平台实现原生的Int读写方法
    protected abstract fun getRawInt(key: String, defaultValue: Int): Int
    protected abstract fun putRawInt(key: String, value: Int)

    companion object {
        const val TIMER_DURATION = "timer_duration"
    }
}

Android端实现(基于SharedPreferences)

Android端只需要简单实现原生的Int读写,完全不用处理null逻辑:

class AndroidSettingsStorage(private val prefs: SharedPreferences) : BaseSettingsStorage() {
    override fun getRawInt(key: String, defaultValue: Int): Int {
        return prefs.getInt(key, defaultValue)
    }

    override fun putRawInt(key: String, value: Int) {
        prefs.edit { putInt(key, value) }
    }
}

iOS端实现(基于UserDefaults)

iOS端同样只需要处理原生的Int存储:

import platform.Foundation.NSUserDefaults

class IosSettingsStorage : BaseSettingsStorage() {
    private val defaults = NSUserDefaults.standardUserDefaults

    override fun getRawInt(key: String, defaultValue: Int): Int {
        return defaults.integerForKey(key) ?: defaultValue
    }

    override fun putRawInt(key: String, value: Int) {
        defaults.setInteger(value, key)
        defaults.synchronize()
    }
}

这个方案的优势

  • 性能最优:完全使用平台原生的Int存储,没有类型转换开销,也不需要移除键的操作
  • 避免异步竞态:每次都是直接put值,即使是异步编辑,最后一次操作的结果会覆盖前面的,数据一致性有保障
  • 逻辑统一:核心模块已经处理了null和标记值的映射,平台端只需要实现最基础的存储逻辑,不用重复编写默认值或null处理代码
  • 兼容性好:Android端用原生的Int存储,和现有SharedPreferences数据兼容;iOS端用UserDefaults的原生Int类型,和原生代码交互也没问题

注意点

选择标记值的时候,一定要确保这个值永远不会出现在你的业务逻辑中。比如如果你的timerDuration是正数,那用Int.MIN_VALUE或者任意负数都可以;如果业务中可能用到所有Int值,那可以考虑用Long类型来存储(用Long的最小值作为标记),不过这种情况非常少见。

内容的提问来源于stack exchange,提问作者m.reiter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 13:39:09