From 27313ada3747038ec85a10155bedf70282646ede Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 12:20:11 +0200 Subject: [PATCH] Get faster results --- .../ChangeAccountProviderFormPresenter.kt | 31 ++----- .../form/HomeserverResolver.kt | 86 ++++++++++++------- 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 157dc8a120..8262c35a27 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -17,16 +17,13 @@ package io.element.android.features.login.impl.changeaccountprovider.form import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,42 +35,28 @@ class ChangeAccountProviderFormPresenter @Inject constructor( override fun present(): ChangeAccountProviderFormState { val localCoroutineScope = rememberCoroutineScope() - var currentJob: Job? = remember { null } - val userInput = rememberSaveable { mutableStateOf("") } - val userInputResult: MutableState>> = remember { - mutableStateOf(Async.Uninitialized) - } + val data by homeserverResolver.flow().collectAsState() fun handleEvents(event: ChangeAccountProviderFormEvents) { when (event) { is ChangeAccountProviderFormEvents.UserInput -> { - currentJob?.cancel() - currentJob = localCoroutineScope.userInput(event.input, userInputResult) + localCoroutineScope.userInput(event.input) } } } return ChangeAccountProviderFormState( userInput = userInput.value, - userInputResult = userInputResult.value, + userInputResult = data, eventSink = ::handleEvents ) } // Could be reworked using LaunchedEffect - private fun CoroutineScope.userInput(userInput: String, state: MutableState>>) = launch { - state.value = Async.Uninitialized - // Debounce - delay(300) - state.value = Async.Loading() - try { - val result = homeserverResolver.resolve(userInput) - state.value = Async.Success(result) - } catch (error: Throwable) { - state.value = Async.Failure(error) - } + private fun CoroutineScope.userInput(userInput: String) = launch { + homeserverResolver.accept(userInput) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt index fceddaf42f..d80b20f1fd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -17,12 +17,20 @@ package io.element.android.features.login.impl.changeaccountprovider.form import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -33,45 +41,59 @@ class HomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, ) { + private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - suspend fun resolve(userInput: String): List { - return withContext(dispatchers.io) { - val cleanedUpUserInput = userInput.trim() - if (cleanedUpUserInput.length < 4) { - // Wait for more chars - emptyList() - } else { + fun flow(): StateFlow>> = mutableFlow + + private var currentJob: Job? = null + + suspend fun accept(userInput: String) { + currentJob?.cancel() + val cleanedUpUserInput = userInput.trim() + mutableFlow.tryEmit(Async.Uninitialized) + if (cleanedUpUserInput.length > 3) { + delay(300) + mutableFlow.tryEmit(Async.Loading()) + withContext(dispatchers.io) { val list = getUrlCandidate(cleanedUpUserInput) - val resolvedList = resolveList(userInput, list) - - // If list is empty, and the user as entered an URL, do not block the user. - if (resolvedList.isEmpty() && userInput.isValidUrl()) { - listOf( - HomeserverData( - userInput = userInput, - homeserverUrl = userInput, - isWellknownValid = false - ) - ) - } else { - resolvedList - } + currentJob = resolveList(userInput, list) } } } - private suspend fun resolveList(userInput: String, list: List): List { - return coroutineScope { - buildList { - list.map { - async { - val isValid = wellknownRequest.execute(it) - if (isValid) { - add(HomeserverData(userInput, it, true)) + private fun CoroutineScope.resolveList(userInput: String, list: List): Job { + val currentList = mutableListOf() + return launch { + list.map { + async { + val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() + if (isValid) { + // Emit the list as soon as possible + currentList.add(HomeserverData(userInput, it, true)) + mutableFlow.tryEmit(Async.Success(currentList)) + } + } + }.joinAll() + .also { + // If list is empty, and the user as entered an URL, do not block the user. + if (currentList.isEmpty()) { + if (userInput.isValidUrl()) { + mutableFlow.tryEmit( + Async.Success( + listOf( + HomeserverData( + userInput = userInput, + homeserverUrl = userInput, + isWellknownValid = false + ) + ) + ) + ) + } else { + mutableFlow.tryEmit(Async.Uninitialized) } } - }.joinAll() - } + } } }