Fix crash caused by immediately removed TextWatchers

This commit is contained in:
Kirill Kamakin
2022-11-21 21:37:01 +01:00
parent a5fcd89588
commit 4c1b840e5d

View File

@@ -13,7 +13,13 @@ import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.* import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
@@ -21,7 +27,9 @@ import kotlinx.coroutines.channels.ChannelResult
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onClosed import kotlinx.coroutines.channels.onClosed
import kotlinx.coroutines.channels.onFailure import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow<Unit> = callbackFlow { fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow<Unit> = callbackFlow {
@@ -37,16 +45,16 @@ fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow<Unit> = callback
} }
} }
fun TextView.textChangesFlow(logger: Logger): Flow<CharSequence?> = callbackFlow { fun TextView.textChangesLiveData(logger: Logger): LiveData<CharSequence?> = callbackFlow {
logger.v { "textChangesFlow() called" } logger.v { "textChangesLiveData() called" }
val textWatcher = doAfterTextChanged { val textWatcher = doAfterTextChanged {
trySend(it).logErrors("textChangesFlow", logger) trySend(it).logErrors("textChangesFlow", logger)
} }
awaitClose { awaitClose {
logger.d { "textChangesFlow: flow is closing" } logger.d { "textChangesLiveData: flow is closing" }
removeTextChangedListener(textWatcher) removeTextChangedListener(textWatcher)
} }
} }.asLiveData() // Use asLiveData() to make sure close() is called with a delay to avoid IndexOutOfBoundsException
fun <T> ChannelResult<T>.logErrors(methodName: String, logger: Logger): ChannelResult<T> { fun <T> ChannelResult<T>.logErrors(methodName: String, logger: Logger): ChannelResult<T> {
onFailure { logger.e(it) { "$methodName: can't send event" } } onFailure { logger.e(it) { "$methodName: can't send event" } }
@@ -66,19 +74,19 @@ fun EditText.checkIfInputIsEmpty(
logger.d { "Input text is \"$text\"" } logger.d { "Input text is \"$text\"" }
return text.ifEmpty { return text.ifEmpty {
inputLayout.error = resources.getString(stringId) inputLayout.error = resources.getString(stringId)
lifecycleOwner.lifecycleScope.launch { val textChangesLiveData = textChangesLiveData(logger)
waitUntilNotEmpty(logger) textChangesLiveData.observe(lifecycleOwner, object : Observer<CharSequence?> {
inputLayout.error = null override fun onChanged(value: CharSequence?) {
} if (value.isNullOrBlank().not()) {
inputLayout.error = null
textChangesLiveData.removeObserver(this)
}
}
})
null null
} }
} }
suspend fun EditText.waitUntilNotEmpty(logger: Logger) {
textChangesFlow(logger).filterNotNull().first { it.isNotEmpty() }
logger.v { "waitUntilNotEmpty() returned" }
}
fun <T> SharedPreferences.prefsChangeFlow( fun <T> SharedPreferences.prefsChangeFlow(
logger: Logger, logger: Logger,
valueReader: SharedPreferences.() -> T, valueReader: SharedPreferences.() -> T,
@@ -114,10 +122,8 @@ fun View.hideKeyboard() {
} }
fun Context.isDarkThemeOn(): Boolean { fun Context.isDarkThemeOn(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) resources.configuration.isNightModeActive
resources.configuration.isNightModeActive else resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
else
resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
} }
fun <T> LifecycleOwner.collectWhenResumed(flow: Flow<T>, collector: FlowCollector<T>) { fun <T> LifecycleOwner.collectWhenResumed(flow: Flow<T>, collector: FlowCollector<T>) {