How can I create an instance of InfoA that contains also title. Do I need to modify the classes?
Can't specify the title.
Also, do I need to create setters for it? To not access with the _
val info = InfoA(_subtitle = "SUBTITLE", title = ...)
open class Info(
open val action: Action = Action(),
open val title: String? = ""
) {
fun hasAction(): Boolean = action.hasAction()
}
class InfoA(
private val _subtitle: String? = "",
private val _image: String? = "",
private val _backgroundImage: String? = "",
private val _backgroundColor: String? = null,
private val _foregroundColor: String? = null,
private val _borderColor: String? = null
) : Info() {
val subtitle: String
get() = _subtitle.orEmpty()
val image: String
get() = _image.orEmpty()
val backgroundImage: String
get() = _backgroundImage.orEmpty()
val backgroundColor: Int?
get() = if (_backgroundColor != null) convertRgbStringToColorInt(_backgroundColor) else null
val foregroundColor: Int?
get() = if (_foregroundColor != null) convertRgbStringToColorInt(_foregroundColor) else null
val borderColor: Int?
get() = if (_borderColor != null) convertRgbStringToColorInt(_borderColor) else null
}
As the code is written, title is a val, so it can't be changed from its initial value — which is empty string if (as in the case of InfoA) something calls its constructor without specifying another value.
If it were changed to be a var, then it could be changed later, e.g.:
val info = InfoA(_subtitle = "SUBTITLE").apply{ title = "..." }
Alternatively, if you want to keep it a val, then InfoA would need to be changed: the most obvious way would be to add a title parameter in its constructor, and pass that up to Info:
class InfoA(
title: String? = "",
// …other fields…
) : Info(title = title) {
Note that this way, InfoA can never use Info's default value for title, so you may need to duplicate that default in InfoA's constructor.
The need to duplicate superclass properties in a subclass constructor is awkward, but there's currently no good way around it. (See e.g. this question.) If there are many parameters, you might consider bundling them together into a single data class, which could then be passed easily up to the superclass constructor — but of course users of the class would need to specify that. (Some people think that having more than a few parameters is a code smell, and that bundling them together can often improve the design.)
Related
I am trying to get all the "Keys" of annotations for use later, I am initializing the value like this:
private val wgKeys = SpecialRequestContext::class.members.associate { m ->
m.name to run {
val headerAnnotation = m.annotations.find { a -> a is Header } as? Header
headerAnnotation?.key
}
}
Unfortunately, the result is a Map with name for keys (correct), but all the values are null.
While debugging I see that m.annotations has no values.
Are annotations not available at this step?
Update: The minimum code to demonstrate this is here, unfortunately Kotlin playground cannot do reflection though:
#Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Header(val key: String)
data class SpecialRequestContext(
#Header("BK-Correlation-Id") val correlationId: String? = null,
#Header("BK-Origin") val origin: String? = null,
#Header("BK-Origin-City") val originCity: String? = null,
)
fun main() {
println(wgKeys.count())
println(wgKeys["origin"])
}
private val wgKeys = SpecialRequestContext::class.members.associate { m ->
m.name to run {
val headerAnnotation = m.annotations.find { a -> a is Header } as? Header
headerAnnotation?.key
}
}
Notice:
#Target(AnnotationTarget.VALUE_PARAMETER)
This means that the annotation is applied to the constructor parameters, not the properties. You won't find them on the properties.
To find them on the properties, you can change it to:
#Target(AnnotationTarget.PROPERTY)
If you need them applied to the parameters for some reason, you can find them like this:
private val wgKeys = SpecialRequestContext::class.primaryConstructor!!.parameters.associate { p ->
p.name to p.annotations.filterIsInstance<Header>().firstOrNull()?.key
}
I'm sure there's a better way of doing this, but basically you need to:
get all the annotation's properties
for each property, you need to reflectively get the value for that annotation instance (that's because 2 different classes could have the same annotation with different values).
A (rough) example:
annotation class Annotation1(val key1: String, val key2: Int)
annotation class Annotation2(val key3: String, val key4: Int)
#Annotation1(key1 = "value1", key2 = 2)
#Annotation2(key3 = "value3", key4 = 4)
class MyClass
fun main() {
val myClassAnnotations = MyClass::class.annotations
val propertiesByAnnotation =
myClassAnnotations.associateWith { (it.annotationClass as KClass<Annotation>).declaredMemberProperties }
val keyValuePerAnnotation = propertiesByAnnotation.map { (annotation, properties) ->
annotation.annotationClass.simpleName!! to properties.map { property ->
property.name to property.get(annotation)
}
}
println(keyValuePerAnnotation)
}
This prints a list of pairs, where the first item is the annotation name and the second item is a list of key-value pairs for each of the annotation's properties. The output for that example is:
[(Annotation1, [(key1, value1), (key2, 2)]), (Annotation2, [(key3, value3), (key4, 4)])]
The Code A is from offical sample code here.
The private val _uiState is val, in my mind, a val can be only assigned a object for one time.
It seems that _uiState.update { it.copy(loading = true) } shows _uiState is assigned to a new object again by update.
I don't understand why the author can reassign a new object to a val via update, could you tell me?
Code A
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
val loading: Boolean = false,
)
class InterestsViewModel(
private val interestsRepository: InterestsRepository
) : ViewModel() {
// UI state exposed to the UI
private val _uiState = MutableStateFlow(InterestsUiState(loading = true))
val uiState: StateFlow<InterestsUiState> = _uiState.asStateFlow()
private fun refreshAll() {
_uiState.update { it.copy(loading = true) }
...
}
...
}
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
val loading: Boolean = false,
)
/**
* Updates the [MutableStateFlow.value] atomically using the specified [function] of its value.
*
* [function] may be evaluated multiple times, if [value] is being concurrently updated.
*/
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
Added Content
To Chaoz: Thanks!
But all members in data class InterestsUiState(...val loading: Boolean = false)is val type, and you can't change any member vaule when you have created the object of InterestsUiState.
So I can't still understand why the member value of _uiState can be changed when the author launch _uiState.update { it.copy(loading = true) }.
And more
_uiState.update { it.copy(loading = true) } is equal to
_uiState.value = _uiState.value.copy(loading = true), right?
The val keyword only refers to which object the variable holds, not the data inside said object. For example:
class MyClass(var value: Int)
The following code is not allowed:
val obj = MyClass(5)
obj = MyClass(7) // compile error
because the val keyword refers to the variable itself being reassigned to a different object. This code, however, is allowed:
val obj = MyClass(5)
obj.value = 7
Here, obj is still the same object, only a property of said object changed value. In your provided code, the update function modifies data stored inside the _uiState object, however it does not swap it for a new object. This is important because:
var obj = MyClass(5)
val copy = obj
obj = MyClass(7)
println(copy.value) // prints 5
println(obj.value) // prints 7
When reassigning a variable, the old object remains, and any other variables referencing that object are not updated. In your case, _uiState.value is modified, not the variable itself. Hope this clears things up!
Edit:
Yes, it.copy() is an expression which creates a new object. However, this code is executed in the line _uiState.update { it.copy(loading = true) }, in the refreshAll() function. As it is the last statement in a lambda expression (also the only one, but doesn't matter), it is the return value of said lambda. Here we have to look at the declaration of the update function.
The lambda is stored in the function variable (of type (T)->T). This means, whenever function() is called, the code inside the lambda is executed, and its result (a new object) is then returned by the function() call. This value is assigned to the val nextValue variable and not to _uiState itself. The compareAndSet function modifies _uiState.value and does not change the object the _uiState variable references.
And by the way, the object returned by it.copy() is of type T, and not of type MutableStateFlow<T>, which your _uiState variable holds. T is the type of _uiState.value.
I am trying to find a solution for a nice kotlin data class solution. I have already this:
data class Object(
var classMember: Boolean,
var otherClassMember: Boolean,
var example: Int = 0) {
fun set(block: Object.() -> kotlin.Unit): Object {
val copiedObject = this.copy()
copiedObject.apply {
block()
}
return copiedObject
}
fun touch(block: Object.() -> kotlin.Unit): Object {
return this.set {
classMember = true
otherClassMember = false
block() }
}
}
val test = Object(true,true,1)
val changedTest = test.touch { example = 2 }
the result of this method is that the changedTest object has classMember = true, otherClassMember = false and example = 2
The problem with this solution is, the class properties are not immutable with var declaration. Does somebody have an idea how to optimize my methods to change var to val?
val says that a variable can't change it's value after initialization at the definition point. Kotlin's generated copy method does not modify an existing copy after construction: this method actually uses retrieved values from an object, replaces these values with ones that provided in copy method (if any), and after that just constructs a new object using these values.
So, it is not possible to perform such an optimization if you are going to change object's state after construction.
If I understood what you want correctly, you can do
data class Object(
val classMember: Boolean,
val otherClassMember: Boolean,
val example: Int = 0) {
fun touch(example: Int = this.example): Object {
return copy(
classMember = true,
otherClassMember = false,
example = example)
}
}
val test = Object(true,true,1)
val changedTest = test.touch(example = 2)
Though you need to repeat parameters other than classMember and otherClassMember but without reflection you can't do better.
Let's take the class of a data class:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups; List<String> = emptyList(),
val screenName: String = "new-user"
)
When calling this function from Kotlin, it is pretty straightforward. I can simply use the named-argument syntax to do so. Calling from Java, I have to specify all values, or use the #JvmOverloads annotation, which generates the following constructors (in addition to the constructor that kotlin generates with the bit-mask for default values):
User(int userNumber, #NotNull String name, #NotNull List userGroups,
#NotNull String screenName)
User(int userNumber, #NotNull String name, #NotNull List userGroups)
User(int userNumber, #NotNull String name)
User(#NotNull String name)
Now, if I want to create a User object in Java equivalent to User(name="John Doe", userGroups=listOf("admin", "super") I can't do it with the above constructors. I CAN however do it if I put val userNumber: Int = -1 at the end in the data class declaration (the generation of constructors seems to depend on the order the optional arguments are defined in). Which is fine, because expecting kotlin to generate all permutations is going to heavily bloat some classes.
The biggest problem that tools like Jackson simply don't work as they have no idea which constructor to use (and not like I can annotate one of the generated ones specially).
So, is there a way to generate a (single) constructor like:
User(Integer userNumber, String name, List<String> userGroups, String screenName) {
this.userNumber = (userNumber == null) ? -1 : userNumber;
this.userGroups = (userGroups == null) ? Collections.emptyList() : userGroups;
//...
}
Currently I am using the above approach, but manually defining the constructors where I need them.
EDIT
I should clarify, creating a similar constructor doesn't work, obviously because both the signatures would clash on the JVM. This is what it would like in my case:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups; List<String> = emptyList(),
val screenName: String = "new-user"
) {
companion object {
#JvmStatic
#JsonCreator
fun constructionSupport(
#JsonProperty("userNumber") userNumber : Int?,
#JsonProperty("name") name : String,
#JsonProperty("userGroups") userGroups : List<String>?,
#JsonProperty("screenName") screenName : String?
) = User(
userNumber = userNumber ?: -1,
name = name,
userGroups = userGroups ?: emptyList(),
screenName = screenName ?: "new-user"
)
}
}
Also note the redundancy where I have to write the default values for the properties twice. I Now that I look at it, I doubt there exists a solution for this. Maybe this is a good use-case for a kapt based side-project of mine :)
Better solution is to add possibility to library understand Kotlin functional. For example, for Jackson exists jackson-module-kotlin. With this library we can use default arguments in data classes.
Example:
data class User(
val userNumber: Int = -1,
val name: String,
val userGroups: List<String> = emptyList(),
val screenName: String = "new-user"
)
fun main(args: Array<String>) {
val objectMapper = ObjectMapper()
.registerModule(KotlinModule())
val testUser = User(userNumber = 5, name = "someName")
val stringUser = objectMapper.writeValueAsString(testUser)
println(stringUser)
val parsedUser = objectMapper.readValue<User>(stringUser)
println(parsedUser)
assert(testUser == parsedUser) {
println("something goes wrong")
}
}
After kicking this around for a minute, I think I found a solution that may work well here. Simply define a top level function in the same source file, that will build the object. Perhaps like so:
fun build_user(userNumber: Int?, name: String, userGroups: List<String>?, screenName: String?) : User {
return User(if(userNumber !== null) userNumber else -1, name, if(userGroups !== null) userGroups else emptyList(),
if(screenName !== null) screenName else "new-user")
}
Then when you need it, you simply call it from Java:
User user = UserKt.build_user(null, "Hello", null, "Porterhouse Steak");
System.out.println(user);
Output from the example:
User(userNumber=-1, name=Hello, userGroups=[], screenName=Porterhouse Steak)
The method is somewhere between a constructor and a builder. It beats hammering out a full-blown Builder object, and avoids cluttering your data class with unnecessary Java-interop glue code messiness.
See Package Level Functions for more information.
For example, if I have the following data class:
data class Data(
val name: String = "",
val number: Long = 0
)
And functions that can return null:
fun newName(): String? {}
fun newNumber(): Long? {}
I know I can use the following to use the value of the functions if they are not null:
val newName = newName()
val newNumber = newNumber()
val data = Data(
if (newName != null) newName else "",
if (newNumber != null) newNumber else 0
)
But is there a way to just use the default value specified in the constructor of the Data class when the values are null?
I could not find anything in the documentation, but I was hoping something like this would work:
val data = Data(newName()?, newNumber()?)
But that does not compile.
You can define a companion object for your data class and overload its invoke operator to use default values when null is passed:
data class Data private constructor(
val name: String,
val number: Long
) {
companion object {
operator fun invoke(
name: String? = null,
number: Long? = null
) = Data(
name ?: "",
number ?: 0
)
}
}
the secondary constructor only supports for the Nullable primitive properties. which means it will result in 2 same constructors if the property is not a primitive type, for example:
data class Data(val name: String) {
constructor(name: String? = null) : this(name ?: "foo");
// ^--- report constructor signature error
}
data class Data(val number: Long = 0) {
constructor(number: Long? = null) : this(number ?: 0)
// ^--- No problem since there are 2 constructors generated:
// Data(long number) and Data(java.lang.Long number)
}
an alternative way is using invoke operator for that, for example:
data class Data(val name: String) {
companion object {
operator fun invoke(name: String? = null) = Data(name ?: "")
}
}
IF the class is not a data class, then you can lazy initializing properties from parameters, rather than define properties on the primary constructor, for example:
class Data(name: String? = null, number: Long? = null) {
val name = name ?: ""
val number = number ?: 0
}
If needed, I can offer another solution:
data class Data(
val inputName: String?,
val inputNumber: Long?
) {
private val name = inputName ?: ""
private val number = inputNumber ?: 0
}