Migrating from legacy code ๐ Kotlin Flow ๐
Content
- Go with the Kotlin Flow ๐
- Diving deeper into Kotlin Flow ๐โโ๏ธ [Coming soon]
- Migrating from legacy code to Kotlin Flow ๐
- Flow on Android ๐ค [Coming soon]
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:
- 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. - 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.
- 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 ๐ - 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:
- Setup guidelines with standard practices ๐ of how team decided to use Flow throughout the codebase
- Setup the necessary base classes for your codebase and to work with your architecture including the dependencies
- Setup the testing framework / utilities so itโs easier to do get started and do testing like setting up the dispatchers
- 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.