搜索
开发文档
应用开发
快应用开发
小游戏开发
开发文档/应用开发/服务能力/CloudKit/客户端接入指南
客户端接入指南更新时间: 2025-04-27 16:28:00

一、使用说明

CloudKit是提供给App接入小米云服务的整套接入API和工具套件,开发者不需要开发复杂的客户端和服务器代码,就可以轻松实现数据和文档的存储、备份与同步、认证与授权、隐私保护、共享等功能

使用CloudKit接入云服务,具有良好的安全性、可靠性、可扩展性、便捷性、低成本等特点

使用CloudKit可以做到:

  • 应用的本地数据库里的数据自动与云端同步
  • 应用的本地文件夹内容与自动与云端同步
  • 如果用户登录多个设备,数据也自动在多个设备之间实时同步

CloudKit特点:

  1. 开发快:客户端几行代码可实现数据上云功能,无需开发服务端代码
  2. 高安全:数据上云传输和存储过程经过多重加密保护,云端多重安全校验技术保障数据不丢、不串
  3. 可伸缩:按需分配云端资源,支持海量用户数据存储
  4. 跨平台:支持pad、可穿戴、车机应用等数据上云场景

总之,CloudKit是一种强大的云服务,它提供了许多优势,包括集成性、安全性、可扩展性和低成本。与自己的服务器相比,它是一种更简单、更经济、更可靠的选择

二、开发流程

三、使用约束

自增主键的多设备同步问题

  • 请参考:开发步骤-数据库适配-自增主键的多设备同步问题与解决方案

四、开发步骤

添加 App Id 和构建版本 Debug 标识

  • App Id

com.xiaomi.xms.APP_ID
当开通服务时,会生成 App Id 唯一标识,用于鉴权
必须配置 App Id,调用服务时会通过 App Id 进行鉴权

  • 构建版本 Debug 标识

com.xiaomi.xms.BUILD_TYPE_DEBUG
当前 APK 是否为 Debug 构建版本。若不配置,默认为 false

备注:鉴权时会验证应用 apk 签名。开发调试阶段,一般不会运行 Release 签名的 apk,通常都是运行 Debug 签名 apk,为了保证在开发阶段仍然可以鉴权成功,开发者可以在平台同时添加正式证书指纹(Release 签名)和测试证书指纹(Debug 签名),用于不同场景下鉴权。

  • 在应用的 AndroidManifest.xml 中添加 meta-data 配置
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>

<meta-data
android:name="com.xiaomi.xms.APP_ID"
android:value="your app id here" />

<meta-data
android:name="com.xiaomi.xms.BUILD_TYPE_DEBUG"
android:value="true or false" />

</application>
</manifest>

1、后台配置数据库&文件,获取ckconfig.xml

如何获取可参考开发者后台使用指南:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2018

2、将上述配置文件ckconfig.xml放到 ../res/xml 文件夹下

3、配置鉴权meta-data

a. 添加 app id 和构建版本 debug 标识
app id
三方应用后台开通服务时,会生成 app id 唯一标识,用于鉴权

三方应用必须配置 app id,鉴权服务会通过 app id 进行鉴权,以保证鉴权流程更加高效和安全

构建版本 debug 标识
三方应用当前 APK 是否为 debug 构建版本
鉴权服务会验证三方应用 APK 签名。开发者在开发调试阶段,一般不会运行 release 签名的 APK,通常都是运行 debug 签名 APK,为了保证在开发阶段仍然可以正常(鉴权成功)调用开放能力,三方开发者可以在后台上传 release 签名和 debug 签名(debug 签名会有一定调用频率的限制)用于鉴权验证

b.在三方应用的 AndroidManifest.xml 中进行配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>

<meta-data
android:name="com.xiaomi.xms.APP_ID"
android:value="your app id here" />

<meta-data
android:name="com.xiaomi.xms.BUILD_TYPE_DEBUG"
android:value="true or false" />

</application>

</manifest>

4、引入SDK包

a. 使用下述网盘下载CloudKitSDK及Sqlite数据库SDK aar,并放到libs下

名称:CloudKitSDK-1.0.1
地址:https://kpan.mioffice.cn/webfolder/ext/kW92DBcM0Jn%24uVm31GQvyw%40%40?n=0.7964614660524483
密码:yRJ1

b. 模块build.gradle引入(app:gradle)

fileTree(dir: 'libs', include: ['*.aar']).each { aarFile ->
implementation files(aarFile)
}
implementation 'com.google.code.gson:gson:2.8.9'

5、数据库适配

如果配置了数据库同步,需要如下修改:
a. 包引用替换

i.替换所有的 android.database.sqliteorg.sqlite.database.sqlite,例如:

import android.database.sqlite.SQLiteDatabase;

import org.sqlite.database.sqlite.SQLiteDatabase;

ii.替换这两个包引用

android.database.SQLException 
android.database.DatabaseErrorHandler

替换为

org.sqlite.database.SQLException
org.sqlite.database.DatabaseErrorHandler

b. Room适配(如果使用的是ROOM数据库框架,需要做以下适配)

在初始化 Room 数据库时,需要做一下适配

i. openHelperFactory 使用 SQLiteOrgOpenHelperFactory

private fun buildDatabase(context: Context): NoteDatabase {
return Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"xxxxx.db"
)
.openHelperFactory(SQLiteOrgOpenHelperFactory)
.build()
}

ii. SDK 初始化需要传入转换之后的 openHelper

(openHelper as CustomOpenHelper).delegateHelper

Room的适配具体可以参考 Demo 代码

c. 自增主键的多设备同步问题与解决方案

在多设备同步环境下,使用本地自增主键(Auto Increment ID)可能导致数据错乱。例如,在 category 表中,主键 id 采用本地自增,而 notes 表的 categoryId 依赖于 category 表的 id,各个设备端新增的 category 数据可能会生成相同的主键 id,小米Cloudkit默认是不同步主键的,所以本地相同的主键id没问题,但是由于 notes 表的 categoryId 依赖于 category 表的 id,从而导致同步后的 notes 表中的 categoryId 指向错误的数据。

两种解决方案,app根据自己的情况选择

i. 使用全局唯一键值生成器生成的uuid替换原有的自增主键
1. 新开发的 App(无线上存留数据)

如果是全新的应用,不存在历史数据,需要在 CloudKit 开发者后台 配置被依赖的表的自增主键,并在插入数据时,调用 CKUtil.generateUniqueID() 生成唯一 id。

示例代码:

ContentValues values = new ContentValues();
Long cloudId = CKUtil.generateUniqueID();
values.put("id", cloudId);
...
values.put("原有的其他字段名", 原有的其他字段value);
...
db.insert("category", null, values);

2. 已存在线上数据的 App

对于已有用户的应用,除了被依赖的表插入新数据时要使用 generateUniqueID() 生成 id 之外,还需要在数据库 onUpgrade 过程中,对已有数据进行主键迁移。
新增一个数据库版本, 并做onUpgrade ,在 onUpgrade 方法中,传入被依赖的表主键列名 及其 被依赖的关系,并调用 CKUtil.handlePrimaryKeyMigration() 进行迁移。
示例代码:

const val DATABASE_VERSION = 2

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d(TAG, "onUpgrade: ")

if (oldVersion < 2) {
//关系需要做迁移,请参考以下数据库升级代码
val relationMap: MutableMap<String, Pair<String, List<Pair<String, String>>>> =
HashMap()
relationMap["category"] = Pair(
"id", listOf(
Pair("notes", "categoryId")
)
) //给出category的主键id以及依赖该字段的表名和字段名列表[(notes, categoryId)]

CKUtil.handlePrimaryKeyMigration(db, relationMap)
}
}

在 relationMap 中:

