Uniapp 自制 Android 原生插件(详细流程,包含打包 aar、本地使用、上传云端)

发布于:2025-05-01 ⋅ 阅读:(157) ⋅ 点赞:(0)

一、简介

  • Uniapp 实现有些特殊需求时,可能需要调用原生的一些 API,或者一些原生第三方库等,但是这些库或 API 不支持 Uniapp 能够直接调用到,这个时候可以通过封装一个 Uniapp 可以使用的原生插件,将一些第三方原生库或 API 封装到插件内,并以 Uniapp 的方式暴露出来,使在 Uniapp 开发中能正常调用或使用到这些库或 API

  • 本文会以一个简单的插件需求,整体的走一遍封装 Uniapp 能够使用的原生插件。

  • 原生插件包内支持内部引入 Kotlin 第三方包。

  • 附:Android 原生插件官方文档

  • 附:Uniapp 原生插件的详细使用步骤(本地插件、云端插件)

  • 插件注意事项

    • Activity 的获取方式。通过 mUniSDKInstance.getContext() 强转 Activity 。建议先instanceof Activity 判断一下再强转。

    • .vue 文件内暂时只能使用 module 形式的插件。component 还不支持在 .vue 下使用,需要放到 .nvue 文件中使用。

    • component、module 的生命周回调,暂时只支持 onActivityDestroyonActivityPauseonActivityResult,其他暂时不支持。

    • 插件代码中用到的 JSONObjectJSONArray 要使用com.alibaba.fastjson.JSONArraycom.alibaba.fastjson.JSONObject; 不要使用 org.json.JSONObjectorg.json.JSONArray 否则造成参数无法正常传递使用等问题。

    • 更多的自行看文档吧,列几个常见问题。

二、准备工作

  • 必须走通 Uniapp Android 本地离线打包(详细流程),在这个基础上会更容易理解插件的本地调试与配置,开发工具与环境配置也都在这个文章内包含了。而且只是在这个流程的基础上稍微做下调整,然后再加上导入开发的插件引入与使用步骤。

  • 重要:目前无论是本地插件还是云端插件,都只支持 云打包的基座 运行,离线打包基座也是无法运行出来效果的,只能走 云打包 流程,运行后才能看到插件的效果。

    但有一种情况可以使用离线基座,那就是在开发原生插件的时候使用官方提供的 UniPlugin-Hello-AS 工程,它打包的离线基座可以运行出来插件的效果,但仅限于开发中的,也就是有源码在的这个插件工程里面的。如果这都不能运行,插件都没法开发了。而离线打包的工程 HBuilder-Integrate-AS 则不行,只能走云打包基座。

    上面这种情况说的就是现在这种,咱们准备进行开发插件的工程就是 UniPlugin-Hello-AS 工程。

  • 下载的 Android 原生插件工程目录结构介绍

    image.png

  • UniPlugin-Hello-AS 拖入 Android Studio,遇到运行问题可以参考下面的文章:

    UniPlugin-Hello-AS 工程中:

  • 插件需要本地调试,流程跟离线打包流程差不多,都需要每次调整好 uniapp 代码后再编译出来导入到项目中进行调试。

    在这里插入图片描述

三、开发中的插件如何调试

  • DCloud-RichAlert(uniplugin_richalert) 为例,它现在是个现成的插件,举例一下插件开发完成或开发中,如何进行本地调试。

  • 找到 uniapp 测试项目,打开一个页面文件,添加上插件使用代码:

    <template>
        <view class="content">
            <text class="title">{{ title }}</text>
        </view>
    </template>
    
    <script>
    export default {
        data() {
            return {
                title: 'DCloud-RichAlert 本地调试'
            }
        },
        onLoad() {
            // 导入插件
            const dcRichAlert = uni.requireNativePlugin('DCloud-RichAlert')
            // 调用显示
            dcRichAlert.show({
                position: 'bottom',
                title: "提示信息",
                titleColor: '#FF0000',
                content: "<a href='https://uniapp.dcloud.io/' value='Hello uni-app'>uni-app</a> 是一个使用 Vue.js 开发跨平台应用的前端框架!\n免费的\n免费的\n免费的\n重要的事情说三遍",
                contentAlign: 'left',
                checkBox: {
                    title: '不再提示',
                    isSelected: true
                },
                buttons: [{
                    title: '取消'
                }, {
                    title: '否'
                }, {
                    title: '确认',
                    titleColor: '#3F51B5'
                }]
            }, result => {
                console.log(result)
            })
        },
        methods: {
    
        }
    }
    </script>
    
    <style>
    </style>
    
  • 然后按离线打包一样,生成本地打包资源

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

  • 按照 Uniapp Android 离线生成自定义基座(详细流程) 配置一下 dcloud_control.xml,并添加 debug-server-release.aarlibs 目录下,配置、添加了即可,不需要往后走流程。

  • 然后按 Uniapp Android 本地离线打包(详细流程) 中的 四、离线工程的调整 进行配置好项目,其实到这里就是离线打包的本地工程配置了,流程一样的,配置好打包运行即可。

    唯独在配置 AppKey 的时候,发现顶部并没有 包名 需要配置,这里可以不需要配置也没问题,只需要将下面的 AppKey 配置好即可。

    在这里插入图片描述

  • 然后运行项目到模拟器即可,如果没有打开后没有生效,没有弹出窗口,可以将设备上安装的 app 删除重新运行,一般这样就解决了。

    在这里插入图片描述

