Examples of migrations


Caribou can help you track all sorts of migrations using its flexible rule engine. Here are some examples of migrations that it can help with. Remember that this list is not comprehensive but only a starting point to get you inspired on how you can use the tool to track anything you like! 😃

Contents

Android
Java
Ruby
Web development
Other




Migrating from Java to Kotlin

Kotlin has been the officially recommended language for building Android apps for a couple of years now. Moving to the language is quite simple as there is an automated converter tool built inside Android Studio that can help you easily convert your project. That, however, will probably not result in the most idiomatic use of the Kotlin language! So please, do take your time to learn Kotlin and migrate gradually to get the best results!

To track this migration on Caribou you can use the following rules:

Migration rules example for Java to Kotlin migration

As you can see, all we are doing is defining a single file group with one rule that matches against all .java files. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.

Tests first

Some engineering teams prefer migrating their tests classes first before jumping into anything else. To track the conversion of your tests from Java to Kotlin on Caribou you can use the following rules:

Migration rules example for Java tests to Kotlin migration

As you can see, we need to create a file group that contains 2 rules:

  • One rule to match all files that end in ..Test (as by convention all test classes in Java end in ..Test).
  • One rule to match all files with extension .java.

As these 2 rules are in the same file group, the result is files that end in ..Test and have the extension .java .

The final bit is defining the migration end criteria for this migration. All that we need to do is to say that we want all of these files to be removed, i.e. the number of files in file group A to be 0.




Migrating to Jetpack Compose

Jetpack Compose is the new kid on the block, bringing declarative UIs to Android, and removing the need for declaring the layout composition of your application screens using XML! Tracking your compose migration on Caribou might depend on your migration strategy but here are some suggestions that you can experiment with:

Migration rules example for jetpack compose

As you can see we are using a variety of rules to capture the migration:

  • First, we create a file group that matches against all XML files in the layout resources directory, as with Jetpack Compose you will be replacing those with Kotlin code instead.

  • Secondly, we create a file group that matches against all files ending in ..View, which are typically your custom Views. We also search for files that contain the import android.utilAttributeSet import statement, which typically exists in these custom views.

  • Finally, we create a file group for all the files that end in ..Fragment. Hopefully, with Jetpack Compose you won’t need any Fragment classes! 🤞

Then we are defining the migration end criteria so that the migration is considered done when there are no more files in any of those file groups.




Introducing a Model-View-ViewModel (MVVM) architecture to your application

The Model-View-ViewModel is the recommended architecture by Google for Android these days. Tracking it on Caribou will depend on what your project looks like, but assuming you currently have one Fragment class per screen, and you want to end up with a ViewModel class for each Fragment, you can track the introduction of MVVM in your project with the following rules:

Migration rules example for MVVM

As you can see, we are defining two file groups, one that matches files ending in ..Fragment and one for files ending in ..ViewModel. Then we are defining the migration end criteria so that the migration is considered complete when all the files ending in ..Fragment have corresponding files ending in ..ViewModel. The interesting thing that is happening in the background is that Caribou will try to match files with the same naming pattern together, so a PaymentsFragment will be considered migrated when there is a matching PaymentsViewModel class, and not any other class that ends in ..ViewModel (like for example a SupportViewModel class). Once every Fragment has a matching ViewModel the migration will be considered complete.




Introducing a Model-View-Presenter (MVP) architecture to your application

The MVP architecture is one of the simplest architectures that one can set up in their application. It started getting popular around 2016 but was superseded by MVVM when Google announced that MVVM was its preferred architecture for building Android apps. Even to this day MVP might make sense for simple applications as it is easy to set up and has a small learning curve. Tracking it on Caribou will depend on what your project looks like, but assuming you currently have one Activity class per screen, and you want to end up with a Presenter class for each Activity, you can track the introduction of MVP in your project with the following rules:

Migration rules example for MVP