- category 表的 id 需要重新生成,同时 notes 表的 categoryId 依赖于 category 表,需要做迁移。

- handlePrimaryKeyMigration 方法会遍历 relationMap,自动更新主键及其依赖关系。

修改后的数据如下图所示:

注意事项

⚠️ 全局唯一 ID 会打破主键的自增顺序

请注意,使用 generateUniqueID() 生成的主键是全局唯一的,而非本地自增的数字。由于它是基于 UUID 生成的,因此不会按照传统的自增顺序递增。因此,在使用全局唯一主键时,请不要依赖主键的顺序,特别是在展示数据或排序时,需根据其他字段(例如时间戳)来实现排序。

ii. 解决方案2:新增 sync_id 列来替代自增主键作为外键

1. 需要在数据库的 onUpgrade 中,定义所有原本使用本地自增主键的表和它们的外键依赖关系,并调用 CKUtil.handleSyncIdMigration() 进行新增数据迁移。

const val DATABASE_VERSION = 2

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d(TAG, "onUpgrade: ")

if (oldVersion < 2) {
//方案2:新增一列 sync_id,以该列作为外键
val relationMap: Map<String, Pair<String, List<Pair<String, String>>>> = mapOf(
"category" to Pair(
"sync_id", listOf(
Pair("notes", "categoryId")
)
)
)
//该函数里会在category表里新增一列sync_id,并填入值,并把notes表的categoryId列填入同样的值
CKUtil.handleSyncIdMigration(db, relationMap)
}
}

2. 在被依赖的表插入数据时,调用 CKUtil 的 generateUniqueID 方法生成一个唯一的sync_id并插入

ContentValues values = new ContentValues();
Long cloudId = Util.INSTANCE.generateUniqueID();
values.put("sync_id", cloudId);
db.insert("category", null, values);

3. 修改依赖的地方代码逻辑: 改用 sync_id 作为外键,而不是原来的自增主键。

修改后的数据如下图所示:

比较方案1和方案2

特性方案1:全局唯一主键替换自增主键方案2:新增sync_id列作为外键
核心思想使用全局唯一的主键替代自增主键来避免冲突新增sync_id列,其他表修改依赖从该表主键到sync_id列,来避免冲突
适用场景适合主键的自增不重要,不依赖用主键排序的场景适合主键的自增重要,依赖用主键排序的场景
迁移难度改动比较少,只需要修改被依赖的表插入主键的地方,依赖关系没有变动改动比较多,依赖关系变了,需修改更多代码,插入依赖的地方都需要修改
开发者平台配置区别1.被依赖(改造过)的表的主键需要配置到开发者平台进行同步,不配置的话同步会有问题
2.其他表的主键(没有被依赖)不需要配置到开发者平台进行同步,配置了反而有问题
所有表的主键都不需要配置到开发者平台进行同步,配置了反而有问题

6、其他步骤

a. api使用参考第六部分-接口说明

b. 上述开发步骤涉及事项可下载以下demo工程参考

名称:SDK使用demo-SDK1.0.1-20250221.zip
地址:https://kpan.mioffice.cn/webfolder/ext/s7aj4K9l9Vf%24uVm31GQvyw%40%40?n=0.7345463615557906
密码:T4p9

五、调测验证

CKManager.init() 如果没有特殊的错误码,则表示正常接入

六、 接口说明

1、 API接口文档

1.1、接口概述

1.初始化接口:CKManager.init()

函数和用到的类型定义:

  /**
* @param context : Android Context
* @param config : CKManagerConfig 设置文件数据库的冲突策略、网络策略及数据库跳过某些行数据
* 不同步的策略
* @param initializationListener : SDK配置及初始化回调接口
* @param eventListener : SDK各类事件回调,如SDK同步中报错的回调
* @param helpers :
* 1. 若要同步数据库:则需要传入 SQLiteOpenHelper ,支持传入多个
* 2. 若仅同步文件:此参数可直接忽略,仅传入前三个参数即可
*/
fun init(context: Context, config: CKManagerConfig,
initializationListener: InitializationListener,
eventListener: EventListener,
vararg helpers: SQLiteOpenHelper)

