AppConfig - KMP中优雅的键值对管理方式

KMP项目中一般使用SharedPreferences(Android)、NSUserDefaults(iOS)来存储键值对。

kotlin 复制代码
// Android端 - SharedPreferences的"简洁"实现
val prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val darkMode = prefs.getBoolean("dark_mode_flag", false)
prefs.edit().putBoolean("dark_mode_flag", true).apply() // 用commit还是apply?这是个问题

// iOS端 - NSUserDefaults的Kotlin版
val defaults = NSUserDefaults.standardUserDefaults
val darkMode = defaults.boolForKey("dark_mode_flag")
defaults.setBool(true, forKey = "dark_mode_flag")

// 再加上expect/actual的地狱级样板:
expect class PreferenceManager {
    fun saveString(key: String, value: String)
    fun getString(key: String): String?
    // 每种类型重复一遍,直到天荒地老
}

这种代码的痛点,每个跨平台开发者都深有体会:

  • 🚫 魔法字符串 满天飞
  • 🚫 平台差异 需要处理平台差异
  • 🚫 管理混乱 键值对管理没有统一管理

由此,我们引入了AppConfig,希望打造优雅的键值对管理,理念是配置管理应该像定义接口一样简单

看看AppConfig如何实现:

kotlin 复制代码
// 1. 定义接口
@Config
interface NetworkConfig {

    @IntProperty(
        defaultValue = 60,
        description = "Network timeout duration"
    )
    var networkTimeout: Int
} 

// 2. 获取配置实例
val prefs = AppConfig.networkconfig
// 3. 读写配置
prefs.networkTimeout = 45
println("${prefs.networkTimeout}")

三步搞定:

  1. 定义接口
  2. 获取实例
  3. 像普通属性一样读写

没有魔法字符串,没有类型转换,没有平台判断------只有纯粹的Kotlin。

实现机制

AppConfig 的架构由两个核心部分组成:

1. KSP 代码生成器

appconfig-processor模块

Kotlin Symbol Processing (KSP) 在编译时分析你的配置接口并生成所有样板代码:

css 复制代码
┌─────────────────┐    KSP     ┌──────────────────┐
│ @Config         │ ────────→  │ 生成的            │
│ 接口定义         │  处理器      │ 实现类           │
└─────────────────┘            └──────────────────┘
                                        │
                                        ▼
                               ┌──────────────────┐
                               │ 扩展属性           │
                               │ AppConfig.xxx    │
                               └──────────────────┘

2. appconfig-lib 跨平台存储

appconfig-lib模块 提供了统一的跨平台键值对存储机制:

scss 复制代码
┌──────────────────┐            ┌──────────────────┐
│ 生成的实现类      │ ────────→  │ ConfigStore      │
│ (类型安全接口)    │            │ (统一抽象层)      │
└──────────────────┘            └──────────────────┘
                                        │
                    ┌───────────────────┼───────────────────┐
                    ▼                   ▼                   ▼
        ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
        │ Android         │   │ iOS             │   │ JVM/Desktop     │
        │SharedPreferences│   │ NSUserDefaults  │   │ Properties      │
        └─────────────────┘   └─────────────────┘   └─────────────────┘

完整工作流程:

  1. 编译时 :KSP 处理器扫描 @Config 注解的接口
  2. 代码生成:为每个配置接口生成实现类和扩展属性
  3. 运行时 :生成的代码通过 ConfigStore 抽象层访问平台存储
  4. 平台适配ConfigStore 自动选择合适的底层存储机制

