Jetpack compose not recomposing view on value change - kotlin

I am using objectbox and attempting to force recomp on my lazy column with information from the viewModel.
I currently use a method that has been state hoisted to the view model but can't seem to retrieve the value in the view model and force recomp.
In my view model (DataFieldsViewModel), I've obtained all the Data Fields like this:
private val _dataFieldsBox = ObjectBox.get().boxFor(DataField::class.java)
var dataFieldsBox: Box<DataField> = _dataFieldsBox
and then it is passed in the composable screen using
fields = viewModel.dataFieldsBox
and the data fields object removed with
is DataFieldEvent.ConfirmDelete -> {
_deletedDataField.value = event.dataField
_dataFieldsBox.remove(deletedDataField.value)
}
the reference for deletedDataField is kept so that it can be restored from a snack bar.
The delete works, the restore works, it's just that the lazy column does not update until I click another view that forces recomp. I've even mocked up a simple button with state hoisted increment and decrement methods and a value in the view model to test out recomp and got that working pretty quick, just don't know how to do it for these ObjectBox DataField objects
Edit 18/08
Tried using mutableStateOf
private var _dataFieldsBox = ObjectBox.get().boxFor(DataField::class.java)
var dataFieldsBox = mutableStateOf(_dataFieldsBox)
and then attempting to collect the value with
val fields by viewModel.dataFieldsBox
and then deleting with
_deletedDataField.value = event.dataField
_dataFieldsBox.remove(event.dataField)
only fix right now is the two hacky ways which i don't like. I read something about despite the values in a mutable list changing because the reference is the same the recomp doesn't happen to save on recomps.
So tried to amend the list and then duplicate with new values which also did nothing.
is DataFieldEvent.ConfirmDelete -> {
_deletedDataField.value = event.dataField
_dataFieldsBox.remove(event.dataField)
val newList = _dataFieldsBox
dataFieldsBox = mutableStateOf(newList)
}

Would be helpful if you update viewmodel code you are having.
Not worked on ObjectBox. With your current code, here is the thing.
Composable function will recompose when it is able to listen to changes. For normal variable it does not listen to changes. You can make them listen in several ways.
You can do it with mutableStateOf(). You can have mutableState variable which takes type of _dataFieldsBox . And so can access it by viewModel._dataFieldsBox.value.
private val _dataFieldsBox = ObjectBox.get().boxFor(DataField::class.java
var _dataFieldsBox = mutableStateOf(_dataFieldsBox)
And then in view model,
_dataFieldsBox.value.remove(_dataFieldsBox) // This will remove the value in the state variable and composable function will recompose.
You can also use LiveData and observe for the changes and it will recompose.

Related

How to manually update a kotlin flow

I'm trying to update the source of a Flow in Kotlin and I'm not sure if this is the right approach and if it's possible with Flow at all.
I have a database containing posts for a user and this returns me a Flow<List<Post>>.
Now when I select another user I want the flow of the database to return me the posts of the newly selected user:
lateinit var userPosts: Flow<List<Post>>
private set
fun getPostsForUser(user: User) {
userPosts = database.getAllPostsForUser(user)
}
But the flow never gets updated with the data of the new selected user. Is Flow still the right choice in this case and if yes, how can I update the flow with the new posts?
I know how to do it manually with fetching data from the database and emitting it using LiveData, but I would like to avoid handling the update of posts everytime the user posts something new or a post is deleted.
I think maybe you're collecting one flow, and then setting a user, which changes the flow in the property, but not any existing previous flow that's already being collected. I'm not sure how else to explain what's happening.
It may be error-prone to have a public Flow property that is reliant on some other function that takes a parameter. You could return a Flow directly from the function so there is no ambiguity about the behavior. The fragment requests a Flow for a specific User and immediately gets it.
distinctUntilChanged() will prevent it from emitting an unchanged list that results from other changes in the repo.
fun getPostsForUser(user: User) Flow<List<Post>> =
database.getAllPostsForUser(user).distinctUntilChanged()
If you do want to use your pattern, I think it you could do it like this. This allows there to only ever be one Flow so it's safe to start collecting it early. Changing the user will change which values it's publishing. Although this is more complicated than above, it has the advantage of not requiring a data refresh on screen rotations and other config changes.
private val mutableUserPosts = MutableStateFlow<List<Post>>(emptyList())
val userPosts: Flow<List<Post>> = mutableUserPosts
private var userPostsJob: Job? = null
var user: User? = null
set(value) {
field = value
userPostsJob?.cancel()
value ?: return
userPostsJob = database.getAllPostsForUser(value)
.onEach { mutableUserPosts.emit(it) }
.launchIn(viewModelScope)
}
Or as Joffrey suggests, it's simpler with a User Flow if you don't mind using the unstable API function flatMapLatest. The user property here could be dropped if you don't mind exposing a public mutable flow where the value should be set externally. Or if you use this pattern repeatedly, you could make operator extension functions for StateFlow/MutableStateFlow to use it as a property delegate.
private val userFlow = MutableStateFlow<User?>(null)
var user: User?
get() = userFlow.value
set(value) {
userFlow.value = value
}
val userPosts: Flow<List<Post>> = userFlow.flatMapLatest { user ->
if (user == null) emptyFlow() else database.getAllPostsForUser(user)
}
This might be helpful for someone...
When you are collecting a flow downstream and in some situations, you need an updated flow or a completely different flow than before, Use flatmapLatest() function.
Example:
There is a flow of search results in an app. When a user enters a search text flow automatically should update with the latest data according to searched chars.
Place the query text in a StateFlow as,
private var _userSearchText = MutableStateFlow("")
val userSearchText = _userSearchText.asStateFlow()
as the user enters text just update the Stateflow as,
fun setUserNameSearchText(data: String) {
_userSearchText.value = data
}
and your flow collecting like,
val response = userSearchText.flatMapLatest {
searchRepository.getResults(it)
.cachedIn(viewModelScope)
}
Stateflow will trigger the API calls automatically. It will notify all the observers on them.
PS: Open to improving...

(Problem solved) Set the value of a livedata variable of type <data class> to a list of strings?

How to populate the value of this variable:
private val _urlList = MutableLiveData<List<Url>>()
of type Url:
data class Url(
val imgSrcUrl: String
)
with the incoming list of url strings from a firebase call?
Here is where the magic happens:
private fun getData(){
viewModelScope.launch {
try {
getImagesUrl {
"Here where I need to set the value of the variable to a listOf(it) with it being strings
of urls retrieved from firebase storage"
}
}catch (e: Exception){
"Handling the error"
}
}
}
Edit
The map function #dominicoder provided solved my problem, answer accepted.
Thank you all for your help
Your question is unclear because you're showing a live data of a single Url object but asking to stuff it with a list of strings. So first, your live data object needs to change to a list of Urls:
private val _urlList = MutableLiveData<List<Url>>()
Then, assuming getImagesUrl yields a list of strings, if I understood you correctly, then you would map that to a list of Urls:
getImagesUrl { listOfImageUrlStrings ->
_urlList.value = listOfImageUrlStrings.map { imageUrlString -> Url(imageUrlString) }
}
If that does not answer your question, you really need to review it and clarify.
You can set values on the MutableLiveDataObject in two ways (depends on what you're doing).
Setting the value as normal from the UI thread can be done with:
myLiveData.value = myobject
If you're setting it from a background thread like you might in a coroutine with a suspended function or async task etc then use:
myLiveData.postValue(myObject)
It's not clear from your question whether the LiveData is meant to hold a list as you mention both lists and single values. But your LiveData holds a set the values as a collection like a list, set or map. It's can be treated as a whole object so adding a value later needs to have the whole collection set again like:
myLiveData.value = mutableListOf<Url>()
//Response received and object created
myLiveData.value = myLiveData.value.apply {
add(myObject)
}
Or if the value is mutable updating the existing value (preferred as it's cleaner):
myLiveData.value.add(myObject)
The problem with that approach is you're exposing the map as a mutable/writeable object. Allowing accessors to change the values which you might not want.

adding to a list that is contained in livedata and adding elements to that list

adding to a list that is contained in livedata and adding elements to that list
val resourcesLiveData by lazy { MutableLiveData<List<File>>() }
I thought this should work as my LiveData is a list of files and I just want to add elements to it. But the value of live data is always an empty list. The res is the different file resources I am trying to add
resourceLiveData.value?.toMutableList()?.add(res)
So I tried it more expicity using this version but the list is still empty
val listOfRes = resourceLiveData.value ?: emptyList()
listOfRes.toMutableList().add(res)
resourceLiveData.value = listOfRes.toList()
Can anyone see if I am doing something wrong.
Just want to add to the list that is contained in the value
Agree to #KeyserSoze answer, if you have to use only List then you can do below
resourceLiveData.value = resourceLiveData.value?.toMutableList()?.apply { add(res) }?: emptyList()
You are creating a new object by calling toMutableList() instead of updating the original.
Change your LiveData type to MutableList:
val resourcesLiveData by lazy { MutableLiveData<MutableList<File>>() }
Then, update the value accordingly:
resourceLiveData.value?.add(res)

How do I duplicate an object in Verold?

I am trying to duplicate an object every time I click it in Verold. I have attached the Object Picker to the scene and successfully triggered a function which prints to the console.
I've tried this code but I get a Type Error - can't read property of undefined.
var xxx = this.getEntity().clone();
var threeDataxxx = xxx.getThreeData();
threeDataxxx.position.x += Math.random() * 5;
The clone() method is asynchronous (because the same method would be used if you were creating persistent copies of your objects on the server). This function, like many functions in the Verold API, takes an 'options' object as a parameter. In here, you need to specify the 'success' callback method like in the following example. Once you have the clone, you then need to add it to the scene hierarchy using the addChild() method. This will automatically trigger the cloned object to load.
var parent = this.getEntity().getParentObject();
this.getEntity().clone( {
success: function( newEntity ) {
parent.addChild( newEntity );
var position = newEntity.getPosition();
position.x += Math.random() * 10;
newEntity.setPosition( position.x, position.y, position.z );
}
});
The multiple steps are useful because you may want to clone several objects and have them ready to add to the scene at a later time.
And, of course, if you don't require the cloned object to have components or any of the other functionality of a VeroldObject, you can always just get the threeData and then use Three.JS's clone() method.
Hope that helps.

belongsTo only being set on first and last member of hasMany

My adapter uses findHasMany to load child records for a hasMany relationship.
My findHasMany adapter method is directly based on the test case for findHasMany. It retrieves the contents of the hasMany on demand, and eventually does the following two operations:
store.loadMany(type, hashes);
// ...
store.loadHasMany(record, relationship.key, ids);
(The full code for the findHasMany is below, in case the issue is there, but I don't think so.)
The really strange behavior is: it seems that somewhere within loadHasMany (or in some subsequent async process) only the first and last child records get their inverse belongsTo property set, even though all the child records are added to the hasMany side. I.e., if posts/1 has 10 comments, this is what I get, after everything has loaded:
var post = App.Posts.find('1');
post.get('comments').objectAt(0).get('post'); // <App.Post:ember123:1>
post.get('comments').objectAt(1).get('post'); // null
post.get('comments').objectAt(2).get('post'); // null
// ...
post.get('comments').objectAt(8).get('post'); // null
post.get('comments').objectAt(9).get('post'); // <App.Post:ember123:1>
My adapter is a subclass of DS.RESTAdapter, and I don't think I'm overloading anything in my adapter or serializer that would cause this behavior.
Has anybody seen something like this before? It's weird enough I though someone might know why it's happening.
Extra
Using findHasMany lets me load the contents of the hasMany only when the property is accessed (valuable in my case because calculating the array of IDs would be expensive). So say I have the classic posts/comments example models, the server returns for posts/1:
{
post: {
id: 1,
text: "Linkbait!"
comments: "/posts/1/comments"
}
}
Then my adapter can retrieve /posts/1/comments on demand, which looks like this:
{
comments: [
{
id: 201,
text: "Nuh uh"
},
{
id: 202,
text: "Yeah huh"
},
{
id: 203,
text: "Nazi Germany"
}
]
}
Here is the code for the findHasMany method in my adapter:
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
var query = relationship.options.query ? relationship.options.query(record) : {};
this.ajax(url, "GET", {
data: query,
success: function(json) {
var serializer = this.get('serializer');
var pluralRoot = serializer.pluralize(root);
var hashes = json[pluralRoot]; //FIXME: Should call some serializer method to get this?
store.loadMany(type, hashes);
// add ids to record...
var ids = [];
var len = hashes.length;
for(var i = 0; i < len; i++){
ids.push(serializer.extractId(type, hashes[i]));
}
store.loadHasMany(record, relationship.key, ids);
}
});
}
Solution
Override the DS.RelationshipChange.getByReference method by inserting the following code into your app:
DS.RelationshipChange.prototype.getByReference = function(reference) {
var store = this.store;
// return null or undefined if the original reference was null or undefined
if (!reference) { return reference; }
if (reference.record) {
return reference.record;
}
return store.materializeRecord(reference);
};
Yes, this is overriding a private, internal method in Ember Data. Yes, it may break at any time with any update. I'm pretty sure this is a bug in Ember Data, but I'm not 100% certain this is the right solution. But it does solve this problem, and possibly other relationship-related problems.
This fix is designed to be applied to Ember Data master as of 29 Apr 2013.
Reason
DS.Store.loadHasMany calls DS.Model.hasManyDidChange, which retrieves references for all the child records and then sets the hasMany's content to the array of references. This kicks off a chain of observers., eventually calling DS.ManyArray.arrayContentDidChange, in which the first line is this._super.apply(this, arguments);, calling the superclass method Ember.Array.arrayContentDidChange. That Ember.Array method includes an optimization that caches the first and last object in the array and calls objectAt on only those two array members. So there's the part that singles out the first and last record.
Next, since DS.RecordArray implements an objectAtContent method (from Ember.ArrayProxy), the objectAtContent implementation calls DS.Store.recordForReference, which in turn calls DS.Store.materializeRecord. This last function adds a record property to the reference that is passed in as a side effect.
Now we get to what I think is a bug. In DS.ManyArray.arrayContentDidChange, after calling the superclass method, it loops through all the new references and creates a DS.RelationshipChangeAdd instance that encapsulates the owner and child record references. But the first line inside the loop is:
var reference = get(this, 'content').objectAt(i);
Unlike what happens above to the first and last record, this calls objectAt directly on the Ember.NativeArray and bypasses the ArrayProxy methods including the objectAtContent hook, which means that DS.Store.materializeRecord--which adds the record property on the reference object--may have never been called on some references.
Next, the relationship changes created in the loop are immediately afterward (in the same run loop) applied with this call tree: DS.RelationshipChangeAdd.sync -> DS.RelationshipChange.getFirstRecord -> DS.RelationshipChange.getByReference. This last method expects the reference object to have a record property. However, the record property is only set on the first and last reference objects, for reasons explained above. Therefore, for all but the first and last records, the relationship fails to be established because it doesn't have access to the child record object!
The above fix calls DS.Store.materializeRecord whenever the record property doesn't exist on the reference. The last line in the function is the only thing added. On the one hand, it looks like this was the original intention: that var store = this.store; line in the original declares a variable that isn't otherwise used in the function, so what's it there for? Also, without the added line, the function doesn't always return a value, which is a little unusual for a function which is expected to do so. On the other hand, this could lead to mass materialization in some cases where that would be undesirable (but, the relationships just won't work without it in some cases, it seems).
Possibly related
The "chain of observers" I mentioned takes a bit of an odd path. The initiating event was setting the content property on a DS.ManyArray, which extends Ember.ArrayProxy--therefore the content property has a dependent property arrangedContent. Importantly, the observers on arrangedContent are executed before observers on content are executed (see Ember.propertyDidChange). However, the default implementation of Ember.ArrayProxy.arrangedContentArrayDidChange simply calls Ember.Array.arrayContentDidChange, which DS.ManyArray implements! The point being, this looks like a recipe for some code to execute in an unintended order. That is, I think Ember.ManyArray.arrayContentDidChange may getting executed earlier than expected. If this is the case, the above mentioned code that expects the record property to already exist on all references may have been expecting this reasonably, as one of the observers directly on the content property may call DS.Store.materializeRecord on each reference. But I haven't dug deep enough to find out if this is true.