I’m trying to make a type to represent, let’s say, different image types in Android. Depending on how the image got to my function, it may be a URI to some resource on the device, a bitmap in memory, or an android drawable. (Note: this example is purely for explanatory purposes, I’m really thinking about a generic problem where something can be one of a discrete number of types. Don’t point out Android or bitmap-specific solutions!)
The way I’d normally write this is with something like:
sealed class UriOrBitmapOrDrawable {
data class Uri(val uri: Uri) : UriOrBitmapOrDrawable ()
data class Bitmap(val bitmap: Bitmap) : UriOrBitmapOrDrawable ()
data class Drawable(val drawable: Drawable) : UriOrBitmapOrDrawable ()
}
And then my function can be
fun displayImage(image: UriOrBitmapOrDrawable, view: ImageView) {
when(image) {
is UriOrBitmapOrDrawable.Uri -> displayUri(image.uri, view)
is UriOrBitmapOrDrawable.Bitmap -> displayBitmap(image.bitmap, view)
is UriOrBitmapOrDrawable.Drawable -> displayDrawable(image.drawable, view)
}
}
… or something like that.
However, in some places my code already knows for sure that it can’t be a Uri
, so any functions like the above really don’t need a branch for UriOrBitmapOrDrawable.Uri
. Moreover, in these places maybe a Uri can’t be processed. Keeping the branch for Uri
here would just be dangerous code, that I hope never gets hit. The easiest solution is to just do throw RuntimeException("unreachable")
in the Uri
branch and hope this is made rigorous by Unit Tests, but I’d really prefer that the compiler could pick up on this instead of relying on runtime exceptions.
One way to solve this is by using several sealed classes, with all the different combinations: UriOrBitmap
, UriOrDrawable
, BitmapOrDrawable
, as well as the UriOrBitmapOrDrawable
class above. This allows me to be completely prescriptive as to what is possible in all my interfaces - and avoids having to deal with impossible cases.
sealed class UriOrBitmap {
data class Uri(val uri: Uri) : UriOrBitmap ()
data class Bitmap(val bitmap: Bitmap) : UriOrBitmap ()
}
sealed class UriOrDrawable {
data class Uri(val uri: Uri) : UriOrDrawable ()
data class Drawable(val drawable: Drawable) : UriOrDrawable ()
}
sealed class BitmapOrDrawable {
data class Bitmap(val bitmap: Bitmap) : BitmapOrDrawable ()
data class Drawable(val drawable: Drawable) : BitmapOrDrawable ()
}
sealed class UriOrBitmapOrDrawable {
data class Uri(val uri: Uri) : UriOrBitmapOrDrawable ()
data class Bitmap(val bitmap: Bitmap) : UriOrBitmapOrDrawable ()
data class Drawable(val drawable: Drawable) : UriOrBitmapOrDrawable ()
}
fun getContactAvatar(contact: Contact, defaultAvatar: Drawable): UriOrDrawable {
// try to get the cached drawable first, else look up in the Android contacts, else use default
return getCachedAvatarDrawable(contact)?.let { UriOrDrawable.Drawable(it) }
?: lookupContactAvatarUri(contact)?.let { UriOrDrawable.Uri(it) }
?: UriOrDrawable.Drawable(defaultAvatar)
}
My question is: is there a nicer way to do this? A few things that would be nice, that we don’t get with the solution above:
-
UriOrDrawable
can be safely cast toUriOrBitmapOrDrawable
- This is clearly desirable, as if something is a
Uri
or aDrawable
, it’s certainly aUri
or aBitmap
or aDrawable
. - In the above,
UriOrDrawable
andUriOrBitmapOrDrawable
are both separate sealed classes. It seems difficult to see how I could makeUriOrDrawable
a subclass ofUriOrBitmapOrDrawable
, while also makingUriOrBitmap
a subclass of it. How would I represent aUri
?
- This is clearly desirable, as if something is a
-
Less bloat
- This clearly doesn’t scale very well for classes with four possibilities, or five.