AndroidアプリでGoogleのML Kitライブラリを使ったオンデバイス翻訳機能の実装方法について詳しく解説します。オンデバイス翻訳はインターネット接続なしで動作するため、オフライン環境でも使えるアプリを開発したい方に最適です。それでは早速、実装方法を見ていきましょう!

ML Kitとは?

ML Kitは、Googleが提供する機械学習のモバイルSDKです。画像認識、顔検出、テキスト認識(OCR)、バーコードスキャン、テキスト翻訳など、様々な機能を簡単に実装できます。今回解説するのは「オンデバイス翻訳」機能で、これを使えば端末内で翻訳処理を完結させることができます。

ML Kitのメリット:

  • インターネット接続なしで翻訳が可能
  • プライバシーの保護(データがサーバーに送信されない)
  • レイテンシの削減(サーバーとの通信が不要)
  • 通信コストの削減

実装の流れ

  1. プロジェクトの設定
  2. 必要な依存関係の追加
  3. 翻訳モデルのダウンロード処理の実装
  4. 翻訳機能の実装
  5. UIの作成
  6. 実際に動かしてみる

1. プロジェクトの設定

まずはML Kitを使うための準備をしましょう。build.gradleファイルに必要な依存関係を追加します。

build.gradle (app)

dependencies {
    // ML Kit翻訳ライブラリ
    implementation 'com.google.mlkit:translate:17.0.1'
    
    // LiveDataとViewModelのサポート
    implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
    implementation 'androidx.lifecycle:lifecycle-livedata:2.6.2'
    implementation 'androidx.lifecycle:lifecycle-runtime:2.6.2'
    
    // UI関連のライブラリ
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'com.google.android.material:material:1.9.0'
}

次に、AndroidManifest.xmlにインターネットアクセス権限を追加します(モデルのダウンロードに必要です):

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mltranslation">

    <uses-permission android:name="android.permission.INTERNET" />
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

2. 翻訳機能のViewModel作成

MVVMパターンを使って翻訳機能を実装します。まずはViewModelを作成しましょう。

TranslateViewModel.kt

package com.example.mltranslation

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.mlkit.nl.translate.TranslateLanguage
import com.google.mlkit.nl.translate.Translation
import com.google.mlkit.nl.translate.Translator
import com.google.mlkit.nl.translate.TranslatorOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.util.*

class TranslateViewModel(application: Application) : AndroidViewModel(application) {

    // 翻訳結果を保持するLiveData
    private val _translatedText = MutableLiveData<String>()
    val translatedText: LiveData<String> = _translatedText

    // ローディング状態を管理するLiveData
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    // モデルのダウンロード状態を管理するLiveData
    private val _modelDownloading = MutableLiveData<Boolean>()
    val modelDownloading: LiveData<Boolean> = _modelDownloading

    // エラーを保持するLiveData
    private val _errorMessage = MutableLiveData<String>()
    val errorMessage: LiveData<String> = _errorMessage

    // 翻訳オブジェクト
    private var translator: Translator? = null

    // 翻訳を実行する関数
    fun translate(text: String, sourceLanguage: String, targetLanguage: String) {
        if (text.isEmpty()) {
            _translatedText.value = ""
            return
        }

        _isLoading.value = true

        // 前回の翻訳オブジェクトがあれば破棄
        translator?.close()

        // 新しい翻訳オブジェクトを作成
        val options = TranslatorOptions.Builder()
            .setSourceLanguage(sourceLanguage)
            .setTargetLanguage(targetLanguage)
            .build()
        translator = Translation.getClient(options)

        viewModelScope.launch(Dispatchers.IO) {
            try {
                // モデルが利用可能かチェック
                val conditions = translator?.downloadModelIfNeeded()
                _modelDownloading.postValue(true)
                
                // モデルのダウンロードを待機
                conditions?.await()
                _modelDownloading.postValue(false)
                
                // 翻訳を実行
                val result = translator?.translate(text)?.await()
                _translatedText.postValue(result)
                _isLoading.postValue(false)
            } catch (e: Exception) {
                _errorMessage.postValue("翻訳エラー: ${e.localizedMessage}")
                _isLoading.postValue(false)
                _modelDownloading.postValue(false)
            }
        }
    }

    // 利用可能な言語のリストを取得
    fun getAvailableLanguages(): List<Pair<String, String>> {
        return TranslateLanguage.getAllLanguages().map { languageCode ->
            val locale = Locale(languageCode)
            val displayName = locale.displayLanguage
            Pair(languageCode, displayName)
        }.sortedBy { it.second }
    }

    // ViewModel破棄時にリソースを解放
    override fun onCleared() {
        super.onCleared()
        translator?.close()
    }
}

3. レイアウトの作成

次に、翻訳アプリのUI部分を作成します。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <com.google.android.material.card.MaterialCardView
        android:id="@+id/sourceCard"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:cardCornerRadius="8dp"
        app:cardElevation="4dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="16dp">

            <Spinner
                android:id="@+id/sourceLanguageSpinner"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp" />

            <EditText
                android:id="@+id/sourceText"
                android:layout_width="match_parent"
                android:layout_height="120dp"
                android:background="@null"
                android:gravity="top|start"
                android:hint="翻訳したいテキストを入力"
                android:inputType="textMultiLine"
                android:textSize="16sp" />
                
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/swapLanguagesButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="言語を入れ替え"
        app:layout_constraintTop_toBottomOf="@id/sourceCard"
        app:layout_constraintBottom_toTopOf="@id/targetCard"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@android:drawable/ic_popup_sync" />

    <com.google.android.material.card.MaterialCardView
        android:id="@+id/targetCard"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        app:cardCornerRadius="8dp"
        app:cardElevation="4dp"
        app:layout_constraintTop_toBottomOf="@id/swapLanguagesButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="16dp">

            <Spinner
                android:id="@+id/targetLanguageSpinner"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp" />

            <TextView
                android:id="@+id/translatedText"
                android:layout_width="match_parent"
                android:layout_height="120dp"
                android:textSize="16sp" />
                
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <Button
        android:id="@+id/translateButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="翻訳"
        app:layout_constraintTop_toBottomOf="@id/targetCard"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/downloadingModelText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="翻訳モデルをダウンロード中..."
        android:visibility="gone"
        app:layout_constraintTop_toBottomOf="@id/progressBar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

4. MainActivity.kt の実装

最後に、MainActivity.ktでUI操作とViewModelの連携を実装します。

MainActivity.kt

package com.example.mltranslation

import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.mlkit.nl.translate.TranslateLanguage

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: TranslateViewModel
    private lateinit var sourceLanguageSpinner: Spinner
    private lateinit var targetLanguageSpinner: Spinner
    private lateinit var sourceText: EditText
    private lateinit var translatedText: TextView
    private lateinit var translateButton: Button
    private lateinit var swapLanguagesButton: FloatingActionButton
    private lateinit var progressBar: ProgressBar
    private lateinit var downloadingModelText: TextView

    // 言語リスト
    private lateinit var languageOptions: List<Pair<String, String>>
    
    // 選択中のインデックス
    private var sourceLanguageIndex = 0
    private var targetLanguageIndex = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModelの初期化
        viewModel = ViewModelProvider(this)[TranslateViewModel::class.java]

        // UIコンポーネントの初期化
        sourceLanguageSpinner = findViewById(R.id.sourceLanguageSpinner)
        targetLanguageSpinner = findViewById(R.id.targetLanguageSpinner)
        sourceText = findViewById(R.id.sourceText)
        translatedText = findViewById(R.id.translatedText)
        translateButton = findViewById(R.id.translateButton)
        swapLanguagesButton = findViewById(R.id.swapLanguagesButton)
        progressBar = findViewById(R.id.progressBar)
        downloadingModelText = findViewById(R.id.downloadingModelText)

        // 利用可能な言語リストを取得
        languageOptions = viewModel.getAvailableLanguages()

        // Spinnerの設定
        setupLanguageSpinners()

        // ボタンイベントの設定
        translateButton.setOnClickListener {
            performTranslation()
        }

        swapLanguagesButton.setOnClickListener {
            swapLanguages()
        }

        // LiveDataの監視
        observeViewModel()
    }

    private fun setupLanguageSpinners() {
        // 言語名のリストを取得
        val languageNames = languageOptions.map { it.second }.toTypedArray()
        
        // Adapter作成
        val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, languageNames)
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        
        // Spinnerにアダプターをセット
        sourceLanguageSpinner.adapter = adapter
        targetLanguageSpinner.adapter = adapter
        
        // デフォルト値を設定(英語と日本語)
        val englishIndex = languageOptions.indexOfFirst { it.first == TranslateLanguage.ENGLISH }.coerceAtLeast(0)
        val japaneseIndex = languageOptions.indexOfFirst { it.first == TranslateLanguage.JAPANESE }.coerceAtLeast(0)
        
        sourceLanguageSpinner.setSelection(englishIndex)
        targetLanguageSpinner.setSelection(japaneseIndex)
        
        // 選択リスナーを設定
        sourceLanguageSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                sourceLanguageIndex = position
            }
            
            override fun onNothingSelected(parent: AdapterView<*>?) {}
        }
        
        targetLanguageSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
                targetLanguageIndex = position
            }
            
            override fun onNothingSelected(parent: AdapterView<*>?) {}
        }
    }

    private fun performTranslation() {
        val text = sourceText.text.toString()
        if (text.isBlank()) {
            Snackbar.make(sourceText, "翻訳するテキストを入力してください", Snackbar.LENGTH_SHORT).show()
            return
        }

        val sourceLanguage = languageOptions[sourceLanguageIndex].first
        val targetLanguage = languageOptions[targetLanguageIndex].first
        
        viewModel.translate(text, sourceLanguage, targetLanguage)
    }

    private fun swapLanguages() {
        // 選択中の言語を入れ替え
        val tempIndex = sourceLanguageIndex
        sourceLanguageIndex = targetLanguageIndex
        targetLanguageIndex = tempIndex
        
        // Spinnerの選択を更新
        sourceLanguageSpinner.setSelection(sourceLanguageIndex)
        targetLanguageSpinner.setSelection(targetLanguageIndex)
        
        // テキストも入れ替え
        val tempText = sourceText.text.toString()
        sourceText.setText(translatedText.text)
        translatedText.text = tempText
    }

    private fun observeViewModel() {
        // 翻訳結果の監視
        viewModel.translatedText.observe(this) { result ->
            translatedText.text = result
        }
        
        // ローディング状態の監視
        viewModel.isLoading.observe(this) { isLoading ->
            progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
            translateButton.isEnabled = !isLoading
        }
        
        // モデルダウンロード状態の監視
        viewModel.modelDownloading.observe(this) { isDownloading ->
            downloadingModelText.visibility = if (isDownloading) View.VISIBLE else View.GONE
        }
        
        // エラーメッセージの監視
        viewModel.errorMessage.observe(this) { errorMsg ->
            if (errorMsg.isNotEmpty()) {
                Snackbar.make(translateButton, errorMsg, Snackbar.LENGTH_LONG).show()
            }
        }
    }
}

5. 実装のポイント解説

ML Kitのオンデバイス翻訳の主要な特徴

モデルのダウンロード処理

ML Kitは翻訳モデルを必要に応じてダウンロードします。各言語のモデルサイズは約5MB程度で、一度ダウンロードしたモデルは端末に保存されます。

// モデルのダウンロード
val conditions = translator?.downloadModelIfNeeded()
conditions?.await() // 非同期処理でダウンロード完了を待機

メモリリソースの管理

Translator オブジェクトは使用後に必ずクローズする必要があります。これにより、メモリリークを防ぎます。

// ViewModel破棄時にリソースを解放
override fun onCleared() {
    super.onCleared()
    translator?.close()
}

対応言語

ML Kitのオンデバイス翻訳は59以上の言語をサポートしています。言語リストは TranslateLanguage.getAllLanguages() で取得できます。

6. アプリの動作と画面サンプル

en
Hello, how are you today?
ja
こんにちは、今日の調子はどうですか?