mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-20 03:25:14 +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 com.afollestad.nocknock.R
|
||||
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.onLayout
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewError
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||
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.doneBtn
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
|
@ -88,9 +88,11 @@ class AddSiteActivity : AppCompatActivity() {
|
|||
.toViewError(this, responseTimeoutInput)
|
||||
|
||||
// Validation mode
|
||||
responseValidationMode.attachLiveData(this, viewModel.validationMode,
|
||||
{ ValidationMode.fromIndex(it) },
|
||||
{ it.toIndex() }
|
||||
responseValidationMode.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationMode,
|
||||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationSearchTermError()
|
||||
.toViewError(this, responseValidationSearchTerm)
|
||||
|
@ -98,7 +100,11 @@ class AddSiteActivity : AppCompatActivity() {
|
|||
.toViewText(this, validationModeDescription)
|
||||
|
||||
// Validation search term
|
||||
responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm)
|
||||
responseValidationSearchTerm.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationSearchTerm,
|
||||
pullInChanges = false
|
||||
)
|
||||
viewModel.onValidationSearchTermVisibility()
|
||||
.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.engine.validation.ValidationManager
|
||||
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.map
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -50,7 +50,9 @@ class AddSiteViewModel(
|
|||
// Public properties
|
||||
val name = MutableLiveData<String>()
|
||||
val url = MutableLiveData<String>()
|
||||
val timeout = MutableLiveData<Int>()
|
||||
val timeout = MutableLiveData<Int>().apply {
|
||||
this.value = 10000
|
||||
}
|
||||
val validationMode = MutableLiveData<ValidationMode>()
|
||||
val validationSearchTerm = MutableLiveData<String>()
|
||||
val validationScript = MutableLiveData<String>()
|
||||
|
@ -214,8 +216,8 @@ class AddSiteViewModel(
|
|||
)
|
||||
return Site(
|
||||
id = 0,
|
||||
name = name.value!!,
|
||||
url = url.value!!,
|
||||
name = name.value!!.trim(),
|
||||
url = url.value!!.trim(),
|
||||
settings = newSettings,
|
||||
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.ValidationMode
|
||||
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.onScroll
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewError
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
||||
|
@ -102,9 +102,12 @@ class ViewSiteActivity : AppCompatActivity() {
|
|||
.toViewError(this, responseTimeoutInput)
|
||||
|
||||
// Validation mode
|
||||
responseValidationMode.attachLiveData(this, viewModel.validationMode,
|
||||
{ ValidationMode.fromIndex(it) },
|
||||
{ it.toIndex() })
|
||||
responseValidationMode.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationMode,
|
||||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationSearchTermError()
|
||||
.toViewError(this, responseValidationSearchTerm)
|
||||
viewModel.onValidationModeDescription()
|
|
@ -38,10 +38,10 @@ import com.afollestad.nocknock.engine.validation.ValidationManager
|
|||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||
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.viewcomponents.ext.isNullOrLessThan
|
||||
import com.afollestad.nocknock.viewcomponents.ext.map
|
||||
import com.afollestad.nocknock.viewcomponents.ext.zip
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -232,8 +232,8 @@ class ViewSiteViewModel(
|
|||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
fun getValidationArgs(): String? {
|
||||
return when (validationMode.value) {
|
||||
TERM_SEARCH -> validationSearchTerm.value
|
||||
JAVASCRIPT -> validationScript.value
|
||||
TERM_SEARCH -> validationSearchTerm.value?.trim()
|
||||
JAVASCRIPT -> validationScript.value?.trim()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -310,8 +310,8 @@ class ViewSiteViewModel(
|
|||
disabled = false
|
||||
)
|
||||
return site.copy(
|
||||
name = name.value!!,
|
||||
url = url.value!!,
|
||||
name = name.value!!.trim(),
|
||||
url = url.value!!.trim(),
|
||||
settings = newSettings
|
||||
)
|
||||
.withStatus(status = WAITING)
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.interval.CheckIntervalLayout
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.interval.CheckIntervalLayout
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
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.engine.validation.ValidationManager
|
||||
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.nhaarman.mockitokotlin2.any
|
||||
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.mockDatabase
|
||||
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.verify
|
||||
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.mockDatabase
|
||||
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.google.common.truth.Truth
|
||||
import com.google.common.truth.Truth.assertThat
|
|
@ -20,7 +20,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
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
|
||||
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines
|
||||
|
@ -30,9 +30,8 @@ dependencies {
|
|||
implementation 'org.mozilla:rhino:' + versions.rhino
|
||||
|
||||
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 'androidx.arch.core:core-testing:' + versions.archTesting
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
|
@ -18,9 +18,29 @@ package com.afollestad.nocknock.utilities.ext
|
|||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
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 {
|
||||
val callbackRunner = Runnable {
|
||||
cb(text.trim().toString())
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) = Unit
|
||||
|
||||
override fun beforeTextChanged(
|
||||
|
@ -35,6 +55,13 @@ fun EditText.onTextChanged(cb: (String) -> Unit) {
|
|||
start: Int,
|
||||
before: 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
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
package com.afollestad.nocknock.utilities.livedata
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class TestLiveData<T>(data: LiveData<T>) {
|
||||
|
@ -34,17 +32,32 @@ class TestLiveData<T>(data: LiveData<T>) {
|
|||
}
|
||||
|
||||
fun assertNoValues() {
|
||||
assertWithMessage("Expected no values, but got: $receivedValues").that(receivedValues)
|
||||
.isEmpty()
|
||||
if (receivedValues.isNotEmpty()) {
|
||||
throw AssertionError("Expected no values, but got: $receivedValues")
|
||||
}
|
||||
}
|
||||
|
||||
fun assertValues(vararg assertValues: T) {
|
||||
val assertList = assertValues.toList()
|
||||
assertThat(receivedValues).isEqualTo(assertList)
|
||||
if (!assertList.contentEquals(receivedValues)) {
|
||||
throw AssertionError("Expected $assertList\n\t\tBut got: $receivedValues")
|
||||
}
|
||||
receivedValues.clear()
|
||||
}
|
||||
|
||||
@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)
|
|
@ -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.util.AttributeSet
|
||||
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.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.layout
|
||||
import com.afollestad.nocknock.viewcomponents.R.string
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenInt
|
||||
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.userInput
|
||||
|
||||
|
@ -41,10 +36,7 @@ import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
|
|||
class JavaScriptInputLayout(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : HorizontalScrollView(context, attrs), LifecycleOwner {
|
||||
|
||||
private val lifecycle = LifecycleRegistry(this)
|
||||
private lateinit var codeData: MutableLiveData<String>
|
||||
) : HorizontalScrollView(context, attrs) {
|
||||
|
||||
init {
|
||||
val contentInset = dimenInt(dimen.content_inset)
|
||||
|
@ -61,51 +53,20 @@ class JavaScriptInputLayout(
|
|||
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(
|
||||
codeData: MutableLiveData<String>,
|
||||
errorData: LiveData<Int?>,
|
||||
visibility: LiveData<Boolean>
|
||||
) {
|
||||
this.codeData = codeData
|
||||
this.codeData.observe(this, Observer {
|
||||
if (it.isNullOrEmpty()) {
|
||||
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 }
|
||||
userInput.attachLiveData(lifecycleOwner(), codeData)
|
||||
errorData.toViewError(lifecycleOwner(), this, ::setError)
|
||||
visibility.toViewVisibility(lifecycleOwner(), this)
|
||||
}
|
||||
|
||||
fun clear() = userInput.setText("")
|
||||
|
||||
private fun setDefaultCode() = userInput.setText(
|
||||
string.default_js
|
||||
)
|
||||
|
||||
override fun getLifecycle() = lifecycle
|
||||
private fun setError(error: String?) {
|
||||
error_text.showOrHide(error != null)
|
||||
error_text.text = error
|
||||
}
|
||||
}
|
|
@ -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
Reference in a new issue