View <-> LiveData connection tweaks, some re-org

This commit is contained in:
Aidan Follestad 2018-12-07 00:11:13 -08:00
commit fc6bdf1c39
85 changed files with 737 additions and 373 deletions

View file

@ -21,12 +21,12 @@ import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.viewcomponents.ext.attachLiveData
import com.afollestad.nocknock.viewcomponents.ext.conceal import com.afollestad.nocknock.viewcomponents.ext.conceal
import com.afollestad.nocknock.viewcomponents.ext.onLayout import com.afollestad.nocknock.viewcomponents.ext.onLayout
import com.afollestad.nocknock.viewcomponents.ext.toViewError import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.ext.toViewText import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_addsite.doneBtn import kotlinx.android.synthetic.main.activity_addsite.doneBtn
import kotlinx.android.synthetic.main.activity_addsite.inputName import kotlinx.android.synthetic.main.activity_addsite.inputName
@ -88,9 +88,11 @@ class AddSiteActivity : AppCompatActivity() {
.toViewError(this, responseTimeoutInput) .toViewError(this, responseTimeoutInput)
// Validation mode // Validation mode
responseValidationMode.attachLiveData(this, viewModel.validationMode, responseValidationMode.attachLiveData(
{ ValidationMode.fromIndex(it) }, lifecycleOwner = this,
{ it.toIndex() } data = viewModel.validationMode,
outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() }
) )
viewModel.onValidationSearchTermError() viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm) .toViewError(this, responseValidationSearchTerm)
@ -98,7 +100,11 @@ class AddSiteActivity : AppCompatActivity() {
.toViewText(this, validationModeDescription) .toViewText(this, validationModeDescription)
// Validation search term // Validation search term
responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm) responseValidationSearchTerm.attachLiveData(
lifecycleOwner = this,
data = viewModel.validationSearchTerm,
pullInChanges = false
)
viewModel.onValidationSearchTermVisibility() viewModel.onValidationSearchTermVisibility()
.toViewVisibility(this, responseValidationSearchTerm) .toViewVisibility(this, responseValidationSearchTerm)

View file

@ -32,8 +32,8 @@ import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.putSite import com.afollestad.nocknock.data.putSite
import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import com.afollestad.nocknock.viewcomponents.ext.map
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -50,7 +50,9 @@ class AddSiteViewModel(
// Public properties // Public properties
val name = MutableLiveData<String>() val name = MutableLiveData<String>()
val url = MutableLiveData<String>() val url = MutableLiveData<String>()
val timeout = MutableLiveData<Int>() val timeout = MutableLiveData<Int>().apply {
this.value = 10000
}
val validationMode = MutableLiveData<ValidationMode>() val validationMode = MutableLiveData<ValidationMode>()
val validationSearchTerm = MutableLiveData<String>() val validationSearchTerm = MutableLiveData<String>()
val validationScript = MutableLiveData<String>() val validationScript = MutableLiveData<String>()
@ -214,8 +216,8 @@ class AddSiteViewModel(
) )
return Site( return Site(
id = 0, id = 0,
name = name.value!!, name = name.value!!.trim(),
url = url.value!!, url = url.value!!.trim(),
settings = newSettings, settings = newSettings,
lastResult = null lastResult = null
) )

View file

@ -26,12 +26,12 @@ import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.ext.attachLiveData import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.onScroll import com.afollestad.nocknock.viewcomponents.ext.onScroll
import com.afollestad.nocknock.viewcomponents.ext.toViewError import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.ext.toViewText import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
@ -102,9 +102,12 @@ class ViewSiteActivity : AppCompatActivity() {
.toViewError(this, responseTimeoutInput) .toViewError(this, responseTimeoutInput)
// Validation mode // Validation mode
responseValidationMode.attachLiveData(this, viewModel.validationMode, responseValidationMode.attachLiveData(
{ ValidationMode.fromIndex(it) }, lifecycleOwner = this,
{ it.toIndex() }) data = viewModel.validationMode,
outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() }
)
viewModel.onValidationSearchTermError() viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm) .toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription() viewModel.onValidationModeDescription()

View file

@ -38,10 +38,10 @@ import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.formatDate import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.utilities.livedata.zip
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import com.afollestad.nocknock.viewcomponents.ext.map
import com.afollestad.nocknock.viewcomponents.ext.zip
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -232,8 +232,8 @@ class ViewSiteViewModel(
@VisibleForTesting(otherwise = PRIVATE) @VisibleForTesting(otherwise = PRIVATE)
fun getValidationArgs(): String? { fun getValidationArgs(): String? {
return when (validationMode.value) { return when (validationMode.value) {
TERM_SEARCH -> validationSearchTerm.value TERM_SEARCH -> validationSearchTerm.value?.trim()
JAVASCRIPT -> validationScript.value JAVASCRIPT -> validationScript.value?.trim()
else -> null else -> null
} }
} }
@ -310,8 +310,8 @@ class ViewSiteViewModel(
disabled = false disabled = false
) )
return site.copy( return site.copy(
name = name.value!!, name = name.value!!.trim(),
url = url.value!!, url = url.value!!.trim(),
settings = newSettings settings = newSettings
) )
.withStatus(status = WAITING) .withStatus(status = WAITING)

View file

@ -93,7 +93,7 @@
<include layout="@layout/include_divider"/> <include layout="@layout/include_divider"/>
<com.afollestad.nocknock.viewcomponents.interval.CheckIntervalLayout <com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
android:id="@+id/checkIntervalLayout" android:id="@+id/checkIntervalLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -115,7 +115,7 @@
android:layout_marginTop="@dimen/content_inset_less" android:layout_marginTop="@dimen/content_inset_less"
/> />
<com.afollestad.nocknock.viewcomponents.interval.CheckIntervalLayout <com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
android:id="@+id/checkIntervalLayout" android:id="@+id/checkIntervalLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -24,7 +24,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.mockDatabase import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.test import com.afollestad.nocknock.utilities.livedata.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.argumentCaptor

View file

@ -23,7 +23,7 @@ import com.afollestad.nocknock.MOCK_MODEL_3
import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.mockDatabase import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.test import com.afollestad.nocknock.utilities.livedata.test
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View file

@ -31,7 +31,7 @@ import com.afollestad.nocknock.data.model.ValidationResult
import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.mockDatabase import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.test import com.afollestad.nocknock.utilities.livedata.test
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
import com.google.common.truth.Truth import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat

View file

@ -20,7 +20,7 @@ android {
dependencies { dependencies {
implementation 'androidx.annotation:annotation:' + versions.androidx implementation 'androidx.annotation:annotation:' + versions.androidx
implementation 'androidx.appcompat:appcompat:' + versions.androidx api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines
@ -30,9 +30,8 @@ dependencies {
implementation 'org.mozilla:rhino:' + versions.rhino implementation 'org.mozilla:rhino:' + versions.rhino
testImplementation 'junit:junit:' + versions.junit testImplementation 'junit:junit:' + versions.junit
testImplementation 'org.mockito:mockito-core:' + versions.mockito
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
testImplementation 'com.google.truth:truth:' + versions.truth testImplementation 'com.google.truth:truth:' + versions.truth
testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting
} }
apply from: '../spotless.gradle' apply from: '../spotless.gradle'

View file

@ -18,9 +18,29 @@ package com.afollestad.nocknock.utilities.ext
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.widget.EditText import android.widget.EditText
import androidx.annotation.IntRange
import kotlin.math.min
fun EditText.onTextChanged(cb: (String) -> Unit) { fun EditText.setTextAndMaintainSelection(text: CharSequence) {
val formerStart = min(selectionStart, text.length)
val formerEnd = min(selectionEnd, text.length)
setText(text)
if (formerEnd <= formerStart) {
setSelection(formerStart)
} else {
setSelection(formerStart, formerEnd)
}
}
fun EditText.onTextChanged(
@IntRange(from = 0, to = 10000) debounce: Int = 0,
cb: (String) -> Unit
) {
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
val callbackRunner = Runnable {
cb(text.trim().toString())
}
override fun afterTextChanged(s: Editable?) = Unit override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged( override fun beforeTextChanged(
@ -35,6 +55,13 @@ fun EditText.onTextChanged(cb: (String) -> Unit) {
start: Int, start: Int,
before: Int, before: Int,
count: Int count: Int
) = cb(s.toString().trim()) ) {
removeCallbacks(callbackRunner)
if (debounce == 0) {
callbackRunner.run()
} else {
postDelayed(callbackRunner, debounce.toLong())
}
}
}) })
} }

View file

@ -0,0 +1,53 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.utilities.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
/** @author Aidan Follestad (@afollestad) */
class DistinctLiveData<T>(source1: LiveData<T>) : MediatorLiveData<T>() {
private var isInitialized = false
private var lastValue: T? = null
init {
super.addSource(source1) {
if (!isInitialized) {
value = it
isInitialized = true
lastValue = it
} else if (lastValue != it) {
value = it
lastValue = it
}
}
}
override fun <S : Any?> addSource(
source: LiveData<S>,
onChanged: Observer<in S>
) {
throw UnsupportedOperationException()
}
override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
throw UnsupportedOperationException()
}
}
fun <T> LiveData<T>.distinct(): MediatorLiveData<T> = DistinctLiveData(this)

View file

@ -0,0 +1,25 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.utilities.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
fun <X, Y> LiveData<X>.map(mapper: (X) -> Y) =
Transformations.map(this, mapper)!!
fun <X, Y> LiveData<X>.switchMap(mapper: (X) -> LiveData<Y>) =
Transformations.switchMap(this, mapper)!!

View file

@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.afollestad.nocknock package com.afollestad.nocknock.utilities.livedata
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class TestLiveData<T>(data: LiveData<T>) { class TestLiveData<T>(data: LiveData<T>) {
@ -34,17 +32,32 @@ class TestLiveData<T>(data: LiveData<T>) {
} }
fun assertNoValues() { fun assertNoValues() {
assertWithMessage("Expected no values, but got: $receivedValues").that(receivedValues) if (receivedValues.isNotEmpty()) {
.isEmpty() throw AssertionError("Expected no values, but got: $receivedValues")
}
} }
fun assertValues(vararg assertValues: T) { fun assertValues(vararg assertValues: T) {
val assertList = assertValues.toList() val assertList = assertValues.toList()
assertThat(receivedValues).isEqualTo(assertList) if (!assertList.contentEquals(receivedValues)) {
throw AssertionError("Expected $assertList\n\t\tBut got: $receivedValues")
}
receivedValues.clear() receivedValues.clear()
} }
@CheckResult fun values(): List<T> = receivedValues @CheckResult fun values(): List<T> = receivedValues
private fun List<T>.contentEquals(other: List<T>): Boolean {
if (this.size != other.size) {
return false
}
for ((index, value) in this.withIndex()) {
if (other[index] != value) {
return false
}
}
return true
}
} }
@CheckResult fun <T> LiveData<T>.test() = TestLiveData(this) @CheckResult fun <T> LiveData<T>.test() = TestLiveData(this)

View file

@ -0,0 +1,102 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.utilities.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
typealias Zipper<T, K, R> = (T, K) -> R
/** @author Aidan Follestad (@afollestad) */
class ZipLiveData<T, K, R>(
source1: LiveData<T>,
source2: LiveData<K>,
private val distinctUntilChanged: Boolean,
private val resetAfterEmission: Boolean,
private val zipper: Zipper<T, K, R>
) : MediatorLiveData<R>() {
private var data1: T? = null
private var data2: K? = null
private var lastNotified: R? = null
init {
super.addSource(source1) {
if (data1 == it) return@addSource
data1 = it
maybeNotify()
}
super.addSource(source2) {
if (data2 == it) return@addSource
data2 = it
maybeNotify()
}
}
private fun maybeNotify() {
if (data1 != null && data2 != null) {
val zippedUp = zipper(data1!!, data2!!)
if (!distinctUntilChanged || zippedUp != lastNotified) {
value = zippedUp
lastNotified = zippedUp
if (resetAfterEmission) {
data1 = null
data2 = null
}
}
}
}
override fun <S : Any?> addSource(
source: LiveData<S>,
onChanged: Observer<in S>
) {
throw UnsupportedOperationException()
}
override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
throw UnsupportedOperationException()
}
}
fun <T, K, R> zip(
source1: LiveData<T>,
source2: LiveData<K>,
distinctUntilChanged: Boolean = true,
resetAfterEmission: Boolean = false,
zipper: Zipper<T, K, R>
) = ZipLiveData(
source1 = source1,
source2 = source2,
distinctUntilChanged = distinctUntilChanged,
resetAfterEmission = resetAfterEmission,
zipper = zipper
)
fun <T, K> zip(
source1: LiveData<T>,
source2: LiveData<K>,
distinctUntilChanged: Boolean = true,
resetAfterEmission: Boolean = false
) = zip(
source1 = source1,
source2 = source2,
distinctUntilChanged = distinctUntilChanged,
resetAfterEmission = resetAfterEmission,
zipper = { left, right -> Pair(left, right) })

View file

@ -0,0 +1,47 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.utilities.livedata
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import org.junit.Rule
import org.junit.Test
/** @author Aidan Follestad (@afollestad) */
class DistinctTest {
@Rule @JvmField val rule = InstantTaskExecutorRule()
@Test fun filterLastValues() {
val data = MutableLiveData<String>()
val distinct = data.distinct()
.test()
data.postValue("Hello")
data.postValue("Hello")
data.postValue("Hi")
data.postValue("Hi")
data.postValue("Hello")
distinct.assertValues(
"Hello",
"Hi",
"Hello"
)
}
}

View file

@ -0,0 +1,123 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.utilities.livedata
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import org.junit.Rule
import org.junit.Test
/** @author Aidan Follestad (@afollestad) */
class ZipTest {
@Rule @JvmField val rule = InstantTaskExecutorRule()
@Test fun test_withDistinct() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, true)
.test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues(Pair("Hello", 24))
data1.postValue("Hello")
data2.postValue(24)
zipped.assertNoValues()
}
@Test fun test_noDistinct() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, false)
.test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues(Pair("Hello", 24))
data1.postValue("Hi")
data2.postValue(24)
zipped.assertValues(Pair("Hi", 24))
}
@Test fun test_noDistinct_resetAfterEmission() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, false, true)
.test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues(Pair("Hello", 24))
data1.postValue("Hi")
data2.postValue(50)
zipped.assertValues(Pair("Hi", 50))
}
@Test fun test_withDistinct_customZipper() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, true,
zipper = { left, right ->
"$left $right"
}).test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues("Hello 24")
data1.postValue("Hello")
data2.postValue(24)
zipped.assertNoValues()
}
@Test fun test_noDistinct_customZipper() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, false,
zipper = { left, right ->
"$left $right"
}).test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues("Hello 24")
data1.postValue("Hi")
data2.postValue(24)
zipped.assertValues("Hi 24")
}
@Test fun test_noDistinct_customZipper_resetAfterEmission() {
val data1 = MutableLiveData<String>()
val data2 = MutableLiveData<Int>()
val zipped = zip(data1, data2, false, true,
zipper = { left, right ->
"$left $right"
}).test()
data1.postValue("Hello")
data2.postValue(24)
zipped.assertValues("Hello 24")
data1.postValue("Hi")
data2.postValue(50)
zipped.assertValues("Hi 50")
}
}

View file

@ -1,148 +0,0 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.viewcomponents.ext
import android.view.View
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import com.afollestad.nocknock.utilities.ext.onTextChanged
fun <X, Y> LiveData<X>.map(mapper: (X) -> Y) =
Transformations.map(this, mapper)!!
fun <X, Y> LiveData<X>.switchMap(mapper: (X) -> LiveData<Y>) =
Transformations.switchMap(this, mapper)!!
inline fun <reified T> EditText.attachLiveData(
lifecycleOwner: LifecycleOwner,
data: MutableLiveData<T>
) {
// Out
when {
T::class == Int::class -> {
onTextChanged { data.postValue(it.trim().toInt() as T) }
}
T::class == Long::class -> {
onTextChanged { data.postValue(it.trim().toLong() as T) }
}
T::class == String::class -> {
onTextChanged { data.postValue(it.trim() as T) }
}
else -> {
throw IllegalArgumentException("Can't send EditText text changes into ${T::class}")
}
}
// In
data.observe(lifecycleOwner, Observer {
when {
T::class == Int::class -> setText(it as Int)
T::class == String::class -> setText(it as String)
}
})
}
fun <T> Spinner.attachLiveData(
lifecycleOwner: LifecycleOwner,
data: MutableLiveData<T>,
outTransformer: (Int) -> T,
inTransformer: (T) -> Int
) {
// Out
onItemSelected { data.postValue(outTransformer(it)) }
// In
data.observe(lifecycleOwner, Observer {
setSelection(inTransformer(it))
})
}
fun LiveData<Int?>.toViewError(
owner: LifecycleOwner,
view: EditText
) = observe(owner, Observer { error ->
view.error = if (error != null) {
view.resources.getString(error)
} else {
null
}
})
inline fun <reified T> LiveData<T>.toViewText(
owner: LifecycleOwner,
view: TextView
) = observe(owner, Observer {
when {
T::class == Int::class -> view.setText(it as Int)
T::class == String::class -> view.text = it as String
else -> throw IllegalStateException("Cannot set ${T::class} as view text.")
}
})
fun LiveData<Boolean>.toViewVisibility(
owner: LifecycleOwner,
view: View
) = observe(owner, Observer { view.showOrHide(it) })
/** @author Aidan Follestad (@afollestad) */
class ZipLiveData<T, K>(
source1: LiveData<T>,
source2: LiveData<K>
) : MediatorLiveData<Pair<T, K>>() {
private var data1: T? = null
private var data2: K? = null
init {
super.addSource(source1) {
data1 = it
maybeNotify()
}
super.addSource(source2) {
data2 = it
maybeNotify()
}
}
private fun maybeNotify() {
if (data1 != null && data2 != null) {
value = Pair(data1!!, data2!!)
}
}
override fun <S : Any?> addSource(
source: LiveData<S>,
onChanged: Observer<in S>
) {
throw UnsupportedOperationException()
}
override fun <T : Any?> removeSource(toRemote: LiveData<T>) {
throw UnsupportedOperationException()
}
}
fun <T, K> zip(
source1: LiveData<T>,
source2: LiveData<K>
): MediatorLiveData<Pair<T, K>> {
return ZipLiveData(source1, source2)
}

