I am trying to define a Kotlin sealed class which consists of a number of data classes. The latter are used to define data transfer objects (DTO) representing the mySQL tables in a room database. I introduced the sealed class to generalize the different DTOs and be able to refer to them all by their supertype (DTO - the common properties each specific DTO has, eg. "id", etc.).
This compiles alright, but I don't think Kotlin understands that the data classes are the "subclasses" of the sealed class - no matter whether I defined them all in the same file as the sealed (parent) class, or - the preferred choice - in the same package... both options should be valid choices, according to the Kotlin documentation.
Any idea, where I'm going wrong here? Thanks.
Code:
package com.tanfra.shopmob.smob.data.local.dto
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.RewriteQueriesToDropUnusedColumns
import com.tanfra.shopmob.smob.data.local.utils.*
/**
* supertype, common to all DTO types - generic part of any DTO class
* (properties declared abstract --> implementation delegated to inheriting concrete class)
*/
sealed class Dto {
abstract val id: String
abstract var itemStatus: SmobItemStatus
abstract var itemPosition: Long
}
@Entity(tableName = "smobGroups")
@RewriteQueriesToDropUnusedColumns
data class SmobGroupDTO(
@PrimaryKey @ColumnInfo(name = "groupId") override val id: String = "invalid smob group entry",
@ColumnInfo(name = "groupItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
@ColumnInfo(name = "groupItemPosition") override var itemPosition: Long = -1L,
@ColumnInfo(name = "groupName") var name: String = "",
@ColumnInfo(name = "groupDescription") var description: String? = "",
@ColumnInfo(name = "groupType") var type: GroupType = GroupType.OTHER,
@ColumnInfo(name = "groupMembers") var members: List<String> = listOf(),
@ColumnInfo(name = "groupActivityDate") var activityDate: String = "",
@ColumnInfo(name = "groupActivityReps") var activityReps: Long = 0,
) : Dto()
@Entity(tableName = "smobLists")
@RewriteQueriesToDropUnusedColumns
data class SmobListDTO(
@PrimaryKey @ColumnInfo(name = "listId") override val id: String = "invalid smob list id",
@ColumnInfo(name = "listItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
@ColumnInfo(name = "listItemPosition") override var itemPosition: Long = -1L,
@ColumnInfo(name = "listName") var name: String = "",
@ColumnInfo(name = "listDescription") var description: String? = "",
@ColumnInfo(name = "listItems") var items: List<SmobListItem> = listOf(),
@ColumnInfo(name = "listMembers") var members: List<String> = listOf(),
@ColumnInfo(name = "listLifecycleStatus") var lcStatus: SmobItemStatus = SmobItemStatus.OPEN,
@ColumnInfo(name = "listLifecycleCompletion") var lcCompletion: Double = -1.0,
) : Dto()
@Entity(tableName = "smobProducts")
@RewriteQueriesToDropUnusedColumns
data class SmobProductDTO(
@PrimaryKey @ColumnInfo(name = "productId") override val id: String = "invalid smob product id",
@ColumnInfo(name = "productItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
@ColumnInfo(name = "productItemPosition") override var itemPosition: Long = -1L,
@ColumnInfo(name = "productName") var name: String = "",
@ColumnInfo(name = "productDescription") var description: String? = "",
@ColumnInfo(name = "productImageUrl") var imageUrl: String? = "",
@ColumnInfo(name = "productCategoryMain") var categoryMain: ProductMainCategory = ProductMainCategory.OTHER,
@ColumnInfo(name = "productCategorySub") var categorySub: ProductSubCategory = ProductSubCategory.OTHER,
@ColumnInfo(name = "productActivityDate") var activityDate: String = "",
@ColumnInfo(name = "productActivityReps") var activityReps: Long = 0L,
@ColumnInfo(name = "productInShopCategory") var inShopCategory: ShopCategory = ShopCategory.OTHER,
@ColumnInfo(name = "productInShopName") var inShopName: String = "dummy shop",
@ColumnInfo(name = "productInShopLocation") var inShopLocation: ShopLocation = ShopLocation(0.0, 0.0),
) : Dto()
@Entity(tableName = "smobShops")
@RewriteQueriesToDropUnusedColumns
data class SmobShopDTO(
@PrimaryKey @ColumnInfo(name = "shopId") override val id: String = "invalid smob shop id",
@ColumnInfo(name = "shopItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
@ColumnInfo(name = "shopItemPosition") override var itemPosition: Long = -1L,
@ColumnInfo(name = "shopName") var name: String = "",
@ColumnInfo(name = "shopDescription") var description: String? = "",
@ColumnInfo(name = "shopImageUrl") var imageUrl: String? = "",
@ColumnInfo(name = "shopLocationLatitude") var locLat: Double = 0.0,
@ColumnInfo(name = "shopLocationLongitude") var locLong: Double = 0.0,
@ColumnInfo(name = "shopType") var type: ShopType = ShopType.INDIVIDUAL,
@ColumnInfo(name = "shopCategory") var category: ShopCategory = ShopCategory.OTHER,
@ColumnInfo(name = "shopBusiness") var business: List<String> = listOf()
) : Dto()
@Entity(tableName = "smobUsers")
@RewriteQueriesToDropUnusedColumns
data class SmobUserDTO(
@PrimaryKey @ColumnInfo(name = "userId") override val id: String = "invalid smob user id",
@ColumnInfo(name = "userItemStatus") override var itemStatus: SmobItemStatus = SmobItemStatus.NEW,
@ColumnInfo(name = "userItemPosition") override var itemPosition: Long = -1L,
@ColumnInfo(name = "userUsername") var username: String = "",
@ColumnInfo(name = "userName") var name: String = "",
@ColumnInfo(name = "userEmail") var email: String = "",
@ColumnInfo(name = "userImageUrl") var imageUrl: String? = ""
) : Dto()
The reason, I believe Kotlin didn't make the desired connection between the sealed class and the data classes (= subclasses) is that it still asks me for an "else" branch in "when" expressions which act upon the members of the sealed class:
package com.tanfra.shopmob.smob.data.net.nto2dto
import com.tanfra.shopmob.smob.data.local.dto.*
import com.tanfra.shopmob.smob.data.net.nto.*
import com.tanfra.shopmob.smob.data.repo.ato.Ato
// ATO --> DTO
fun <DTO: Dto, ATO: Ato> ATO._asDatabaseModel(d: DTO): DTO? {
return when (d) {
is SmobGroupDTO -> {
SmobGroupDTO(
id = (this as SmobGroupNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
type = this.type,
members = this.members,
activityDate = this.activity.date,
activityReps = this.activity.reps,
) as DTO
}
is SmobListDTO -> {
SmobListDTO(
id = (this as SmobListNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
items = this.items,
members = this.members,
lcStatus = this.lifecycle.status,
lcCompletion = this.lifecycle.completion,
) as DTO
}
is SmobProductDTO -> {
SmobProductDTO(
id = (this as SmobProductNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
imageUrl = this.imageUrl,
categoryMain = this.category.main,
categorySub = this.category.sub,
activityDate = this.activity.date,
activityReps = this.activity.reps,
inShopCategory = this.inShop.category,
inShopName = this.inShop.name,
inShopLocation = this.inShop.location,
) as DTO
}
is SmobShopDTO -> {
SmobShopDTO(
id = (this as SmobShopNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
name = this.name,
description = this.description,
imageUrl = this.imageUrl,
locLat = this.location.latitude,
locLong = this.location.longitude,
type = this.type,
category = this.category,
business = this.business,
) as DTO
}
is SmobUserDTO -> {
SmobUserDTO(
id = (this as SmobUserNTO).id,
itemStatus = this.itemStatus,
itemPosition = this.itemPosition,
username = this.username,
name = this.name,
email = this.email,
imageUrl = this.imageUrl,
) as DTO
}
else -> null
} // when(DTO) ... resolving generic type to concrete type
}
CodePudding user response:
It's caused by your use of generics on the method signature :
fun <DTO: Dto, ATO: Ato> ATO._asDatabaseModel(d: DTO): DTO?
There's a good thread on Reddit which is very like your example. See here:
https://www.reddit.com/r/Kotlin/comments/ei8zh5/kotlin_requires_else_branch_in_when_statement/
So, to solve your problem, just change the method signature to return a type of DTO
not DTO?
It's almost as if the compiler is forgetting that the DTO is a sealed class when you make it a generic parameter, so you need an exhaustive check.
As you as using is
in a when
statement Kotlin will smart cast the DTO to the right type anyway, so no need for the generic argument.
Here's a cut down example based on your code that works without the else:
package paul.sealed
sealed class DTO {
abstract val id: String
}
data class SmobGroupDTO(override val id: String = "invalid smob user id", val name: String = "") : DTO()
data class SmobListDTO(override val id: String = "invalid smob user id", val name: String = "") : DTO()
fun main() {
fun processDTO(dto: DTO): String {
return when (dto) {
is SmobGroupDTO -> "Group"
is SmobListDTO -> "List"
}
}
}