InsertKoinIO / koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform
https://insert-koin.io
Apache License 2.0
8.92k stars 711 forks source link

Addition to KoinScopeComponent by delegation instead of concrete component classes only in v2.2.0-beta-1 #894

Closed wax911 closed 3 years ago

wax911 commented 3 years ago

Is your feature request related to a problem? Please describe. Firstly thank you for the awesome work so far, I love koin and have been using it for more than a year now :100:! Seeing how scope and lifecycleScope have been deprecated in favour of ScopeActivity, ScopeFragment, ScopeService which is a great change, however in most cases (I don't speak for everyone) most people tend to have a custom activity, fragment or service base classes either it be a framework in use or just an additional layer of abstraction.

In such a case providing concrete abstractions of activity, fragment or service may prove difficult in such a case were a person is already using another framework e.g. workflow, mvi-core, conductor e.t.c I am aware that KoinScopeComponent is an interface which provides the flexibility for the developer to implement and mirror the Scope* components, but I believe it maybe a nice addition to offer these scope components through delegation.

Describe the solution you'd like I have created the following structures to demonstrate the use-case:

Sample implementation of KoinScopeComponent
package co.anitrend.core.ui.component

import co.anitrend.arch.extension.ext.UNSAFE
import org.koin.core.context.GlobalContext
import org.koin.core.scope.KoinScopeComponent
import org.koin.core.scope.ScopeID

class KoinScope : KoinScopeComponent {

    private val scopeID: ScopeID by lazy(UNSAFE) { getScopeId() }

    override val koin by lazy(UNSAFE) {
        GlobalContext.get()
    }

    override val scope by lazy(UNSAFE) {
        createScope(scopeID, getScopeName(), this)
    }
}

Given a scope component class that implements the KoinScopeComponent similar to what ScopeActivity, ScopeFragment, ScopeService would do, with the exception of using GlobalContext for koin since we cannot provide this with a KoinComponent interface.

Function for delegation providing KoinScope
package co.anitrend.core.extension

@Suppress("FunctionName")
fun ScopeComponent() = KoinScope()

Provides an instance of KoinScope which we will use later

Implementing on android service class
package co.anitrend.core.service

import androidx.lifecycle.LifecycleService
import co.anitrend.core.ui.ScopeComponent
import org.koin.core.scope.KoinScopeComponent

abstract class AniTrendService : LifecycleService(), KoinScopeComponent by ScopeComponent() {

    override fun onCreate() {
        super.onCreate()
        runCatching {
            koin._logger.debug("Open activity scope: $scope")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        runCatching {
            koin._logger.debug("Close service scope: $scope")
            scope.close()
        }
    }
}

Demonstrating the use of a different service super class but not limited to LifecycleService, added runCatching just to allow the used of scope even when the component in used does not have any modules tied to it. Extension functions for handling both cases will be provided at the end.

Implementing on android fragment/activity class e.t.c
package co.anitrend.core.ui.fragment

import android.os.Bundle
import android.view.View
import co.anitrend.arch.core.model.ISupportViewModelState
import co.anitrend.arch.ui.fragment.SupportFragment
import co.anitrend.core.ui.ScopeComponent
import org.koin.core.scope.KoinScopeComponent

abstract class AniTrendFragment : SupportFragment(), KoinScopeComponent by ScopeComponent() {

    /**
     * Proxy for a view model state if one exists
     */
    override fun viewModelState(): ISupportViewModelState<*>? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        runCatching {
            koin._logger.debug("Open fragment scope: $scope")
        }
    }

    /**
     * Called when the fragment is no longer in use. This is called
     * after [.onStop] and before [.onDetach].
     */
    override fun onDestroy() {
        super.onDestroy()
        runCatching {
            koin._logger.debug("Close fragment scope: $scope")
            scope.close()
        }
    }
}

Demonstrating implementation in a case where a framework provides an abstraction layer over the standard Fragment class.

Optional extension functions for resolving scope modules
package co.anitrend.core.extension

import co.anitrend.arch.extension.ext.UNSAFE
import co.anitrend.core.ui.component.KoinScope
import co.anitrend.core.ui.fragment.model.FragmentItem
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.KoinScopeComponent

/**
 * Get given dependency
 *
 * @param qualifier - bean qualifier / optional
 * @param parameters - injection parameters
 */
inline fun <reified T : Any> KoinScopeComponent.get(
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null
): T = runCatching {
    scope.get<T>(qualifier, parameters)
}.getOrElse {
    koin.get(qualifier, parameters)
}

/**
 * Inject lazily
 *
 * @param qualifier - bean qualifier / optional
 * @param parameters - injection parameters
 */
inline fun <reified T : Any> KoinScopeComponent.inject(
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null
) = lazy(UNSAFE) { get<T>(qualifier, parameters) }

@Suppress("FunctionName")
fun ScopeComponent() = KoinScope()

As demonstrated above the following extension function are just an addition and not really mandatory for everyone's case, but more of a convenient way of switching back and forth between modules tied to a scope and unbound modules. Perhaps not the best way since the extension functions may shadow ComponentCallback.get and ComponentCallback.inject which someone could easily overlook when working with scope.

Describe alternatives you've considered Prior to the use-case above I was implementing KoinScopeComponent directly and copying the contents of ScopeActivity, ScopeFragment, ScopeService e.t.c but I worried that I may miss future changes/improvements that might be added to the Scope* components. The obvious limitation of the provided examples is the inability to provide the source to KoinScopeComponent.createScope(scopeID: ScopeID, scopeName: Qualifier, source: Any? = null) as this in KoinScope.scope would refer to the context of KoinScope

Target Koin project

Conclusion I would like to know what you think about this and possibly shed some light over potential shortcomings. Thanks again :smiley:

arnaudgiuliani commented 3 years ago

You can check the last rc-3, with proposal of KoinScopeComponent design and new delegate solutions.

https://github.com/InsertKoinIO/koin/blob/master/koin-projects/koin-core/src/main/kotlin/org/koin/core/scope/KoinScopeComponent.kt

https://github.com/InsertKoinIO/koin/blob/master/koin-projects/koin-androidx-scope/src/main/java/org/koin/androidx/scope/ComponentActivityExt.kt#L10

wax911 commented 3 years ago

Thank you @arnaudgiuliani I have actually been using both KoinScopeComponent and ComponentActivityExt with activityScope() and fragmentScope in rc-3 which has indeed improved the ease of maintaining of KoinScopeComponent significantly

arnaudgiuliani commented 3 years ago

great :)

egorikftp commented 3 years ago

@wax911 Do you have an example of usages this delegate? Thanks

wax911 commented 3 years ago

Hi @egorikftp I used to, but since rc-3 introduced activityScope and fragmentScope I have long since moved from this approach, as I had pointed out earlier in the issue, the main limitation with the delegate approach is that if you plan to scope anything it would have to be tied to the delegate class ScopeComponent, anything that would try to access scope modules would result in a definition not found since the ScopeComponent would be final

Instead I've been using the APIs mentioned by @arnaudgiuliani above that does an awesome job of abstracting away the creation of scope objects so you don't have to worry about keeping the implementation details up to date