View file

@ -1,133 +0,0 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.viewcomponents.interval
import android.content.Context
import android.util.AttributeSet
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import androidx.lifecycle.Lifecycle.State.DESTROYED
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK
import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.viewcomponents.R.array
import com.afollestad.nocknock.viewcomponents.R.layout
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
import kotlinx.android.synthetic.main.check_interval_layout.view.input
import kotlinx.android.synthetic.main.check_interval_layout.view.spinner
/** @author Aidan Follestad (@afollestad) */
class CheckIntervalLayout(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs), LifecycleOwner {
companion object {
const val INDEX_MINUTE = 0
const val INDEX_HOUR = 1
const val INDEX_DAY = 2
const val INDEX_WEEK = 3
}
init {
orientation = VERTICAL
inflate(context, layout.check_interval_layout, this)
}
private lateinit var valueData: MutableLiveData<Int>
private lateinit var multiplierData: MutableLiveData<Long>
private val lifecycle = LifecycleRegistry(this)
override fun onFinishInflate() {
super.onFinishInflate()
val spinnerAdapter = ArrayAdapter(
context,
layout.list_item_spinner,
resources.getStringArray(array.interval_options)
)
spinnerAdapter.setDropDownViewResource(
layout.list_item_spinner_dropdown
)
spinner.adapter = spinnerAdapter
lifecycle.markState(STARTED)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
lifecycle.markState(RESUMED)
}
override fun onDetachedFromWindow() {
lifecycle.markState(DESTROYED)
super.onDetachedFromWindow()
}
fun setError(error: String?) {
input.error = error
}
fun attach(
valueData: MutableLiveData<Int>,
multiplierData: MutableLiveData<Long>,
errorData: LiveData<Int?>
) {
this.valueData = valueData
this.multiplierData = multiplierData
this.valueData.observe(this, Observer {
input.setText("$it")
})
this.multiplierData.observe(this, Observer { multiplier ->
val targetPos = when (multiplier) {
MINUTE -> 0
HOUR -> 1
DAY -> 2
WEEK -> 3
else -> throw IllegalStateException("Unknown multiplier: $multiplier")
}
if (spinner.selectedItemPosition != targetPos) {
spinner.setSelection(targetPos)
}
})
errorData.observe(this, Observer {
setError(if (it != null) resources.getString(it) else null)
})
input.onTextChanged { this.valueData.value = it.toInt() }
spinner.onItemSelected {
this.multiplierData.value = when (it) {
INDEX_MINUTE -> MINUTE
INDEX_HOUR -> HOUR
INDEX_DAY -> DAY
INDEX_WEEK -> WEEK
else -> throw IllegalStateException("Unknown multiplier index: $it")
}
}
}
override fun getLifecycle() = lifecycle
}

