For example i have construction like this:
lifecycleScope.launch {
viewModel.handleAppLoad() {
val app = AppFactory.createApp(
context = Application.instance.applicationContext
)
app.doSmth()
startActivity(
SuccessActivity.createIntent(
requireContext()
)
)
}
}
In my fragment code, when i clicked on some button.
suspend fun handleAppLoad(
scope: CoroutineScope = viewModelScope,
block: suspend () -> Unit
) {
scope.launch {
progress.value = true
try {
delay(1000)
block()
} catch (ex: MsalOperationCanceledException) {
// B2C process was cancelled, do nothing
} catch (ex: MsalException) {
_msalErrorEvent.emit(ex)
Timber.e(ex)
}
progress.value = false
}
}
^ My coroutine wrapper
Also i have this code in AppFactory.
object AppFactory {
suspend fun createApp(
context: Context
): App {
return suspendCoroutine { cont ->
App.create(
context,
object : IApp.ApplicationCreatedListener {
override fun onCreated(application: App) {
cont.resume(application)
}
override fun onError(exception: Exception) {
cont.resumeWithException(exception)
}
}
)
}
}
}
The problem is that when the application goes to the background and the callback cont.resume(application) works in the background, the coroutine does not stop, but continues to wait for the same cont.resume(application), so that's why my progress stay active, while cont.resume(application)already happened. I know a way to fix it by removing the callback->coroutine construction, but I am interested in the way to fix the current version, since the coroutine has a wrapper that controls the progress at the start and end of the coroutine.
Related
I'm looking for filter a list on user input (SearchView)
fun onQuery(query: String) {
viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
If the user tap quickly on the keyboard the function will be called many times and sometimes before the previous call is finished. So I'm looking to stop the coroutine if a new call is done and the coroutine is already running. How can I achieved this please ?
What I tried :
fun onQuery(query: String) {
val job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
if (job.isActive) job.cancel()
job.start()
}
If there is supposed to be only one instance of that job running, then I would try taking a reference to it outside of that code block and storing it as a private variable inside the class in which the method onQuery operates:
private var job: Job? = null
...
fun onQuery(query: String) {
job?.run { if (isActive) cancel() }
job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
I'm seeing some odd behavior. I have a simple StateFlow<Boolean> in my ViewModel that is not being collected in the fragment. Definition:
private val _primaryButtonClicked = MutableStateFlow(false)
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked
and here is where I set the value:
fun primaryButtonClick() {
_primaryButtonClicked.value = true
}
Here is where I'm collecting it.
repeatOnOwnerLifecycle {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
repeatOnOwnerLifecycle:
inline fun Fragment.repeatOnOwnerLifecycle(
state: Lifecycle.State = Lifecycle.State.RESUMED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state) {
block()
}
}
What am I doing wrong? The collector never fires.
Does this make sense?
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked.asStateFlow()
Also I couldn't understand the inline function part, because under the hood seems you wrote something like this
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
}
Why are you launching one coroutine in another and collect the flow from IO dispatcher? You need to collect the values from the main dispatcher.
I'm using the code below for a network request throught okhttp3:
runOnDefaultDispatcher {
try {
val newsResponse = Xxxx.xxxxClient.getNews()
if (newsResponse.success && newsResponse.data != null && newsResponse.data.count() > 0) {
runOnMainDispatcher {
val adapter = NewsAdapter(newsResponse.data, getString(R.string.news)).also {
initListener(it, newsResponse.data)
}
binding.list.adapter = adapter
adapter.notifyDataSetChanged()
}
}
} catch (exception: Exception) {
runOnMainDispatcher {
binding.list.visibility = View.GONE
val errorLayout = view.findViewById<RelativeLayout>(R.id.error_layout)
errorLayout.visibility = View.VISIBLE
errorLayout.findViewById<TextView>(R.id.error_title).text = "Oops..."
errorLayout.findViewById<TextView>(R.id.error_message).text = exception.message
}
}
}
The implementation code of runOnDefaultDispatcher and runOnMainDispatcher is down below:
import kotlinx.coroutines.*
fun block(block: suspend CoroutineScope.() -> Unit): suspend CoroutineScope.() -> Unit {
return block
}
fun runOnDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.Default, block = block)
suspend fun <T> onDefaultDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.Default, block = block)
fun runOnIoDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.IO, block = block)
suspend fun <T> onIoDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.IO, block = block)
fun runOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.Main.immediate, block = block)
suspend fun <T> onMainDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.Main.immediate, block = block)
I except the exception would be caught and no crash would appear.
However the application still CRASH:
FATAL EXCEPTION: DefaultDispatcher-worker-2
Java.net.SocketException: Connection reset
The calls to launch don't work well with try/catch.
e.g. this will crash the app
try {
GlobalScope.launch { throw Excepton() }
} catch (e: Exception) {
}
On the other hand, suspend functions work with try/catch as you would expect so this example DOES NOT crash the app:
suspend fun bang(): Unit = throw Exception()
try {
bang()
} catch (e: Exception) {
}
In your code you have launch inside try/catch, meaning you have a scenario like the first example here.
The solution is to build your program as suspend functions, and only use launch one to execute the result (note: this doesn't apply universally but does apply in this scenario).
When running the program you probably want to use lifecycleScope.
Also you might want to consider using a ViewModel so that the network call survives configuration changes.
You can check the Kotlin Coroutines on Android guide for more.
You can see the difference between to code blocks. The first one will crash because exception occurs in a different thread. The second one will not crash because you are catching exception in the right place.
fun main() {
val scope = CoroutineScope(Job() + Dispatchers.Default)
//First way
try {
scope.launch {
exceptionThrowingFunction()
}
} catch (e: Exception) {
println(e.message)
}
//Second way
scope.launch {
try {
exceptionThrowingFunction()
} catch (e: Exception) {
println(e.message)
}
}
Thread.sleep(10000)
}
private suspend fun exceptionThrowingFunction() {
delay(10)
throw IllegalArgumentException("Test Error")
}
How can I call a composable function from context of corrutines?
I trying the following code but I getting the error.
#Composable
fun ShowItems(){
var ListArticle = ArrayList<Article>()
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
viewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is MainViewModel.LatestNewsUiState.Success -> {
//Log.e(TAG,"${uiState.news}")
if(uiState.news != null){
for(i in uiState.news){
ListArticle.add(i)
}
context.ItemNews(uiState.news.get(4))
Log.e(TAG,"${uiState.news}")
}
}
is MainViewModel.LatestNewsUiState.Error -> Log.e(TAG,"${uiState.exception}")
}
}
}
}
You should do something like this:
#Composable
fun ShowItems(){
val uiState = viewModel.uiState.collectAsState()
// Mount your UI in according to uiState object
when (uiState.value) {
is MainViewModel.LatestNewsUiState.Success -> { ... }
is MainViewModel.LatestNewsUiState.Error -> { ... }
}
// Launch a coroutine when the component is first launched
LaunchedEffect(viewModel) {
// this call should change uiState internally in your viewModel
viewModel.loadYourData()
}
}
In my app, I have this flow:
ClickListender in my fragment:
search_button.setOnClickListener {
if(search_input.text.isNullOrEmpty())
Toast.makeText(activity, "Input Error", Toast.LENGTH_LONG).show()
else
viewModel.onSearchButtonClicked(search_input.text.toString())
}
onSearchButtonClicked inside viewModel:
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
repo.insertToDatabase(input)
}
}
insertToDatabase inside Repository:
suspend fun insertToDatabase(string: String) {
withContext(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
//show error
} else {
//all good
database.myDataBase.insertAll(dataList)
}
}
}
I need to show error message if intialDataResult is less then one.
I thought about create MutableLiveData inside my repository with initial value of false and listen from the fragment through the viewModel, but it's not good approach because I have no way to set the LiveData to "false" again after I show error message.
I also tried to return bool from the insertToDatabase function and decide if to show error or not, with no success.
Any ideas how can I solve this?
Why not create a LiveData to manage your work's result state?
Create a class to store result of work why sealed class?
sealed class ResultState{
object Success: ResultState() // this is object because I added no params
data class Failure(val message: String): ResultState()
}
Create a LiveData to report this result
val stateLiveData = MutableLiveData<ResultState>()
Make insertToDatabase() return a result
suspend fun insertToDatabase(input: String): ResultState {
return withContext<ResultState>(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
return#withContext ResultState.Failure("Reason of error...")
} else {
database.myDataBase.insertAll(dataList)
return#withContext ResultState.Success
}
}
}
Now, report result to UI
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
val resultState = repo.insertToDatabase(input)
stateLiveData.value = resultState
}
}
In UI,
viewModel.stateLiveData.observe(viewLifeCycleOwner, Observer { state ->
when (state) {
is ResultState.Success -> { /* show success in UI */ }
is ResultState.Failure -> { /* show error in UI with state.message variable */ }
}
})
Similarly, you can add a ResultState.PROGRESS to show that a task is running in the UI.
If you have any queries, please add a comment.