From eec0a5ccb8fa33b50e6c81dcbd19458afc75b307 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Apr 2022 18:13:05 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/common/CompositionCounter.kt | 40 +++++++++ .../common/compose/ComposableTextDebouncer.kt | 24 ------ .../compose/ComposableValueDebouncer.kt | 47 +++++++++++ .../tap/common/compose/OutlinedSpinner.kt | 34 +------- .../common/compose/OutlinedTextFieldWidget.kt | 84 ++++++++++++++----- .../addCustomToken/compose/FormFieldViews.kt | 20 ++--- .../com/tangem/domain/common/form/Form.kt | 2 +- .../domain/common/util/ValueDebouncer.kt | 30 ++++--- .../features/addCustomToken/FormFields.kt | 6 +- 9 files changed, 185 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/CompositionCounter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt diff --git a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt b/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt new file mode 100644 index 0000000000..4e80061209 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.common + +import timber.log.Timber + +class CompositionCounter( + val id: String, + count: Int = 0 +) { + var count: Int = count + private set + + fun increase(id: String): CompositionCounter { + if (this.id != id) return this + + count += 1 + return CompositionCounter(id, count) + } +} + +class CompositionLogger( + private val recomposeViewId: String, + private val tag: String = recomposeViewId, + private var turnOnForIds: List = listOf(recomposeViewId) +) { + val count: Int + get() = compositionCounter.count + + private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId) + + fun nextComposition() { + compositionCounter = compositionCounter.increase(recomposeViewId) + log("") + } + + fun log(message: String) { + if (!turnOnForIds.contains(recomposeViewId)) return + + Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt deleted file mode 100644 index 0e9040f220..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.tangem.domain.common.util.ValueDebouncer - -/** -[REDACTED_AUTHOR] - * This is an empty compose view. It just remember the ValueDebouncer inside of itself. - */ -@Composable -fun valueDebouncerAsState( - debounce: Long = 400, - onValueChanged: (T) -> Unit -): ValueDebouncer { - return remember { - ValueDebouncer( - debounce = debounce, - onValueChanged = { changedValue -> - changedValue?.let { onValueChanged(it) } - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt new file mode 100644 index 0000000000..2e2288d721 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt @@ -0,0 +1,47 @@ +package com.tangem.tap.common.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.tangem.domain.common.util.ValueDebouncer + +/** +[REDACTED_AUTHOR] + * This is an empty compose view. It just remember the ValueDebouncer inside of itself. + */ +@Composable +fun valueDebouncerAsState( + initialValue: T, + debounce: Long = 400, + onEmitValueReceived: (T) -> Unit = {}, + onValueChanged: (T) -> Unit, +): ValueDebouncer { + return remember { + ValueDebouncer( + initialValue = initialValue, + debounceDuration = debounce, + onEmitValueReceived = { emitValue -> + emitValue?.let { onEmitValueReceived(it) } + }, + onValueChanged = { changedValue -> + changedValue?.let { onValueChanged(it) } + }, + ) + } +} + +@Composable +fun valueDebouncerNullableAsState( + initialValue: T?, + debounce: Long = 400, + onEmitValueReceived: (T?) -> Unit = {}, + onValueChanged: (T?) -> Unit, +): ValueDebouncer { + return remember { + ValueDebouncer( + initialValue = initialValue, + debounceDuration = debounce, + onEmitValueReceived = onEmitValueReceived, + onValueChanged = onValueChanged, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt index dfcdf48551..fde9c1ccb7 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback import com.tangem.domain.common.form.Field import com.tangem.tap.common.extensions.ValueCallback -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -28,24 +27,14 @@ fun OutlinedSpinner( isEnabled: Boolean = true, onClose: VoidCallback = {} ) { - val compositionCounter = remember { CompositionCounter(label) } - val counter = compositionCounter.increase(label) - val rIsExpanded = remember { mutableStateOf(false) } val stateSelectedItem = remember { mutableStateOf(selectedItem.value) } - osLog(label, "recompose ------------------------------------", counter) - osLog(label, "selectedItem: $selectedItem", counter) - osLog(label, "stateSelectedItem: ${stateSelectedItem.value}", counter) if (!selectedItem.isUserInput) { - osLog(label, "stateSelectedItem.value: update = selectedItem.value", counter) stateSelectedItem.value = selectedItem.value } - osLog(label, "stateSelectedItem: ${stateSelectedItem.value}", counter) val onDropDownItemSelectedInternal: (T) -> Unit = { - osLog(label, "onDropDownItemSelectedInternal: value: [${it.toString()}]", counter) stateSelectedItem.value = it - osLog(label, "onDropDownItemSelectedInternal: stateSelectedItem: ${stateSelectedItem.value}", counter) rIsExpanded.value = false onItemSelected(it) } @@ -85,27 +74,6 @@ fun OutlinedSpinner( } } -private fun osLog(id: String, log: String, counter: CompositionCounter) { - if (id == "Сеть") Timber.d( - "OutlinedSpinner[$id]:[${counter.count}] - $log" - ) -} - -class CompositionCounter( - val id: String, - count: Int = 0 -) { - var count: Int = count - private set - - fun increase(id: String): CompositionCounter { - if (this.id != id) return this - - count += 1 - return CompositionCounter(id, count) - } -} - @Preview @Composable fun TestSpinnerPreview() { @@ -113,7 +81,7 @@ fun TestSpinnerPreview() { OutlinedSpinner( label = "Blockchain name", itemList = listOf(Blockchain.values()), - selectedItem = Field.Data(Blockchain.Avalanche), + selectedItem = Field.Data(Blockchain.Avalanche, false), onItemSelected = {}, ) } diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 3081ef0b45..e7fd761361 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.common.module.ModuleError import com.tangem.domain.common.form.Field +import com.tangem.tap.common.CompositionLogger import com.tangem.tap.common.compose.extensions.stringResourceDefault import com.tangem.tap.common.moduleMessage.ModuleMessageConverter @@ -33,7 +34,7 @@ import com.tangem.tap.common.moduleMessage.ModuleMessageConverter @Composable fun OutlinedTextFieldWidget( modifier: Modifier = Modifier, - textFieldData: Field.Data, + fieldData: Field.Data, labelId: Int? = null, label: String = "", placeholderId: Int? = null, @@ -56,7 +57,7 @@ fun OutlinedTextFieldWidget( ) { OutlinedProgressTextField( modifier = modifier, - textFieldData = textFieldData, + fieldData = fieldData, label = stringResourceDefault(labelId, label), placeholder = stringResourceDefault(placeholderId, placeholder), trailingIcon = trailingIcon, @@ -75,7 +76,7 @@ fun OutlinedTextFieldWidget( @Composable private fun OutlinedProgressTextField( modifier: Modifier = Modifier, - textFieldData: Field.Data, + fieldData: Field.Data, label: String = "", placeholder: String = "", isEnabled: Boolean = true, @@ -87,24 +88,70 @@ private fun OutlinedProgressTextField( trailingIcon: @Composable (() -> Unit)? = null, onTextChanged: (String) -> Unit, ) { - val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged) - val rText = remember { mutableStateOf(textFieldData.value) } + val logger = remember { + CompositionLogger(label, "OutlinedProgressTextField", listOf("Адрес контракта")) + } + logger.nextComposition() - fun updateFieldValueAndEmmit(value: String) { - rText.value = value - rTextDebouncer.emmit(value) - } - // This action came from redux. Update the field value and send a new event as if from the user - if (!textFieldData.isUserInput) { - updateFieldValueAndEmmit(textFieldData.value) + val textValueState = remember { mutableStateOf(fieldData.value) } + val textDebouncer = valueDebouncerAsState( + initialValue = fieldData.value, + debounce = debounce, + onEmitValueReceived = { + logger.log("DEBOUNCER: onEmitValueReceived: [$it]") + logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]") + textValueState.value = it + }, + onValueChanged = { + logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])") + onTextChanged(it) + }) + + logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]") + logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]") + logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]") + logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]") + logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]") + + if (!fieldData.isUserInput) { + // initial value is not from an user + val isNotUserInput = "-- IS NOT USER INPUT" + logger.log("recompose $isNotUserInput") + if (textValueState.value == fieldData.value) { + logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле") + } else { + logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле") + if (textDebouncer.emittedValue != textDebouncer.debounced) { + logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE") + } else { + logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные") + if (textValueState.value != textDebouncer.emittedValue || textValueState.value != textDebouncer.debounced) { + logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer") + if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) { + logger.log("$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]") + textValueState.value = fieldData.value + } else { + logger.log("$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]") + textValueState.value = fieldData.value + } + } else { + logger.log("$isNotUserInput: UNKNOWN -> start RECOMPOSE by new value for textValueState.value = [${fieldData.value}]") + textValueState.value = fieldData.value + } + } + } } + logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]") Box { OutlinedTextField( modifier = Modifier .fillMaxWidth(), - value = rText.value, - onValueChange = ::updateFieldValueAndEmmit, + value = textValueState.value, + onValueChange = { + logger.log("WIDGET: textDebouncer.emmit([$it])") + textDebouncer.emmit(it) + }, keyboardOptions = keyboardOptions, label = { Text(label) }, placeholder = { @@ -128,7 +175,6 @@ private fun OutlinedProgressTextField( visible = isLoading, ) { LinearProgressIndicator() } } - } @Composable @@ -168,7 +214,7 @@ fun OutlinedTextFieldWithErrorTest() { ) { OutlinedTextFieldWidget( modifier = modifier, - textFieldData = Field.Data(""), + fieldData = Field.Data("", false), label = "First label", placeholder = "1 placeholder", error = null, @@ -176,7 +222,7 @@ fun OutlinedTextFieldWithErrorTest() { ) {} OutlinedTextFieldWidget( modifier = modifier, - textFieldData = Field.Data("First"), + fieldData = Field.Data("First", false), label = "First label", placeholder = "1 placeholder", error = null, @@ -184,7 +230,7 @@ fun OutlinedTextFieldWithErrorTest() { ) {} OutlinedTextFieldWidget( modifier = modifier, - textFieldData = Field.Data("First"), + fieldData = Field.Data("First", false), label = "First label", placeholder = "1 placeholder", isLoading = true, @@ -193,7 +239,7 @@ fun OutlinedTextFieldWithErrorTest() { ) {} OutlinedTextFieldWidget( modifier = modifier, - textFieldData = Field.Data("First"), + fieldData = Field.Data("First", false), label = "First label", placeholder = "1 placeholder", error = SimpleError(), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt index 1d0ee9869e..7a61b5c911 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -27,7 +27,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) { val tokenField = screenFieldData.field as TokenField OutlinedTextFieldWidget( - textFieldData = tokenField.data, + fieldData = tokenField.data, labelId = R.string.custom_token_contract_address_input_title, placeholder = "0x0000000000000000000000000000000000000000", isEnabled = screenFieldData.viewState.isEnabled, @@ -35,7 +35,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) { error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, ) { - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it))) + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it, true))) } SpacerH8() } @@ -47,14 +47,14 @@ fun TokenNameView(screenFieldData: ScreenFieldData) { val tokenField = screenFieldData.field as TokenField OutlinedTextFieldWidget( - textFieldData = tokenField.data, + fieldData = tokenField.data, labelId = R.string.custom_token_name_input_title, placeholderId = R.string.custom_token_name_input_placeholder, isEnabled = screenFieldData.viewState.isEnabled, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, ) { - domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it))) + domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it, true))) } SpacerH8() } @@ -72,7 +72,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it, true))) } SpacerH8() } @@ -83,13 +83,13 @@ fun TokenSymbolView(screenFieldData: ScreenFieldData) { val tokenField = screenFieldData.field as TokenField OutlinedTextFieldWidget( - textFieldData = tokenField.data, + fieldData = tokenField.data, labelId = R.string.custom_token_token_symbol_input_title, placeholderId = R.string.custom_token_token_symbol_input_placeholder, isEnabled = screenFieldData.viewState.isEnabled, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it, true))) } SpacerH8() } @@ -100,14 +100,14 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) { val tokenField = screenFieldData.field as TokenField OutlinedTextFieldWidget( - textFieldData = tokenField.data, + fieldData = tokenField.data, labelId = R.string.custom_token_decimals_input_title, placeholder = "8", isEnabled = screenFieldData.viewState.isEnabled, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it, true))) } SpacerH8() } @@ -129,6 +129,6 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo val blockchainName = state.blockchainToName(blockchain) ?: notSelected TitleSubtitle(derivationPathName, blockchainName) } - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it, true))) } SpacerH8() } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/src/main/java/com/tangem/domain/common/form/Form.kt index 81758003bc..7cf9b4e41d 100644 --- a/domain/src/main/java/com/tangem/domain/common/form/Form.kt +++ b/domain/src/main/java/com/tangem/domain/common/form/Form.kt @@ -38,7 +38,7 @@ interface Field { data class Data( val value: Data, - val isUserInput: Boolean = true + val isUserInput: Boolean ) } diff --git a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt index 36dd4441f4..83e856abb7 100644 --- a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt +++ b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt @@ -10,31 +10,37 @@ import kotlinx.coroutines.launch [REDACTED_AUTHOR] */ class ValueDebouncer( - private val debounce: Long = 400, - private val onValueChanged: (T?) -> Unit + private val initialValue: T, + private val debounceDuration: Long = 400, + private val onValueChanged: (T) -> Unit, + private val onEmitValueReceived: (T) -> Unit = {}, ) { - private var value: T? = null - private val debounceScope = CoroutineScope(Job() + Dispatchers.Main) - private val flow = MutableStateFlow(value) + var emittedValue: T = initialValue + private set + var debounced: T = initialValue + private set + + private val debounceScope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Main) + private val flow = MutableStateFlow(debounced) init { - initFlow() - } - - private fun initFlow() { debounceScope.launch { - flow.filter { if (value == null) true else value != it } - .debounce(debounce) + flow.filter { if (debounced == null) true else debounced != it } + .debounce(debounceDuration) .onEach { - value = it + debounced = it onValueChanged(it) } .collect() } } + fun isDebounced(value: T): Boolean = this.debounced == value + fun emmit(emmitValue: T) { + emittedValue = emmitValue + onEmitValueReceived.invoke(emmitValue) debounceScope.launch { flow.emit(emmitValue) } } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt index 33336b560e..e9ec16b2a1 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt @@ -19,14 +19,14 @@ enum class CustomTokenFieldId : FieldId { data class TokenField( override val id: FieldId, -) : BaseDataField(id, Field.Data("")) +) : BaseDataField(id, Field.Data("", false)) data class TokenBlockchainField( override val id: FieldId, val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown)) +) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) data class TokenDerivationPathField( override val id: FieldId, val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown)) \ No newline at end of file +) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) \ No newline at end of file