Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ViewModel State In BaseViewModel With Generic Type Compiles As Any #15

Open
KwabenBerko opened this issue Jan 23, 2023 · 9 comments
Open
Assignees

Comments

@KwabenBerko
Copy link

KwabenBerko commented Jan 23, 2023

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

@rickclephas
Copy link
Owner

rickclephas commented Jan 23, 2023

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 rickclephas self-assigned this Jan 23, 2023
@KwabenBerko
Copy link
Author

KwabenBerko commented Jan 23, 2023

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

@matthiaslao
Copy link

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.

@rickclephas
Copy link
Owner

rickclephas commented Sep 26, 2023

@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?

@matthiaslao
Copy link

@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
Copy link

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...

@rickclephas
Copy link
Owner

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.

@matthiaslao
Copy link

matthiaslao commented Sep 28, 2023

@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
Copy link
Owner

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants