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 some of the Prefekt internals.
The previous article showed how to actually use Prefekt in a project and (hopefully) demonstrated how clean and simple the API for consuming Prefekt is. However using a third-party library requires deeper knowledge than just the public facing API. Without understanding a little a bit about the internals then how can we make an informed decision about whether any given library is right for our project? Often there may be hidden costs along with hidden benefits from using any given library. For that reason I felt that it was worth offering some information about how Prefekt actually works so that those who may be interested in using it are able to make a much more informed decision about whether it is right for their project.
There are essentially three layers to Prefekt. At the base there is a persistence layer which is the only place where SharedPreferences are used. Having this in a distinct layer is useful for a number of reasons, not least of which it makes everything else totally independent of SharedPreferences. Whilst I currently have no plans to move the persistence to anything other than SharedPreferences, having this encapsulated within its own component (which could be easily interchanged with another component with the same interface) offers good separation of concerns. The persistence layer registers a change listener with the underlying SharedPreferences instance to detect any changes made outside of Prefekt.
The middle layer is the in-memory cache which holds the value of a given shared preference value in memory. This has a one to one relationship with the persistence layer – so one cache object has its own corresponding persistence object. This is actually implemented as a LiveData instance from the Architecture Components support library so provides the publish / subscribe behaviour inherent in LiveData.
The top layer is the API layer which is an adapter which provides an abstraction away from clients having to consume LiveData. While there is much to like about LiveData I did not want to force it upon those that weren’t already using it in their project and there is no requirement to include any of the architecture components support libraries in your project if you decide to use Prefekt. Furthermore, as I was developing Prefekt, it because clear that such a pattern didn’t quite work for Prefekt which ruled out exposing Prefekt objects as either LiveData, or even Kotlin property delegates. There will be a further discussion of this in the next article which explores the API design. There is a many-to-one relationship between the API layer objects and the cache layer objects. So for a given SharedPreferences item stored in a cache object, there may be multiple API objects which will all be notified of changes. This means that multiple objects which reference the same SharedPreference (based upon the key name and object type) will all share a single in-memory cache object.
The API layer is the only part to which clients of Prefekt have any direct access. When an Prefekt object is created by your app, it is created in real time, but the other components may be created lazily. While you may declare the Prefekt object as a property within your Activity or Fragment, the Context will not immediately be useable. For this reason, the Prefekt object uses a ViewModel to obtain instances of the memory cache, and persistence layer pair. This will not be created until the Activity or Fragment context is ready to use. At this point the SharedPreference value will be retrieved, and you’ll get a callback with the value. This means that you can create a Prefekt instance without having to wait until the Context is actually in a useable state.
This separation is also useful because the cache layer will stop emitting changes once the Activity or Fragment is destroyed, thus removing the need to any checks within client code to check lifecycle state, or even have to implement any kind of registration and unregistration from the Activity or Fragment lifecycle. That’s all done internally within Prefekt.
I have been asked why the memory cache layer is required when SharedPreferences itself performs in-memory caching. There is no single reason, but a number of contributing factors. Firstly the OnSharedPreferenceChangedListener may get called if the value is changed to its existing value. I want Prefekt to only notify clients when the actual value has changed and the cache layer in Prefekt allows for those kinds of optimisations. Coupled with this when Prefekt is notified of changes to a SharedPreference, it is only able to determine whether the value has actually changed if it knows what the previous value was. That is where it’s own in-memory cache becomes a requirement.
Secondly, it would make the whole architecture more tightly coupled to rely on the SharedPreferences cache as a core behaviour. Putting the in-memory cache and the persistence in to distinct components results in a far cleaner separation of concerns, and there are some future extensions planned which simply will not be possible without that degree of separation. Hopefully this second point will make more sense once I’m in a position to release these new features.
In the final article in this series we’ll take a look at some of the decisions behind the API design, and look at how some language features of Kotlin can help us enormously in API design.
Prefekt is released under Apache 2.0 license and the source is available here.
© 2018, Mark Allison. All rights reserved.
Gurupriyan is a Software Engineer and a technology enthusiast, he’s been working on the field for the last 6 years. Currently focusing on mobile app development and IoT.