I want to load some data inside activity after the button is clicked. I came up with the following solution and it works as I expect. But I just started learning kotlin coroutines and I want someone else to comment on my code. For example, is it okay that I update the UI using lifecycleScope.launch? I could probably use withContext(Dispatchers.Main) instead but is there a difference?
Is my implementation good in general? Is there something that could be optimzed/refactored?
I understand that it's better to use ViewModel and make API calls there but in this case I want all action to happen inside the activity.
class MainActivity : AppCompatActivity() {
var apiCallScope: CoroutineScope? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.btn_load_content).setOnClickListener {
// Cancel previous API call triggered by the click.
// I don't want to have multiple API calls executing at the same time.
apiCallScope?.cancel()
showProgress(true)
apiCallScope = CoroutineScope(Dispatchers.IO)
apiCallScope!!.launch {
// Execute Retrofit API call
val content = api.loadContent().await()
// Update UI with the content from API call on main thread
lifecycleScope.launch {
showProgress(false)
drawContent(content)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
apiCallScope?.cancel()
}
private fun showProgress(show: Boolean) {
// TODO implement
}
private fun drawContent(content: String) {
// TODO implement
}
}
It's preferable to use ViewModel to make such types of operations and not perform them inside Activity, especially in the onCreate method.
ViewModel gives you the viewModelScope property, any coroutine launched in this scope is automatically canceled if the ViewModel is cleared to avoid consuming resources.
Related
I am writing a library that allow user to start my activity with pass in params and callbacks.
The architecture I been following is the MVVM. I dont have any issue with the MVVM pattern however, I am running into an occasional issue with the callback pass in from my caller.
class mainActivity :Activity {
// standard lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
...
}
override fun onResume() {
// here I am accessing the static callback from companion object.
// but sometimes I am getting lateinit property callback has not been initialized
//exception.
callback.invoke()
}
companion object {
lateinit var callback: () -> Unit
//user will call my library from this method and provide me the callback
fun play(
callback: () -> Unit
){
callback = callback // here I assigned the callback so that my activity can use it later.
}
}
}
inside onResume I am accessing the callback. sometimes I am getting
lateinit property callback has not been initialized
however, I am not available to reproduce it 100% at all. What can be an issue? can you please shed some lights with me please?
I am trying to convert some Java code to Kotlin. I have a "heavy" object that I cannot reason about how to initialize properly in the app. The object can take some time to create and I don't want to block except for when the functionality is actually required. I wrote some code that meets my requirements, but it doesn't seem like a good pattern and I was hoping someone tell me what the proper pattern here (will list what I don't like about it after the code):
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
import javax.inject.Provider
import kotlinx.coroutines.channels.Channel
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = initHeavyObject()
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.get()
}
}
fun initHeavyObject(): Provider<HeavyInitObject> {
val cnl = Channel<HeavyInitObject>(Channel.BUFFERED)
//purposely block to ensure to ensure it is initialized
val provider = Provider { runBlocking { cnl.receive().also { cnl.close() }}}
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener{
override fun onReady(heavyInitObject: HeavyInitObject) = runBlocking {
cnl.send(heavyInitObject)
}
})
return provider
}
// Mocked library I am using (i.e. I don't have control over the implementation)
class HeavyInitObject {
companion object {
fun get(listener: HeavyInitObjectListener) {
val heavyInitObject = HeavyInitObject()
listener.onReady(heavyInitObject)
}
}
interface HeavyInitObjectListener {
fun onReady(heavyInitObject: HeavyInitObject)
}
}
What I don't like
Should be a val
It naturally really be a val, because the value should never change once initialized.
class MainActivity : AppCompatActivity() {
val heavyInitObject: Provider<HeavyInitObject> = initHeavyObject()
// OR...
val heavyInitObject: HeavyInitObject by lazy {
initHeavyObject().get()
}
The first option seems like it could do too much too fast. Depending on how someone would add MainActivity to the object graph it could really affect startup performance.
The second one is too slow. If we haven't requested the heavy object to be created before it is needed, there will be definite jank in the application when the heavy object is queried the first time.
Is there a good way to have the object be a val while requesting the object to be created in onCreate (understanding that I don't have control over implementation of the underlying library)?
Is channel the right data structure here?
Maybe this is bareable, but I wanted to see if there is a better option. A RENDEZVOUS channel makes more sense, but send suspends until receive is called and I don't want to block anything on thread initializing the object (i.e. since i can't convert the implementation to a suspend function). Switching to a bufferend channel won't block since I only send one element through, but that seems like a hack. What is the best data structure for this task?
Edit:
Thanks to some help in the comments I have improved the second condition (eliminate akward use of channel). I have a couple ideas for how to improve the first condition...
Code for getting rid of channel
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override suspend fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = lifecycleScope.async {deferredHeavyObject()}
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.await()
}
}
suspend fun initHeavyObject(): HeavyInitObject = suspendCancellableCoroutine { continuation ->
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener {
override fun onReady(heavyInitObject: HeavyInitObject) {
continuation.resume(heavyInitObject)
}
})
}
Code to finalize heavyInitObject
class MainActivity : AppCompatActivity() {
val heavyInitObject by lazy { heavyInitObjectBackingField }
private lateinit var heavyInitObjectBackingField: Deferred<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObjectBackingField = lifecycleScope.async { deferredHeavyObject()}
}}
I basically get a lateinit val that way... and can be confident I won't get an error for it not being initialized. Ideally, it makes me realize this is overcomplicated, because I can't get under the hood and easily seperate the object initialization from the call back initialization.. Unless anyone else has a better idea?
Channel is kind of weird for returning a single item. I would load it in a Deferred privately and publicly expose a suspend getter that awaits the Deferred result. Once the object is ready, it won’t have to be waited for. And since it’s a suspend function, you can access it via a coroutine without unlocking your main thread.
object HeavyObjectCreator {
private val heavyObject: Deferred<HeavyObject> = GlobalScope.async {
// Long running actions to generate the object…
HeavyObject(params)
}
suspend fun getInstance(): HeavyObject =
heavyObject.await()
}
In your activity you can use lifecycleScope.launch to start a coroutine when you need to do a task that uses the object and it can call the above function to get it in a suspending way. If you want to preload the heavy object, you can put the statement HeavyObjectCreator in onCreate of your Application class or your Activity so the creator object will be instantiated and start the coroutine to load the heavy object.
This is just one example of a way to do it for an object that you’ll be reusing on multiple screens. If you intend to load a new heavy object only on screens that need it, I’d consider putting the contents of the class above directly in a ViewModel and use viewModelScope instead of GlobalScope.
The following code gives me error as registering occurs after onResume:
class TempActivity: AppCompatActivity(){
private lateinit var binding: ActivityTempBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTempBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tempBtn.setOnClickListener {
val a = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
//SomeCode
}
a.launch(
//SomeIntent
)
}
}
However, if I use activityResultRegistry, I am not getting any errors. The code is
class TempActivity: AppCompatActivity(){
private lateinit var binding: ActivityTempBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTempBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tempBtn.setOnClickListener {
val a = activityResultRegistry.register("key", ActivityResultContracts.StartActivityForResult()){
// SomeCode
}
a.launch(
//Some Intent
)
}
}
The latter code run without any problem and launches the corresponding intent. I just want to know how safe is latter one and is there any unwanted behaviors I should be aware of?
It gives you an error because you are registering the contract conditionally after the Activity is well into its lifecycle.
The guide says:
You must always call registerForActivityResult() in the same order for each creation of your fragment or activity to ensure that the inflight results are delivered to the correct callback.
It's clear that if you register something after the Activity is created and it only happens when a condition (click event in this case) is met, the order of registration cannot be ensured.
A better solution would be to register the contract before the Activity is created and just call launch() when you need it. The guide, once again, says it is completely safe:
registerForActivityResult() is safe to call before your fragment or activity is created, allowing it to be used directly when declaring member variables for the returned ActivityResultLauncher instances.
So in your case, the Activity would look like this:
class TempActivity: AppCompatActivity() {
private lateinit var binding: ActivityTempBinding
// registering the contract here
private val a = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
//SomeCode
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTempBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tempBtn.setOnClickListener {
// launching the registered contract
a.launch(
//SomeIntent
)
}
}
}
Further explanation:
The registerForActivityResult() is a convenience method that internally calls the registry's register method with an automatically created key. The key is derived from an internal AtomicInteger that is retrieved and incremented every time you call registerForActivityResult(). Since this key is used to look up the callback that will handle the result, every call to the registerForActivityResult must be in the same order, otherwise it might happen that you once call it in the order of A (key=0), B (key=1) but then you call it B (key=0), A (key=1), or not even call the register method for one of the contracts (this is exactly what happens when you register in the OnClickListener).
In your specific case if the Activity gets recreated while you're waiting for the launched contract to return (for example, configuration change happens or the system simply kills the app), the callback will be removed from the registry (the key remains there though), meaning that it will not be called with the results.
So, to summarize the whole thing: you can (should) safely register any contract as a member field in your Activity or in the onCreate(...), and you should never register a contract on-the-fly (a.k.a. conditionally). Registering the contract will do nothing special, the real deal happens when you launch it.
Whenever I want to start a coroutine on a main thread,
fun main(args: Array<String>) {
GlobalScope.launch {
suspededFunction()
}
}
suspend fun suspededFunction() {
delay(5000L) //heavy operation
}
GlobalScope is highlighted, and always taunt that its usage is delicate and require care.
What delicacies are involved with GlobalScope, and importantly how can I start a coroutine without using GlobalScope?
To start coroutine without using a GlobalScope, one can do as:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
suspededFunction()
}
As mentioned in comments, some classes already have scopes available, like ViewModel class as viewModelScope.
in Activity or Fragment you can as follows:
//not recommended to use co-routines inside fragment or activity class
// this is just for example sack shown here.
// otherwise you have to do all your processing inside viewmodel
class Fragment : CoroutineScope by MainScope() {
...
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
Kotlin already created some scope. you can use it according to your situation. and you also create your own scope. but I suggest in the beginning it is better to use that already created
check official documentation https://developer.android.com/topic/libraries/architecture/coroutines
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
lifecycleScope.launch {
//for activity
}
}
//for viewmodel
suspend fun abc() = viewModelScope.launch {
}
I want to create a Service that makes a network operation but i want it to run as long as an activity is open. So i want to bind it in the activity's lifecycle. If the user navigates to another activity and back i want it to restart. If the screen goes off and the user reopens it i want it to start again if its not possible to keep it
class PushService: Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// ToDo Create the request that i want
}
}
So i have to start and stop the service in the onResume and onStop of the Activity?
override fun onResume() {
super.onResume()
Intent(this, PushService::class.java).also { intent ->
startService(intent)
}
}
override fun onStop() {
super.onStop()
stopService(Intent(this, PushService::class.java))
}
Im not sure how to do that. Does anybody know the correct way?
Perhaps it would be a good idea to just create the proccess that i want inside the ViewModel instead of start a Service for it?
You are mostly doing it correctly, except you should either be using onResume/onPause or onStart/onStop, not mixing the two pairs. onStart and onStop are only called when your activity is going out of view entirely. So in your example, if a dialog from another app appeared in front of yours, onStop would not get called, but onResume would get called so your already started service will get multiple onStartCommand calls.
However, the whole point of Services is to run operations that continue when your app is not visible. If you're not doing that, it would be simpler to write your own class (maybe that implements LifecycleObserver or borrows lifecycleScope from the Activity) to handle the background work. Then you wouldn't have to deal with registering it in the manifest and handling intents.
Example of a LifecycleObserver:
// lifecycle is a property of AppCompatActivity. You can instantiate this class
// from your activity.onCreate()
class MyNeworkTaskManager(lifecycle: Lifecycle): LifecycleObserver, CoroutineScope by lifecycle.coroutineScope {
init {
lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun onResume() {
startMyRequest()
}
#OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun onPause() {
pauseOrCancelMyRequest()
}
// Alternatively, if you want to expose suspend functions so your activity can request
// and respond to data in a coroutine without callbacks:
suspend fun getMyData(args: String): MyData {
val results = someNetworkRequestSuspendFunction(args)
return MyData(results)
}
// Or if you want to use coroutines for your network request, but still want
// your activity to use callbacks so it doesn't have to use coroutines to call
// these functions:
fun getMyDataAsync(args: String, callback: (MyData) -> Unit) = launch {
val results = someNetworkRequestSuspendFunction(args)
callback(MyData(results))
}
}
I don't do much with networking myself. But whatever library you're using, you can usually convert callbacks to coroutines using suspendCancellableCoroutine. There are tutorials for that you can look up.