The @Reusable Scope is not for Correctness

On one screen of the app I work on there are some UI elements that can be animated depending on the data received from backend. To avoid too much movement going on, when the screen is loaded at most one of these elements can be animated.
Given that the elements belong to different fragments, we use a shared coordinator object that is in charge of deciding what animation should play and that exposes this information as an observable piece of data. We inject this object where it’s needed so that each fragment/view model can then observe it and react accordingly.

Some days ago I was investigating a bug where basically this coordination was not happening anymore, causing multiple animation to play at the same time.

I start investigating and I go debug the coordinator. I check what are the values it is emitting and everything seems fine. Then I go check what are the observers receiving and I notice one of them is not receiving all emissions and I’m like excuse me?!?

what

After a bit more debugging and hair pulling it dawns on me: maybe, just maybe, there are multiple coordinators. I check and that’s correct, there are two coordinators instead of one.
I take a look at the injection setup and I notice that the coordinator is annotated with the @Reusable scope.

@Reusable

I have to admit, up until some months ago I was not very familiar with the @Reusable scope because I had never properly researched it. I think it’s not too far fetched to say that without taking the time to read the documentation one could assume that @Reusable worked somewhat like @Singleton. This assumption could then lead to resorting to @Reusable as a shortcut instead of properly scoping the dependency to a component.

But let’s do our duty this time and check the documentation under Reusable scope.

Sometimes you want to limit the number of times an @Inject-constructed class is instantiated or a @Provides method is called, but you don’t need to guarantee that the exact same instance is used during the lifetime of any particular component or subcomponent.

Notice that it explicitly says: you don’t need to guarantee that the exact same instance is used.

Then, if we continue reading we find that:

[…] if you install a module with a @Reusable binding in a component, but only a subcomponent actually uses the binding, then only that subcomponent will cache the binding’s object. If two subcomponents that do not share an ancestor each use the binding, each of them will cache its own object. If a component’s ancestor has already cached the object, the subcomponent will reuse it.

This is telling us that the caching mechanism of @Reusable bindings depends on the relations between components. And this is precisely what was causing the bug in my case!
Probably, when the coordinator was originally implemented, the components were setup in such a way that the @Reusable coordinator was effectively the same object at each injection location. Then, at some point, the setup was changed and with different components came different coordinators because each component would cache its own instance, hence the bug.

before

To fix the bug I:

  1. Replaced @Reusable with a custom scope tied to the parent Activity’s component
  2. Provided the coordinator through this component
  3. Made each fragment-specific component a subcomponent of the Activity’s one

With this setup the coordinator object is effectively a singleton in the main component’s scope, i.e. it lives as long as the component.

after

Key lessons

RTFM and don’t rely on @Reusable for correctness, so

  • don’t use it with mutable/stateful objects
  • don’t use it when it’s important to refer to the same instance
  • don’t replace a different scope with it, if you want to keep the program semantically equivalent

@Reusable is to be considered only as an optimization over unscoped dependencies, to be used when limiting the number of instances provides a performance benefit.