UTS 插件开发完整教程
目录
1. 前置概念
UTS 插件是 uni-app x 中扩展原生能力的机制。一个 UTS 插件包含:
- 跨平台接口层(interface.uts):用 UTS 语法定义统一的类型和方法签名
- 平台实现层(app-android / app-ios):用 Kotlin / Swift 实现原生逻辑
- UTS 桥接层(index.uts):将原生方法导出为 UTS 函数,供 uvue 页面调用
当 UTS 插件包含
AndroidManifest.xml、res/资源文件时,每次修改这些文件后必须重新制作自定义基座,差量编译不会同步原生资源。
2. 插件目录结构
bash
uni_modules/uni-todo-widget/ # 插件根目录
├── package.json # 插件元信息(id、版本、平台兼容性)
└── utssdk/ # UTS SDK 源码目录
├── index.uts # 统一导出入口(重新导出 interface)
├── interface.uts # 跨平台类型与方法签名定义
├── unierror.uts # 自定义 UniError 错误定义(可选)
├── app-android/ # Android 平台实现
│ ├── index.uts # UTS→Kotlin 桥接导出
│ ├── AndroidManifest.xml # 原生组件注册
│ ├── TodoWidgetNative.kt # 原生数据操作类
│ ├── TodoWidgetProvider.kt # AppWidgetProvider
│ ├── TodoListRemoteViewsService.kt # RemoteViewsService
│ └── res/ # 原生资源文件
│ ├── layout/ # Widget 布局 XML
│ └── xml/ # Widget 配置 XML
└── app-ios/ # iOS 平台实现
├── index.uts # UTS→Swift 桥接导出
└── TodoWidgetNative.swift # Swift 原生实现
3. 逐步开发流程
Step 1:创建插件目录
在 uni_modules/ 下创建插件目录:
go
uni_modules/你的插件名/
├── package.json
└── utssdk/
├── index.uts
├── interface.uts
├── app-android/
│ └── index.uts
└── app-ios/
└── index.uts
package.json 基础模板:
json
{
"id": "你的插件id",
"displayName": "显示名称",
"version": "1.0",
"description": "描述",
"keywords": [],
"engines": { "HBuilderX": "^4.53" },
"dcloudext": {
"type": "uts",
"sale": { "regular": { "price": "0.00" } }
},
"uni_modules": {
"dependencies": [],
"platforms": {
"client": {
"App": {
"app-android": { "minVersion": "21" },
"app-ios": { "minVersion": "12" }
}
}
}
}
}
Step 2:定义接口(interface.uts)
用 type 关键字定义数据结构和函数签名。必须使用 type 而非 interface。
ts
// 数据结构
export type TodoItem = {
id: string
date: string
content: string
isCompleted: boolean
createTime: number
}
// 函数签名
export type AddTodo = (date: string, content: string) => void
export type GetTodos = (date?: string) => Array<TodoItem>
// 可选参数类型
export type UpdateTodoParams = {
content?: string
isCompleted?: boolean
date?: string
}
Step 3:统一导出(index.uts)
将接口重新导出,供页面通过 @/uni_modules/插件名 路径引用:
ts
export { addTodo, removeTodo, getTodos } from './interface.uts'
export type { TodoItem } from './interface.uts'
Step 4:实现 Android 原生代码(Kotlin)
在 app-android/ 下创建 .kt 文件。
包名规则 :所有 Kotlin 文件、AndroidManifest.xml 中的 package、index.uts 的 import 三者必须完全一致 。推荐使用 io.dcloud.uniplugin。
kotlin
package io.dcloud.uniplugin // 必须与 AndroidManifest 的 package 一致
import io.dcloud.uts.UTSAndroid // 获取 UniApp Activity 上下文
import io.dcloud.uts.UTSJSONObject
import io.dcloud.uts.console
object TodoWidgetNative {
private fun getPrefs(): SharedPreferences? {
val context = UTSAndroid.getUniActivity() ?: return null
return context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
}
fun addTodoKotlin(date: String, content: String) {
// 原生实现...
refreshWidget() // 数据变更后刷新 Widget
}
}
Step 5:实现 UTS 桥接(index.uts)
在 app-android/index.uts 中将 Kotlin 方法导出为 UTS 函数:
ts
import { AddTodo, TodoItem } from '../interface.uts'
import { TodoWidgetNative } from 'io.dcloud.uniplugin' // 对应 Kotlin 包名
export const addTodo: AddTodo = function (date: string, content: string): void {
TodoWidgetNative.addTodoKotlin(date, content)
}
导入路径规则 :import { ClassName } from '包名' 对应 Kotlin 的 import 包名.ClassName。
Step 6:注册原生组件(AndroidManifest.xml)
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.dcloud.uniplugin"> <!-- 包名必须与 .kt 一致 -->
<application>
<receiver
android:name=".TodoWidgetProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/todo_widget_info" />
</receiver>
<!-- Android 5.0+ 必须加 BIND_REMOTEVIEWS 权限 -->
<service
android:name=".TodoListRemoteViewsService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>
- 类名前加
.表示使用 manifest 的 package 作为前缀 - Service 必须有
BIND_REMOTEVIEWS权限,否则系统不会绑定
Step 7:编写原生资源文件(res/)
在 app-android/res/ 下按标准 Android 资源目录结构存放:
res/layout/todo_widget_layout.xml--- Widget 主布局res/xml/todo_widget_info.xml--- Widget 配置信息res/layout/todo_widget_item.xml--- 列表项布局
Widget 布局注意事项:
- 禁止使用
<View>标签(Class not allowed to be inflated) - 分隔线用
<TextView>替代<View> - 列表使用
<ListView>(非RecyclerView) RemoteViewsService必须通过context.resources.getIdentifier()运行时获取资源 ID,不要直接引用 R 类
Step 8:在 uvue 页面中调用
ts
import {
addTodo, removeTodo, updateTodo,
getTodos, getAllTodos, updateTodoWidget,
TodoItem, UpdateTodoParams
} from '@/uni_modules/uni-todo-widget'
// 调用示例
addTodo('2026-05-31', '买牛奶')
const list = getTodos('2026-05-31')
updateTodo(id, { isCompleted: true } as UpdateTodoParams)
updateTodoWidget() // 刷新桌面 Widget
Step 9:制作自定义基座(必须步骤)
每次修改 res/、AndroidManifest.xml 或新增 Kotlin 文件后:
- 「运行」→「运行到手机或模拟器」→「制作自定义基座」
- 等待编译完成(约 1-3 分钟)
- 「运行」→「运行到手机或模拟器」→「Android 基座运行」
仅修改
index.uts/ .uvue 页面 / 纯 UTS 代码时,差量编译(热更新)即可。 涉及原生资源时必须重做基座。
4. 常见错误与解决方法
| 错误 | 原因 | 解决方法 |
|---|---|---|
找不到名称 "R" |
R 类路径与包名不一致 | 统一 AndroidManifest.package、Kotlin 包名、index.uts import;不要手动 import R |
FileNotFoundException: xxx/R$id.class |
UTS 编译器找不到 R 类 | 确认 res/ 目录存在且包名与 AndroidManifest 一致 |
ENOENT: index.kt |
差量编译缓存异常 | 停止运行 → 删除 unpackage/cache/ → 重新启动运行 |
Error inflating class android.view.View |
Widget 布局中使用了 <View> |
替换为 <TextView> 或 <LinearLayout> |
Class not allowed to be inflated |
某个 View 类不在 RemoteViews 白名单中 | 只使用 LinearLayout、TextView、ImageView、ListView、Button |
Widget 显示"今日暂无待办"但数据存在 |
SharedPreferences 跨进程不可见 | App 端用 commit()(同步写入),Widget 端用 MODE_MULTI_PROCESS |
Service 没有被绑定 |
缺少 BIND_REMOTEVIEWS 权限 |
Service 声明添加 android:permission="android.permission.BIND_REMOTEVIEWS" |
Resource not found (layoutId=0) |
getIdentifier 找不到布局 |
检查包名是否正确;检查 res/layout/ 下文件名是否匹配 |
自定义基座制作失败 |
Android SDK 未配置 | HBuilderX → 设置 → 插件配置 → Android SDK 配置,检查 SDK 路径 |
5. 核心注意事项
5.1 包名三统一
以下三处的包名必须完全一致,否则编译失败或运行时找不到类:
go
AndroidManifest.xml 的 package="io.dcloud.uniplugin"
↓
所有 .kt 文件的 package io.dcloud.uniplugin
↓
index.uts 的 import { ClassName } from 'io.dcloud.uniplugin'
5.2 res/ 修改必须重做基座
差量编译(热更新)不同步原生资源 。res/ 中的 XML 布局、AndroidManifest.xml 中的组件声明都需要打包进 APK 才能生效。
修改步骤:差量编译测试 UTS 代码 → 确认无语法错误 → 制作自定义基座 → 运行验证
5.3 Widget 跨进程数据共享
App(Activity)进程和 Widget(RemoteViewsService)进程是隔离的:
kotlin
// App 端保存数据 --- 用 commit() 同步写入
prefs.edit().putString(KEY, json).commit()
// Widget 端读取数据 --- 用 MODE_MULTI_PROCESS
context.getSharedPreferences(NAME, Context.MODE_MULTI_PROCESS)
5.4 Widget 布局白名单
RemoteViews 只支持有限的一组 View 类:
- ✅
LinearLayout(不要嵌套View做分隔线) - ✅
TextView、ImageView、Button - ✅
ListView、GridView - ✅
ViewStub、AnalogClock、Chronometer - ❌
<View>(会导致 inflate 失败) - ❌ 自定义 View /
RecyclerView
规则:能替代的都用 TextView 替代(包括分隔线)。
5.5 资源 ID 推荐运行时获取
不要在 Kotlin 中 import R 类。推荐使用 getIdentifier 运行时获取:
kotlin
companion object {
fun getLayoutId(context: Context, name: String): Int {
return context.resources.getIdentifier(name, "layout", context.packageName)
}
fun getViewId(context: Context, name: String): Int {
return context.resources.getIdentifier(name, "id", context.packageName)
}
}
5.6 差量编译缓存问题
修改包名或删除文件后,必须清理缓存:
bash
rm -rf unpackage/cache/.app-android/
或在 HBuilderX 中停止运行 → 重新启动运行。
5.7 UTS 语法约束
- 数据类型用
type,不要用interface - 变量必须初始化,不支持
undefined,空值用null - 条件表达式必须返回
boolean类型 - 对象字面量如无显式类型标注会推断为
UTSJSONObject,需要用obj["key"] as string取值
附录:调试技巧
-
查看日志 :使用
console.log("消息")在 Kotlin 和 UTS 中输出调试信息 -
查看 Widget 日志 :在模拟器桌面添加 Widget 后,日志会显示
AppWidgetHostView: Error inflating...或TodoWidgetProvider onUpdate... -
确认 APK 已更新:重新制作基座并运行后,旧 Widget 可能还显示旧数据------手动移除 Widget 重新添加
-
缓存彻底清理 :
bashrm -rf unpackage/cache/ rm -rf unpackage/dist/dev/