有时,ConflatedBroadcastChannel 会在没有任何操作的情况下触发最近的值
Sometimes, ConflatedBroadcastChannel fires recent value without any action
在Google关于advanced-coroutines-codelab示例的官方代码实验室中,他们使用了ConflatedBroadcastChannel
到watch a variable/object change。
我在我的一个业余项目中使用了相同的技术,当恢复监听 activity 时,有时 ConflatedBroadcastChannel
会触发它的最新值,导致执行 flatMapLatest
body 没有任何变化。
我认为这是在系统收集垃圾时发生的,因为我可以通过从另一个 activity 调用 System.gc()
来重现此问题。
这是代码
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
这是完整的源代码:
CoroutinesFlowTest.zip
- 为什么会这样?
- 我错过了什么?
原因很简单,ViewModels
可以在Activities
的生命周期之外持久化。通过移动到另一个 activity 并进行垃圾收集,您将处理原始 MainActivity
但保留原始 MainViewModel
.
然后当你从 DetailActivity
return 它重新创建 MainActivity
但重用视图模型,它仍然具有具有最后已知值的广播频道,当 count.observe
被调用。
如果您添加日志记录以观察 activity 的 onCreate
和 onDestroy
方法,您应该会看到生命周期得到推进,而视图模型应该只创建一次。
除了:
这可能不是你的情况,但你可以尝试在接收端使用 BroadcastChannel(1).asFlow().conflate
,但在我的情况下,它导致了一个错误,有时接收端的代码没有被触发(我认为是因为 conflate 在单独的协程或其他东西中工作)。
或者您可以使用自定义版本的无状态 ConflatedBroadcastChannel(已找到 here)。
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
引用自official response,(简单明了的解决方案)
The problem here is that you are trying to use
ConflatedBroadcastChannel
for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData
is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel
for events.
To fix it, you can replace ConflatedBroadcastChannel
with
BroadcastChannel<IncrementRequest>(1)
(non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
在协程 1.4.2 和 Kotlin 1.4.31 上
不使用实时数据
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
使用流和协程
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this@MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
不使用 BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
在Google关于advanced-coroutines-codelab示例的官方代码实验室中,他们使用了ConflatedBroadcastChannel
到watch a variable/object change。
我在我的一个业余项目中使用了相同的技术,当恢复监听 activity 时,有时 ConflatedBroadcastChannel
会触发它的最新值,导致执行 flatMapLatest
body 没有任何变化。
我认为这是在系统收集垃圾时发生的,因为我可以通过从另一个 activity 调用 System.gc()
来重现此问题。
这是代码
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
这是完整的源代码: CoroutinesFlowTest.zip
- 为什么会这样?
- 我错过了什么?
原因很简单,ViewModels
可以在Activities
的生命周期之外持久化。通过移动到另一个 activity 并进行垃圾收集,您将处理原始 MainActivity
但保留原始 MainViewModel
.
然后当你从 DetailActivity
return 它重新创建 MainActivity
但重用视图模型,它仍然具有具有最后已知值的广播频道,当 count.observe
被调用。
如果您添加日志记录以观察 activity 的 onCreate
和 onDestroy
方法,您应该会看到生命周期得到推进,而视图模型应该只创建一次。
除了
这可能不是你的情况,但你可以尝试在接收端使用 BroadcastChannel(1).asFlow().conflate
,但在我的情况下,它导致了一个错误,有时接收端的代码没有被触发(我认为是因为 conflate 在单独的协程或其他东西中工作)。
或者您可以使用自定义版本的无状态 ConflatedBroadcastChannel(已找到 here)。
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
引用自official response,(简单明了的解决方案)
The problem here is that you are trying to use
ConflatedBroadcastChannel
for events, while it is designed to represent current state as shown in the codelab. Every time the downstreamLiveData
is reactivated it receives the most recent state and performs the incrementing action. Don't useConflatedBroadcastChannel
for events.To fix it, you can replace
ConflatedBroadcastChannel
withBroadcastChannel<IncrementRequest>(1)
(non-conflated channel, which is Ok for events to use) and it'll work as you expect it too.
在协程 1.4.2 和 Kotlin 1.4.31 上
不使用实时数据
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
使用流和协程
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this@MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
不使用 BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}