As you can see, we are defining two file groups, one that matches files ending in ..Activity and one for files ending in ..Presenter. Then we are defining the migration end criteria so that the migration is considered complete when all the files ending in ..Activity have corresponding files ending in ..Presenter. The interesting thing that is happening in the background is that Caribou will try to match files with the same naming pattern together, so a PaymentsActivity will be considered migrated when there is a matching PaymentsPresenter class, and not any other class that ends in ..Presenter (like for example a SupportPresenter class). Once every Activity has a matching Presenter the migration will be considered complete.




Migrating from Timber to Kotlin Multiplatform Arbor

Everyone is excited about Kotlin Multiplatform and so are we in the Caribou team! If you want to track the removal of the Timber logging library and transition to a similar library that has Kotlin Multiplatform support (like Arbor) on Caribou you can use the following rules:

Migration rules example for Timber to Arbor migration

As you can see, for this migration we are tracking all the import statements for the Timber library (import timber) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from Android Log class statements

If your project is slightly older, then parts of it might still be using the good old android.util.Log class! If you want to track removing it from your project on Caribou you can use the following rules:

Migration rules example for removing Android Log class

As you can see, for this migration we are tracking all the import statements for the Android SDK Log class (import android.util.Log) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from Gson

Gson has served the Android community well for several years, but these days there are better alternatives out there, especially for Kotlin projects. One of them is Moshi, while the newest kid on the block is a library by Jetbrains called kotlinx.serialization, which has support for Kotlin Multiplatform ❤️. To track the removal of Gson you can use the following rules:

Migration rules example for removing Gson library

As you can see, for this migration we are tracking all the import statements for the Gson library (import com.google.gson) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from Moshi

Similarly to the migration away from Gson, to track the removal of Moshi you can use the following rules:

Migration rules example for removing Moshi library

As you can see, for this migration we are tracking all the import statements for the Moshi library (import com.squareup.moshi) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from RxJava

RxJava started to get widely used in Android projects around 2016, with many projects being full of RxJava up to this day. With all the benefits it brought, it also introduced quite a bit of complexity and a steep learning curve in many codebases, which led to the Android community welcoming Kotlin Coroutines as an alternative over the last few years. If you want to track the removal of RxJava on Caribou or you just want to upgrade from one version of RxJava to another check the rules below:

Removing RxJava 1

RxJava 1 reached its end of life as of March 31, 2018, with the recommendation to move to newer versions of RxJava, either RxJava 2 or RxJava 3. To track the removal of RxJava 1 from your project on Caribou, you can use the following rules:

Migration rules example for removing RxJava 1

As you can see, for this migration we are tracking all the import statements for the RxJava 1 library (import rx) across our repository in a single file group. This package name is unique to RxJava 1, as the other versions of RxJava have different package names:

  • RxJava 1: import rx
  • RxJava 2: io.reactivex
  • RxJava 3: io.reactivex.rxjava3

Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.

Removing RxJava 2

RxJava 2 reached its end of life as of February 28, 2021, with the recommendation to move to the latest version of RxJava, RxJava 3. To track the removal of RxJava 2 from your project on Caribou, you can use the following rules:

Migration rules example for removing RxJava2

As you can see, we are searching for files that have contents that match this regular expression: (?!.*rxjava3)import io\.reactivex. This is because there’s an overlap between the RxJava 2 and RxJava 3 package namespace as can be seen here:

  • RxJava 1: import rx
  • RxJava 2: io.reactivex
  • RxJava 3: io.reactivex.rxjava3

To only target files that contain RxJava 2 for this migration, we need to use a regular expression with a negative lookahead where we are only matching against files that contain the io.reactivex part, but not the rxjava3 part. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.

Removing any RxJava version

If you want to remove any RxJava version then you can track this migration on Caribou using the following rules:

Migration rules example for removing RxJava

As you can see, for this migration we are tracking all the import statements that either contain import rx or contain import io.reactivex, by using a regular expression import (rx|io\.reactivex). Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from findViewById/Butterknife/Kotlin synthetic imports

Android has a long history of attempting to bridge the XML and the Java/Kotlin world. First the findViewById function, then Butterknife and more recently the Kotlin compiler synthetic imports plugin and View binding. Here are some suggestions for tracking these migrations on Caribou:

