It is well known that Android places our application components (Activities, Services) under threat of being killed at any moment. This makes things really complicated if you want to provide a robust, non-leaky solution, while at the same time keeping the code clean and addressing separation of concerns.
Problem:
An activity starts a time-consuming task (in form of a Runnable
, AsyncTask
or whichever other mechanism). A progress dialog should be displayed. The task will open several connections, but it should have a means of updating a progress bar, and it might need to display a choice dialog half-way to completion. It also should be able to close the dialog and show a Toast when an error occurs, or when it completes.
When an Activity is killed and later a new instance is recreated, I can think of two options:
Try to kill the running task as well, and spawn another task instance when a new substitute activity instance is created. This is not an option for a long running task, as it is unacceptable from a user point of view to start the task and see the progress bar indicator going from 80% back to 0% with no apparent reason. This however is the approach followed in the Shelves example by Romain Guy. Apparently this is the approach recommended in the official Developer Guide.
Keep the task running: So we could have an activity subscribing to (and possibly starting) the task for updates at onResume
, and unsubscribing (and possibly pausing) it at onPause
.
Following the second option means that we should prevent the task of being collected in case the activity is temporarily destroyed. For instance, we could declare it inside a custom Application class, or even offer it as a service to the rest of the application (using a Singleton? an Android Service?) I'm no fan of this, though, since the task is used only by the activity, so it doesn't make sense to make it available to other classes. Android Services on the other hand also have to deal with life cycle.
Option #2 also involves ensuring the old activity is not leaked. We should not try to update the GUI if no activity is present. The whole think should be thread-safe. Finally the task should not remain in memory after we are sure it is no longer needed. But at the same time, if the initial activity is destroyed, the task keeps running, and a new substitute activity kicks in, the task has to be around to update the GUI immediately and display the task current status (or result in case it completed).
The fact that the task might be running when no activity is shown is another problem to deal with, since we could need user interaction (choice dialog) synchronously at some point. This could be solved if the activity had the ability to immediately pause the task when reaching onPause
, but the task could take some time to pause itself or even be unable to do so.
I know similar questions has been asked before, but I'm not asking specifically about how to solve the problem, but about what design would be recommended in order to achieve loose coupling and isolate the activity from the task as much as possible.
In short:
- This is a recurring problem in Android development and someone has probably come up with a clever design before. Is there a design pattern, or a well tested library that simplifies this?
- If you were to implement #2, which class would you choose to for the task (Runnable, Service, etc) and how would it communicate with the activity?
P.S. Please refrain from posting solutions based on the "orientation | keyboardHidden" hack XD. Other than this, any help would be appreciated.
UPDATE:
My first attempt has got a bit messy. The app is a bluetooth printing utility, which involves Detecting BT status (and asking the user to enable it if it were disabled), retrieving paired devices (and asking the user to pick one if there were more than one), then sending the data and finally inform the user about the result. As this task was 80% IO code, 20% GUI-related operations, I had to group the GUI code at the beginning and end of the task, then moving it out of the task back to the activity. I have an IntentService
that does the heavy lifting, then reports back to the activity via BroadcastReceiver
. This solves most of the problems but there are a few issues with such a design. First, there are all those constant strings used as keys to put and retrieve fields from the input and output Intents, which are introducing semantic coupling. I'd have preferred type-safety in the activity<->service communication. And I yet have to solve how to pass complex custom objects to the service in the Intent, by now I'm stuck with Parcelables and primitive parameters. Finally, both the activity and the service are using the same API (bluetooth), I'd have preferred to have al the BT-related code in a single class. I think I'll throw away this design, and try again with a custom printing queue based on threads, despite it will make harder dealing with killed activities.
why not have your activity start a service to host the long running task? a service is your best-bet for keeping things running long term. you can provide hints to the OS to leave it running. see startForeground()
and START_STICKY
.
you can communicate back to the activity by broadcasting from the service. have your activity programmatically register a broadcast receiver to listen for those intents. if the activity is paused, unregister your receiver, that way you won't respond when when you aren't in the foreground.
if the OS destroys your service, then it's killing the process, so your task is doomed anyway. the best you can do is restart it. anyway, this won't happen except in the most extreme conditions if you consider the above.
EDIT: summarizing some thoughts captured in the comments ... keeping a long running worker processe and automatically restarting it is almost always the wrong thing to do. mobile devices do not have the power source to make this feasible. the author stated this he has a special condition that negates this concern ... that would only be true if the device is connected to a power source almost always.
the android model is to provide a robust set of intents (notifications) to which your application can listen. these intents wake up your device / application when it's time to do something. your app should handle the notification, and then allow the device to go back to sleep immediately.
if there's no stock intent that maps to your event, you can use Google Cloud Messaging to wake up devices from a server. in other words, let the long-running processes run on the server, and wake up the device when required.
if you can do that, an alternative would be to use AlarmManager
to wake up periodically and check some state. even if you did this frequently (e.g., every 5 minutes, which is also a no-no), it'd be much better than keeping the device awake always as is being suggested. also, make use of inexact wakeup intervals (see documentation linked above).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With