Many Android developers want to use Kotlin in existing Android applications written in Java. Some already do. After all, Kotlin is fully interoperable with Java and the tooling is great. What can go wrong?
Well, I think that lots of things can go wrong.
In this article I’ll explain why two common strategies used to introduce Kotlin into existing Java applications are non-optimal, and provide recommendations for a better approach.
Don’t Start by Writing Unit Tests in Kotlin
For a long time I was puzzled by the fact that so many teams decided to introduce Kotlin into unit tests first. I personally think that it’s the worst way to start using a new programming language, but all these teams chose it seemingly independently of one another. Surely there is something that I missed which makes this approach not as bad as I thought.
I indeed missed something: the fact that it’s the officially recommended way to introduce Kotlin into existing applications. As the official Get Started with Kotlin on Android page states:
Start by writing tests in Kotlin. Tests are useful to check for code regression, and they add a level of confidence when refactoring your code. Tests are especially useful when converting existing Java code into Kotlin. Because tests are not bundled with your app during packaging, they are a safe place to add Kotlin to the codebase.
Even though I did miss this official recommendation, I don’t think it makes this approach any less unfortunate. As a proponent and advocate for unit testing as part of professional culture, I find this paragraph terribly misleading.
[Side note: I don’t make distinction between unit testing and test driven development. I’ve yet to see a team that adopted unit testing culture without doing some kind of TDD. In my opinion, it’s practically impossible to do large scale unit testing if you write the tests after the production code.]
So, what’s the issue with the above official guidelines? Everything.
Bugs in Unit Tests Are Production Bugs
The aforementioned paragraph states that:
Tests are useful to check for code regression, and they add a level of confidence when refactoring your code.
That’s correct in general, but these are derivative benefits that depend on the quality of the unit tests.
The main feature of unit tests is that they function as executable specification for the system under test (SUT). In other words, they capture the raw requirements of the SUT in code. If buggy unit tests pass, all these bugs are guaranteed to be reflected into the production code as well.
Therefore, if you use Kotlin and introduce bugs into unit tests due to lack of experience, be assured that your production code will be broken as well.
On the other hand, if you have a high quality test suite, then it functions as your safety net. You can experiment with the unit tested parts of the production code however you want and still confidently ship code in any language as long as unit tests pass.
You Won’t Refactor Unit Tests Later
As the official documentation correctly points out, unit tests make refactoring safe.
Let’s say that you used Kotlin in production and wrote really bad code. Is it a big deal? Nope. You’ll be able to refactor any unit tested code later with no risk. Even if production code will end up completely unreadable, you will still be able to understand the requirements from high quality unit tests.
However, you won’t do the same with unit tests. The dependency between unit tests and production code is uni-directional: unit tests verify the production code, but production code doesn’t verify unit tests. For example, if you refactor a test case and accidentally break it in a subtle way (e.g. assetThat(true, is(true)) – pass unconditionally), it will still pass while not testing anything.
What you usually do is write very high quality unit tests and don’t change them in the future. You can definitely add or remove test cases as unit’s requirements change, but you won’t refactor the existing ones.
Therefore, if you start with Kotlin in unit tests, there is a high chance that your worst Kotlin code will stay with you forever.
Unit Tests is the Worst Place to Start With Kotlin
So, bugs in unit tests are reflected into production code and you don’t usually refactor test cases. Therefore, this statement is wrong:
Because tests are not bundled with your app during packaging, they are a safe place to add Kotlin to the codebase
The reality is that unit tests is the most sensitive and dangerous place to add Kotlin to the codebase.
It’s much safer to keep writing unit tests in Java (or whatever language you’ve been using to date) and introduce Kotlin into production code that is covered with high quality unit tests. This way you rely on one of the benefits of a good test suite – an ability to safely refactor the production code.
You should postpone using Kotlin in unit tests until you get comfortable with it and converge on a coding style as a team.
I guess that at this point you might point out that some companies started with Kotlin in unit tests and it worked out alright for them. First of all, good for these companies. Secondly, I’d like to remind you that Telegram, which is one of the most successful mobile projects, has Activities with up to 12 thousands lines of code.
Therefore, the fact that something “works out alright” in specific cases doesn’t imply it’s a good practice.
Don’t Mix Java and Kotlin Files Randomly
Hopefully I convinced you that you should not use Kotlin to write unit tests until you become reasonably experienced with it. Or I failed to convince you. Or, maybe, you don’t write unit tests at all.
In any case, if you want to use Kotlin, then, sooner or later, you’ll be introducing it into an existing Java production code. One of the most popular ways to do that is the following:
- Write all new classes in Kotlin.
- Convert existing Java classes that require modifications to Kotlin.
This approach, alongside many other potential approaches, results in a random mix of Java and Kotlin classes in the codebase. It might be alright for small projects, but I think it can be non-optimal and even risky for larger ones.
See, it can take months and, potentially, even years to migrate an application of a considerable size to Kotlin. During all this time your application will be written in two different languages. As similar as Kotlin and Java might look, it’s much harder to maintain a polyglot codebase.
Let me list some factors that make polyglot codebases more challenging (this list is non-exhaustive by any mean). Afterwards I’ll explain how random mix of languages makes the situation even worse and what you can do to mitigate it.
The most straightforward implication of mixing Java and Kotlin is that developers will need to be fluent in both languages.
Even today, more than a year after the official announcement of Kotlin support, most developers don’t have professional experience with it. Therefore, if your project will need additional developers in the near future, the pool of fully qualified candidates for hire will be much smaller.
While it’s definitely possible to hire developers who don’t know Kotlin and let them learn it, it will be a considerable expense and slowdown.
In addition, at some point in the future new Android developers might start learning Kotlin instead of Java. These developers will be similarly unqualified for polyglot projects.
Therefore, polyglot codebase is a big risk in context of staffing.
Kotlin has a great interoperability with Java, but it’s not completely free and there is a list of rules to follow when mixing the two. Therefore, there is a “tax” for integration points between Kotlin and Java in the codebase.
Even if you’re an expert in both Kotlin and Java, reading polyglot code will be more difficult than code that uses just one language. If you’re not an expert in both (like the absolute majority of developers), then reading a polyglot codebase can be really challenging.
Longer Build Times
Very important consideration when it comes to Android development are project build times. They can easily become the main productivity killers on bigger projects.
To measure the effect of Kotlin introduction on clean build, I did the following with my pet Java project (~10 KLOC, single module, Dagger 2):
- Executed several clean builds for a warm-up.
- Executed three clean builds and measured their performance using Gradle build scans.
- Converted one data structure to Kotlin.
- Executed several clean builds for a warm-up.
- Executed three clean builds and measured their performance using Gradle build scans.
The average duration of the clean build increased by ~18%.
However, I don’t usually care about clean builds too much. It’s the incremental builds that matter the most. Therefore, I performed additional set of measurements with incremental builds. In these experiments I followed the same methodology as before, but changed the name of one class field (property) between runs and allowed for incremental build.
The average duration of the incremental build increased by ~8%.
It’s important to note that these numbers are probably specific to my case. However, I believe that it’s generally true that introduction of Kotlin increases the build times.
See, I had a very interesting chat with several experienced developers at the last conference I attended. All of them understand much more than me in all this stuff and it was them who brought this limitation to my attention. According to them, this increase in build times is a general limitation experienced by all projects that introduce Kotlin.
I hadn’t had a reason to doubt what they said, but I wanted to see the magnitude of the effect myself. Such an increase in build times due to addition of a single Kotlin file is really unfortunate. [I wonder if it gets worse with more Kotlin code. If you happen to know – please share with us in the comments.]
Hopefully, you decided to adopt Kotlin and either replace Java completely, or, at least, segregate Java and Kotlin into different modules. Hopefully, you also have a clear schedule for this migration with specific mid-points to track progress.
Allow me to challenge you with a quick sanity check quiz. In your opinion, which 10% of migration will be the most difficult? The variants are:
- Something in between 10%-90%
I personally think that it’s the last 10% that will be the hardest. The reasons for that are both practical and psychological.
[Disclaimer: I didn’t perform this specific refactoring myself, but I have some experience with large scale refactorings and rewrites; I tend to believe that migration from Java to Kotlin is conceptually the same.]
The practical reason is that the last part of the code that you migrate will probably be either the least used or the most complicated. In both cases, it might be challenging to understand the requirements. Hopefully, you also have executable specifications for this logic aka. unit tests.
The psychological reason is boredom.
When you’ll start the migration to Kotlin you’ll probably be enthusiastic about the new language and constantly discover new tricks. During most of the migration you’ll use Kotlin to write new features, refactoring some small portions of the code along the way. However, inevitably, you’ll get to the point when you’ll need to invest a dedicated effort for pushing through the last mile. Otherwise, your codebase might remain polyglot for years, or even forever.
During the last mile you won’t learn new tricks and won’t work on new cool features. You will be doing boring and seemingly unimportant work. If you work in a company that evaluates employees per features completed, you’ll not get any bonus points for the last mile push. If you don’t have a good test suite, you’ll break stuff and might be hold responsible for that. Heck, you’ll probably break something even with tests in place.
In short – it’s very easy to introduce Kotlin into Android apps. JetBrains did a good job on that. However, it will probably be much more difficult to drive the application to full Kotlin, or, at least, to have Java encapsulated in library projects.
That’s why I’m not impressed by projects that adopted Kotlin and sit at XX%. It’s the easy part, folks.
Mitigation of Polyglot Codebase Drawbacks
The best way to avoid all the negative effects of polyglot codebase is to avoid making the codebase polyglot in the first place. In other words – don’t use Kotlin. However, you might have your reasons to use Kotlin and still be willing to take the risks. If that’s the case, you’ll need a mitigation strategy.
Fortunately, there is one approach that can greatly mitigate all the aforementioned drawbacks: separate between Java and Kotlin parts of your application at a higher level than class.
The next level construct would be package. It’s surely better to have Kotlin-only or Java-only packages, but it’s still not enough because closely related code can be spread across multiple packages. In addition, big projects might have hundreds and even thousands packages.
However, the root of your project contains a limited number of top-level packages. Can you get away by making sure that your top-level packages use either Java or Kotlin exclusively?
I think it will be much better approach. This way you will reduce the number of integration points and make sure that closely related code uses the same language. That said, it will be feasible only if your top-level packages follow the “package by feature” convention.
Unfortunately, separating Kotlin and Java at the level of top level packages won’t help you with build times. You will need to make sure that Kotlin and Java are separated by modules boundaries if you’d want to address this issues. As far as I understand, that’s the only mitigation strategy for longer build times due to Kotlin.
Segregation of Kotlin in Java into different modules also happens to be the best mitigation strategy in context of other drawbacks as well. Unfortunately, it’s the most complicated approach that requires much effort.
If you follow my blog then you probably know my opinion about adoption of Kotlin for Android development. It’s not very positive, to say the least.
However, it’s evident that Kotlin gains popularity and many projects adopt it. Since the official migration guide is absolutely terrible in my opinion, in this post I decided to give advice to new Kotlin adopters:
- Don’t use Kotlin in your unit tests until you feel fluent in it.
- Don’t add (or convert) random Kotlin classes, scattered all over your codebase.
- Use a structured approach and clearly separate Kotlin and Java parts of your application.
- The best separation level for Kotlin and Java classes is different modules.
That would be all for this time.
Please feel free to leave your comments and questions below.
5 comments on "Migrate Android Applications to Kotlin with Caution"
thank you for highlighting some of the drawbacks of mixed Kotlin/Java code bases. i have not seen these problems mentioned elsewhere.
As always you provide a thought provoking perspective. Everyone always suggests adding a new language in tests ‘because it is safer’.
As you’ve described, it’s actually much higher risk.
I think we all know why this is. It’s much easier to sell a manager on changing the tests then it is to change the production code.
The manager doesn’t think about the tests, and just thinks ‘new language not in production’ is a good thing, and doesn’t care how tests are done.
Thanks for your comment.
I think you might be onto something because that’s exactly what several developers told me in response to this article: it was easier to convince the management that way.
However, I personally (and you seemingly too) don’t think that it’s actually a “positive” justification. I wouldn’t like to sound judgemental, but this sounds a bit unprofessional, to say the least.
I liked the build times comparison, but it’s incomplete without a 100% Kotlin module. You are saying mixed languages is bad, but maybe is just that Kotlin compiler is slower. I mean a Kotlin module is faster or slower than a mixed module?
That’s a very good question and I agree that the comparison is incomplete. In fact, it was never my goal to cover all the bases.
I don’t know about pure Kotlin module for sure. If you’ll test it yourself, please share your results with us.