KMP-ObservableViewModel icon indicating copy to clipboard operation
KMP-ObservableViewModel copied to clipboard

ViewModel State In BaseViewModel With Generic Type Compiles As `Any`

Open KwabenBerko opened this issue 2 years ago • 9 comments

I'm not sure if this is an issue with Kotlin/Native itself but I have a BaseViewModel with generic type T which is used to construct a stateflow:

abstract class BaseViewModel<T: Any>(initialState: T) : KMMViewModel() {
    protected val scope = viewModelScope.coroutineScope
    private val _state = MutableStateFlow(viewModelScope, initialState)
    @NativeCoroutinesState val state = _state.asStateFlow()

    protected fun setState(newState: T) {
        _state.value = newState
    }
}

All viewmodels in the project subclass this like so:

class CurrenciesViewModel(
    private val getCurrencies: GetCurrencies
) : BaseViewModel<CurrenciesViewModel.State>(State.Idle) {

    init {
        loadCurrencies()
    }

    sealed class State {
        object Idle : State()
        data class Content(
            val currencies: Map<String, List<Currency>>,
        ) : State()
    }
}

When using this in Xcode, viewModel.state in Swift has a type of Any, which means i have to cast it:

viewModel.state as! CurrenciesViewModel.State

Any idea why? Thanks in advance

KwabenBerko avatar Jan 23 '23 18:01 KwabenBerko

This is an unfortunate limitation of the KMP-NativeCoroutines implementation combined with the Kotlin Objective-C interop. KMP-NativeCoroutines generates extension properties for properties annotated with @NativeCoroutinesState. Which will be a generic property for a generic class like your BaseViewModel. Objective-C doesn't support generic properties, which is why Kotlin uses the generic upper bound (in this case Any).

While it can't be completely fix, there might be a way to add the correct type on the Swift side again. I will do some experimenting and see if we could support such a case.

rickclephas avatar Jan 23 '23 19:01 rickclephas

Alright, that makes sense. Thank you Again, great library!

KwabenBerko avatar Jan 23 '23 21:01 KwabenBerko

I just had the same issue and realized that in Swift you have access to viewModel.state_ which is already cast to the right type and can use it instead of viewModel.state.

I don't know if it's new and I am not sure of the side effects in using this solution.

matthiaslao avatar Sep 26 '23 12:09 matthiaslao

@matthiaslao it sounds like you somehow have two properties with the name state (which results in one of them being suffixed with an underscore). Could you possibly share the relevant Kotlin code?

rickclephas avatar Sep 26 '23 12:09 rickclephas

@rickclephas Sure, here I kept only the relevant code.

abstract class BaseViewModel<State : Reducer.ViewState> : KMMViewModel() {
    @NativeCoroutinesState
    abstract val state: StateFlow<State>
}
class Store<State : Reducer.ViewState>(
    viewModelScope: ViewModelScope,
    initialState: State,
) {
    private val _state: MutableStateFlow<State> = MutableStateFlow(viewModelScope, initialState)
    val state: StateFlow<State>
        get() = _state.asStateFlow()
}
class MovieDetailViewModel : BaseViewModel<MovieDetailViewState>() {

    private val store = Store(
        viewModelScope = viewModelScope,
        initialState = MovieDetailViewState.initial()
    )

    @NativeCoroutinesState
    override val state: StateFlow<MovieDetailViewState>
        get() = store.state
}

matthiaslao avatar Sep 28 '23 13:09 matthiaslao

Ok, I realized that I have set @NativeCoroutinesState on both BaseViewModel and my MovieDetailViewModel, which creates me a viewModel.state and viewModel.state_ randomly with no possibility to distinguish them...

matthiaslao avatar Sep 28 '23 13:09 matthiaslao

Ok, I realized that I have set @NativeCoroutinesState on both BaseViewModel and my MovieDetailViewModel, which creates me a viewModel.state and viewModel.state_ randomly with no possibility to distinguish them...

@matthiaslao that's correct. You can either specify the name explicitly with e.g. @ObjCName("baseState"). Or you can drop the @NativeCoroutinesState annotation on the base property. Which also solves the generics issue since the subclasses aren't generic.

rickclephas avatar Sep 28 '23 14:09 rickclephas

@rickclephas I cannot drop the @NativeCoroutinesState on the base property, I am getting this error on the implementation if I do so

Refined declaration "state" overrides declarations with different or no refinement from BaseViewModel

I found another workaround by putting the @NativeCoroutinesState on an interface instead

abstract class BaseViewModel<State : Reducer.ViewState> : KMMViewModel(), ViewModelInterface<State>

interface ViewModelInterface<State : Reducer.ViewState> {
    @NativeCoroutinesState // if deleted here, we have a compilation error
    val state: StateFlow<State>
}

and we have to leave it also in the implementation

class MovieDetailViewModel : BaseViewModel<MovieDetailViewState>() {

    private val store = Store(
        viewModelScope = viewModelScope,
        initialState = MovieDetailViewState.initial()
    )

    @NativeCoroutinesState
    override val state: StateFlow<MovieDetailViewState>
        get() = store.state
}

In that case we won't have any duplicated state anymore and we can have a viewModel.state of the good type on iOS!

Thank you a lot for your help and the awesome library!

matthiaslao avatar Sep 28 '23 14:09 matthiaslao

@rickclephas I cannot drop the @NativeCoroutinesState on the base property, I am getting this error on the implementation if I do so

Ah you are right. You can satisfy this requirement with the @HiddenFromObjC annotation. Using that instead of one of the KMP-NativeCoroutines annotations will hide the declaration, but won't generate an extension property.

I found another workaround by putting the @NativeCoroutinesState on an interface instead In that case we won't have any duplicated state anymore and we can have a viewModel.state of the good type on iOS!

Correct, although that is only a side effect of the Kotlin-ObjC interop regarding interfaces. It is still creating an extension property/function. It's just not directly accessible (in Swift) on the view model class.

rickclephas avatar Sep 28 '23 18:09 rickclephas