四、Java 开发一个简单的 module 扩展(无界面原生插件)

  • 第一步:创建 Android StudioModule 模块

    在这里插入图片描述

    在这里插入图片描述

    Module name:app:mylibrary 的意思就是在 app 目录下新建一个 mylibrary 模块,如果要跟官方结构保持一致在 app 目录外面,直接保留 mylibrary 即可,为了跟官方保持命名统一,可以改成 uniplugin_mylibrary

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    创建好 Module 后需要配置一下它的 build.gradle 文件,最简单的就是拷贝一下其他插件的,推荐拷贝 uniplugin_richalert 的,看起来它的依赖比较干净。注意备份新建插件 build.gradlenamespace,拷贝覆盖后,在修改为它自己的。

    在这里插入图片描述

    在这里插入图片描述

    然后修改 namespace 为这个插件新建时候的包名

    在这里插入图片描述

  • 第二步:在 app 目录的 build.gradle 中引入新建的插件

    在这里插入图片描述

    然后编译代码是否报错,没有报错不用管,我看其他插件也没配置,如果有报错 Failed to calculate the value of property 'namespace',可以添加配置一下插件的命名空间:

    在这里插入图片描述

    细节:每次修改配置后,如何快速生效,点一下工具顶部显示的 Sync Now 然后等待即可:

    在这里插入图片描述

  • 第三步:创建插件入口文件

    需要给插件创建一个入口文件,文件名随便起,也可以参考其他插件的 XXXXModule(例如:RichAlertModule)

    在这里插入图片描述

    回车即可创建:

    在这里插入图片描述

    然后继承 UniModule

    package com.example.uniplugin_mylibrary;
    
    import io.dcloud.feature.uniapp.common.UniModule;
    
    public class MylibraryModule extends UniModule {
    
    }
    

    在这里插入图片描述

  • 第四步:到 app 目录 dcloud_uniplugins.json 中配置插件信息

    首先拷贝入口文件的 class

    在这里插入图片描述

    在这里插入图片描述

    找到 app 目录下的 dcloud_uniplugins.json 进行配置插件:

    在这里插入图片描述

    到这插件配置相关的就完成了,现在主要就是实现代码了。

  • 第五步:添加一份简单的调试代码

    package com.example.uniplugin_mylibrary;
    
    import android.util.Log;
    
    // JSONObject 需要使用 com.alibaba.fastjson 这个
    import com.alibaba.fastjson.JSONObject;
    
    import io.dcloud.feature.uniapp.annotation.UniJSMethod;
    import io.dcloud.feature.uniapp.bridge.UniJSCallback;
    import io.dcloud.feature.uniapp.common.UniModule;
    
    public class MylibraryModule extends UniModule {
    
        // 日志标识,后续可以通过日志表示快速排查问题
        private String Tag = "MylibraryModule";
    
        // 私有方法
        private int add(int a, int b) {
            return a + b;
        }
    
        // 对外暴露的方法需要添加 @UniJSMethod (uiThread = true/false),并且设置为public 。
        // uiThread: 是否在 UI 线程执行。
        // JSONObject options: 可以接收 UniApp 传过来的数据,也可以为空。
        // callback: 回调的时候,可以用 JSONObject 存放数据,回调 json 数据,但是某些时候回调不到,可以切换成 Map。
        @UniJSMethod(uiThread = false)
        public void testAdd (JSONObject options, UniJSCallback callback) {
            Log.e(Tag, "TestAddFunc:" + options);
            int sum = add(10, 8);
            if (callback != null) {
                JSONObject data = new JSONObject();
                data.put("code", "success");
                data.put("sum", sum);
                // 组装好的数据通过回调外抛给 uniapp
                callback.invoke(data);
            }
        }
    }
    
  • @UniJSMethod(uiThread = false) 使用分析:

    UniApp 的自定义封装组件中,@UniJSMethod 注解用于标记方法,使其可以被 JavaScript 调用。注解中的 uiThread 参数决定了方法运行的线程,具体作用如下:

    @UniJSMethod(uiThread = true)
    public void updateUI() {
        // 更新UI操作,需要运行在主线程
    }
    
    @UniJSMethod(uiThread = false)
    public String performBackgroundTask() {
        // 耗时操作,比如读取文件或网络请求
        return "Task Done";
    }
    
    • uiThread = true(默认)

      • 运行线程:在主线程(UI 线程)执行。

      • 适用场景:如果方法需要直接操作与 UI 相关的内容(如更新视图或触发动画),需要在 UI 线程中运行。

      • 优点:确保与 UI 的操作线程安全。

      • 缺点:如果方法执行耗时操作,可能导致 UI 卡顿,影响用户体验。

    • uiThread = false

      • 运行线程:在工作线程中执行。

      • 适用场景:用于执行耗时的任务,例如网络请求或复杂计算,避免阻塞主线程。

      • 优点:不会影响 UI 响应速度。

      • 注意事项:如果方法需要操作 UI,必须切换到主线程,否则可能会出现线程冲突或崩溃。

    • 总结:

      • uiThread = true 适合与 UI 相关的任务。

      • uiThread = false 适合耗时任务,可以避免阻塞 UI

  • 没遇到可跳过,导入 Uni 相关插件报错找不到解决方案,细节问题:

    需要把代码改不会报错的情况下,点这个 Sync Now 按钮,要不然依然不会生效,也可以通过修改插件的 build.gradle 中的 namespace 'com.example.uniplugin_mylibrary' 然后保存,重新唤起配置按钮点击生效。

    在这里插入图片描述

  • 第六步:使用插件

    <template>
        <view class="content">
            <text class="title">{{ title }}</text>
        </view>
    </template>
    
    <script>
    export default {
        data() {
            return {
                title: 'MylibraryModule 本地调试'
            }
        },
        onLoad() {
            // 导入插件
            const mylibraryModule = uni.requireNativePlugin('MylibraryModule')
            // 测试引入情况
            if (mylibraryModule === null) {
                uni.showToast({
                    icon: 'none',
                    title: '插件引入失败'
                })
                return
            }
            // 调用方法
            // 这里仅作为测试:第一个参数指定的是 json 对象,第二个是回调方法,回调返回的是 add(a, b) 计算的结果,但是内部也有输出传入的 json 对象
            mylibraryModule.testAdd({ name: 'dzm', age: 18 }, (res) => {
                // 本地输出与弹窗都输出一下呗
                console.log('testAdd:', res);
                uni.showToast({
                    icon: 'none',
                    title: `testAdd:${res}`
                })
            })
        },
        methods: {
    
        }
    }
    </script>
    
    <style>
    </style>
    

    使用后,重新按离线打包一样,记得先通过工具栏的 Build -> 先 Clean Project -> 再 Rebuild Project -> 最后在打包或运行,生成基座或者测试离线包都适用这套流程。

    生成本地打包资源,导入到 Android Studroid 重新运行到手机上,如果没生效,可以将手机上之前安装的 app 删除再运行一次。

    如果遇到这种 Test 文件报错的,直接删掉,留着主文件夹就行了,如果需要用它可以自己修好,这两个测试文件夹本来就是可有可无的。

    在这里插入图片描述

  • 调试细节:

    • 方式一:直接用 Android Studio 运行打包好的 UniApp 离线代码。

    • 方式二:按照自定义基座一样,将 Android Studio 编译好的 apk 丢到 UniApp 项目中运行基座到手机或模拟器,这样可以两边都能看到之前输出的调试日志,效果图:

      在这里插入图片描述

五、Java 开发一个简单的 componet 扩展(有界面原生插件)

  • 可以了解下 Uniapp .vue 与 .nvue 区别与书写区别;混搭使用与场景注意事项;.nvue 对原生界面支持比较好,如果想要在 .vue 文件中引入安卓原生界面组件的话,推荐放在 .nvue 文件中引用,不建议放在 .vue 文件内引用使用,可能会出别的兼容问题。

  • 所以这里准备做一个 componet 扩展(有界面原生插件),那么则需要放到 .nvue 文件中使用,因此需要修改一下当前页面的文件后缀。

  • 第一步:创建 Android StudioModule 模块

    同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致。

  • 第二步:在 app 目录的 build.gradle 中引入新建的插件

    同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致。

  • 第三步:创建插件入口文件

    同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致,只是继承对象不同。

    需要给插件创建一个入口文件,文件名随便起,也可以参考其他插件的 XXXXComponent(例如:MylibraryComponent),然后继承 UniComponent

    package com.example.uniplugin_mylibrary;
    
    import android.widget.TextView;
    
    import io.dcloud.feature.uniapp.UniSDKInstance;
    import io.dcloud.feature.uniapp.ui.action.AbsComponentData;
    import io.dcloud.feature.uniapp.ui.component.AbsVContainer;
    import io.dcloud.feature.uniapp.ui.component.UniComponent;
    
    public class MylibraryComponent extends UniComponent<TextView> {
    
        // 构造函数
        // 在 Android Studio 中,快捷添加 构造函数 (Constructor) 非常简单,快捷键:
        // Windows/Linux:Alt + Insert
        // macOS:Command + N
        public MylibraryComponent(UniSDKInstance uniSDKInstance, AbsVContainer absVContainer, AbsComponentData absComponentData) {
            super(uniSDKInstance, absVContainer, absComponentData);
        }
    }
    

    在这里插入图片描述

    在这里插入图片描述

  • 第四步:到 app 目录 dcloud_uniplugins.json 中配置插件信息

    同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致。

    在这里插入图片描述

    到这插件配置相关的就完成了,现在主要就是实现代码了。

  • 第五步:添加一份简单的调试代码

    package com.example.uniplugin_mylibrary;
    
    import android.content.Context;
    import android.graphics.Color;
    import android.widget.TextView;
    
    import androidx.annotation.NonNull;
    
    import io.dcloud.feature.uniapp.UniSDKInstance;
    import io.dcloud.feature.uniapp.annotation.UniJSMethod;
    import io.dcloud.feature.uniapp.ui.action.AbsComponentData;
    import io.dcloud.feature.uniapp.ui.component.AbsVContainer;
    import io.dcloud.feature.uniapp.ui.component.UniComponent;
    import io.dcloud.feature.uniapp.ui.component.UniComponentProp;
    
    public class MylibraryComponent extends UniComponent<TextView> {
    
        // 构造函数
        // 在 Android Studio 中,快捷添加 构造函数 (Constructor) 非常简单,快捷键:
        // Windows/Linux:Alt + Insert
        // macOS:Command + N
        public MylibraryComponent(UniSDKInstance uniSDKInstance, AbsVContainer absVContainer, AbsComponentData absComponentData) {
            super(uniSDKInstance, absVContainer, absComponentData);
        }
    
        // 创建UI组件,直接输入 init 会自动出来初始化函数
        @Override
        protected TextView initComponentHostView(@NonNull Context context) {
            TextView textView = new TextView(context);
            textView.setTextSize(30);
            textView.setTextColor(Color.RED);
            return textView;
        }
    
        // 给UI组件添加可以设置的属性 tel
        @UniComponentProp(name = "tel")
        public void setTel(String telNumber) {
            getHostView().setText("tel:" + telNumber);
        }
    
        // 给UI组件添加可以调用的方法,默认 uiThread = true ,所以不写就是默认
        @UniJSMethod
        public  void clearTel () {
            getHostView().setText("");
        }
    }
    
  • 第六步:使用插件

    UI 插件 不需要引入,直接使用即可:

    <template>
        <view class="content">
            <MylibraryTextView ref="RefTextView1" style="width: 200rpx; height: 100rpx; background-color: blue;" tel="123456"></MylibraryTextView>
            <MylibraryTextView ref="RefTextView2" class="text-view2" tel="123456"></MylibraryTextView>
            <button @click="touchClear">清空</button>
        </view>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    // vue3 获取 ref
    let RefTextView1 = ref(null)
    
    // 调用组件方法
    function touchClear () {
        RefTextView1.value.clearTel()
        // vue2 获取 ref
        // this.$ref.RefTextView.clearTel()
    }
    </script>
    
    <style>
    .text-view2 {
        width: 300rpx;
        height: 200rpx;
        background-color: yellow;
    }
    </style>
    

    编译细节同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致。

  • 调试细节:

    同第 四、Java 开发一个简单的 module 扩展(无界面原生插件) 一致,效果图:

    在这里插入图片描述

