【Kotlin】【Flow】使用zip和combine监听数据变化

1、前言

最近在项目中遇到了需要同时监听四个数据变化的需求。注册两个Sensor采集a+g的数据,并获取时间戳和xyz的数据,同时完成对应的操作,这个需求需要同时监听四个数据的变化,之前使用observe()去监听一个数据的变化,所以我想到了使用Flow来监听数据变化。

2、问题描述

在ViewModule中注册两个Sensor用于分别采集acc和gyro的数据,并创建一个工具类来监听Sensor数据的变化、初始化、注册和解注册。

class MotionGameSensorUtil(private val handleData: (Long, ArrayList<Float>) -> Unit) {

    private val TAG = "MotionGameSensorUtil"
    private var lastData: ArrayList<Float> = arrayListOf()
    private var timestamp: Long = 0L
    private var hasData: Boolean = false
    private val sensorListener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            val time = event!!.timestamp
            timestamp = TimeUnit.NANOSECONDS.toMillis(time) * 1000L
            val x = event.values[0]
            val y = event.values[1]
            val z = event.values[2]
            lastData.apply {
                clear()
                add(x)
                add(y)
                add(z)
            }
            hasData = true
            postData()
        }

        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        }
    }

    fun init(lifecycle: Lifecycle, sensorManager: SensorManager, sensor: Sensor) {
        lifecycle.addObserver(object : DefaultLifecycleObserver {

            override fun onStart(owner: LifecycleOwner) {
                super.onStart(owner)
                register(sensorManager, sensor)
            }

            override fun onStop(owner: LifecycleOwner) {
                super.onStop(owner)
                unregister(sensorManager)
                hasData = false
            }

            override fun onDestroy(owner: LifecycleOwner) {
                super.onDestroy(owner)
                lifecycle.removeObserver(this)
            }
        })
    }

    fun register(sensorManager: SensorManager, sensor: Sensor) {
        sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_GAME)
    }

    fun unregister(sensorManager: SensorManager) {
        sensorManager.unregisterListener(sensorListener)
    }

    fun postData() {
        if (hasData) {
            handleData(timestamp, lastData)
        }
    }
}

在ViewModule中创建两个工具类的实力,分别用于获取acc和gyro数据。

	val gyroscopeSensorUtil: MotionGameSensorUtil by lazy {
	   MotionGameSensorUtil(handleData = { time, data ->
	        runBlocking {
	        	//对于time、data数据操作
	        }
	    })
	}
	
	val accSensorUtil: MotionGameSensorUtil by lazy {
	    MotionGameSensorUtil(handleData = { time, data ->
	        runBlocking {
	            //对于time、data数据操作
	        }
	    })
	}

Flow数据定义如下:

	val gyroscopeData by lazy { MutableStateFlow<ArrayList<Float>>(arrayListOf()) }
	val accData by lazy { MutableStateFlow<ArrayList<Float>>(arrayListOf()) }
	val gyroscopeTime by lazy { MutableStateFlow(0L) }
	val accTime by lazy { MutableStateFlow(0L) }

起初我的想法是同时监听四个数据,于是我想到了combine和zip两个方式来监听Flow数据流变化,于是有了以下逻辑去监听数据。

	@Parcelize
	data class SensorData(
	    var accTimestamp: Long = 0L,
	    var gyroTimestamp: Long = 0L,
	    var accData: ArrayList<Float> = arrayListOf(),
	    var gyroData: ArrayList<Float> = arrayListOf()
	) : Parcelable
	lifecycleScope.launch {
	    processViewModule.let {
	        combine(
	            it.accTime,
	            it.accData,
	            it.gyroscopeTime,
	            it.gyroscopeData
	        ) { accTime, accData, gyroscopeTime, gyroscopeData ->
	            it.sensorData.accTimestamp = accTime
	            it.sensorData.accData = accData
	            it.sensorData.gyroTimestamp = gyroscopeTime
	            it.sensorData.gyroData = gyroscopeData	
	            it.sensorData
	        }.collect{ sensorData->
	            //对sensorData的操作
	        }
	    }
	}

这时候我发现回调的数据有很多老数据,尝试用zip来解决问题。这里先说说zip和combine的区别。

zip:该操作符将两个或多个Flow的对应元素组合在一起。它等待每每个Flow都发出一个新元素,然后将这些元素组合成一个新的元素(通常是一个包含所有源Flow当前元素的元组),再发射这个新元素。如果某个Flow发射元素的速度比其他Flow慢,那么zip会等待,直到每个Flow都有新的元素可供组合。

combineLatest:这个操作符也会组合多个Flow的元素,但它并不要求每个Flow同时发射新元素。每当任何一个源Flow发射一个新元素时,combinelatest都会立即使用每个Flow的最新元素(如果可用的话)来组合并发射一个新元素。这意味着,如果某个Flow更新得更频繁,那么组合的结果也会更频繁地更新。

