At Blockchain, we recently updated our Android app to use more robust push notifications via FCM, notifying users of BTC received to their wallets.
As you might expect, we added a custom vibration pattern to our notifications, but this presented a slight problem. As it turns out, devices where Android ≤4.3 will throw an errant SecurityException: Requires VIBRATE permission
. This is easily fixed, but how do we do this in an idiomatic way?
One suggestion was to try/catch firing the notification — this is obviously quite nasty and is a raw deal for users on old devices. The simple way would be to this would be to turn the builder into a local val
and then simply amend the builder separately:
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | |
.setSmallIcon(icon) | |
// Excluded for brevity | |
.setContentText(text) | |
if (AndroidUtils.is19orHigher()) { | |
builder.setVibrate(longArrayOf(100)) | |
} else { | |
builder.setVibrate(longArrayOf()) | |
} |
This is absolutely fine but it breaks up the Builder pattern and we can do better. What if we could put this logic in-line?
Using extension functions, we can extend NotificationCompat.Builder
and create a new function that accepts a predicate, and calls either one function or another depending on the outcome. Taking advantage of function literals with receivers (ie, A.() -> A
), we can ensure that the functions to be triggered are both existing public methods of NotificationCompat.Builder
and return the NotificationCompat.Builder
object itself.
private fun NotificationCompat.Builder.ternaryBuilder( | |
predicate: () -> Boolean, | |
trueFunc: NotificationCompat.Builder.() -> NotificationCompat.Builder, | |
falseFunc: NotificationCompat.Builder.() -> NotificationCompat.Builder | |
): NotificationCompat.Builder = if (predicate()) this.trueFunc() else this.falseFunc() |
This allows us to do this:
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | |
.setSmallIcon(icon) | |
// Excluded for brevity | |
.ternaryBuilder( | |
{ AndroidUtils.is19orHigher() }, | |
{ setVibrate(longArrayOf(100)) }, | |
{ setVibrate(longArrayOf()) } | |
) | |
.setContentText(text) |
Much cleaner, and doesn’t break the Builder flow. This ternary function seems pretty useful, can we generify it for use with any Builder class? Of course:
fun <T> T.ternaryBuilder( | |
predicate: () -> Boolean, | |
trueFunc: T.() -> T, | |
falseFunc: T.() -> T | |
): T = if (predicate()) this.trueFunc() else this.falseFunc() |
So this worked great and I felt pretty good about it for about 15 minutes, and then I realised that infact, Kotlin already had me covered:
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | |
.setSmallIcon(icon) | |
// Excluded for brevity | |
.apply { | |
if (AndroidUtils.is19orHigher()) { | |
setVibrate(longArrayOf(100)) | |
} else { | |
setVibrate(longArrayOf()) | |
} | |
} | |
.setContentText(text) |
apply
is in-fact what exactly I was looking for, as this
is both it’s receiver and return type. Sheepishly, I corrected my fun-but-not-idiomatic code and pushed it to origin.
Still, this was a fun learning experience that I thought I’d share. Kotlin and it’s high-order functions, including those built-in, are incredibly powerful and I love finding new applications for them.
Top comments (1)
Ooohh, wow. It's very useful. I'll to update my code xD.
Thanks for sharing.