Skip to content

Instantly share code, notes, and snippets.

@imminent
Last active March 29, 2018 23:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imminent/623da9a0563374f938c14f1ecafdfa87 to your computer and use it in GitHub Desktop.
Save imminent/623da9a0563374f938c14f1ecafdfa87 to your computer and use it in GitHub Desktop.
OnDestroy Property Delegate
/**
* MIT License
*
* Copyright (c) 2018 Dandré Allison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleObserver
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.experimental.Job
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Delegate function exposed to [LifecycleOwner]s that will destroy a property during the onDestroy
* lifecycle of the owner.
*/
fun <T> LifecycleOwner.onDestroy(finalizer: Finalizer<T>): ReadWriteProperty<LifecycleOwner, T> =
OnDestroyProperty<LifecycleOwner, T>(finalizer).also {
lifecycle.addObserver(it)
}
fun LifecycleOwner.androidJob() = onDestroy<Job> {
cancel()
}
private typealias Finalizer<T> = T.() -> Unit
private val uninitializedValue = Any()
/**
* A property that is destroyed when [LifecycleOwner] is destroyed
*/
private class OnDestroyProperty<in R, T>(finalizer: Finalizer<T>) : ReadWriteProperty<R, T>, LifecycleObserver {
private var finalizer: Finalizer<T>? = finalizer
private var _value: Any? = uninitializedValue
@Suppress("UNCHECKED_CAST")
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
(_value as? T)?.let { finalizer?.invoke(it) }
finalizer = null
_value = null
}
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: R, property: KProperty<*>): T = _value as T
override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
_value = value
}
}
/**
* MIT License
*
* Copyright (c) 2018 Dandré Allison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleObserver
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.experimental.Job
import org.hamcrest.core.Is
import org.hamcrest.core.IsNull
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.lang.ClassCastException
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.jvm.isAccessible
class OnDestroyPropertyTest {
lateinit var lifecycleOwner: LifecycleOwner
var state: Lifecycle.State = Lifecycle.State.INITIALIZED
var observers: Collection<LifecycleObserver> = mutableListOf()
@Before
fun setup() {
lifecycleOwner = LifecycleOwner {
object : Lifecycle() {
override fun addObserver(observer: LifecycleObserver?) {
observer?.let { observers += observer }
}
override fun removeObserver(observer: LifecycleObserver?) {
observer?.let { observers -= observer }
}
override fun getCurrentState() = state
}
}
}
@Test
fun getValueReturnsValue() {
class TestClass : LifecycleOwner by lifecycleOwner {
var property: String by onDestroy { }
}
val testObject = TestClass()
testObject.property = "yay"
assertThat(testObject.property, Is.`is`("yay"))
}
@Test
fun destroyProperty() {
var onDestroyCalled = false
class TestClass : LifecycleOwner by lifecycleOwner {
var property: String by onDestroy { onDestroyCalled = true }
}
val testObject = TestClass()
testObject.property = "yay"
observers.triggerOnDestroy()
assertTrue(onDestroyCalled)
assertThat(testObject.property, IsNull.nullValue())
}
@Test
fun nothingDestroyedYet() {
var onDestroyCalled = false
class TestClass : LifecycleOwner by lifecycleOwner {
var property: String by onDestroy { onDestroyCalled = true }
}
val testObject = TestClass()
testObject.property = "yay"
assertFalse(onDestroyCalled)
}
@Test
fun androidJobUnset() {
class TestClass : LifecycleOwner by lifecycleOwner {
var job: Job by androidJob()
}
val testObject = TestClass()
try {
assertFalse(testObject.job.isCancelled)
fail("No Job set, should cause class cast issue")
} catch (error: ClassCastException) {
}
}
@Test
fun androidJobNotCanceled() {
class TestClass : LifecycleOwner by lifecycleOwner {
var job: Job by androidJob()
}
val testObject = TestClass()
testObject.job = Job()
assertFalse(testObject.job.isCancelled)
}
@Test
fun cancelAndroidJobOnDestroy() {
class TestClass : LifecycleOwner by lifecycleOwner {
var job: Job by androidJob()
}
val testObject = TestClass()
testObject.job = Job()
observers.triggerOnDestroy()
assertThat(testObject.job, IsNull.nullValue())
}
private fun Collection<LifecycleObserver>.triggerOnDestroy() {
forEach { observer ->
observer::class.memberFunctions.asSequence()
.filter {
it.annotations.any { annotation ->
(annotation as? OnLifecycleEvent)?.value == Lifecycle.Event.ON_DESTROY
}
}
.forEach {
it.isAccessible = true
System.out.print("${it.name}: ${it.parameters.joinToString()}")
it.call(observer)
it.isAccessible = false
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment