移动插件开发
插件可以运行用 Kotlin(或 Java)和 Swift 编写的原生移动代码。默认插件模板包括一个使用 Kotlin 的 Android 库项目和一个 Swift 包,其中包括一个示例移动命令,展示如何从 Rust 代码触发其执行。
¥Plugins can run native mobile code written in Kotlin (or Java) and Swift. The default plugin template includes an Android library project using Kotlin and a Swift package including an example mobile command showing how to trigger its execution from Rust code.
初始化插件项目
¥Initialize Plugin Project
按照 插件开发指南 中的步骤初始化新插件项目。
¥Follow the steps in the Plugin Development guide to initialize a new plugin project.
如果你有一个现有的插件,并想为其添加 Android 或 iOS 功能,你可以使用 plugin android init
和 plugin ios init
来引导移动库项目并指导你完成所需的更改。
¥If you have an existing plugin and would like to add Android or iOS capabilities to it, you can use plugin android init
and plugin ios init
to bootstrap the mobile library projects and guide you through the changes needed.
默认插件模板将插件的实现分为两个独立的模块:desktop.rs
和 mobile.rs
。
¥The default plugin template splits the plugin’s implementation into two separate modules: desktop.rs
and mobile.rs
.
桌面实现使用 Rust 代码来实现功能,而移动实现向原生移动代码发送消息以执行功能并返回结果。如果两个实现都需要共享逻辑,则可以在 lib.rs
中定义它:
¥The desktop implementation uses Rust code to implement a functionality, while the mobile implementation sends a message to the native mobile code to execute a function and get a result back. If shared logic is needed across both implementations, it can be defined in 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 }}
此实现简化了共享 API 的过程,该 API 可由命令和 Rust 代码使用。
¥This implementation simplifies the process of sharing an API that can be used both by commands and Rust code.
开发 Android 插件
¥Develop an Android Plugin
Android 版 Tauri 插件定义为扩展 app.tauri.plugin.Plugin
并使用 app.tauri.annotation.TauriPlugin
注释的 Kotlin 类。每个用 app.tauri.annotation.Command
注释的方法都可以由 Rust 或 JavaScript 调用。
¥A Tauri plugin for Android is defined as a Kotlin class that extends app.tauri.plugin.Plugin
and is annotated with app.tauri.annotation.TauriPlugin
. Each method annotated with app.tauri.annotation.Command
can be called by Rust or JavaScript.
Tauri 默认使用 Kotlin 来实现 Android 插件,但如果你愿意,也可以切换到 Java。生成插件后,右键单击 Android Studio 中的 Kotlin 插件类,然后从菜单中选择 “将 Kotlin 文件转换为 Java 文件” 选项。Android Studio 将指导你将项目迁移到 Java。
¥Tauri uses Kotlin by default for the Android plugin implementation, but you can switch to Java if you prefer. After generating a plugin, right click the Kotlin plugin class in Android Studio and select the “Convert Kotlin file to Java file” option from the menu. Android Studio will guide you through the project migration to Java.
开发 iOS 插件
¥Develop an iOS Plugin
iOS 版 Tauri 插件定义为从 Tauri
包扩展 Plugin
类的 Swift 类。每个具有 @objc
属性和 (_ invoke: Invoke)
参数的函数(例如 @objc private func download(_ invoke: Invoke) { }
)都可以由 Rust 或 JavaScript 调用。
¥A Tauri plugin for iOS is defined as a Swift class that extends the Plugin
class from the Tauri
package. Each function with the @objc
attribute and the (_ invoke: Invoke)
parameter (for example @objc private func download(_ invoke: Invoke) { }
) can be called by Rust or JavaScript.
插件被定义为 Swift 包,因此你可以使用其包管理器来管理依赖。
¥The plugin is defined as a Swift package so that you can use its package manager to manage dependencies.
插件配置
¥Plugin Configuration
有关开发插件配置的更多详细信息,请参阅插件开发指南的 插件配置部分。
¥Refer to the Plugin Configuration section of the Plugin Development guide for more details on developing plugin configurations.
移动设备上的插件实例有一个用于插件配置的 getter:
¥The plugin instance on mobile has a getter for the plugin configuration:
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 {} }}
生命周期事件
¥Lifecycle Events
插件可以挂接到多个生命周期事件:
¥Plugins can hook into several lifecycle events:
-
load:当插件加载到 Web 视图中时
¥load: When the plugin is loaded into the web view
-
onNewIntent:仅限 Android,当活动重新启动时
¥onNewIntent: Android only, when the activity is re-launched
插件开发指南中还有额外的 插件的生命周期事件。
¥There are also the additional lifecycle events for plugins in the Plugin Development guide.
load
-
时间:当插件加载到 Web 视图中时
¥When: When the plugin is loaded into the web view
-
原因:执行插件初始化代码
¥Why: Execute plugin initialization code
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
注意:这仅适用于 Android。
¥Note: This is only available on Android.
-
时间:当活动重新启动时。有关更多信息,请参阅 Activity#onNewIntent。
¥When: When the activity is re-launched. See Activity#onNewIntent for more information.
-
原因:处理应用重新启动,例如单击通知或访问深层链接时。
¥Why: Handle application re-launch such as when a notification is clicked or a deep link is accessed.
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 }}
添加移动命令
¥Adding Mobile Commands
在相应的移动项目内部有一个插件类,可以在其中定义可由 Rust 代码调用的命令:
¥There is a plugin class inside the respective mobile projects where commands can be defined that can be called by the Rust code:
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) }}
如果你想使用 Kotlin suspend
函数,则需要使用自定义协程范围
¥If you want to use a Kotlin suspend
function, you need to use a custom coroutine scope
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"]) }}
使用 tauri::plugin::PluginHandle
从 Rust 调用移动命令:
¥Use the tauri::plugin::PluginHandle
to call a mobile command from 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) }}
命令参数
¥Command Arguments
参数被序列化为命令,并可以在移动插件上使用 Invoke::parseArgs
函数进行解析,采用描述参数对象的类。
¥Arguments are serialized to commands and can be parsed on the mobile plugin with the Invoke::parseArgs
function, taking a class describing the argument object.
Android
在 Android 上,参数定义为用 @app.tauri.annotation.InvokeArg
注释的类。内部对象也必须注释:
¥On Android, the arguments are defined as a class annotated with @app.tauri.annotation.InvokeArg
. Inner objects must also be annotated:
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) }}
:::note 注意
可选参数定义为 var <argumentName>: Type? = null
¥Optional arguments are defined as var <argumentName>: Type? = null
具有默认值的参数定义为 var <argumentName>: Type = <default-value>
¥Arguments with default values are defined as var <argumentName>: Type = <default-value>
必需参数定义为 lateinit var <argumentName>: Type
¥Required arguments are defined as lateinit var <argumentName>: Type
:::
iOS
在 iOS 上,参数定义为继承 Decodable
的类。内部对象还必须继承 Decodable 协议:
¥On iOS, the arguments are defined as a class that inherits Decodable
. Inner objects must also inherit the Decodable protocol:
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"]) }}
:::note 注意
可选参数定义为 var <argumentName>: Type?
¥Optional arguments are defined as var <argumentName>: Type?
不支持具有默认值的参数。改用可空类型并在命令函数上设置默认值。
¥Arguments with default values are NOT supported. Use a nullable type and set the default value on the command function instead.
必需参数定义为 let <argumentName>: Type
¥Required arguments are defined as let <argumentName>: Type
:::
权限
¥Permissions
如果插件需要终端用户的权限,Tauri 会简化检查和请求权限的过程。
¥If a plugin requires permissions from the end user, Tauri simplifies the process of checking and requesting permissions.
首先定义所需的权限列表和别名以在代码中标识每个组。这是在 TauriPlugin
注释中完成的:
¥First define the list of permissions needed and an alias to identify each group in code. This is done inside the TauriPlugin
annotation:
@TauriPlugin( permissions = [ Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification") ])class ExamplePlugin(private val activity: Activity): Plugin(activity) { }
首先覆盖 checkPermissions
和 requestPermissions
函数:
¥First override the checkPermissions
and requestPermissions
functions:
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 会自动为插件实现两个命令:checkPermissions
和 requestPermissions
。这些命令可以直接从 JavaScript 或 Rust 调用:
¥Tauri automatically implements two commands for the plugin: checkPermissions
and requestPermissions
.
Those commands can be directly called from JavaScript or 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) }}
插件事件
¥Plugin Events
插件可以使用 trigger
函数在任何时间点触发事件:
¥Plugins can emit events at any point of time using the trigger
function:
@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]) }}
然后可以使用 addPluginListener
辅助函数从 NPM 包中调用辅助函数:
¥The helper functions can then be called from the NPM package by using the addPluginListener
helper function:
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 );}
Tauri 中文网 - 粤ICP备13048890号