六、Android Studio 删除插件

  • 新建好的插件,需要移除,右键没找到 Delete 按钮,因为组件文件不能直接删除,需要先移除组件,变成普通文件夹才可以删除。

  • 先移除配置,这样避免删除后报错:

    在这里插入图片描述

  • 然后移除组件,记得顶部切换到 Project 展示模式,不要使用 Android 展示进行移除。

    在这里插入图片描述

    在这里插入图片描述

  • 按上面操作之后,文件夹还在,但是变成了普通文件夹。看下面两个文件夹的图标不一样了,这就变成了普通文件夹,然后右键就有删除按钮了,删除即可。

    如果没有生效,可以看下 工具上面顶部弹出的蓝色提示条上的按钮,点一点

    在这里插入图片描述

七、插件混合开发

  • 一个插件包下面,是可以存在多个 模块(Module)或组件(Component)的,上面的案例是拆开单个讲解而已,其实放一起也没问题的,无非多配置一个。

    在这里插入图片描述

  • 配置好后重新打个包导入运行效果是一样的,而且两个插件模块都会生效。

    <template>
        <view class="content">
            <TelTextView ref="RefTextView1" style="width: 200px; height: 100px; background-color: blue;" tel="123456"></TelTextView>
            <TelTextView ref="RefTextView2" class="text-view" tel="123456"></TelTextView>
            <button @click="touchClear">清空</button>
        </view>
    </template>
    
    <script setup>
    import { onBeforeMount, ref } from 'vue';
    
    // vue3 获取 ref
    let RefTextView1 = ref(null)
    
    onBeforeMount(() => {
        // 导入插件
        const mylibraryModule = uni.requireNativePlugin('MylibraryModule')
        // 测试引入情况
        if (mylibraryModule === null) {
            uni.showToast({
                icon: 'none',
                title: '插件引入失败'
            })
            return
        }
        // 调用方法
        // 这里仅作为测试:第一个参数指定的是 json 对象,第二个是回调方法,回调返回的是 add(a, b) 计算的结果,但是内部也有输出传入的 json 对象
        mylibraryModule.testAdd({ name: 'dzm', age: 18 }, (res) => {
            // 本地输出与弹窗都输出一下呗
            console.log('testAdd:', res);
            uni.showToast({
                icon: 'none',
                title: `testAdd:${res}`
            })
        })
    })
    
    // 调用组件方法
    function touchClear () {
        RefTextView1.value.clearTel()
        // vue2 获取 ref
        // this.$ref.RefTextView.clearTel()
    }
    </script>
    
    <style>
    .text-view {
        width: 300px;
        height: 200px;
        background-color: yellow;
    }
    </style>
    

八、打包插件(导入本地插件使用)

  • 找到 Android Studio 右侧菜单栏上的 Gradle,打开需要打包的插件目录,找到打包按钮。

    在这里插入图片描述

    在打开这个菜单后,一般是在 Gradle -> uniplugin_mylibrary -> Tasks -> other 里面有 assembleDebugassembleRelease 按钮的,点一下可以直接构建测试与正式版本的 aar,结果跟下面是一样的。

    尝试了下没找到这两个按钮,那么也没事,其实这两个选项也就是执行了两个命令,找不到不会自己执行一下么。

  • 一般在项目根目录有一个 gradlew 文件,通过它就可以执行 assembleDebugassembleRelease 命令:

    在这里插入图片描述

    然后打开 Android Studio 底部的 Terminal 手动执行命令:

    在这里插入图片描述

    这里会分两种情况:

    1、如果当前工程是一个模块工程,那么可以直接执行命令

    # Debug 构建
    $ ./gradlew assembleDebug
    
    # Release 构建
    $ ./gradlew assembleRelease
    
    
    # 其他命令(知道就行)
    # 清理项目
    $ ./gradlew clean
    
    # 列出当前模块支持的 Gradle 任务,检查是否有 `assembleDebug` 或 `assembleRelease`
    $ ./gradlew tasks
    
    # 如果 Gradle 配置有问题,运行以下命令强制重新加载:
    $ ./gradlew --refresh-dependencies assembleRelease
    

    2、如果当前工程是一个正常项目工程,然后在项目工程内新建了模块,就类似我上面截图的这种,则需要这么执行:

    your-project/
    ├── gradlew
    ├── settings.gradle
    ├── mylibrary/
    │   ├── build.gradle
    ├── app/
    │   ├── build.gradle
    
    # Debug 构建
    $ ./gradlew :mylibrary:assembleDebug
    
    # Release 构建
    $ ./gradlew :mylibrary:assembleRelease
    
    # :mylibrary 冒号后面的表示模块名称。
    # 如果模块名称与你的实际模块不同,请检查模块 settings.gradle 中的模块声明。
    
    # 其他命令(知道就行,使用同理)
    
  • 执行命令后,发现报错没执行权限 zsh: permission denied: ./gradlew,添加执行权限:

    $ chmod +x ./gradlew
    
  • 添加后再次执行,运行成功:

    在这里插入图片描述

  • 拿到 .aar 文件后,那么安卓的自定义插件也就开发完成了,然后到 uniapp 项目中根目录找到 nativeplugins 文件夹,没则自己手动创建。然后按照官方的文件夹结构创建创建一下,不清楚结构可以下载一个官方的插件 DCloud-RichAlert,下离线包即可,可以参考 package.json 配置。

    在这里插入图片描述

    调整好后,丢入自己的插件包:

    在这里插入图片描述

    拖入 uniapp 项目中 nativeplugins 文件夹内,然后修改 package.json。附 UniApp 官方原生插件开发 package.json 书写规则与插件结构文档,只需添加需要的配置即可,不需要全部配置,可以参考官方上传的插件配置。

    在这里插入图片描述

  • 配置好后,到配置中添加一下本地插件,插件这里显示的名称是 package.json 中的 name 字段,名字随意起。勾选添加一下插件,然后打个自定义基座运行即可,本地插件云插件 都是需要运行在 自定义基座 上面的,标准基座 是会报找不到的插件的。

    插件(包含云插件、本地插件) 只能通过 云打包的基座 运行。

    在这里插入图片描述

    上面显示支持 Android / iOS 是刚才 package.json 配置的,移除 iOS 的配置就只有安卓了。

  • 最后重新打一个包,运行看看效果,

    注意必须使用云基座打包才能看到效果,放到离线打包工程中运行时看不到效果的哦。推荐删除安装好的 app,在用基座运行,免得没生效。

    打包就用公共证书速度走,正式包了再填信息。

    在这里插入图片描述

  • 运行成功:

    在这里插入图片描述

九、上传到云端插件

  • 将刚才的插件文件夹,压缩成 .zip 就行。

  • 登录注册 DCloud插件市场 按提示步骤提交插件(需要编写对应插件的说明文档,md(markdown) 格式)。

    登录后,点击自己的昵称,就能进入 我的插件 页面,然后按着填信息,上传插件压缩包。

  • 后面没啥东西了,看着来就行了。


网站公告

今日签到

点亮在社区的每一天
去签到