Search code examples
androidkotlinandroid-recyclerviewandroid-livedatacountdowntimer

LiveData not updating for every list item, only for the last one


I have a string LiveData which is a generated identifier from one of my own methods. My problem is that I have a list in recyclerview and when a countdown timer finish, all of items of the recyclerview must generate an identifier but it only generates the identifier for the last item of the list.

This is what I tried:

var userPhone = 0
private fun setUpObservable() {
    myViewModel.apply {
        identifierAction.observe() {response - >
           if(response is IdentifierAction.Identifier)
              handleIdentifier(response.identifier)
        }
    }
}

private fun handleIdentifier(identifier: String) {
    //list using data class with name(String), lastname(String), phone(Int) and identifier(String)
    var myList = MySharedPreference.getMyList(context)
    val itemToOverride = myList.find {
        it.phone == userPhone
    }
    if (itemToOverride != null) {
        MySharedPreference.removeUser(context, itemToOverride)
        itemToOverride.identifier = identifier
        MySharedPreference.addUser(context, itemToOverride)
        myList = MySharedPreference.getMyList(context)
        myAdapter.setList(myList)

    }
}
override fun onFinish(position: Int) {
    //list using data class with name(String), lastname(String), phone(Int) and identifier(String)
    val myList = MySharedPreference.getMyList(context)
    if (position != 1) {
        userPhone = myList[position].phone
        myViewModel.generateIdentifier(IdentifierRequisites(name = myList[position].name, lastname = myList[position].lastname, phone = myList[position].phone))
    }
}

Adapter:

class MyAdapter(private var list: MutableList <MyUser> = mutableListOf(),private var listener: OnTimerFinishListener) {
    interface OnTimerFinishListener {
        fun onFinish(position: Int)
    }
    private fun startCountDownTimer(myList: MyUser) {
        var timer = object: CountDownTimer(15000, 1000) {
            override fun onTick(left: Long) {
                //do something every second
            }

            private fun setList(myList: List <MyUser> ) {
                this.list = myList.sortedBy {
                    it.name
                }.toMutableList()
                notifyDataSetChanged()
            }
            override fun onFinish() {
                listener.onFinish(absoluteAdapterPosition)
            }
        }
    }
}

ViewModel:

private val _identifierAction: MutableLiveData < IdentifierAction > by lazy {
    MutableLiveData < IdentifierAction > ().apply {
        value = IdentifierAction.Init
    }
}
val identifierAction: LiveData < IdentifierAction > get() = _identifierAction
fun generateIdentifier(data: IdentifierRequisites) {
    viewModelScope.launch {
        val response = generateIdentifierUseCase.invoke(data)
        _identifierAction.postValue(IdentifierAction.Identifier(response))
    }
}

When timer finishes, the method onFinish is called for every item of the list but only the last item is triggering handleIdentifier method. How can I solve this? How can I wait for every item to get the identifier?


Solution

  • They're probably generating fine, it's just that the value of the LiveData is updated so quickly that by the time it pushes a new value to observers, it's already been set to the last ID in your list. Like this:

    class CoolVM : ViewModel() {
        val someLiveData = MutableLiveData<Int>()
        fun setData(number: Int) {
            viewModelScope.launch {
                Log.i("ViewModel", "Updating with number: $number")
                someLiveData.postValue(number)
            }
        }
    }
    
    class MainFragment : Fragment(R.layout.fragment_main) {
    
        private val model: CoolVM by viewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            model.someLiveData.observe(viewLifecycleOwner) { num ->
                Log.i("Observer", "Observer got number: $num")
            }
            (1..5).forEach(model::setData)
        }
    }
    

    And the log output is

    Updating with number: 1
    Updating with number: 2
    Updating with number: 3
    Updating with number: 4
    Updating with number: 5
    Observer got number: 5
    

    This is just how LiveData works, and I'm not aware of a way to force it to push every update immediately. Its focus is more on the current value and it's mostly intended as a way to drive the UI in a reactive way. Since the UI only needs to update every frame, it doesn't need to handle every intermediate value when there are a bunch of updates in rapid succession.


    I'm no coroutine expert so maybe someone else can provide a better solution, but you could try using a Channel (which is like a queue) and collect that stream of values as a Flow:

    class CoolVM : ViewModel() {
        val numbers = Channel<Int>()
        
        fun setData(number: Int) {
            viewModelScope.launch {
                Log.i("ViewModel", "Updating with number: $number")
                numbers.send(number)
            }
        }
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                model.numbers.consumeAsFlow().collect { num ->
                    Log.i("Observer", "Observer got number: $num")
                }
            }
        }
        (1..5).forEach(model::setData)
    }
    

    Which does handle each value immediately, in turn

    Updating with number: 1
    Observer got number: 1
    Updating with number: 2
    Observer got number: 2
    Updating with number: 3
    Observer got number: 3
    Updating with number: 4
    Observer got number: 4
    Updating with number: 5
    Observer got number: 5
    

    I'm not 100% sure if that's the right approach or if there are any gotchas to watch out for, if someone knows better let me know!