I'm trying to factor out a base implementation of ...
# getting-started
d
I'm trying to factor out a base implementation of ViewModel for use with a state machine, but I'm having problems with generics and smart casting. More info in the thread...
Copy code
abstract class VMStateMachine<S: Any, A: Any, E: Any>(
    initialState: S
) : FlowReduxStateMachine<S, A>(initialState) {
    protected val _effects = MutableSharedFlow<AppSideEffects>()
    val effects = _effects.asSharedFlow()
}

abstract class ComposeViewModelBase<S: Any, A: Any, E: Any> : ViewModel() {
    abstract val stateMachine: VMStateMachine<S, A, E>

    val uiState: StateFlow<S> by lazy {
        stateMachine.state.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            ExternalAppsUiState2.Loading
        ) as StateFlow<S>
    }

    val effects by lazy { stateMachine.effects }

    fun dispatch(event: A) {
        viewModelScope.launch {
            stateMachine.dispatch(event)
        }
    }
}
Why do I need to cast uiState
as StateFlow
if
stateMaching.state
is a Flow<S>?
And when I try using it in a compose view:
Copy code
val uiState by vm.uiState.collectAsState()

    when(uiState) {
        is ApplicationDetailsState.Loading -> {}
        is ApplicationDetailsState.ShowContent -> {
              // Here uiState is NOT smart casted to ShowContent... why?
y
Sorry I haven't done Andriod in a while. What type appears if you Ctrl+Shift+P on
Copy code
stateMachine.state.stateIn
👍🏼 1
I think for your second issue, it's because Kotlin can't trust that
uiState
will return the same object, so you need to do something like:
Copy code
val uiState by vm.uiState.collectAsState()

    when(val uiState = uiState) {
        is ApplicationDetailsState.Loading -> {}
        is ApplicationDetailsState.ShowContent -> {
              // Here uiState is smart casted to ShowContent...
d
Ok, first problem solved, I accidentally use another type for the initial value of my state flow... For the second issue, it's funny, because in some places it manages to smart cast... is it because of my by lazy in the base class?
Oh, thanks! By using the
val uiState
in the
when
it managed to smart cast that at least!
y
I don't think it's because of the
by lazy
. I believe it's because Compose's
State.getValue
could theoretically return a different value between your when branch condition and inside the when block
Your local
uiState
is delegated, and so it has a custom getter, and hence there's no guarantees that the object won't change between different calls to
State.getValue
d
It doesn't like the shadowing, and naming it otherwise isn't too nice, but at least I don't have to cast it...
Your local
uiState
is delegated, and so it has a custom getter,
But it's a
val
?
and
lazy
can't change the instance
If it would be my own
getValue
, I guess it COULD change though... so the
by
IS the problem... just not necessarily the
lazy
?
y
uiState is a flow that is being collected by compose as a
State
. What the compiler sees is that yes you have one singular
State
instance that you've defined, the issue is that your local variable is delegating to
State
, and the Kotlin compiler doesn't know the intrinsic mechanics of
State
, so to it, it thinks that
State
might change the value it's returning between calls. Let me stress this enough: every time you write
uiState
, that's replaced by a call to
State.getValue
. Every time. And so there is no telling what side effects can change the value returned by that function. If I understand correctly, the way that compose works is that it will cancel your composition if the value changes and will run it again from the beginning, but the compiler isn't aware of that. If that's how it works, then rerhaps the Compose compiler plugin can somehow trick the compiler into allowing smart casts there
d
Copy code
@Stable
    val uiState: StateFlow<S> by lazy {
        stateMachine.state.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            stateMachine.initialState
        )
    }
doesn't seem to work
I think the
@Stable
tells the compose plugin that it's immutable?
But I'm not sure...
y
Btw, you can also just do
Copy code
val uiState = vm.uiState.collectAsState().value
But that means your entire composable would be recomposed when uiState changes. The upside of the
by
approach is that only the places that directly use
uiState
would get recomposed
d
I think I'm starting to understand... you're talking about the
val uiState by vm.uiState.collectAsState()
... not mine in the view model... (maybe it would also cause these problems, but the one in the screen causes them first...)
y
Yes, I'm talking about that local one, not the view model one
d
Wow, thanks for your explanation... now I'm realising that it was happening in other places, and I guess now I'll know why and what I can do about it 👍🏼!