Hello, I have an idea for lich/component. The following is the proposal for the idea.
Summary
This proposal is to suggest to add some new API for lich component. The new APIs will release the component object when given lifecycle takes ON_DESTROY
event, and then it will not be used anymore.
Proposal
Background
In now, the component created by lich is considered as singleton because ComponentFactory<T>
is defined by object
keyword. Also this implementation is guided in README.md
. But this will imply some problems.
- Even if the component is not used anymore, it will not be collected by the GC. This will hold a lot of memory to hold the components. For example, components for log-in Activity, will not be created again if user logged in.
context.getComponent
has some confused definition. This function call looks that the created component will receive the given context
instance. But the component will receive the applicationContext
.
Thanksfully, there is a concept of Lifecycle
and ViewModelStore.clear()
in Android framework. I hope that the lifetime of the component is the same as the lifecycle of context.
Sketched change area
I don't have the concrete implementation for this proposal. So this section is just a sketch to implement this proposal. After this proposal is approved, the concrete implementation should be followed.
The steps to implement this proposal is followings.
- Change the interface of
ComponentProvider
- Implement new observers for each lifecycles
- Implement
DefaultComponentProvider.getComponent
to observe lifecycle.
- Add some new APIs to support new features.
Change of interface
To get the two types of owners, the ComponentProvider
is needed to be changed.
interface ComponentProvider {
fun <T : Any> getComponent(context: Context, factory: ComponentFactory<T>, owner: Any?) : T
}
New ViewModel or LifecycleObserver for releasing component
New classes for releasing the component are needed.
LifecycleObserver
for releasing with the lifecycle
ViewModel
for releasing with the viewmodel lifecycle
// A [ViewModel] for releasing components
// The components will be released with the [ViewModelStore.clear] call.
internal class ResetComponentViewModel(
private val factory: ComponentFactory<*>
): ViewModel() {
override fun onCleared() {
super.onCleared()
ComponentFactory.Accessor.setComponent(factory, null)
}
internal class Factory(
private val factory: ComponentFactory<*>
): ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ResetComponentViewModel(factory) as T
}
}
}
// A [LifecycleObserver] for releasing components
// The components will be released with [ON_DESTROY] event.
internal class ResetComponentLifecycleObserver(
private val factory: ComponentFactory<*>
): DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
ComponentFactory.Accessor.setComponent(factory, null)
}
}
Implement ComponentProvider.getComponent
ComponentProvider
should observe the given lifecycle
using the context
.
internal class DefaultComponentProvider : ComponentProvider {
override fun <T : Any> getComponent(context: Context, factory: ComponentFactory<T>, owner: Any?): T {
val accessor = ComponentFactory.Accessor
accessor.getComponent(factory)?.let {
@Suppress("UNCHECKED_CAST")
return if (it is Creating) it.await() else it as T
}
val creating = Creating()
while (!accessor.compareAndSetComponent(factory, null, creating)) {
accessor.getComponent(factory)?.let {
@Suppress("UNCHECKED_CAST")
return if (it is Creating) it.await() else it as T
}
}
// Pass the given context using lifecycle
val componentContext = when(owner) {
is ViewModelStoreOwner -> context.applicationContext
is LifecycleOwner -> context
else -> context.applicationContext
}
val result = runCatching { accessor.createComponent(factory, componentContext) }
creating.setResult(result)
accessor.setComponent(factory, result.getOrNull())
// observe lifecycle
when(owner) {
is ViewModelStoreOwner -> {
val viewModelFactory = ComponentFactory.ResetComponentViewModel.Factory(factory)
ViewModelProvider(owner, viewModelFactory)
.get<ComponentFactory.ResetComponentViewModel>()
}
is LifecycleOwner -> {
owner.lifecycle.addObserver(
ComponentFactory.ResetComponentLifecycleObserver(factory)
)
}
}
return result.getOrThrow()
}
}
Add new APIs
From the Components.kt
, the entry point of this new features should be provided.
@JvmName("get")
fun <T : Any> Context.getComponent(factory: ComponentFactory<T>): T = when(this) {
// component is bounded to the lifecycle of ViewModel
is ViewModelStoreOwner -> componentProvider.getComponent(applicationContext, factory, this)
// component is bounded to the lifecycle of the android component
is LifecycleOwner -> componentProvider.getComponent(this, factory, this)
// component is singleton
else -> componentProvider.getComponent(applicationContext, factory, null)
}
// This API will be used for the ViewModelProvider.Factory.
// The extras[APPLICATION_KEY] is application object so there is no way to determine whether the given context is bounded to lifecylce.
// So a new API will be needed to support components in ViewModel.
@JvmName("get")
fun <T: Any> CreationExtras.getComponent(factory: ComponentFactory<T>): T {
val context = requireNotNull(this[APPLICATION_KEY])
val viewModelStoreOwner = requireNotNull(this[VIEW_MODEL_STORE_OWNER_KEY])
return componentProvider.getComponent(context, factory, viewModelStoreOwner)
}
Affected impact
If an application is using Lich-component, there is no things to change immediately. The public APIs are not changed, so it is okay to use lich-component, if there is no need to optimize memory. But:
- if you want to optimize memory, then you must change the API calls in
ViewModelProvider.Factory
to use CreationExtras.getComponent
to handle the lifecycle of components in ViewModel
.
- or if your component has some mutable state, then you must change the
context.getComponent
calls to context.applicationContext.getComponent
manually.
Remained problems of above change
The remained problems of the above change are considered as followings.
- If a component is used in multiple lifecycle-aware Android components at once, then
context
object might be leaked. The developers should use applicationContext
for multiple lifecycle-aware Android components.
If you have an idea, please let me know.