Fueled Reactive apps with Asynchronous Flow — Part 6 — Lessons learned & Next steps

Raul Hernandez Lopez
ProAndroidDev
Published in
7 min readJul 2, 2020

--

Photo by Ray Hennessy on Unsplash

(This article was featured at Android #421 & Kotlin #205 Weekly)

Once we’ve completed a full migration strategy, we should feel happy because our objective is completed (✅), isn’t it?

However, are we sure this is it?

In my opinion, our work here is not completed just yet. There are a few things we certainly have learned along the way. Moreover, we may still need to do some significant changes.

From this point, I assume you had a similar experience regarding this “Migration Strategy” proposed at Part 1:

To recap previous articles please go to the end of this article where I index all of them.

Now I want to focus on what I personally learned.

Lessons Learned

Flow does (mostly) all we need

Jigsaw on operators, image used during the slides presentation

I had a great experience playing with different operators in Kotlin Flow since the many similarities with others I typically used on RxJava. I was able to use them on Flow too.

When I worked on this project (by March 2020), other Flow APIs like StateFlow or SharedFlow didn’t exist. That’s why I decided to complete the refactor on the Use Cases. This strategy also gives a full perspective of how we can keep certain classes in Java at the same time than others in Kotlin. Remember Java and Kotlin cannot be combined using suspend functions, those are aimed to be included in Kotlin classes.

Exchangeable

Thanks to the awesome kotlinx-coroutines-rx2 library, we can easily un-plug those pieces we had on our well-assembled RxJava 2 mechanics:

RxJava jigsaw piece

Then play with other brand-new Kotlin Coroutines pieces of the bigger jigsaw that we want to complete.

Flow jigsaw piece

Thinking about the process, I had a great experience using this library, being able to play in both directions. It is definitely a game-changer for heading towards a full migration, starting small and going bigger.

Migrating needs your team agreement, therefore I would never start a big refactor without appropriated backwards compatibility. Honestly, I don’t believe a big team of engineers will be really convinced to go forward unless the right tools are in a good stable state.

What definitely helped me going forward quickly?

I used a black-box strategy to develop this — this can be used both for testing and manually trying your work. Using a particular input and expecting this to be properly transformed into the expected output. By means of this approach, I didn’t have to mind about what happens on each layer, the only important thing is when mapping a Flow input ← into the output while migrating, therefore using → Observable.

Structured Concurrency

Structured Concurrency collaborating with Coroutines

The fact that Coroutines can benefit from Structured Concurrency: we get for free self-cancellation handled by the parent scope, plus autocompletion of the open streams. It really sounds delightful.

But in the middle of a migration, this seems to vanish.

Why?

Structured Concurrency is gone when using Observables and Dispatchers simultaneously

Yes, what you read here is essentially true. This is due to having two completely different frameworks with two ways of handling their lifecycles very differently. Unfortunately, this cannot be avoided. Meanwhile, you transform layer outputs into Observables, then, of course, you are losing the advantages of Structured Concurrency. Furthermore, we will have to maintain both ways of making both Rx and Coroutines lifecycles work nicely. Wishing to arrive at the magical moment when we just need to maintain an only lifecycle.

Unfortunately, this cannot be fulfilled until the migration of a deep Scope is fully completed.

Imperative vs Declarative programming

Programming turns out in many different ways

This other than being my favourite meme when talking about programming represents very well this situation.

When doing this migration I started with a very naive RxJava approach if you recall Part 3. This was purely a declarative way of programming, it felt more natural just looking for expressing the logic without indicating the actual flow. However, when I started to feel more comfortable using Coroutines, then I introduced a more expressive imperative code. Flows can be developed in a declarative way. But what is inside a flow{} may just need regular suspend functions inside, this needs to be imperative though.

Thus, it wasn’t obvious to me what to use and why. I had a learning curve to get used to what should be introduced under different situations.

Next steps

Wait for stable Flow APIs

I like the adventure, probably you too. I think the question we need to ask ourselves now is:

Do you want to change certain operators and broader mechanisms each release though?

@Deprecated is something I don’t want to do that frequently, neither do you. My tip here is, be pragmatic, only do something when is needed, think about your team peers.

Channels and Flows into the same API

Hot Channels

So far we need Channels for our Synchronous streams.

Cold Flows

Furthermore, we need Flows for our Asynchronous streams.

It would be ideal to have both under the same API. I could see a bright future when this is happening at some point. Currently ShareFlow is very close to release, this is aimed to replace BroadcastChannels.

Closing notes

“RxJava” vs “Coroutines + Flow” main differences table

The table above, which wasn’t included during the presentation (🆕), shows the main differences I noticed from both frameworks. Probably I explained very extensively the first point about declarative and imperative programming paradigms so no need for more here. Some people would prefer one way or another.

The main advantage for me of Coroutines over RxJava is clearly Structured Concurrency. Having to manually care about finalising the lifecycle of all different disposables is not a lot of fun. Scopes on Coroutines do a really good job closing for us their lifecycle and auto-completion in open streams.

However, an advantage of RxJava over Coroutines is the endless amount of operators we can use anytime, all they are well proved over the years, that is a guarantee but the learning curve of RxJava is huge, more than Flow in my opinion. Even though here RxJava wins (at the moment), block builders on Coroutines and especially in Kotlin Flow are really useful, providing a full range of possibilities as we discovered for flow{} (Part 3) & channelFlow{} (Part 5).

Moreover, RxJava can be combined with Java and Kotlin files. For this reason, the biggest disadvantage of Coroutines though is (quoted from a paragraph above):

Remember Java and Kotlin cannot be combined using suspend functions, those are aimed to be included in Kotlin classes only.

We need Kotlin files using suspend functions to use Coroutines, thus, taking advantage of Structured Concurrency.

To summarise it all in a sentence, under my personal experience when creating this side project, slides and talk: Kotlin Flow API felt very natural for a developer coming from a RxJava world. I would recommend 100% going forward towards this direction, at least into existing Kotlin codebase modules. Surely introducing Kotlin Coroutines, especially Flow, will be the next steps. At the very least, I would experiment as soon as possible to get a sense of it.

I believe this is all for this series of articles. I hope you enjoyed them as much as I did writing them.

If you liked this article, clap and share it, please!

Cheers!

Raul Hernandez Lopez

GitHub | Twitter | Gists

I want to give a special thanks to Manuel Vivo (follow him!) & Carlos Mota (follow him too!) for reviewing this article, suggesting new improvements and to make it more readable.

(Update) The newest article talking about “Synchronous communication with the UI using StateFlow" (aka how to get rid of the Callbacks):

To recap all we have done so far, this is the full list of previous articles:

Introduction of the main Use Case and most importantly the Migration Strategy where all came from in Part 1:

We studied quite in-depth before going straight to the Implementation details from all important concepts 🥶 🥵 (Part 2):

Our Data Layer was fully migrated to Kotlin Coroutines, of course, using Flows 🥶 and suspend functions (Part 3):

Our business logic from the Use Case Layer was migrated using Flows 🥶 & Scopes (Part 4):

Our View Delegate: collaborators and companions were also migrated using Channels 🥵 → Flows 🥶 (Part 5):

--

--

Senior Staff Software Engineer. Continuous learner, sometimes runner, some time speaker & open minded. Opinions my own.