View file

@ -0,0 +1,101 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.viewcomponents.interval
import android.content.Context
import android.util.AttributeSet
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK
import com.afollestad.nocknock.viewcomponents.R.array
import com.afollestad.nocknock.viewcomponents.R.layout
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import kotlinx.android.synthetic.main.validation_interval_layout.view.input
import kotlinx.android.synthetic.main.validation_interval_layout.view.spinner
/** @author Aidan Follestad (@afollestad) */
class ValidationIntervalLayout(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
companion object {
const val INDEX_MINUTE = 0
const val INDEX_HOUR = 1
const val INDEX_DAY = 2
const val INDEX_WEEK = 3
}
init {
orientation = VERTICAL
inflate(context, layout.validation_interval_layout, this)
}
override fun onFinishInflate() {
super.onFinishInflate()
val spinnerAdapter = ArrayAdapter(
context,
layout.list_item_spinner,
resources.getStringArray(array.interval_options)
)
spinnerAdapter.setDropDownViewResource(
layout.list_item_spinner_dropdown
)
spinner.adapter = spinnerAdapter
}
fun attach(
valueData: MutableLiveData<Int>,
multiplierData: MutableLiveData<Long>,
errorData: LiveData<Int?>
) {
input.attachLiveData(lifecycleOwner(), valueData)
spinner.attachLiveData(
lifecycleOwner = lifecycleOwner(),
data = multiplierData,
inTransformer = {
when (it) {
MINUTE -> 0
HOUR -> 1
DAY -> 2
WEEK -> 3
else -> throw IllegalStateException("Unknown multiplier: $it")
}
},
outTransformer = {
when (it) {
INDEX_MINUTE -> MINUTE
INDEX_HOUR -> HOUR
INDEX_DAY -> DAY
INDEX_WEEK -> WEEK
else -> throw IllegalStateException("Unknown multiplier index: $it")
}
}
)
errorData.toViewError(lifecycleOwner(), this, ::setError)
}
private fun setError(error: String?) {
input.error = error
}
}

View file

