【完全無料】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 件のコメント :
コメントを投稿