Replies: 2 comments
-
A note on the grouping of the interfaces into a |
Beta Was this translation helpful? Give feedback.
-
Thanks for raising! I actually thought about it when I was adding Regarding your concern:
I also bumped into it, I think the following code should illustrate your point: interface MyComponentContext : GenericComponentContext<MyComponentContext>, LoggerOwner
class MyComponentContextImpl<T : GenericComponentContext<T>>(
delegate: GenericComponentContext<T>,
override val logger: Logger,
) : MyComponentContext,
LifecycleOwner by delegate, // <-- We have to delegate all 4 interfaces one by one here
StateKeeperOwner by delegate,
InstanceKeeperOwner by delegate,
BackHandlerOwner by delegate {
override val componentContextFactory: ComponentContextFactory<MyComponentContext> = ...
} To overcome this limitation with the existing API, you can use the following approach, let me know your thoughts. interface MyComponentContext : GenericComponentContext<MyComponentContext>, LoggerOwner
interface BareModule : LifecycleOwner, StateKeeperOwner, InstanceKeeperOwner, BackHandlerOwner
// Define this function once, next to BareModule interface
fun <T> BareModule(delegate: T): BareModule where
T : LifecycleOwner,
T : InstanceKeeperOwner,
T : StateKeeperOwner,
T : BackHandlerOwner =
object : BareModule,
LifecycleOwner by delegate,
InstanceKeeperOwner by delegate,
StateKeeperOwner by delegate,
BackHandlerOwner by delegate {}
class MyComponentContextImpl<T : GenericComponentContext<T>>(
delegate: GenericComponentContext<T>,
override val logger: Logger,
) : MyComponentContext, BareModule by BareModule(delegate) { // Now delegation fits in one line
override val componentContextFactory: ComponentContextFactory<MyComponentContext> =
ComponentContextFactory { lifecycle, stateKeeper, instanceKeeper, backHandler ->
MyComponentContextImpl(
delegate = delegate.componentContextFactory.invoke(
lifecycle = lifecycle,
stateKeeper = stateKeeper,
instanceKeeper = instanceKeeper,
backHandler = backHandler,
),
logger = logger,
)
}
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hello! With Decompose v4 on the horizon, I'd like to open a discussion around a powerful pattern for dependency management and explore how we could improve the developer experience for it.
Motivation
Creating a Custom
ComponentContext
is a great feature for injecting app-wide dependencies. Many applications can take this a step further by creating a hierarchy of contexts, where different branches of the component tree have access to different, scoped dependencies.I prefer to explain it as a mental model of "upgrading" the context where it's needed. A parent component extends the base context with new capabilities, making them available to its entire sub-tree where they are relevant. While passing dependencies via component constructors is always an option, this pattern complements it by drawing a clear distinction between transient data (ideal for parameters) and foundational, scoped dependencies that define the environment for an entire component tree.
While this is already possible today, the process involves some boilerplate or custom wrappers. By introducing small, thoughtful enhancements to the API, Decompose could formalize this pattern, making it more discoverable, elegant, and easier to implement (yet not enforced, like much of the rest of the library). This would unlock a very powerful way to build modular, type-safe applications without needing to rely on external dependency injection frameworks.
The Core Idea
The goal is to enhance the Decompose API to make it easier to create a new
ComponentContext
that inherits from a parent context while adding new, scoped dependencies. These new dependencies are unknown to the parent but become a foundational part of the environment for the entire child hierarchy. This creates a clear dependency graph that mirrors the component tree, ensuring components only have access to the dependencies they need.Use Cases & Examples
This pattern elegantly solves several common architectural challenges:
Authentication Scopes: An application can have a base
AppContext
, but once a user logs in, the authenticated part of the app can operate within anAuthenticatedAppContext
.AppContext
provides:Logger
,Clock
,ApiClient
.AuthenticatedContext
(derives fromAppContext
) adds:UserSession
,AuthTokenProvider
.Any component within this scope has type-safe access to the user's session.
Multi-Project/Document Scopes: In a code editor or multi-document application, each open project or document has its own isolated dependencies.
ApplicationContext
provides:AppPreferences
,PluginManager
.ProjectContext
(distinct root per open project) adds:ProjectRepository
,BuildSystem
.This ensures that components for one project can't access the state or dependencies of another.
Feature Module Scopes: This pattern promotes strong modularity. A feature module can define its own context, encapsulating its internal dependencies.
Search
feature module's root component receives theAppContext
.SearchContext
that adds aSearchRepository
.Search
feature use thisSearchContext
, keeping theSearchRepository
entirely private to the module.An Advanced Hierarchical Example
Assume an IDE with modular architecture. The project-related evolution of the context can be modeled as:
In my opinion, in a project with Decompose from the ground up, this approach can be one of the most (if not the most) integrated, scalable and maintainable solutions. The intention is not to pollute the context, but to be able to upgrade it from its previous structure to a more detailed one, when the added detail is "core" to the scope of what the module is supposed to operate on.
A Concrete Starting Point for Discussion
To kickstart the conversation, here's a small API adjustment that I've found useful for reducing boilerplate. Currently,
GenericComponentContext
is defined as:My suggestion is to extract the core Decompose dependencies into a shared base interface:
This small change offers a sweet benefit for hierarchical contexts. It allows us to compose new context layers more elegantly without re-declaring the four core interfaces. For example, in the heart of my way of using Decompose, I have:
This change should have minimal impact on existing usage but would formalize the grouping of Decompose's core concerns, making the API more composable for advanced use cases like this.
Call to Action
My primary goal is to introduce this powerful pattern and open a discussion on how we can better support it from the library side. The
BareModule
idea is just one potential path, and I am sure there are others.With the breaking changes of v4, now is a great time to consider these kinds of ergonomic improvements. I'm keen to hear what @arkivanov and the community think about this pattern and how we could best support it.
Beta Was this translation helpful? Give feedback.
All reactions