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.
Inicializar Proyecto de Plugin
Sección titulada «Inicializar Proyecto de Plugin»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:
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.
Desarrollar un Plugin para Android
Sección titulada «Desarrollar un Plugin para Android»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.
Desarrollar un Plugin para iOS
Sección titulada «Desarrollar un Plugin para iOS»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.
Configuración del Plugin
Sección titulada «Configuración del Plugin»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.Activityimport android.webkit.WebViewimport app.tauri.annotation.TauriPluginimport app.tauri.annotation.InvokeArg
@InvokeArgclass Config { var timeout: Int? = 3000}
@TauriPluginclass 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 } }}struct Config: Decodable { let timeout: Int?}
class ExamplePlugin: Plugin { var timeout: Int? = 3000
@objc public override func load(webview: WKWebView) { do { let config = try parseConfig(Config.self) self.timeout = config.timeout } catch {} }}Eventos del Ciclo de Vida
Sección titulada «Eventos del Ciclo de Vida»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.Activityimport android.webkit.WebViewimport app.tauri.annotation.TauriPlugin
@TauriPluginclass ExamplePlugin(private val activity: Activity): Plugin(activity) { override fun load(webView: WebView) { // perform plugin setup here }}class ExamplePlugin: Plugin { @objc public override func load(webview: WKWebView) { let timeout = self.config["timeout"] as? Int ?? 30 }}onNewIntent
Sección titulada «onNewIntent»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.Activityimport android.content.Intentimport app.tauri.annotation.TauriPlugin
@TauriPluginclass ExamplePlugin(private val activity: Activity): Plugin(activity) { override fun onNewIntent(intent: Intent) { // handle new intent event }}Agregar Comandos Móviles
Sección titulada «Agregar Comandos Móviles»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.Activityimport app.tauri.annotation.Commandimport app.tauri.annotation.TauriPlugin
@TauriPluginclass 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.Activityimport app.tauri.annotation.Commandimport app.tauri.annotation.TauriPlugin
// Change to Dispatchers.IO if it is intended for fetching dataval scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@TauriPluginclass 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) }}class ExamplePlugin: Plugin { @objc public func openCamera(_ invoke: Invoke) throws { invoke.resolve(["path": "/path/to/photo.jpg"]) }}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) }}Argumentos de Comandos
Sección titulada «Argumentos de Comandos»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.
Android
Sección titulada «Android»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.Activityimport android.webkit.WebViewimport app.tauri.annotation.Commandimport app.tauri.annotation.InvokeArgimport app.tauri.annotation.TauriPlugin
@InvokeArginternal class OpenAppArgs { lateinit var name: String var timeout: Int? = null}
@InvokeArginternal class OpenArgs { lateinit var requiredArg: String var allowEdit: Boolean = false var quality: Int = 100 var app: OpenAppArgs? = null}
@TauriPluginclass 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"]) }}Llamar a Rust desde Plugins Móviles
Sección titulada «Llamar a Rust desde Plugins Móviles»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.
Android
Sección titulada «Android»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)); }}Páginas de Memoria de 16KB de Android
Sección titulada «Páginas de Memoria de 16KB de Android»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"]Permisos
Sección titulada “Permisos”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) { }Primero sobrescribe las funciones checkPermissions y requestPermissions:
class ExamplePlugin: Plugin { @objc open func checkPermissions(_ invoke: Invoke) { invoke.resolve(["postNotification": "prompt"]) }
@objc public override func requestPermissions(_ invoke: Invoke) { // request permissions here // then resolve the request invoke.resolve(["postNotification": "granted"]) }}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 stateconst 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 permissionif (permission.postNotification.startsWith('prompt')) { const state = await invoke<Permissions>('plugin:<plugin-name>|requestPermissions', { permissions: ['postNotification'] })}use serde::{Serialize, Deserialize};use tauri::{plugin::PermissionState, Runtime};
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]struct PermissionResponse { pub post_notification: PermissionState,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]struct RequestPermission { post_notification: bool,}
impl<R: Runtime> Notification<R> { pub fn request_post_notification_permission(&self) -> crate::Result<PermissionState> { self.0 .run_mobile_plugin::<PermissionResponse>("requestPermissions", RequestPermission { post_notification: true }) .map(|r| r.post_notification) .map_err(Into::into) }
pub fn check_permissions(&self) -> crate::Result<PermissionResponse> { self.0 .run_mobile_plugin::<PermissionResponse>("checkPermissions", ()) .map_err(Into::into) }}Eventos de Plugin
Sección titulada «Eventos de Plugin»Los plugins pueden emitir eventos en cualquier momento utilizando la función trigger:
@TauriPluginclass 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) }}class ExamplePlugin: Plugin { @objc public override func load(webview: WKWebView) { trigger("load", data: [:]) }
@objc public func openCamera(_ invoke: Invoke) { trigger("camera", data: ["open": true]) }}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