Several people havealreadywritten about their journey migrating from Gson to Moshi. So why write this post? While migrating a fairly large project to Moshi, my team learned a few things (and came up with a few simple tools) that made it safer and easier to do this migration. Some of these points were either mentioned in passing in the aforementioned articles, or were skipped over altogether, and they are the focus of this post. Consequently, I’ll skip things like the motivation of moving to Moshi, since some of the aforementioned articles cover that pretty well.
A Quick Note on Gson’s field naming policy
One of the earliest concerns about migration is how to deal with Gson’s .setFieldNamingPolicy method, which allows the automatic mapping of something like created_at in json to createdAt in Java/Kotlin. One of the articles on this matter in specific that resonated with me when I read it long ago is Jesse Wilson’s article on naming conventions, which specifically addresses this point. Due to the arguments in that article, we decided to explicitly add annotations (@Json(name = "")) to the fields. While manually adding them is a lot of work, it can be done incrementally, and, as we discovered during the migration, it can help identify existing bugs that no one noticed before.
Overall Strategy
Moshi-Gson-Interop
Slack’s moshi-gson-interop is a useful tool for slowly migrating from Gson to Moshi. It tries to intelligently decide whether a particular model should be deserialized using Gson or Moshi, based on some heuristics (which one can optionally tweak if needed). One such heuristic is that classes that are annotated with Moshi’s @JsonClass are given to Moshi to deserialize.
There’s also a logger parameter that lets us tell the library how to log various events.With this, we can easily watch logcat (for example) to know which classes are deserialized by Gson and which by Moshi and annotate more classes until the entire payload of a particular screen is all deserialized via Moshi.
Notice the nice property allowing us to gate this feature to a subset of people and safely continue using Gson while we build trust in our migration. This helps us stage our migration in a (mostly) safe and iterative manner.
Migration Strategy
At a high level, the migration strategy we used went a bit like this:
choose a screen to migrate.
enable moshi-gson-interop.
run the app, go to the screen, and see what is still being deserialized by Gson.
iteratively add annotations to models until the entire screen is deserialized via Moshi.
run some runtime validations to ensure results are properly deserialized.
fix issues found from validations.
repeat.
As mentioned above, moshi-gson-interop makes it easy to toggle this feature to make this migration incremental. Initially, for example, only a single developer might enable interop. Once a critical mass of screens has been migrated, we might enable this for all engineers, and only start rolling it out to employees after we’ve gained enough confidence on it.
Validation and Other Useful Tools
Since most of our network calls use Retrofit, we wanted to build something for testing purposes to help us validate the correctness of the json data. Retrofit allows us to add a Converter.Factory while initializing it. Typically, this is the in-built GsonConverterFactory.create(gson) instance, for example. We can choose to either replace (or wrap) this instance to do some neat things.
For validation, for instance, we use a Converter.Factory that deserializes the data twice - once in Gson, and once in Moshi, and compares the outputs. This should only be used for development, since it’s very inefficient (both due to the double deserialization of the payload, and the one-shot reading of the entire response body). This looks something like this:
classGsonMoshiValidationConverterFactory(
privateval gson: Gson,
privateval moshi: Moshi
) : Converter.Factory() {
overridefunresponseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
return InteropConverter<Any>(type, gson, moshi)
}
// TODO: override requestBodyConverter here if needed for migration
// in a similar way to what we did for the response body above. We'll
// need to write another Converter that takes the actual value and
// converts it to json, once for Moshi and once for Gson. Because the
// field ordering maybe different, we can do validation by converting
// the json strings back to objects and comparing the objects.
}
classInteropConverter<T>(
privateval type: Type,
privateval gson: Gson,
privateval moshi: Moshi
): Converter<ResponseBody, T> {
overridefunconvert(value: ResponseBody): T? {
// WARNING: This is very inefficient, do NOT use this outside of development
val bodyAsString = value.string()
val moshiResult = try {
val adapter = moshi.adapter<T>(type)
adapter.fromJson(bodyAsString)
} catch (exception: Exception) {
// handle parsing exception via Moshi
}
val gsonResult = gson.fromJson<T>(bodyAsString, type)
if (moshiResult != gsonResult) {
// flag mismatch between Moshi and Gson
}
return gsonResult
}
}
Using this, we can clearly flag cases where Moshi and Gson deserialize something differently and can work to fix it. In order to work, however, the caveat is that the types being compared need to properly have an implemented .equals methods. One nice side effect of this is that whenever a particular item mismatches, we can also get the body as a string, and write a small validation unit test to iron out the cases.
This same trick of rolling our own Converter.Factory is useful for other things also, such as for very roughly measuring the performance of deserialization in Moshi versus Gson, and for surfacing exceptions at parsing time that are swallowed somewhere in the upstream code.
Note that if json is being sent, we should also override requestBodyConverter in the Converter.Factory similar to what we did for the rsponseBodyConverter. In this method, we can then convert the object to json for both Moshi and Gson. Note that if we compare them at this time, we’ll get a lot of noise due to the ordering of the fields being different. To work around this, we can re-serialize the json back to the type again, and check equality after the round trip.
Gotchas
While developing, we ran into several interesting problems and issues that we’ll go over here. Many of these are arguably working around bugs on the backend, and often times, the right solution to these problems is to reach out to the backend team to fix the result instead of working around it on mobile.
Sealed Classes
For sealed class hierarchies, we used moshi-sealed. Under the hood, moshi-sealed will create a PolymorphicJsonAdapterFactory, which will decide the flavor to create based on the parameter type. In case of an unknown type, a fallbackJsonAdapter can be passed in. One known issue is that if the backend sends no type (i.e. the field is absent), Moshi will throw an exception. This can likely be worked around with a custom JsonAdapter if necessary, but it would make more sense to ask backend to properly send a type in this case instead.
Handling Alternate Keys
There are some scenarios where a payload will have a field coming back with any one of several json fields, such as in this example:
In this case, the actualName field in our Person object should contain whichever the server sent back of name or full_name. One approach for solving this is mentioned in one of the articles mentioned earlier, but that approach doesn’t work well when the model has many fields (since it would otherwise result in a lot of duplication just for mapping).
When it wasn’t easy to use the above approach, we opted to take what we thought would be an easier approach to this. Note that the below code has a bug, we’ll get to that after:
dataclassPerson(
val name: String?,
@Json(name = "full_name")
val fullName: String?
) {
// WARNING: This is actually wrong when using Gson and will result in a null
// value of actualName, even if one of name or fullName are passed in.
@Transientval actualName = (name ?: fullName)!!}
We figured we’d make all the different names as nullable variables, and knowing that the backend will always return one, we force our result to be whichever one of those fields is non-null. Note that, without the @Transient, Gson will try to look for an actual_name in the json to set this to.
While this works well for Moshi with codegen, this doesn’t actually work for Gson (when the type doesn’t use a type adapter), where actualName will return null, irrespective of the value of name or fullName. This happens to be a side-effect of how Gson makes these values via reflection.
If we look at the decompiled bytecode for our Kotlin data class, we can see that this variable gets set as part of the constructor:
Looking through theseposts on StackOverflow, we find out that the reason is that without a custom deserializer, Gson uses Unsafe.allocateInstance to make an instance reflectively, bypassing the constructor. It reflectively sets the properties afterwards, resulting in our actualName never being set. To fix this, we can just change actualName to a getter instead:
dataclassPerson(
val name: String?,
@Json(name = "full_name")
val fullName: String?
) {
val actualName: String
get() = (name ?: fullName)!!}
Handling non-nullable Primitives
Suppose we had a model with:
// case A
dataclassWidget(identifier: Int)
// case B
dataclassWidget(identifier: Int = 0)
In case A, if no identifier is set in the json, the Gson result would set it to 0, whereas the Moshi result would crash due to the field being missing. We can fix this by updating to case B, where we set a default value.
What if the json contained an identifier set to null? We’d expect the value to be defaulted to 0, and, in Gson, it is. However, in reality, this throws an error instead. While there are some suggested solutions in this thread about working around this, we opted for a similar approach to the above:
dataclassWidget(
@Json(name = "identifier")
@SerializedName("identifier")
internalval internalIdentifier: Int?
) {
val identifier: Int
get() = identifier ?:0}
One interesting note is that if the default in case B is not 0, Gson will still default to 0, which should make sense considering the note in the alternate keys section above.
Summary
This article offered a set of suggestions to make the migration from Gson to Moshi easier. By making the upgrade incremental, toggleable, and able to be validated, we make the migration a lot more achievable.
A little over a year ago, I gave a talk about building a PrayerTimes mobile application for iOS and Android using Kotlin Multiplatform some time ago. I recently gave a second talk about expanding this application to run on a plethora of other platforms (desktop, web, macOS, Linux, watchOS, iOS using Compose). You can watch it here and see the code on GitHub.
In a previous article, I talked about writing profiler-util, a tool I open sourced for visualizing build performance over time for personal projects (it interops the files generated by gradle-profiler and uploads the data to a Google Spreadsheet, along with providing tools for detecting regressions).
I typically run gradle-profiler on my personal projects every handful of PRs, especially those that I expect might affect build speeds (Gradle plugin versions, AGP versions, compiler plugins, etc).
The scenarios file I use for gradle-profiler consists of a number of
cases, including abi and non-abi changes to app and non-app modules, changing
resources, adding a composable function, clean builds using build cache, and
configuration (see this page from the Android developer documentation and
the gradle-profiler readme for more details).
A Mysterious Situation
Recently, I checked to see how the build performance for my app was doing, and
was surprised to see this:
Looking closer, I could see the times greatly increasing for all but 3 metrics - configuration change, the change of a resource, and an incremental build without having changed any code. Given this, I looked and found that the change was upgrading AGP from 7.2.1 to 7.2.2 in this project. Surprised, I checked my other projects, and none of them had the same issue. A few weeks later, when I upgraded to AGP 7.3.0, the build times still didn’t improve, which surprised me.
I decided to look into this to try to figure out what was happening.
Thinking about Potential Explanations
Given that this was only happening in this project and none of my other projects, I decided to look into one of the following initial potential explanations:
Maybe nothing was wrong and re-running the test would fix the issue.
Gradle configurations across the profiler are different than those in real
life - maybe there is no regression at all and is just a case of mismatched
properties?
This project uses Realm, whereas none of my other projects do. Could
this be related?
Going through these quickly:
I re-ran the test multiple times on the commit with AGP 7.2.1 and the commit with the single change to AGP 7.2.2 and got consistent results every time. So much for that idea.
I chose one of the cases above, clean_build_with_cache, and ran it without gradle-profiler (./gradlew clean; ./gradlew assembleDebug) with AGP 7.2.1 and again with AGP 7.2.2 to eyeball the results - once again, they seemed consistent with the profiler results, thus removing the mismatched properties as an explanation.
An update to Realm happened recently and it unfortunately didn’t change the numbers at all. Moreover, no upgrade to Realm had happened in the window before the upgrade.
Finding the Issue
Given that none of the above worked, I decided to try something else that
ultimately lead me to the problem - I ran the above clean_build_with_cache
commands locally with --profile for both AGP 7.2.1 and AGP 7.2.2:
git checkout <last commit with 7.2.1>
# warm up caches, etc due to AGP version change to 7.2.1./gradlew clean
./gradlew assembleDebug
./gradlew clean
./gradlew assembleDebug --profile
git checkout <first commit with 7.2.2>
# note that profile html output files survive gradle clean# warm up caches, etc due to AGP version change to 7.2.2./gradlew clean
./gradlew assembleDebug
./gradlew clean
./gradlew assembleDebug --profile
I then compared the result files:
The delta in build times is the only thing that stands out. Going through the configuration, dependency resolution, and artifact transforms tabs, nothing stands out and all numbers are close to each other.
This leaves the task execution tab, which shows us something interesting:
Why is Realm taking 6 seconds when using 7.2.2 but not when using 7.2.1? And what’s Realm’s processor doing in app?
The Culprit
Realm objects have various annotations (@RealmModule on module classes, @PrimaryKey for primary keys, @Index for indices, etc), and requires an annotation processor, bundled within a realm-android plugin.
Historically, I had all said annotated classes in a separate, standalone module just for Realm models, since I know they’d rarely change and I didn’t want to pay the price of said processor every time. This is a good optimization in said cases.
Indeed, looking at app/build.gradle, I found the realm-android plugin in use there. So how did it get to app? Apparently, sometime back in May of 2016, io.realm imports were not resolving in the app module anymore (by not resolving, I am guessing this was in the IDE and not actually at compile time, though today, I have no way of being sure).
Removing the realm-android plugin brings back the compilation times to what they were before AGP 7.2.2 and above. This, however, doesn’t explain what AGP 7.2.2 had to do with this.
Warning - this is purely speculation at this point, and I am not certain about
anything else in this paragraph. Looking at the release notes for 7.2.2, there are only 2 fixes there. One of them is this one, which has to do with fixing a bug related to the transform API when used with the ASM api. ASM is used for bytecode manipulation, and is in use by realm-android. I suspect that this bug caused the plugin to do nothing in app (which was fine since it’s technically not needed as mentioned above). The bug was introduced in 7.2 alpha, and wasn’t fixed until 7.2.2. The first commit in my profiling was using 7.2.0, so I never noticed any “improvement” between 7.1.x and 7.2.x, only to see it being lost again in 7.2.2. This is, however, only a guess.
Wrapping Up
Some takeaways here:
Monitor your build times - the results may be quite surprising.
gradle-profiler is your friend.
the --profile flag is your friend.
If none of this had worked, I would have resorted to running a build scan next.
As an Android code base grows and has increasing code churn, unused resources are very likely to exist. While Android Studio has an option for cleaning up and removing unused resources, there are some reported bugs in this functionality that are still not fixed.
I started searching for another solution for finding and cleaning up unused resources in Android projects. I found this project, which has some Python 2 scripts to directly manipulate resources. Since it hadn’t been updated since 2018, I took the liberty to build something based on it, which fixes some of the issues I ran into while using it.
Introducing resource-cleanup - an open source project to help identify and clean unused resources (in retrospect, I wish I had named it “Mr. Kaplan”). It relies on severalopen sourcetools and heavily relies on the command line to accomplish its role.
The project has a set of shell scripts - one per resource type. It searches for usages of @[resourceType] or R.resourceType references. In the absence of these, it removes the given resource. The shell scripts take a single file and checks for usages accordingly. The documentation gives examples of how to combine this with tools like fd and shell loops.
These scripts have proven invaluable to us at work and helped us reduce our apk size by a sizable amount. Please give it a try!
I gave a talk at The Assembly in Dubai about tips and tricks for productively building mobile applications. In it, I discuss experimentation, catching issues earlier in CI/CD, multiplatform, and various other topics. Here are the slides.
Monitoring Gradle Build Speeds for Smaller Projects
Today, there are manygreatarticles and videos about how to optimize Gradle performance for Android builds. Larger companies watch these metrics closely, since build time translates into money. These twocomics sum up the situation pretty nicely.
I write and maintain several of my own Android applications. This got me to thinking about my own app build times. I started off using the gradle profiler, as recommended by many of the aforementioned articles and videos (this article gives a nice introduction on how to use the gradle-profiler in the light of Android apps).
Having a way to measure is great, but how do I know how my build times are doing over time? I wrote and open sourced profiler-util, a Kotlin app, for just this purpose.
Visualizing Build Performance over Time
After running a gradle-profile, one of the outputs is a benchmark.csv file with the format. The tool I wrote has two modes - the first uploads the results from the benchmark.csv file into a Google Sheets spreadsheet.
What’s nice about using Google Sheets is that we also get graphing out of the box:
Note that the incomplete lines are benchmarks I added later on.
Detecting regressions
The second tool uses the step fitting method that I learned about from this excellent article about benchmarks to figure out which builds caused a regression or helped improve the build speeds. Essentially, build metrics from a single profiling run might not be enough to determine whether a change occurred or not, especially due to variations that could occur on the system running the profiling. Using the --iterations flag of gradle-profiler helps.
Note: Today, I use the average of all iteration times for a given scenario as the number I consider. Only looking at the mean causes a loss of information (and assumes the data follows a normal distribution when it doesn’t). For more accuracy, additional signals (such as median and p95) should also be considered.
Instead of using changes in a given value to determine whether or not there is an improvement or regression due to a particular PR, this tool instead uses a windowed approach to check if there is a notable difference in the build performance when a window before the commit is compared to one after the commit.
From this, we can see that the commit in which I updated to use non-transitive R files helped speed up things when adding a resource, and when making an abi change to app. Note that, while the graph shows some other potential improvements, the tool doesn’t list them because the threshold doesn’t consider them as substantial as the other changes reported.
Where to Run Benchmarks
When I began this process, I made an initial mistake, thinking that I could run these tests as part of CI on every pull request or merge of code. Unfortunately, after trying it out, the numbers were all over the place. In retrospect, this makes sense - there are no guarantees about the VMs or machines that a particular provider is using for running these builds, how loaded the physical hardware is, and so on. Moreover, running these profiling builds on CI typically takes forever. The combination of all of these can cause a great deal of variance. It is worth noting, however, that some measures can be taken to make this data more usable - using bare metal instances instead of virtual machines, for example, or running a massive number of builds such that outliers can be smoothed out can both help.
Instead, I settled on running these on my own laptop after making changes that I think would be relevant data points to have. In the future, I could probably rent a dedicated, bare metal server or purchase a dedicated machine and run these tests on a cron, but given that these are personal projects, it’s probably overkill at this point.
Conclusion
In the future, it’d be nice to change this to allow various data export methods - instead of writing to Google Sheets, have an option to write to a database, for example. The Google Sheets integration piece is also interesting, since it can be used for quick visualizations of other pieces of data - apk size and macro/micro benchmark results (though this post from the performance-samples project might be the way to go on this one). Please feel free to share your ideas or feature requests on the GitHub project.
Special thanks to Tony Robalik and Efeturi Money for reviewing this post and giving valuable suggestions.
I wanted to write a short post about how to find the reason for a particular version of a dependency to be selected by Gradle.
Today, while building our app, we started getting this error:
The minCompileSdk (31) specified in a
dependency's AAR metadata (META-INF/com/android/build/gradle/aar-metadata.properties)
is greater than this module's compileSdkVersion (android-30).
Dependency: androidx.core:core:1.7.0-alpha02.
AAR metadata file: /Users/ahmedre/.gradle/caches/transforms-3/3a90b660583f6aafd513bafe95732d86/transformed/core-1.7.0-alpha02/META-INF/com/android/build/gradle/aar-metadata.properties.
The error makes sense, but my question was, “who pulls in androidx.core:core:1.7.0-alpha02 - we shouldn’t be using alpha dependencies at the moment, so where is this coming from?”
The first place to look was through Gradle’s dependency target -
Looking through this, I can see indeed that androidx.core:core is being updated from various other versions to 1.7.0-alpha02, but I don’t know why. I did some searching, and today I learned about the dependencyInsight target. Quoting the Gradle documentation:
The dependencies report provides you with the raw list of dependencies but does not explain why they have been selected or which dependency is responsible for pulling them into the graph.
This is the culprit! The publisher of this dependency seems to have added an androidx.core:core-ktx:+ dependency. I checked and this was indeed the case. Mystery solved!
The next question was, how do we avoid having this happen again? Some more searching lead me to this post in the Gradle documentation, which suggests the usage of the failOnDynamicVersions(). We can also prevent snapshots by doing failOnChangingVersions(). Awesome!
I gave a talk about building a PrayerTimes mobile application for iOS and Android using Kotlin Multiplatform. The talk is mostly a live coding talk in which we write a simple PrayerTimes mobile app from scratch. Under the hood, the app is using a Kotlin Multiplatform port of the BatoulApps Adhan library. In addition to the video, you can see the code on Github here, and the (very short, 10 slide) slide deck here.
Note - A plethora of excellent articles and talks describing how to write an annotation processor exist - consequently, this blog post will not talk about the details on how to build an annotation processor. Instead, it addresses a specific case that I had a much more difficult time finding answers for online and a strategy for solving it.
Scenario
Suppose we are shipping an app with multiple libraries pulled from a company Maven repository. Let’s say we want to write an annotation processor that outputs a list of all classes annotated with a certain annotation. In other words, given:
// in repo1, artifact com.company.app:module1
@CompanyModuleclassFoo : CommonModule
// in repo2, artifact com.company.app:module2
@CompanyModuleclassBaz : CommonModule
// in the app repo
@CompanyModuleclassBin : CommonModule
// we want an implementation of this generated
@CompanyModuleRepositoryinterfaceCompanyRepository {
val modules: List<CommonModule>
}
we want a class in our app module generated that looks like:
Note - Uber has open sourced an annotation processor, Crumb, that handles this case painlessly.
Initial Attempts at a Picasso
Given this problem, let’s write an annotation processor to do this. Let’s take the obvious approach first, one in which we get all classes annotated with @CompanyModuleRepository and all the classes annotated with @CompanyModule. Using this information, we’ll write code to generate the CompanyRepositoryImpl class.
If we do this, we’ll find that the only modules we’ll be able to pick up are the ones in app - so in the above case, the Bin module is the only one that will be added.
Roadblocks and Imperfect Frescos
If we debug our annotation processor code, we’ll find that a line that looks for all the CompanyModules -
val modules = roundEnv.getElementsAnnotatedWith(CompanyModule::class.java)
Only finds the single module, Bin, within our app module. If we add another module in app, it will also be found, but all our modules from our artifacts won’t be found.
Why? When the annotation processor runs, it will run against a particular module. In this case, we’re running the annotation processor against our app module. It will find any annotated classes in app with no problem.
On the other hand, our dependencies, module1 and module2, are coming from a Maven repository as binary dependencies - i.e. they’re precompiled. Consequently, the annotation processor will not run on them at this point, since it’s too late for that. This is true even if the RETENTION on the annotations is properly set to AnnotationRetention.BINARY as it should be in this case. To solve this, we’ll need something to run while module1 and module2 are compiling, and take this result into consideration while compiling app at the end.
Gliding to Completion
After a lot of digging, I found people pointing to Glide as one of the canonical reference annotation processors that people look at while writing their own annotation processors. Reading their code and stepping through it with a sample project, here’s a summary of what Glide does:
For each LibraryGlideModule or GlideExtension (CompanyModule in our example), an Indexer class is generated in a consistent package (irrespective of the aforementioned annotated class’s package) with an @Index annotation and the fully qualified path of the original annotated class. In Glide’s case, all of these generated indices are written to com.bumptech.glide.annotation.compiler. This output generated Indexer class looks something like this:
For each AppGlideModule (Glide restricts these to only 1 - this is our CompanyModuleRepository), the processor looks for all files in the aforementioned directory (com.bumptech.glide.annotation.compiler), filters out only the ones with an @Index annotation, and uses those to generate the list of modules.
val glideGenPackage = processingEnv.elementUtils.getPackageElement(COMPILER_PACKAGE_NAME);
Knowing how Glide does this, we can apply the same strategy to our problem. In our case, we’ll generate an @Index annotated class for each @CompanyModule and we’ll write that to a common directory - com.company.generated.module for example.
Using the beginning example, our modules that will be pushed to Maven will look something like this:
When the annotation processor is run against App, it can then find all files present in com.company.generated.module, check which ones properly have the @Index annotation (and read the full path from them), and use that information to build the list.
One Last Potential Pitfall
Glide’s annotation processor is outputting Java code using JavaPoet. If, instead, we decide to generate Kotlin code using KotlinPoet, there’s one more gotcha we need to look out for.
Consider the case where we have a single module with 1 class - running our code, we might find nothing generated, and re-running it, something might be generated. Huh? Why? The point to remember here is that kapt does not process newly generated Kotlin sources across multiple rounds (see bug here). The workaround for this is to generate our Indexer classes using JavaPoet instead of KotlinPoet (see this KotlinPoet issue for some useful extension functions for making this easier).
Special thanks to Efeturi for reviewing this post.
One of the interesting projects I worked on this year was building Careem’s SuperApp. To do this, we combined Careem’s main two applications, the RideHailing application and the Food deliveries application, into a single app. This may sound simple at first, but it is actually an interesting problem with many layers to unravel.
I gave a talk about this at 360|AnDev this year. You can find the slides here.