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?
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!