Race Condition with LiveData - android

TL;DR of this Question:
Is it possible that a LiveData with a backing property (MutableLiveData) inside the ViewModel that is used to Observe and Add to an
ArrayList can have a race condition and using Synchronized or a Lock is required?
Provided that the ArrayList will get its value from a callback
I am trying to setup a group video call using the Agora Android SDK. I followed the documentation here. The problem is with the callbacks (onUserJoined, onUserOffline) from IRtcEngineEventHandler.
OnUserJoined Callback
mRtcEngine = RtcEngine.create(baseContext, APP_ID, object : IRtcEngineEventHandler() {
override fun onUserJoined(uid: Int, elapsed: Int) {
// onUserJoined callback is called anytime a new remote user joins the channel
super.onUserJoined(uid, elapsed)
// We mute the stream by default so that it doesn't consume unnecessary bandwidth
mRtcEngine?.muteRemoteVideoStream(uid, true)
// We are using a lock since uidList is shared and there can be race conditions
lock.lock()
try {
// We are using uidList to keep track of the UIDs of the remote users
uidList.add(uid)
} finally {
lock.unlock()
}
onUserOffline Callback
override fun onUserOffline(uid: Int, reason: Int) {
// onUserOffline is called whenever a remote user leaves the channel
super.onUserOffline(uid, reason)
// We use toRemove to inform the RecyclerView of the index of item we are removing
val toRemove: Int
// We are using a lock since uidList is shared and there can be race conditions
lock.lock()
try {
// We are fetching the index of the item we are about to remove and then remove the item
toRemove = uidList.indexOf(uid)
uidList.remove(uid)
} finally {
lock.unlock()
}
Here Lock is being used to access the uidlist in a thread safe way to prevent race condition. It was working for me when I followed the docs exactly, but when I tried to use a LiveData with a backing property (MutableLiveData) in a ViewModel for saving the uidlist, the observer on uidlist always returned an empty list.
My ViewModel
class MainViewModel: ViewModel() {
private val _uidList: MutableLiveData<ArrayList<Int>> = MutableLiveData()
val uidList: LiveData<ArrayList<Int>> get() = _uidList
init {
_uidList.value = ArrayList<Int>()
}
fun addToUserList(uid: Int) {
_uidList.value?.add(uid)
Log.d("adding user ","$uid")
}
fun removeFromUserList(uid: Int) {
_uidList.value?.remove(_uidList.value!!.indexOf(uid))
}
}
I am calling addToUserList() inside onUserJoined() and removeFromUserList() inside the onUserOffline()
Please guide me to the solution to this problem,
Thank you

You should not be mutating the value stored in LiveData, you'll get very strange behaviour. You have to swap the value out completely.
I'm feeling somewhat lazy so I will just give you the answer.
class MainViewModel: ViewModel() {
private val _uidList: MutableLiveData<List<Int>> = MutableLiveData()
val uidList: LiveData<List<Int>> get() = _uidList
init {
_uidList.value = emptyList<Int>()
}
fun addToUserList(uid: Int) {
_uidList.value = (_uidList.value ?: emptyList()) + uid
Log.d("adding user ","$uid")
}
fun removeFromUserList(uid: Int) {
val value = _uidList.value?.toMutableList()
if (value == null) return
value.remove(value.indexOf(uid))
_uidList.value = value
}

Related

Emit Article with title Kotlin Flow

My task is to get whole Article with provided title from RecyclerView.
When I click on specific Article i get title from it.
Room database:
#Query("SELECT * FROM article_table WHERE title = :title")
fun getArticleDetails(title: String): Flow<ArticleLocal>
Repository:
fun getArticleDetails(title: String): Flow<ArticleLocal> {
return articleDao.getArticleDetails(title)
}
ViewModel:
val articleDetail = MutableStateFlow<ArticleLocal>(ArticleLocal("","","","",""))
fun getArticle(title: String) {
viewModelScope.launch {
articleRepository.getArticleDetails(title).collect {
articleDetail.emit(it)
}
}
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title)
viewModel.articleDetail.collect {
Log.d(TAG, "onCreate: $it")
}
}
Problem with this code is that articleDetail on first touch gives me empty ArticleLocal e.g. title = "" I defined in ViewModel, later I get good result.
EDIT: With MyActivity .collet I get whole object but cannot access propert like it.title
Use a SharedFlow so it doesn't have to publish a default result. The flow won't emit anything until it receives its first value. Use replay = 1 to get similar behavior as StateFlow as far as new subscribers getting the most recent value immediately.
You also need to consider that if the title changes, it should not keep publishing values with the old title. Currently, you have it collecting from more and more flows each time the title changes.
If you use another MutableSharedFlow just for the title, you can get it to automatically cancel unnecessary collection of those old title flows. It also allows you to get the benefit of SharingStarted.WhileSubscribed to avoid unnecessary collection from the repository when there are no subscribers.
In ViewModel:
private val articleTitle = MutableSharedFlow<String>(bufferOverflow = BufferOverflow.DROP_OLDEST)
val articleDetail = articleTitle.flatMapLatest { articleRepository.getArticleDetails(it) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun getArticle(title: String) {
articleTitle.tryEmit(title)
}
You can get rid of additional flow to emit data and use the flow returned from the repository directly.
ViewModel:
fun getArticle(title: String): Flow<ArticleLocal> {
return articleRepository.getArticleDetails(title)
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title).collect {
Log.d(TAG, "onCreate: $it")
}
}

Flow provides null from room database but it should have data

I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}

MutableStateFlow is not updating value

I have problem working with MutableStateFlow, I cannot understand how it is working or I am mistaken somewhere. For example purpose I created simpler classes to get the idea what I am doing.
First I have data class which holds the values and controller which update values in the data class
data class ExampleUiState(
val dataFlag: Boolean = false
)
class ExampleController {
private val _exampleUiState = MutableStateFlow(ExampleUiState())
val exampleUiState = _exampleUiState.asStateFlow()
fun onChangeFlag(flag: Boolean) {
_exampleUiState.update { it.copy(dataFlag = flag) }
}
}
I am using koin, and I created Example controller singleton.
Second I am injection it in my ViewModel where I have two functions there
class ExampleViewModel(
private val exampleController: ExampleController
) : ViewModel() {
val exampleUiState = exampleController.exampleUiState.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
ExampleUiState()
)
//called second
private fun useFlagInViewModelFun() {
//here the value is not updated
exampleUiState.value.dataFlag
}
//called first from UI
fun changeValueFromUi(flag: Boolean) {
//change it from default false to true
exampleController.onChangeFlag(flag)
useFlagInViewModelFun()
}
}
The idea is when I call changeValueFromUi from some compose function, I update the value with my controller function, and after it I call other function where I want to use already updated state of data class, but I don't get the correct value.
Where I am mistaken?
Is there any time needed for onChangeFlag() to react and update the value?
Am I mistaken the way that I am trying to get the value after exampleUiState.value.dataFlag ?

What's the recommended way to update Jetpack Compose UI on Room database update?

Right now, my method of updating my jetpack compose UI on database update is like this:
My Room database holds Player instances (or whatever they're called). This is my PlayerDao:
#Dao
interface PlayerDao {
#Query("SELECT * FROM player")
fun getAll(): Flow<List<Player>>
#Insert
fun insert(player: Player)
#Insert
fun insertAll(vararg players: Player)
#Delete
fun delete(player: Player)
#Query("DELETE FROM player WHERE uid = :uid")
fun delete(uid: Int)
#Query("UPDATE player SET name=:newName where uid=:uid")
fun editName(uid: Int, newName: String)
}
And this is my Player Entity:
#Entity
data class Player(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "name") val name: String,
)
Lastly, this is my ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getDatabase(application)
val playerNames = mutableStateListOf<MutableState<String>>()
val playerIds = mutableStateListOf<MutableState<Int>>()
init {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().getAll().collect {
playerNames.clear()
playerIds.clear()
it.forEach { player ->
playerNames.add(mutableStateOf(player.name))
playerIds.add(mutableStateOf(player.uid))
}
}
}
}
fun addPlayer(name: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().insert(Player(name = name))
}
}
fun editPlayer(uid: Int, newName: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().editName(uid, newName)
}
}
}
As you can see, in my ViewHolder init block, I 'attach' a 'collector' (sorry for my lack of proper terminology) and basically whenever the database emits a new List<Player> from the Flow, I re-populate this playerNames list with new MutableStates of Strings and the playerIds list with MutableStates of Ints. I do this because then Jetpack Compose gets notified immediately when something changes. Is this really the only good way to go? What I'm trying to achieve is that whenever a change in the player table occurs, the list of players in the UI of the app gets updated immediately. And also, I would like to access the data about the players without always making new requests to the database. I would like to have a list of Players at my disposal at all times that I know is updated as soon as the database gets updated. How is this achieved in Android app production?
you can instead use live data. for eg -
val playerNames:Livedata<ListOf<Player>> = db.playerDao.getAll().asliveData
then you can set an observer like -
viewModel.playerNames.observe(this.viewLifecycleOwner){
//do stuff when value changes. the 'it' will be the changed list.
}
and if you have to have seperate lists, you could add a dao method for that and have two observers too. That might be way more efficient than having a single function and then seperating them into two different lists.
First of all, place a LiveData inside your data layer (usually ViewModel) like this
val playerNamesLiveData: LiveData<List<Player>>
get() = playerNamesMutableLiveData
private val playerNamesMutableLiveData = MutableLiveData<List<Player>>
So, now you can put your list of players to an observable place by using playerNamesLiveData.postValue(...).
The next step is to create an observer in your UI layer(fragment). The observer determines whether the information is posted to LiveData object and reacts the way you describe it.
private fun observeData() {
viewModel.playerNamesLiveData.observe(
viewLifecycleOwner,
{ // action you want your UI to perform }
)
}
And the last step is to call the observeData function before the actual data posting happens. I prefer doing this inside onViewCreated() callback.

How to pass different value when switchMap is not executed? Android + Kotlin

I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}

Categories

Resources