UNIAPPX UTS插件Widget开发完整教程(Android版)

UTS 插件开发完整教程

目录

  1. 前置概念
  2. 插件目录结构
  3. 逐步开发流程
  4. 常见错误与解决方法
  5. 核心注意事项

1. 前置概念

UTS 插件是 uni-app x 中扩展原生能力的机制。一个 UTS 插件包含:

  • 跨平台接口层(interface.uts):用 UTS 语法定义统一的类型和方法签名
  • 平台实现层(app-android / app-ios):用 Kotlin / Swift 实现原生逻辑
  • UTS 桥接层(index.uts):将原生方法导出为 UTS 函数,供 uvue 页面调用

当 UTS 插件包含 AndroidManifest.xmlres/ 资源文件时,每次修改这些文件后必须重新制作自定义基座,差量编译不会同步原生资源。


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. 「运行」→「运行到手机或模拟器」→「制作自定义基座」
  2. 等待编译完成(约 1-3 分钟)
  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 白名单中 只使用 LinearLayoutTextViewImageViewListViewButton
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 做分隔线)
  • TextViewImageViewButton
  • ListViewGridView
  • ViewStubAnalogClockChronometer
  • <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 取值

附录:调试技巧

  1. 查看日志 :使用 console.log("消息") 在 Kotlin 和 UTS 中输出调试信息

  2. 查看 Widget 日志 :在模拟器桌面添加 Widget 后,日志会显示 AppWidgetHostView: Error inflating...TodoWidgetProvider onUpdate...

  3. 确认 APK 已更新:重新制作基座并运行后,旧 Widget 可能还显示旧数据------手动移除 Widget 重新添加

  4. 缓存彻底清理

    bash 复制代码
    rm -rf unpackage/cache/
    rm -rf unpackage/dist/dev/
相关推荐
宋浮檀s1 小时前
应急响应——Web高危漏洞应急(SQL注入+XSS跨站+文件上传)
前端·网络·安全·web安全·xss
大家的林语冰1 小时前
AI 遥控代码截图,录制终端动画,定制自动化批量制图流程,解放你的双手~
前端·ai编程·trae
无聊的老谢1 小时前
Vue 3 + Leaflet 实现高性能 Web GIS 基站监控平台
前端·javascript·vue.js
之歆1 小时前
Day23_Bootstrap 前端框架完全指南:从栅格系统到组件化开发
开发语言·前端·javascript·前端框架·bootstrap·ecmascript·less
前端 贾公子1 小时前
3.响应式系统基础:从发布订阅模式的角度理解 Vue2 的数据响应式原理(上)
前端·javascript·vue.js
2501_940041741 小时前
纯前端高阶实战:涵盖3D、音频可视化与复杂交互的开发命题
前端
AIFQuant1 小时前
外汇交易平台技术栈深度解析:行情 API、清算、风控、前端一体化方案
前端·python·websocket·金融·restful
NiceCloud喜云9 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
wordbaby10 小时前
React Native + RNOH:跨页面数据回传的最佳实践与避坑指南
前端·react native