Add Kotlin Multiplatform (KMP) wrapper for MMKV

KMP module at KMP/ exposes a Kotlin-idiomatic API in commonMain (com.tencent.mmkv.kmp),
with platform-specific implementations:
- androidMain: delegates to the existing com.tencent:mmkv Java library
- darwinMain: uses Kotlin/Native ObjC interop with the MMKV CocoaPod

Includes a Compose Multiplatform sample app (KMP/sample/) that demonstrates
MMKV initialization, encode/decode for all primitive types, named instances,
and key management on both Android and iOS.
This commit is contained in:
guoling
2026-03-23 18:57:47 +08:00
parent b6edcb64dd
commit e197236714
34 changed files with 2812 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"preferences": {
"git_commit_no_co_author": true
}
}
+9
View File
@@ -0,0 +1,9 @@
.gradle
build
.kotlin
local.properties
*.podspec
Pods
Podfile.lock
*.xcworkspace
*.xcuserdata
+8
View File
@@ -0,0 +1,8 @@
plugins {
kotlin("multiplatform") version "2.2.20" apply false
kotlin("plugin.compose") version "2.2.20" apply false
kotlin("android") version "2.2.20" apply false
id("com.android.library") version "8.13.1" apply false
id("com.android.application") version "8.13.1" apply false
id("org.jetbrains.compose") version "1.8.1" apply false
}
+11
View File
@@ -0,0 +1,11 @@
# Project-wide Gradle settings.
org.gradle.jvmargs=-Xmx2048m
# Android
android.useAndroidX=true
android.nonTransitiveRClass=false
# Kotlin
kotlin.mpp.stability.nowarn=true
kotlin.native.cacheKind.iosArm64=none
kotlin.native.cacheKind.iosSimulatorArm64=none
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Fri Oct 16 21:16:39 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
Vendored Executable
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
+84
View File
@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+91
View File
@@ -0,0 +1,91 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
}
kotlin {
// Android target
androidTarget {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
// iOS targets
iosArm64()
iosSimulatorArm64()
iosX64()
// macOS targets
macosArm64()
macosX64()
// Use the default hierarchy template which automatically creates
// appleMain, iosMain, macosMain, etc. intermediate source sets.
// We add a custom darwinMain that covers both iOS and macOS.
applyDefaultHierarchyTemplate {
common {
group("darwin") {
group("ios")
group("macos")
}
}
}
// CocoaPods integration for Darwin platforms
cocoapods {
summary = "MMKV Kotlin Multiplatform wrapper"
homepage = "https://github.com/Tencent/MMKV"
version = "2.4.0"
ios.deploymentTarget = "13.0"
osx.deploymentTarget = "10.15"
pod("MMKV") {
version = "2.4.0"
}
}
// Source set hierarchy
sourceSets {
androidMain {
dependencies {
implementation("com.tencent:mmkv:2.4.0")
}
}
}
}
android {
namespace = "com.tencent.mmkv.kmp"
compileSdk = 35
defaultConfig {
minSdk = 23
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
@@ -0,0 +1,281 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
import android.content.Context
import com.tencent.mmkv.MMKV as AndroidMMKV
import com.tencent.mmkv.MMKVConfig as AndroidMMKVConfig
import com.tencent.mmkv.MMKVHandler as AndroidMMKVHandler
import com.tencent.mmkv.MMKVLogLevel as AndroidMMKVLogLevel
import com.tencent.mmkv.MMKVRecoverStrategic as AndroidMMKVRecoverStrategic
// region Platform-specific initialization (Android needs Context)
/**
* Initialize MMKV with default configuration.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param logLevel The log level of MMKV, defaults to [MMKVLogLevel.Info].
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
fun MMKV.Companion.initialize(
context: Context,
logLevel: MMKVLogLevel = MMKVLogLevel.Info,
): String {
return AndroidMMKV.initialize(context, logLevel.toAndroid())
}
/**
* Initialize MMKV with customize root folder.
*
* @param context The context of Android App, usually from Application.
* @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv.
* @param logLevel The log level of MMKV, defaults to [MMKVLogLevel.Info].
* @param handler The unified callback handler for MMKV.
* @return The root folder of MMKV.
*/
fun MMKV.Companion.initialize(
context: Context,
rootDir: String,
logLevel: MMKVLogLevel = MMKVLogLevel.Info,
handler: MMKVHandler? = null,
): String {
val androidHandler = handler?.let { AndroidMMKVHandlerAdapter(it) }
return AndroidMMKV.initialize(context, rootDir, null, logLevel.toAndroid(), androidHandler)
}
// endregion
/**
* The Android actual implementation of [MMKV] that delegates to the Java `com.tencent.mmkv.MMKV` library.
*
* Since the KMP class is now in `com.tencent.mmkv.kmp`, we can reference the Java
* `com.tencent.mmkv.MMKV` directly via import alias without any naming collision.
*/
actual class MMKV internal constructor(private val impl: AndroidMMKV) {
actual companion object {
actual fun onExit() = AndroidMMKV.onExit()
actual fun setLogLevel(level: MMKVLogLevel) = AndroidMMKV.setLogLevel(level.toAndroid())
actual fun defaultMMKV(): MMKV = MMKV(AndroidMMKV.defaultMMKV())
actual fun defaultMMKV(config: MMKVConfig): MMKV = MMKV(AndroidMMKV.defaultMMKV(config.toAndroid()))
actual fun mmkvWithID(mmapID: String): MMKV = MMKV(AndroidMMKV.mmkvWithID(mmapID))
actual fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV = MMKV(AndroidMMKV.mmkvWithID(mmapID, config.toAndroid()))
actual fun pageSize(): Int = AndroidMMKV.pageSize()
actual fun version(): String = AndroidMMKV.version()
actual fun rootDir(): String = AndroidMMKV.getRootDir() ?: ""
actual fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String?): Boolean =
AndroidMMKV.backupOneToDirectory(mmapID, dstDir, rootPath)
actual fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String?): Boolean =
AndroidMMKV.restoreOneMMKVFromDirectory(mmapID, srcDir, rootPath)
actual fun backupAllToDirectory(dstDir: String): Long = AndroidMMKV.backupAllToDirectory(dstDir)
actual fun restoreAllFromDirectory(srcDir: String): Long = AndroidMMKV.restoreAllFromDirectory(srcDir)
actual fun isFileValid(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.isFileValid(mmapID, rootPath)
actual fun removeStorage(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.removeStorage(mmapID, rootPath)
actual fun checkExist(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.checkExist(mmapID, rootPath)
actual fun registerHandler(handler: MMKVHandler) = AndroidMMKV.registerHandler(AndroidMMKVHandlerAdapter(handler))
actual fun unRegisterHandler() = AndroidMMKV.unregisterHandler()
}
// region Properties
actual val mmapID: String get() = impl.mmapID()
actual val isMultiProcess: Boolean get() = impl.isMultiProcess
actual val isReadOnly: Boolean get() = impl.isReadOnly
actual val totalSize: Long get() = impl.totalSize()
actual val actualSize: Long get() = impl.actualSize()
actual val count: Long get() = impl.count()
actual val allKeys: List<String> get() = impl.allKeys()?.toList() ?: emptyList()
actual val cryptKey: String? get() = impl.cryptKey()
// endregion
// region Encode
actual fun encodeBool(key: String, value: Boolean): Boolean = impl.encode(key, value)
actual fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeInt(key: String, value: Int): Boolean = impl.encode(key, value)
actual fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeLong(key: String, value: Long): Boolean = impl.encode(key, value)
actual fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeFloat(key: String, value: Float): Boolean = impl.encode(key, value)
actual fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeDouble(key: String, value: Double): Boolean = impl.encode(key, value)
actual fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeString(key: String, value: String): Boolean = impl.encode(key, value)
actual fun encodeString(key: String, value: String, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
actual fun encodeBytes(key: String, value: ByteArray): Boolean = impl.encode(key, value)
actual fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt())
// endregion
// region Decode
actual fun decodeBool(key: String, defaultValue: Boolean): Boolean = impl.decodeBool(key, defaultValue)
actual fun decodeInt(key: String, defaultValue: Int): Int = impl.decodeInt(key, defaultValue)
actual fun decodeLong(key: String, defaultValue: Long): Long = impl.decodeLong(key, defaultValue)
actual fun decodeFloat(key: String, defaultValue: Float): Float = impl.decodeFloat(key, defaultValue)
actual fun decodeDouble(key: String, defaultValue: Double): Double = impl.decodeDouble(key, defaultValue)
actual fun decodeString(key: String, defaultValue: String?): String? = impl.decodeString(key, defaultValue)
actual fun decodeBytes(key: String): ByteArray? = impl.decodeBytes(key)
// endregion
// region Key management
actual fun containsKey(key: String): Boolean = impl.containsKey(key)
actual fun countNonExpiredKeys(): Long = impl.countNonExpiredKeys()
actual fun allNonExpiredKeys(): List<String> = impl.allNonExpireKeys()?.toList() ?: emptyList()
actual fun removeValueForKey(key: String) = impl.removeValueForKey(key)
actual fun removeValuesForKeys(keys: List<String>) = impl.removeValuesForKeys(keys.toTypedArray())
actual fun clearAll() = impl.clearAll()
actual fun clearAllKeepSpace() = impl.clearAllWithKeepingSpace()
// endregion
// region Encryption
actual fun reKey(newKey: String?, aes256: Boolean): Boolean = impl.reKey(newKey, aes256)
actual fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean) = impl.checkReSetCryptKey(cryptKey, aes256)
// endregion
// region Utility
actual fun sync() = impl.sync()
actual fun async() = impl.async()
actual fun trim() = impl.trim()
actual fun close() = impl.close()
actual fun clearMemoryCache() = impl.clearMemoryCache()
actual fun importFrom(source: MMKV): Long = impl.importFrom(source.impl)
actual fun enableAutoKeyExpire(expiredInSeconds: UInt): Boolean = impl.enableAutoKeyExpire(expiredInSeconds.toInt())
actual fun disableAutoKeyExpire(): Boolean = impl.disableAutoKeyExpire()
actual fun enableCompareBeforeSet(): Boolean {
impl.enableCompareBeforeSet()
return true
}
actual fun disableCompareBeforeSet(): Boolean {
impl.disableCompareBeforeSet()
return true
}
actual fun checkContentChanged() = impl.checkContentChangedByOuterProcess()
// endregion
}
// region Enum conversion helpers
private fun MMKVLogLevel.toAndroid(): AndroidMMKVLogLevel = when (this) {
MMKVLogLevel.Debug -> AndroidMMKVLogLevel.LevelDebug
MMKVLogLevel.Info -> AndroidMMKVLogLevel.LevelInfo
MMKVLogLevel.Warning -> AndroidMMKVLogLevel.LevelWarning
MMKVLogLevel.Error -> AndroidMMKVLogLevel.LevelError
MMKVLogLevel.None -> AndroidMMKVLogLevel.LevelNone
}
private fun AndroidMMKVLogLevel.toKmp(): MMKVLogLevel = when (this) {
AndroidMMKVLogLevel.LevelDebug -> MMKVLogLevel.Debug
AndroidMMKVLogLevel.LevelInfo -> MMKVLogLevel.Info
AndroidMMKVLogLevel.LevelWarning -> MMKVLogLevel.Warning
AndroidMMKVLogLevel.LevelError -> MMKVLogLevel.Error
AndroidMMKVLogLevel.LevelNone -> MMKVLogLevel.None
}
private fun MMKVRecoverStrategic.toAndroid(): AndroidMMKVRecoverStrategic = when (this) {
MMKVRecoverStrategic.OnErrorDiscard -> AndroidMMKVRecoverStrategic.OnErrorDiscard
MMKVRecoverStrategic.OnErrorRecover -> AndroidMMKVRecoverStrategic.OnErrorRecover
}
// endregion
// region Config conversion
private fun MMKVConfig.toAndroid(): AndroidMMKVConfig {
val config = AndroidMMKVConfig()
config.mode = mode
config.cryptKey = cryptKey
config.aes256 = aes256
config.rootPath = rootPath
config.expectedCapacity = expectedCapacity
config.enableKeyExpire = enableKeyExpire
config.expiredInSeconds = expiredInSeconds.toInt()
config.enableCompareBeforeSet = enableCompareBeforeSet
config.recover = recover?.toAndroid()
config.itemSizeLimit = itemSizeLimit.toInt()
return config
}
// endregion
// region Handler adapter
/**
* Adapts the KMP [MMKVHandler] to the Java [AndroidMMKVHandler] interface.
*/
private class AndroidMMKVHandlerAdapter(
private val handler: MMKVHandler,
) : AndroidMMKVHandler {
override fun onMMKVCRCCheckFail(mmapID: String): AndroidMMKVRecoverStrategic {
return handler.onMMKVCRCCheckFail(mmapID).toAndroid()
}
override fun onMMKVFileLengthError(mmapID: String): AndroidMMKVRecoverStrategic {
return handler.onMMKVFileLengthError(mmapID).toAndroid()
}
override fun wantLogRedirecting(): Boolean = handler.wantLogRedirect()
override fun mmkvLog(level: AndroidMMKVLogLevel, file: String, line: Int, function: String, message: String) {
handler.mmkvLog(level.toKmp(), file, line, function, message)
}
override fun wantContentChangeNotification(): Boolean = handler.wantContentChangeNotification()
override fun onContentChangedByOuterProcess(mmapID: String) = handler.onContentChangedByOuterProcess(mmapID)
override fun onMMKVContentLoadSuccessfully(mmapID: String) = handler.onMMKVContentLoadSuccessfully(mmapID)
}
// endregion
@@ -0,0 +1,321 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* An highly efficient, reliable, multi-process key-value storage framework.
* Kotlin Multiplatform wrapper for MMKV.
*
* Note: Platform-specific initialization methods are provided as extension functions
* on [MMKV.Companion] in each platform source set, since Android requires [Context]
* while other platforms do not.
*/
expect class MMKV {
companion object {
/**
* Notify MMKV that App is about to exit. It's totally fine not calling it at all.
*/
fun onExit()
/**
* Set the log level of MMKV.
* @param level Defaults to [MMKVLogLevel.Info].
*/
fun setLogLevel(level: MMKVLogLevel)
/**
* Create the default MMKV instance in single-process mode.
*/
fun defaultMMKV(): MMKV
/**
* Create the default MMKV instance with the given configuration.
* @param config The all-in-one configuration for the MMKV instance.
*/
fun defaultMMKV(config: MMKVConfig): MMKV
/**
* Create an MMKV instance with an unique ID (in single-process mode).
* @param mmapID The unique ID of the MMKV instance.
*/
fun mmkvWithID(mmapID: String): MMKV
/**
* Create an MMKV instance with an unique ID and the given configuration.
* @param mmapID The unique ID of the MMKV instance.
* @param config The all-in-one configuration for the MMKV instance.
*/
fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV
/**
* @return The device's memory page size.
*/
fun pageSize(): Int
/**
* @return The version of MMKV.
*/
fun version(): String
/**
* @return The root folder of MMKV.
*/
fun rootDir(): String
/**
* Backup one MMKV instance to dstDir.
* @param mmapID The MMKV ID to backup.
* @param dstDir The backup destination directory.
* @param rootPath The customize root path of the MMKV, if null then backup from the root dir of MMKV.
*/
fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String? = null): Boolean
/**
* Restore one MMKV instance from srcDir.
* @param mmapID The MMKV ID to restore.
* @param srcDir The restore source directory.
* @param rootPath The customize root path of the MMKV, if null then restore to the root dir of MMKV.
*/
fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String? = null): Boolean
/**
* Backup all MMKV instances to dstDir.
* @param dstDir The backup destination directory.
* @return Count of MMKV successfully backed up.
*/
fun backupAllToDirectory(dstDir: String): Long
/**
* Restore all MMKV instances from srcDir.
* @param srcDir The restore source directory.
* @return Count of MMKV successfully restored.
*/
fun restoreAllFromDirectory(srcDir: String): Long
/**
* Check whether the MMKV file is valid or not.
* Note: Don't use this to check the existence of the instance, the result is undefined on nonexistent files.
*/
fun isFileValid(mmapID: String, rootPath: String? = null): Boolean
/**
* Remove the storage of the MMKV, including the data file & meta file (.crc).
* Note: the existing instance (if any) will be closed & destroyed.
*/
fun removeStorage(mmapID: String, rootPath: String? = null): Boolean
/**
* Check existence of the MMKV file.
*/
fun checkExist(mmapID: String, rootPath: String? = null): Boolean
/**
* Register a unified callback handler for MMKV.
*/
fun registerHandler(handler: MMKVHandler)
/**
* Unregister the callback handler.
*/
fun unRegisterHandler()
}
/** The unique ID of the MMKV instance. */
val mmapID: String
/** Whether the MMKV instance is in multi-process mode. */
val isMultiProcess: Boolean
/** Whether the MMKV instance is in read-only mode. */
val isReadOnly: Boolean
/**
* Get the size of the underlying file.
* Align to the disk block size, typically 4K for an Android device.
*/
val totalSize: Long
/**
* Get the actual used size of the MMKV instance.
*/
val actualSize: Long
/**
* The total count of all the keys.
*/
val count: Long
/**
* All keys.
*/
val allKeys: List<String>
/**
* The encryption key (no more than 16 bytes, or 32 bytes for AES-256).
*/
val cryptKey: String?
// region Encode
fun encodeBool(key: String, value: Boolean): Boolean
fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean
fun encodeInt(key: String, value: Int): Boolean
fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean
fun encodeLong(key: String, value: Long): Boolean
fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean
fun encodeFloat(key: String, value: Float): Boolean
fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean
fun encodeDouble(key: String, value: Double): Boolean
fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean
fun encodeString(key: String, value: String): Boolean
fun encodeString(key: String, value: String, expireDuration: UInt): Boolean
fun encodeBytes(key: String, value: ByteArray): Boolean
fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean
// endregion
// region Decode
fun decodeBool(key: String, defaultValue: Boolean = false): Boolean
fun decodeInt(key: String, defaultValue: Int = 0): Int
fun decodeLong(key: String, defaultValue: Long = 0L): Long
fun decodeFloat(key: String, defaultValue: Float = 0f): Float
fun decodeDouble(key: String, defaultValue: Double = 0.0): Double
fun decodeString(key: String, defaultValue: String? = null): String?
fun decodeBytes(key: String): ByteArray?
// endregion
// region Key management
/**
* Check whether or not MMKV contains the key.
*/
fun containsKey(key: String): Boolean
/**
* @return Count of non-expired keys. Note that this call has costs.
*/
fun countNonExpiredKeys(): Long
/**
* @return All non-expired keys. Note that this call has costs.
*/
fun allNonExpiredKeys(): List<String>
fun removeValueForKey(key: String)
fun removeValuesForKeys(keys: List<String>)
/**
* Clear all the key-values inside the MMKV instance.
*/
fun clearAll()
/**
* Faster [clearAll] implementation. The file size is kept as previous for later use.
*/
fun clearAllKeepSpace()
// endregion
// region Encryption
/**
* Transform plain text into encrypted text, or vice versa by passing a null encryption key.
* You can also change existing crypt key with a different cryptKey.
* @param newKey The new encryption key, or null to remove encryption.
* @param aes256 Use AES 256 key length.
*/
fun reKey(newKey: String?, aes256: Boolean = false): Boolean
/**
* Just reset the encryption key (will not encrypt or decrypt anything).
* Usually you should call this method after another process has [reKey] the multi-process MMKV instance.
*/
fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean = false)
// endregion
// region Utility
/**
* Save all mmap memory to file synchronously.
*/
fun sync()
/**
* Save all mmap memory to file asynchronously.
*/
fun async()
/**
* The [totalSize] of an MMKV instance won't reduce after deleting key-values,
* call this method after lots of deleting if you care about disk usage.
*/
fun trim()
/**
* Call this method if the MMKV instance is no longer needed in the near future.
* Any subsequent call to the instance is undefined behavior.
*/
fun close()
/**
* Clear memory cache of the MMKV instance.
* You can call it on memory warning.
*/
fun clearMemoryCache()
/**
* Import all key-value items from source.
* @return Count of items imported.
*/
fun importFrom(source: MMKV): Long
/**
* Enable auto key expiration.
* @param expiredInSeconds 0 means no common expiration duration, aka each key will have its own expire date.
*/
fun enableAutoKeyExpire(expiredInSeconds: UInt = 0u): Boolean
/**
* Disable auto key expiration.
*/
fun disableAutoKeyExpire(): Boolean
/**
* Enable data compare before set, for better performance.
*/
fun enableCompareBeforeSet(): Boolean
/**
* Disable data compare before set.
*/
fun disableCompareBeforeSet(): Boolean
/**
* Check if content changed by other process.
*/
fun checkContentChanged()
// endregion
}
@@ -0,0 +1,37 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* The all-in-one configuration for creating an MMKV instance.
*/
data class MMKVConfig(
val mode: Int = MMKVMode.SINGLE_PROCESS,
val cryptKey: String? = null,
val aes256: Boolean = false,
val rootPath: String? = null,
val expectedCapacity: Long = 0,
val enableKeyExpire: Boolean? = null,
val expiredInSeconds: UInt = 0u,
val enableCompareBeforeSet: Boolean = false,
val recover: MMKVRecoverStrategic? = null,
val itemSizeLimit: UInt = 0u,
)
@@ -0,0 +1,74 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* Unified callback handler for MMKV.
* Callback is called on the operating thread of the MMKV instance.
*/
abstract class MMKVHandler {
/**
* By default MMKV will discard all data on crc32-check failure.
* @param mmapID The unique ID of the MMKV instance.
* @return Return [MMKVRecoverStrategic.OnErrorRecover] to recover any data on the file.
*/
open fun onMMKVCRCCheckFail(mmapID: String): MMKVRecoverStrategic = MMKVRecoverStrategic.OnErrorDiscard
/**
* By default MMKV will discard all data on file length mismatch.
* @param mmapID The unique ID of the MMKV instance.
* @return Return [MMKVRecoverStrategic.OnErrorRecover] to recover any data on the file.
*/
open fun onMMKVFileLengthError(mmapID: String): MMKVRecoverStrategic = MMKVRecoverStrategic.OnErrorDiscard
/**
* @return Return true if you want log redirecting.
*/
open fun wantLogRedirect(): Boolean = false
/**
* Log redirecting.
* @param level The level of this log.
* @param file The file name of this log.
* @param line The line of code of this log.
* @param function The function name of this log.
* @param message The content of this log.
*/
open fun mmkvLog(level: MMKVLogLevel, file: String, line: Int, function: String, message: String) {}
/**
* @return Return true if you want inter-process content change notification.
*/
open fun wantContentChangeNotification(): Boolean = false
/**
* Inter-process content change notification.
* @param mmapID The unique ID of the changed MMKV instance.
*/
open fun onContentChangedByOuterProcess(mmapID: String) {}
/**
* Called when an MMKV file is loaded successfully.
* @param mmapID The unique ID of the loaded MMKV instance.
*/
open fun onMMKVContentLoadSuccessfully(mmapID: String) {}
}
@@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* The levels of MMKV log.
*/
enum class MMKVLogLevel {
/** Debug level. Not available for release/production build. */
Debug,
/** Info level. The default level. */
Info,
/** Warning level. */
Warning,
/** Error level. */
Error,
/** Special level for disabling all logging. */
None
}
@@ -0,0 +1,47 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* MMKV instance mode constants.
*/
object MMKVMode {
/** Single-process mode. The default mode on an MMKV instance. */
const val SINGLE_PROCESS = 1 shl 0
/** Multi-process mode. */
const val MULTI_PROCESS = 1 shl 1
/** Read-only mode. */
const val READ_ONLY = 1 shl 5
}
/**
* MMKV expire duration constants (in seconds).
*/
object MMKVExpireDuration {
const val Never: UInt = 0u
const val InMinute: UInt = 60u
const val InHour: UInt = 3_600u
const val InDay: UInt = 86_400u
const val InMonth: UInt = 2_592_000u
const val InYear: UInt = 946_080_000u
}
@@ -0,0 +1,32 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
/**
* The recover strategic of MMKV on errors.
*/
enum class MMKVRecoverStrategic {
/** The default strategic is to discard everything on errors. */
OnErrorDiscard,
/** The recover strategic will try to recover as much data as possible. */
OnErrorRecover
}
@@ -0,0 +1,419 @@
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv.kmp
import cocoapods.MMKV.MMKV as DarwinMMKV
import cocoapods.MMKV.MMKVConfig as DarwinMMKVConfig
import cocoapods.MMKV.MMKVHandlerProtocol
import cocoapods.MMKV.MMKVLogDebug
import cocoapods.MMKV.MMKVLogError
import cocoapods.MMKV.MMKVLogInfo
import cocoapods.MMKV.MMKVLogLevel as DarwinMMKVLogLevel
import cocoapods.MMKV.MMKVLogNone
import cocoapods.MMKV.MMKVLogWarning
import cocoapods.MMKV.MMKVMultiProcess
import cocoapods.MMKV.MMKVOnErrorDiscard
import cocoapods.MMKV.MMKVOnErrorNotSet
import cocoapods.MMKV.MMKVOnErrorRecover
import cocoapods.MMKV.MMKVReadOnly
import cocoapods.MMKV.MMKVRecoverStrategic as DarwinMMKVRecoverStrategic
import cocoapods.MMKV.MMKVSingleProcess
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.Foundation.NSNumber
import platform.Foundation.NSString
import platform.Foundation.create
import platform.Foundation.dataUsingEncoding
import platform.Foundation.NSUTF8StringEncoding
import platform.darwin.NSObject
import platform.posix.memcpy
// region Platform-specific initialization (Darwin doesn't need Context)
/**
* Initialize MMKV with customize settings.
* Call this in main thread, before calling any other MMKV methods.
*
* @param rootDir The root dir of MMKV, passing null defaults to {NSDocumentDirectory}/mmkv.
* @param logLevel MMKVLogInfo by default, MMKVLogNone to disable all logging.
* @param handler The unified callback handler for MMKV.
* @return Root dir of MMKV.
*/
@OptIn(ExperimentalForeignApi::class)
fun MMKV.Companion.initialize(
rootDir: String? = null,
logLevel: MMKVLogLevel = MMKVLogLevel.Info,
handler: MMKVHandler? = null,
): String {
val darwinHandler = handler?.let { DarwinMMKVHandlerAdapter(it) }
return DarwinMMKV.initializeMMKV(rootDir, logLevel = logLevel.toDarwin(), handler = darwinHandler) ?: ""
}
/**
* Initialize MMKV with a group directory for multi-process access.
*
* @param rootDir The root dir of MMKV, passing null defaults to {NSDocumentDirectory}/mmkv.
* @param groupDir The root dir of multi-process MMKV.
* @param logLevel MMKVLogInfo by default.
* @param handler The unified callback handler for MMKV.
* @return Root dir of MMKV.
*/
@OptIn(ExperimentalForeignApi::class)
fun MMKV.Companion.initialize(
rootDir: String? = null,
groupDir: String,
logLevel: MMKVLogLevel = MMKVLogLevel.Info,
handler: MMKVHandler? = null,
): String {
val darwinHandler = handler?.let { DarwinMMKVHandlerAdapter(it) }
return DarwinMMKV.initializeMMKV(rootDir, groupDir = groupDir, logLevel = logLevel.toDarwin(), handler = darwinHandler) ?: ""
}
// endregion
@OptIn(ExperimentalForeignApi::class)
actual class MMKV internal constructor(private val impl: DarwinMMKV) {
actual companion object {
actual fun onExit() {
DarwinMMKV.onAppTerminate()
}
actual fun setLogLevel(level: MMKVLogLevel) {
@Suppress("DEPRECATION")
DarwinMMKV.setLogLevel(level.toDarwin())
}
actual fun defaultMMKV(): MMKV {
return MMKV(DarwinMMKV.defaultMMKV()!!)
}
actual fun defaultMMKV(config: MMKVConfig): MMKV {
return MMKV(DarwinMMKV.defaultMMKVWithConfig(config.toDarwinCValue())!!)
}
actual fun mmkvWithID(mmapID: String): MMKV {
return MMKV(DarwinMMKV.mmkvWithID(mmapID)!!)
}
actual fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV {
return MMKV(DarwinMMKV.mmkvWithID(mmapID, config = config.toDarwinCValue())!!)
}
actual fun pageSize(): Int {
return DarwinMMKV.pageSize().toInt()
}
actual fun version(): String {
return DarwinMMKV.version()
}
actual fun rootDir(): String {
return DarwinMMKV.mmkvBasePath()
}
actual fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String?): Boolean {
return DarwinMMKV.backupOneMMKV(mmapID, rootPath = rootPath, toDirectory = dstDir)
}
actual fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String?): Boolean {
return DarwinMMKV.restoreOneMMKV(mmapID, rootPath = rootPath, fromDirectory = srcDir)
}
actual fun backupAllToDirectory(dstDir: String): Long {
return DarwinMMKV.backupAll(null, toDirectory = dstDir).toLong()
}
actual fun restoreAllFromDirectory(srcDir: String): Long {
return DarwinMMKV.restoreAll(null, fromDirectory = srcDir).toLong()
}
actual fun isFileValid(mmapID: String, rootPath: String?): Boolean {
return DarwinMMKV.isFileValid(mmapID, rootPath = rootPath)
}
actual fun removeStorage(mmapID: String, rootPath: String?): Boolean {
return DarwinMMKV.removeStorage(mmapID, rootPath = rootPath)
}
actual fun checkExist(mmapID: String, rootPath: String?): Boolean {
return DarwinMMKV.checkExist(mmapID, rootPath = rootPath)
}
private var currentHandler: DarwinMMKVHandlerAdapter? = null
actual fun registerHandler(handler: MMKVHandler) {
val adapter = DarwinMMKVHandlerAdapter(handler)
currentHandler = adapter
@Suppress("DEPRECATION")
DarwinMMKV.registerHandler(adapter)
}
actual fun unRegisterHandler() {
currentHandler = null
DarwinMMKV.unregiserHandler()
}
}
// region Properties
actual val mmapID: String get() = impl.mmapID()
actual val isMultiProcess: Boolean get() = impl.isMultiProcess()
actual val isReadOnly: Boolean get() = impl.isReadOnly()
actual val totalSize: Long get() = impl.totalSize().toLong()
actual val actualSize: Long get() = impl.actualSize().toLong()
actual val count: Long get() = impl.count().toLong()
@Suppress("UNCHECKED_CAST")
actual val allKeys: List<String> get() = (impl.allKeys() as? List<String>) ?: emptyList()
actual val cryptKey: String?
get() {
val data = impl.cryptKey() ?: return null
return NSString.create(data = data, encoding = NSUTF8StringEncoding) as? String
}
// endregion
// region Encode
actual fun encodeBool(key: String, value: Boolean): Boolean = impl.setBool(value, forKey = key)
actual fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean = impl.setBool(value, forKey = key, expireDuration = expireDuration)
actual fun encodeInt(key: String, value: Int): Boolean = impl.setInt32(value, forKey = key)
actual fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean = impl.setInt32(value, forKey = key, expireDuration = expireDuration)
actual fun encodeLong(key: String, value: Long): Boolean = impl.setInt64(value, forKey = key)
actual fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean = impl.setInt64(value, forKey = key, expireDuration = expireDuration)
actual fun encodeFloat(key: String, value: Float): Boolean = impl.setFloat(value, forKey = key)
actual fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean = impl.setFloat(value, forKey = key, expireDuration = expireDuration)
actual fun encodeDouble(key: String, value: Double): Boolean = impl.setDouble(value, forKey = key)
actual fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean = impl.setDouble(value, forKey = key, expireDuration = expireDuration)
actual fun encodeString(key: String, value: String): Boolean = impl.setString(value, forKey = key)
actual fun encodeString(key: String, value: String, expireDuration: UInt): Boolean = impl.setString(value, forKey = key, expireDuration = expireDuration)
actual fun encodeBytes(key: String, value: ByteArray): Boolean {
val data = value.toNSData()
return impl.setData(data, forKey = key)
}
actual fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean {
val data = value.toNSData()
return impl.setData(data, forKey = key, expireDuration = expireDuration)
}
// endregion
// region Decode
actual fun decodeBool(key: String, defaultValue: Boolean): Boolean = impl.getBoolForKey(key, defaultValue = defaultValue)
actual fun decodeInt(key: String, defaultValue: Int): Int = impl.getInt32ForKey(key, defaultValue = defaultValue)
actual fun decodeLong(key: String, defaultValue: Long): Long = impl.getInt64ForKey(key, defaultValue = defaultValue)
actual fun decodeFloat(key: String, defaultValue: Float): Float = impl.getFloatForKey(key, defaultValue = defaultValue)
actual fun decodeDouble(key: String, defaultValue: Double): Double = impl.getDoubleForKey(key, defaultValue = defaultValue)
actual fun decodeString(key: String, defaultValue: String?): String? = impl.getStringForKey(key, defaultValue = defaultValue)
actual fun decodeBytes(key: String): ByteArray? {
val data = impl.getDataForKey(key) ?: return null
return data.toByteArray()
}
// endregion
// region Key management
actual fun containsKey(key: String): Boolean = impl.containsKey(key)
actual fun countNonExpiredKeys(): Long = impl.countNonExpiredKeys().toLong()
@Suppress("UNCHECKED_CAST")
actual fun allNonExpiredKeys(): List<String> = (impl.allNonExpiredKeys() as? List<String>) ?: emptyList()
actual fun removeValueForKey(key: String) = impl.removeValueForKey(key)
actual fun removeValuesForKeys(keys: List<String>) = impl.removeValuesForKeys(keys)
actual fun clearAll() = impl.clearAll()
actual fun clearAllKeepSpace() = impl.clearAllWithKeepingSpace()
// endregion
// region Encryption
actual fun reKey(newKey: String?, aes256: Boolean): Boolean {
val keyData = newKey?.toNSData()
return impl.reKey(keyData, aes256 = aes256)
}
actual fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean) {
val keyData = cryptKey?.toNSData()
impl.checkReSetCryptKey(keyData, aes256 = aes256)
}
// endregion
// region Utility
actual fun sync() = impl.sync()
actual fun async() = impl.async()
actual fun trim() = impl.trim()
actual fun close() = impl.close()
actual fun clearMemoryCache() = impl.clearMemoryCache()
actual fun importFrom(source: MMKV): Long = impl.importFrom(source.impl).toLong()
actual fun enableAutoKeyExpire(expiredInSeconds: UInt): Boolean = impl.enableAutoKeyExpire(expiredInSeconds)
actual fun disableAutoKeyExpire(): Boolean = impl.disableAutoKeyExpire()
actual fun enableCompareBeforeSet(): Boolean = impl.enableCompareBeforeSet()
actual fun disableCompareBeforeSet(): Boolean = impl.disableCompareBeforeSet()
actual fun checkContentChanged() = impl.checkContentChanged()
// endregion
}
// region Conversion helpers
@OptIn(ExperimentalForeignApi::class)
private fun MMKVLogLevel.toDarwin(): DarwinMMKVLogLevel = when (this) {
MMKVLogLevel.Debug -> MMKVLogDebug
MMKVLogLevel.Info -> MMKVLogInfo
MMKVLogLevel.Warning -> MMKVLogWarning
MMKVLogLevel.Error -> MMKVLogError
MMKVLogLevel.None -> MMKVLogNone
}
@OptIn(ExperimentalForeignApi::class)
private fun darwinLogLevelToCommon(level: DarwinMMKVLogLevel): MMKVLogLevel = when (level) {
MMKVLogDebug -> MMKVLogLevel.Debug
MMKVLogInfo -> MMKVLogLevel.Info
MMKVLogWarning -> MMKVLogLevel.Warning
MMKVLogError -> MMKVLogLevel.Error
MMKVLogNone -> MMKVLogLevel.None
else -> MMKVLogLevel.Info
}
@OptIn(ExperimentalForeignApi::class)
private fun MMKVRecoverStrategic.toDarwin(): DarwinMMKVRecoverStrategic = when (this) {
MMKVRecoverStrategic.OnErrorDiscard -> MMKVOnErrorDiscard
MMKVRecoverStrategic.OnErrorRecover -> MMKVOnErrorRecover
}
@OptIn(ExperimentalForeignApi::class)
private fun darwinRecoverToCommon(value: DarwinMMKVRecoverStrategic): MMKVRecoverStrategic = when (value) {
MMKVOnErrorRecover -> MMKVRecoverStrategic.OnErrorRecover
else -> MMKVRecoverStrategic.OnErrorDiscard
}
@OptIn(ExperimentalForeignApi::class)
private fun MMKVConfig.toDarwinCValue(): CValue<DarwinMMKVConfig> = cValue {
mode = when {
this@toDarwinCValue.mode and MMKVMode.READ_ONLY != 0 -> MMKVReadOnly
this@toDarwinCValue.mode and MMKVMode.MULTI_PROCESS != 0 -> MMKVMultiProcess
else -> MMKVSingleProcess
}
aes256 = this@toDarwinCValue.aes256
cryptKey = this@toDarwinCValue.cryptKey?.toNSData()
rootPath = this@toDarwinCValue.rootPath
expectedCapacity = this@toDarwinCValue.expectedCapacity.toULong()
enableKeyExpire = this@toDarwinCValue.enableKeyExpire?.let { NSNumber(bool = it) }
expiredInSeconds = this@toDarwinCValue.expiredInSeconds
enableCompareBeforeSet = this@toDarwinCValue.enableCompareBeforeSet
recover = this@toDarwinCValue.recover?.toDarwin() ?: MMKVOnErrorNotSet
itemSizeLimit = this@toDarwinCValue.itemSizeLimit
}
// endregion
// region NSData <-> ByteArray helpers
@OptIn(ExperimentalForeignApi::class)
private fun ByteArray.toNSData(): NSData {
if (isEmpty()) return NSData()
return usePinned { pinned ->
NSData.create(bytes = pinned.addressOf(0), length = size.toULong())
}
}
@OptIn(ExperimentalForeignApi::class)
private fun NSData.toByteArray(): ByteArray {
val length = this.length.toInt()
if (length == 0) return ByteArray(0)
val bytes = ByteArray(length)
bytes.usePinned { pinned ->
memcpy(pinned.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
}
return bytes
}
@OptIn(ExperimentalForeignApi::class)
private fun String.toNSData(): NSData {
@Suppress("CAST_NEVER_SUCCEEDS")
return (this as NSString).dataUsingEncoding(NSUTF8StringEncoding) ?: NSData()
}
// endregion
// region Handler adapter
/**
* Adapts the common [MMKVHandler] abstract class to the Darwin MMKVHandler ObjC protocol.
*/
@OptIn(ExperimentalForeignApi::class)
private class DarwinMMKVHandlerAdapter(
private val handler: MMKVHandler,
) : NSObject(), MMKVHandlerProtocol {
override fun onMMKVCRCCheckFail(mmapID: String?): DarwinMMKVRecoverStrategic {
return handler.onMMKVCRCCheckFail(mmapID ?: "").toDarwin()
}
override fun onMMKVFileLengthError(mmapID: String?): DarwinMMKVRecoverStrategic {
return handler.onMMKVFileLengthError(mmapID ?: "").toDarwin()
}
override fun mmkvLogWithLevel(level: DarwinMMKVLogLevel, file: kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar>?, line: Int, func: kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar>?, message: String?) {
val fileName = file?.toKString() ?: ""
val funcName = func?.toKString() ?: ""
handler.mmkvLog(darwinLogLevelToCommon(level), fileName, line, funcName, message ?: "")
}
override fun onMMKVContentChange(mmapID: String?) {
handler.onContentChangedByOuterProcess(mmapID ?: "")
}
override fun onMMKVContentLoadSuccessfully(mmapID: String?) {
handler.onMMKVContentLoadSuccessfully(mmapID ?: "")
}
}
// endregion
+67
View File
@@ -0,0 +1,67 @@
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
kotlin("plugin.compose") version "2.2.20"
id("com.android.application")
id("org.jetbrains.compose") version "1.8.1"
}
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
iosArm64()
iosSimulatorArm64()
iosX64()
cocoapods {
summary = "MMKV KMP Sample App"
homepage = "https://github.com/Tencent/MMKV"
version = "1.0.0"
ios.deploymentTarget = "15.0"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain {
dependencies {
implementation(project(":mmkv"))
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
}
}
androidMain {
dependencies {
implementation("androidx.activity:activity-compose:1.10.1")
}
}
}
}
android {
namespace = "com.tencent.mmkv.sample"
compileSdk = 35
defaultConfig {
applicationId = "com.tencent.mmkv.sample"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="MMKV KMP Sample"
android:supportsRtl="true"
android:theme="@style/Theme.MMKVSample">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,20 @@
package com.tencent.mmkv.sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.tencent.mmkv.kmp.MMKV
import com.tencent.mmkv.kmp.initialize
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Android-specific initialization (requires Context)
MMKV.initialize(this)
setContent {
App()
}
}
}
@@ -0,0 +1,5 @@
package com.tencent.mmkv.sample
actual fun platformSleep(millis: Long) {
Thread.sleep(millis)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MMKVSample" parent="@android:style/Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,118 @@
package com.tencent.mmkv.sample
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tencent.mmkv.kmp.MMKV
@Composable
fun App() {
var results by remember { mutableStateOf<List<String>>(emptyList()) }
val demo = remember { MMKVDemo() }
MaterialTheme {
Scaffold(
topBar = {
@OptIn(ExperimentalMaterial3Api::class)
TopAppBar(
title = {
Column {
Text("MMKV KMP Sample")
Text(
"MMKV Version: ${MMKV.version()}",
fontSize = 12.sp,
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
// Test buttons row
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Button(
onClick = { results = demo.functionalTest() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Functional Test")
}
Button(
onClick = { results = demo.testReKey() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Encryption Test")
}
Button(
onClick = { results = demo.testAutoExpire() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Auto Expiration Test")
}
Button(
onClick = { results = demo.testCompareBeforeSet() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Compare Before Set Test")
}
Button(
onClick = { results = demo.testBackupAndRestore() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Backup & Restore Test")
}
Button(
onClick = { results = demo.testRemoveStorageAndCheckExist() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Remove Storage & Check Exist")
}
}
Spacer(modifier = Modifier.height(8.dp))
// Results output
if (results.isNotEmpty()) {
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
Column(
modifier = Modifier
.padding(12.dp)
.verticalScroll(rememberScrollState())
) {
results.forEach { line ->
Text(
text = line,
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(vertical = 1.dp)
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,421 @@
package com.tencent.mmkv.sample
import com.tencent.mmkv.kmp.MMKV
import com.tencent.mmkv.kmp.MMKVConfig
import com.tencent.mmkv.kmp.MMKVExpireDuration
import com.tencent.mmkv.kmp.MMKVMode
/**
* Platform-agnostic MMKV demo logic, ported from the Flutter example app.
* Platform-specific initialization is handled in androidMain/iosMain before calling this.
*/
class MMKVDemo {
private val log = mutableListOf<String>()
private fun print(msg: String) {
log += msg
}
// region Functional Test
fun functionalTest(): List<String> {
log.clear()
// val mmkv = MMKV.defaultMMKV()
val mmkv = MMKV.mmkvWithID("functionalTest", MMKVConfig(MMKVMode.MULTI_PROCESS))
print("MMKV version: ${MMKV.version()}")
print("MMKV root dir: ${MMKV.rootDir()}")
print("Page size: ${MMKV.pageSize()}")
print("")
mmkv.encodeBool("bool", true)
print("bool = ${mmkv.decodeBool("bool")}")
mmkv.encodeInt("int32", Int.MAX_VALUE)
print("max int32 = ${mmkv.decodeInt("int32")}")
mmkv.encodeInt("int32", Int.MIN_VALUE)
print("min int32 = ${mmkv.decodeInt("int32")}")
mmkv.encodeLong("int", Long.MAX_VALUE)
print("max int = ${mmkv.decodeLong("int")}")
mmkv.encodeLong("int", Long.MIN_VALUE)
print("min int = ${mmkv.decodeLong("int")}")
mmkv.encodeDouble("double", Double.MAX_VALUE)
print("max double = ${mmkv.decodeDouble("double")}")
mmkv.encodeDouble("double", Double.MIN_VALUE)
print("min positive double = ${mmkv.decodeDouble("double")}")
var str = "Hello dart from MMKV"
mmkv.encodeString("string", str)
print("string = ${mmkv.decodeString("string")}")
mmkv.encodeString("string", "")
print("empty string = ${mmkv.decodeString("string")}")
print("contains \"string\": ${mmkv.containsKey("string")}")
str += " with bytes"
val bytes = str.encodeToByteArray()
mmkv.encodeBytes("bytes", bytes)
val decodedBytes = mmkv.decodeBytes("bytes")
print("bytes = ${decodedBytes?.decodeToString()}")
// test empty bytes
run {
val emptyBytes = ByteArray(0)
mmkv.encodeBytes("empty_bytes", emptyBytes)
val result = mmkv.decodeBytes("empty_bytes")
print("empty_bytes = ${result?.toList()}")
}
print("contains \"bool\": ${mmkv.containsKey("bool")}")
mmkv.removeValueForKey("bool")
print("after remove, contains \"bool\": ${mmkv.containsKey("bool")}")
mmkv.removeValuesForKeys(listOf("int32", "int"))
print("all keys: ${mmkv.allKeys}")
mmkv.trim()
mmkv.clearMemoryCache()
print("all keys: ${mmkv.allKeys}")
mmkv.clearAll()
print("all keys: ${mmkv.allKeys}")
mmkv.checkContentChanged()
print("is file valid: ${MMKV.isFileValid(mmkv.mmapID)}")
// Import test
testImport()
return log.toList()
}
private fun testMMKVImp(mmkv: MMKV, decodeOnly: Boolean) {
print("MMKV mmapID = ${mmkv.mmapID}")
print("string = ${mmkv.decodeString("string")}")
val str = "Hello dart from MMKV"
if (!decodeOnly) {
mmkv.encodeString("string", str)
}
if (!decodeOnly) {
mmkv.encodeBool("bool", true)
}
print("bool = ${mmkv.decodeBool("bool")}")
if (!decodeOnly) {
mmkv.encodeInt("int32", Int.MAX_VALUE)
}
print("max int32 = ${mmkv.decodeInt("int32")}")
if (!decodeOnly) {
mmkv.encodeInt("int32", Int.MIN_VALUE)
}
print("min int32 = ${mmkv.decodeInt("int32")}")
if (!decodeOnly) {
mmkv.encodeLong("int", Long.MAX_VALUE)
}
print("max int = ${mmkv.decodeLong("int")}")
if (!decodeOnly) {
mmkv.encodeLong("int", Long.MIN_VALUE)
}
print("min int = ${mmkv.decodeLong("int")}")
if (!decodeOnly) {
mmkv.encodeDouble("double", Double.MAX_VALUE)
}
print("max double = ${mmkv.decodeDouble("double")}")
if (!decodeOnly) {
mmkv.encodeDouble("double", Double.MIN_VALUE)
}
print("min positive double = ${mmkv.decodeDouble("double")}")
val bytesStr = "$str with bytes"
if (!decodeOnly) {
mmkv.encodeBytes("bytes", bytesStr.encodeToByteArray())
}
val decoded = mmkv.decodeBytes("bytes")
print("bytes = ${decoded?.decodeToString()}")
print("contains \"bool\": ${mmkv.containsKey("bool")}")
mmkv.removeValueForKey("bool")
print("after remove, contains \"bool\": ${mmkv.containsKey("bool")}")
mmkv.removeValuesForKeys(listOf("int32", "int"))
print("all keys: ${mmkv.allKeys}")
}
private fun testMMKV(mmapID: String, cryptKey: String?, aes256: Boolean, decodeOnly: Boolean, rootPath: String?): MMKV {
val config = MMKVConfig(cryptKey = cryptKey, aes256 = aes256, rootPath = rootPath)
val mmkv = MMKV.mmkvWithID(mmapID, config)
testMMKVImp(mmkv, decodeOnly)
return mmkv
}
private fun testImport() {
print("")
print("=== Import Test ===")
val src = MMKV.mmkvWithID("testImportSrc")
src.encodeBool("bool", true)
src.encodeInt("int", Int.MIN_VALUE)
src.encodeLong("long", Long.MAX_VALUE)
src.encodeString("string", "test import")
val dst = MMKV.mmkvWithID("testImportDst")
dst.clearAll()
dst.enableAutoKeyExpire(1u)
dst.encodeBool("bool", false)
dst.encodeInt("int", -1)
dst.encodeLong("long", 0)
dst.encodeString("string", "testImportSrc")
val count = dst.importFrom(src)
if (count != 4L || dst.count != 4L) {
print("MMKV: import check count fail (count=$count, dst.count=${dst.count})")
} else {
print("import count: $count")
}
if (!dst.decodeBool("bool")) print("MMKV: import check bool fail")
if (dst.decodeInt("int") != Int.MIN_VALUE) print("MMKV: import check int fail")
if (dst.decodeLong("long") != Long.MAX_VALUE) print("MMKV: import check long fail")
if (dst.decodeString("string") != "test import") print("MMKV: import check string fail")
print("import test passed")
}
// endregion
// region Encryption Test
fun testReKey(): List<String> {
log.clear()
print("=== Encryption Test ===")
val mmapID = "testAES_reKey1"
val kv = testMMKV(mmapID, null, false, false, null)
kv.reKey("Key_seq_1")
kv.clearMemoryCache()
print("")
print("--- after reKey to Key_seq_1 ---")
testMMKV(mmapID, "Key_seq_1", false, true, null)
kv.reKey("Key_Seq_Very_Looooooooong", aes256 = true)
kv.clearMemoryCache()
print("")
print("--- after reKey to AES256 ---")
testMMKV(mmapID, "Key_Seq_Very_Looooooooong", true, true, null)
kv.reKey(null)
kv.clearMemoryCache()
print("")
print("--- after reKey to null (remove encryption) ---")
testMMKV(mmapID, null, false, true, null)
print("")
print("encryption test passed")
return log.toList()
}
// endregion
// region Auto Expiration Test
fun testAutoExpire(): List<String> {
log.clear()
print("=== Auto Expiration Test ===")
// test enable auto expire by config
val config = MMKVConfig(enableKeyExpire = true, expiredInSeconds = 1u)
val mmkv = MMKV.mmkvWithID("test_auto_expire", config)
mmkv.clearAllKeepSpace()
mmkv.encodeBool("auto_expire_key_1", true)
mmkv.encodeInt("auto_expire_key_2", 1, 1u)
mmkv.encodeLong("auto_expire_key_3", 2L, 1u)
mmkv.encodeDouble("auto_expire_key_4", 3.0, 1u)
mmkv.encodeString("auto_expire_key_5", "hello auto expire", 1u)
mmkv.encodeBytes("auto_expire_key_6", "hello auto expire".encodeToByteArray(), 1u)
mmkv.encodeBool("never_expire_key_1", true, MMKVExpireDuration.Never)
print("before sleep: count = ${mmkv.count}")
print("sleeping 2 seconds for expiration...")
// sleep 2 seconds to let keys expire
platformSleep(2000)
print("auto_expire_key_1: ${mmkv.containsKey("auto_expire_key_1")}")
print("auto_expire_key_2: ${mmkv.containsKey("auto_expire_key_2")}")
print("auto_expire_key_3: ${mmkv.containsKey("auto_expire_key_3")}")
print("auto_expire_key_4: ${mmkv.containsKey("auto_expire_key_4")}")
print("auto_expire_key_5: ${mmkv.containsKey("auto_expire_key_5")}")
print("auto_expire_key_6: ${mmkv.containsKey("auto_expire_key_6")}")
print("never_expire_key_1: ${mmkv.containsKey("never_expire_key_1")}")
print("")
print("count non-expired keys: ${mmkv.countNonExpiredKeys()}")
print("all non-expired keys: ${mmkv.allNonExpiredKeys()}")
return log.toList()
}
// endregion
// region Compare Before Set Test
fun testCompareBeforeSet(): List<String> {
log.clear()
print("=== Compare Before Set Test ===")
val config = MMKVConfig(expectedCapacity = 4096 * 2)
val mmkv = MMKV.mmkvWithID("testCompareBeforeSet", config)
mmkv.enableCompareBeforeSet()
mmkv.encodeString("key", "extra")
// int test
run {
val key = "int"
val v = 12345
mmkv.encodeInt(key, v)
val actualSize = mmkv.actualSize
print("actualSize = $actualSize")
print("v = ${mmkv.decodeInt(key)}")
mmkv.encodeInt(key, v) // same value
val actualSize2 = mmkv.actualSize
print("actualSize after same write = $actualSize2")
if (actualSize2 != actualSize) {
print("FAIL: size changed for same value!")
} else {
print("OK: size unchanged for same value")
}
mmkv.encodeInt(key, v * 23) // different value
print("actualSize after different write = ${mmkv.actualSize}")
print("v = ${mmkv.decodeInt(key)}")
}
// string test
print("")
run {
val key = "string"
val v = "w012A\uD83C\uDFCA\uD83C\uDFFBgood"
mmkv.encodeString(key, v)
val actualSize = mmkv.actualSize
print("actualSize = $actualSize")
print("v = ${mmkv.decodeString(key)}")
mmkv.encodeString(key, v) // same value
val actualSize2 = mmkv.actualSize
print("actualSize after same write = $actualSize2")
if (actualSize2 != actualSize) {
print("FAIL: size changed for same value!")
} else {
print("OK: size unchanged for same value")
}
mmkv.encodeString(key, "temp data \uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFEB")
print("actualSize after different write = ${mmkv.actualSize}")
print("v = ${mmkv.decodeString(key)}")
}
return log.toList()
}
// endregion
// region Backup & Restore Test
fun testBackupAndRestore(): List<String> {
log.clear()
print("=== Backup & Restore Test ===")
val rootDir = MMKV.rootDir()
val parentDir = rootDir.substringBeforeLast('/')
val backupRootDir = "$parentDir/mmkv_backup_3"
val mmapID = "test/AES"
val cryptKey = "Tencent MMKV"
val otherDir = "$parentDir/mmkv_3"
testMMKV(mmapID, cryptKey, false, false, otherDir)
val ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, otherDir)
print("")
print("backup one [$mmapID] return: $ret")
val backupAllDir = "$parentDir/mmkv_backup"
val count = MMKV.backupAllToDirectory(backupAllDir)
print("backup all count: $count")
// Restore
print("")
print("--- Restore ---")
val kv = MMKV.mmkvWithID(mmapID, MMKVConfig(cryptKey = cryptKey, rootPath = otherDir))
kv.encodeString("test_restore", "value before restore")
print("before restore [${kv.mmapID}] allKeys: ${kv.allKeys}")
val restoreRet = MMKV.restoreOneFromDirectory(mmapID, backupRootDir, otherDir)
print("restore one [${kv.mmapID}] ret = $restoreRet")
if (restoreRet) {
print("after restore [${kv.mmapID}] allKeys: ${kv.allKeys}")
}
val restoreAllDir = "$parentDir/mmkv_backup"
val restoreCount = MMKV.restoreAllFromDirectory(restoreAllDir)
print("restore all count $restoreCount")
if (restoreCount > 0) {
val defaultKV = MMKV.defaultMMKV()
print("check restore [${defaultKV.mmapID}] allKeys: ${defaultKV.allKeys}")
}
return log.toList()
}
// endregion
// region Remove Storage & Check Exist Test
fun testRemoveStorageAndCheckExist(): List<String> {
log.clear()
print("=== Remove Storage & Check Exist Test ===")
var mmapID = "test_remove"
run {
val mmkv = MMKV.mmkvWithID(mmapID, MMKVConfig(mode = MMKVMode.MULTI_PROCESS))
mmkv.encodeBool("bool", true)
}
print("check exist = ${MMKV.checkExist(mmapID)}")
MMKV.removeStorage(mmapID)
print("after remove, check exist = ${MMKV.checkExist(mmapID)}")
run {
val mmkv = MMKV.mmkvWithID(mmapID, MMKVConfig(mode = MMKVMode.MULTI_PROCESS))
if (mmkv.count != 0L) {
print("storage not successfully removed")
} else {
print("storage successfully removed")
}
}
print("")
mmapID = "test_remove/sg"
val rootDir = "${MMKV.rootDir()}_sg"
var mmkv = MMKV.mmkvWithID(mmapID, MMKVConfig(rootPath = rootDir))
mmkv.encodeBool("bool", true)
print("check exist = ${MMKV.checkExist(mmapID, rootDir)}")
MMKV.removeStorage(mmapID, rootDir)
print("after remove, check exist = ${MMKV.checkExist(mmapID, rootDir)}")
mmkv = MMKV.mmkvWithID(mmapID, MMKVConfig(rootPath = rootDir))
if (mmkv.count != 0L) {
print("storage not successfully removed")
} else {
print("storage successfully removed")
}
return log.toList()
}
// endregion
}
@@ -0,0 +1,3 @@
package com.tencent.mmkv.sample
expect fun platformSleep(millis: Long)
@@ -0,0 +1,24 @@
package com.tencent.mmkv.sample
import androidx.compose.ui.window.ComposeUIViewController
import com.tencent.mmkv.kmp.MMKV
import com.tencent.mmkv.kmp.initialize
import platform.Foundation.NSFileManager
fun MainViewController() = ComposeUIViewController {
App()
}
fun initializeMMKV() {
// Retrieve the shared App Group container path for multi-process MMKV access.
// This requires the "App Groups" entitlement configured in Xcode with matching identifier.
val groupDir = NSFileManager.defaultManager
.containerURLForSecurityApplicationGroupIdentifier("group.tencent.mmkv")
?.path
if (groupDir != null) {
MMKV.initialize(groupDir = groupDir)
} else {
MMKV.initialize()
}
}
@@ -0,0 +1,7 @@
package com.tencent.mmkv.sample
import platform.posix.usleep
actual fun platformSleep(millis: Long) {
usleep((millis * 1000).toUInt())
}
+9
View File
@@ -0,0 +1,9 @@
source 'https://cdn.cocoapods.org'
platform :ios, '15.0'
target 'iosApp' do
use_frameworks!
pod 'composeApp', :path => '../composeApp'
pod 'MMKV', '~> 2.4.0'
end
@@ -0,0 +1,376 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
058557D9273AAEEB004C7B11 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* iOSApp.swift */; };
7391C56D8E2CE0650C4F9123 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45F2E8507A4C3BC870FEB494 /* Pods_iosApp.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
058557D8273AAEEB004C7B11 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
2599BC3579F0BB1AFB228FAC /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = "<group>"; };
45F2E8507A4C3BC870FEB494 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
A1B2C3D4E5F60001 /* iosApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosApp.entitlements; sourceTree = "<group>"; };
E90F2910DC86776F271E36B9 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
7555FF79242A565900829871 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7391C56D8E2CE0650C4F9123 /* Pods_iosApp.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
058557D7273AAEEB004C7B11 /* iosApp */ = {
isa = PBXGroup;
children = (
058557D8273AAEEB004C7B11 /* iOSApp.swift */,
A1B2C3D4E5F60001 /* iosApp.entitlements */,
);
path = iosApp;
sourceTree = "<group>";
};
5EA0BB31BC116CFF3F6DB702 /* Frameworks */ = {
isa = PBXGroup;
children = (
45F2E8507A4C3BC870FEB494 /* Pods_iosApp.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
7555FF72242A565900829871 = {
isa = PBXGroup;
children = (
058557D7273AAEEB004C7B11 /* iosApp */,
7555FF7C242A565900829871 /* Products */,
C0AF7F7AD3EDEF43E551B988 /* Pods */,
5EA0BB31BC116CFF3F6DB702 /* Frameworks */,
);
sourceTree = "<group>";
};
7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup;
children = (
7555FF7B242A565900829871 /* iosApp.app */,
);
name = Products;
sourceTree = "<group>";
};
C0AF7F7AD3EDEF43E551B988 /* Pods */ = {
isa = PBXGroup;
children = (
2599BC3579F0BB1AFB228FAC /* Pods-iosApp.debug.xcconfig */,
E90F2910DC86776F271E36B9 /* Pods-iosApp.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
7555FF7A242A565900829871 /* iosApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
0CAE9018171FE65CAE2ADB20 /* [CP] Check Pods Manifest.lock */,
7555FF77242A565900829871 /* Sources */,
7555FF79242A565900829871 /* Frameworks */,
0FCF1DF3C47C2ED870C086D4 /* [CP] Embed Pods Frameworks */,
23976C90E20A1A4C83F0B17A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = iosApp;
productName = iosApp;
productReference = 7555FF7B242A565900829871 /* iosApp.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7555FF73242A565900829871 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1540;
};
buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7555FF72242A565900829871;
productRefGroup = 7555FF7C242A565900829871 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
7555FF7A242A565900829871 /* iosApp */,
);
};
/* End PBXProject section */
/* Begin PBXShellScriptBuildPhase section */
0CAE9018171FE65CAE2ADB20 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
0FCF1DF3C47C2ED870C086D4 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
23976C90E20A1A4C83F0B17A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
7555FF77242A565900829871 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
058557D9273AAEEB004C7B11 /* iOSApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
7555FFA3242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
7555FFA4242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
7555FFA6242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2599BC3579F0BB1AFB228FAC /* Pods-iosApp.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = SG5PVJM4JW;
INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.tencent.mmkvdemo;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MMKVDemo;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
7555FFA7242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E90F2910DC86776F271E36B9 /* Pods-iosApp.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = SG5PVJM4JW;
INFOPLIST_FILE = "$(SRCROOT)/iosApp/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.tencent.mmkvdemo;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MMKVDemo;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7555FFA3242A565B00829871 /* Debug */,
7555FFA4242A565B00829871 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7555FFA6242A565B00829871 /* Debug */,
7555FFA7242A565B00829871 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7555FF73242A565900829871 /* Project object */;
}
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MMKV KMP Sample</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,5 @@
{
"sourceLanguage" : "en",
"strings" : {},
"version" : "1.0"
}
+23
View File
@@ -0,0 +1,23 @@
import SwiftUI
import ComposeApp
@main
struct iOSApp: App {
init() {
MainViewControllerKt.initializeMMKV()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.tencent.mmkv</string>
</array>
</dict>
</plist>
+20
View File
@@ -0,0 +1,20 @@
rootProject.name = "MMKV-KMP"
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
include(":mmkv")
include(":sample:composeApp")