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

相关推荐
拉不动的猪2 分钟前
# 关于初学者对于JS异步编程十大误区
前端·javascript·面试
玖釉-7 分钟前
解决PowerShell执行策略导致的npm脚本无法运行问题
前端·npm·node.js
Larcher40 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐1 小时前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭1 小时前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信2 小时前
我们需要了解的Web Workers
前端
brzhang2 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js