Migrating from legacy code ๐Ÿ‘‰ Kotlin Flow ๐Ÿšš

Akshay Chordiya
6 min readJun 25, 2020

--

Birds migrating. Photo by Gareth Davies on Unsplash

Content

A lot of the codebases are stuck with legacy code either callbacks, RxJava 1, loaders or other old asynchronous programming APIs and itโ€™s totally fine!

If itโ€™s working itโ€™s great but the problem lies with slower ๐ŸŒ and hard code maintenance and refactoring when there is a (new) requirement. This slows the efficiency and the velocity โฉ of developing the given requirement.

The basic idea of refactoring is to make the code more efficient and maintainable so we can build ๐Ÿ›  and deliver ๐Ÿ“ฆ things faster โšก๏ธ

This article is meant to provide how you can build a process to gradually migrate from legacy asynchronous APIs mainly showcasing RxJava to Kotlin Flow and Coroutines.

The process can be applied even if you are planning to migrate to some other alternative which suits your team and you the best ๐Ÿฅ‡

The article is not meant to explain what Kotlin Flow is and also not about why you should migrate to Kotlin Flow [you need to evaluate that ๐Ÿ˜›].

Setting up the migration process ๐Ÿšš

Setting up the process with the team is super important so the whole team works towards the single goal of migration ๐Ÿฅ…

1. Discuss with your team ๐ŸŽณ

First order of business is to discuss within your team and trying to figure what alternative to pick, that works for your team and the use-case you are trying to solve.

Iโ€™d recommend doing as followed ๐Ÿ‘‡ to get started and decide what other tool suits the best for your codebase:

  1. Setup a meeting ๐Ÿ“… with the team and list out all the possible alternatives like RxJava 2, 3 or Flow.
    Discuss the pros โ€” cons โ€” advantages of all the options.
  2. Itโ€™s best if the team gets to try out and get comfortable ๐Ÿ›‹ with the option you all like, this might include doing a small sample app, giving an internal presentation, articles, talks, etc.
  3. Itโ€™s also important to evaluate ๐Ÿงฎ how hard the migration of certain option looks like for your codebase, the learning curve for new joiners to the team.
    For instance, we at Clue migrated from RxJava 1 to Flow and we had evaluated RxJava 2 and 3 at that time and for our codebase it would have been super hard and we really liked Flow and everyone felt comfortable with it ๐Ÿ›‹
  4. At the end of this, you have a common goal ๐ŸŽฏ in mind; for example migrate from RxJava 1 to Flow.

For this article letโ€™s say it was decided to use Kotlin Flow ๐ŸŒŠ

2. Setup foundation โ›ฒ๏ธโ›ฉ

Setting up the foundation is very key ๐Ÿ— to the whole migration because itโ€™s what will motivate the team to write the new code in Flow or whatever is decided.

You donโ€™t want any developer to start thinking to write the new feature in Flow and then realise there is no testing framework ๐Ÿคทโ€โ™‚๏ธ which will make the developer go back into comfort zone ๐Ÿ›€ since all of us are humans.

Hence make sure to:

  1. Setup guidelines with standard practices ๐Ÿ“‘ of how team decided to use Flow throughout the codebase
  2. Setup the necessary base classes for your codebase and to work with your architecture including the dependencies
  3. Setup the testing framework / utilities so itโ€™s easier to do get started and do testing like setting up the dispatchers
  4. Each codebase has some base APIs which are commonly used by features like getting the user details. Itโ€™s ideal if those base APIs are exposed as Flow equivalent functions as described below ๐Ÿ‘‡

Converting commonly used APIs as Flow

The ideal scenario ๐ŸŒˆ is if you can right away convert the existing function to use Flow or coroutine, usually this is a problem cause the function is very likely used in a lot of places in the app, this forces to update all the usages and itโ€™s not great to spend time fixing this everywhere in the whole app and not to mention testing ๐Ÿ”ฅ

The optimal path is to gradually migrate ๐Ÿงฑ๐Ÿฐ, one idea is to

  • Duplicate ๐Ÿ‘ฏโ€โ™‚๏ธ those base APIs and expose the duplicated API as Flow or coroutine

For instance,

interface UserRepository {
// Legacy function
fun observeUser(): Observable<User>

// Flow equivalent of [observeUser].
fun getUser(): Flow<User>
}
  • Mark the legacy function as @Deprecated so everyone uses the new function and avoids the legacy one โ˜ ๏ธ
interface UserRepository {
// Legacy function
@Deprecated(
message = "Use the flow equivalent -> getUser()",
replaceWith = ReplaceWith("getUser()", "<package>")
)
fun observeUser(): Observable<User>

// Flow equivalent of [observeUser].
fun getUser(): Flow<User>
}
  • Try to extract the business logic ๐Ÿ’ต so the existing function and new Flow function shares the same logic preventing updating it twice when needed

3. Setup safeguards โ˜ข๏ธ

Now that we are super motivated to write Kotlin Coroutines, Safeguards will prevent kinda force the team to write more and more code ๐Ÿ“ˆ in Kotlin Flow / Coroutines and also convert the legacy code.

Some of the safeguards can be:

  • Ensuring that the new code is written using Kotlin Coroutines while doing the PR reviews
  • Ensuring when the legacy code is touched, the developer has tried to convert it coroutines or flow
  • Monthly or quarterly checking of progress of the gradual migration ๐Ÿšš
  • And other ideas which will pop in your brilliant minds ๐Ÿง 

4. Interoperability

The key to success ๐Ÿ’ฏ of Java-Kotlin migration was interoperability and ease of using one from another which opened the doors ๐Ÿšช for gradual migration.

Similarly having this ability allows to quickly interop between the legacy code and Flow or Coroutines which can drastically change the landscape ๐Ÿ”

For instance, having a helper function to convert Observable to Flow saves a lot time โณ for developer when they want to focus on building something rather than spending time migrating legacy code at that moment.

RxJava 2/3 โ†”๏ธ Flow

Kotlin Flow follows the reactive stream specification ๐Ÿ“„ which is followed in RxJava which makes things conceptually similar to understand.

Plus, Kotlin Coroutines library provides the functions to easily convert Single / Maybe / Completable to coroutines and Observable / Flowable to Flow and the reverse.

You can find those helper functions here:

RxJava 1 โ†”๏ธ Flow

Migrating from RxJava 1 to Kotlin Flow or Coroutines is bit tricky since there isnโ€™t official support from Kotlin Team [which makes sense since itโ€™s super old].

One of the factor why one should migrate from legacy code is when the legacy library is not supported anymore which also makes it hard to use modern libraries and tools ๐Ÿ˜ž

This is one of the factor why one should migrate from legacy code.

Luckily, I recently created a library ๐Ÿ‘‡ with the necessary APIs to provide the similar functionality of converting Single / Maybe / Completable to coroutines and Observable to Flow and the reverse.

Callback based โ†”๏ธ Flow

Kotlin coroutines library provides are mainly 2 different APIs to deal with callback ๐Ÿ”€ based legacy code.

Though suspendCoroutine is part of Kotlin standard library and unlike suspendCancellableCoroutine it doesnโ€™t support cancellation โœ‹

suspendCoroutine / suspendCancellableCoroutine

suspendCancellableCoroutine allows to convert callback based API ๐Ÿ”€ to a suspending function aka coroutine.

It does that by providing continuation instance which gives us control over the suspension and we are responsible to guide it.

Here is an example of suspendCancellableCoroutine with callback based API of Retrofit library:

suspend fun <T> Call<T>.await(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { cancel() }
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, error: Throwable){
continuation.resumeWithException(error)
}

override fun onResponse(
call: Call<T>,
response: Response<T>
) {
continuation.resume(response) { cancel() }
}
})
}
}

callbackFlow

callbackFlow allows to convert callback listeners to Flow . It internally creates a channel to which we can send the values produced by the callbacks and remove the listener when the flow is cancelled.

Here is an example of callbackFlow with callback based listeners from FlowBinding library to get stream of clicks as Flow:

fun View.clicks(): Flow<Unit> = callbackFlow {
val listener = View.OnClickListener { offer(Unit) }
setOnClickListener(listener)
awaitClose {
setOnClickListener(null)
}
}

Final thoughts ๐Ÿ’ญ

Legacy code can sometimes be painful but itโ€™s important to distinguish between a real problem or new needs vs some cool new tool out there i.e unless you have a real problem or new requirement there is no real reason for you to migrate ๐Ÿšš

Whatever tool you decide itโ€™s important to work as a team and setup the migration strategy / process and work towards the common goal ๐Ÿ† and iterate โ™ผ

Please leave some claps ๐Ÿ‘ if you learned something or enjoyed the article or loved my emojis or all of it.

--

--

Akshay Chordiya

Google Developer Expert @ Android | Android Engineer @ Clue | Instructor @Caster.IO