Home > Enterprise >  Sealed classes generics
Sealed classes generics

Time:10-21

I have this scenario where I have a super abstract class that emits different types of events using Kotlin sealed classes.

These events are modeled as follows.

sealed class BaseEvent {
    object ConnectionStarted : BaseEvent()
    object ConnectionStopped : BaseEvent()
}

sealed class LegacyEvent : BaseEvent() {
    object TextChanged : LegacyEvent()
    object TextCleared : LegacyEvent()
}

sealed class AdvancedEvent : BaseEvent() {
    object ButtonClick : AdvancedEvent()
    object ButtonLongClick : AdvancedEvent()
}

And here are the classes that emit these events

abstract class BaseViewModel<E : BaseEvent> {

    private fun startConnection() {
        emit(BaseEvent.ConnectionStarted) // <-- Error
    }

    fun emit(event: E){
        //...
    }
}

class LegacyBaskan : BaseViewModel<LegacyEvent>() {
    fun textChanged() {
        emit(LegacyEvent.TextChanged) // <-- Works
    }
}

class AdvancedBaskan : BaseViewModel<AdvancedEvent>() {
    fun buttonClicked() {
        emit(AdvancedEvent.ButtonClick) // <-- Works
    }
}

Here, it only works for the subclass and I can emit any event in the LegacyEvent or AdvancedEvent in their associated classes. However, for the BaseBaskan class, I can't emit the events from the BaseEvent although I stated that the generic type E must extend the BaseEvent.

I need each subclass to have access to its own events as well as the superclass events, but not the other subclasses' events.

How can I still emit events from BaseEvent in the base class, while giving each class the access to emit its own events only?

CodePudding user response:

Not sure if you're confused about why it's not letting you emit the item from the base class. Since E could be any subtype of BaseEvent, if your class could emit ConnectionStarted, then it would be violating its contract any time it is declared as a BaseViewModel<AnythingBesidesConnectionStarted>.

Only way I can think of to make this work is have both private and public versions of the emit function. You might have to change code elsewhere in your class that you haven't shown. If there's some function that returns E, you will have to change it so it returns BaseEvent.

abstract class BaseViewModel<E : BaseEvent> {

    private fun startConnection() {
        emitInternal(BaseEvent.ConnectionStarted)
    }

    private fun emitInternal(event: BaseEvent) {
      //...
    }

    fun emit(event: E){
        emitInternal(event)
    }
}

CodePudding user response:

You can't emit BaseEvent.ConnectionStarted in BaseViewModel (and other events as well) because E is not defined yet, so the type system can't be sure that you won't emit events of another subtype breaking generic type invariance.

Just add an overloaded private version, which accepts BaseEvent argument (you'll need some @JvmName annotation to make it compilable for JVM target):

abstract class BaseViewModel<E : BaseEvent> {
    private fun startConnection() {
        emit(BaseEvent.ConnectionStarted)
    }

    @JvmName("emitBaseEvent")
    private fun emit(event: BaseEvent) {
        //...
    }

    fun emit(event: E) {
        emit(event as BaseEvent)
    }
}

CodePudding user response:

It looks like you need contravariance, which can be achieved using in. Assuming your base class only has methods such as emit that use type E as parameter type, not as return type, then:

abstract class BaseViewModel<in E : BaseEvent> {

See https://kotlinlang.org/docs/generics.html#use-site-variance-type-projections.

  • Related