Saltar al contenido

Desarrollo de Plugins Móviles

Los plugins pueden ejecutar código móvil nativo escrito en Kotlin (o Java) y Swift. La plantilla de plugin por defecto incluye un proyecto de biblioteca Android que utiliza Kotlin y un paquete Swift que incluye un comando móvil de ejemplo que muestra cómo activar su ejecución desde código Rust.

Sigue los pasos de la Guía de desarrollo de plugins para inicializar un nuevo proyecto de plugin.

Si tienes un plugin existente y deseas agregarle capacidades para Android o iOS, puedes usar plugin android init y plugin ios init para inicializar los proyectos de biblioteca móvil y guiarte a través de los cambios necesarios.

La plantilla de plugin por defecto divide la implementación del plugin en dos módulos separados: desktop.rs y mobile.rs.

La implementación de escritorio utiliza código Rust para implementar una funcionalidad, mientras que la implementación móvil envía un mensaje al código móvil nativo para ejecutar una función y obtener un resultado de vuelta. Si se necesita lógica compartida entre ambas implementaciones, se puede definir en lib.rs:

src/lib.rs
use tauri::Runtime;
impl<R: Runtime> <plugin-name><R> {
pub fn do_something(&self) {
// do something that is a shared implementation between desktop and mobile
}
}

Esta implementación simplifica el proceso de compartir una API que puede ser utilizada tanto por comandos como por código Rust.

Un plugin de Tauri para Android se define como una clase Kotlin que extiende app.tauri.plugin.Plugin y está anotada con app.tauri.annotation.TauriPlugin. Cada método anotado con app.tauri.annotation.Command puede ser llamado por Rust o JavaScript.

Tauri utiliza Kotlin por defecto para la implementación del plugin de Android, pero puedes cambiar a Java si lo prefieres. Después de generar un plugin, haz clic derecho en la clase de plugin de Kotlin en Android Studio y selecciona la opción "Convert Kotlin file to Java file" del menú. Android Studio te guiará a través de la migración del proyecto a Java.

Un plugin de Tauri para iOS se define como una clase Swift que extiende la clase Plugin del paquete Tauri. Cada función con el atributo @objc y el parámetro (_ invoke: Invoke) (por ejemplo @objc private func download(_ invoke: Invoke) { }) puede ser llamada por Rust o JavaScript.

El plugin se define como un paquete Swift para que puedas usar su gestor de paquetes para administrar dependencias.

Consulta la Sección de configuración de plugins de la Guía de desarrollo de plugins para obtener más detalles sobre el desarrollo de configuraciones de plugins.

La instancia del plugin en móviles tiene un getter para la configuración del plugin:

import android.app.Activity
import android.webkit.WebView
import app.tauri.annotation.TauriPlugin
import app.tauri.annotation.InvokeArg
@InvokeArg
class Config {
var timeout: Int? = 3000
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private var timeout: Int? = 3000
override fun load(webView: WebView) {
getConfig(Config::class.java).let {
this.timeout = it.timeout
}
}
}

Los plugins pueden vincularse a varios eventos del ciclo de vida:

  • load: Cuando el plugin se carga en la web view
  • onNewIntent: Solo Android, cuando la actividad se vuelve a lanzar

También hay eventos del ciclo de vida adicionales para plugins en la Guía de desarrollo de plugins.

  • Cuándo: Cuando el plugin se carga en la web view
  • Por qué: Ejecutar código de inicialización del plugin
import android.app.Activity
import android.webkit.WebView
import app.tauri.annotation.TauriPlugin
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
// perform plugin setup here
}
}

Nota: Esto solo está disponible en Android.

  • Cuándo: Cuando la actividad se vuelve a lanzar. Consulta Activity#onNewIntent para obtener más información.
  • Por qué: Manejar el re-lanzamiento de la aplicación, como cuando se hace clic en una notificación o se accede a un enlace profundo (deep link).
import android.app.Activity
import android.content.Intent
import app.tauri.annotation.TauriPlugin
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun onNewIntent(intent: Intent) {
// handle new intent event
}
}

Hay una clase de plugin dentro de los respectivos proyectos móviles donde se pueden definir comandos que pueden ser llamados por el código Rust:

import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun openCamera(invoke: Invoke) {
val ret = JSObject()
ret.put("path", "/path/to/photo.jpg")
invoke.resolve(ret)
}
}

Si quieres usar una función suspend de Kotlin, necesitas usar un scope de corrutina personalizado

import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
// Change to Dispatchers.IO if it is intended for fetching data
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun openCamera(invoke: Invoke) {
scope.launch {
openCameraInner(invoke)
}
}
private suspend fun openCameraInner(invoke: Invoke) {
val ret = JSObject()
ret.put("path", "/path/to/photo.jpg")
invoke.resolve(ret)
}
}

Usa tauri::plugin::PluginHandle para llamar a un comando móvil desde Rust:

use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::Runtime;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraRequest {
quality: usize,
allow_edit: bool,
}
#[derive(Deserialize)]
pub struct Photo {
path: PathBuf,
}
impl<R: Runtime> <plugin-name;pascal-case><R> {
pub fn open_camera(&self, payload: CameraRequest) -> crate::Result<Photo> {
self
.0
.run_mobile_plugin("openCamera", payload)
.map_err(Into::into)
}
}

Los argumentos se serializan a comandos y se pueden analizar en el plugin móvil con la función Invoke::parseArgs, tomando una clase que describe el objeto de argumento.

En Android, los argumentos se definen como una clase anotada con @app.tauri.annotation.InvokeArg. Los objetos internos también deben estar anotados:

import android.app.Activity
import android.webkit.WebView
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
@InvokeArg
internal class OpenAppArgs {
lateinit var name: String
var timeout: Int? = null
}
@InvokeArg
internal class OpenArgs {
lateinit var requiredArg: String
var allowEdit: Boolean = false
var quality: Int = 100
var app: OpenAppArgs? = null
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun openCamera(invoke: Invoke) {
val args = invoke.parseArgs(OpenArgs::class.java)
}
}

En iOS, los argumentos se definen como una clase que hereda Decodable. Los objetos internos también deben heredar el protocolo Decodable:

class OpenAppArgs: Decodable {
let name: String
var timeout: Int?
}
class OpenArgs: Decodable {
let requiredArg: String
var allowEdit: Bool?
var quality: UInt8?
var app: OpenAppArgs?
}
class ExamplePlugin: Plugin {
@objc public func openCamera(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(OpenArgs.self)
invoke.resolve(["path": "/path/to/photo.jpg"])
}
}

A menudo es preferible escribir código de plugin en Rust, por rendimiento y reutilización. Si bien Tauri no proporciona directamente un mecanismo para llamar a Rust desde el código de tu plugin, el uso de JNI en Android y FFI en iOS permite que los plugins llamen a código compartido, incluso cuando la WebView de la aplicación está suspendida.

En el Cargo.toml de tu plugin, agrega el crate jni como dependencia:

[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"

Carga la biblioteca de la aplicación estáticamente y define funciones nativas en tu código Kotlin. En este ejemplo, la clase Kotlin es com.example.HelloWorld, necesitamos hacer referencia al nombre completo del paquete desde el lado de Rust.

private const val TAG = "MyPlugin"
init {
try {
// Load the native library (libapp_lib.so)
// This is the shared library built by Cargo with crate-type = ["cdylib"]
System.loadLibrary("app_lib")
Log.d(TAG, "Successfully loaded libapp_lib.so")
} catch (e: UnsatisfiedLinkError) {
Log.e(TAG, "Failed to load libapp_lib.so", e)
throw e
}
}
external fun helloWorld(name: String): String?

Luego, en el código Rust de tu plugin, define la función que JNI buscará. El formato de la función es Java_package_class_method, por lo que para nuestra clase anterior esto se convierte en Java_com_example_HelloWorld_helloWorld para que sea llamada por nuestro método helloWorld:

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_example_HelloWorld_helloWorld(
mut env: JNIEnv,
_class: JClass,
name: JString,
) -> jstring {
log::debug!("Calling JNI Hello World!");
let result = format!("Hello, {}!", name);
match env.new_string(result) {
Ok(jstr) => jstr.into_raw(),
Err(e) => {
log::error!("Failed to create JString: {}", e);
std::ptr::null_mut()
}
}
}

iOS solo usa FFI C estándar, por lo que no necesita nuevas dependencias. Agrega el hook en tu código Swift, así como cualquier limpieza necesaria. Estas funciones se pueden llamar de cualquier forma válida, pero deben estar anotadas con @_silgen_name(FFI_FUNC), donde FFI_FUNC es el nombre de la función que se llamará desde Rust:

@_silgen_name("hello_world_ffi")
private static func helloWorldFFI(_ name: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar>?
@_silgen_name("free_hello_result_ffi")
private static func freeHelloResult(_ result: UnsafeMutablePointer<CChar>)
static func helloWorld(name: String) -> String? {
// Call Rust FFI
let resultPtr = name.withCString({ helloWorldFFI($0) })
// Convert C string to Swift String
let result = String(cString: resultPtr)
// Free the C string
freeHelloResult(resultPtr)
return result
}

Luego, implementa el lado de Rust. Las funciones extern aquí deben coincidir con las anotaciones @_silgen_name del lado de Swift:

#[no_mangle]
pub unsafe extern "C" fn hello_world_ffi(c_name: *const c_char) -> *mut c_char {
let name = match CStr::from_ptr(c_name).to_str() {
Ok(s) => s,
Err(e) => {
log::error!("[iOS FFI] Failed to convert C string: {}", e);
return std::ptr::null_mut();
}
};
let result = format!("Hello, {}!", name);
match CString::new(result) {
Ok(c_str) => c_str.into_raw(),
Err(e) => {
log::error!("[iOS FFI] Failed to create C string: {}", e);
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn free_hello_result_ffi(result: *mut c_char) {
if !result.is_null() {
drop(CString::from_raw(result));
}
}

Google está avanzando para hacer que las páginas de memoria de 16KB sean un requisito en todos los envíos de nuevas aplicaciones de Android. Compilar con una versión de NDK 28 o superior debería generar automáticamente paquetes que cumplan con este requisito, pero en caso de que deba usarse una versión anterior de NDK o los archivos generados no estén alineados a 16KB, se puede agregar lo siguiente a .cargo/config.toml para indicarlo a rustc:

[target.aarch64-linux-android]
rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]

Si un plugin requiere permisos del usuario final, Tauri simplifica el proceso de verificación y solicitud de permisos.

Primero define la lista de permisos necesarios y un alias para identificar cada grupo en el código. Esto se hace dentro de la anotación TauriPlugin:

@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
]
)
class ExamplePlugin(private val activity: Activity): Plugin(activity) { }

Tauri implementa automáticamente dos comandos para el plugin: checkPermissions y requestPermissions. Esos comandos se pueden llamar directamente desde JavaScript o Rust:

import { invoke, PermissionState } from '@tauri-apps/api/core'
interface Permissions {
postNotification: PermissionState
}
// check permission state
const permission = await invoke<Permissions>('plugin:<plugin-name>|checkPermissions')
if (permission.postNotification === 'prompt-with-rationale') {
// show information to the user about why permission is needed
}
// request permission
if (permission.postNotification.startsWith('prompt')) {
const state = await invoke<Permissions>('plugin:<plugin-name>|requestPermissions', { permissions: ['postNotification'] })
}

Los plugins pueden emitir eventos en cualquier momento utilizando la función trigger:

@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
trigger("load", JSObject())
}
override fun onNewIntent(intent: Intent) {
// handle new intent event
if (intent.action == Intent.ACTION_VIEW) {
val data = intent.data.toString()
val event = JSObject()
event.put("data", data)
trigger("newIntent", event)
}
}
@Command
fun openCamera(invoke: Invoke) {
val payload = JSObject()
payload.put("open", true)
trigger("camera", payload)
}
}

Las funciones auxiliares se pueden llamar desde el paquete NPM utilizando la función auxiliar addPluginListener:

import { addPluginListener, PluginListener } from '@tauri-apps/api/core';
export async function onRequest(
handler: (url: string) => void
): Promise<PluginListener> {
return await addPluginListener(
'<plugin-name>',
'event-name',
handler
);
}

© 2026 Colaboradores de Tauri. CC-BY / MIT