I have these piece of code and I want it to make it more optimal
I guess I can use kotlin-flow's flatMapMerge
but I don't think how should I convert my code to flow
val quiries = listof("s","b","s","g","d","r","t")
quiries.map { query ->
viewModelScope.launch {
val results = fetchColumns(query)
resultMutableData.postValue(results)
}
}
and fetchColumns() are suspended functions
I am thinking maybe I need to have flows of queries ???? what is the way of using flatMapMerge()?
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-merge.html
Try using something like this:
listOf("s","b","s","g","d","r","t").asFlow()
.map { fetchColumns(it) }
.onEach { resultMutableData.postValue(it) }
.launchIn(viewModelScope)
Since you don't switch onto another flow, there is no need for any of flatMap* functions, just map will be enough. Moreover, map parameter is already declared as suspend, so you won't block your thread.
But map operator was designed to process data sequentially, so these transformations won't be run in parallel. To achieve parallel processing, a workaround using flatMapMerge can be used:
listOf("s","b","s","g","d","r","t").asFlow()
.onEach { println("onEach: $it") }
.flatMapMerge {
flow {
emit(fetchColumns(it))
}
}
.onEach { resultMutableData.postValue(it)) }
.launchIn(viewModelScope)
Related
I'm trying to implement One Tap, so I have created this function:
override fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
emit(Result.Error(e.message))
}
}
//.flowOn(Dispatchers.IO)
And some programmer told me that I need to add .flowOn(Dispatchers.IO) to the above function, so it can be correct. My code work correct without it. Here is how I call this function in the ViewModel:
fun oneTapSignIn() = viewModelScope.launch {
repo.oneTapSignInWithGoogle().collect { response ->
oneTapSignInResponse = response
}
}
Is it really necessary to do that? I'm really confused.
You're calling beginSignIn which returns a Task, so it does its own stuff in the background. Now Task.await is suspending, not blocking, so it won't block the current thread while waiting for the task.
Therefore, the body of your flow doesn't contain any blocking stuff, so there is no reason to use flowOn(Dispatchers.IO) here.
Basically I want to make a network request when initiated by the user, collect the Flow returned by the repository and run some code depending on the result. My current setup looks like this:
Viewmodel
private val _requestResult = MutableSharedFlow<Result<Data>>()
val requestResult = _requestResult.filterNotNull().shareIn(
scope = viewModelScope,
started = SharingStarted.WhileViewSubscribed,
replay = 0
)
fun makeRequest() {
viewModelScope.launch {
repository.makeRequest().collect { _requestResult.emit(it) }
}
}
Fragment
buttonLayout.listener = object : BottomButtonLayout.Listener {
override fun onButtonClick() {
viewModel.makeRequest()
}
}
lifecycleScope.launchWhenCreated {
viewModel.requestResult.collect { result ->
when (result) {
Result.Loading -> {
doStuff()
}
is Result.Success -> {
doDifferentStuff(result.data)
}
is Result.Failure -> {
handleError()
}
}
}
}
The first time the request is made everything seems to work. But starting with the second time the collect block in the fragment does not run anymore. The request is still made, the repository returns the flow as expected, the collect block in the viewmodel runs and emit() also seems to be executed successfully.
So what could be the problem here? Something about the coroutine scopes? Admittedly I lack any sort of deeper understanding of the matter at hand.
Also is there a more efficient way of accomplishing what I'm attempting using Kotlin Flows in general? Collecting a flow and then emitting the same flow again seems a bit counterintuitive.
Thanks in advance:)
According to the documentation there are two recommended alternatives:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
//your thing
}
}
I rather the other alternative:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeReques().flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
I like the flowWithLifecycle shorter syntax and less boiler plate. Be carefull thar is bloking so you cant have anything after that.
The oficial docs
https://developer.android.com/topic/libraries/architecture/coroutines
Please be aware you need the lifecycle aware library.
So I've been exploring MVI with Kotlin Flows, basically MutableSharedFlow for Events and MutableStateFlow for States. but I have a problem adding logic to control the text changes event emissions, writing this in a human way would be something like that.
Observe text changes and only search for the latest word written
within half of a second.
So if the user removes or adds letters I only take the search for the latest word after half-second passed.
My attempt to achieve this was by writing the following, here I subscribe to events
events.flatMapConcat { it.eventToUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
And I map each event to a use case using this function:
private fun SearchScreenEvent.eventToUsecase(): Flow<SearchState> {
return when (this) {
is SearchClicked -> searchUsecase(this.query)
is SearchQueryChanged ->
flowOf(this.query)
.debounce(500)
.flatMapConcat { searchUsecase(it) }
}
}
I know that I have to control the event itself, but how to control only the SearchQueryChanged event independently. with RxJava I was using Publish and switchMap operators is there something like this with Flow.
debounce() can take a lambda parameter that determines the latency per item, so I think this will work:
events.debounce {
when (it) {
is SearchClicked -> 0L
is SearchQueryChanged -> 500L
}
}
.flatMapConcat { searchUsecase(it.query) }
.onEach { _states.value = it }
.launchIn(viewModelScope)
#Tenfour04 Solution will work, but it shows a design problem, so I have to take care of the flow logic in two places now, may be more, so I've posted the same question in slack and someone guides me to redesign my flow as follow:
merge( searchCurrentQuery(),
searchWhileTyping(),
updateUiSearchText())
.flatMapMerge { it.toUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
By seperating events and using merge to add them into the stream, now I could control each Flow of them as I want.
While I was learning coroutines and how to properly use them in an android app I found something I was surprised about.
When launching a coroutine using viewModelScope.launch { } and setting a breakpoint inside the launch lambda I noticed my app wasn't responsive anymore because it was still on the main thread.
This confuses me because the docs of viewModelScope.launch { } clearly state:
Launches a new coroutine without blocking the current thread
Isn't the current thread the main thread ? What is the whole purpose of launch if it doesn't run on a different thread by default ?
I was able to run it on anther thread using viewModelScope.launch(Dispatchers.IO){ } which works as I was expecting, namely on another thread.
What I am trying to accomplish from the launch method is to call a repository and do some IO work namely call a webservice and store the data in a room db. So I was thinking of calling viewModelScope.launch(Dispatchers.IO){ } do all the work on a different thread and in the end update the LiveData result.
viewModelScope.launch(Dispatchers.IO){
liveData.postValue(someRepository.someWork())
}
So my second question is, is this the way to go ?
ViewModelScope.launch { } runs on the main thread, but also gives you the option to run other dispatchers, so you can have UI & Background operations running synchronously.
For you example:
fun thisWillRunOnMainThread() {
viewModelScope.launch {
//below code will run on UI thread.
showLoadingOnUI()
//using withContext() you can run a block of code on different dispatcher
val result = novel.id = withContext(Dispatchers.IO) {
withsomeRepository.someWork()
}
//The below code waits until the above block is executed and the result is set.
liveData.value = result
finishLoadingOnUI()
}
}
For more reference, I would say there are some neat articles that will help you understand this concept.
Medium link that explains it really neat.
So my second question is, is this the way to go ?
I would expect two things to be different in your current approach.
1.) First step would be to define the scheduler of the background operation via withContext.
class SomeRepository {
suspend fun doWork(): SomeResult = withContext(Dispatchers.IO) {
...
}
}
This way, the operation itself runs on a background thread, but you didn't force your original scope to be "off-thread".
2.) Jetpack Lifecycle KTX provides the liveData { coroutine builder so that you don't have to postValue to it manually.
val liveData: LiveData<SomeResult> = liveData {
emit(someRepository.someWork())
}
Which in a ViewModel, you would use like so:
val liveData: LiveData<SomeResult> = liveData(context = viewModelScope.coroutineContext) {
withContext(Dispatchers.IO) {
emit(someRepository.someWork())
}
}
And now you can automatically trigger data-loading via observing, and not having to manually invoke viewModelScope.launch {}.
The idea behind main thread being default is you can run UI operations without having to change the context. It is a convention I guess Kotlin coroutine library writers have chosen
Suppose if by default if the launch runs on IO thread then the code would look like this
viewmodelScope.launch {
val response = networkRequest()
withContext(Dispatchers.Main) {
renderUI(response)
}
}
Suppose if by default if the launch runs on Default thread then the code would look like this
viewmodelScope.launch {
val response: Response = null
withContext(Dispatchers.IO) {
response = networkRequest()
}
withContext(Dispatchers.Main) {
renderUI(response)
}
}
Since the default launch is on main thread, now you have to do below
viewmodelScope.launch {
val response: Response = null
withContext(Dispatchers.IO) {
response = networkRequest()
}
renderUI(response)
}
To avoid the messy code initializing the response with null, we can also make the networkRequest as suspend and wrap the business logic of networkRequest() function in withContext(Dispatchers.IO) and that's how lot of people write their networkRequest() function as well! Hope this makes sense
One of the main reasons it runs on Main thread, is because it's more practical for general use in ViewModel, like murali kurapati wrote. It was a design choice.
It's also important to note that all suspending functions should be "main safe" according to best pracices. That means, that your repository should switch coroutine context accordingly, like so:
class someRepository(private val ioDispatcher: CoroutineDispatcher) {
suspend fun someWork() {
withContext(ioDispatcher) {
TODO("Heavy lifting")
}
}
}
I'm using Retrofit in order to make some network requests. I'm also using the Coroutines in combination with 'suspend' functions.
My question is: Is there a way to improve the following code. The idea is to launch multiple requests in parallels and wait for them all to finish before continuing the function.
lifecycleScope.launch {
try {
itemIds.forEach { itemId ->
withContext(Dispatchers.IO) { itemById[itemId] = MyService.getItem(itemId) }
}
} catch (exception: Exception) {
exception.printStackTrace()
}
Log.i(TAG, "All requests have been executed")
}
(Note that "MyService.getItem()" is a 'suspend' function.)
I guess that there is something nicer than a foreach in this case.
Anyone with an idea?
I've prepared three approaches to solving this, from the simplest to the most correct one. To simplify the presentation of the approaches, I have extracted this common code:
lifecycleScope.launch {
val itemById = try {
fetchItems(itemIds)
} catch (exception: Exception) {
exception.printStackTrace()
}
Log.i(TAG, "Fetched these items: $itemById")
}
Before I go on, a general note: your getItem() function is suspendable, you have no need to submit it to the IO dispatcher. All your coroutines can run on the main thread.
Now let's see how we can implement fetchItems(itemIds).
1. Simple forEach
Here we take advantage of the fact that all the coroutine code can run on the main thread:
suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> {
val itemById = mutableMapOf<Long, Item>()
coroutineScope {
itemIds.forEach { itemId ->
launch { itemById[itemId] = MyService.getItem(itemId) }
}
}
return itemById
}
coroutineScope will wait for all the coroutines you launch inside it. Even though they all run concurrently to each other, the launched coroutines still dispatch to the single (main) thread, so there is no concurrency issue with updating the map from each of them.
2. Thread-Safe Variant
The fact that it leverages the properties of a single-threaded context can be seen as a limitation of the first approach: it doesn't generalize to threadpool-based contexts. We can avoid this limitation by relying on the async-await mechanism:
suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> = coroutineScope {
itemIds.map { itemId -> async { itemId to MyService.getItem(itemId) } }
.map { it.await() }
.toMap()
}
Here we rely on two non-obvious properties of Collection.map():
It performs all the transformation eagerly, so the first transformation to a collection of Deferred<Pair<Long, Item>> is completely done before entering the second stage, where we await on all of them.
It is an inline function, which allows us to write suspendable code in it even though the function itself is not a suspend fun and gets a non-suspendable lambda (Deferred<T>) -> T.
This means that all the fetching is done concurrently, but the map gets assembled in a single coroutine.
3. Flow-Based Approach with Improved Concurrency Control
The above solved the concurrency for us, but it lacks any backpressure. If your input list is very large, you'll want to put a limit on how many simultaneous network requests you're making.
You can do this with a Flow-based idiom:
suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> = itemIds
.asFlow()
.flatMapMerge(concurrency = MAX_CONCURRENT_REQUESTS) { itemId ->
flow { emit(itemId to MyService.getItem(itemId)) }
}
.toMap()
Here the magic is in the .flatMapMerge operation. You give it a function (T) -> Flow<R> and it will execute it sequentially on all the input, but then it will concurrently collect all the flows it got. Note that I couldn't simplify flow { emit(getItem()) } } to just flowOf(getItem()) because getItem() must be called lazily, while collecting the flow.
Flow.toMap() is not currently provided in the standard library, so here it is:
suspend fun <K, V> Flow<Pair<K, V>>.toMap(): Map<K, V> {
val result = mutableMapOf<K, V>()
collect { (k, v) -> result[k] = v }
return result
}
If you are looking for just a nicer way to write it and eliminate foreach
lifecycleScope.launch {
try {
itemIds.asFlow()
.flowOn(Dispatchers.IO)
.collect{ itemId -> itemById[itemId] = MyService.getItem(itemId)}
} catch (exception: Exception) {
exception.printStackTrace()
}
Log.i(TAG, "All requests have been executed")
}
Also please look at lifecycleScope I suspect it is using Dispatchers.Main. If that is the case you can remove this .flowOn(Dispatchers.IO) extra dispatcher declaration.
For more info: Kotlin Asynchronous Flow