Create a service that runs while activity is open - android

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.

Related

Different between Android onResume(), onStart() and lifecycleScope

I have viewmodel call TestViewModel and a method call fetchDataFromDataSource() to call fetch data from the server, I used to call load data on OnResume() until I bump into lifecycleScope
I have tried to read more but didn't really get which is better.
class TestViewModel: Viewmodel() {
fun fetchDataFromDataSource(){
....
}
}
class TestActivity : AppCompatActivity() {
private val viewModel: TestViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Is it best to call here
viewModel.fetchDataFromDataSource()
}
}
}
onResume(){
super.onResume()
// or is it best to call here
viewModel.fetchDataFromDataSource()
}
}
where is the best place to call fetchDataFromDataSource(), is it in onResume() or lifecycleScope and what is the advantage lifecycleScope has over onResume() or onStart()
I know the view has rendered at onResume() so what benefit does lifecycleScope has over android lifecycle (onResume onCreate onStart...)
repeatOnLifecycle is similar to calling methods on the respective lifecycle events every time the Activity hits that state but with a quick access to the lifecycleScope which can launch a coroutine.
Example:
override fun onResume(){
super.onResume()
viewModel.fetchDataFromDataSource()
}
is equivalent to -
class MainActivity : AppCompatActivity {
init {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.fetchDataFromDataSource()
}
}
}
}
If you want to load the data from ViewModel every time the user comes to foreground from background, use onStart or repeatOnLifecycle(Lifecycle.State.STARTED).
If you need to load the data everytime the Activity resumes, then use onResume or the lifecycleScope equivalent as shown above but if this is just a one-time op, consider using onCreate.

How safe is registering contracts with ActivityResultRegistry after onResume in android?

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.

Make Retrofit API call in Activity using Kotlin coroutines

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.

why coroutine method in ViewModel is continuing to process after leaving Fragment?

Here is my method I am calling updateProgress() in Fragment onCreate()
and after navigating forward to another Activity or Fragment this updateProgress is still continue to work. How can I stop this ?
I was expecting if I am navigating to another Activity or Fragment
ViewModel onCleared() should be called and it will stop this update
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun updateProgress() {
uiScope.launch {
delay(UPDATE_PROGRESS_INTERVAL)
updateCurrentProgramProgress()
contentRowsMutableData.postValue(contentRows)
updateProgress()
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
The onCleared is called when the fragment is destroyed. If you move to another fragment, the previous one will remain in the backstack (if you used addToBackstack) and thus is paused, but not destroyed.
If you want to stop processing data when the fragment is paused, you can have your ViewModel implement the LifecycleObserver interface, then you would add
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
// add code here to stop work
}
You can then also resume processing with this
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// code here
}
However, note that working while the fragment is paused may be desirable, so when the user returns to the fragment the work is complete and data can be shown immediately. It all depends on your specific circumstances.

How to access a service from a fragment, via its activity?

I have an activity that, as far as I can tell, is happily binding to a service every time the activity is created. The first time the activity is created, it also starts the service with the startService command, as follows:
private fun attachRecorderService() {
val intent = Intent(this, AudioRecorderService::class.java)
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
if (!mBooleanRecorderServiceStarted) {
startService(intent)
mBooleanRecorderServiceStarted = true
}
}
That activity's fragments all get a reference to that service in their onActivityCreated() function as follows:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val parentActivity = activity as MainActivity
mAudioRecorderService = parentActivity.mAudioRecorderService
That works fine the first time the fragment is created, but as soon as the screen is rotated I get an error telling me the service hasn't been initialised in the activity.
lateinit property mAudioRecorderService has not been initialized
As far as I can tell, onActivityCreated() in the fragment is racing with onCreate() in the activity, and trying to get the reference before onCreate() initialises it.
Which I don't understand. I thought onActivityCreated() waited until after onCreate() had completed.
What am I doing wrong? Should I use some sort of callback in the fragment, that only triggers when the activity has bound to the service? I've seen mention of that, but I have no idea how to do it.
The question Communication between Activity and Service deals with Activities and Services. I'm asking about the Fragments that are attached to the Activity, and how they can access a service that the Activity has already bound to.
The service is not available directly after calling bindService. Use a ServiceConnection. When onServiceConnected is called the service is ready to use
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(p0: ComponentName?) {
}
override fun onServiceConnected(p0: ComponentName, binder: IBinder) {
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
application.bindService(Intent(application, MyService::class.java), connection, Context.BIND_AUTO_CREATE)
}

Categories

Resources