mirror of
https://github.com/Tencent/MMKV.git
synced 2026-10-16 20:21:12 +08:00
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:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"preferences": {
|
||||
"git_commit_no_co_author": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.gradle
|
||||
build
|
||||
.kotlin
|
||||
local.properties
|
||||
*.podspec
|
||||
Pods
|
||||
Podfile.lock
|
||||
*.xcworkspace
|
||||
*.xcuserdata
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
+172
@@ -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" "$@"
|
||||
Vendored
+84
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
+24
@@ -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())
|
||||
}
|
||||
@@ -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 */;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,20 @@
|
||||
rootProject.name = "MMKV-KMP"
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
include(":mmkv")
|
||||
include(":sample:composeApp")
|
||||
Reference in New Issue
Block a user