Android SDK 集成指南
概述
Width Identity SDK 为 Android 应用提供即插即用的身份验证流程。支持 KYC 和 KYB 工作流,包括证件采集、活体检测与审核。SDK 在独立的 Activity 中运行;你只需添加一个依赖、调用一个入口,SDK 即返回单一结果。
系统要求
| 要求 | 最低版本 |
|---|---|
| Android SDK | API 26 (Android 8.0)+ |
| compileSdk |
36 — SDK 发布产物的 AAR 元数据里写着
minCompileSdk=36,构建工具会强制校验,配得比它低会直接构建失败
|
| Kotlin | 1.9+ |
| Gradle / AGP | 8.0+ |
| Java | 源代码及目标兼容版本 11 |
| 宿主应用 | Jetpack Compose 或传统 View 体系均可(SDK 在自己的 Activity 中运行) |
安装
步骤 1:添加 Maven 仓库
在 settings.gradle.kts 中。Width 仓库提供 SDK 及其由 Width
托管的传递依赖(com.width.risksec、com.width.camera);其余依赖(AndroidX
/ Kotlin / Ktor / CameraX)从 Google + Maven Central 解析。
SDK 内置的证件采集引擎由各自的厂商托管,不在 Width 的 Maven 上,所以它们的仓库也要加上 — 构建期必须有,哪怕某个采集商在你的流程里用不到。下面两个厂商仓库块请原样照抄。
dependencyResolutionManagement {
repositories {
maven { url = uri("https://maven.width.com/repository/sdk-releases/") }
google()
mavenCentral()
// Document-capture provider repository (bundled transitively; required to resolve)
maven {
url = uri("https://cdn.veriff.me/android/")
content { includeGroup("com.veriff") }
}
// Document-reader provider repository (bundled transitively; required to resolve)
maven {
url = uri("https://maven-master.regulaforensics.com/RegulaDocumentReader/")
content {
includeGroup("com.regula.documentreader")
includeGroup("com.regula.documentreader.core")
includeGroup("com.regula.common")
}
}
}
}步骤 2:添加依赖
在应用级
build.gradle.kts 中。单个坐标即可传递引入相机、活体及配套库
— 无需 BOM,也无需额外坐标。
dependencies {
implementation("com.width.identity:sdk-activity:1.0.11")
}minSdk 要求:SDK 要求
minSdk 26。
步骤 3:必需的构建配置
下面两项配置加在你自己的 App 模块上,不是 SDK 侧的事。不加,构建直接失败 — 两项都不需要改你的任何源码。
必须开启 core library desugaring(把新版 Java
标准库的能力降级到旧 Android 版本上可用的编译期处理)。内置的 AWS
Rekognition 活体引擎带的 Amplify AAR 在元数据里声明了
requires core library desugaring,AGP 会在
App 侧强制校验(checkReleaseAarMetadata /
checkDebugAarMetadata),与你的
minSdk 是多少无关。在 App 模块加上:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
AGP 9 上必须加
android.uniquePackageNames=false。
AWS 活体引擎传递引入的两个产物(com.google.ai.edge.litert:litert-support
与 litert-support-api,都是
1.4.1)声明了同一个库命名空间
org.tensorflow.lite.support —
这是上游的打包缺陷。AGP 9 的清单合并会校验命名空间唯一性,冲突就以
Validation failed, exiting 中断构建。在项目的
gradle.properties 里加上:
# gradle.properties android.uniquePackageNames=false
sdk-bom 平台依赖加上单独的
sdk-ui-width-modern 坐标。现在不再需要 —
sdk-activity 已聚合全部内容。
AndroidManifest.xml 的
<meta-data> 节点 /
strings.xml 注册。现在改为在运行时作为
CredentialsConfig 中的
sdkToken 字段传入(见快速开始)。
快速开始
SDK 提供两种等价的入口风格 — 按你的调用点选择即可。两者驱动完全相同的验证界面,并返回单一结果。
直接输入(ActivityResultContract)
import com.width.identity.activity.CredentialsConfig
import com.width.identity.activity.CustomerConfig
import com.width.identity.activity.ThemeConfig
import com.width.identity.activity.UILanguage
import com.width.identity.activity.Verification
import com.width.identity.activity.VerificationInput
import com.width.identity.activity.VerificationResult
class MainActivity : ComponentActivity() {
private val launcher =
registerForActivityResult(Verification()) { result ->
when (result) {
is VerificationResult.Completed -> // .APPROVED / .REJECTED / .REVIEW
Log.i(TAG, "done: ${result.executionId} ${result.decision}")
is VerificationResult.Waiting ->
Log.i(TAG, "waiting: ${result.executionId}")
is VerificationResult.Dismissed ->
Log.i(TAG, "dismissed: ${result.executionId ?: "(no execution)"}")
is VerificationResult.Cancelled ->
Log.i(TAG, "cancelled: ${result.executionId}")
is VerificationResult.Failed ->
Log.w(TAG, "failed: ${result.code} ${result.message}")
}
}
private fun startVerification() {
val input = VerificationInput(
credentials = CredentialsConfig(
domain = "YOUR_DOMAIN",
sdkToken = "YOUR_SDK_TOKEN",
),
customer = CustomerConfig(
// Both customerId and email are required and must be non-blank.
customerId = "user-123",
email = "user@example.com",
),
theme = ThemeConfig(themeMode = "SYSTEM"), // optional: "SYSTEM" / "LIGHT" / "DARK"
eventType = "KYC_EVENT", // optional: "KYC_EVENT" / "KYB_EVENT" to skip in-SDK selection; omit to let the user pick
language = UILanguage.ZH, // optional: UILanguage.EN (default) / .ZH; omit = English (does not follow device locale)
)
launcher.launch(input)
}
}Builder 入口(Compose)
链式构造器(fluent builder)把启动所需的输入串成一条链装配起来 —
依次给出 access token、凭据、申请人,再按需挂上 handler,最后
build 并启动,返回单一的
WIDTHCompletionResult。先用 WIDTHVerification
契约在 composition 中注册 launcher,再在调用点构建并启动。
import androidx.activity.compose.rememberLauncherForActivityResult
import com.width.identity.activity.WIDTHCompletionResult
import com.width.identity.activity.WIDTHDayNightMode
import com.width.identity.activity.WIDTHMobileSDK
import com.width.identity.activity.WIDTHVerification
@Composable
fun VerifyButton() {
val launcher = rememberLauncherForActivityResult(WIDTHVerification()) { result ->
when (result) {
is WIDTHCompletionResult.SuccessTermination -> // see status table
Log.i(TAG, "done: ${result.status} ${result.executionId}")
is WIDTHCompletionResult.AbnormalTermination ->
Log.w(TAG, "failed: ${result.exception.code} ${result.exception.message}")
}
}
Button(onClick = {
WIDTHMobileSDK.Builder()
.withAccessToken("YOUR_SDK_TOKEN") // RiskSec sdkToken (encodes appId/apiKey)
.withCredentials(domain = "YOUR_DOMAIN") // only domain is client-supplied
.withApplicant(customerId = "user-123", email = "user@example.com")
.withEventType("KYC_EVENT") // optional; "KYC_EVENT"/"KYB_EVENT" to skip in-SDK selection
.withForcedDayNightMode(WIDTHDayNightMode.NIGHT) // optional; omit to follow the system
.withHandlers(onEvent = myLogger) // optional; see Event logging
.withLauncher(launcher)
.build()
.launch()
}) { Text("Verify") }
}
这只是另一种入口写法,不是另一条启动路径:builder
在底层装配出原生的 VerificationInput,跑的仍是同一套流程。withAccessToken
传的就是 sdkToken,租户的 appId /
apiKey 已经编码在这个 token
里、由 SDK 在设备上解密;客户端只需要通过
withCredentials 传 domain,通过
withApplicant 传申请人身份。
withEventType 可选("KYC_EVENT" /
"KYB_EVENT")—
传了就直接进入对应流程、跳过 SDK 内的选择页;不传则由用户自己选。
VerificationInput(...) 在下列情况抛出
IllegalArgumentException:domain /
sdkToken 为空,customerId /
email 为空,或 eventType
传了非空值但不是 "KYC_EVENT" /
"KYB_EVENT";build()
则在必填的 builder 字段没设时抛出。请在调用点捕获。
事件日志(可选)
注册一个 SdkEventLogger 即可将 SDK 生命周期事件(INFO /
WARN / ERROR)接入你自己的管道(Crashlytics、Timber、DataDog
等)。回调触发前已脱敏已知的 PII(个人身份信息)。SdkEventLogger
是函数式接口,任何需要它的位置都可以传入 lambda。
import com.width.identity.activity.SdkLogLevel
import com.width.identity.activity.SdkLogging
SdkLogging.setLogger { level, tag, message, throwable ->
when (level) {
SdkLogLevel.INFO -> Log.i(tag, message)
SdkLogLevel.WARN -> Log.w(tag, message, throwable)
SdkLogLevel.ERROR -> Log.e(tag, message, throwable)
}
}
该 logger 是全局且可选的;调用
SdkLogging.setLogger(null) 即可停止转发。也可以通过
withHandlers(onEvent = ...) 在 builder 上就地注册同一个
logger。
配置参考
VerificationInput
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
credentials |
CredentialsConfig |
是 | SDK 认证配置 |
customer |
CustomerConfig |
是 | 客户身份信息 |
theme |
ThemeConfig |
否 | UI 主题定制 |
eventType |
String | 否 |
传 "KYC_EVENT" / "KYB_EVENT" 可跳过 SDK
内的流程选择页;不传则由用户自己选
|
language |
UILanguage |
否 |
界面语言:UILanguage.EN(默认)/
.ZH
|
CredentialsConfig
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
domain |
String | 是 | 多租户路由的租户标识符 |
sdkToken |
String | 是 |
SDK token(以往通过 manifest
<meta-data> 注册)
|
CustomerConfig
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
customerId |
String | 是 | 唯一客户标识符(不可为空) |
email |
String | 是 | 客户邮箱地址(不可为空) |
customerId 和 email
两者都必须提供且不可为空。任一为空,构造时即抛出
IllegalArgumentException。(此规则比以往更严格 —
以往只要求二者至少提供其一。)
公开 API
com.width.identity.activity 是唯一对客户开放的包 —
其余都是内部实现,在你的应用里引用会直接编译报错。
| 符号 | 用途 |
|---|---|
Verification |
ActivityResultContract 入口
|
VerificationInput / CredentialsConfig /
CustomerConfig
|
启动参数 |
VerificationResult / VerificationDecision
|
直接输入入口的结果 |
WIDTHMobileSDK.Builder + WIDTHVerification
|
Builder 入口(Compose) |
WIDTHCompletionResult / WIDTHStatus /
WIDTHException
|
Builder 入口的结果类型 |
ThemeConfig / WIDTHDayNightMode
|
可选的主题 / 日夜模式覆盖 |
UILanguage |
可选的界面语言(EN 为默认值 /
ZH)
|
SdkLogging / SdkEventLogger /
SdkLogLevel
|
可选的事件日志回调 |
UILanguage
| 取值 | 说明 |
|---|---|
EN |
英文(默认) |
ZH |
简体中文 |
不传 language 就是英文 — SDK
不会跟随设备语言。第三方采集页面另有限制:Veriff
只有在设备联网、且租户的集成开启了中文时才显示中文,否则仍是英文;Regula
只支持简体中文。
结果处理
直接输入的 VerificationResult
VerificationResult 是一个密封类,包含五种变体:
| 结果 | 字段 | 描述 |
|---|---|---|
Completed |
executionId, decision |
服务端返回决定:APPROVED / REJECTED /
REVIEW(在带外进行人工审核)
|
Waiting |
executionId |
等待服务端通知最终决定 |
Dismissed |
executionId? |
用户退出但打算稍后返回(X →
保留进度,或在工作流开始前退出)。服务端执行被保留,下次启动即可恢复;若工作流尚未开始,executionId
为 null。可恢复。
|
Cancelled |
executionId |
用户主动放弃验证(X → 放弃)。SDK 已等待服务端的
workflow.execution.cancel,执行已销毁,无法恢复。
|
Failed |
code, message, executionId?
|
内部错误 |
Cancelled 现在被拆分为两种:Dismissed
表示用户暂停、可恢复;Cancelled
表示用户放弃、服务端执行已销毁(不可恢复)。
Builder 的 WIDTHCompletionResult
| 变体 | 何时 |
|---|---|
SuccessTermination(status, executionId?) |
流程正常结束;最终状态由 status 携带
|
AbnormalTermination(exception, executionId?) |
技术性失败;exception.code /
exception.message 携带细节
|
status 是
WIDTHStatus:Approved、FinallyRejected、Pending(人工审核)、Incomplete(可恢复
/ 已 dismissed)、Cancelled(已销毁)。exception
是
WIDTHException:Init、Network、Api、Permission、Workflow、Unknown
之一 — 每个都暴露 code 与 message。
主题定制
ThemeMode
| ThemeMode | 行为 |
|---|---|
"SYSTEM" |
跟随系统设置 |
"LIGHT" |
强制浅色主题 |
"DARK" |
强制深色主题 |
通过 ThemeConfig 设置模式,或在 Builder
入口覆盖日间/夜间。两者省略时均跟随系统设置。
ThemeConfig(themeMode = "SYSTEM") // "SYSTEM" / "LIGHT" / "DARK" // On the Builder entry point — omit to follow the system: .withForcedDayNightMode(WIDTHDayNightMode.NIGHT)
恢复与生命周期
| 用户选择 | 行为 | 结果 | 可恢复? |
|---|---|---|---|
| 暂停并退出(保留进度) | 服务端执行被保留 | Dismissed |
是 |
| 结束验证(放弃) | 服务端取消,执行已销毁 | Cancelled |
否 |
恢复验证时,使用相同的 customerId 调用
launcher.launch(...),SDK 会自动检测进行中的工作流。
权限
SDK 与其内置的采集引擎声明了所需的全部权限,并会自动合并进你的应用 — 你无需声明或申请它们:
<!-- WIDTH identity SDK core --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Added transitively by the capture / liveness engine --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.VIBRATE" /> <!-- Added transitively by the document-reader engine --> <uses-permission android:name="android.permission.FLASHLIGHT" />
| 权限 | 类型 | 用途 / 来源 |
|---|---|---|
CAMERA |
运行时 | 证件采集、活体检测(SDK 核心) |
INTERNET |
安装时 | API 通信(SDK 核心) |
ACCESS_NETWORK_STATE |
安装时 | 网络连接检测(SDK 核心) |
RECORD_AUDIO |
运行时 | 由采集 / 活体引擎合并引入 |
NFC |
安装时 | 由采集 / 活体引擎合并引入 |
WAKE_LOCK |
安装时 | 由采集 / 活体引擎合并引入 |
VIBRATE |
安装时 | 由采集 / 活体引擎合并引入 |
FLASHLIGHT |
安装时 | 由证件阅读引擎合并引入 |
CAMERA 与
RECORD_AUDIO;由 SDK 及其采集引擎在各自的 Activity
内部申请,你的应用不会弹窗。上述每一项权限都会出现在应用的权限列表里,必须如实反映到你的 Google Play Data
Safety(数据安全)申报中。SDK 不需要任何存储 / 媒体权限 —
选择证件图片走系统照片选择器。
ProGuard / R8:SDK 内置了混淆规则,无需额外配置。
故障排查
| 问题 | 解决方案 |
|---|---|
无法解析 com.width.identity:...
|
确保 Width Maven 仓库配置在
settings.gradle.kts 中(而非
build.gradle.kts),并使用单一坐标
com.width.identity:sdk-activity:1.0.11(不再用
BOM)。如果解析不到的是 com.veriff 或
com.regula.*,说明步骤 1 里那两个厂商仓库没加。
|
启动时抛出 IllegalArgumentException
|
检查 domain / sdkToken 以及
customerId 和
email 是否都非空。还要检查
eventType:一旦传了值,就必须正好是
"KYC_EVENT" 或 "KYB_EVENT"。
|
| 摄像头无法使用 |
检查合并后的 manifest 是否存在权限冲突;SDK 会从其库 manifest
自动合并 CAMERA。
|
| 主题颜色未生效 |
检查十六进制值(无需
# 前缀),令牌名称区分大小写
|
构建在 checkReleaseAarMetadata /
checkDebugAarMetadata 上失败,提示
requires core library desugaring
|
在 App 模块开启 core library desugaring 并加上
desugar_jdk_libs 依赖 — 见「安装」的步骤
3。这项校验与你的 minSdk 是多少无关。
|
同一道 AAR 元数据校验失败,原因是模块的
compileSdk 低于 SDK 的要求
|
SDK 的几个 AAR 在元数据里写着
minCompileSdk=36,构建工具会强制校验,模块拿比它低的版本编译就会中断构建。在
App 模块把 compileSdk 设为
36 — 见「系统要求」。这与
minSdk 是两回事,后者仍是 26。
|
AGP 9 上清单合并失败,报
Validation failed, exiting
|
在项目的 gradle.properties 里加上
android.uniquePackageNames=false —
见「安装」的步骤 3。
|
build.gradle.kts
里的版本号改成新版本,下次 sync 时 Gradle 会从
maven.width.com 拉取新产物。
集成说明
请按上文获取软件包访问权限并完成安装。SDK 构建版本及所用凭证须与目标 WIDTH 环境保持一致。