生成的内容包括:

  • ✅ 带有类型安全 getter/setter 的实现类
  • ✅ 便于访问的扩展属性(AppConfig.networkconfig
  • ✅ 与 ConfigStore 的集成代码
  • ✅ 管理界面的配置元数据
  • ✅ 批量操作(重置、远程更新等)

appconfig-lib 提供的核心功能:

  • 🔄 跨平台键值对存储抽象
  • 🛡️ 类型安全的数据转换
  • ⚡ 高性能的缓存机制
  • 🔧 统一的配置管理 API

我们可以以上面的NetworkConfig接口为例,看编译生成的代码:

kotlin 复制代码
public class NetworkConfigImpl : NetworkConfig {
  private val store: ConfigStore = AppConfig.getConfigStore("NetworkConfig")

  private var networkTimeoutDefault: Int = 60

  override var networkTimeout: Int
    get() = store.getInt("networkTimeout", networkTimeoutDefault)
    set(`value`) {
      store.putInt("networkTimeout", value)
    }
  
  public fun getConfigItems(): List<ConfigItemDescriptor<*>>  = listOf(
     StandardConfigItem<Int>(
      key = "networkTimeout",
      groupName = "NetworkConfig",
      description = "Network timeout duration",
      getCurrentValue = { this.networkTimeout },
      defaultValue = 60,
      panelType = PanelType.TEXT_INPUT,
      dataType = DataType.INT,
      updateValue = { newValue -> this.networkTimeout = newValue },
      resetToDefault = { this.networkTimeout = 60 }
    ))
}

public val AppConfig.networkconfig: NetworkConfigImpl
  get() = NetworkConfigImpl()

可以看到,其中包括:

  • ✅ 带有类型安全 getter/setter 的实现类UserInterfaceConfigImpl
  • ✅ 便于访问的扩展属性(AppConfig.networkconfig
  • ✅ 获取ConfigStore,处理键值对的存储
  • ✅ 管理界面的配置元数据,用于可视化管理所有键值对

平台特定配置

AppConfig不仅仅生成平台无关的代码 - 它实际上支持平台特定配置,这些配置只存在于特定平台上

共享配置(commonMain)

适用于所有平台,在 commonMain 中定义:

kotlin 复制代码
// commonMain/kotlin/.../UserSettings.kt
@Config(groupName = "UserSettings")
interface UserSettings {
    @BooleanProperty(defaultValue = false, description = "启用深色模式")
    var isDarkModeEnabled: Boolean
    
    @IntProperty(defaultValue = 30, description = "请求超时时间(秒)")
    var timeoutSeconds: Int
}

AppConfig 在 commonMain 中生成实现,你可以在任何地方使用:

kotlin 复制代码
// 在 Android 和 iOS 上都能工作
val settings = AppConfig.usersettings
settings.isDarkModeEnabled = true

Android 专用配置(androidMain)

可以定义只在 Android 上存在的设置:

kotlin 复制代码
// androidMain/kotlin/.../AndroidSettings.kt
@Config
interface AndroidSettings {
    @BooleanProperty(defaultValue = false, description = "启用 Camera X")
    var isCameraXEnabled: Boolean
    
    @BooleanProperty(defaultValue = true, description = "使用 Material You 颜色")
    var useMaterialYou: Boolean
    
    @IntProperty(defaultValue = 2, description = "通知重要性级别")
    var notificationImportance: Int
}

AppConfig 检测到这是在 androidMain 中,只为 Android 生成代码

kotlin 复制代码
// 只在 Android 目标平台生成
class AndroidSettingsImpl : AndroidSettings {
    private val store = AppConfig.getConfigStore("AndroidSettings")
    
    override var isCameraXEnabled: Boolean
        get() = store.getBoolean("isCameraXEnabled", false)
        set(value) = store.putBoolean("isCameraXEnabled", value)
    
    override var useMaterialYou: Boolean
        get() = store.getBoolean("useMaterialYou", true)
        set(value) = store.putBoolean("useMaterialYou", value)
    
    // ... 等等
}

// 扩展属性只在 Android 上可用
val AppConfig.androidsettings: AndroidSettingsImpl
    get() = AndroidSettingsImpl()

iOS中也可以定义类似于Android的专属配置

支持的数据类型

Annotation Kotlin Type Description
@StringProperty String Text values
@BooleanProperty Boolean True/false toggles
@IntProperty Int 32-bit integers
@LongProperty Long 64-bit integers
@FloatProperty Float 32-bit floating point
@DoubleProperty Double 64-bit floating point
@OptionProperty Sealed Class Enum-like choices

支持密封类、密封接口

使用方式如下:

kotlin 复制代码
@Config
interface AppSettings {
    @OptionProperty(description = "应用主题")
    var theme: AppTheme
}

@Option
sealed class AppTheme {
    @OptionItem(optionId = 0, description = "跟随系统", isDefault = true)
    object System : AppTheme()
    
    @OptionItem(optionId = 1, description = "浅色主题")
    object Light : AppTheme()
    
    @OptionItem(optionId = 2, description = "深色主题")  
    object Dark : AppTheme()
}

// 使用方式类型安全且优雅
settings.theme = AppTheme.Dark
when (settings.theme) {
    AppTheme.System -> applySystemTheme()
    AppTheme.Light -> applyLightTheme()
    AppTheme.Dark -> applyDarkTheme()
}

生成的代码内部将这些存储为整数,但是使用的时候可以保证完整类型安全。

适合A/B 测试: 这个选项系统对于功能开关和 A/B 测试组来说非常强大。不用管理多个布尔标志或神秘的字符串标识符,都可以定义清晰、类型安全的变体:

kotlin 复制代码
@Option
sealed class OnboardingGroup {
    @OptionItem(0, "A 组 - 教程", isDefault = true)
    object Tutorial : OnboardingGroup()
    
    @OptionItem(1, "B 组 - 交互演示")
    object Demo : OnboardingGroup()
    
    @OptionItem(2, "C 组 - 跳过引导")
    object Skip : OnboardingGroup()
}

自动生成的管理界面

kotlin 复制代码
@Composable
fun DebugSettingsScreen() {
    ConfigPanel(
        configItems = AppConfig.getAllConfigItems()
    )
}

这会生成一个完整的界面,包含开关、文本框、下拉菜单和所有好东西。就像每个配置接口都免费送一个设置界面。

适合A/B 测试: 这个功能对于需要 A/B 测试的应用特别强大。不用在你的 A/B 测试平台上切换测试组,你可以在应用内即时更改测试配置。QA 团队可以即时测试不同测试组,开发者可以快速验证功能开关,产品经理可以准确看到不同配置如何影响用户体验 - 所有这些都不用离开应用或等待远程配置更新。

快速上手

🚀 开始使用: AppConfig on GitHub

相关推荐
Mintopia1 分钟前
LOD:图形世界里的 “看人下菜碟” 艺术
前端·javascript·计算机图形学
黑客老李3 分钟前
EDUSRC:智慧校园通用漏洞挖掘(涉校园解决方案商)
服务器·前端·网络·安全·web安全
拾光拾趣录4 分钟前
Vue依赖收集机制:响应式原理的核心实现
前端·vue.js
Mintopia5 分钟前
Three.js ArrowHelper:三维世界里的 “方向向导”
前端·javascript·three.js
归于尽6 分钟前
浏览器和 Node.js 的 EventLoop,原来差别这么大
前端·node.js·浏览器
雲墨款哥7 分钟前
Vue 3 路由管理实战:从基础配置到性能优化
前端·vue.js
Jacob023411 分钟前
JavaScript 模块系统二十年:混乱、分裂与出路
前端·javascript
独立开阀者_FwtCoder16 分钟前
Vite Devtools 要发布了!期待
前端·面试·github
独立开阀者_FwtCoder17 分钟前
国外最流行的 UI 组件库!适配 Vue、React、Angular!
前端·vue.js·后端
CodeSheep24 分钟前
小米汽车这薪资是认真的吗?
前端·后端·程序员