KMP-ObservableViewModel
KMP-ObservableViewModel copied to clipboard
ViewModel State In BaseViewModel With Generic Type Compiles As `Any`
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
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.
Alright, that makes sense. Thank you Again, great library!
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 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 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
}
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...
Ok, I realized that I have set
@NativeCoroutinesStateon bothBaseViewModeland myMovieDetailViewModel, which creates me aviewModel.stateandviewModel.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
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!
@rickclephas I cannot drop the
@NativeCoroutinesStateon 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
@NativeCoroutinesStateon an interface instead In that case we won't have any duplicatedstateanymore and we can have aviewModel.stateof 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.