Иногда ConflatedBroadcastChannel запускает недавнее значение без каких-либо действий
В официальной кодовой лаборатории Google, посвященной образцу расширенных сопрограмм-кодов, они использовалиConflatedBroadcastChannel
чтобы наблюдать за изменением переменной / объекта.
Я использовал ту же технику в одном из своих побочных проектов, и, возобновляя слушание, иногда ConflatedBroadcastChannel
запускает его недавнее значение, вызывая выполнение flatMapLatest
кузов без каких-либо изменений.
Я думаю, что это происходит, когда система собирает мусор, так как я могу воспроизвести эту проблему, вызвав 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
- Почему это происходит?
- Что мне не хватает?
4 ответа
Цитата из официального ответа(простое и понятное решение)
Проблема здесь в том, что вы пытаетесь использовать
ConflatedBroadcastChannel
для событий, в то время как он предназначен для представления текущего состояния, как показано в кодовой таблице. Каждый раз, когда нисходящий потокLiveData
повторно активируется, он получает самое последнее состояние и выполняет действие приращения. Не использоватьConflatedBroadcastChannel
для мероприятий.Чтобы исправить это, можно заменить
ConflatedBroadcastChannel
сBroadcastChannel<IncrementRequest>(1)
(несвязанный канал, который подходит для событий), и он тоже будет работать так, как вы этого ожидаете.
В дополнение к ответу Кискэ:
Возможно, это не ваш случай, но вы можете попробовать использовать BroadcastChannel(1).asFlow().conflate
на стороне получателя, но в моем случае это привело к ошибке, когда код на стороне получателя иногда не запускался (я думаю, потому что conflate работает в отдельной сопрограмме или что-то в этом роде).
Или вы можете использовать настраиваемую версию ConflatedBroadcastChannel без сохранения состояния (можно найти здесь).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
На Coroutine 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")
}
Используйте Flow и Coroutine
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"
}
Причина очень проста, ViewModels
может сохраняться вне жизненного цикла Activities
. Переходя к другому занятию и собирая мусор, вы избавляетесь от оригинала.MainActivity
но сохраняя оригинал MainViewModel
.
Затем, когда вы вернетесь из DetailActivity
он воссоздает MainActivity
но повторно использует модель просмотра, у которой все еще есть канал широковещания с последним известным значением, вызывая обратный вызов, когда count.observe
называется.
Если вы добавите ведение журнала для наблюдения onCreate
а также onDestroy
методы действия, вы должны увидеть, что жизненный цикл расширяется, в то время как модель просмотра должна быть создана только один раз.