【Tauri Android】Kotlinで実装したネイティブ機能と連携する

# はじめに

[Tauri](https://v2.tauri.app/)でAndroidアプリを開発する際、フロントエンドはWebViewベースで構築されています。そのため、OS固有のネイティブ機能にアクセスするには、まずKotlinでその機能を実装し、TypeScript、Rust、Kotlin間の橋渡し(ブリッジ)を構築する必要があります。
ただし、このブリッジの仕組みはTauriフレームワークによって提供されているため、開発者は用意された手順に従って実装するだけで、言語間の連携を実現できます。

この記事では、フロントエンドからKotlinで書かれたネイティブ機能との連携方法を紹介します。

[^mb-support]:https://v2.tauri.app/blog/tauri-20/#mobile-support

# プラグインの作成

今のところ、ネイティブ機能と連携するためには、[プラグイン](https://v2.tauri.app/develop/plugins/)を作成する必要があります[^mb-support]。

## プラグインプロジェクトの生成

Tauriアプリのプロジェクトのルートディレクトリで次のコマンドを打ちます。 `[name]` は任意のプラグイン名です。

```
npx @tauri-apps/cli plugin new [name] --android --ios
```

`--ios` は、iOSをサポートしない場合は不要です。

コマンドに成功すると、ルートディレクトリ直下にプラグイン用のプロジェクトが生成されます。プラグイン名には自動的に `tauri-plugin-` というプレフィックスが付きます。

これ以降、説明のためにプラグイン名をtauri-plugin-exampleとします。

アプリのディレクトリ構成は以下のようになります。

```
[tauri-app]/  <- アプリのルートディレクトリ
└ index.html
└ public/
└ src/
└ src-tauri/
└ package.json
└ ...
└ tauri-plugin-example/  <- プラグイン用のプロジェクトが生成される
```

## プラグインの追加

上記で生成したプラグインをアプリの依存関係に追加します。

### Cargo.toml

```diff_toml:src-tauri/Cargo.toml
[dependencies]
+ tauri-plugin-example = { path = "../tauri-plugin-example/" }
```

### package.json

```diff_json:package.json
{
  "dependencies": {
+   "tauri-plugin-example-api": "file:./tauri-plugin-example"
  }
}
```

### lib.rs

`src-tauri/src/lib.rs` にプラグインの初期化コードを追加します。

```diff_rust:src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
+       .plugin(tauri_plugin_example::init())
        ...
}
```

### パーミッションの設定

アプリのパーミッションに追加したプラグインを設定します。パーミッションは `default` にします。なお、プラグイン名に `tauri-plugin-` プレフィックスは不要です。

```diff_json:src-tauri/capabilities/default.json
{
  "permissions": [
    "core:default",
    "shell:allow-open"
+   "example:default"
  ]
}
```

## プラグインの実装

この記事では、例として、フロントエンドからメッセージを受け取ってToastを表示するコマンドを実装します。

### ExamplePlugin.kt

`tauri-plugin-example/android/src/main/java/ExamplePlugin.kt` に下記のような `showToast` メソッドを実装します。メソッドには `@Command` アノテーションを付けます。 `ShowToastArgs` クラスはフロントエンドから渡されるデータの型を表します。

```kotlin:tauri-plugin-example/android/src/main/java/ExamplePlugin.kt
@InvokeArg
class ShowToastArgs {
    var message: String? = null

    /**
     * "short" or "long"
     */
    var length: String? = null
}

@TauriPlugin
class ExamplePlugin(private val activity: Activity) : Plugin(activity) {
    @Command
    fun showToast(invoke: Invoke) {
        val args = invoke.parseArgs(ShowToastArgs::class.java)
        args.message?.let {
            val length = when (args.length) {
                "short" -> Toast.LENGTH_SHORT
                "long" -> Toast.LENGTH_LONG
                else -> Toast.LENGTH_SHORT
            }
            Toast.makeText(activity, it, length).show()
        }

        invoke.resolve()
    }
}
```

コマンドが正常に終了したときは `invoke.resolve()` メソッドを呼びます。

:::note info
- フロントエンドにデータを返したいときは `resolve` メソッドの引数に `JSObject` を渡します
- コマンドが失敗したときは、 `resolve` メソッドの代わりに `reject` メソッドを呼ぶことでエラーを返せます
:::

:::note info
`ExamplePlugin` コンストラクタの引数には `Activity` が渡されるため、大抵のことはできそうです。
:::

### models.rs

Kotlinで書いた `ShowToastArgs` クラスに対応する形でモデルを定義します。変数名はスネークケースで書きます。

```rust:tauri-plugin-example/src/models.rs
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShowToastRequest {
    pub message: Option<String>,
    pub length: Option<String>,
}
```

フロントエンドにデータを返す場合、Responseのモデルもこのファイルに定義します。

### mobile.rs

モバイルプラットフォーム向けに `show_toast` メソッドを実装します。 `run_mobile_plugin` の第1引数は、Kotlinで実装したコマンドのメソッド名です。

```rust:tauri-plugin-example/src/mobile.rs
impl<R: Runtime> Example<R> {
    pub fn show_toast(&self, payload: ShowToastRequest) -> crate::Result<()> {
        self.0
            .run_mobile_plugin("showToast", payload)
            .map_err(Into::into)
    }
}
```

### desktop.rs

デスクトップ向けの `show_toast` メソッドですが、今回、デスクトップアプリは対象外のため、メソッドだけ用意し中身は何も実装しません。

```rust:tauri-plugin-example/src/desktop.rs
impl<R: Runtime> Example<R> {
    pub fn show_toast(&self, payload: ShowToastRequest) -> crate::Result<()> {
        // Nothing to do
        Ok(())
    }
}
```

### commands.rs

`tauri-plugin-example/src/commands.rs` に `show_toast` 関数を書きます。

```rust:tauri-plugin-example/src/commands.rs
#[command]
pub(crate) async fn show_toast<R: Runtime>(
    app: AppHandle<R>,
    payload: ShowToastRequest,
) -> Result<()> {
    app.example().show_toast(payload)
}
```

### lib.rs

`tauri-plugin-example/src/lib.rs` の `Builder` において、上記commands.rsで書いた `show_toast` を追加します。

```diff_rust:tauri-plugin-example/src/lib.rs
Builder::new("example")
    .invoke_handler(tauri::generate_handler![
        commands::ping,
+       commands::show_toast
    ])
```

### build.rs

`tauri-plugin-example/build.rs` の `COMMANDS` に `show_toast` を追加します。

```diff_rust:tauri-plugin-example/build.rs
const COMMANDS: &[&str] = &[
    "ping",
+   "show_toast",
];
```

### default.toml

`tauri-plugin-example/permissions/default.toml` の `permissions` に `allow-show-toast` を追加します。

```toml:tauri-plugin-example/permissions/default.toml
[default]
description = "Default permissions for the plugin"
permissions = ["allow-ping", "allow-show-toast"]
```

### index.ts

下記のような `showToast` 関数を書きます。 `payload` にセットするオブジェクトは、上記models.rsで書いた `ShowToastRequest` に対応します。キー名はキャメルケースで書きます。

```typescript:tauri-plugin-example/guest-js/index.ts
export async function showToast(
  message: string,
  length?: string,
): Promise<void> {
  return await invoke("plugin:example|show_toast", {
    payload: {
      message,
      length: length || "short",
    },
  });
}
```

`tauri-plugin-example/guest-js/index.ts` に書いたexport文は、フロントエンドでimportすることができます。

```typescript
import { showToast } from "tauri-plugin-example-api";
```

## プラグインのビルド

最後にビルドすることを忘れないようにしてください。

```
cd tauri-plugin-example
npm run build
```

# プラグインを使う

上記のindex.tsでエクスポートした `showToast` 関数をインポートして使えます。下記は使用例です。

```typescript:App.tsx
import { showToast } from "tauri-plugin-example-api";

...

function App() {
  ...

  // ボタンが押されたら、Toastで"Hello!"と表示する
  function handleClick() {
    showToast("Hello!").then(() => {
      // コマンドの実行に成功したとき(resolve)
    }).catch(error => {
      // コマンドの実行に失敗したとき(reject)
    });
  }

  ...
}

export default App;
```

# おわりに

フロントエンドからKotlinで書かれたネイティブ機能との連携方法を紹介しました。
実装するにあたり手数が多い印象ですが、Tauriの今後のバージョンアップで改善されることを期待しています。

# 参考

[Develop a Tauri plugin for Android](https://tauritutorials.com/blog/develop-a-tauri-plugin-for-android)