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"
)
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 want to reference an object within this class I have below:
class HerbData {
object Dill {
const val herbName: String = "This is Dill!"
const val scientificName: String = "Anethum Graveolens"
val dullThumbnail: Int = R.drawable.dill_thumbnail_attr
}
object Peppermint {
val herbName: String = "This is Peppermint!"
}
}
Is there anyway that I can reference the object by using a string in Kotlin? Here is somewhat what I mean:
HerbData."Dill".herbname
I can't find anything on this topic for Kotlin.
Another way you could do this is with an enum class. The advantage over a map is that you have a data structure you can reference directly in code, so you could use HerbData.Dill as well as HerbData["Dill"]. And that will enable you to take advantage of compile-time checking and lint warnings, refactoring, exhaustive pattern matching, code completion etc, because the data is defined in your code
enum class HerbData(
val herbName: String,
val scientificName: String? = null,
val dullThumbnail: Int? = null
) {
Dill("This is Dill!", "Anethum Graveolens", R.drawable.dill_thumbnail_attr),
Peppermint("This is Peppermint!");
companion object {
operator fun get(name: String): HerbData? =
try { valueOf(name) } catch(e: IllegalArgumentException) { null }
}
}
fun main() {
// no guarantee these lookups exist, need to null-check them
HerbData["Peppermint"]?.herbName.run(::println)
// case-sensitive so this fails
HerbData["peppermint"]?.herbName.run(::println)
// this name is defined in the type system though! No checking required
HerbData.Peppermint.herbName.run(::println)
}
>> This is Peppermint!
null
This is Peppermint!
Enum classes have that valueOf(String) method that lets you look up a constant by name, but it throws an exception if nothing matches. I added it as a get operator function on the class, so you can use the typical getter access like a map (e.g. HerbData["Dill"]). As an alternative, you could do something a bit neater:
companion object {
// storing all the enum constants for lookups
private val values = values()
operator fun get(name: String): HerbData? =
values.find() { it.name.equals(name, ignoreCase = true) }
}
You could tweak the efficiency on this (I'm just storing the result of values() since that call creates a new array each time) but it's pretty simple - you're just storing all the enum entries and creating a lookup based on the name. That lets you be a little smarter if you need to, like making the lookup case-insensitive (which may or may not be a good thing, depending on why you're doing this)
The advantage here is that you're generating the lookup automatically - if you ever refactor the name of an enum constant, the string label will always match it (which you can get from the enum constant itself using its name property). Any "Dill" strings in your code will stay as "Dill" of course - that's the limitation of using hardcoded string lookups
The question really is, why do you want to do this? If it's pure data where no items need to be explicitly referenced in code, and it's all looked up at runtime, you should probably use a data class and a map, or something along those lines. If you do need to reference them as objects within the code at compile time (and trying to use HerbData."Dill".herbName implies you do) then an enum is a fairly easy way to let you do both
Declare a Data Class
data class HerbData (
val scientificName: String,
val dullThumbnail: Int
)
Initialize a muteable map and put data in it
val herbData = mutableMapOf<String, HerbData>()
herbData.put("Dill", HerbData("Anethum Graveolens", R.drawable.dill_thumbnail_attr))
herbData.put("Peppermint", HerbData("Mentha piperita", R.drawable.peppermint_thumbnail_attr))
You can now just
herbData["Dill"]?.scientificName
class HerbData {
interface Herb {
val herbName: String
val scientificName: String
}
object Dill : Herb {
override val herbName: String = "This is Dill!"
override val scientificName: String = "Anethum Graveolens"
}
object Peppermint: Herb {
override val herbName: String = "This is Peppermint!"
override val scientificName: String = "Mentha piperita"
}
companion object {
operator fun get(name: String): Herb? {
return HerbData::class
.nestedClasses
.find { it.simpleName == name }
?.objectInstance as? Herb
}
}
}
println(HerbData["Dill"]?.herbName) // Prints: This is Dill!
println(HerbData["Peppermint"]?.scientificName) // Prints: Mentha piperita
println(HerbData["Pepper"]?.herbName) // Prints: null
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 would like to convert $document.data details to useful format so that I could use it for further applications. This is data from firestore documents.
private val mFireStore = FirebaseFirestore.getInstance()
mFireStore.collection("Users").whereEqualTo("lastName","H").whereEqualTo("firstName", "Uc").get()
.addOnSuccessListener{ documents ->
for(document in documents){
Log.d("TAG","${document.id}=>${document.data}")
Toast.makeText(applicationContext, "${document.id} => ${document.data}",
Toast.LENGTH_LONG).show()
}
}
.addOnFailureListener{exception ->
Log.w("TAG","Error getting documents:",exception)
Toast.makeText(applicationContext, "Failed",
Toast.LENGTH_LONG).show()
}
This is my code. Now when I run the code get this in the logcat
OL0rD4UfgHSh2K8UoTMnX6Xea6P2=>{lastName=H, image=, firstName=Uc, B=L, gender=, organization=, profileCompleted=0, mobile=0, blood group=o+, id=OL0rD4UfgHSh2K8UoTMnX6Xea6P2, email=jojoy09#gmail.com}
Now I want to convert this result to a useful format so that I could use it later. I wpuld like to convert the data so that I could load it in listview.
In the following for-loop:
for(document in documents) { ... }
The "document" object is of type DocumentSnapshot. When you call getData() on such an object, the type of object that is returned is a Map<String, Object>.
In Kotlin, this object is represented by Map<String, Any>. In order to get the data, you can simply iterate over the Map and get the data accordingly, using the following lines of code:
val map = document.data
for ((key, value) in map) {
Log.d("TAG", "$key = $value")
}
Or even simpler, using:
map.forEach { (key, value) -> Log.d("TAG", "$key = $value") }
However, if you only need, the value of a particular property, for example, the value of the email address, you can simply get it by using DocumentSnapshot#getString(String field) method:
val email = document.getString("email")
Log.d("TAG", email)
The result in the logcat will be:
jojoy09#gmail.com
.................
As I see in your screenshot, almost all of the properties are of type String. However, you can find different flavors for each type of field, like getLong(String field), getDouble(String field), getDate(String field), getTimestamp(String field), and so on.
Furthermore, if you need to get the entire document, and you want to convert it into an object of a specific class, as also #mehulbisht mentioned in his answer, you should use DocumentSnapshot#toObject(Class valueType). So assuming that you have a data class that looks like this:
data class User(
var email: String? = null,
var firstName: String? = null,
var lastName: String? = null,
//Other necessary fields
)
To convert the "document" object into an object of the "User" class, please use the following line of code:
val user = document.toObject(User::class.java)
Log.d("TAG", user.email)
The result in the logcat will be the same as above.
If you want to display a list of "User" objects in a ListView, then please check my answer from the following post:
What miss, with connect Firestore and ListView for random results in sample?
It's really simple to convert the Java code into Kotlin.
The Model that you used to set this data will be used here. You can convert the documents to your Model class using the .toObjects() method on it. Just use:
val myObjs = documents.toObjects(Model::class.java)
EDIT
For displaying this as a Log in Logcat use:
Log.d("myObjs ","""
$myObjs
""".trimIndent())
Do tell if this doesn't work for you :)
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