The problem with this as I see it is that you have to guarantee that registerForActivityResult() is called before your own activity's OnCreate() completes. OnCreate() is obviously not a suspending function, so I can't wrap registerForActivityResult() and ActivityResultLauncher.launch() in a suspendCoroutine{} to wait for the callback, as I can't launch the suspendCoroutine from OnCreate and wait for it to finish before letting OnCreate complete...
...which I did think I might be able to do using runBlocking{}, but I have found that invoking runBlocking inside OnCreate causes the app to hang forever without ever running the code inside the runBlocking{} block.
So my question is whether runBlocking{} is the correct answer but I am using it wrong, or whether there is some other way to use registerForActivityResult() in a coroutine, or whether it is simply not possible at all.
You can do something like this.
Please refer to the implementation below.
class RequestPermission(activity: ComponentActivity) {
private var requestPermissionContinuation: CancellableContinuation<Boolean>? = null
#SuppressLint("MissingPermission")
private val requestFineLocationPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
requestPermissionContinuation?.resumeWith(Result.success(isGranted))
}
suspend operator fun invoke(permission: String) = suspendCancellableCoroutine<Boolean> { continuation ->
requestPermissionContinuation = continuation
requestFineLocationPermissionLauncher.launch(permission)
continuation.invokeOnCancellation {
requestPermissionContinuation = null
}
}
}
Make sure you initialize this class before onStart of the activity. registerForActivityResult API should be called before onStart of the activity. Refer to the sample below
class SampleActivity : AppCompatActivity() {
val requestPermission: RequestPermission = RequestPermission(this)
override fun onResume() {
super.onResume()
lifecycleScope.launch {
val isGranted = requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)
//Do your actions here
}
}
}
Related
Hey I want to call api from object class. I am new in Coroutines. I tried some code, but i am not sure is it correct way of doing it or not.
Inside LoginHelper there is function called logout have more that one function. I want to excute api call first. then i want to excute other function inside logout.
In Mainactivity I am calling LoginHelper.logout it will finish then i need to excute other line. But i don't want to make suspend function because it's using other place as well.
Also i got a errorProcess:
com.dimen.app, PID: 12496
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1605)
Session.kt
interface Session{
#DELETE("/session/delete")
fun deleteSession(): Call<Void>
}
SessionRepository.kt
suspend fun deleteSession(): RequestResult<Void> {
return apiCall(api.deleteSession())
}
RequestResult is a Sealed Class
sealed class RequestResult<out T : Any> {
data class Success<out T : Any>(): RequestResult<T>
data class Error(): RequestResult<Nothing>()
fun result(success: (data: T?) -> Unit),error: (error: Error) -> Unit)
}
MainActivity.kt
private fun setupLogout() {
logoutButton.setOnClickListener {
LoginHelper.logout() // need to wait untill this finish
// more logic here....
}
}
LoginHelper.kt
object LoginHelper {
fun logout() {
logD("logout")
deleteSession() // need to wait untill this finish and then excute more function....
}
private fun deleteSession() {
runBlocking{
apiCall.deleteSession().execute()
}
}
}
Never use runBlocking in an Android app unless you know exactly what you're doing. It's the wrong choice 99% of the time because it defeats the purpose of using coroutines. Blocking means the current thread waits for the coroutine to run its asynchronous code. But you cannot block the main thread because that freezes the UI.
Since your LoginHelper is an object or singleton, it needs its own CoroutineScope if it's going to launch coroutines.
You can make deleteSession() a suspend function so it can call the api.deleteSession() suspend function.
You can make logout() launch a coroutine to sequentially delete the session and subsequently perform other tasks. And you can make it return the launched Job so other classes can choose whether or not to simply start the logout, or to start and wait for the logout in a coroutine.
object LoginHelper {
private val scope = CoroutineScope(SupervisorJob() + CoroutineName("LoginHelper"))
fun logout(): Job = scope.launch {
logD("logout")
deleteSession()
// .... more functions that happen after deleteSession() is complete
}
private suspend fun deleteSession() {
Tokenclass.getToken()?.let {
logE("token ::-> $it")
apiCall.deleteSession(it).execute()
}
}
}
If you want the outside class to be able to wait for the logout to complete, it can call join() on the returned Job in its own coroutine, for example:
logoutButton.setOnClickListener {
lifecycleScope.launch {
LoginHelper.logout().join()
// more logic here....
}
}
If you don't need to wait for it in the activity, you don't need to start a coroutine, and you don't need to call join().
I have collect flow from shared viewmodel in fragment :
private val viewModel: MyViewModel by sharedViewModel()
private fun observeViewModelStateFlowData() {
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
in ViewModel :
private val _stateFlowData = MutableStateFlow<ViewStates<Model>>(ViewStates.Idle)
val stateFlowData: StateFlow<ViewStates<Model>> get() = _stateFlowData
but when I go to next fragment and back to this fragment again, flow collect again.
I cancel the job in onStop() lifecycle method of fragment :
override fun onStop() {
job?.cancel()
super.onStop()
}
but not cancel and collect again!!!
This happens even when I leave the activity (when the viewmodel is cleared) and come back to activity again!!!
How can I do this so that I can prevent the collecting of flow ?
Well you have to know something about coroutine. If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.
You need to make sure that all the coroutine work you’re implementing is cooperative with cancellation, therefore you need to check for cancellation periodically or before beginning any long running work. Try to add check before handling a result.
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
ensureActive()
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
For more info take a look on this article https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629
I checked the other questions but none of them seem to address my issue.
I have two suspend funs in my HomeViewModel and I'm calling them in my HomeFragment (with a spinner text parameter).
The two suspend functions in HomeViewModel:
suspend fun tagger(spinner: Spinner){
withContext(Dispatchers.IO){
val vocab: String = inputVocab.value!!
var tagger = Tagger(
spinner.getSelectedItem().toString() + ".tagger"
)
val sentence = tagger.tagString(java.lang.String.valueOf(vocab))
tagAll(sentence)
}
}
suspend fun tagAll(vocab: String){
withContext(Dispatchers.IO){
if (inputVocab.value == null) {
statusMessage.value = Event("Please enter sentence")
}
else {
insert(Vocab(0, vocab))
inputVocab.value = null
}
}
}
and this is how I call them in the HomeFragment:
GlobalScope.launch (Dispatchers.IO) {
button.setOnClickListener {
homeViewModel.tagger(binding.spinner)
}
}
At tagger I get the error "Suspension functions can be called only within coroutine body". But it's already inside a global scope. How can I avoid this problem?
But it's already inside a global scope.
The call to button.onSetClickListener() is in a launched coroutine from a CoroutineScope. However, the lambda expression that you are passing to onSetClickListener() is a separate object, mapped to a separate onClick() function, and that function call is not part of that coroutine.
You would need to change this to:
button.setOnClickListener {
GlobalScope.launch (Dispatchers.IO) {
homeViewModel.tagger(binding.spinner)
}
}
BTW, you may wish to review Google's best practices for coroutines in Android, particularly "The ViewModel should create coroutines".
I've been mulling this over for some time now and I just can't get it to work.
So in brief, I have a Splash Activity from where I call another activity that contains my ViewModel. The ViewModel in simple terms just needs to sequentially run function A(which is getfbdata below; it is a network call.). And only after this function completes, it should run function B (which is dosavefbdata below; save info to DB.). Again, it should wait for function B to complete before running the main thread function, function C(which is confirm first below; it checks whether function B has completed by getting the result from function B (dosavefbdata below). If function C is positive, it closes the Splash activity.
Suffice to say, none of the above works. Println results show all functions were run sequentially without waiting for each to complete. Lastly, SplashActivity().killActivity() call on function C did not work.
Note: withContext does not require to await() on the suspended functions right? I also tried using viewModelScope.async instead of viewModelScope.launch.
I would really appreciate your help here. Thanks in advance.
*Under SplashActivity:
fun killActivity(){
finish()
}
*Under onCreate(SplashActivity):
CoroutingClassViewModel(myc).initialize()
**
class CoroutingClassViewModel(val myc: Context): ViewModel() {
fun initialize() {
viewModelScope.launch(Dispatchers.Main) {
try {
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
val test1 = withContext(Dispatchers.IO) { test1(fbdata) }
val savedfbdata = withContext(Dispatchers.IO) { dosavefbdata(fbdata,myc) }
val confirmfirst = { confirmfunc(savedfbdata,myc) }
println("ran savedfbdata.")
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}
}
fun confirmfunc(savedfbdata: Boolean, myc: Context){
if (savedfbdata==true){
SplashActivity().killActivity()
}
}
suspend fun getfbdata(): MutableList<FirebaseClass> {
return withContext(Dispatchers.IO) {
//perform network call
return#withContext fbdata
}
}
suspend fun dosavefbdata(fbdata: MutableList<FirebaseClass>,myc: Context): Boolean{
return withContext(Dispatchers.IO) {
//save to database
return#withContext true
}
}
suspend fun test1(fbdata: MutableList<FirebaseClass>){
return withContext(Dispatchers.IO) {
println("test1: fbdata is: $fbdata")
}
}
}
Use AndroidViewModel if you want to have Context in it:
class CoroutingClassViewModel(myc: Application) : AndroidViewModel(myc) { ... }
In onCreate method of SplashActivity activity instantiate the view model like this:
val vm = ViewModelProvider(this)[CoroutingClassViewModel::class.java]
vm.initialize()
In CoroutingClassViewModel class create LiveData object to notify activity about operations completion:
val completion = MutableLiveData<Boolean>()
fun confirmfunc(savedfbdata: Boolean, myc: Context) {
if (savedfbdata) {
completion.postValue(true)
}
}
In your SplashActivity use this code to observe completion:
vm.completion.observe(this, Observer {
if (it) killActivity()
})
You use withContext(Dispatchers.IO) function two times for the same operation. Don't do that. For example in this code:
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
if we look at getfbdata function we see that function withContext(Dispatchers.IO) is already called there. So get rid of repeated calls:
val fbdata = getfbdata()
I had same issue with withContext(Dispatcher.IO), I thought that switching coroutine context doesn't work, while in fact in splash screen i launched super long operation on Dispatcher.IO, then later when trying to use the same Dispatcher.IO it didn't work or in other words it waited until the first work in splash screen finished then started the new work.
I have a class that launches coroutines and allows them to be cancelled when the Activity/Fragment they are called from is destroyed. However it is not working like I expect. When I back out of the fragment while the operation is running, the coroutine cancel does not take, and I get an NPE when trying to access a View that does not exist anymore.
open class CoroutineLauncher : CoroutineScope {
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
private val supervisorJob = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = dispatcher + supervisorJob
fun launch(action: suspend CoroutineScope.() -> Unit) = launch(block = action)
fun cancelCoroutines() {
supervisorJob.cancelChildren() //coroutineContext.cancelChildren() has same results
}
}
here is the usage
class MyFragment : Fragment {
val launcher = CoroutineLauncher()
fun onSomeEvent() {
launcher.launch {
val result = someSuspendFunction()
if (!isActive) return
// CAUSES CRASH
myTextView.text = result.userText
}
}
override fun onDestroyView() {
super.onDestroyView()
launcher.cancelCoroutines()
}
}
I added log lines to ensure onDestroyView and cancelCoroutines are both being called before the crash. I feel like I'm missing something obvious but what I'm doing seems to be inline with the recipes suggested here: https://proandroiddev.com/android-coroutine-recipes-33467a4302e9
Any ideas?
Ok I figured it out. onSomeEvent was being invoked after cancelCoroutines was called. Since we call cancelChildren on the SupervisorJob instead of cancel, the launcher does not refuse new Jobs, and since the cancel already happened, the new coroutine runs like normal and crashes. I fixed this by checking if the fragment is visible before calling launcher.launch and bailing out of the method if the fragment is not visible.
This could also be fixed by calling supervisorJob.cancel() instead of supervisorJob.cancelChildren(), though that has some other side effects that I didn't want