并编写了如下代码:

	lifecycleScope.launch {
	   processViewModule.let {
	        it.accTime.zip(it.accData) { accTime, accData ->
	            it.sensorData.accTimestamp = accTime
	            it.sensorData.accData = accData
	            it.sensorData
	        }.zip(it.gyroscopeTime) { sensorData, gyroscopeTime ->
	            it.sensorData.gyroTimestamp = gyroscopeTime
	            sensorData
	        }.zip(it.gyroscopeData) { sensorData, gyroscopeData ->
	            it.sensorData.gyroData = gyroscopeData
	            sensorData
	        }.collect { sensorData ->
	            //对sensorData的操作
	        }
	    }
	}

根据log我发现collect只执行了一次。

3、问题解决

在使用四个zip操作符进行监听数据时,我发现我只用两个传感器,理论上只需要两个Flow就可以完成功能,于是有了如下代码。

	val gyroEvent by lazy {
        MutableStateFlow<Pair<Long, ArrayList<Float>>>(
            Pair(0L, arrayListOf())
        )
    }
    val accEvent by lazy {
        MutableStateFlow<Pair<Long, ArrayList<Float>>>(
            Pair(0L, arrayListOf())
        )
    }
	lifecycleScope.launch {
	    processViewModule.let {
	        it.accEvent.zip(it.gyroEvent) { acc, gyro ->
	            it.sensorData.accTimestamp = acc.first
	            it.sensorData.accData = acc.second
	            it.sensorData.gyroTimestamp = gyro.first
	            it.sensorData.gyroData = gyro.second
	            it.sensorData
	        }.collect { sensorData ->
	            it.sendAccAndGyroData(sensorData)
	        }
	    }
	}

至此,功能完成。

4、结语

此功能让我深入了解了Kotlin响应式编程框架以及combine与zip操作符的使用,但是为什么在监听四个数据的时候只会回调一次,这个问题还是没有想明白,有无大佬评论区解答一下,万分感谢。

### 使用Kotlin Flow进行响应式编程 #### 创建发射Flow `Flow` 是一种冷流,意味着它不会自动执行直到被收集。创建 `Flow` 的方式有很多,最常见的是使用 `flow { }` 构建器。 ```kotlin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow fun simple(): Flow<Int> = flow { for (i in 1..3) { emit(i) } } ``` 此代码定义了一个简单的整数序列生成器[^2]。 #### 收集Flow中的数据 要消费由 `Flow` 发射的数据项,则需调用 `collect()` 函数并提供一个挂起 lambda 表达式的参数,在每次接收到新值时都会被执行。 ```kotlin import kotlinx.coroutines.runBlocking runBlocking { simple().collect { value -> println(value) } } ``` 这段代码会打印出从简单流中发出的三个数值。 #### 处理异常情况 当遇到错误时,可以通过捕获异常来优雅地处理它们;也可以利用内置的操作符如 `catch` 或者 `retryWhen` 来增强健壮性。 ```kotlin val numbers = flowOf(1, 2, 3).map { it / 0 }.onEach { println(it) } numbers.catch { e -> if (e is ArithmeticException) emit(-1) else throw e }.collect { println(it) } ``` 这里展示了如果除零发生算术异常则替换为 `-1` 输出[^4]。 #### 组合多个Flows 有时可能希望将几个不同的 `Flow` 结合起来形成一个新的单一输出源。这可通过多种组合运算符完成,比如 `combine()`, `zip()`, `flatMapConcat()/flatMapMerge()/transformLatest()` 等等。 ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* val nums = flowOf(1, 2, 3).delayEach(100L) val strs = flowOf("one", "two", "three").delayEach(200L) nums.zip(strs) { a, b -> "$a -> $b" } .collect { println(it) } ``` 上述例子说明了怎样同步来自不同源头的信息流,并按顺序配对元素一起传递给下游消费者。 #### 集成到ViewModel与LiveData 为了使基于MVVM架构的应用能够监听UI层的变化,通常会在 ViewModel 中暴露 LiveData 类型属性供 Activity/Fragment 订阅更新状态变化。此时可以借助于 `StateFlow` 或者 `SharedFlow` 将内部私有的 `MutableStateFlow/MutableSharedFlow` 转换成只读形式对外发布出去。 ```kotlin class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow("") val uiState: StateFlow<String> get() = _uiState.asStateFlow() fun updateUi(text: String){ viewModelScope.launch{ _uiState.emit(text) } } } ``` 在这个片段里,每当 `_uiState` 变化时就会触发所有观察者的回调方法去刷新界面显示的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JD_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值