In advance of a new series of posts which start next week, I ported the code from a previous series of articles to Kotlin. I opted to port the code manually rather than rely on any automated conversions and there are a couple of things worthy of explanation hence this article.



Before we begin, I should point out that this isn’t going to be a full explanation of how all of the code works. Please refer to the original series for a full explanation of the code.

The first area worthy of discussion is where we obtain a Context which will store data to device encrypted storage, permitting our app to access it before the user has logged in to the device. To do this in Kotlin we can use an extension function:

Extensions.kt fun Context.safeContext(): Context = takeUnless { isDeviceProtectedStorage }?.run { it.applicationContext.let { ContextCompat.createDeviceProtectedStorageContext(it) ?: it } } ?: this 1 2 3 4 5 6 fun Context . safeContext ( ) : Context = takeUnless { isDeviceProtectedStorage } ? . run { it . applicationContext . let { ContextCompat . createDeviceProtectedStorageContext ( it ) ? : it } } ? : this

takeUnless will only execute the run block if the predicate (in this case isDeviceProtectedStorage ) evaluates to false. So we avoid repeating things if we already have the required device protected storage Context.

ContextCompat.createDeviceProtectedStorageContext() will return null on devices running Android lower than Nougat, so we use the elvis operator to ensure that we always return a valid Context – in this case it will be an application Context.

There’s nothing amazing happening here, but it gives us a really lightweight call to obtain a device protected storage Context without repeating the createDeviceProtectedStorageContext() call if we already have one.

The next trick is concerning SharedPreferences. A number of people have written about using Kotlin property delegates to really simplify accessing data which via SharedPreferences, so I claim absolutely no originality in the fundamental concept. However, I was not completely happy with any of the implementations that I found. Some required different functions to be called depending on the type of data being persisted to SharedPreferences (i.e. separate functions for Boolean, Int, Long, Float, and String values). Those which did not have this limitation generally determined the data type in the getValue() and setValue() functions of the ReadWriteProperty implementation.

I had a feeling that Kotlin had the capabilities to overcome these limitations, and came up with this approach:

SharedPreferenceDelegate.kt private class SharedPreferenceDelegate<T>( private val context: Context, private val defaultValue: T, private val getter: SharedPreferences.(String, T) -> T, private val setter: Editor.(String, T) -> Editor, private val key: String ) : ReadWriteProperty<Any, T> { private val safeContext: Context by lazyFast { context.safeContext() } private val sharedPreferences: SharedPreferences by lazyFast { PreferenceManager.getDefaultSharedPreferences(safeContext) } override fun getValue(thisRef: Any, property: KProperty<*>) = sharedPreferences .getter(key, defaultValue) override fun setValue(thisRef: Any, property: KProperty<*>, value: T) = sharedPreferences .edit() .setter(key, value) .apply() } @Suppress("UNCHECKED_CAST") fun <T> bindSharedPreference(context: Context, key: String, defaultValue: T): ReadWriteProperty<Any, T> = when (defaultValue) { is Boolean -> SharedPreferenceDelegate(context, defaultValue, SharedPreferences::getBoolean, Editor::putBoolean, key) is Int -> SharedPreferenceDelegate(context, defaultValue, SharedPreferences::getInt, Editor::putInt, key) is Long -> SharedPreferenceDelegate(context, defaultValue, SharedPreferences::getLong, Editor::putLong, key) is Float -> SharedPreferenceDelegate(context, defaultValue, SharedPreferences::getFloat, Editor::putFloat, key) is String -> SharedPreferenceDelegate(context, defaultValue, SharedPreferences::getString, Editor::putString, key) else -> throw IllegalArgumentException() } as ReadWriteProperty<Any, T> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private class SharedPreferenceDelegate < T > ( private val context : Context , private val defaultValue : T , private val getter : SharedPreferences . ( String , T ) - > T , private val setter : Editor . ( String , T ) - > Editor , private val key : String ) : ReadWriteProperty < Any , T > { private val safeContext : Context by lazyFast { context . safeContext ( ) } private val sharedPreferences : SharedPreferences by lazyFast { PreferenceManager . getDefaultSharedPreferences ( safeContext ) } override fun getValue ( thisRef : Any , property : KProperty < * > ) = sharedPreferences . getter ( key , defaultValue ) override fun setValue ( thisRef : Any , property : KProperty < * > , value : T ) = sharedPreferences . edit ( ) . setter ( key , value ) . apply ( ) } @ Suppress ( "UNCHECKED_CAST" ) fun < T > bindSharedPreference ( context : Context , key : String , defaultValue : T ) : ReadWriteProperty < Any , T > = when ( defaultValue ) { is Boolean - > SharedPreferenceDelegate ( context , defaultValue , SharedPreferences : : getBoolean , Editor : : putBoolean , key ) is Int - > SharedPreferenceDelegate ( context , defaultValue , SharedPreferences : : getInt , Editor : : putInt , key ) is Long - > SharedPreferenceDelegate ( context , defaultValue , SharedPreferences : : getLong , Editor : : putLong , key ) is Float - > SharedPreferenceDelegate ( context , defaultValue , SharedPreferences : : getFloat , Editor : : putFloat , key ) is String - > SharedPreferenceDelegate ( context , defaultValue , SharedPreferences : : getString , Editor : : putString , key ) else - > throw IllegalArgumentException ( ) } as ReadWriteProperty < Any , T >

The bindSharedPreference() function is the only externally visible function. It is here that we perform the type checking and pass in function references for the getter and setter according to the type that is determined from the default value.

SharedPreferenceDelegate is a private class which implements the delegate. The getValue() and setValue() functions call the getter and setter that are specified in the constructor. The actual SharedPreferences instance is created lazily because the delegate may be created before the Context is valid. Provided the value is not accessed or changed are not called until we have a valid Context then everything works nicely.

SO what does this actually give us? Here’s an example of how we can use this:

messenger/NotificationBuilder.kt private var notificationId: Int by bindSharedPreference(context, KEY_NOTIFICATION_ID, 0) fun sendBundledNotification(message: Message) = with(notificationManager) { notify(notificationId++, buildNotification(message)) notify(SUMMARY_ID, buildSummary(message)) } 17 18 19 20 21 22 23 private var notificationId : Int by bindSharedPreference ( context , KEY_NOTIFICATION_ID , 0 ) fun sendBundledNotification ( message : Message ) = with ( notificationManager ) { notify ( notificationId ++ , buildNotification ( message ) ) notify ( SUMMARY_ID , buildSummary ( message ) ) }

When we declare notificationId we delegate to the SharedPreferenceDelegate instance that is returned by bindSharedPreference() . We can now access it and modify its value just like we can with any Kotlin var but, because of the delegation, it will be automatically persisted from or to SharedPreferences. Using the increment operator (++) will read the value from SharedPreferences, increment it, and then save it again.

Property delegation is a really useful and powerful thing, but we need to recognise where it is being used. For example: Reading a value from SharedPreferences is far more expensive than reading a value stored in memory. When a team is working on a common codebase it would be easy for someone inexperienced in Kotlin to misuse a delegated property without checking how expensive accessing or changing it might be. There are ways that we can mitigate this, and in a future article we’ll explore some strategies for this.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Related

Copyright © 2017 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.