第3回 顔認識デバイス Android編 〜 LiveDataとDataBindingを実装する 〜
公開日:2021.08.19 更新日:2021.09.08
前回に引き続き、MVVMの実装をしていきます。
ViewModelの実装では、UIの状態管理やロジックの分離を実施しました。
今回はLiveDataを使用して、Bluetoothデバイスとの接続状態の変化を監視しつつ適切なタイミングで自動更新する実装と、
DataBindingを使用してレイアウトファイルからその状態変化をUIへ更新できるように実装します。
前回:Android編 JetPackを使ってViewModelを実装する
LiveData、DataBinding を使用するために必要なライブラリを追加します。
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
}
// 追加
dataBinding {
enabled = true
}
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"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // 追加
// coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
}
前回作成した、ConnectViewModelへ下記状態を表すLiveDataを実装していきます。
- Bluetooth 接続状態(sppConnectStatus)
- 接続ボタンの Enabled 状態(connectButtonEnabled)
- 切断ボタンの Enabled 状態(disconnectButtonEnabled)
また、Activity で継承していたISppClient(SPP通信のイベントリスナー)はViewModelに継承させます。
SppConnectStatusは通信状態を管理するために定義しています。
ConnectionViewModel.kt
class ConnectViewModel(val app: Application) : AndroidViewModel(app), ISppClient {
...
// liveData
private val mSppConnectStatus = MutableLiveData<SppConnectStatus>()
private val mConnectButtonEnabled = MutableLiveData<Boolean>()
private val mDisconnectButtonEnabled = MutableLiveData<Boolean>()
val sppConnectStatus: LiveData<SppConnectStatus> get() = mSppConnectStatus
val connectButtonEnabled: LiveData<Boolean> get() = mConnectButtonEnabled
val disconnectButtonEnabled: LiveData<Boolean> get() = mDisconnectButtonEnabled
/**
* Bluetoothデバイスとの接続状態
*/
enum class SppConnectStatus {
CONNECTED {
override fun toast(context: Context) {
Toast.makeText(context, "接続成功", Toast.LENGTH_SHORT).show()
}
},
CONNECT_ERROR {
override fun toast(context: Context) {
Toast.makeText(context, "接続失敗", Toast.LENGTH_SHORT).show()
}
},
DISCONNECTED {
override fun toast(context: Context) {
Toast.makeText(context, "接続切断", Toast.LENGTH_SHORT).show()
}
},
NOT_CONNECTION {
override fun toast(context: Context) {}
};
abstract fun toast(context: Context)
}
init {
deviceAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item)
mSppConnectStatus.value = SppConnectStatus.NOT_CONNECTION
mConnectButtonEnabled.value = false
mDisconnectButtonEnabled.value = false
// SPP通信のイベントリスナー
MyApplication.instance.sppClient.addListener(this)
}
...
fun updateButtonState() {
if (MyApplication.instance.sppClient.isConnected()) {
mConnectButtonEnabled.postValue(false)
mDisconnectButtonEnabled.postValue(true)
} else {
mConnectButtonEnabled.postValue(btDevice != null)
mDisconnectButtonEnabled.postValue(false)
}
}
// ISppClient method
override fun onConnected(result: Boolean) {
viewModelScope.launch {
if (result) {
// 接続成功
mSppConnectStatus.postValue(SppConnectStatus.CONNECTED)
} else {
// 接続失敗
mSppConnectStatus.postValue(SppConnectStatus.CONNECT_ERROR)
}
updateButtonState()
}
}
// ISppClient method
override fun onDisconnected() {
viewModelScope.launch {
// 接続切断
mSppConnectStatus.postValue(SppConnectStatus.DISCONNECTED)
updateButtonState()
// 300msec の遅延を挿入後、ステータスを戻す
delay(300)
mSppConnectStatus.postValue(SppConnectStatus.NOT_CONNECTION)
}
}
}
2.1 初期化
まずは、LiveDataオブジェクトの宣言と初期化です。
外部から参照するときは、イミュータブル型にしていますが、このあたりはお好みです。
値の変更は、valueに代入するか、postValue()を使用することになります。
どちらも意味合いは同じですが、valueはメインスレッドから実行する必要があり、
postValue()はバックグラウンドから実行してもメインスレッドへ最新値がディスパッチされるようになっています。
2.2 ボタンの状態管理
既存コードではボタンの Enabled を直接操作していましたが、後述するDataBindingで UI操作をActivityから分離するようにします。
postValue()を使用してボタンの最新状態をViewへ反映します。
fun updateButtonState() {
if (MyApplication.instance.sppClient.isConnected()) {
mConnectButtonEnabled.postValue(false)
mDisconnectButtonEnabled.postValue(true)
} else {
mConnectButtonEnabled.postValue(btDevice != null)
mDisconnectButtonEnabled.postValue(false)
}
}
2.3 Bluetooth通信の状態管理
接続成功、接続失敗、接続切断の通信状態を監視し、状態更新に合わせてトーストを表示できるようにします。
// ISppClient method
override fun onConnected(result: Boolean) {
viewModelScope.launch {
if (result) {
// 接続成功
mSppConnectStatus.postValue(SppConnectStatus.CONNECTED)
} else {
// 接続失敗
mSppConnectStatus.postValue(SppConnectStatus.CONNECT_ERROR)
}
updateButtonState()
}
}
// ISppClient method
override fun onDisconnected() {
viewModelScope.launch {
// 接続切断
mSppConnectStatus.postValue(SppConnectStatus.DISCONNECTED)
updateButtonState()
// 300msec の遅延を挿入後、ステータスを戻す
delay(300)
mSppConnectStatus.postValue(SppConnectStatus.NOT_CONNECTION)
}
}
LiveDataの値変更はobserve()で監視します。
ConnectActivity.kt
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_connect)
...
// liveData observer
viewModel.sppConnectStatus.observe(this, Observer { connectStatus ->
connectStatus.toast(this)
})
}
DataBindingはレイアウトファイルから指定したクラスの変数やメソッドを参照することが出来ます。これにより、ActivityやFragmentからUI操作を分離できます。
今回は、ボタンの 有効/無効 と クリックイベント を紐付けていきます。
3.1 レイアウトファイルの修正
レイアウトファイルからViewModelを参照するにはルート階層を<layout> … </layout>で囲む必要があります。
ルート階層を右クリックして、[Show Context Actions] → [Convert to data binding layout] をクリックすると自動変換してくれます。便利です!
activity_connect.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="jp.co.avancesys.sample.demo001.view.screen.connect.ConnectViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/app_background"
tools:context=".view.screen.connect.ConnectActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Spinner
android:id="@+id/spinnerDevices"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="8dp"
android:layout_weight="1"
android:backgroundTint="#FFFFFF" />
<Button
android:id="@+id/buttonConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="connect"
android:enabled="@{viewModel.connectButtonEnabled}"
android:onClick="@{(v) -> viewModel.connectBtDevice()}"/>
<Button
android:id="@+id/buttonDisconnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="disconnect"
android:enabled="@{viewModel.disconnectButtonEnabled}"
android:onClick="@{(v) -> viewModel.disconnectBtDevice()}"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<data> … </data>で囲まれた箇所が参照するViewModelとなり、呼び出しは@{}で囲みます。
ここでは、先程LiveDataで定義した接続ボタンと切断ボタンの状態変数をandroid:enabled=へ、ボタンをタップしたときの内部処理をandroid:onClick=へバインドしています。
3.2 Activityの修正
それでは、仕上げにActivityを修正します。
まず、DataBindingUtil.setContentView<T>()でDataBindingのオブジェクトを取得します。Tに指定しているActivityConnectBindingは自動生成です。
次に、binding.viewModelとbinding.lifecycleOwnerを紐付けたら完了です!
どうでしょう。Activity内の処理がほとんどなくなりスッキリした気がします。
ConnectActivity.kt
class ConnectActivity : BaseActivity() {
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)
val binding = DataBindingUtil.setContentView<ActivityConnectBinding>(this, R.layout.activity_connect)
binding.viewModel = viewModel
binding.lifecycleOwner = this
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// スピナー
binding.spinnerDevices.adapter = viewModel.deviceAdapter
binding.spinnerDevices.onItemSelectedListener = viewModel.spinnerOnItemSelectedListener
// liveData observer
viewModel.sppConnectStatus.observe(this, Observer { connectStatus ->
connectStatus.toast(this)
})
}
override fun onResume() {
super.onResume()
// スピナーの内容を更新
viewModel.updateSpinner { selectIndex ->
spinnerDevices.setSelection(selectIndex, false)
}
viewModel.updateButtonState()
}
}
Android JetPackを使用して簡単なMVVM設計に修正してみました。
UIとロジックの責任分担ができ、ソースコードの保守性も上がりました。
さらには、これまで開発者が気にしなければならなかったライフサイクル絡みのUI実装がJetPackライブラリで吸収され、効率よくアプリ開発が可能になったと思います。
この他にもNavigation、Room等の開発スピードを上げるライブラリを弊社では積極的に取り入れています。
Bluetooth SPP通信の実装を紹介していきます。
関連記事
-
第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