第2回 顔認識デバイス Android編 〜 JetPackを使ってViewModelを実装する 〜
公開日:2021.07.19 更新日:2021.07.19
最近のAndroidアプリはJetPackライブラリが浸透してきたこともあり、MVVMを使うことは当たり前のようになっていますよね。
そこで、今回は、アプリのBluetooth接続画面をViewModel + Coroutine + Lifecycleを使って、MVVM設計にしてみようと思います!

既存ソースコード
下記はBluetooth通信のイベントハンドラーやボタン状態変化も含めた既存コードです。ここから処理を分離していきます。
class ConnectActivity : BaseActivity(), ISppClient {
private lateinit var deviceAdapter: ArrayAdapter<String>
private var btDeviceSet: Set<BluetoothDevice>? = null
private var btDevice: BluetoothDevice? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connect)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
MyApplication.instance.sppClient.addListener(this)
deviceAdapter = ArrayAdapter(this,
R.layout.spinner_item
)
deviceAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item)
spinnerDevices.adapter = deviceAdapter
buttonConnect.setOnClickListener {
MyApplication.instance.sppClient.connect(btDevice)
}
buttonDisconnect.setOnClickListener {
MyApplication.instance.sppClient.disconnect()
}
}
override fun onResume() {
super.onResume()
// 接続中の場合はそのデバイスを初期選択項目にする
if (MyApplication.instance.sppClient.isConnected()) {
btDevice = MyApplication.instance.sppClient.mBTDevice
}
deviceAdapter.clear()
val btAdapter = BluetoothAdapter.getDefaultAdapter()
btDeviceSet = btAdapter.bondedDevices
if (btDeviceSet != null) {
var selectIndex = 0
var index = 0
for (device in btDeviceSet!!) {
deviceAdapter.add(device.name)
if (device.name.equals(btDevice?.name)) {
selectIndex = index
}
index++
}
if (btDevice == null && btDeviceSet!!.size > 0) {
btDevice = btDeviceSet!!.first()
selectIndex = 0
}
if (btDevice != null && spinnerDevices.adapter.count > selectIndex) {
spinnerDevices.setSelection(selectIndex, false)
}
}
spinnerDevices.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
// デバイス変更時に切断する
MyApplication.instance.sppClient.disconnect()
if (btDeviceSet != null) {
val selectedName = deviceAdapter.getItem(p2)
for (device in btDeviceSet!!) {
if (device.name.equals(selectedName)) {
btDevice = device
break
}
}
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
updateButtonState()
}
fun updateButtonState() {
if (MyApplication.instance.sppClient.isConnected()) {
buttonConnect.isEnabled = false
buttonDisconnect.isEnabled = true
} else {
buttonDisconnect.isEnabled = false
buttonConnect.isEnabled = (btDevice != null)
}
}
override fun onConnected(result: Boolean) {
runOnUiThread {
if (isRunning) {
if (result) {
Toast.makeText(this, "接続成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "接続失敗", Toast.LENGTH_SHORT).show()
}
updateButtonState()
}
}
}
override fun onDisconnected() {
runOnUiThread {
if (isRunning) {
Toast.makeText(this, "接続切断", Toast.LENGTH_SHORT).show()
updateButtonState()
}
}
}
}
1. 準備
Coroutine, Lifecycle を使用するために必要なライブラリを追加します。
buid.gradle(app)
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // 追加
android {
...
// 追加
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// 追加
kotlinOptions {
jvmTarget = '1.8'
}
dependencies {
...
// lifecycle 追加
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// coroutine 追加
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
}
2. ViewModelの実装
Bluetooth接続画面ではペアリングしたデバイスのスピナー表示と接続、切断ボタンのイベントが存在するため、これらの処理をViewModelへ記述していきます。
ConnectViewModel.kt
class ConnectViewModel(val app: Application) : AndroidViewModel(app) {
private var mBtDeviceSet: Set<BluetoothDevice>? = null // ペアリング済みのデバイス一覧
private var mBtDevice: BluetoothDevice? = null // 接続中のデバイス
val deviceAdapter: ArrayAdapter<String> = ArrayAdapter(app, R.layout.spinner_item)
val btDeviceSet get() = mBtDeviceSet
val btDevice get() = mBtDevice
init {
deviceAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item)
}
fun connectBtDevice() = MyApplication.instance.sppClient.connect(mBtDevice)
fun disconnectBtDevice() = MyApplication.instance.sppClient.disconnect()
val spinnerOnItemSelectedListener = object : SpinnerOnItemSelectedListener {
override var isSelectedFirst: Boolean = false
override fun onItemSelectedSpinner(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
// デバイス変更時に切断する
MyApplication.instance.sppClient.disconnect()
if (mBtDeviceSet != null) {
val selectedName = deviceAdapter.getItem(position)
for (device in mBtDeviceSet!!) {
if (device.name == selectedName) {
mBtDevice = device
break
}
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
fun updateSpinner(callback: (selectIndex: Int) -> Unit) =
viewModelScope.launch {
var selectIndex = 0
// 接続中の場合はそのデバイスを初期選択項目にする
if (MyApplication.instance.sppClient.isConnected()) {
mBtDevice = MyApplication.instance.sppClient.mBTDevice
}
deviceAdapter.clear()
val btAdapter = BluetoothAdapter.getDefaultAdapter()
// ペアリング済みのデバイスを取得
mBtDeviceSet = btAdapter.bondedDevices
mBtDeviceSet?.let { btSet ->
for ((index, device) in btSet.withIndex()) {
deviceAdapter.add(device.name)
if (device.name == mBtDevice?.name) {
selectIndex = index
}
}
if (btSet.isNotEmpty()) {
mBtDevice = btSet.first()
selectIndex = 0
}
}
callback(selectIndex)
}
}
解説します。
ViewModelを作成する際は、ViewModelもしくはAndroidViewModelを継承します。
今回はContextをViewModelで使用するため、AndroidViewModelを継承しています。Contextを使用しない場合はViewModelで良いです。
Activityでこれまで管理していた以下の要素は全てViewModelへ移行しています。
- スピナーアイテム(deviceAdapter)
- Bluetoothデバイスオブジェクト(btDeviceSet, btDevice)
- 接続ボタンの内部処理(connectBtDevice())
- 切断ボタンの内部処理(disconnectBtDevice())
- スピナーアイテムをタップしたときの内部処理(spinnerOnItemSelectedListener)
これはViewに関する処理はActivity、Viewの状態管理をViewModel、と役割を分けているためです。
updateSpinner()は端末設定からペアリングデバイスを追加したあと、アプリへ遷移したことを想定してonResume()から呼び出しています。
ここで下記viewModelScopeが登場しますが、これはViewModel用に設計されたコルーチンです。ViewModelが破棄されるとき、このコルーチン内で実行されている処理は全て自動的にキャンセルされる事になっているため、AsyncTask等で管理する必要が無くなるということになりますね。
非同期処理からUI更新をするようなときは積極的に使っていきます。
viewModelScope.launch {
...
}
少し話がそれてしまうのですが、スピナーへAdapterView.OnItemSelectedListenerをアタッチした際にonItemSelected()が呼ばれてしまうので、これを回避するための実装をSpinnerOnItemSelectedListenerにしています。
3. Activityの実装
Activityの実装は簡単です!
ViewModelを生成し、スピナーやボタンと関連付けるだけです。
class ConnectActivity : BaseActivity(), ISppClient {
private val viewModel: ConnectViewModel by lazy {
val factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application)
ViewModelProvider(this, factory).get(ConnectViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connect)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// SPP通信のイベントリスナー
MyApplication.instance.sppClient.addListener(this)
// スピナー
spinnerDevices.adapter = viewModel.deviceAdapter
spinnerDevices.onItemSelectedListener = viewModel.spinnerOnItemSelectedListener
// 接続ボタン
buttonConnect.setOnClickListener {
viewModel.connectBtDevice()
}
// 切断ボタン
buttonDisconnect.setOnClickListener {
viewModel.disconnectBtDevice()
}
}
override fun onResume() {
super.onResume()
// スピナーの内容を更新
viewModel.updateSpinner { selectIndex ->
spinnerDevices.setSelection(selectIndex, false)
}
updateButtonState()
}
...
}
参考
- https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
- https://developer.android.com/kotlin/coroutines
次回
まだ少し不十分なので、ボタンの表示状態を ViewModel + LiveData + DataBinding を使用することでより、MVVM設計に近づける実装していきます。
関連記事
-
第1回 ラズパイを使用したBLE通信 ~ ディスプレイ、キーボード、マウスを接続しないで設定 前編 ~
こんにちは、GTです。よろしくお願いします。 最近業務でラズパイのBluetooth機能を使...
公開日:2021.12.24 更新日:2021.12.24
tag : Bluetooth Raspberry Pi
-
-
第1回 Visual C++で作成したDLL内のクラスをC#で利用する方法
こんにちは、ILCです。 Visual C++ (以下 VC++)で作成されたDynamic...
公開日:2024.01.19 更新日:2024.01.19
tag : Windows
-
-
こんにちは、TMIHです。 今回から組み込み系のソフトウェア設計標準規格である、MISRA-...
公開日:2022.02.25 更新日:2022.02.25
-
第3回 ラズパイを使用したBLE通信 ~ A/D変換・D/A変換を用いた入出力編 ~
こんにちは、GTです。よろしくお願いします。 第3回の今回は、ラズパイの入出力についてご紹介...
公開日:2023.02.24 更新日:2023.02.24
tag : Bluetooth BLE Raspberry Pi