Existe-t-il un moyen sophistiqué d'implémenter la logique debounce
avec Kotlin Android?
Je n'utilise pas Rx dans le projet.
Il y a un moyen dans Java , mais c'est trop grand pour moi ici.
Vous pouvez utiliser coroutines kotlin pour y parvenir. Voici un exemple .
Sachez que les coroutines sont expérimentales sur kotlin 1.1 + et qu'elles peuvent être modifiées dans les prochaines versions de kotlin.
Depuis Kotlin 1. release, coroutines sont maintenant stables.
Merci à https://medium.com/@pro100svitlo/edittext-debounce-with-kotlin-coroutines-fd134d54f4e9 et https://stackoverflow.com/a/50007453/291414 J'ai écrit ce code:
private var textChangedJob: Job? = null
private lateinit var textListener: TextWatcher
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
textListener = object : TextWatcher {
private var searchFor = "" // Or view.editText.text.toString()
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val searchText = s.toString().trim()
if (searchText != searchFor) {
searchFor = searchText
textChangedJob?.cancel()
textChangedJob = launch(Dispatchers.Main) {
delay(500L)
if (searchText == searchFor) {
loadList(searchText)
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
editText.setText("")
loadList("")
}
override fun onResume() {
super.onResume()
editText.addTextChangedListener(textListener)
}
override fun onPause() {
editText.removeTextChangedListener(textListener)
super.onPause()
}
override fun onDestroy() {
textChangedJob?.cancel()
super.onDestroy()
}
Je n'ai pas inclus coroutineContext
ici, donc cela ne fonctionnera probablement pas, s'il n'est pas défini. Pour plus d'informations, voir Migrer vers les coroutines Kotlin dans Android avec Kotlin 1. .
Une solution plus simple et générique consiste à utiliser une fonction qui renvoie une fonction qui fait la logique anti-rebond et à la stocker dans une val.
fun <T> debounce(delayMs: Long = 500L,
coroutineContext: CoroutineContext,
f: (T) -> Unit): (T) -> Unit {
var debounceJob: Job? = null
return { param: T ->
if (debounceJob?.isCompleted != false) {
debounceJob = CoroutineScope(coroutineContext).launch {
delay(delayMs)
f(param)
}
}
}
}
Maintenant, il peut être utilisé avec:
val handleClickEventsDebounced = debounce<Unit>(500, coroutineContext) {
doStuff()
}
fun initViews() {
myButton.setOnClickListener { handleClickEventsDebounced(Unit) }
}
J'ai créé un Gist avec trois opérateurs anti-rebond inspirés de cette élégante solution de Patrick où j'ai ajouté deux autres cas similaires: throttleFirst
et throttleLatest
. Ces deux sont très similaires à leurs analogues RxJava ( throttleFirst , throttleLatest ).
throttleLatest
fonctionne de manière similaire à debounce
mais il fonctionne sur des intervalles de temps et renvoie les dernières données pour chacun, ce qui vous permet d'obtenir et de traiter des données intermédiaires si vous en avez besoin.
fun <T> throttleLatest(
intervalMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: (T) -> Unit
): (T) -> Unit {
var throttleJob: Job? = null
var latestParam: T
return { param: T ->
latestParam = param
if (throttleJob?.isCompleted != false) {
throttleJob = coroutineScope.launch {
delay(intervalMs)
latestParam.let(destinationFunction)
}
}
}
}
throttleFirst
est utile lorsque vous devez traiter le premier appel immédiatement, puis ignorer les appels suivants pendant un certain temps pour éviter un comportement indésirable (évitez de démarrer deux activités identiques sur Android, par exemple).
fun <T> throttleFirst(
skipMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: (T) -> Unit
): (T) -> Unit {
var throttleJob: Job? = null
return { param: T ->
if (throttleJob?.isCompleted != false) {
throttleJob = coroutineScope.launch {
destinationFunction(param)
delay(skipMs)
}
}
}
}
debounce
aide à détecter l'état lorsqu'aucune nouvelle donnée n'est soumise pendant un certain temps, vous permettant efficacement de traiter une donnée lorsque la saisie est terminée.
fun <T> debounce(
waitMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: (T) -> Unit
): (T) -> Unit {
var debounceJob: Job? = null
return { param: T ->
debounceJob?.cancel()
debounceJob = coroutineScope.launch {
delay(waitMs)
destinationFunction(param)
}
}
}
Tous ces opérateurs peuvent être utilisés comme suit:
val onEmailChange: (String) -> Unit = throttleLatest(
300L,
viewLifecycleOwner.lifecycleScope,
viewModel::onEmailChanged
)
emailView.onTextChanged(onEmailChange)
J'ai créé une seule fonction d'extension à partir des anciennes réponses de débordement de pile:
fun View.clickWithDebounce(debounceTime: Long = 600L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
else action()
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
Affichez onClick à l'aide du code ci-dessous:
buttonShare.clickWithDebounce {
// Do anything you want
}