user15301088
user15301088

Reputation:

Kotlin error Smart cast to 'X' is impossible, because 'state' is a property that has open or custom getter when trying to observe state

I'm try to observe state as you see but when i use when and try to get data, compiler says Smart cast is impossible by casting it solves the problem but It felt like i'm doing it in wrong way, i want to know there is any other solution to fix this error.

sealed class Response<out T : Any> {
    object Loading : Response<Nothing>()
    data class Success<out T : Any>(val data: T) : Response<T>()
    data class Error(val error: ResultError, val message: String? = null) : Response<Nothing>()
}
val userState by userViewModel.userState.collectAsState()
when(userState){
    is Response.Error   -> userState.error // Smart cast to 'Response.Error' is impossible, because 'userState' is a property that has open or custom getter
    Response.Loading    -> Unit
    is Response.Success -> userState.data // Smart cast to 'Response.Success<User>' is impossible, because 'userState' is a property that has open or custom getter
}

Upvotes: 16

Views: 6560

Answers (3)

ianribas
ianribas

Reputation: 1356

This is one of the few examples where there is too much "magic".

Doing this without the delegate property (by) works as expected, allowing smart cast to work:

val userState = userViewModel.userState.collectAsState().value
when(userState){
    is Response.Error   -> userState.error
    Response.Loading    -> Unit
    is Response.Success -> userState.data
}

I think this is what Sweeper meant on the comments to the original question.

Upvotes: 2

Joffrey
Joffrey

Reputation: 37729

This line:

val userState by userViewModel.userState.collectAsState()

Defines userState through a delegate, so the compiler cannot guarantee that subsequent reads of the property's value will give the same value. In particular here, it means the access in the when() condition and the access within the when's branches might not return the same value from the compiler's point of view, thus it cannot smart cast.

You could use an intermediate variable here:

val userState by userViewModel.userState.collectAsState()
when(val s = userState){
    is Response.Error   -> s.error
    Response.Loading    -> Unit
    is Response.Success -> s.data
}

Now since s is a local val the compiler can guarantee it will have the same value in the condition and in the when branches, and smart casting works

Upvotes: 36

broot
broot

Reputation: 28402

Compiler can only perform smart casts when it can guarantee that the value won't change with time. Otherwise, we might get into the situation where after the type check the variable changed to another value and does no longer satisfy the previous constraint.

Delegated properties (ones declared with by keyword) are much different than "normal" variables. They don't really hold any value, but each time we access them, we actually invoke getValue() (or setValue()) on their delegate. With each access the delegate may provide a different value. Compiler can't guarantee immutability of the value and therefore smart casts are disallowed.

To fix this problem, we need to create a local copy of the data that is delegated. This is like invoking getValue() and storing the result as a local variable, so it can no longer change. Then we can perform smart casts on this local data copy. It can be understood better with the following example:

fun main() {
    val delegated by Delegate()

    println(delegated) // 0
    println(delegated) // 1
    println(delegated) // 2

    val local = delegated // `local` set to 3

    println(local) // 3
    println(delegated) // 4
    println(local) // 3
}

class Delegate {
    var i = 0

    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return i++
    }
}

Each time we access delegated it returns a different value. It may change between null and not null or even change the type entirely. When we assign it to local we take "current" value of delegated and store its copy locally. Then delegated still changes with each access, but local is constant, so we can perform smart casts on it.

Depending on your case, if there is a way to acquire "current" or "direct" value of userViewModel.userState.collectAsState() then you can use it when assigning to userState - then it should work as you expect. If there is no such function, then I think the easiest is to use another variable to store a local copy, like this:

val _userState by userViewModel.userState.collectAsState() // delegated
val userState = _userState // local copy, immutable
when(userState){
    is Response.Error   -> userState.error // Smart cast to 'Response.Error' is impossible, because 'userState' is a property that has open or custom getter
    Response.Loading    -> Unit
    is Response.Success -> userState.data // Smart cast to 'Response.Success<User>' is impossible, because 'userState' is a property that has open or custom getter
}

Upvotes: 4

Related Questions