<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rizo</title>
    <description>The latest articles on DEV Community by Rizo (@rizo_6da387ef0e9890c21450).</description>
    <link>https://dev.to/rizo_6da387ef0e9890c21450</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3492743%2F80f6afce-a714-4689-92f0-121f4f6b46fe.jpg</url>
      <title>DEV Community: Rizo</title>
      <link>https://dev.to/rizo_6da387ef0e9890c21450</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rizo_6da387ef0e9890c21450"/>
    <language>en</language>
    <item>
      <title>Bringing Android AppFunctions to Tauri with Rust</title>
      <dc:creator>Rizo</dc:creator>
      <pubDate>Sat, 07 Nov 2026 17:30:51 +0000</pubDate>
      <link>https://dev.to/rizo_6da387ef0e9890c21450/bringing-android-appfunctions-to-tauri-with-rust-3m55</link>
      <guid>https://dev.to/rizo_6da387ef0e9890c21450/bringing-android-appfunctions-to-tauri-with-rust-3m55</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8brf0qpczgo39es6m7at.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8brf0qpczgo39es6m7at.png" alt="Tauri, Rust, and Android AppFunctions" width="798" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently, I decided to explore what the built-in Gemini agent, which replaced Google Assistant, could actually do. I spent almost an hour walking around the room, asking it all sorts of questions and testing its limits.&lt;/p&gt;

&lt;p&gt;I started with things like “go to this website and find this for me,” then tried to figure out whether it could interact with the device itself. It turned out it could work with Google apps: notes, the calendar, and the rest of the usual Google ecosystem. It could tell me how much battery was left, but not which Wi-Fi network the device was connected to. Things like that.&lt;/p&gt;

&lt;p&gt;What I liked most was discovering that it could work with Spotify: play tracks by voice and add them to my favorites.&lt;/p&gt;

&lt;p&gt;That got me thinking: could it interact with other services or apps through some kind of MCP? I started imagining an MCP-like mechanism specifically for apps, including native ones. I wanted to avoid having every app spin up a web server and only being able to use it while the app was running.&lt;/p&gt;

&lt;p&gt;Something built into Android itself: a registry that Gemini could query and use according to predefined permissions. That would be pretty useful.&lt;/p&gt;

&lt;p&gt;I went looking, and it turned out Google had already thought of it and presented it back in May this year.&lt;/p&gt;

&lt;p&gt;I'm a fullstack developer, and I've been building desktop applications for the past two years. I wanted to combine a nice frontend with a fast, safe Rust backend, so I learned Tauri 2.0 and started using it.&lt;/p&gt;

&lt;p&gt;Since Tauri is cross-platform, I decided to build a plugin that would make AppFunctions convenient to use from Rust, without having to switch to Kotlin for function declarations. Macros instead of annotations.&lt;/p&gt;

&lt;p&gt;The feature is still experimental and requires Android 16 or later. So far, I've tested it through adb.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The plugin is called &lt;a href="https://github.com/NikolaKaplin/tauri-plugin-google-app-functions" rel="noopener noreferrer"&gt;tauri-plugin-google-app-functions&lt;/a&gt;. Add it to an existing Tauri project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# src-tauri/Cargo.toml&lt;/span&gt;
&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dependencies&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;tauri-plugin-google-app-functions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"0.2"&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build-dependencies&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;tauri-plugin-google-app-functions-build&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"0.2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add schema generation to &lt;code&gt;build.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tauri_build&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;tauri_plugin_google_app_functions_build&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare a regular Rust function with a macro and register the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri_plugin_google_app_functions&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;app_function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cd"&gt;/// Counts words in the supplied text.&lt;/span&gt;
&lt;span class="cd"&gt;///&lt;/span&gt;
&lt;span class="cd"&gt;/// # Arguments&lt;/span&gt;
&lt;span class="cd"&gt;/// * `text` - Text whose words should be counted.&lt;/span&gt;
&lt;span class="nd"&gt;#[app_function]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;count_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="nf"&gt;.split_whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[cfg_attr(mobile,&lt;/span&gt; &lt;span class="nd"&gt;tauri::mobile_entry_point)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_google_app_functions&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;generate_context!&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error while running the application"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The doc comments become descriptions for the agent, and the arguments and return type become part of the schema. Android sees this function as &lt;code&gt;countWords&lt;/code&gt;. Build the app through the Tauri CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rust → Kotlin
&lt;/h3&gt;

&lt;p&gt;The Rust declaration above is conceptually equivalent to the following native Jetpack AppFunctions declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.annotation.RequiresApi&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.AppFunctionDeclaration&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.AppFunctionService&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.AppFunctionServiceEntryPoint&lt;/span&gt;

&lt;span class="nd"&gt;@RequiresApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@AppFunctionServiceEntryPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TextFunctionsService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;appFunctionXmlFileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text_functions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextFunctions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/** Counts words in the supplied text. */&lt;/span&gt;
    &lt;span class="nd"&gt;@AppFunctionDeclaration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;countWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(?U)\\s+"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLong&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compares two ways of declaring a function. Internally, the plugin generates an XML schema and uses a JNI bridge; the handler itself stays in Rust. You don't need to write Kotlin declarations manually for ordinary AppFunctions integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A music example
&lt;/h2&gt;

&lt;p&gt;For the demo, I built a small Radix-based player: two tracks, play/pause, and track switching. An Android service owns playback, so AppFunctions can control it without opening the interface. The notification has playback controls too.&lt;/p&gt;

&lt;p&gt;Switching tracks is exposed as another Rust function. Here's an excerpt from the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Switches to the next demo track without opening the app.&lt;/span&gt;
&lt;span class="nd"&gt;#[app_function]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;next_track&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Playback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppFunctionError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;command&lt;/code&gt; forwards the action to the native player, and &lt;code&gt;Playback&lt;/code&gt; contains its playback state. The complete implementation is in &lt;a href="https://github.com/NikolaKaplin/tauri-plugin-google-app-functions/blob/v0.2.0/examples/tauri-app/src-tauri/src/music.rs" rel="noopener noreferrer"&gt;music.rs&lt;/a&gt;. Android sees this handler as &lt;code&gt;nextTrack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/NikolaKaplin/tauri-plugin-google-app-functions/releases/download/v0.2.0/demo.mp4" rel="noopener noreferrer"&gt;Watch the demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once the example is installed, you can test track switching from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples/tauri-app&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\test-music.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important clarification about Gemini: according to &lt;a href="https://developer.android.com/ai/appfunctions" rel="noopener noreferrer"&gt;Google's documentation&lt;/a&gt;, the integration is still in private preview. A successful adb call doesn't automatically mean Gemini will have access to your app's functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/NikolaKaplin/tauri-plugin-google-app-functions" rel="noopener noreferrer"&gt;Source code, example, and setup instructions&lt;/a&gt; · &lt;a href="https://crates.io/crates/tauri-plugin-google-app-functions" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>android</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
