I’ve officially released the first version of a library I have been working on for a while now, that carries the very unimaginative title ‘WakatimeClient’. For the technical details you can take a look at the library page or go straight to github.
Backstory Link to heading
Wakatime is a fantastic coding activity tracker that not only has a fantastic range of plugins for (it seems) almost any IDE on the planet, but it also has a fully functional restful api that can be used for consuming and creating events.
I’ve been an active customer since 2014 and during that time I’ve not only enjoyed being able to see the statistics about my coding activity, but it has also come in handy in salary negotiation and compensation talks after times of immense crunch and overtime.
There have been a few situations where my activity tracking wasn’t being tracked nor uploaded (my fault every time) which made me long for an intelligent mechanism that could produce notifications when it wouldn’t detect activity where there should be.
And so in 2016 I started working on a mobile application for this exact purpose (there is something to be said about mobile developers wanting to capture everything into an app).
Since then the project has gone in many circles, and still has a long way to go until it can see the light of day (with Jetpack Compose now available, I think the layout will go through yet another transformation). After a couple of years of dabbling with it, the realization dawned on me that all the effort going into the project wouldn’t be of any benefit to anyone if things would continue the way they were.
And so in late 2019 the networking layer was spun out of the project, into a separate library project called ‘wakatime-client’. The idea was to take care of as much of the interaction with the api surface as possible. And I’m rather happy with the outcome, as it stands.
Library Link to heading
The library takes care of the authentication flow, with great help from the fine library appauth, has built in caching mechanisms for the networking layer and fluent interfaces for interacting with the api surface. It is opinionated in places where it matters, in the hopes of reducing headache and taking care of the boiler plate details.
On top of that, the library leverages the power of kotlinx-serialization to not only perform de/serialization of payloads, but to transform the incoming responses to better utlize the power of composition.
As an example of this transformation, lets look at the organization dashboard member endpoint /api/v1/users/:user/orgs/:org/dashboards/:dashboard/members
.
It’s response payload will contain a list of members, containing detailed user information along with user centric configuration values for the the dashboard
{
"data": [
{
"id": <string>,
"email": <string>,
"full_name": <string>,
"is_view_only": <boolean>,
"photo": <string>,
"username": <string>,
"is_hireable": <boolean>,
"display_name": <string>,
"website": <string>,
"location": <string>,
"can_view_dashboard": <boolean>,
}, …
],
This payload combines values from a previously defined model for a User
with additional context related variables. Instead of creating yet another model Member
that either inherits from the User
model or duplicates all of its fields, the library uses json transformations prior to serializing the payloads to extract the values into a User
variable inside the Member
public data class Member internal constructor(
/**
* Indicates whether the user can view the dashboard
*/
val canViewDashboard: Boolean = CAN_VIEW_DASHBOARD_DEFAULT,
/**
* Indicates whether the user is only viewing the dashboard, or participating
* in supplying activity
*/
val isOnlyViewingDashboard: Boolean = IS_ONLY_VIEWING_DASHBOARD_DEFAULT,
/**
* The user backing up this member
*/
val user: User
)
This not only reduces the library surface that consumers will need to deal with, but also feels much better to work with.
Initial setup of the library is relatively straight forward, and is available through both a builder pattern and a dsl, giving consumers some choice in how they would like to use it. No part of the underlying networking stack is hidden, so any modifications that consumers would like to make are possible
builder.network {
val interceptor = HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
getOKHttpBuilder().apply {
addInterceptor(interceptor)
callTimeout(<Your value>, TimeUnit)
// Here you have full access to the OkHttpClient.Builder
}
getRetrofitBuilder().apply {
// Here you have full access to the Retrofit.Builder
}
// If network layer caching should be done by the client,
// it can be configured by using the built in caching mechanism
enableCache(context.cacheDir, cacheLifetimeInSeconds = 30)
}...
Construction of query/post payloads is also exposed both in a builder pattern or a dsl to keep both options available for consumers.
val request = Summaries.request(start, end) {
timezone = end.timeZone.displayName
project {
projectName = "wakatime-client"
branches("develop", "master")
}
meta {
writesOnly = true
}
}
All network operations are constructed around the use of Kotlin Coroutines, and any results are returned through state wrappers for effortless consumption.
when(val results: Results<Summaries> = client.getSummaries(request)) {
is Results.Success.Values -> // consume values
is Results.Success.Empty -> // notify user of empty results
is Resuls.Error -> // notify user of error
}
Conclusion Link to heading
During the time put into this library I’ve been able to go deeper into kotlin serialization than I had previously done, general Android library creation and publishing to Maven Central (which will get it’s own write up soon). And in that sense it has been a rather good learning experience.
Even after almost a decade of professional mobile development, I had never published a library before.
I hope that the library comes in handy for someone out there that wishes to build a native Android client using it. And if you do take a look at it, feel free to tweet at me and give me your thoughts @hrafn_thor.
All constructive criticism is valued.