// 参数解释:CKManagerConfig
class CKManagerConfig{
// 设置数据冲突策略
fun setConflictStrategy(strategy: ConflictStrategy)


/*可构建一个查询条件来筛选特定的记录,设置数据库跳过某行数据不同步
DatabaseConditionBuilder 是一个用于构建 SQL 查询条件的工具类。它允许开发者链式调用方法来组合各种条件,例如 equal, notEqual, greaterThan 等,最终生成 SQL 查询的条件字符串。它支持多种 SQL 运算符,包括等于、不等于、大于、小于、LIKE 模糊匹配等*/
fun setDatabaseSkipConditions(Map<tableName: String, databaseConditionBuilder: DababaseConditionBuilder>)

// 设置同步时网络策略
fun setNetworkStrategy(strategy: NetworkStrategy)

/**
* 网络策略可选如下enum
* 1. WIFI_ONLY :只有在WIFI下才能同步数据
* 2. ALL_NETWORK :任意网络类型都可同步数据
*/
enum class NetworkStrategy(val value: Int) {
ALL_NETWORK,
WIFI_ONLY
}

/**
* 冲突策略可选如下enum
* 1. LOCAL_FIRST :数据以本地数据为准,保留本地数据
* 2. CLOUD_FIRST :数据以云端数据为准,保留云端数据
* 3. KEEP_BOTH :数据保留本地和云数据,两份数据都下载
*/
enum class ConflictStrategy {
LOCAL_FIRST,
CLOUD_FIRST,
KEEP_BOTH
}
}

// SDK初始化监听
interface InitializationListener {

// 初始化成功
fun onInitSuccess()

// 初始化失败,带错误代码
fun onInitFail(errorCode: Int, errorMsg: String)
}

// SDK各个事件回调
interface EventListener {
// SDK 事件(会多次回调)
fun onEvent(event : Event)
}

// 回调事件封装
sealed class Event {
data class ErrorEvent(
val errorCode: Int,
val errorMsg: String
) : Event()
}

参数解释:

  • context:Android Context,用于提供上下文环境。
  • config:CKManagerConfig 设置文件,可设置数据库的冲突策略、网络策略及数据库跳过某些行数据不同步的策略。
  • initializationListener:SDK 配置及初始化回调接口,包含初始化成功和失败的回调方法。
  • eventListener:SDK 各类事件回调,如 SDK 同步中报错的回调。
  • helpers:若要同步数据库,则需要传入 SQLiteOpenHelper,支持传入多个;若仅同步文件,此参数可直接忽略,仅传入前三个参数即可。

注意事项:仅支持在主进程初始化。

代码示例:

