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
Related
Let's say I have an object
data class Person(
val name: String,
val surname: String,
val street: String,
val postalCode: String,
val telephoneNumber: String,
)
And then I have a list of persons :
val personsList = listOf(
Person(name="John", surname="Hams", street="Gariolg", postalCode="929429", telephoneNumer="+2142422422",),
Person(name="Karl", surname="Hamsteel", street="Gariolg", postalCode="124215", telephoneNumer="+3526522",),
Person(name="Stepf", surname="Hiol", street="Bubmp", postalCode="5342", telephoneNumer="+7574535",),
Person(name="Germa", surname="Foo", street="Hutioa", postalCode="235236", telephoneNumer="+112355",)
)
So now if the user types for instance Hams it should return John and Karl, because both have the word "Hams" inside the object. What I mean is doesn't matter if the user types postalCode, name, or whatever I'd like to loop throughout the object to check if there's any coincidence.
How i would do it, is create a function inside the data class, say, for example, like this. This will check if any field inside your data class matches with the given string.
In my example i check if whole string matches, but you can change this however you want. You probably want it.contains(searchString) inside the any block.
fun checkIfStringMatches(searchString: String) : Boolean =
setOf(this.name, this.surname, this.strees, this.postalCode, this.telephone).any { it == string }
Then, you can use this function on your list of persons to filter if any object matches your string search.
personList.filter{it.checkIfStringMatches(mySearchString)} // this will return a list with all the objects that match your search criteria
The problem is that if you add more fields, you will have to change this function and add it to the listOf() block. But i don't know any way to do this automatically without reflection, which is not really recommended to use. If you still want to use it, here is a question on this topic. Kotlin: Iterate over components of object
Try this, it will work.
personsList.filter { it.surname.startsWith("Hams") }.map {
Log.d("filter_name", it.name)
}
Hey You can apply filter method on list and grab the expected output like below :
val filtered = personsList.filter { it.toString().contains("Hams", true) }
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.
I am using a POJO data class with GSON to parse the data which is being called from the Firestore database.
For example, I have a POJO class with few non-nullable, and nullable values like userID
data class Users(id:String="", userID:String="" ...)
I am then using GSON to parse the JSON data to object for that class
val gson = Gson()
val jsonObjects = gson.toJson(querySnapshot.data)
val parseData = gson.fromJson(jsonObjects,Users::class.java)
My question is if someone uploads data in the database and forgets to add the userID (i.e. it is null), is there a way I can check if the User data class is valid when being parsed?
I am using a check like if(userID == ""){return false} . But as the number of non-nullable fields grows it gets tedious and there must be a better way to check this.
My question is if someone uploads data in the database and forgets to add the userID (i.e. it is null), is there a way I can check if the User data class is valid when being parsed?
If you don't want to have null values at all, why would you then let the user the possibility to provide incomplete data? The simplest solution would be to restrict the data that is added to the database. How? Simply by creating some constraints. For example, your users cannot get access to a feature in your app if they do not fill in all the required fields. That's an operation that is widely used.
If you already have null values, then checking against nullity is a solution you can go ahead with. In Kotlin, null safety can be handled in many ways, either by checking for null in conditions, using safe calls, elvis operator or even using the !! operator.
Maybe the extension method of Kotlin is an accepted approach.
Let me show a demo, I assume the class User looks like this:
data class User(
val id: String,
val userId: String? // nullable
)
Create a extension method User.isValid() to verify the fields you want.
fun User.isValid() = when {
userId.isNullOrEmpty() -> false
else -> true
}
The method isNullOrEmpty() is in stdlib of Kotlin. I test the code, it works fine.
val user1 = User("id 001", null)
val user2 = User("id 002", "userId 001")
println(user1.isValid()) //==> print false
println(user2.isValid()) //==> print true
Now, back to your worry point:
...But as the number of non nullable fields grow it gets tedious
I changed the class User
data class User(
val id: String,
val userId: String?,
val email: String?,
val name: String?
)
it means that when the fields of userId, email, name, any of them is null, the User is invalid.
Just add conditions in extention method, like this:
fun User.isValid() = when {
userId.isNullOrEmpty()
|| email.isNullOrEmpty()
|| name.isNullOrEmpty() -> false
else -> true
}
We just need to maintain the method of isValid.
conclusion
Kotlin Extension Method can be used in your case.
It's better don't use id="", also can create an extension method for this empty string if need.
fun String.Companion.empty() = ""
data class User(
val id: String,
val userId: String? = String.empty()
...
)
All the extension methods can be placed in a class, like UserExt.kt for easy maintaining.
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.
I have text values I retrieve from text inputs. I want to allow the user to not fill in these inputs. But if the user has not filled one or more values I want to display default values for these inputs.
I have a data class that looks something like this:
#Parcelize
data class Profile(
val firstName: String = "",
val lastName: String = "",
val description: String = "",
val imageUri: String = ""
) : Parcelable
On click I call a method from my ViewModel class and pass it the current input values which is then persisted using a Repository class:
viewModel.createProfile(
etFirstName.text.toString(),
etLastName.text.toString(),
etProfileDescription.text.toString(),
profileImageUri.toString()
)
// The createProfile function itself
fun createProfile(
firstName: String = "John",
lastName: String = "Doe",
description: String = "Default Description",
imageUri: String = ""
) {
val profile = Profile(firstName, lastName, description, imageUri)
// Persist data
}
In a another fragment I set some UI data using this persisted data like this:
private fun observeProfile() {
viewModel.getProfile()
viewModel.profile.observe(viewLifecycleOwner, Observer {
val profile = it
tvName.text = getString(R.string.profile_name, profile.firstName, profile.lastName)
tvDescription.text = profile.description
ivProfileImage.setImageURI(Uri.parse(profile.imageUri))
})
}
So currently createProfile expects 4 arguments. I'm able to pass less because I have optional parameters, but how can I conditionally pass arguments to createProfile based on if the value is non null or empty. I can of course create checks for each value, but what is the best way to approach this?
Update
I don't think I was clear enough in my original question, because I don't only pass values from text inputs to createProfile. profileImageUri is a class variable of type Uri? and is initially set to null. The user can select an image and this variable is updated with the image data. The reason I'm passing and storing the image data as a String is because all the profile data also gets persisted to Firestore so Strings are easier to work with.
Compared to your own answer, I'd create a helper function
fun CharSequence?.ifNullOrEmpty(default: String) = if (this.isNullOrEmpty()) default else this.toString()
And use it as
viewModel.createProfile(
etFirstName.text.ifNullOrEmpty("John"),
etLastName.text.ifNullOrEmpty("Doe"),
etProfileDescription.text.ifNullOrEmpty("Default Description"),
profileImageUri.ifNullOrEmpty("Default Uri")
)
EDIT: given the update, I'd consider
fun Any?.ifNullOrEmpty(default: String) =
if (this == null || (this is CharSequence && this.isEmpty()))
default
else
this.toString()
I have found a workaround.
Turns out it's possible to pass if-else statements as parameters, because if statements are expressions in Kotlin:
viewModel.createProfile(
if (!etFirstName.text.isNullOrEmpty()) etFirstName.text.toString() else "John",
if (!etLastName.text.isNullOrEmpty()) etLastName.text.toString() else "Doe",
if (!etProfileDescription.text.isNullOrEmpty()) etProfileDescription.text.toString() else "Default Description",
if (profileImageUri != null) profileImageUri.toString() else ""
)
Using this approach I also don't have to set default values for my data class variables and for my createProfile function parameters.
I additionally added a check in my observeProfile function so if profileImageUri is null it won't try to set the image:
// ...
if (profile.imageUri.isNotEmpty()) {
ivProfileImage.setImageURI(Uri.parse(profile.imageUri))
}
// ...
My initial idea doesn't seem to be possible using a data class. It does seem to be possible using a regular class and varargsbut it has problems:
#Parcelize
class Profile(
vararg val params: String
) : Parcelable
...
val params = arrayOfValues.filter{ !it.isNullOrBlank() } // filter out all unwanted data
val profile = Profile(*params) // pass every param separately using spread operator
Main problem here is that the parameters themselves are obfuscated. I can still get the reference to individual parameters using an index and do stuff with them, but it doesn't work as nicely.
I think what you want to use is the Elvis Operator in Kotlin: ?:.
val test = exampleExpression ?: "alternative value"
If the expression to the left of ?: is not null, the elvis operator returns it, otherwise it returns the expression to the right. Note that the right-hand side expression is evaluated only if the left-hand side is null.
viewModel.createProfile(
etFirstName.text.toString() ?: "John",
etLastName.text.toString() ?: "Doe",
etProfileDescription.text.toString() ?: "Default Description",
profileImageUri.toString() ?: "Default Uri"
)