mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-08-07 00:18:38 +00:00
View <-> LiveData connection tweaks, some re-org
This commit is contained in:
parent
9a849ab8ac
commit
fc6bdf1c39
85 changed files with 737 additions and 373 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -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)
|
|
@ -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)!!
|
|
@ -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)
|
|
@ -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) })
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
}
|
|
@ -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) })
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
4
viewcomponents/src/main/res/values/ids.xml
Normal file
4
viewcomponents/src/main/res/values/ids.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item format="integer" name="view_lifecycle_registry" type="id"/>
|
||||||
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue