How to use Koin scopes with Decompose components? | by Sergei Mikhailovskii | Nov, 2024

Image generated by ChatGPT

In this article I’d like to share the approach of how I resolved the issue with the registration of the big amount of named dependencies by using scopes in Koin. Let me show the problem at the beginning. We have two components that represent whole screens — list screen component and details screen component. Both these components have child slots to represent their own modal components.

Image that describes the problem

Here’s the code example of how it looked like

So, as you can see, to fetch the needed modal component from the container, I had to use the named approach (since both modals used the same interface). Of course, I could create different interfaces and use them as markers but imho, it breaks the abstraction approach since it makes interface not flexible at all.

And here’s the place where scopes come for the rescue!

First of all, let the documentation explain, what the scope is.

Scope is a fixed duration of time or method calls in which an object exists. Another way to look at this is to think of scope as the amount of time an object’s state persists. When the scope context ends, any objects bound under that scope cannot be injected again (they are dropped from the container).

Koin provides the convenient way to work with scopes tied to Android lifecycle so it was not a big deal to transfer this logic to the Decompose component lifecycle.

The first what I did is created the following interface:

This interface is the marker that the component that implements it has it’s own scope. It provides the onScopeClose callback (is invoked when the scope is closed), implements the KoinScopeComponent (probably makes the whole magic with the injection of the right modal component) and ComponentContext (is needed to get the access to the lifecycle, so probably it’s enough to implement only the Lifecycle interface).

And the second step I did is created these extensions:

The first function is retrieves the already existing scope or creates the new otherwise, second is just a lazy delegate and the third is most interesting among them. It creates the scope and registers it inside the Koin, registers the callback that triggers the onScopeClose method (that I declared in interface) and attaches the scope to the lifecycle, so when it is in destroyed state, the scope is closed. So, now let me show how the code is transformed.

As you can see, components started to implement the DecomposeScopeComponent interface, create the scope with the help of delegate. Now there’re no named injections and it is resolved in the DI module. ListModalComponent and DetailsModalComponent are tied to scopes of the screens, so there’s no conflict now.

If we’ll look to the KoinComponent.get implementation under the hood, we’ll see why it became possible

Since the DecomposeScopeComponent implements the KoinScopeComponent, dependencies are taken not from the common graph by default but from the scope. But you still have the access to the dependencies that registered without any scope since the scope.get iterates recursively through the other linked scopes too.

Of course, you can use the scopes not only to create other components but to inject any dependency you need.

Leave a Reply