Moving away from the usage of findViewById function

Migration rules example for removing findViewById

As you can see, for this migration we are tracking all the instances of the findViewById function call in a single file group. Note that we are only searching for findViewById(R.id. Then we are setting the migration end criteria so that the migration is considered done when there are no more files in that file group.

Moving away from Butterknife

Migration rules example for removing Butterknife library

As you can see, for this migration we are tracking all the import statements for the Butterknife library (import butterknife) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.

Moving away from Kotlin compiler synthetic imports

Migration rules example for removing Kotlin compiler synthetic imports

As you can see, for this migration we are tracking all the Kotlin compiler synthetic import statements (import kotlinx.android.synthetic) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Decreasing the size of your application: Migrating away from JPGs and PNGs

It’s very easy to bloat your application with large images that take up many Megabytes of space each, and as there’s a correlation between the size of the application and the number of downloads it gets that’s definitely something you want to avoid ❗. Thankfully, on Android, you can replace your JPG and PNG images with images using the WebP format that provides substantially smaller size of images with lossless compression (i.e., the images look exactly the same) starting from API level 18 (Android 4.3). In some cases images can also be replaced by vector drawables, which are typically only up to a couple of KiloBytes large, resulting in even bigger savings. To track the removal of these images from your repository on Caribou you can use the following rules:

Migration rules example for removing large images

As you can see, we are defining three different file groups, one for each type of file that we want to remove, then we are defining the migration end criteria so that the migration is considered done when there are no more files in any of those file groups.




Migrating away from the Volley networking library

Volley is an HTTP networking library introduced by Google a few years back, and has fully been superseded by other networking libraries, most notably by Retrofit. To track the removal of Volley from your project on Caribou, you can use the following rules:

Migration rules example for removing Volley library

As you can see, for this migration we are tracking all the import statements for the Volley library (import com.android.volley) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from Retrofit

As cool as Retrofit is, some projects are starting to use Ktor instead for the Kotlin Multiplatform support that it provides. To track the migration away from Retrofit on Caribou you can use the following rules:

Migration rules example for removing Retrofit

As you can see, for this migration we are tracking all the import statements for the Retrofit library (import java.retrofit) across our repository in a single file group. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating from JUnit4 to JUnit5

JUnit4 has served us well over the years, but JUnit5 has been out for a while now, and it’s a complete rewrite with lots of interesting new features! If you want to track the removal of JUnit4 from your project on Caribou, you can use the following rules:

Migration rules example for removing JUnit 4

As you can see, for this migration we are tracking the import statements for the JUnit4 @Test annotation that is added on top of every test. The JUnit5 has a @Test annotation as well, but under a different package (org.junit.jupiter.api.Test) so they are not picked up by this rule. Finally, we are setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Migrating away from Data binding

Data binding has been out for a while, and the Android community has had a love it or hate it reaction to the library. In some scenarios it might make things easier, but at the same times others would argue that it can introduce more complexity in projects by putting more logic than required in XML files and making things harder to debug. To track the migration away from Data binding on Caribou you can use the following rules:

Migration rules example for removing Android databinding

As you can see, for this migration we are tracking all the xml files that contain the </data> XML tag, which is unique to data binding. Then we are setting the migration end criteria so that the migration is considered done when there are no more files in that file group.




Moving files into another directory (e.g. introducing a feature module)

Modularization of Android projects is quite common as, among other benefits, it can improve your project build times and can also help introduce more clear ownership of the features in your organization. Projects modularizing their codebase sometimes already have some package structure for the different features that the application has, but all the features are in the same Gradle module. To track the act of moving these to separate modules on Caribou you can use the following rules:

(assuming that you have some files under app/src/main/kotlin/com/awesomeapp/payments, and you want to move that code to features/payments/src/main/kotlin/com/awesomeapp/payments)

Migration rules example for removing files from a specific directory

As you can see, for this migration we are creating a file group that tracks all the files in the app/src/main/kotlin/com/awesomeapp/payments directory. Then we are just setting the migration end criteria so that the migration is considered done when there are no more files in that file group. The number of files in this file group will gradually decrease over time, as files get moved into the new feature module directory.




Introduce tests against specific classes of a project

Sometimes we want to make sure that classes that have a specific naming convention have tests against them. This isn’t really a migration, but you can easily use Caribou to track this with the following rules:

(assuming that you have some classes that end in ..Handler (e.g. PaymentsHandler, SubscriptionHandler), and you want to introduce tests for them, so classes that end in ..HandlerTest (e.g. PaymentsHandlerTest, SubscriptionHandlerTest)).

Migration rules example for introducing some test classes

As you can see, we are defining two different file groups, one for the files that end in ..Handler and one for the files that end in ..HandlerTest. Then we are defining the migration end criteria so that the migration is considered complete when the number of the files in the two groups match. The interesting thing that is happening in the background is that Caribou will try to match files with the same naming pattern together, so a PaymentsHandler will only be matched with a PaymentsHandlerTest and not with any other ..HandlerTest class. For example, if you have 2 files currently in the project: PaymentsHandler and SubscrionHandlerTest, then Caribou will suggest that there are 2 files missing for this migration to be considered complete. Those files are the PaymentsHandlerTest and the SubscrionHandler.




Decommissioning a Ruby On Rails codebase

In this example, we will consider the scenario where a team wants to decommission or reduce the usage of a Ruby on Rails codebase. Let’s start by first considering an example where all the Ruby code needs to be removed.

Decommissioning all Ruby code

The way to do this is to create a migration with rules that match .rb and .erb files as shown below. The first rule is matching files with extension .rb and the second one matches files with extension .erb. The migration is considered complete when both rules do not match any files, i.e. all .rb and .erb files are removed.

Migration rules example for removing all ruby files

Decommissioning only parts of a Ruby codebase

The second scenario to consider is if it is not possible to remove all the Ruby files, i.e. some part of the Ruby codebase won’t be migrated. In this case, there are two options. The first option can be used if the Ruby files that need to be migrated are in specific directories. In this case, the rules can be set to be directory specific as shown in the image below:

Migration rules example for removing some ruby files

In this case, the rules are set to match Ruby files that are in a directory called /payments/. Once all the Ruby files in this directory are removed, the migration will be considered done.

If it is not easy to segment the Ruby files that need to be migrated based on directories, then you can set the migration end criteria to an actual number of files that do not need to be migrated. In the example below, the migration will be considered complete when there are less than 30 .rb files and 12 .erb files remaining in the codebase:

Migration rules example for removing some ruby files




Moving from Javascript to Typescript

Typescript is seeing quite a bit of adoption over the last few years as the type safety that it provides is deemed as an important feature that larger dev teams can lean on.

To track this migration on Caribou you can use the following rules:

Migration rules example for Javascript to Typescript migration

As you can see in the example above, we are defining two file groups: one that matches against all standard JavaScript .js files, and one that matches against .jsx files, a very popular syntax extension that the React framework introduced. After the file groups are configured, we are setting the migration end criteria so that the migration is considered done when there are no more files in any of the 2 file groups.




Removing usages of a deprecated/legacy function from a codebase

You can use Caribou to remove any type of reference to a function or class that you want to stop using in your project. For this example, let’s assume that there’s a function called loginDeprecated that you want to remove from your project. To track this migration on Caribou you can use the following rules:

Migration rules example for removing legacy function calls

As you can see, we are simply defining one file group for the files that contain the function invocation. Then we are defining the migration end criteria so that the migration is considered complete when the number of the files in the file group reaches zero.

If you are only looking to remove the legacy invocations only from specific files or directories then you will need to tweak your rules a bit:

Removing loginDeprecated from all files whose names end in ..Controller: Migration rules example for removing legacy function calls in specific files

Removing loginDeprecated from all files in the no/legacy/path directory: Migration rules example for removing legacy function calls in directory

As a side-note, if the function that you want to remove is not sa unique as loginDeprecated is, then you could explore the possibility of renaming it to something unique before you start the migration so that it’s easier to track.