Get faster results

This commit is contained in:
Benoit Marty 2023-06-07 12:20:11 +02:00
parent 9836b3fc8a
commit 27313ada37
2 changed files with 61 additions and 56 deletions

View file

@ -17,16 +17,13 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import androidx.compose.runtime.Composable 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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -38,42 +35,28 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
override fun present(): ChangeAccountProviderFormState { override fun present(): ChangeAccountProviderFormState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
var currentJob: Job? = remember { null }
val userInput = rememberSaveable { val userInput = rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
val userInputResult: MutableState<Async<List<HomeserverData>>> = remember { val data by homeserverResolver.flow().collectAsState()
mutableStateOf(Async.Uninitialized)
}
fun handleEvents(event: ChangeAccountProviderFormEvents) { fun handleEvents(event: ChangeAccountProviderFormEvents) {
when (event) { when (event) {
is ChangeAccountProviderFormEvents.UserInput -> { is ChangeAccountProviderFormEvents.UserInput -> {
currentJob?.cancel() localCoroutineScope.userInput(event.input)
currentJob = localCoroutineScope.userInput(event.input, userInputResult)
} }
} }
} }
return ChangeAccountProviderFormState( return ChangeAccountProviderFormState(
userInput = userInput.value, userInput = userInput.value,
userInputResult = userInputResult.value, userInputResult = data,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
// Could be reworked using LaunchedEffect // Could be reworked using LaunchedEffect
private fun CoroutineScope.userInput(userInput: String, state: MutableState<Async<List<HomeserverData>>>) = launch { private fun CoroutineScope.userInput(userInput: String) = launch {
state.value = Async.Uninitialized homeserverResolver.accept(userInput)
// 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)
}
} }
} }

View file

@ -17,12 +17,20 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest 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.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.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.core.uri.isValidUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async 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.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -33,45 +41,59 @@ class HomeserverResolver @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest, private val wellknownRequest: WellknownRequest,
) { ) {
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
suspend fun resolve(userInput: String): List<HomeserverData> { fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
return withContext(dispatchers.io) {
val cleanedUpUserInput = userInput.trim() private var currentJob: Job? = null
if (cleanedUpUserInput.length < 4) {
// Wait for more chars suspend fun accept(userInput: String) {
emptyList() currentJob?.cancel()
} else { 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 list = getUrlCandidate(cleanedUpUserInput)
val resolvedList = resolveList(userInput, list) currentJob = 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
}
} }
} }
} }
private suspend fun resolveList(userInput: String, list: List<String>): List<HomeserverData> { private fun CoroutineScope.resolveList(userInput: String, list: List<String>): Job {
return coroutineScope { val currentList = mutableListOf<HomeserverData>()
buildList { return launch {
list.map { list.map {
async { async {
val isValid = wellknownRequest.execute(it) val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse()
if (isValid) { if (isValid) {
add(HomeserverData(userInput, it, true)) // 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() }
}
} }
} }