Prefekt / SharedPreferences

Prefekt – Api Design

On Friday 23rd February 2018 I released Prefekt – an Android SharedPreference library for Kotlin. The API is extremely clean and easy to consume, and you don’t need to worry about performance because that is all managed by the library itself. In this post we’ll look at the API design of Prefekt and see how some features of Kotlin help to create a very clean API.

A simple, lightweight API can make a library much easier to use, and that was certainly one of the goals when designing Prefekt. Thanks to some features afforded by Kotlin, I felt I was able to create an API which was much cleaner than if I had created Prefekt in pure Java. I could certainly have adapted the same patterns that Kotlin uses (it’s all Java bytecode, after all) but I certainly found that working with Kotlin helped to create a very clean API. In this final post in this series, we’ll look at the API design, and see how Kotlin and the Android Architecture Components benefitted.

Probably the most important aspect of the really clean API was the use of Kotlin extension functions to control the scope of where Prefekt instances can be created. Previously we discussed the internals of Prefekt – specifically the use of Android Architecture Components to hook the in-memory cache up to the Activity or Fragment lifecycle. For many libraries which wrap SharedPreferences it is necessary to pass in a Context because a Context is required to obtain a SharedPrfeferences instance. If we look at the instantiation of a Prefekt instance it would appear that no Context is required:

However that is not the case. A Context is required and not just any Context, it has to be a LifecyleOwner. The subtlety here is that the prefekt() function is actually an extension function in this case it is an extension of android.support.v4.app.FragmentActivity, but there is also another variant which is an extension of android.support.v4.app.Fragment. Both FragmentActivity and Fragment from the support library implement the LifecycleOwner interface.

The use of extension functions means that we have quite carefully controlled where a Prefekt instance can be instantiated, and because the classes that we’re extending both implement LifecycleOwner, we do not need to pass in a LifecyleOwner as an argument.

A further subtlety to this is that although both FragmentActivity and Fragment implement LifecycleObserver, and Prefekt uses this internally, this interface is defined in the lifecycles support library. If the API required a consumer to pass in a LifecycleOwner as an argument then the consumer would have a dependency on this library. By passing the LifecycleOwner implicitly through an extension function to a class which implements LifecycleOwner, the consumer becomes agnostic of LifecycleOwner and so the dependency is no longer required. Although this only appears to be a small thing, it does have a major impact on the dependencies. Without this, Prefekt would impose third-party dependencies upon any consumer.

Related to this is my decision to go with a subscription model to receive updates whenever the underlying value changes, even if it wasn’t changed through Prefekt. While Prefekt relies on OnSharedPreferenceChangeListener internally, this requires a subscribe and unsubscribe to use. While having this feature was an important aspect of what I was hoping to achieve with Prefekt, I was concerned that having a full subscribe / unsubscribe model could lead to leaks if clients did not unsubscribe. While there is that option available, the recommended mechanism is to supply a labmda at point of declaration, and the subscribe / unsubscribe is done automatically by Prefekt – removing the risk of leaks. It is for this reason I opted to use the an Architecture Components ViewModel internally as this enables tapping in to the lifecycle of the Activity or Fragment and therefore able to perform the necessary subscribe / unsubscribe operations at the relevant lifecycle changes. Removing responsibility from the client app in this way makes any library much easier and safer to consume.

I should add that there is a possible future change which may reduce the risk of leaks from the manual subscribe / unsubscribe mechanism but, for now, using Prefekt in that way requires the correct discipline to match any manual subscribe() calls with a symmetrical unsubscribe() call or you could leak your Activity or Fragment.

The other aspect of the API design worthy of mention is that although there only appears to be a single constructor method, there are, in fact, 20 different public constructor methods – 10 each for FragmentActivity and Fragment, these each offer minor variations in the method signature and are all overloads of the same method name – prefekt(). Of these 10, half take a lambda as a callback, and the other take a Subscriber object – providing different mechanisms of handling value changed callbacks. The final groups of five methods handle different types for the default value – Boolean, Int, Long, Float, and String. These provide type safety and ensure that only Prefekt instances which have a value of a type supported type can be created. So, although 20 different methods sounds a lot, the actual breakdown is fairly logical, and code completion means that you only need to remember the method name of prefekt(). Having an easy to memorise API and one which is designed for ease of use through code completion is an important aspect of any library, imo. The usefulness of a library to me is far greater if I do not have to refer to the documentation to if I only use it occasionally.

That’s where we’ll leave Prefekt to now. However there are some interesting additions that I have planned and we’ll take a look at those once they are implemented.

Prefekt is released under Apache 2.0 license and the source is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.