Since Tauri 2.0 many common functions, from cross-platform notifications, to accessing hardware features have been broken out into plugins. So, to access some platform specific features, especially on mobile, you may need to create a custom plugin.
In this guide we'll setup a fresh Tauri app, setup for Android, initialize and walk through a plugin, then add some android-specific functionality.
Setting up the project
In a terminal window run the following commands:
cargo create-tauri-app android-plugin-example cd android-plugin-example npm install cargo tauri android init
Here we initialize a Tauri app, install JavaScript dependencies, and setup android support for our app. For more info setting up a Tauri project go here.
You can choose whatever frontend stack you want I'll be using Vue for this guide.
The Android app should be ready to work on, run the following to test.
cargo tauri android dev
If you don't have your environment setup for android development read this guide for mac.
Setting up the plugin
Now let's setup the plugin by running the following commands:
tauri plugin new --android example cd tauri-plugin-examplenpm installnpm run build
Avoid using hyphens in the name due to an issue with android imports.
Update Cargo.toml
Add our plugin to the main Cargo.toml
file.
tauri-plugin-example = { path = "../tauri-plugin-example/" }
Update package.json
In your package.json
, add the plugin as a dependency:
"dependencies": { ... "tauri-plugin-example-api": "file:./tauri-plugin-example"}
Initialize the plugin in the Tauri app
Open up src-tauri/src/lib.rs
and init the plugin in the run()
function.
tauri::Builder::default() .plugin(tauri_plugin_example::init()) ...
Configuring Permissions
Next, we'll add permissions to our capabilities.json
file.
"permissions": [ ... "example:default"]
Here we add the default permission set to the plugin, this will expose the ping command that comes default with the plugin.
To test the example plugin I'll replace App.vue
with the following:
<script setup lang="ts">import { ref, onMounted} from "vue";import { ping } from "tauri-plugin-example-api"; let result = ref(""); onMounted(async () => { result.value = await ping("Hello") ?? "";}); </script> <template> <main class="container"> {{ result }} </main></template>
Here we call the ping command when the component mounts and display the result on the page, which just echos back whatever we pass into it.
Now re-run our app.
cargo tauri android dev
You should see "Hello" on the screen.
Exploring the plugin structure
Here's an overview of what the plugin consists of:
build.rs
Inside of build.rs
we will define the commands that we want to expose to the users of our plugin, this will auto generate the permission files for every command we put in here.
const COMMANDS: &[&str] = &["ping"]; fn main() { tauri_plugin::Builder::new(COMMANDS) .android_path("android") .ios_path("ios") .build();}
guest-js/
The guest-js/
directory contains all the JavaScript that will be published with our plugin, mostly helper functions to call our plugin's commands.
src/
The src/
directory contains all of our rust code.
lib.rs
This file is the main entry point of our rust crate, and the init function will setup our plugin.
#[cfg(desktop)]use desktop::Example;#[cfg(mobile)]use mobile::Example; /// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the example APIs.pub trait ExampleExt<R: Runtime> { fn example(&self) -> &Example<R>;} impl<R: Runtime, T: Manager<R>> crate::ExampleExt<R> for T { fn example(&self) -> &Example<R> { self.state::<Example<R>>().inner() }} /// Initializes the plugin.pub fn init<R: Runtime>() -> TauriPlugin<R> { Builder::new("example") .invoke_handler(tauri::generate_handler![commands::ping]) .setup(|app, api| { #[cfg(mobile)] let example = mobile::init(app, api)?; #[cfg(desktop)] let example = desktop::init(app, api)?; app.manage(example); Ok(()) }) .build()}
You'll notice the #[cfg(mobile)]
attributes throughout this file, this will optionally include code depending on what platforms we're targeting. So, the mobile code will only be included while we're building for mobile.
The tauri::plugin::Builder
is very similar to the tauri::Builder
, we create a new instance with a name, this will namespace our invoke commands, we generate and register our invokable commands, and we can do additional setup when our app begins to run.
commands.rs
Contains the higher level commands that will get passed down to our individual implementations either desktop or mobile.
mobile.rs
Contains the mobile specific rust implementations for our plugin.
android/
android directory will contain all the android specific code including custom kotlin code for the plugin.
android/src/main/java contains the logic of our plugin.
ExamplePlugin.kt
The plugin comes with an example kotlin implementation for our ping command.
@InvokeArgclass PingArgs { var value: String? = null} @TauriPluginclass ExamplePlugin(private val activity: Activity): Plugin(activity) { private val implementation = Example() @Command fun ping(invoke: Invoke) { val args = invoke.parseArgs(PingArgs::class.java) val ret = JSObject() ret.put("value", implementation.pong(args.value ?: "default value :(")) invoke.resolve(ret) }}
Example.kt
class Example { fun pong(value: String): String { Log.i("Pong", value) return value }}
Adding new commands
Now that we have an understanding of how a tauri plugin is laid out, we can start adding custom android-specific functionality. We'll build a command to open an Android native toast pop up.
To do this we'll just edit our ExamplePlugin.kt
file.
First import the required Toast class.
import android.widget.Toast
Then add a class to model our command's input.
@InvokeArgclass ToastArgs { var value: String? = null}
Then add the toast function with the @Command
annotation.
@Commandfun toast(invoke: Invoke) { val args = invoke.parseArgs(PingArgs::class.java) Toast.makeText(activity, args.value, Toast.LENGTH_SHORT).show()}
Now go to the models.rs
file and add a struct to model the input.
#[derive(Debug, Deserialize, Serialize)]#[serde(rename_all = "camelCase")]pub struct ToastRequest { pub value: String,}
Then add a command in commands.rs
for our higher level command.
#[command]pub(crate) async fn toast<R: Runtime>( app: AppHandle<R>, payload: ToastRequest,) -> Result<()> { app.example().toast(payload)}
then add an implementation for toast in mobile.rs
under the ping function.
pub fn toast(&self, payload: ToastRequest) -> crate::Result<()> { self.0 .run_mobile_plugin("toast", payload) .map_err(Into::into)}
Then add a helper in index.ts
export async function toast(value: string): Promise<void> { await invoke<{ value?: string }>("plugin:example|toast", { payload: { value, }, }).then((r) => (r.value ? r.value : null));}
remember to run npm run build
when updating the typescript file.
Add "toast" to the build.rs
file, this will generate the proper permission files.
const COMMANDS: &[&str] = &["ping", "toast"];
You may also update the permissions/default.toml
file to include the "allow-toast" permission by default, otherwise the users of your plugin will have to specify "allow-toast" in their capabilities.
[default]description = "Default permissions for the plugin"permissions = ["allow-ping", "allow-toast"]
Finally we can edit our App.vue
file to test it out.
<script setup lang="ts">import { toast } from "tauri-plugin-example-api"; async function showToast() { await toast("Hello Toast!");}</script> <template> <main class="container"> <button @click="showToast">Toast me</button> </main></template>
Now when we run cargo tauri android dev
again we can press the button and see our native toast notification pop up.
This is just a primer on Android-specific development for Tauri, if you want more examples and inspiration I suggest looking through the existing plugin ecosystem.