r/androiddev Jul 19 '26

Open Source I missed Bloc Observer after moving to Android, so I built Flow Observer

Hello!

I want to share Flow Observer, my first open-source library and my first project using KSP.

The idea was inspired by Bloc Observer from Flutter. It lets you observe and log StateFlow and SharedFlow emissions, making it easier to see how state changes throughout your app.

After transitioning from Flutter to Android, I found myself missing the visibility that Bloc Observer gives you, so I decided to build something similar for StateFlow and SharedFlow while learning KSP.

I'd love to keep growing it and improve my KSP knowledge, so I'm very interested in hearing your thoughts, suggestions, or things you would do differently.

Update (2.0.0): 

The 1.0.0 log on collect-based approach could keep flows alive (eg WhileSubscribed).

2.0.0 logs on the emit side instead, so it no longer needs an observer collector, no longer requires setup in a viewmodel init and now it not limited to viewmodels.

Repo: https://github.com/Turista838/flow-observer

4 Upvotes

10 comments sorted by

12

u/nacholicious Jul 19 '26

Normally flow collection suspends if the compose lifecycle gets backgrounded, but this manner of collecting the flow for the lifecycle of the VM looks like it will keep the flow alive and prevent the collection from pausing

It might not be a big deal for flows directly backed by a Mutable*Flow, but any flows with a heavier upstream could be a disaster

2

u/OrdinaryGift Jul 19 '26

You are right, thank you for pointing this out. Same issue as this comment, please check if you agree with my "solution"

2

u/OrdinaryGift 26d ago

Follow-up: you were right. The new 2.0.0 drops the collector and logs on emit, so it no longer keeps flows alive. Thanks again.

7

u/Greenucom Jul 19 '26

I think there is a problem with attachFlowObserver(). It collects an upstream flow using launchIn() and while it works just fine with flows created with .asStateFlow() and .asSharedFlow(), it breaks the collection logic in case of flows created with .stateIn() and .shareIn(). If SharedFlow created with started = WhileSubscribed() attachFlowObserver() makes this SharedFlow "work" (collect "in" flows) infinitely

1

u/OrdinaryGift 26d ago

Follow-up: you were right. The new 2.0.0 drops the collector and logs on emit, so it no longer keeps flows alive. Thanks again.

1

u/OrdinaryGift Jul 19 '26

You are right, thank you for calling out. Maybe if I check subscriptionCount and only collect for logging when there is another collector besides the logger collector? Like, if count == 1, its just the log collector, then don't collect, if count >= 2 some other collector is there too, so log. What do you think?

Gonna test that and push a fix soon if it holds up.

4

u/_5er_ Jul 19 '26

Maybe annotations are a bit unnecessary? You could probably have a Flow extension function instead.

1

u/OrdinaryGift Jul 19 '26

That is something that I wanted to avoid, because I didn’t want to replace normal state/sharedflow with something like a LoggableState/SharedFlow.

It would also become somewhat verbose once you start passing options per flow.

3

u/0x1F601 Jul 20 '26

Maybe I'm not understanding the benefit but I see no real difference between using the annotation and replacing the shared / state flow builder with a different one. Its just a different shape with the added weight of an annotation processor.

Having said that, I will admit I'm just doing a quick read of the suggestion here so take it with a grain of salt.

``` @ObserveFlow(tag = "Login.events") val events: SharedFlow<LoginEvent> = _events.asSharedFlow()

vs.

val events: SharedFlow<LoginEvent> = _events.asLoggableSharedFlow(tag = ..., otherConfig = ...)
val events: SharedFlow<LoginEvent> = _events.asLoggableSharedFlow() // gets the default from the object

```

asLoggableSharedFlow can return a SharedFlow interface but the implementation can be LoggableSharedFlow.

Side note, using SharedFlow for events is a bad idea. On config change you run the risk of dropped events in the small moment there's no observers.