Updated on 2026-08-14
This commit is contained in:
parent
79f35fe41e
commit
eec0a5ccb8
9 changed files with 185 additions and 102 deletions
|
|
@ -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<String> = 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <T> valueDebouncerAsState(
|
||||
debounce: Long = 400,
|
||||
onValueChanged: (T) -> Unit
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
debounce = debounce,
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <T> valueDebouncerAsState(
|
||||
initialValue: T,
|
||||
debounce: Long = 400,
|
||||
onEmitValueReceived: (T) -> Unit = {},
|
||||
onValueChanged: (T) -> Unit,
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer<T>(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = { emitValue ->
|
||||
emitValue?.let { onEmitValueReceived(it) }
|
||||
},
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> valueDebouncerNullableAsState(
|
||||
initialValue: T?,
|
||||
debounce: Long = 400,
|
||||
onEmitValueReceived: (T?) -> Unit = {},
|
||||
onValueChanged: (T?) -> Unit,
|
||||
): ValueDebouncer<T?> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = onEmitValueReceived,
|
||||
onValueChanged = onValueChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <T> 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 <T> 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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
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<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ interface Field<T> {
|
|||
|
||||
data class Data<Data>(
|
||||
val value: Data,
|
||||
val isUserInput: Boolean = true
|
||||
val isUserInput: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,31 +10,37 @@ import kotlinx.coroutines.launch
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ValueDebouncer<T>(
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@ enum class CustomTokenFieldId : FieldId {
|
|||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
) : BaseDataField<String>(id, Field.Data(""))
|
||||
) : BaseDataField<String>(id, Field.Data("", false))
|
||||
|
||||
data class TokenBlockchainField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown))
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
||||
data class TokenDerivationPathField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown))
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
Loading…
Add table
Add a link
Reference in a new issue