Skip to content

Instantly share code, notes, and snippets.

@pberdnik
Created August 13, 2024 13:11
Show Gist options
  • Save pberdnik/057b7d70bc01c3a0ea8f4f59aca7facc to your computer and use it in GitHub Desktop.
Save pberdnik/057b7d70bc01c3a0ea8f4f59aca7facc to your computer and use it in GitHub Desktop.
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