Search code examples
androidkotlinenumssealed-class

How can I use sealed classes to describe a finite set of cases with associated values, and a smaller set of such values?


I am looking at using sealed class to represent a finite set of possible values.

This is part of a codegeneration project that will write a very large number of such classes, which can each have a lot of cases. I am therefore concerned about app size. As it is very likely that several cases can have the same attributes, I am looking at using wrappers such as:

data class Foo(val title: String, ...lot of other attributes)
data class Bar(val id: Int, ...lot of other attributes)

sealed class ContentType {
    class Case1(val value: Foo) : ContentType()
    class Case2(val value: Bar) : ContentType()

    // try to reduce app size by reusing the existing type,
    // while preserving the semantic of a different case
    class Case3(val value: Bar) : ContentType()
}

fun main() {
    val content: ContentType = ContentType.Case1(Foo("hello"))
    when(content) {
        is ContentType.Case1 -> println(content.value.title)
        is ContentType.Case2 -> println(content.value.id)
        is ContentType.Case3 -> println(content.value.id)
    }
}

Is this how I should approach this problem?

If so, how can I best make the properties of the associated value accessible from the sealed class? So that

        is ContentType.Case2 -> println(content.value.id)

becomes

        is ContentType.Case2 -> println(content.id)

Solution

  • Is this how I should approach this problem?

    IMHO, yes, but with some semantic changes, listed after.

    How can I best make the properties of the associated value accessible from the sealed class?

    You can generate extensions or instance functions for each subclass.

    e.g.

    val ContentType.Case2.id: String get() = value.id
    

    In this way, you can successfully call:

    is ContentType.Case2 -> println(content.id)
    

    How can I reduce the app size while preserving the semantic of another case?

    You can do it generating only one class for all the cases which need the same types as parameters and using Kotlin contracts to handle them.

    Taking your example, you could generate:

    sealed class ContentType {
        class Case1(val value: Foo) : ContentType()
        class Case2_3(val value: Bar, val caseSuffix: Int) : ContentType()
    }
    

    As you can see, the classes Case2 and Case3 are now only one class and caseSuffix identifies which one of them it is.

    You can now generate the following extensions (one for each case):

    @OptIn(ExperimentalContracts::class)
    fun ContentType.isCase1(): Boolean {
        contract {
            returns(true) implies (this@isCase1 is ContentType.Case1)
        }
        return this is ContentType.Case1
    }
    
    @OptIn(ExperimentalContracts::class)
    fun ContentType.isCase2(): Boolean {
        contract {
            returns(true) implies (this@isCase2 is ContentType.Case2_3)
        }
        return this is ContentType.Case2_3 && caseSuffix == 2
    }
    
    @OptIn(ExperimentalContracts::class)
    fun ContentType.isCase3(): Boolean {
        contract {
            returns(true) implies (this@isCase3 is ContentType.Case2_3)
        }
        return this is ContentType.Case2_3 && caseSuffix == 3
    }
    

    Since you are using contracts the client can now use them with:

    when {
        content.isCase1() -> println(content.title)
        content.isCase2() -> println(content.id)
        content.isCase3() -> println(content.id)
    }
    

    As you can see, a further optimization could be removing the property caseSuffix for cases with only one suffix to avoid unnecessary properties.