Jetpack Compose Room LiveData does not update after update query - android-jetpack-compose

i am using Jetpack Compose 1.2.0 and Room 2.4.3
everything works well and my state changes when i use Read, Insert, Delete but i don't know why it does not work with update (when i navigate back or re enter the screen Its okay and i will get updated data)
this is my DAO
#Dao
abstract class MessageDAO : BaseDao<Message>() {
#Query("SELECT * FROM messages")
abstract fun getAllMessages(): LiveData<List<Message>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract override fun insert(obj: Message): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract override fun insert(obj: MutableList<Message>?): MutableList<Long>
#Update
abstract override fun update(obj: Message)
#Update
abstract override fun update(obj: MutableList<Message>?)
#Delete
abstract override fun delete(obj: Message)
#Query("delete from messages where id in (:messageIDs)")
abstract fun delete(messageIDs: List<Long>)
}
also this my viewModel
#HiltViewModel
class MessagesViewModel #Inject constructor(
private val application: Application,
private val messageRepository: MessageRepository
) : ViewModel() {
fun sendMessage(message: Message) =
CoroutineScope(Dispatchers.IO).launch {
message.localId = messageRepository.insert(message)
}
fun editMessage(message: Message) =
CoroutineScope(Dispatchers.IO).launch {
messageRepository.update(message)
}
fun deleteMessage(message: Message) {
CoroutineScope(Dispatchers.IO).launch {
messageRepository.delete(message)
}
}
}
and this is my Composable function to show data
#Composable
fun Messaging(
navController: NavController,
to_user_id: String,
messagesViewModel: MessagesViewModel,
currentUserId: Long,
) {
val messages: List<Message> by messagesViewModel.getConversationMessages(to_user_id.toLong())
.observeAsState(
listOf()
)
Column {
MessagingHeader(
navController,
profileViewModel,
to_user_id.toLong(),
selection
)
Column(Modifier.weight(1f)) {
Column(
Modifier.verticalScroll(
state = rememberScrollState(),
reverseScrolling = true
)
) {
messages.forEach { message ->
Message(
currentUserId,
message
)
}
}
}
}
UPDATE for getConversationMessages func:
fun getConversationMessages(targetUserId: Long) =
messageDAO.getMessagesByTargetUserId(targetUserId)
and this getMessagesByTargetUserId func for my MessageDAO
#Query("SELECT * FROM messages WHERE receiver = :targetUserId OR sender = :targetUserId ORDER BY createdAt")
abstract fun getMessagesByTargetUserId(targetUserId: Long): LiveData<List<Message>>

It sounds like the issue you're experiencing is that the data in your Composable function is not being updated when you call the "update" method in your ViewModel. The reason for this is that the LiveData returned by the "getConversationMessages" function in your repository is not being updated when you call "update" in your ViewModel.
One solution to this issue is to use the "postValue" method instead of "setValue" when updating the value of the LiveData object in your repository. When you call "postValue", it will trigger the observer in your Composable function and update the UI with the latest data.
Another solution is to use the Transformations.map method in your Messaging function to convert the LiveData returned by the getConversationMessages to another LiveData type, and then observe it, this way when the data change inside the map the observer will be triggered.
For example:
val conversationLiveData = messagesViewModel.getConversationMessages(to_user_id.toLong())
val messages: List<Message> by Transformations.map(conversationLiveData){it}.observeAsState(listOf())
It's also possible that the issue is caused by a problem with the Room database's ability to notify the observer when the data is updated. One way to check this is to add a log statement in the "update" method in your ViewModel to ensure that it is being called when you expect it to be.
It's also possible that the issue is related to the use of CoroutineScope with Dispatchers.IO in your ViewModel. In this case it might be useful to try calling the update method with Dispatchers.Main instead.
In summary, the issue you're experiencing is likely related to the LiveData object not being updated when the update method is called. There are several possible solutions to this issue, including using the "postValue" method instead of "setValue" when updating the value of the LiveData object, using Transformations.map, making sure the update method is being called when it should be, or trying to call the update method with Dispatchers.Main instead of Dispatchers.IO in your ViewModel.

Related

Update jetpcak composable when a static class member changes

I have a Paho MQTT connection, with a callback updating a object and i need to update a coposable..
I can see the in Logcat that i receive information, but the composable is not updated.
I am suspecting that the issue is that i am using a static object and it is not mutable. What is the practice on this scenario? I did not implement a ViewModel. It could be done with a timer, but i think it is not an elegant solution.
snippet:
object MyCallBack : MqttCallback {
public var message = ""
override fun messageArrived(topic: String?, message: MqttMessage?) {
this.message = message.toString()
Log.e(ContentValues.TAG,"mqtt Arrived: $message")
}
......
}
and a composable function used to display the information:
#Composable
fun ShowMessage() {
var myCallBack = MyCallBack //here i can access the updated information
var message by remember {
mutableStateOf(myCallBack.message)
Text("Changed ${myCallBack.message}", color = Color.White)
}
}
Thank you!
i have tried to use mutableStateOf() but it did not called for composition, i think it is not observable.

Automatically update a flow from a changes of another flow (StateFlow) Jetpack Compose

I have a StateFlow from which my List composable collects any changes as a State.
private val _people = MutableStateFlow(personDataList())
val people = _people.asStateFlow()
And inside my viewModel, I perform modifications on _people and I verify that people as a read-only StateFlow is also getting updated. I also have to make a copy of the original _people as an ordinary kotlin map to use for some verifications use-cases.
val copyAsMap : StateFlow<MutableMap<Int, Person>> = people.map {
it.associateBy( { it.id }, { it } )
.toMutableMap()
}.stateIn(viewModelScope, SharingStarted.Eagerly, mutableMapOf())
however, with my attempt above, it (the copyAsMap) doesn't get updated when I try to modify the list (e.g delete) an item from the _people StateFlow
Any ideas..? Thanks!
Edit:
Nothing is collecting from the copyAsMap, I just display the values everytime an object is removed from _person state flow
delete function (triggered by an action somewhere)
private fun delete(personModel: Person) {
_person.update { list ->
list.toMutableStateList().apply {
removeIf { it.id == personModel.id }
}
}
copyAsMap.values.forEach {
Log.e("MapCopy", "$it")
}
}
So based on your comment how you delete the item, that's the problem:
_people.update { list ->
list.removeIf { it.id == person.id }
list
}
You get an instance of MutableList here, do the modification and you "update" the flow with the same instance. And, as StateFlow documentation says:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
Which means that your updated list is actually never emitted, because it is equal to the previous value.
You have to do something like this:
_people.update { list ->
list.toMutableList().apply { removeIf { ... } }
}
Also, you should define your state as val _people: MutableStateFlow<List<T>> = .... This would prevent some mistakes you can make.

Jetpack Compose - UI Test Coil Fetched Image

I have a custom Image composable that uses Coil's rememberAsyncImagePainter.
However, I have another component that uses resources and has logic that is handled separately.
I successfully render the custom resource placeholder, however I'm not sure how I can write a test to check that the actual url image is loaded & visible.
Both the url image and the resource image have different testTags, however in the test, the node with the url image's tag never exists.
Does Coil have any solution to mock the ImageRequest.Builder so that I can guarantee that the URL image successfully loads?
I would prefer to not add any test-related code to the component itself, but if that's the only way, then I would prefer the component to be testable.
According to the official docs https://coil-kt.github.io/coil/image_loaders/#testing, you can create a FakeImageLoader class like this:
class FakeImageLoader(private val context: Context) : ImageLoader {
override val defaults = DefaultRequestOptions()
override val components = ComponentRegistry()
override val memoryCache: MemoryCache? get() = null
override val diskCache: DiskCache? get() = null
override fun enqueue(request: ImageRequest): Disposable {
// Always call onStart before onSuccess.
request.target?.onStart(request.placeholder)
val result = ColorDrawable(Color.BLACK)
request.target?.onSuccess(result)
return object : Disposable {
override val job = CompletableDeferred(newResult(request, result))
override val isDisposed get() = true
override fun dispose() {}
}
}
override suspend fun execute(request: ImageRequest): ImageResult {
return newResult(request, ColorDrawable(Color.BLACK))
}
private fun newResult(request: ImageRequest, drawable: Drawable): SuccessResult {
return SuccessResult(
drawable = drawable,
request = request,
dataSource = DataSource.MEMORY_CACHE
)
}
override fun newBuilder() = throw UnsupportedOperationException()
override fun shutdown() {}
}
And you UI test, you can write
#Test
fun testCustomImageComposable() {
Coil.setImageLoader(FakeCoilImageLoader())
setContent {
CutomImageComposable(...)
}
// ... assert image is displayed, etc.
}
This should guarantee the image to be shown every-time the test is executed.
Alternatively, you can mock ImageLoader and mimic the behavior above.
However, I would not recommend mocking ImageLoader, as we do not know whether rememberAsyncImagePainter uses all of the methods, but if you must mock it, it would be worth a try.

Overloading a method in Groovy using Closure arguments with different return types

I'm reasonably proficient with Groovy insofar as my job requires, but not having a background in OOP means that some things still elude me, so apologies if some of the wording is a little off here (feel free to edit if you can make the question clearer).
I'm trying to create an overloaded method where the signature (ideally) differs only in the return type of the single Closure parameter. The Closure contains a method call that returns either an ItemResponse or ListResponse object, both of which could contain an object/objects of any type (which is the type I would like to infer).
The following code is a simplified version of what I'm trying to implement - an error handling method which takes a reference to a service call, safely attempts to resolve it, and returns the item/items from the response as appropriate:
public <T> T testMethod(Closure<ItemResponse<T>> testCall) {
testCall.call().item as T
}
public <T> List<T> testMethod(Closure<ListResponse<T>> testCall) {
testCall.call().items as T
}
Obviously this doesn't work, but is there any alternate approach/workaround that would achieve the desired outcome?
I'm trying to create an overloaded method where the signature
(ideally) differs only in the return type of the single Closure
parameter.
You cannot do that because the return type is not part of the method signature. For example, the following is not valid:
class Demo {
int doit() {}
String doit() {}
}
As mentioned by yourself and #jeffscottbrown, you can't have two methods with the same parameters but different return value. The workaround I can see here is to use a call-back closure. The return value of your testMethod would default to Object and you would provide an "unwrapper" that would the bit after the closure call (extract item or items). Try this out in your GroovyConsole:
class ValueHolder <T> {
T value
}
Closure<List<Integer>> c = {
[1]
}
Closure<ValueHolder<String>> d = {
new ValueHolder(value:'hello world')
}
Closure liu = {List l ->
l.first()
}
Closure vhsu = {ValueHolder vh ->
vh.value
}
// this is the generic method
public <T> Object testMethod(Closure<T> testCall, Closure<T> unwrapper) {
unwrapper(testCall.call()) as T
}
println testMethod(c, liu)
println testMethod(d, vhsu)
It works with both a list or a value holder.

What does Cannot create delegate without target for instance method or closure mean

I am using vala.
This is the source code that gives that compile time bug :
private Gee.HashMap<string,VoidFunc> fill_actions()
{
var actions = new Gee.HashMap<string,VoidFunc>();
MainWindow win = window;
actions["t"] = () => _puts(win.title);
return actions;
}
First I tried to access this.window directly but that gave another error so I tried this with a local scope variable.
Error when doing directly this.window :
This access invalid outside of instance methods
It sounds like VoidFunc is declared with [CCode (has_target = false)]. What that means is that no context information is passed to it, and AFAIK that is the only way delegates work as generic type arguments. The reason for this is limitations in C, so assuming VoidFunc looks like this:
[CCode (has_target = false)]
public delegate void VoidFunc ();
What you'll get in C is something like this:
typedef void (*VoidFunc)();
As opposed to something like this if you didn't have the [CCode (has_target = false)]:
typedef void (*VoidFunc)(gpointer user_data);
When you pass around callbacks in C you generally do so with between one and three arguments. Something with all three would look like this:
void foo (VoidFunc void_func, gpointer user_data, GDestroyNotify notify);
The first parameter is the actual function. The second parameter is the value to pass as user_data to the callback, and is what Vala uses to pass context information to the callback (which is what allows it to act as an instance method, or even a closure). The third parameter is used to specify a function to free user_data when it is no longer needed.
What [CCode (has_target = false)] means is that the delegate doesn't have a user_data argument, and therefore cannot be used as a closure or instance method.
The reason this is necessary with a generic argument is that generics look something like this at the C level:
void foo_bar (gpointer data, GDestroyNotify notify);
The first parameter is the data that you want to use as a generic value, the second is actually only added if the generic argument is owned (as it is in the case of the set methods in Gee), and is called with user_data as an argument when user_data is no longer needed.
As you can see, when trying to use a delegate as a generic, there is nowhere to put the user_data argument, which is why Vala only allows delegates without targets to be generic arguments.
The solution is basically to wrap the delegate in a class:
public delegate void VoidFunc ();
public class YourClass {
private class VoidFuncData {
public VoidFunc func;
public VoidFuncData (owned VoidFunc func) {
this.func = (owned) func;
}
}
private Gee.HashMap<string,VoidFuncData> fill_actions() {
var actions = new Gee.HashMap<string,VoidFuncData>();
string win = "win";
actions["t"] = new VoidFuncData (() => GLib.debug (win));
return actions;
}
}

Resources