Created
August 13, 2024 13:11
-
-
Save pberdnik/057b7d70bc01c3a0ea8f4f59aca7facc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.ComponentCallbacks | |
import android.os.Bundle | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.activity.ComponentActivity | |
import androidx.annotation.LayoutRes | |
import androidx.appcompat.app.AppCompatActivity | |
import androidx.fragment.app.Fragment | |
import androidx.lifecycle.DefaultLifecycleObserver | |
import androidx.lifecycle.LifecycleOwner | |
import com.google.android.material.bottomsheet.BottomSheetDialogFragment | |
import org.koin.android.ext.android.getKoin | |
import org.koin.android.scope.AndroidScopeComponent | |
import org.koin.core.qualifier.TypeQualifier | |
import org.koin.core.scope.Scope | |
import org.koin.core.scope.ScopeCallback | |
/* Adds an ability to create scope<KoinActivity> in all gradle modules. | |
* Is used ONLY for Koin DI. | |
* DO NOT extend it with other functionality not related to DI. | |
* | |
* Implementations of createActivityScope() and createFragmentScope() are copied from koin library. | |
* The only difference is that getScopeId() and getScopeName() return scope for KoinActivity | |
* and not for its descendant. Since we use single activity architecture it doesn't make any | |
* difference with original code, but now KoinActivity scope can be used in every gradle module. | |
*/ | |
abstract class KoinActivity : AppCompatActivity(), AndroidScopeComponent { | |
override val scope by lazy { createActivityScope() } | |
override fun onCreate(savedInstanceState: Bundle?) { | |
// when creating scope (via createActivityScope()) Koin registers only final class | |
// not its parent; so we need to declare here explicitly that our activity | |
// is descendant of KoinActivity, and that it could be obtained via get<KoinActivity>() | |
scope.declare(this, secondaryTypes = listOf(KoinActivity::class)) | |
super.onCreate(savedInstanceState) | |
} | |
} | |
/* Complements KoinActivity so Fragments get correct KoinActivity scope. | |
* Is used ONLY for Koin DI. | |
* DO NOT extend it with other functionality not related to DI. | |
*/ | |
open class KoinFragment(@LayoutRes contentLayoutId: Int = 0) : Fragment(contentLayoutId), AndroidScopeComponent { | |
override val scope get() = _scope!! | |
private var _scope: Scope? = null | |
/** | |
* Single fragment instance can be used many times by android system. | |
* But semantically it'll be new fragment each time onCreateView() is called. | |
* So we bind Koin scope to fragments view lifecycle. It means we create new scope | |
* in each onCreateView and destroy it in each onDestroyView. scopeCount is increased | |
* after each destroy so next koin scope has different scopeId and doesn't interfere with | |
* previous one. | |
*/ | |
internal var scopeCount = 0 | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
_scope = createFragmentScope() | |
return super.onCreateView(inflater, container, savedInstanceState) | |
} | |
override fun onDestroyView() { | |
super.onDestroyView() | |
_scope?.close() | |
_scope = null | |
scopeCount++ | |
} | |
} | |
open class KoinBottomSheetFragment : BottomSheetDialogFragment(), AndroidScopeComponent { | |
override val scope get() = _scope!! | |
private var _scope: Scope? = null | |
internal var scopeCount = 0 | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
_scope = createFragmentScope() | |
return super.onCreateView(inflater, container, savedInstanceState) | |
} | |
override fun onDestroyView() { | |
super.onDestroyView() | |
_scope?.close() | |
_scope = null | |
scopeCount++ | |
} | |
} | |
private fun Fragment.createFragmentScope(): Scope { | |
val scopeId = getScopeId() | |
val scopeOrNull = getKoin().getScopeOrNull(scopeId) | |
val scope = scopeOrNull ?: createScopeForCurrentFragment() | |
val activityScope = requireActivity().getScopeOrNull() | |
if (activityScope != null) { | |
scope.linkTo(activityScope) | |
} else { | |
scope.logger.debug("Fragment '$this' can't be linked to parent activity scope") | |
} | |
return scope | |
} | |
private fun ComponentActivity.getScopeOrNull(): Scope? = getKoin().getScopeOrNull(getScopeId()) | |
private fun ComponentActivity.createActivityScope(): Scope { | |
val scopeId = getScopeId() | |
val scopeOrNull = getKoin().getScopeOrNull(scopeId) | |
val scope = scopeOrNull ?: createScopeForCurrentLifecycle(this) | |
return scope | |
} | |
private fun Fragment.createScopeForCurrentFragment(): Scope { | |
val scope = getKoin().createScope(getScopeId(), getScopeName(), this) | |
scope.registerCallback(object : ScopeCallback { | |
override fun onScopeClose(scope: Scope) { | |
(this@createScopeForCurrentFragment as AndroidScopeComponent).onCloseScope() | |
} | |
}) | |
return scope | |
} | |
private fun ComponentCallbacks.createScopeForCurrentLifecycle(owner: LifecycleOwner): Scope { | |
val scope = getKoin().createScope(getScopeId(), getScopeName(), this) | |
scope.registerCallback(object : ScopeCallback { | |
override fun onScopeClose(scope: Scope) { | |
(owner as AndroidScopeComponent).onCloseScope() | |
} | |
}) | |
owner.registerScopeForLifecycle(scope) | |
return scope | |
} | |
private fun LifecycleOwner.registerScopeForLifecycle(scope: Scope) { | |
lifecycle.addObserver( | |
object : DefaultLifecycleObserver { | |
override fun onDestroy(owner: LifecycleOwner) { | |
super.onDestroy(owner) | |
scope.close() | |
} | |
} | |
) | |
} | |
private fun ComponentCallbacks.getScopeId(): String { | |
return when (this) { | |
is AppCompatActivity -> KoinActivity::class.qualifiedName + "@" + this.hashCode() | |
is KoinFragment -> this::class.qualifiedName + "@" + this.hashCode() + "@" + this.scopeCount | |
is KoinBottomSheetFragment -> this::class.qualifiedName + "@" + this.hashCode() + "@" + this.scopeCount | |
is Fragment -> this::class.qualifiedName + "@" + this.hashCode() | |
else -> error("Can't create scope") | |
} | |
} | |
private fun ComponentCallbacks.getScopeName(): TypeQualifier { | |
return when (this) { | |
is AppCompatActivity -> TypeQualifier(KoinActivity::class) | |
is Fragment -> TypeQualifier(this::class) | |
else -> error("Can't create scope") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment