line / lich

A library collection that enhances the development of Android apps.
Apache License 2.0
194 stars 21 forks source link

[New Feature][Component] Support lifecycle-aware component #136

Closed nullist0 closed 10 months ago

nullist0 commented 1 year ago

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.

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.

  1. Change the interface of ComponentProvider
  2. Implement new observers for each lifecycles
  3. Implement DefaultComponentProvider.getComponent to observe lifecycle.
  4. 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.

// 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:

Remained problems of above change

The remained problems of the above change are considered as followings.

If you have an idea, please let me know.

yamasa commented 1 year ago

Thanks for your great proposal. However, Lich Component is singleton-specific and we have no plans to support component scoping. This way, it is easy to recognize at a glance that the code context.getComponent(FooComponent) returns a singleton. Our goal is not to provide a generic framework for component generation, but to provide a mechanism for handling singletons that are unit testable.

nullist0 commented 12 months ago

@yamasa Thanks for comments, and kind explanation about the goal of this project. I hope that this feature is checked if there are some plans to make more features!