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

相关推荐
蓝胖子的多啦A梦几秒前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚
前端·npm·node.js
LinCC73 分钟前
在Vite中构建项目出错-Top-level await is not available in the configured target environme
前端
用户882093216674 分钟前
如何优雅拆分一个充斥十几种逻辑的 SDK 回调函数?
前端
Momoly085 分钟前
vue3+el-table 利用插槽自定义数据样式
前端·javascript·vue.js
唯有选择6 分钟前
让你的应用界面好看的基石:Flutter主题Theme使用和扩展自定义字段
前端·flutter
山有木兮木有枝_6 分钟前
告别布局间隙:浮动(float)在网页排版中的高阶应用
前端
满分观察网友z7 分钟前
vue的<router-link>的to里面的query和params的区别
前端
小约翰仓鼠8 分钟前
vue3表格使用Switch 开关
前端·javascript·vue.js
JiangJiang10 分钟前
🔥 面试官:Webpack 为什么能热更新?你真讲得清吗?
前端·面试·webpack
anyup29 分钟前
快崩溃了!华为应用商店已经 4 次驳回我的应用上线
前端·华为·uni-app