I'm trying to use Kotlin co-routines to trigger a network request and handle Exceptions. I've looked at a lot of tutorials on co-routines but I'm really struggling to relate what i know to the problem i have.
The problem
Trying to get an exception to be caught in the View, but no Exception is thrown from the ViewModel so the application crashes.
The Code
My Android app has three layers that relate to this issue. I have the View, ViewModel and a Service layer.
In the service layer, request.execute() can throw a UserAuthException.
MyView (View)
private val mViewModel: MyViewModel by viewModels()
private fun getFileId() {
try {
mViewModel.requestFileId()
} catch (e: UserAuthException) {
Timber.i(e)
}
}
MyViewModel (ViewModel)
private val apiService = MyApiService()
fun requestFileId() {
viewModelScope.launch {
ApiService.requestFileId()
}
}
MyApiService (Service Layer)
suspend fun requestFileId(): FileId = withContext(Dispatchers.IO) {
request.execute()
}
Things that i have looked at
I have played around with CoroutineExceptionHandlers, supervisorJobs with no luck, but without the fundamental knowledge of how these things work I'm not really making any progress.
Any help would be appreciated, thanks.
fun requestFileId() {
viewModelScope.launch {
ApiService.requestFileId()
}
}
This is not a suspendable function. It launches a concurrent coroutine and returns right away. Clearly, calling requestFileId() will never throw an exception.
Launching a coroutine is just like starting another thread, it introduces concurrency to your code. If your current code hopes to stay non-concurrent while observing the results of suspendable functions, you may be looking at significant architectural changes to the application to make it behave correctly under concurrency.
In your model, change it to something like this:
fun requestFileId() {
viewModelScope.launch {
try {
ApiService.requestFileId()
} catch (e: Exception) {
// inform your view
}
}
}
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.
I wrote code which works fine without kotlin flow, but i want to try kotlin flow and when i used it inside repository but somehow it is not even entering inside i could not find any solution since it is not throwing any error, it is just not entering inside function and i think it is because of flow collector. It is coming till repository but for repo it is not entering.
class NewsRepositoryImpl(private val newsService: NewsService) : NewsRepository {
override suspend fun getNews(search: String): Flow<ResultWrapper<List<Article>>> = flow {
emit(ResultWrapper.Loading)
try {
val news = newsService.getNews(search, BuildConfig.API_KEY)
emit(ResultWrapper.Success(news.articles.map { it.toArticle() }))
} catch (e: Exception) {
emit(ResultWrapper.Error(e.message))
}
}
}
Using flow builder you create a cold stream of data. The code inside it is executed only when the flow is being collected, i.e. terminal operators invoked, for example collect, collectLatest, first... .
coroutineScope.launch {
newsRepository.getNews("news").collectLatest { result ->
// use result
}
}
I am really happy that I switched my long running tasks, which constantly produce results to UI thread to coroutines. It improved performance and decreased memory usage by 3 times and all memory leaks disappeared compared to AsyncTask or regular Threads in Android.
The only problem remains is that, I don't know how should I restart my long running operation after exception has occurred at some time...
I feel I did not understand exception handling in coroutines at all after reading tons of article. Let me know how can I achieve desired behaviour.
I have coroutine scope in fragment(will move to VM in near future).
lateinit var initEngineJob: Job
override val coroutineContext: CoroutineContext
get() = initEngineJob + Dispatchers.Main
Long running task with async/await.
fun initWorkEngineCoroutine()
{
launch {
while(true) {
val deferred = async(Dispatchers.Default) {
getResultsFromEngine()
}
val result = deferred.await()
if (result != null) {
//UI thread
draw!!.showResult(result)
}
}
}
}
fun getResultsFromEngine() :Result? {
result = // some results from native c++ engine, which throws exception at some times
return result
}
i don't know where should I put try catch. I tried to surround deferred.await() with try catch, but I could not call same method in catch block to retry long running task. I tried SupervisorJob(), but no success either. I still could not call initWorkEngineCoroutine() again and start new coroutine...
Help to solve this issue finally :)
You should treat your code as linear imperative and try/catch where it makes the most logical sense in your code. With this mindset, your question is probably less about coroutines and more about try/catch retry. You might do something like so:
fun main() {
GlobalScope.launch {
initWorkEngineCoroutine()
}
}
suspend fun initWorkEngineCoroutine() {
var failures = 0
val maxFailures = 3
while(failures <= maxFailures) {
try {
getResultsFromEngine()?.let {
draw!!.showResult(it)
}
} catch (e: Exception) {
failures++
}
}
}
// withContext is like async{}.await() except an exception occuring inside
// withContext can be caught from inside the coroutine.
// here, we are mapping getResultFromEngine() to a call to withContext and
// passing withContext the lambda which does the work
suspend fun getResultsFromEngine() :Result? = withContext(Dispatchers.Default) {
Result()
}
I've included some logic to prevent infinite loop. It's probably not going to fit your requirements, but you might consider some sort of thing to prevent an issue where exceptions are raised immediately by getResultsFromEngine() and end up causing an infinite loop that could result in unexpected behavior and potential stackoverflow.
I try to manage my threads for IO Processes. One of is for Realm usage.
like…
init {
val ai = app.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
bundle = ai.metaData
runBlocking(appExecutors.dbContext) {
Realm.init(app)
}
}
It works fine if I set val dbContext as newSingleThreadContext(“databaseIO”)…
But I develop an android-library, so If there is an implementation of Realm on the app module, I need to set the usage on the same thread. And generally, everyone uses main thread to access to Realm. In that case I tried to set UI but it caused ANR. I can understand why it causes ANR, but I can’t find a proper solution for this scenario.
Note: if I use it with launch… it works for here. But on my RealmManager class I need to use runBlocking. So there is no way to use only launch…:slight_smile:
like…
fun getProfile(id: String): Profile? {
try {
return runBlocking(dbCoroutine) {
val query = realm!!.where(Profile::class.java).equalTo("numbers.id", id)
query.findFirst()
}
} catch (ex: Exception) {
logger.e(TAG, ex)
return null
}
}
or
internal val allProfiles: List<Profile>
get() = runBlocking(dbCoroutine) { realm!!.where(Profile::class.java).findAll() }
Is there anything I do wrong way, or any advice for better implementation?