Created
January 27, 2022 09:09
-
-
Save ildar2/9586f05fcf1b282f75c363ea4045a602 to your computer and use it in GitHub Desktop.
Common UI for DisplayDiffAdapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Стандартный элемент для отображения информации | |
*/ | |
class CommonInfoDisplay( | |
val textRes: Int, | |
vararg val args: Any, | |
val gravity: Int? = null, | |
val textSize: Float? = null | |
) : DisplayDiffItem(R.layout.common_item_info, textRes.toString(), "") { | |
class ViewHolder(view: View) : DisplayViewHolder<CommonInfoDisplay>(view) { | |
override fun bind(item: CommonInfoDisplay) { | |
if (item.gravity == Gravity.START) { | |
tv_info.gravity = Gravity.START | |
} else { | |
tv_info.gravity = Gravity.CENTER | |
} | |
tv_info.setText(item.textRes) | |
item.textSize?.let { | |
tv_info.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) | |
} | |
tv_info.text = tv_info.resources.getString(item.textRes, *item.args) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Стандартное поле ввода текста | |
*/ | |
class CommonInputDisplay( | |
val name: String, | |
var value: String? = null, | |
var enabled: Boolean = true, | |
val filter: (FilterConfig.() -> Unit)? = null, | |
val mask: String? = null, | |
val inputMode: InputMode = InputMode.TEXT, | |
val shouldUpdate: Boolean = false, | |
var error: String? = null, | |
val onInput: ((String) -> Unit)? = null | |
) : DisplayDiffItem(R.layout.common_item_input, name, shouldUpdate.toString()) { | |
class ViewHolder(view: View) : DisplayViewHolder<CommonInputDisplay>(view) { | |
private val watcher = object : SimpleTextWatcher { | |
override fun afterTextChanged(s: Editable?) { | |
action(s?.toString().orEmpty()) | |
} | |
} | |
private var action: (String) -> Unit = {} | |
private var maskListener: MaskedTextChangedListener? = null | |
override fun bind(item: CommonInputDisplay) { | |
input_layout.hint = item.name | |
input_layout.isEnabled = item.enabled | |
input_layout.isErrorEnabled = !item.error.isNullOrBlank() | |
input_layout.error = item.error | |
item.filter?.let { config -> | |
input_et.setFilters { | |
config.invoke(this) | |
} | |
} ?: run { | |
input_et.filters = arrayOf() | |
} | |
input_et.removeTextChangedListener(watcher) | |
input_et.inputType = item.inputMode.value | |
item.mask?.let { mask -> | |
maskListener = MaskedTextChangedListener.installOn( | |
input_et, mask, | |
object : MaskedTextChangedListener.ValueListener { | |
override fun onTextChanged( | |
maskFilled: Boolean, | |
extractedValue: String, | |
formattedValue: String | |
) { | |
item.value = extractedValue | |
item.itemDisplayValue = extractedValue | |
input_layout.error = null | |
input_layout.isErrorEnabled = false | |
item.onInput?.invoke(extractedValue) | |
} | |
}) | |
/** | |
* Смена inputType с Text на Phone нужна из-за того что на некоторых клавиатурах | |
* происходит задвоение цифр при наборе номера телефона, если inputType=text. Проблема описана на гитхабе: | |
* https://github.com/RedMadRobot/input-mask-android/issues/108 | |
*/ | |
when (mask) { | |
Constants.PHONE_MASK -> { | |
input_et.inputType = InputType.TYPE_CLASS_PHONE | |
val number = item.value.orEmpty() | |
maskListener?.setText( | |
if (number.length == Constants.PHONE_LENGTH) Constants.PHONE_PREFIX + number else number, | |
true | |
) | |
} | |
else -> { | |
input_et.setText(item.value) | |
} | |
} | |
} ?: run { | |
input_et.removeTextChangedListener(maskListener) | |
maskListener = null | |
input_et.setText(item.value) | |
action = { text -> | |
item.value = text | |
item.itemDisplayValue = text | |
input_layout.error = null | |
input_layout.isErrorEnabled = false | |
item.onInput?.invoke(text) | |
} | |
input_et.addTextChangedListener(watcher) | |
} | |
} | |
override fun bind(item: CommonInputDisplay, payloads: MutableList<Any>) { | |
if (payloads.firstOrNull() != DIFF_SKIP_UPDATE) bind(item) | |
} | |
override fun unbind() { | |
itemView.hideKeyboard() | |
} | |
} | |
/** | |
* Тип клавиатуры при вводе | |
* @see InputType | |
*/ | |
enum class InputMode( | |
val value: Int | |
) { | |
TEXT(InputType.TYPE_CLASS_TEXT), | |
NUMBERS(InputType.TYPE_CLASS_NUMBER), | |
PHONE(InputType.TYPE_CLASS_PHONE), | |
PASSWORD(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD), | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Класс-обертка для событий, которые должны отрабатывать только один раз | |
* Необходимо оборачивать [LiveData]-события для [Toast] и для навигации в этот класс, чтобы избежать повторных вызовов | |
* | |
* Used as a wrapper for data that is exposed via a LiveData that represents an event | |
* got from https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 | |
*/ | |
open class EventWrapper<out T>(private val content: T) { | |
var hasBeenHandled = false | |
private set | |
/** | |
* Returns the content and prevents its use again. | |
*/ | |
fun get(): T? = if (hasBeenHandled) { | |
null | |
} else { | |
hasBeenHandled = true | |
content | |
} | |
/** | |
* Returns the content, even if it's already been handled. | |
*/ | |
fun peek(): T = content | |
} | |
/** | |
* Can be used for no-data events | |
*/ | |
object VoidEvent { | |
val WRAPPED: EventWrapper<VoidEvent> | |
get() = EventWrapper(VoidEvent) | |
} | |
/** | |
* An [Observer] for [EventWrapper]s, simplifying the pattern of checking if the [EventWrapper]'s content has | |
* already been handled. | |
* | |
* [onEventUnhandledContent] is *only* called if the [EventWrapper]'s contents has not been handled. | |
*/ | |
class EventObserver<T>( | |
private val onEventUnhandledContent: (T) -> Unit | |
) : Observer<EventWrapper<T>> { | |
override fun onChanged(event: EventWrapper<T>?) { | |
event?.get()?.let(onEventUnhandledContent) | |
} | |
} | |
fun <T : Any> T.wrapEvent() = EventWrapper(this) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.text.InputFilter | |
import android.text.LoginFilter | |
import android.widget.EditText | |
/** | |
* Выставляем фильтр ввода для [EditText] | |
* можно сделать всё капсом, выставить [FilterConfig.maxLength], запретить кириллицу, цифры и тд | |
* @see FilterConfig | |
*/ | |
fun EditText.setFilters( | |
filterConfig: FilterConfig.() -> Unit | |
) { | |
val array = mutableListOf<InputFilter>() | |
val filter = FilterConfig().apply(filterConfig) | |
filter.maxLength?.let { array.add(InputFilter.LengthFilter(it)) } | |
if (filter.allCaps) array.add(InputFilter.AllCaps()) | |
if (filter.enabled) array.add(CustomInputFilter(filter)) | |
filters = array.toTypedArray() | |
} | |
class FilterConfig( | |
var allowChars: Boolean = true, | |
var allowNumbers: Boolean = true, | |
var allCaps: Boolean = false, | |
var allowCyrillic: Boolean = false, | |
var allowDecimal: Boolean = false, | |
var maxLength: Int? = null, | |
var enabled: Boolean = true | |
) { | |
companion object Presets { | |
val LATIN_NUMBERS: FilterConfig.() -> Unit = { /* default */ } | |
val NUMBERS: FilterConfig.() -> Unit = { allowChars = false } | |
val DECIMAL: FilterConfig.() -> Unit = { | |
allowChars = false | |
allowDecimal = true | |
} | |
/** | |
* Для ввода имени владельца карты | |
*/ | |
val CAPS_LATIN: FilterConfig.() -> Unit = { | |
allowNumbers = false | |
allCaps = true | |
} | |
/** | |
* Для ввода номера счёта | |
*/ | |
val CAPS_LATIN_NUMBERS: FilterConfig.() -> Unit = { | |
allCaps = true | |
} | |
} | |
} | |
private class CustomInputFilter( | |
private val filterType: FilterConfig, | |
appendInvalid: Boolean = false | |
) : LoginFilter.UsernameFilterGeneric(appendInvalid) { | |
override fun isAllowed(c: Char): Boolean = when { | |
c in '0'..'9' && filterType.allowNumbers -> true | |
c in 'a'..'z' && filterType.allowChars -> true | |
c in 'A'..'Z' && filterType.allowChars -> true | |
c in 'а'..'я' && filterType.allowCyrillic -> true | |
c in 'А'..'Я' && filterType.allowCyrillic -> true | |
c == '.' && filterType.allowDecimal -> true | |
else -> false | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MainAuthViewModel( | |
private val repository: AuthRepository, | |
private val prefManager: PrefManager, | |
) : BaseViewModel(), CommonItemViewModel<AuthActionData> { | |
override val itemsLiveData = MutableLiveData<List<DisplayDiffItem>>() | |
override val actionLiveData = MutableLiveData<EventWrapper<AuthActionData>>() | |
val certErrorsLiveData = MutableLiveData<EventWrapper<Int>>() | |
/** | |
* Номер телефона | |
* в формате 0004567890 | |
*/ | |
var phone = PhoneNumber(prefManager.getPhone()) | |
/** | |
* ИИН | |
*/ | |
var taxCode = ""//prefManager.getTaxCode() | |
/** | |
* Файл ЭЦП | |
*/ | |
private var pickedFile: Uri? = null | |
private var fileName: String? = null | |
/** | |
* Пароль от ЭЦП | |
*/ | |
private var pass = "" | |
/** | |
* Статус депозитора | |
* | |
* у ЦОИД нет лиц нерезидентов РК, поэтому лайвнес для них пропускаем | |
*/ | |
private var isResident = prefManager.getResidency() | |
/** | |
* Элементы, которые нужно валидировать | |
*/ | |
private val elementValidityMap: HashMap<Int, Boolean> = hashMapOf( | |
R.string.auth_phone_hint to phone.isValid(), | |
R.string.auth_login_hint to taxCode.isValidTaxCode(), | |
R.string.auth_cert_hint to false, | |
R.string.auth_pass_hint to false, | |
) | |
init { | |
loadItems() | |
} | |
override fun loadItems(update: Boolean) { | |
itemsLiveData.value = listOf( | |
CommonInfoDisplay(R.string.auth_header_desc, gravity = Gravity.START), | |
AuthResidentDisplay(isResident) { residency -> | |
changeResidency(residency) | |
}, | |
CommonInputDisplay( | |
getLocz().static(R.string.auth_phone_hint), | |
phone.value, | |
mask = Constants.PHONE_MASK, | |
inputMode = CommonInputDisplay.InputMode.PHONE, | |
shouldUpdate = update, | |
) { s -> | |
phone = PhoneNumber(s) | |
elementValidityMap[R.string.auth_phone_hint] = phone.isValid() | |
loadItems() | |
}, | |
CommonInputDisplay( | |
getLocz().static(R.string.auth_login_hint), | |
taxCode, | |
inputMode = CommonInputDisplay.InputMode.NUMBERS, | |
filter = { | |
apply(FilterConfig.NUMBERS) | |
maxLength = 12 | |
}, | |
shouldUpdate = update, | |
) { s -> | |
taxCode = s | |
elementValidityMap[R.string.auth_login_hint] = s.isValidTaxCode() | |
loadItems() | |
}, | |
CommonInfoButtonDisplay( | |
fileName ?: getLocz().static(R.string.auth_cert_hint), | |
pickedFile != null, | |
R.drawable.ic_key, | |
) { | |
pickCert() | |
}, | |
CommonInputDisplay( | |
getLocz().static(R.string.auth_pass_hint), | |
pass, | |
inputMode = CommonInputDisplay.InputMode.PASSWORD, | |
shouldUpdate = update, | |
) { s -> | |
pass = s | |
elementValidityMap[R.string.auth_pass_hint] = s.isNotEmpty() | |
loadItems() | |
}, | |
CommonButtonDisplay(R.string.auth_button, enabled = validateButton()) { | |
login() | |
}, | |
CommonLinkDisplay(R.string.auth_info_ecp) { | |
openEdsInfo() | |
}, | |
) | |
} | |
private fun pickCert() { | |
actionLiveData.value = AuthActionData( | |
AuthActionType.PICK_CERT | |
).wrapEvent() | |
} | |
private fun proceed() { | |
actionLiveData.value = AuthActionData( | |
if (isResident) AuthActionType.SUCCESS_LIVENESS else AuthActionType.SUCCESS_BMG | |
).wrapEvent() | |
} | |
private fun openEdsInfo() { | |
actionLiveData.value = AuthActionData( | |
AuthActionType.OPEN_EDS_INFO | |
).wrapEvent() | |
} | |
private fun changeResidency(isResident: Boolean) { | |
this.isResident = isResident | |
prefManager.saveResidency(isResident) | |
} | |
private fun validateButton() = !elementValidityMap.containsValue(false) | |
&& statusLiveData.value != Status.SHOW_LOADING | |
private fun login() { | |
if (pickedFile == null) { | |
setError(getLocz().static(R.string.auth_cert_hint)) | |
return | |
} | |
makeRequest({ | |
prefManager.savePhone(phone.value) | |
prefManager.saveTaxCode(taxCode) | |
repository.loginWithEds(phone, taxCode, pickedFile!!, pass) | |
}) { | |
when (it) { | |
is RequestResult.Success -> proceed() | |
is RequestResult.Error -> { | |
if (it.code in AuthRepository.CERT_ERROR_PASSWORD..AuthRepository.CERT_ERROR_INVALID) { | |
certErrorsLiveData.value = it.code.wrapEvent() | |
} else { | |
setError(it.error) | |
} | |
Analytics.reportError(EDS_ERROR, it.error) | |
} | |
} | |
} | |
} | |
fun setPickedFile(file: Uri, fileName: String?) { | |
pickedFile = file | |
this.fileName = fileName | |
elementValidityMap[R.string.auth_cert_hint] = fileName != null | |
loadItems() | |
} | |
fun setDebugData() { | |
if (RuntimeBehavior.isFeatureEnabled(FeatureFlag.DEBUG_AUTOFILL)) { | |
phone = PhoneNumber(prefManager.getAutoFillValue(AutofillValue.DEBUG_PHONE)) | |
elementValidityMap[R.string.auth_phone_hint] = true | |
taxCode = prefManager.getAutoFillValue(AutofillValue.DEBUG_TAX_CODE) | |
elementValidityMap[R.string.auth_login_hint] = true | |
pass = prefManager.getAutoFillValue(AutofillValue.DEBUG_PASS) | |
elementValidityMap[R.string.auth_pass_hint] = true | |
loadItems(true) | |
} | |
} | |
} | |
class AuthActionData(val type: AuthActionType, val data: Bundle? = null) | |
enum class AuthActionType { | |
PICK_CERT, SUCCESS_LIVENESS, SUCCESS_BMG, OPEN_EDS_INFO | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* inline class [PhoneNumber] служит оберткой для номера телефона. | |
* функция [formatNumber] для форматирования, при выводе номера на экран по дизайну | |
*/ | |
@JvmInline | |
value class PhoneNumber( | |
val value: String | |
) { | |
fun formatNumber(): String = format( | |
"+" + Constants.PHONE_PREFIX + " (%s) %s %s", | |
value.substring(0, 3), | |
value.substring(3, 6), | |
value.substring(6) | |
) | |
val length: Int | |
get() = value.length | |
fun isValid(): Boolean = length == Constants.PHONE_LENGTH | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment