DataStore / Jetpack

DataStore: Wire

In Early September 2020 Google released the first Alpha of a new Jetpack library named DataStore. DataStore is a replacement for SharedPreferences. While there are some similarities to SharedPreferences, DataStore offers far greater flexibility. In this series of posts we’ll take a detailed look at this new library.

NOTE: There is an update to this series of articles that covers how to use newer releases of DataStore.

In the previous article we saw how to use the Protobuf variant of DataStore with the Google Protobuf code generation tools. However, in a discussion of DataStore Joe Timmins mentioned that he’s been playing with Wire for the codegen rather than the Google tools. His reasoning for this was that he wanted Kotlin models over Java ones. This got me thinking about alternatives to the Google tools. So thanks to Joe for inspiring this article.

Wire

Wire is a creation of the smart folks at Square and is an alternative to the Google codegen tools. That alone makes it worth considering. As has already been mentioned, Wire is capable of generating Kotlin code for the Proto models. The Google tools currently only support Java code generation. Furthermore, we can optimise wire code generation to Android. The main aspect of this is that proto models generated with this option enabled are Parcelable. Meaning that they are very easy to pass around in Android.

Moreover it generates far more compact code than the Google tools. The code generated by Google tools was a total of 321 lines. The Android / Kotlin code generated by Wire was a total of 114 lines. This was for the SmileData object – a single field – that we saw in the previous article. This is quite a significant difference, and certainly will be important for mobile development where we want to minimise our APK sizes.

This may already seem like a no-brainer decision to use Wire over the Google tools. However it’s not quite that straightforward. There are different versions of Protobuf the main two being proto2 and proto3. The Google tools supports them both. However Wire only officially supports proto2 at the time of writing. Shortly after I wrote this article, Wire 3.3.0 was released which includes support for proto3.

There are not huge differences between proto2 and proto3 – there’s a nice comparison here. I’ll not go in to the specifics here.

Pbandk

A library named Pbandk is a third option for codegen. This generates some nicely optimised Kotlin models and supports both proto2 and proto3. While there is much to like, I do not personally see this as a viable option for use in a project with multiple developers. The big problem is that it relies upon the protoc compiler that is part of the Google tooling. If we use the Google plugin, this will pull down and install protoc as part of the build process, but Pbandk requires it to be manually installed.

Therein lies the problem. If you have a team of developers (and a CI) each machine upon which the code will be built must have the correct version of protoc installed. If one developer needs to change the version of protoc then this change will need to be properly communicated through the team (and the CI) to avoid lost time trying to fix broken builds.

I have experienced first hand issues of this type causing major pain points in development teams. For that single reason, I would not use Pbandk in a serious project.

Conversely both Wire and the Google tools have all of the necessary version and configuration as part of the build config. Meaning that if one developer needs to change versions, the necessary changes will be committed with the breaking changes. So barring developers messing up, this should means that as developers pull any breaking changes, they also pull the updated build configs to update the tools.

Choices

So the real choice of which tool to use for codegen is between Wire and the Google tools. Different projects will have different requirements so there’s no simple recommendation. However, if you’re project is an Android app written in Kotlin, the code generated by Wire can be tailored to be much more specific to the needs of your app.

Of course if you are already using protobuf within your project then the simple solution would be to stick with whichever tool you’re currently using.

So, as we’ve already covered the Google tools, the remainder of this article will cover how to migrate from the Google tools to Wire.

Build Config

Next we need to replace the Google plugin with the Wire equivalent in the top level build.gradle:

buildscript {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.0-alpha09"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0"
        classpath "com.google.dagger:hilt-android-gradle-plugin:2.28.3-alpha"
        classpath "com.squareup.wire:wire-gradle-plugin:3.3.0"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
.
.
.

We also need to apply this in the app build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.squareup.wire'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    .
    .
    .
}

dependencies {
    .
    .
    .
    implementation "com.squareup.wire:wire-runtime:3.3.0"
    .
    .
    .
}
.
.
.
wire {
    kotlin {
        android = true
    }
}

First we apply the Wire plugin, then we add the Wire runtime as a dependency, and finally we switch the Goole tool configuration to the wire one. The Wire config here is actually much simpler than the Google one. the Kotlin block instructs it to generate Kotlin code, and the android = true flag adds the Android optimisation that we discussed earlier.

The Serializer

There is one more change that is required – to the DataStore Serializer implementation. The reason for this is that the generated Wire classes have a different API to their Google-created counterparts. Whereas the Google class had parseFrom() and writeTo() methods, the Wire classes have an ADAPTER which is used for serialisation:

object SimpleDataSerializer : Serializer {

    override fun readFrom(input: InputStream): SimpleData {
        return if (input.available() != 0) try {
            SimpleData.ADAPTER.decode(input)
        } catch (exception: IOException) {
            throw CorruptionException("Cannot read proto", exception)
        } else {
            SimpleData("")
        }
    }

    override fun writeTo(t: SimpleData, output: OutputStream) {
        SimpleData.ADAPTER.encode(output, t)
    }
}

That’s it. The accessor APIs are pretty much the same, so there are no changes required to our consumer code to use this. However, our consumer code is actually pretty basic, so there’s not much to break!

However, it is worth noting that SimpleData is now Parcelable which means that we can easily pass it around in a Bundle. We didn’t get that with the Google tooling, so we would need to convert to a different data model to be able to do that.

Gotchas

There are actually a couple of issues that I hit with other tools when using Wire. The first was that Android Studio was not finding the generated code even though the gradle build was. The solution for this was to manually add the folder containing the generated code to the sourceSet:

.
.
.
android {
    .
    .
    .
    sourceSets {
        main.java.srcDirs += "$buildDir/generated/source/wire"
    }
    .
    .
    .
}

This fix then caused a second issue: The static analysis tool ktlint was reporting code quality issues with the generated code. Clearly this is a false positive because we are not in control of the generated code, so it was necessary to add an exclusion to the config for ktlint-gradle:

.
.
.
ktlint {
    android = true
    version = "0.38.1"
    ignoreFailures = false
    reporters {
        reporter "plain"
        reporter "checkstyle"
    }
    filter {
        exclude { projectDir.toURI().relativize(it.file.toURI()).path.contains("/generated/") }
    }
    outputToConsole = true
}
.
.
.
}

Conclusion

Switching to Wire is fairly easy to do on a small project like this. However that may not be the case for a larger project which already has tooling for Protobuf codegen. Personally I prefer the code generated by Wire, so we’ll stick with that for the remainder of this series.

Once again huge thanks to Joe Timmins for inspiring this exploration.

In the next article we’ll turn our attention to security.

The source code for this article is available here.

© 2020 – 2021, Mark Allison. All rights reserved.

Copyright © 2020 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.