Expressing Relationships Between Nullable Parameters In Kotlin
Every time we declare a pair of nullable parameters:
fun plotOnMap(
location: LatLng?,
accuracyMeters: Double?
)
we allow 4 different combinations of parameter values:
accuracyMeters



null

Nonnull


location

null

✅  ✅ 
Nonnull

✅  ✅ 
Sometimes we know extra information about these parameters that makes one or more of these combinations impossible. We can write working code that accepts and ignores these invalid combinations^{1}, but that code will be prone to accidental misuse and misinterpretation in the future. This post demonstrates how to use Kotlin’s type system to eliminate these risks.
Example 1: “Either both parameters are nonnull or both parameters are null”
Original function
fun plotOnMap(
location: LatLng?,
accuracyMeters: Double?
)
Function context
We’re plotting a user’s location on a map. If we know the user’s location, we must always include a circle representing location accuracy. If we don’t know the user’s location, we plot nothing.
Valid parameter combinations
2 out of 4 combinations of parameter values are valid:
accuracyMeters



null

Nonnull


location

null

✅  🚫 
Nonnull

🚫  ✅ 
Improved function
We can express this relationship in our code by introducing a wrapper type:
data class LocationData(
val location: LatLng,
val accuracyMeters: Double
)
and updating our function signature to receive a nullable instance of that type:
fun plotOnMap(locationData: LocationData?)
Example 2: “If parameter one is null, so is parameter two”
This is an asymmetric version of example 1.
Original function
fun plotOnMap(
location: LatLng?,
accuracyMeters: Double?
)
Function context
We’re plotting a user’s location on a map. If we know the user’s location, we include a circle representing location accuracy if that accuracy is available. If we don’t know the user’s location, we plot nothing.
Valid parameter combinations
3 out of 4 combinations of parameter values are valid:
accuracyMeters



null

Nonnull


location

null

✅  🚫 
Nonnull

✅  ✅ 
Improved function
We can express this relationship in our code by introducing a wrapper type (note that accuracyMeters
is nullable this time):
data class LocationData(
val location: LatLng,
val accuracyMeters: Double?
)
and updating our function signature to receive a nullable instance of that type:
fun plotOnMap(locationData: LocationData?)
Example 3: “Exactly one parameter must be nonnull”
Original function
data class NewCreditCard(/*...*/)
data class SavedCreditCard(/*...*/)
fun chargeOrder(
newCreditCard: NewCreditCard?,
savedCreditCard: SavedCreditCard?
)
Function context
We’re placing an online order. An order should be charged to exactly one credit card.
Valid parameter combinations
2 out of 4 combinations of parameter values are valid:
savedCreditCard



null

Nonnull


newCreditCard

null

🚫  ✅ 
Nonnull

✅  🚫 
Improved function
We can express this relationship in our code by introducing a sealed type:
sealed class PaymentMethod {
data class NewCreditCard(/*...*/) : PaymentMethod()
data class SavedCreditCard(/*...*/) : PaymentMethod()
}
and updating our function signature to receive a nonnull
instance of that sealed type:
fun chargeOrder(paymentMethod: PaymentMethod)
In this example we’ve managed to remove nullability entirely!
Example 4: “At most one parameter is nonnull”
Original function
data class NewBankAccount(/*...*/)
data class NewCreditCard(/*...*/)
fun chargeOrder(
newBankAccount: NewBankAccount?,
newCreditCard: NewCreditCard?
)
Function context
We’re placing an online order. An order can be charged to a new bank account or a new credit card. If the user provides neither of these new payment methods, the order is charged to the user’s default saved payment method.
Valid parameter combinations
3 out of 4 combinations of parameter values are valid:
savedCreditCard



null

Nonnull


newCreditCard

null

✅  ✅ 
Nonnull

✅  🚫 
Improved function
We can express this relationship in our code by introducing a sealed type as in example 3:
sealed class NewPaymentMethod {
data class NewBankAccount(/*...*/) : NewPaymentMethod()
data class NewCreditCard(/*...*/) : NewPaymentMethod()
}
and updating our function signature to receive a nullable
instance of that sealed type:
fun chargeOrder(newPaymentMethod: NewPaymentMethod?)
Summary
Use Kotlin’s type system to make illegal parameter combinations impossible. You’ll thank yourself later.

Typically such code will deliberately include noops or theoretically throw a
NullPointerException
. ↩