Praveen P.
Praveen P.

Reputation: 1106

Nested safe call Null check Kotlin

I need to display an image in an ImageView, so I need to make sure that the image url is not null first. Are these 3 options valid?

Answer data class

data class Answer(
    val id: Long?,
    val title: String?,
    val answerImage: AnswerImage?
) {
    data class AnswerImage(
        val x0: AnswerImageData?,
        val x1: AnswerImageData?,
        val x2: AnswerImageData?
    ) {
        data class AnswerImageData(
            val id: String?,
            val url: String?
        )
    }
}

Option 1

answer.answerImage?.let { answerImage ->
     answerImage.x0?.let { answerImageData ->
        answerImageData.url?.let {
             //display image
         }
      }
}

Option 2

if (answer.answerImage?.x0?.url != null)
{
   //display image
}

Option 3

answer.answerImage?.x0?.url?.let {
      //display image
}

Upvotes: 3

Views: 1642

Answers (4)

Mohsen
Mohsen

Reputation: 1389

All the answers above were great but I wanted to mention something. You declared your properties as nullable so I'm guessing you are getting them from somewhere else (from your data layer if you're familiar with clean architecture).

my recommendation is to create a domain model for your class and map the data to your domain model(which has non-null properties). this way you handle nulls in the mapper. it's cleaner and follows the separation of concerns and single responsibility principles.

interface Mapper<F, S> {
    fun firstToSecond(first: F): S
    fun secondToFirst(second: S): F
}
data class DataModel(
    val id: Long?,
    val title: String?,
    val answerImage: AnswerImage?
)

data class DomainModel(
    val id: Long,
    val title: String,
    val answerImage: AnswerImage
)
class DataToDomainMapper: Mapper<DataModel, DomainModel> {
    override fun firstToSecond(first: DataModel): DomainModel {
        return DomainModel(
            id = first.id ?: -1,
            title = first.title ?: "no title",
            answerImage = first.answerImage ?: AnswerImage()
        )
    }

    override fun secondToFirst(second: DomainModel): DataModel {
        return DataModel(
            id = second.id,
            title = second.title,
            answerImage = second.answerImage
        )
    }
}

this way you don't have to handle nulls anywhere else in your code. and for data validation, you can check the id not to be negative. I've shortened your models, but you get the idea

Upvotes: 1

cactustictacs
cactustictacs

Reputation: 19622

Ken covered the answers (they're all fine and do the same thing, the last one is how the language is designed to be used really, nice and neat!) but I wanted to touch on your actual data model.

First, you say you need to check that an AnswerImageData's url isn't null. But the only reason it could be null, is because you've explicitly made it nullable, with a String? type. Is an AnswerImageData with a null url ever valid? Or does it always need to have one? I'm guessing it does, and I'm guessing it always needs an id too - so just make them non-null!

data class AnswerImageData(
    val id: String,
    val url: String
)

Now all your AnswerImageData objects are guaranteed to have non-null values - they're all valid in that sense, it's baked into your design. So you don't need to null check them anymore!

And the same goes for your other classes - can you have an AnswerImage with null values? This might be a trickier one, let's assume there needs to always be at least one AnswerImageData in an AnswerImage - in which case you can make the first non-null, and the others optional:

data class AnswerImage(
    val x0: AnswerImageData,
    val x1: AnswerImageData?,
    val x2: AnswerImageData?
)

This isn't necessarily the best way to do this - I'd personally prefer a vararg parameter, or some kind of collection, so you can have an arbitrary number of AnswerImageDatas and do operations like .first(predicate) to loop over them all easily. But if you want exactly three slots, three parameters is a way to do it!

Same goes for Answer - I'm guessing that requires an id, title and answerImage - if so, don't let them be null. Enforce that valid structure through your types, it'll make your life a lot easier! And so will avoiding nullable types unless you actually need them!


I don't know if that applies to what you're doing, but it probably does, so it's worth mentioning. (This kind of thing is called *Domain-Driven Design if you want to look into it - basically enforcing the rules and structure of your business logic through the way you design your code, your types, your objects etc.)

Upvotes: 2

Nicola Gallazzi
Nicola Gallazzi

Reputation: 8723

As specified by @Ken Van Hoeylandt all 3 options are valid, another valid option could be to use elvis operator:

fun attemptDisplayingImage(answer: Answer) {
    val answerImage = answer.answerImage ?: return
    val answerImageData = answerImage.x0 ?: return
    val answerImageDataUrl = answerImageData.url ?: return
    // display image
}

There's an interesting article about this topic here

Upvotes: 4

ByteWelder
ByteWelder

Reputation: 5614

Short answer: yes.

  • Option 1: Would only be a good choice if you actually need to do more things with answerImage and answerImageData rather than just cast it safely. In this specific case, we don't have a use for declaring those variables explicitly. To conclude: option 1 in this case is not a very neat solution, but it does work.

  • Option 2: should work, because all attributes are immutable. The compiler can then deduce on the next line (inside if scope), that the url property will still be non-null.

  • Option 3: this is in my opinion the best one: it's the easiest one to process as a reader of the code, as you would generally finish it with code like this: .let { safeUrl -> .. }.

Upvotes: 4

Related Questions