Иногда 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 методы действия, вы должны увидеть, что жизненный цикл расширяется, в то время как модель просмотра должна быть создана только один раз.