CKManager.init(
baseContext,
CKManagerConfig
.builder()
// 设置网络策略,可选
.setNetworkStrategy(NetworkStrategy.ALL_NETWORK)
// 设置数据冲突策略,可选
.setConflictStrategy(ConflictStrategy.LOCAL_FIRST)
// 设置数据库跳过某些行不同步,可选,示例的跳过的记录满足条件:select * from notes where noteText = '我不上云' OR done = 0 AND value > 6000 AND noteText != '上云' AND value < 10000 AND noteText LIKE '%aa%' AND noteText IS NULL
.setDatabaseSkipConditions(
mutableMapOf<String, DatabaseConditionBuilder>(
"notes" to DatabaseConditionBuilder()
.equal("noteText", "我不上云")
.or()
.equal("done", 0)
.and()
.greaterThan("value", 6000)
.and()
.notEqual("noteText", "上云")
.and()
.lessThan("value", 10000)
.and()
.like("noteText", "%aa%")
.and()
.isNull("noteText")
)
)
.build(),
//SDK初始化监听,回调初始化的成功或错误
object : InitializationListener {
override fun onInitSuccess() {
CKManager.setCTAAgreed(true)
CKManager.observeCloudDataChanges(object : CloudDataChangeListener {
override fun onDatabaseDataChanged(databaseEvent: DatabaseDataChangeEvent) {

Log.d(TAG, "onDatabaseDataChanged , event = $databaseEvent")
for (databaseDataChangeRecord in databaseEvent.dataChanges) {
when (databaseDataChangeRecord.databaseOpType) {
DatabaseOpType.INSERT -> {
// logic
}

DatabaseOpType.UPDATE -> {
// logic
}

DatabaseOpType.DELETE -> {
// logic
}
}
}
}

override fun onFileDataChanged(fileEvent: FileDataChangeEvent) {

Log.d(TAG, "onFileDataChanged , event = $fileEvent")
when (fileEvent.fileOpType) {
FileOpType.UPDATE -> {
// logic
}

FileOpType.DELETE -> {
// logic
}
}
}
})
}

override fun onInitFail(errorCode: Int, errorMsg: String) {
Log.d(TAG, "onInitFail , errorCode = $errorCode , errorMsg = $errorMsg")
}
},
// SDK各种事件回调,比如同步中的错误回调
object : EventListener {
override fun onEvent(event: Event) {

Log.d(TAG, "onEvent changed,event = $event")
when (event) {
is Event.ErrorEvent -> {
Log.e(TAG, "CKManager Event.ErrorEvent error code = ${event.errorCode} , error msg = ${event.errorMsg}")
}
}
}
},
NoteRepositoryFactory.getSQLiteOpenHelper1()
)

2. 同意隐私协议接口:CKManager.setCTAAgreed()

函数定义:

fun setCTAAgreed(agreed: Boolean = true)

参数解释:

  • agreed:代表用户是否同意隐私条款,默认值为 true。true 表示可以同步数据到服务端存储,false 表示不可以同步数据到服务端存储。

示例代码:

CKManager.setCTAAgreed(true)

3. 触发同步接口:CKManager.requestSync()

函数定义:

fun requestSync(force: Boolean = false)

参数解释:

  • force:若传入 true,代表强制触发同步;若传入 false,代表以同步调度限制为准,同步时机需符合调度管理。

示例代码:

CKManager.requestSync()

4. 监听云数据变化接口:CKManager.observeCloudDataChanges

函数定义:

fun observeCloudDataChanges(listener: CloudDataChangeListener)

参数解释:

  • listener:CloudDataChangeListener 接口的实现,用于监听云端数据是否发生变化,包含数据库数据变化和文件数据变化的回调方法。

代码示例:

// 监听云端数据发生变更后,已经将数据同步到本地,此时回调此接口
CKManager.observeCloudDataChanges(object : CloudDataChangeListener {
override fun onDatabaseDataChanged(databaseEvent: DatabaseDataChangeEvent) {

Log.d(TAG, "onDatabaseDataChanged , event = $databaseEvent")

for (databaseDataChangeRecord in databaseEvent.dataChanges) {
when (databaseDataChangeRecord.databaseOpType) {
DatabaseOpType.INSERT -> {
// logic
}

DatabaseOpType.UPDATE -> {
// logic
}

DatabaseOpType.DELETE -> {
// logic
}
}
}
}

override fun onFileDataChanged(fileEvent: FileDataChangeEvent) {

Log.d(TAG, "onFileDataChanged , event = $fileEvent")

when (fileEvent.fileOpType) {
FileOpType.UPDATE -> {
// logic
}

FileOpType.DELETE -> {
// logic
}
}
}
})

// 数据库数据改变
data class DatabaseDataChangeEvent(
val databaseName: String,
val dataChanges: List<DataChangeRecord>
)

data class DataChangeRecord(
val tableName: String,
val rowId: Long,
val databaseOpType: DatabaseOpType
)

