Skip to content

Instantly share code, notes, and snippets.

@ologe
Last active October 7, 2020 07:45
Show Gist options
  • Save ologe/8baa21c370eca3aeacdc3a46babc72a6 to your computer and use it in GitHub Desktop.
Save ologe/8baa21c370eca3aeacdc3a46babc72a6 to your computer and use it in GitHub Desktop.
Create an auto cancellable coroutine scope for any View
val View.viewScope: ViewScope
get() {
require(Looper.getMainLooper() == Looper.myLooper())
var scope = getTag(R.id.job_key) as? ViewScope
if (scope != null) {
return scope
}
scope = ViewScope(this)
doOnAttach {
AttachableView(this)
}
setTag(R.id.job_key, scope)
return scope
}
class ViewScope(
view: View
) : CoroutineScope {
private val context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
private val view = WeakReference(view)
override val coroutineContext: CoroutineContext
get() = context
fun launchWhenAttached(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
): Job {
return launch(context) {
view.get()?.awaitOnAttach() ?: return
block()
}
}
}
private class AttachableView(
view: View
) {
private val viewWeak = WeakReference(view)
private val observer = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
viewWeak.get()?.let {
it.removeOnAttachStateChangeListener(detachListener)
it.viewScope.cancel()
}
}
}
private val detachListener = object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) {
v.viewScope.coroutineContext.cancelChildren()
}
override fun onViewAttachedToWindow(v: View?) {}
}
init {
val lifecycle = try {
view.findFragment<Fragment>().viewLifecycleOwner.lifecycle
} catch (ex: IllegalStateException) {
// not child of a fragment
view.findActivity().lifecycle
}
lifecycle.addObserver(observer)
view.addOnAttachStateChangeListener(detachListener)
}
}
suspend fun View.awaitOnAttach() = suspendCancellableCoroutine<Unit> { continuation ->
if (isAttachedToWindow) {
continuation.resume(Unit)
} else {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
removeOnAttachStateChangeListener(this)
continuation.resume(Unit)
}
override fun onViewDetachedFromWindow(v: View) {}
}
addOnAttachStateChangeListener(listener)
continuation.invokeOnCancellation { removeOnAttachStateChangeListener(listener) }
}
}
/**
* Find a [FragmentActivity] associated with a [View].
*
* This method will locate the [FragmentActivity] associated with this view.
*
* Calling this on a View that does not have a FragmentActivity set will result in an
* [IllegalStateException]
*/
fun View.findActivity(): FragmentActivity {
var context: Context = context
while (context is ContextWrapper) {
if (context is FragmentActivity) {
return context
}
context = context.baseContext
}
throw IllegalStateException("View $this does not have a FragmentActivity set")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment