4

Hey I am working in kotlin flow in android. I noticed that my kotlin flow collectLatest is calling twice and sometimes even more. I tried this answer but it didn't work for me. I printed the log inside my collectLatest function it print the log. I am adding the code

MainActivity.kt

class MainActivity : AppCompatActivity(), CustomManager {

    private val viewModel by viewModels<ActivityViewModel>()
    private lateinit var binding: ActivityMainBinding
    private var time = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setupView()
    }

    private fun setupView() {
        viewModel.fetchData()

        lifecycleScope.launchWhenStarted {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.conversationMutableStateFlow.collectLatest { data ->
                    Log.e("time", "${time++}")
                   ....
                }
            }
        }
    }
}

ActivityViewModel.kt

class ActivityViewModel(app: Application) : AndroidViewModel(app) {

    var conversationMutableStateFlow = MutableStateFlow<List<ConversationDate>>(emptyList())

    fun fetchData() {
        viewModelScope.launch {
            val response = ApiInterface.create().getResponse()
            conversationMutableStateFlow.value = response.items
        }
    }

   .....
}

I don't understand why this is calling two times. I am attaching logs

2022-01-17 22:02:15.369 8248-8248/com.example.fragmentexample E/time: 0
2022-01-17 22:02:15.629 8248-8248/com.example.fragmentexample E/time: 1

As you can see it call two times. But I load more data than it call more than twice. I don't understand why it is calling more than once. Can someone please guide me what I am doing wrong. If you need whole code, I am adding my project link.

Sergio
  • 27,326
  • 8
  • 128
  • 149
Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127
  • I'm not expert here, but even name of `repeatOnLifecycle()` suggests that it calls the code block multiple times. And with each repetition you start another `collectLatest()`. – broot Jan 17 '22 at 22:49
  • @broot what can i do this situation ? – Kotlin Learner Jan 17 '22 at 22:52
  • I'm not very familiar with Android, but maybe remove this `repeatOnLifecycle()` and put `collectLatest()` directly inside `launchWhenStarted()`? Because this is what I think you need here: execute collection only once when started, not with each lifecycle change. – broot Jan 17 '22 at 22:56
  • @broot I tried this already but it's not working :( – Kotlin Learner Jan 17 '22 at 23:01
  • You have snipped out some code from collectLatest that might have to do with the problem. Can you add it back in? – Tenfour04 Jan 18 '22 at 02:06
  • @Tenfour04 sorry I didn't get it. – Kotlin Learner Jan 18 '22 at 08:12

3 Answers3

9

You are using a MutableStateFlow which derives from StateFlow, StateFlow has initial value, you are specifying it as an emptyList:

var conversationMutableStateFlow = MutableStateFlow<List<String>>(emptyList())

So the first time you get data in collectLatest block, it is an empty list. The second time it is a list from the response.

When you call collectLatest the conversationMutableStateFlow has only initial value, which is an empty list, that's why you are receiving it first.

You can change your StateFlow to SharedFlow, it doesn't have an initial value, so you will get only one call in collectLatest block. In ActivityViewModel class:

var conversationMutableStateFlow = MutableSharedFlow<List<String>>()

fun fetchData() {
    viewModelScope.launch {
        val response = ApiInterface.create().getResponse()
        conversationMutableStateFlow.emit(response.items)
    }
}

Or if you want to stick to StateFlow you can filter your data:

viewModel.conversationMutableStateFlow.filter { data ->
                data.isNotEmpty()
            }.collectLatest { data ->
                // ...
            }
Sergio
  • 27,326
  • 8
  • 128
  • 149
2

Replace

lifecycleScope.launchWhenStarted

with

viewLifecycleOwner.lifecycleScope.launch
0

The reason is collectLatest like backpressure. If you pass multiple items at once, flow will collect latest only, but if there are some time between emits, flow will collect each like latest

EDITED: You really need read about MVVM architecture.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    setupView()
}

private fun setupView() {
    if (supportFragmentManager.findFragmentById(R.id.fragmentView) != null) 
        return
    
    supportFragmentManager
        .beginTransaction()
        .add(R.id.fragmentView, ConversationFragment())
        .commit()
}
}

Delele ActivityViewModel and add that logic to FragmentViewModel. Also notice you don't need use AndroidViewModel, if you can use plain ViewModel. Use AndroidViewModel only when you need access to Application or its Context

heckfyxe
  • 9
  • 2