@ -18,22 +18,17 @@ package com.afollestad.nocknock.viewcomponents.js
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.HorizontalScrollView import android.widget.HorizontalScrollView
import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.Lifecycle.State.DESTROYED
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.viewcomponents.R.dimen import com.afollestad.nocknock.viewcomponents.R.dimen
import com.afollestad.nocknock.viewcomponents.R.layout import com.afollestad.nocknock.viewcomponents.R.layout
import com.afollestad.nocknock.viewcomponents.R.string
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.dimenInt import com.afollestad.nocknock.viewcomponents.ext.dimenInt
import com.afollestad.nocknock.viewcomponents.ext.showOrHide import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
@ -41,10 +36,7 @@ import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
class JavaScriptInputLayout( class JavaScriptInputLayout(
context: Context, context: Context,
attrs: AttributeSet? = null attrs: AttributeSet? = null
) : HorizontalScrollView(context, attrs), LifecycleOwner { ) : HorizontalScrollView(context, attrs) {
private val lifecycle = LifecycleRegistry(this)
private lateinit var codeData: MutableLiveData<String>
init { init {
val contentInset = dimenInt(dimen.content_inset) val contentInset = dimenInt(dimen.content_inset)
@ -61,51 +53,20 @@ class JavaScriptInputLayout(
inflate(context, layout.javascript_input_layout, this) inflate(context, layout.javascript_input_layout, this)
} }
override fun onFinishInflate() {
super.onFinishInflate()
lifecycle.markState(CREATED)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
lifecycle.markState(RESUMED)
}
override fun onDetachedFromWindow() {
lifecycle.markState(DESTROYED)
super.onDetachedFromWindow()
}
fun setError(error: String?) {
error_text.showOrHide(error != null)
error_text.text = error
}
fun attach( fun attach(
codeData: MutableLiveData<String>, codeData: MutableLiveData<String>,
errorData: LiveData<Int?>, errorData: LiveData<Int?>,
visibility: LiveData<Boolean> visibility: LiveData<Boolean>
) { ) {
this.codeData = codeData userInput.attachLiveData(lifecycleOwner(), codeData)
this.codeData.observe(this, Observer { errorData.toViewError(lifecycleOwner(), this, ::setError)
if (it.isNullOrEmpty()) { visibility.toViewVisibility(lifecycleOwner(), this)
setDefaultCode()
} else {
userInput.setText(it)
}
})
errorData.observe(this, Observer {
setError(if (it != null) resources.getString(it) else null)
})
visibility.toViewVisibility(this, this)
userInput.onTextChanged { this.codeData.value = it }
} }
fun clear() = userInput.setText("") fun clear() = userInput.setText("")
private fun setDefaultCode() = userInput.setText( private fun setError(error: String?) {
string.default_js error_text.showOrHide(error != null)
) error_text.text = error
}
override fun getLifecycle() = lifecycle
} }

View file

@ -0,0 +1,120 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.viewcomponents.livedata
import android.view.View
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
inline fun <reified T> EditText.attachLiveData(
lifecycleOwner: LifecycleOwner,
data: MutableLiveData<T>,
debounce: Int = 100,
pushOutChanges: Boolean = true,
pullInChanges: Boolean = false
) {
// Initial value
if (T::class == String::class) {
data.value = this.text.trim().toString() as T
}
// Out
if (pushOutChanges) {
when {
T::class == Int::class -> {
onTextChanged(debounce) { data.postValue(it.trim().toInt() as T) }
}
T::class == Long::class -> {
onTextChanged(debounce) { data.postValue(it.trim().toLong() as T) }
}
T::class == String::class -> {
onTextChanged(debounce) { data.postValue(it as T) }
}
else -> {
throw IllegalArgumentException("Can't send EditText text changes into ${T::class}")
}
}
}
// In
if (pullInChanges) {
data.distinct()
.observe(lifecycleOwner, Observer {
when {
T::class == Int::class -> setText(it as Int)
T::class == String::class -> setTextAndMaintainSelection(it as String)
}
})
}
}
fun <T> Spinner.attachLiveData(
lifecycleOwner: LifecycleOwner,
data: MutableLiveData<T>,
outTransformer: (Int) -> T,
inTransformer: (T) -> Int
) {
// Out
onItemSelected { data.postValue(outTransformer(it)) }
// In
data.distinct()
.observe(lifecycleOwner, Observer {
setSelection(inTransformer(it))
})
}
fun LiveData<Int?>.toViewError(
owner: LifecycleOwner,
view: View,
setter: (String?) -> Unit
) = observe(owner, Observer { error ->
setter(
if (error != null) {
view.resources.getString(error)
} else {
null
}
)
})
fun LiveData<Int?>.toViewError(
owner: LifecycleOwner,
view: EditText
) = toViewError(owner, view, view::setError)
fun LiveData<*>.toViewText(
owner: LifecycleOwner,
view: TextView
) = distinct().observe(owner, Observer {
when (it) {
is Int -> view.setText(it)
is String -> view.text = it
else -> throw IllegalStateException("Can't set $it to a text view.")
}
})
fun LiveData<Boolean>.toViewVisibility(
owner: LifecycleOwner,
view: View
) = distinct().observe(owner, Observer { view.showOrHide(it) })

View file

@ -0,0 +1,59 @@
/**
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.afollestad.nocknock.viewcomponents.livedata
import android.view.View
import android.view.View.OnAttachStateChangeListener
import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.Lifecycle.State.DESTROYED
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.afollestad.nocknock.viewcomponents.R
/** @author Aidan Follestad (@afollestad) */
class ViewLifecycleOwner(view: View) : LifecycleOwner, OnAttachStateChangeListener {
private val lifecycleRegistry = LifecycleRegistry(this)
init {
view.addOnAttachStateChangeListener(this)
}
override fun getLifecycle() = lifecycleRegistry
override fun onViewAttachedToWindow(v: View?) {
lifecycleRegistry.markState(CREATED)
lifecycleRegistry.markState(STARTED)
lifecycleRegistry.markState(RESUMED)
}
override fun onViewDetachedFromWindow(v: View) {
lifecycleRegistry.markState(DESTROYED)
}
}
fun View.lifecycleOwner(): LifecycleOwner {
val tagOwner = getTag(R.id.view_lifecycle_registry) as? ViewLifecycleOwner
return if (tagOwner != null) {
tagOwner
} else {
val newOwner = ViewLifecycleOwner(this)
setTag(R.id.view_lifecycle_registry, newOwner)
newOwner
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item format="integer" name="view_lifecycle_registry" type="id"/>
</resources>