Tauri 2.0 で ネイティブモジュール作成(Android版) [MLKitでオンデバイス翻訳]
1.はじめに
Tauri 2.0 が StableRelease になった!
Tauri 2.0 が StableRelease になったんですが、これがモバイルアプリも作成できるとのこと。
- モバイルサポート。デスクトップだけでなく、iOS/Android向けのアプリも「Tauri」で(上記リンクより引用)
Tauri 2.0 って?(個人的な理解)
ざっくり、 "Tauri" はクロスプラットフォーム対応のアプリフレームワークで、
バージョン 2.0 からは Android や iOS 向けのアプリケーションのビルドも可能になった。 フロントエンドが WebView だからフロントエンドフレームワークは大体なんでも選べて、バックエンドは Rust だけど、 Kotlin や Swift でも書けるから各OS向けのネイティブモジュールが書ける。ありていに言えば Electron + React Native みたいなやつ?
おもしろそ~!
ただ、 Tauri 2.0 から提供されるのは WebView と基本的なモジュールだけのようです。 じゃあ Android や iOS など各OSのネイティブな機能にアクセスするためにはどうすればいいの? ということで、ネイティブモジュールの作成をやってみました。
成果物(参考)
こんな感じのものを作りました。入力したテキストを英訳して返します。
注意事項
前提条件
Cargo とか Rust とか create-tauri-app とか tauri-cli とか Android Studio とか Node.js(系のもの) とかのインストールが必要と思われる。とりあえず1つ目のプロジェクトを作ってビルドしてみるとその辺が確認できるのでオススメ。
環境構築
Android で JAVA_HOME に Path を通す必要があるので注意! この辺目を通しておけば引っかからないと思います。
2.プロジェクトのスタート
プラグインを作り始めてからはこんな感じ。実は create-tauri-app で普通の Tauri プロジェクト(非プラグインのプロジェクト)を作るときは tauri-cli が必須じゃないので、プラグインづくりを始めてから tauri-cli をインストールする人もいるのでは。プラグイン名には "tauri-plugin-" が自動でくっつくようなので注意。
cargo install tauri-cli
cargo tauri plugin new my-tauri-app --android
cd my-tauri-app
npm install
npm run build
cd examples/tauri-app
npm install
npm tairi android init
npm tairi android dev
Android のエミュレータで examples/tauri-app が実行されたら OK です。
3.基本的なコード
生成されるテンプレートに "Ping" という Command が登録されているので、これを書き換えていけばOKです。
雰囲気としては、 cargo tauri plugin new [name] してあげると、ほかのアプリに公開するネイティブモジュール本体の部分と、それを同じリポジトリ内でお試しで実行するための examples/tauri-app が出来る感じ。作成したネイティブモジュールは、 examples/tauri-app で npm tairi android dev を実行すればお試しできます。このへんは React Native のネイティブモジュールづくりとかと同じ感じ?
以下のようにそれぞれ編集
3-1.ネイティブモジュール関係
android\build.gradle.kts
Androidの設定ファイル
implementation("com.google.mlkit:translate:17.0.3")
android\src\main\java\ExamplePlugin.kt
ネイティブモジュールの本体
package net.smart9b.plugin.tauri_mlkit_plugin
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import com.google.mlkit.nl.translate.Translation
import com.google.mlkit.nl.translate.TranslatorOptions
import com.google.mlkit.nl.translate.TranslateLanguage
@InvokeArg
class TranslateArgs {
var text: String? = null
}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun translate(invoke: Invoke) {
val args = invoke.parseArgs(TranslateArgs::class.java)
val options = TranslatorOptions.Builder()
.setSourceLanguage(TranslateLanguage.JAPANESE)
.setTargetLanguage(TranslateLanguage.ENGLISH)
.build()
val translator = Translation.getClient(options)
translator.downloadModelIfNeeded()
.addOnSuccessListener {
// モデルのダウンロードが成功した場合、翻訳を実行
translator.translate(args.text ?: "").addOnSuccessListener { translatedText ->
val ret = JSObject()
ret.put("value", translatedText)
invoke.resolve(ret)
}.addOnFailureListener { exception ->
invoke.reject("Translation failed: ${exception.message}")
}
}
.addOnFailureListener { exception ->
invoke.reject("Model download failed: ${exception.message}")
}
}
}
android\src\main\java\Example.kt
必要ないので削除
//削除
android\src\main\AndroidManifest.xml
翻訳Modelのダウンロードが発生するので、インターネットアクセスを許可
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
3-2.RUSTコード関係
src\mobile.rs
モバイルアプリからAPIにアクセスするRUSTコード
/// Access to the tauri-mlkit-plugin APIs.
pub struct TauriMlkitPlugin<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> TauriMlkitPlugin<R> {
pub fn translate(&self, payload: TranslateRequest) -> crate::Result<TranslateResponse> {
self
.0
.run_mobile_plugin("translate", payload)
.map_err(Into::into)
}
}
src\commands.rs
コマンドを管理するRUSTコード
use tauri::{AppHandle, command, Runtime};
use crate::models::*;
use crate::Result;
use crate::TauriMlkitPluginExt;
#[command]
pub(crate) async fn translate<R: Runtime>(
app: AppHandle<R>,
payload: TranslateRequest,
) -> Result<TranslateResponse> {
app.tauri_mlkit_plugin().translate(payload)
}
src\models.rs
モデルを管理するRUSTコード
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranslateRequest {
pub text: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranslateResponse {
pub value: String,
}
src\desktop.rs
デスクトップアプリからAPIを利用するRUSTコード
コンパイルエラー対策でとりあえず仮に設定
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::models::*;
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<TauriMlkitPlugin<R>> {
Ok(TauriMlkitPlugin(app.clone()))
}
/// Access to the tauri-mlkit-plugin APIs.
pub struct TauriMlkitPlugin<R: Runtime>(AppHandle<R>);
impl<R: Runtime> TauriMlkitPlugin<R> {
pub fn translate(&self, payload: TranslateRequest) -> crate::Result<TranslateResponse> {
// デスクトップ版では実際の翻訳を行わず、入力をそのまま返します
Ok(TranslateResponse {
value: payload.text,
})
}
}
build.rs
コマンドを書き換えておく
const COMMANDS: &[&str] = &["translate"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.android_path("android")
.ios_path("ios")
.build();
}
3-3.パーミッション関係
permissions\autogenerated\reference.md
autogeneratedって書いてあるけど手で書き換えたけどいいの?少なくとも必要そうな設定ではあるので修正。
## Default Permission
Default permissions for the plugin
- `allow-translate`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`tauri-mlkit-plugin:allow-translate`
</td>
<td>
Enables the translate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`tauri-mlkit-plugin:deny-translate`
</td>
<td>
Denies the translate command without any pre-configured scope.
</td>
</tr>
</table>
permissions/default.toml
permissionsを書き換え
[default]
description = "Default permissions for the plugin"
permissions = ["allow-translate"]
3-4.フロントエンド関係
examples/tauri-app/src/App.svelte
フロントエンド側、ネイティブモジュールをインポートして普通に使える
<script>
import Greet from './lib/Greet.svelte'
import { translate } from 'tauri-plugin-tauri-mlkit-plugin-api'
let response = ''
let inputText = ''
function updateResponse(returnValue) {
response += `[${new Date().toLocaleTimeString()}] ` + (typeof returnValue === 'string' ? returnValue : JSON.stringify(returnValue)) + '<br>'
}
function _translate() {
translate(inputText).then(updateResponse).catch(updateResponse)
}
</script>
<main class="container">
<h1>Welcome to Tauri!</h1>
<div class="row">
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
</a>
<a href="https://svelte.dev" target="_blank">
<img src="/svelte.svg" class="logo svelte" alt="Svelte Logo" />
</a>
</div>
<p>
Click on the Tauri, Vite, and Svelte logos to learn more.
</p>
<div>
<input bind:value={inputText} placeholder="Enter text to translate" />
<button on:click="{_translate}">Translate</button>
<div>{@html response}</div>
</div>
</main>
<style>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.svelte:hover {
filter: drop-shadow(0 0 2em #ff3e00);
}
</style>
guest-js/index.ts
プラグインを使う設定
import { invoke } from '@tauri-apps/api/core'
export async function translate(text: string): Promise<string | null> {
return await invoke<{value?: string}>('plugin:tauri-mlkit-plugin|translate', {
payload: {
text,
},
}).then((r) => (r.value ? r.value : null));
}
4.ビルド時にエラーが出た
エミュレータではうまくいかなかった ので、実機でビルドした。
また、次のように修正。
android\build.gradle.kts
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
5.個人的な感想
なるほど Rust 分かんなくても書き換えていけばやれんことはないなと思いました(それでいいかはともかくとして)。
React Native でのネイティブモジュール作りと比べてみて、 Permission や Command 周りはいろいろ書かないといけないけど、 Command 名で分かりやすくリレーしている(?)のでわりと見通しは良い感じがしました。ただ、結局 React Native とおなじつらみ ( Kotlin と Swift は結局書かないといけない) は変わらないかも。
ここまで読んでいただきありがとうございました!
🌟 とか 💖 とかもらえると励みになります。
Discussion