문제 상황
MVVM + 클린 아키텍쳐 형태로 블루투스 기능을 구현하던 도중 StateFlow에 관한 문제가 발생했다.
블루투스 장치가 새로 검색 되어도 StateFlow에서 값이 발행되지 않아 화면에 장치 리스트가 표시 되지 않았다.
처음 한 번만 발행되고 그 이후엔 발행이 안되었는데, 그 이유를 적어보고자 한다.
일단 State 패턴을 이용하여 UI의 State를 관리하는 방식을 사용하고 있는데, Bluetooth 검색 화면의 State 클래스가 아래와 같이 작성 되어 있었다.
sealed class BluetoothSearchFragmentState {
object Init : BluetoothSearchFragmentState()
/.../
data class IsScanning(
val scanResults: Set<BluetoothDevice>
) : BluetoothSearchFragmentState()
/.../
}
블루투스 검색 결과를 방출하는 코드는 아래와 같았는데, 기본적으로 동일한 값은 방출하지 않는 StateFlow 특성상 아래의 코드로는 데이터 방출이 되지 않았다.
새로운 장치가 발견되면 deviceList에 삽입하게 되는데, deviceList는 ViewModel에 선언된 변수이다.
StartScan
- 블루투스 스캔을 시작할 때 _state.vlaue에 IsScanning이 처음 대입되면서 데이터가 발행된다.
- 이때, IsScanning은 빈 리스트인 deviceList를 인스턴스로 가지게 된다.
/**
* BLE 스캔 시작 메소드
*/
fun startScan() {
Handler().postDelayed({
stopScan()
}, SCAN_PERIOD)
isScanning = true
deviceList.clear() // 스캔 시작하면 기존 리스트 초기화
// 여기서 첫 발행 됨
_state.value = BluetoothSearchFragmentState.IsScanning(UUID.randomUUID(), deviceList)
btScanner.startScan(scanCallback)
}
onScanResult
- 이후 스캔에 대한 발행은 여기서 진행 된다.
- 코드를 보면 deviceList에 포함되지 않은 새로운 장치들만 deviceList에 넣게 되어 있는데, 이 부분 때문에 발행이 되지 않았다.
- _state.value의 값인 IsScanning이 갖고 있던 리스트도 deviceList와 동일한 객체이기 때문에 deviceList에 값을 추가해버리면 결국 동일한 값을 가지게 된다. ( 그냥 deviceList 하나의 객체에 값만 추가하고 있는 것 )
/**
* BLE 장치가 검색되면 호출
*/
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
// 기기의 이름이 null이 아니고, 추가되지 않은 기기인 경우에만 리스트에 추가
result?.let {
if (!deviceList.contains(result.device) && result.device.name != null) {
deviceList.add(result.device)
}
// notify
_state.value = BluetoothSearchFragmentState.IsScanning(
scanResults = deviceList
)
}
}
예시를 들자면 아래 코드와 같은 상황인 것
data class IsScanning(
val deviceList:MutableList<String>
)
fun main() {
// 장치를 담을 리스트
val deviceList = mutableListOf<String>()
var s1 = IsScanning(deviceList) // 초기 상태
var s2 = IsScanning(deviceList) // 이후 상태
println("${s1.deviceList}")
println("${s2.deviceList}")
println(s1 == s2) // 동일
deviceList.add("새로운 장치") // 새로운 장치 발견
s2 = IsScanning(deviceList) // s2에 추가된 list를 반영
println("${s1.deviceList}")
println("${s2.deviceList}")
println(s1 == s2) // 결국 s1의 list에도 값이 반영 되었기 때문에 두 클래스가 동일한 값을 가짐 -> 발행 되지 않음
}
[]
[]
true
[새로운 장치]
[새로운 장치]
true
- StateFlow가 참조 비교를 하는 줄 알고, deviceList를 새로운 리스트에 담아보기도 했는데 결과는 동일했다.
- 즉, StateFlow는 값을 비교하고 있다는 것이다.
해결 방안
- IsScanning 클래스에 구분자 역할을 할 UUID 변수를 추가해준다.
sealed class BluetoothSearchFragmentState {
object Init : BluetoothSearchFragmentState()
/.../
data class IsScanning(
private val id: UUID,
val scanResults: Set<BluetoothDevice>
) : BluetoothSearchFragmentState()
/.../
}
- 이제 아래와 같이 IsScanning 클래스를 생성할 때 랜덤 UUID를 부여해주면 다른 값으로 인식하여 StateFlow가 값을 발행하게 된다.
/**
* BLE 장치가 검색되면 호출
*/
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
Log.d(TAG, "ScanResult = ${result?.device?.name} 찾음")
// 기기의 이름이 null이 아니고, 추가되지 않은 기기인 경우에만 리스트에 추가
result?.let {
if (!deviceList.contains(result.device) && result.device.name != null) {
deviceList.add(result.device)
}
// notify
_state.value = BluetoothSearchFragmentState.IsScanning(
id = UUID.randomUUID(),
scanResults = deviceList
)
}
}
실행 결과
- 이제 아래 사진처럼 결과가 잘 나오는 것을 볼 수 있다.