// 文件数据改变
data class FileDataChangeEvent(
val fileAbsPath: String, //发生变化的文件绝对路径
val fileOpType: FileOpType // 发生改变的类型
)

5. 生成全局唯一键值方法接口:CKUtil.generateUniqueID()

函数定义:

//该接口用于生成全局唯一主键,可用于数据库插入操作
fun generateUniqueID(): Long

代码示例:

ContentValues values = new ContentValues();
Long cloudId = CKUtil.generateUniqueID();
values.put("id", cloudId);
...
values.put("原有的其他字段名", 原有的其他字段value);
...
db.insert("表名", null, values);

6. 数据库改造主键迁移接口(方案1):CKUtil.handlePrimaryKeyMigration()

函数定义:

//对已有数据进行主键迁移,以确保历史数据不会发生错乱。
fun handlePrimaryKeyMigration(
db: SQLiteDatabase,
relationMap: Map<String, Pair<String, List<Pair<String, String>>>>,
excludedIdsMap: Map<String, Set<Long>> = emptyMap()
)

参数解释:

  • db: SQLiteDatabase:要进行主键迁移的 SQLite 数据库实例,用于执行 SQL 操作修改表数据。
  • relationMap: Map<String, Pair<String, List<Pair<String, String>>>>:定义需主键迁移的表及其外键依赖关系,键为表名,值含自增主键的字段名及依赖信息。
  • excludedIdsMap: Map<String, Set<Long>> = emptyMap():可选参数,指定迁移时需排除的主键 ID,比如这些id是多设备上固定的,避免不必要修改。

示例代码

const val DATABASE_VERSION = 2

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d(TAG, "onUpgrade: ")

if (oldVersion < 2) {
//自增id以及关系需要做迁移,请参考以下数据库升级代码
val relationMap: MutableMap<String, Pair<String, List<Pair<String, String>>>> =
HashMap()
relationMap["category"] = Pair(
"id", listOf(
Pair("notes", "categoryId")
)
) //给出category的主键id以及依赖该字段的表名和字段名列表[(notes, categoryId)]

CKUtil.handlePrimaryKeyMigration(db, relationMap)
}
}

7. 数据库新增一列迁移接口(方案2):CKUtil.handleSyncIdMigration()

函数定义:

fun handleSyncIdMigration(
db: SQLiteDatabase,
relationMap: Map<String, Pair<String, List<Pair<String, String>>>>,
excludedIdsMap: Map<String, Set<Long>> = emptyMap()
)

参数解释:

  • db: SQLiteDatabase:要进行迁移的 SQLite 数据库实例,用于执行 SQL 操作修改表数据。
  • relationMap: Map<String, Pair<String, List<Pair<String, String>>>>:定义需新增一列迁移的表及其外键依赖关系,键为表名,值为新增的关联列的字段名及依赖表信息,确保数据一致性。
  • excludedIdsMap: Map<String, Set<Long>> = emptyMap():可选参数,指定迁移时需排除的主键 id,比如这些id是多设备上固定的,直接拷贝到sync_id列,避免不必要修改。

示例代码

const val DATABASE_VERSION = 2

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d(TAG, "onUpgrade: ")

if (oldVersion < 2) {
//在category表上新增一列 sync_id,以该列作为外键,请参考以下数据库升级代码
val relationMap: MutableMap<String, Pair<String, List<Pair<String, String>>>> =
HashMap()
relationMap["category"] = Pair(
"sync_id", listOf(
Pair("notes", "categoryId")
)
) //给出新增的列名sync_id以及依赖该字段的表名和字段名列表[(notes, categoryId)]

CKUtil.handleSyncIdMigration(db, relationMap)
}
}

2、错误码

1. InitializationListener错误码

2. EventListener错误码

上一篇:使用入门
下一篇:小米云服务隐私协议
文档内容是否有帮助?
有帮助
无帮助