Bruno Aybar

Compose Insights

You can get pretty cool insights about Compose by reading maintainers and contributors' answers in Compose's Slack channel.

I'm trying not to keep track of questions regarding specific implementations, as they are subject to a lot of changes. My main goal here is to keep a personal record of some of the motivations behind Compose architectural decisions.

Please beware that while the answers may have been valid at the time they were responded, they may no longer apply by the time you read them. Do not take any of these as absolute truths.

📌 Why functions instead of classes?

Compose team has addressed that a few times.

In the very early days of Compose (before first public release), we actually had full support for Composable classes. We made a conscious decision to remove that functionality, for a variety of reasons that we might go into in a blog post at some point. But at a high level, be aware that attempting to use this pattern will encourage code styles that end up harming long-term readability and maintainability by creating complexity in the wrong places. Using functions helps break people of their imperative prior inclinations.

And an even more detailed explanation:

Explanation of why function instead of class

📌 Why do some "generic" Components have Material implementation by default?

Quite a few people had the same doubt (me included).

Right now, if you use TextField you'll get the Material implementation. If you want a "raw" text field, you would need BaseTextField.

Wouldn't it be better to rename: BaseTextField -> TextField TextField -> MaterialTextField?

There's a reason why that's not the case:

That used to be the case, but then we heard the opposite feedback - developers would intuitively reach for TextField first, and be confused because it has no opinionated UX or styling, and requires a lot of work to look like a Material TextField. The current rename was to point developers to the opinionated component first, as this is usually the desired component.

📌 Are Ambients a Service Locator?

Basically, yes.

More here:

they’re a service locator with the very specific and singular scoping ability of scoping something to a composable sub-hierarchy. With the additional caveat that the API also gives you the tools to change the value over time, and have the usage sites will recompose as a result. I think service locator is a reasonable way to think about it, but i think it’s useful to not overlook the differences, some of which were the primary motivations of the API. If Dagger/Hilt does the thing you need, then you should probably use that instead of Ambients.

📌 Why was Stack deprecated in favor of Box?

This change was applied starting alpha04. There are a bunch of reasons for that change:

  • There's no good proper name for the concept of layout that stacks children in Z axis. It's called FrameLayout in android, ZStack in swiftui etc. We received some feedback that people expect Stack to be vertical, or even horizontal. People have different backgrounds and different expectation from that name. Overall we've decided that being 100% semantically correct is not worth it, and just having simple name for a simple concept will work just fine, hence Box .

  • We wanted to reduce number of APIs for general layouts. People used Box as a modifier wrapper or even as a leaf node with just modifiers on it. Sometimes they used Box to wrap many children as well. Name Box can be both (layouts that overlays children AND thing that you apply modifiers), allowing us to reduce complexity be reducing amount of API for people to learn. Stack wasn't striking us as smth that would read nicely without children.

  • We can concentrate on one thing performance wise and tune it nicely as we expect Box to be used quite regularly.

📌 Do I need Fragments if I use Compose?

Not a Googler's opinion, but general opinion seems to be:

I think fragment approach is just for architectures that are already using fragments so they can migrate easily but for new projects fragments should not be used

📌 How do I use Dagger / Hilt / Koin with Compose?

This is an incredibly open question and nothing is set in stone now. However, I found these pretty great threads ( 1 and 2) with interesting discussions. Check them out, some highlights:

My personal style is to limit interaction with DI to a single composable and provide injection via parameters (parameters to composable are very similar to constructor arguments to a class). This makes it easy for tests to specialize any parameter, and avoids mixing DI with the rest of the code.

For apps that are fully Compose top-to-bottom, I wonder if there would really be that many advantages to this approach of wrapping your DI/SL library of choice in a Compose API vs just providing your dependencies directly via function parameters, lambdas/lexical scope, or using Ambients directly when necessary.

One of the best practices for Compose apps is that UI should be defined in terms of pure data and event callbacks and not have many heavy external dependencies in the first place, so I would maybe be concerned that if UI has a very complex dependency graph it probably isn't separated from business and other non-UI logic as cleanly as it should be.

📌 Can I make assumptions about recompositions?

Mentioned in quite a few places, but short answer: you shouldn't.

Keep in mind that your code should NEVER make assumptions about when composables will run. Compose is free to run composable functions for any reason and at any time, at its sole discretion.

📌 Parameters vs Ambients? When to use what?

Check out this question. Some remarks:

Semantically speaking you probably want to split out individual sub-pieces of state that each composable consumes via parameters as you descend down the tree rather than having the whole app state everywhere, which makes this pattern of ambient usage less useful.

sometimes (Ambients) it's an appropriate and elegant solution to a problem; other times it's hiding things that should probably remain visible and explicit

Personally my current rule for ambient vs. parameter goes something like this: "Ambients should be reserved for select foundational app-wide or ecosystem-wide architectural pieces, everything else is parameters"

📌 Compose multiplatform?

Of course nothing official yet AFAIK, but here's a comment from someone from Jetbrains:

stay tuned, we’re working hard to bring some news here! Also note, that Compose itself is already a multiplatform library, and so Compose classes/APIs shall be easily used in multiplatform applications.

📌 Why is the mutableStateOf not wrapped by remember { } automatically?

Question here, answer here:

Once you get past the basic examples the ratio of remember { mutableStateOf(...) } to remember { MyStateClass() } that uses by mutableStateOf internally to create separate observable properties becomes weighted rather heavily towards the latter

The presence of the old state {} helper for this created a hurdle that kept people juggling many state fields themselves long past when they should be grouped within an object, because they weren't comfortable with the component primitives involved

So we decided it was better not to have it to encourage better understanding in the 1.0 timeframe. Once these ideas and patterns become well understood in the wider community we might add it back for convenience

📌 Why I can't have @Composable functions inside try catch blocks?

To reproduce, attempt calling a composable from within the try-catch block. Right now, you can't do it. Here's why:

Long term, we do intend (or at least, would like) to eventually support try-catch blocks. The reason we don't support them yet is primarily due to prioritization of our engineering efforts. Supporting try-catch semantics is a lot of engineering work because the way Compose works with recomposition and parallelization, it is possible that a child is called in situations where the parent isn't on the call stack. But when an exception is thrown, you want the control flow to pass back through the parent, even if the parent was skipped or executed on a different thread.

Getting this right requires a decent amount of engineering, and so we had to prioritize other things that were more critical to an initial stable release. Hopefully in a followup release, this can be supported.

📌 How does compose subscribe to state changes for knowing when to recompose ?

These threads (1 & 2) provide valuable discussion on the topic. Please make sure to check them out for full details.

In summary, a lot of that logic is handled by Snapshot.kt

When composable functions (or layout or drawing code) run, we set up a local snapshot observation to see what state objects are read in that scope and it doesn't care how many layers of function calls it goes through.

Later on when snapshots are applied, we check to see if any of the objects we care about change for each invalidation scope and if so, we invalidate the relevant components

over 3 years ago

Bruno Aybar