Replies: 2 comments 5 replies
-
Hello @aartikov and thanks for the question! Indeed there is no dedicated API for this use case. However it still possible with the existing API, but it will be a bit verbose. I think this use case is quite rare, so I am not convinced to create something special for it as of now. Here is an example of how it can be implemented currently. The idea is to use multiple First of all, let's define the interface Root {
val models: Value<Model>
data class Model(
val children: List<Child> = emptyList()
)
sealed interface Child {
class ChildA(val a: A) : Child
class ChildB(val b: B) : Child
class ChildC(val c: C) : Child
class ChildZ(val z: Z) : Child
}
} And now an example of the implementation. I used Reaktive, but it should be easily convertible to coroutines as well. class RootComponent(componentContext: ComponentContext) : Root, ComponentContext by componentContext {
private val routerA = router(key = "A") { config, context -> AComponent() }
private val routerB = router(key = "B") { config, context -> BComponent() }
private val routerC = router(key = "C") { config, context -> CComponent() }
private val routerZ = router(key = "Z") { config, context -> ZComponent() }
override val models: Value<Root.Model> =
combineLatest(
routerA.asChildObservable(Root.Child::ChildA),
routerB.asChildObservable(Root.Child::ChildB),
routerC.asChildObservable(Root.Child::ChildC),
routerZ.asChildObservable(Root.Child::ChildZ),
) { children: List<Root.Child?> -> Root.Model(children.filterNotNull()) }
.asValue(Root.Model(), lifecycle)
private fun <T : Any, S : Root.Child> Router<*, Holder<T>>.asChildObservable(mapper: (T) -> S): Observable<S?> =
state.asObservable().map { it.activeChild.instance.value?.let(mapper) }
private fun <T : Any> router(key: String, factory: (Config.Component, ComponentContext) -> T): Router<Config, Holder<T>> =
router(initialConfiguration = Config.Component, key = key) { config, context ->
when (config) {
is Config.None -> Holder(null)
is Config.Component -> Holder(factory(config, context))
}
}
private class Holder<out T : Any>(val value: T?)
private sealed interface Config : Parcelable {
@Parcelize
object None : Config
@Parcelize
object Component : Config
}
} Now we are able to individually control child components. The UI can listen for |
Beta Was this translation helpful? Give feedback.
-
@aartikov I addressed the use case by adding a fun ComponentContext.childContext(key: String, lifecycle: Lifecycle? = null): ComponentContext Now it is possible to create non-permanent components and manually destroy them when no longer needed. The documentation is updated to reflect the change. Here is an example of how the new API can be used to create a dynamic list of child componentsclass Root(
componentContext: ComponentContext
) : ComponentContext by componentContext {
private val _children = MutableValue<List<Child.Created<*, Component>>>(emptyList())
val children: Value<List<Child.Created<*, Component>>> = _children
private var holders: List<Holder> by Delegates.observable(emptyList()) { _, _, newValue ->
_children.value = newValue.map { it.child }
}
init {
set(
stateKeeper
.consume<SavedState>(key = KEY_SAVED_STATE) // Restore previous child configurations
?.childConfigs
?: listOf(Config.A, Config.C) // Or create initial configurations
)
// Save current child configurations
stateKeeper.register(key = KEY_SAVED_STATE) {
SavedState(
childConfigs = holders.map { it.child.configuration },
)
}
}
fun set(list: List<Config>) {
val oldHolders = holders
// Either reuse existing components or create new ones
val newHolders =
list.map { config ->
oldHolders.find { it.child.configuration == config } ?: holder(config)
}
// Destroy all components that are no longer needed
oldHolders.forEach { holder ->
if (holder.child.configuration !in list) {
holder.lifecycle.destroy()
}
}
holders = newHolders
}
private fun holder(config: Config): Holder {
val lifecycle = LifecycleRegistry() // An instance of LifecycleRegistry associated with the new child
val childContext = childContext(key = config.key, lifecycle = lifecycle)
val child = Child.Created(configuration = config, instance = child(config, childContext))
lifecycle.resume()
return Holder(
child = child,
lifecycle = lifecycle,
)
}
private fun child(config: Config, componentContext: ComponentContext): Component =
when (config) {
is Config.A -> Component.A(ComponentA(componentContext))
is Config.B -> Component.B(ComponentB(componentContext))
is Config.C -> Component.C(ComponentC(componentContext))
}
private companion object {
private const val KEY_SAVED_STATE = "SAVED_STATE"
}
@Parcelize
private class SavedState(
val childConfigs: List<Config>
) : Parcelable
private class Holder(
val child: Child.Created<Config, Component>,
val lifecycle: LifecycleRegistry,
)
sealed interface Config : Parcelable {
val key: String
@Parcelize
object A : Config {
override val key: String = "A"
}
@Parcelize
object B : Config {
override val key: String = "B"
}
@Parcelize
object C : Config {
override val key: String = "C"
}
}
sealed interface Component {
class A(val componentA: ComponentA) : Component
class B(val componentB: ComponentB) : Component
class C(val componentC: ComponentC) : Component
}
} |
Beta Was this translation helpful? Give feedback.
-
Hi.
There are two ways to add child components in Decompose:
childContext
to create a permanent child component.router
to control a stack of components.But it doesn't cover the case when we need a dynamic set of components, all are active at the same time.
Imagine an application for a smart home. The main screen contains a bunch of device cards, each with quite complex behavior.
I think the api could be similar to
router
:Ideally, it should make a diff of configuration lists to prevent unwanted component recreations. And a state should be preserved in Android Bundle, of course.
Beta Was this translation helpful? Give feedback.
All reactions