I have a ViewModel class that looks like this:
class EditUserViewModel(
private val initUser: User,
) : ViewModel() {
private val _user = MutableLiveData(initUser)
val user: LiveData<User>
get() = _user
fun hasUserChanged() = initUser != _user.value
}
User can update some properties of the User data class instance through the UI.
To check if there are any changes when navigating from the fragment I use hasUserChanged method.
The problem is that is always false. I checked and it seems that the initialUser changes every time I change the _user MutableLiveData.
Why is that? Is the initial value of MutableLiveData passed by reference? I always thought that Kotlin is a "pass-by-value" type of language.
Update:
The problem seems to disappear when copying initUser before putting it inside the MutableLiveData.
private val _user = MutableLiveData(initUser.copy())
But it still doesn't make sense to me why I have to do that.
Kotlin is like java and they are pass-by-value. If you implement the equals function in User class, or make it as data class (which implements the equals function implicitly), it makes you sure that the content of the user objects is checked by != operator.
Update
If you are changing the value of LiveData directly, for example like this:
_user.value.name = "some name"
it means that you are changing the name property of the initUser, because _user.value exactly refers to the object that the initUser does. Consequently, the != operator always returns false, because we have one object with two references to it.
Now, when you are doing so:
private val _user = MutableLiveData(initUser.copy())
you are creating a deep copy of initUser (let's call it X) which is a new object in memory with the same property values of initUser.
Thus, by changing its properties like: _user.value.name = "some name", in fact, you are making this change on X, not initUser. It leads to preserving the initial values in initUser, meaning do not changing them, and solving the issue.
Related
I got a StateFlow of type UserStateModel (data class) in my app.
private val _userStateFlow: MutableStateFlow<UserStateModel?> = MutableStateFlow(UserStateModel())
val userStateFlow: StateFlow<UserStateModel?> = _userStateFlow
here is the UserStateModel
data class UserStateModel(
val uid: String? = null,
val username: String? = null,
val profileImageUrl: String? = null,
var isLoggedIn: Boolean = false,
val isPremiumUser: Boolean = false,
val posts: List<Post>? = listOf()
)
When I update the StateFlow with a new Username it emits the change to the collectors and the UI updates.
But when I change a property inside the posts: List? list it doesnt emit the changes.
When I change the size of the list it does, when I change the name property of the Post at index 0 it doesnt.
How can I detect changes to the child properties of the Data class?
Right now I use an ugly workaround, I add
val updateErrorWorkaround: Int = 0
to the UserStateModel data class and increase it by one so the collectors get notified
P.s I'm using MVVM + Clean Architecture and Jeptack Compose
EDIT
Thats my Post Model:
data class Post(
val id: Int,
val name: String,
val tags: MutableList<Tag>? = null
)
Here is how I update the MutableList:
val posts = userStateFlow.value?.posts
posts.get(index).tags?.add(myNewTag)
_userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Those changes are not emitted to the collectors
StateFlow emits only if it detects changes to the value, it ignores replacing the value with the same data. To do this, it compares the previous value with the new one. For this reason, we shouldn't modify the data that we already provided to the StateFlow, because it won't be able to detect changes.
For example, we set value to a User(name=John). Then we mutate the same user object by modifying its name to James and we set the value to this "new" user object. StateFlow compares "new" User(name=James) with its stored value, which is now also User(name=James), so it doesn't see any changes.
In your example you created a copy of UserStateModel, but inside you re-use the same objects and you mutate them. In this case you added a new item to tags and this change affected old UserStateModel as well, so StateFlow doesn't detect the change.
To fix the problem, you need to copy all the data that was changed and do not mutate anything in-place. It is safer to make all the data immutable, so val and List - this way you are forced to make copies. I changed tags to val tags: List<Tag> = listOf(), then your code could look like the following:
val posts = userStateFlow.value?.posts!!.toMutableList()
posts[index] = posts[index].copy(tags = posts[index].tags + myNewTag)
userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Here we create a copy of not only UserStateModel. We also copy posts list, the Post that we modify and we also copy the list of tags.
Alternatively, if this behavior of StateFlow is more annoying to you than helpful, you can use SharedFlow which doesn't compare values, but just emits.
context: data binding with a ViewModel, which gets data from a remote source in the form of JSON. I want to display a textual value from that JSON in a TextView, but if the data is absent in the JSON, I want to fall back to a string defined in strings.xml.
android:text="#{viewModel.theText}"
How I currently solved it is with a custom binding adapter that accepts an Any, and checks if the value is an Int or String:
app:anyText="#{viewModel.theText}". The viewModel has something like val theText = json.nullableString ?: R.string.placeholder.
I'm guessing that this is a problem more people deal with, and I was hoping if someone knows a more elegant solution.
You could provide Application context to your ViewModel or Resources and then do something like this:
val theText = json.nullableString ?: resources.getString(R.string.placeholder)
The other option would be keep using binding adapter like you do but I would wrap text input in another object like this:
data class TextWrapper(
val text: String?,
#StringRes val default: Int
)
#BindingAdapter("anyText")
fun TextView.setAnyText(textWrapper: TextWrapper) {
text = textWrapper.text ?: context.getString(textWrapper.default)
}
val theText = TextWrapper(text = json.nullableString, default = R.string.placeholder)
You do not need an adapter to handle this use Null coalescing operator operator ?? in xml.
Try below code:
android:text="#{viewModel.theText?? #string/your_default_text}"
Use case :
The null coalescing operator (??) chooses the left operand if it isn't null or the right if the former is null.
P.S: lean more about DB and expressions here-> https://developer.android.com/topic/libraries/data-binding/expressions
I have an app with multiple fragments in ViewPager.
There are RecyclerViews with data that may be repeated in other fragments.
So i decided to use one MutableLiveData for each unit (using a unique key),
without extra copy, in one repository
Data in livedata like that:
class Data {
var id = 0
var type = 0
var name = ""
var onlineStatus = OnlineStatus.GroupOrWithout
var icon: Bitmap: ?= null
}
map[data.id + data.type] = Data()
// when we need this data
fun getData(id: Int, type: Int) : MutableLiveData<Data>? {
val res = map[id + type]
if(res != null) {
return res
}
// or create new one if this fist time when we need it
....
}
But now i realized, that the traditional approach with LiveData limited to fragment's lifecycle.
has the advantage that is destroys unused data after the fragment is destroyed.
But this way looks more complicated to me, because events in another fragment
may be lost and then i will have not-synchronous data even of the same element.
So I was thinking, can I manually manipulate and destroy the MutableLiveData,
if it has no references from fragments?
Probably in the android is an analogue of shared_ptr from c++?
I really like current architecture but i don't know how to clean not used more LiveData
I am kind of new to Android. I can't figure this out. I want to create an object that is accessible from two different functions. Here is the object:
class Person(var firstName: String="", var lastName: String="", var order: List<Orders> )
class Order(var orderId: String="", var orderTitle: String="")
Then in an activity:
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var order: Order()
var person: Person(order) //I am sure I am not doing this right
}
fun Function1(){
person.order[1].orderTitle = "New Order" //to update order title
}
fun Function2(){
// to read new order title
var newOrderTitle = person.order[1].orderTitle
}
}
You created your Person instance as a local variable inside the onCreate() function, so it is only accessible inside the onCreate() function. To make it accessible from your other functions, it needs to be a property member of the class (defined outside any functions). You also need to use the = symbol to set the initial value. The : symbol is for declaring what type the property or variable is, and is optional in most cases.
By the way, in Kotlin, the convention is to always start function names with a lower-case letter, so it is easy to distinguish them from constructors. (This differs from languages like C#, where the new keyword makes constructor calls obvious.)
class MainActivity : AppCompatActivity(){
var order = Order()
var person = Person(order)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun function1(){
person.order[1].orderTitle = "New Order" //to update order title
}
fun function2(){
// to read new order title
var newOrderTitle = person.order[1].orderTitle
}
}
As well as what #TenFour04 says about making the variables visible to the functions, there's a couple of problems with how you're creating your Person object.
First, you're using default values for everything so you don't need to pass in a value for every parameter, right? That's how you can call Order() without providing any other data. But if you are passing in data, like with your Person(order) call, you need to tell it which parameter you're passing by using a named argument:
Person(order = order)
using the same name for the variable you're passing in and the name of the argument maybe makes it look more confusing, but you're specifically saying "the argument called order, here's a value for it".
You can pass in arguments without names, but you have to provide them in the order they're declared - so the 1st argument (a String), or the 1st and 2nd, or the 1st, 2nd and 3rd. Since you want to jump straight to the 3rd argument, you need to explicitly name it.
Second issue is your 3rd argument's type isn't Order, it's a List of orders. You can't just pass in one - so you need to wrap it in a list:
Person(order = listOf(order))
that's all you need to do!
The third problem is you've actually written the type as List<Orders> (sorry about the formatting). The type is Order, so we say List<Order> because it's a list holding objects of the Order type. You can use plurals in your variable names though (like val listOfOrders: List<Order>)
I've been using Kotlin to develop some apps in Android and what i want to do currently is to set a field value inside the defining class without calling the setter method.
Here is the code inside my class:
var projectList: List<Project>? = null
set(value) {
saveProjects(value as ArrayList<Project>)
field = value
}
//GO to the database and retrieve list of projects
fun loadProjects(callback: Project.OnProjectsLoadedListener) {
database.projectDao().getAll().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
callback.onProjectsLoaded(success)
//Here i don't want to save the projects, because i've loaded them from the database
this.projectList = success
},
{ error -> GenericErrorHandler.handleError(error,callback.retrieveContext())}
)
}
Does anybody know a way to do this without calling the set(value) method?
You can only gain access to the field directly from the setter. Inside a setter, the field can be accessed through the invisible field variable.
There are perhaps some other ways around your requirements though. Here are 2 examples. You wouldn't have to follow them exactly, but could instead also combine them to make whatever solution you want.
You could use another shell property to act as the setter for your actual property:
class Example1 {
var fieldProperty: Int = 0
var setterPropertyForField: Int
get() = fieldProperty
set(value) {
fieldProperty = value
}
}
You could use setters as you actually would in Java with a JVM field and a set method. The #JvmField is probably not necessary.
class Example2 {
#JvmField var fieldProperty: Int = 0
fun setField(value: Int) {
fieldProperty = value
}
}
You could probably access the field and change it through reflection, but I don't recommend that approach. That would likely only lead to problems.