Issue
I have two similar data class
objects Post
and Review
, which both extend my BasicListItem
class. They also share a view, where their data is being processed and transformed to readable text and images. The idea is fine, but the execution is where I'm struggling with. Let's pick the method for setting up the item view for example:
fun setupListItemViews(item: BasicListItem) { //here I pass either the Post or Review item I've received from my backend
//option #1
var post: Post? = null
var review: Review? = null
if (isPost(item)) {
post = item as Post
} else {
review = item as Review
}
val data = post ?: review as Review
//this makes "data" a BasicListItem object and I can't access "title" and "description"
//parameters of both Post and Review classes
item_title.text = data.title //Unresolved reference: title
item_descr.text = data.description //Unresolved reference: description
...
//option #2
//this way I can handle only the cases where my item is of type Post
//which crashes when I pass an item of type Review, since it's impossible
//to cast Post to Review
item_title.text = (item as Post).title
item_descr.text = (item as Post).description
...
So what exactly should be the way of differentiating the type of item
if I want to access its parameters?
Edit: This method is not the only place where I need to know the exact type of the method parameter. There are 4 more methods and duplicating code is not an option. I am looking for a clean solution, if such is possible.
Solution
To answer the title of your question, the documentation goes into detail about how to cast between types in Kotlin. To demonstrate, I'll clean up your options.
Example data structure
Here is the data structure I am using in these examples
open class BasicListItem
data class Post(
val title: String,
val description: String,
val comments: List<String>,
) : BasicListItem()
data class Review(
val title: String,
val description: String,
val score: Int,
) : BasicListItem()
As @mightyWOZ explains in their answer, using inheritance would help a lot. But it might not always be possible though - maybe it's external code that you can't change, or refactoring would be too much work.
Option 2 - cleaned up
I don't think option 2 is viable, so let's look at it first to get it out of the way.
Kotlin has safe casting with the as?
keyword. If the cast isn't possible, it returns null, and then we can use the Elvis operator to handle the nullable object.
fun setupListItemViews(item: BasicListItem) {
//option #2
val post: Post? = (item as? Post) // safe cast, returns null if item is NOT an instance of Post
val title = post?.title
val description = post?.description
println("$title - $description")
}
fun main() {
setupListItemViews(Post("title", "description", listOf("comment")))
// output: title - description
setupListItemViews(Review("title", "description", 10))
// output: null - null
}
In your case I don't think this is a suitable solution, it's too inflexible. Using smart casting and separating out logic into methods is better, which is what Option 1 can do.
Option 1 - cleaned up
Here's a cleaned up version of option 1, using smart casts in a when
statement.
// here I pass either the Post or Review item I've received from my backend
fun setupListItemViews(item: BasicListItem) {
//option #1
when (item) {
is Post -> handlePost(item) // smart cast item to Post
is Review -> handleReview(item) // smart cast item to Review
else ->
throw IllegalStateException("unknown item ${item::class} - can't map to ListItemViewDto")
}
}
// separate the handling of posts and reviews into separate methods
private fun handlePost(post: Post) {
println("Handling Post: ${post.title} - ${post.description} - ${post.comments}")
}
private fun handleReview(review: Review) {
println("Handling Review: ${review.title} - ${review.description} - ${review.score}")
}
Note: the
else -> throw
can be eliminated by using sealed classes
I think this is the quickest and cleanest option for you - though usually it's best to keep the frontend and backend completely separate (search for 'Clean Architecture' or read 'multitier architecture' for more details).
At the moment both frontend and backend have a hard dependency on the Post
and Review
DTOs. What happens if you want to change a database field? Well, that means updating all usages of Post
or `Review.
Let's make a single class where we can specifically handover from the backend to the frontend.
Option 1 - mapper classes
First we need a frontend specific DTO:
/** A DTO to hold data specific to the ListItemView */
data class ListItemViewDto(
val item_title: String,
val item_descr: String,
)
Next we make a mapper. This should be the only point in which the backend and frontend meet. We're using smart casting again to handle any type of BasicListItem
, and separate methods to create instances of ListItemViewDto
.
class ListItemViewMapper {
/** Map from [BasicListItem] to DTO specifically for [ListItemView] */
fun fromBasicItem(basicListItem: BasicListItem) = when (basicListItem) {
is Post -> mapPost(basicListItem)
is Review -> mapReview(basicListItem)
else ->
throw IllegalStateException("unknown item ${basicListItem::class} - can't map to ListItemViewDto")
}
private fun mapPost(post: Post): ListItemViewDto {
return ListItemViewDto(
post.title,
post.description
)
}
private fun mapReview(review: Review): ListItemViewDto {
return ListItemViewDto(
review.title,
review.description
)
}
}
Now before the frontend view is created, we can just use the mapper to covert from the database DTO to the frontend DTO.
class ListItemSetup {
private val mapper: ListItemViewMapper = ListItemViewMapper()
fun setupListItemViews(basicListItem: BasicListItem) {
val listItemViewDto: ListItemViewDto = mapper.fromBasicItem(basicListItem)
println("listItemViewDto: ${listItemViewDto.item_title} / ${listItemViewDto.item_descr}")
}
}
Answered By - aSemy
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.