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