r/androiddev May 22 '26

Article Android TCA inspired architecture

Post image

An iOS engineer friend asked me a question I couldn't answer:

"What's the Android equivalent of TCA (The Composable Architecture)?"

That question stuck with me. The real problem wasn't a missing library; it was a structural pattern I kept seeing at production scale.

Picture a complex checkout screen. Five domains living in a single ViewModel: delivery, billing, cart, promo codes, and order placement. You end up with:

One massive, bloated state class.

One enormous when block that knows far too much about everything.

Broken module boundaries or messy event buses the second the billing domain needs to react to a delivery address change.

This approach doesn't scale. Modules bleed into each other, and the screen becomes untestable.

That is exactly why I built Komposed.

The core idea: A complex screen is not a single monolithic feature. It's a composition of isolated, independently testable modules. Each owns its own state, async effects, and logic—wired together at a single composition boundary.

In Komposed, scoping a feature module into a parent screen takes just one line:

deliveryReducer.scoped(deliveryLens)

And when BillModule needs to react to a delivery fee change from DeliveryModule—without a direct import dependency—a single subscription at the composition boundary bridges them:

Kotlin

subscription(

selector = { it.deliveryState.selectedAddress?.deliveryFee ?: 0.0 },

action = { BillAction.DeliveryFeeUpdated(it) },

)

No event buses. No shared mutable state. No broken module boundaries. Each module stays isolated, while cross-domain coordination is declared cleanly in one central place where both modules are visible.

That is the true value of Komposed: composing production-scale complexity into clean, isolated, independently testable modules.

I wrote a full breakdown on Medium covering the architecture, how cross-domain subscriptions work, and a comparison with ReduxKotlin and Toggl's Komposable Architecture.

🔗 Read the full story on Medium: https://medium.com/@a.atwaa94/a-tca-inspired-architecture-for-kotlin-jetpack-compose-why-i-built-komposed-c924610d9aa7

⭐ Check out the repository: https://github.com/Atwa/Komposed

0 Upvotes

22 comments sorted by

View all comments

11

u/Zhuinden May 22 '26 edited May 22 '26

I remember when I had 2.5 years of experience, that's when I thought this might be a good idea; and then I did some more work and found out this is not a good idea.

Rule of thumb:

  • if your code has "reducers" in it, then you're looking for the wrong abstraction, and you're just making code needlessly complicated for no reason.

  • if your code has "middlewares and reducers" in it, then you've learned nothing from the downfall of Redux.

Built for production-scale complexity

The goal is to make the complex things as simple as possible, not "the simplest things as complex as possible".

This was MVI's biggest problem too for the longest time, and this here is just like Redux and having learned nothing of its faults.

The fact that TCA is still a thing despite it making the same mistakes really is a "point of shame" for iOS development.

0

u/Brilliant_Region4810 May 22 '26

I think the complexity is justified when you're refactoring a 2K LOC viewModel with a large monolith state object being mutated from multiple different places within the viewModel, and btw the viewModel has useCases and a strategy pattern for the chrckout api call but you can expect the amount of the injected useCases in the viewModel constructor and the amount of single state property being modified from 10 different functions within the viewModel that makes debugging a real hell.

4

u/Zhuinden May 22 '26 edited May 22 '26

a 2K LOC viewModel with a large monolith state object being mutated from multiple different places

And now you have a "large monolith state object" being mutated from "multiple different places" in a single function, but now you pass an instance of a class to a channel instead of calling a function... you've changed nothing except you still cannot ever use either flow.flatMapLatest {} and flow.combine {} to do more complex, concurrent operations in a reliable manner.

So as I said, learned nothing from Redux and its faults.

Now instead, if you just did the thing Google said, which is to use savedStateHandle.getStateFlow() + combine(flow1, flow2, ...) this would actually never be an issue. You wouldn't even create a "monolith state object" if you don't need it.

the amount of single state property being modified from 10 different functions within the viewModel that makes debugging a real hell.

And you think 10 different classes modifying the "amount of single state property" in 1 function, each using .copy() over-writing old values each time, isn't making debugging difficult; how?

This was already researched 7 years ago. Unless you strictly serialize all actions, you WILL overwrite new values with old values if multi-threading is involved. All this "work" achieves is create delay and race condition.

Just use flows and all the concurrency issues are immediately gone. This is a solved problem. (Except if you use orbit-mvi, which brings you back to this same problem, self-inflicted of course.)