【完全無料】ML Kitを使ってオフライン翻訳アプリを実際に作ってみた・Androidアプリ開発
AndroidアプリでGoogleのML Kitライブラリを使ったオンデバイス翻訳機能の実装方法について詳しく解説します。オンデバイス翻訳はインターネット接続なしで動作するため、オフライン環境でも使えるアプリを開発したい方に最適です。それでは早速、実装方法を見ていきましょう!
ML Kitとは?
ML Kitは、Googleが提供する機械学習のモバイルSDKです。画像認識、顔検出、テキスト認識(OCR)、バーコードスキャン、テキスト翻訳など、様々な機能を簡単に実装できます。今回解説するのは「オンデバイス翻訳」機能で、これを使えば端末内で翻訳処理を完結させることができます。
ML Kitのメリット:
- インターネット接続なしで翻訳が可能
- プライバシーの保護(データがサーバーに送信されない)
- レイテンシの削減(サーバーとの通信が不要)
- 通信コストの削減
実装の流れ
- プロジェクトの設定
- 必要な依存関係の追加
- 翻訳モデルのダウンロード処理の実装
- 翻訳機能の実装
- UIの作成
- 実際に動かしてみる
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()
で取得できます。
コメント
0 件のコメント :
コメントを投稿