|
| 1 | +# Android-Retainable-Tasks |
| 2 | +[  ](https://bintray.com/rolf-smit/maven/android-retainable-tasks/_latestVersion) |
| 3 | +[](http://android-arsenal.com/details/1/3236) |
| 4 | + |
| 5 | +Android-Retainable-Tasks is an easy to use mini-library for easy asynchronous background tasking with callback support to the UI. This library is based on the Android `AsyncTask` implementation but with support for retaining tasks and therefore surviving configuration changes (orientation). |
| 6 | + |
| 7 | +*Key features:* |
| 8 | + |
| 9 | + - Light weight & Simple API |
| 10 | + - Annotation support |
| 11 | + - Same Task API on all Android versions, based on the Nougat AsyncTask implementation. |
| 12 | + - Supports the [LifecycleObserver](https://developer.android.com/reference/android/arch/lifecycle/LifecycleObserver.html) class from the [Android Architecture Lifecycle](https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.html) library; |
| 13 | + - Supports API 14+ <sub>(with or without the Android support libraries)</sub> |
| 14 | + |
| 15 | +**Add it to your project** |
| 16 | + |
| 17 | +Android-Retainable-Tasks is available on jCenter, just add the following compile dependency to your modules build.gradle file. |
| 18 | + |
| 19 | +```groovy |
| 20 | +dependencies { |
| 21 | + // The library dependency |
| 22 | + compile 'org.neotech.library:android-retainable-tasks:1.0.0-alpha-1' |
| 23 | + |
| 24 | + // Only needed if you want to use annotations |
| 25 | + annotationProcessor 'org.neotech.library:android-retainable-tasks-compiler:1.0.0-alpha-1' |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +**Index** |
| 30 | + |
| 31 | +1. [Basic usage](#1-basic-usage) |
| 32 | +2. [How it works](#2-how-it-works) |
| 33 | +3. [Advanced usage](#3-advanced-usage) |
| 34 | +4. [FAQ](#4-faq) |
| 35 | +5. [ToDo](#5-to-do) |
| 36 | + |
| 37 | +>**Why use this library?** |
| 38 | +>*Always! its awesome!* |
| 39 | +>No, obviously you should not always use this library. This library is useful when you need to do stuff in the background which is heavily bound to the user-interface, like: Refreshing large lists from a database, loading an article from a network source or decoding an image, all those things are quit tightly bound to the user-interface. You need to use an additional library if you need: task scheduling, automatic retries, task persistence across reboots, task constrains (network availability) etc. |
| 40 | +
|
| 41 | + |
| 42 | +## 1. Basic usage |
| 43 | +--- |
| 44 | +In order to, execute a task which modifies the user-interface and to retain it across configuration changes you will need to do four things: |
| 45 | + |
| 46 | +1. Create an implementation of the `Task` class; |
| 47 | +2. Make your Activity extend the `TaskActivityCompat` class (or for Fragments the `TaskFragmentCompat` class); |
| 48 | +3. Implement the `Callback` interface somewhere and execute the task using the `TaskManager`; |
| 49 | +4. Retain the task using the `TaskActivityCompat.onPreAttach()` method. |
| 50 | + |
| 51 | +### 1.1 Creating the task |
| 52 | + |
| 53 | +You will need to extend the `Task` class to create a custom task. The `Task` class is heavily based on the default Android `AsyncTask` class and you will need to override at least the `doInBackground` method and provide an appropriate constructor. |
| 54 | + |
| 55 | +>**Note:** |
| 56 | +>A`Task` doesn't come with a generic type for input parameters like the Android `AsyncTask`, instead you should provide input when constructing the `Task` (using the constructor for example). |
| 57 | +
|
| 58 | +**Example:** |
| 59 | + |
| 60 | +```java |
| 61 | +private class ExampleTask extends Task<Integer, String> { |
| 62 | + |
| 63 | + public ExampleTask(String tag){ |
| 64 | + super(tag); |
| 65 | + } |
| 66 | + |
| 67 | + protected String doInBackground() { |
| 68 | + for(int i = 0; i < 100; i++) { |
| 69 | + if(isCancelled()){ |
| 70 | + break; |
| 71 | + } |
| 72 | + SystemClock.sleep(50); |
| 73 | + publishProgress(i); |
| 74 | + } |
| 75 | + return "Result"; |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +### 1.2 Extending from the TaskActivityCompat class |
| 81 | +The `TaskActivityCompat` class is the most easy way to use this library, make sure your Activity extends from it and it wil take care of retaining all `Tasks` started by the Activity's `TaskManager`. |
| 82 | + |
| 83 | +```java |
| 84 | +public class Main extends TaskActivityCompat { |
| 85 | + |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +>**Help, I already extend some custom Activity implementation!** |
| 90 | +>Don't worry, you can easily add the `TaskActivityCompat` behaviour to any Activity or Fragment by using the `TaskManagerLifeCycleProxy` class . Check out [this sample](#using-the-taskmanagerlifecycleproxy-to-mimic-the-taskactivity). |
| 91 | +
|
| 92 | +### 1.3 Execute the Task and receive callback |
| 93 | +Before you can execute the `ExampleTask` you first need to get the current Activity's `TaskManager`. A `TaskManager` keeps references to tasks and executes them. You can obtain the current Activity's `TaskManager` using the `TaskActivityCompat.getTaskManager()` method. |
| 94 | + |
| 95 | +Then you can execute the task using the `TaskManager.execute()` method. This method needs two arguments, the task to execute and a `Callback` listener to send feedback to. Preferably your activity implements the `Callback` interface, but this isn't necessarily needed. The `TaskManager` currently always executes tasks in parallel *(work in progress to make it a option)*. |
| 96 | + |
| 97 | + |
| 98 | +```java |
| 99 | +public class Main extends TaskActivityCompat implements Task.Callback { |
| 100 | + |
| 101 | + @Override |
| 102 | + public void onClick(View view){ |
| 103 | + ExampleTask task = new ExampleTask("activity-unique-tag"); |
| 104 | + getTaskManager().execute(task, this); |
| 105 | + } |
| 106 | + |
| 107 | + @Override |
| 108 | + public void onPreExecute(Task<?, ?> task) { |
| 109 | + //Task started |
| 110 | + } |
| 111 | + |
| 112 | + @Override |
| 113 | + public void onPostExecute(Task<?, ?> task) { |
| 114 | + //Task finished |
| 115 | + Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show(); |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | + |
| 121 | +>**Tip:** |
| 122 | +>You can also make your Fragment extend the `TaskFragmentCompat` class and use a Fragment to execute and retain your task in. It works exactly the same, but keep in mind that the `Callback` listeners are removed as soon as the Fragments stops (`onStop()`). |
| 123 | +
|
| 124 | + |
| 125 | +### 1.4 Retaining the task |
| 126 | +When the configuration changes (device rotates) the `TaskManager` will automatically remove the `Callback` listeners from all active tasks and retain all `Tasks`. Removing the `Callback` is needed otherwise the tasks could leak Activity, Fragment or other references. |
| 127 | + |
| 128 | +> **In-depth:** |
| 129 | +> The `TaskManger` (or actually a internal class) will detect when the Activity stops (`onStop()`). and will automatically remove all `Callback` listeners when this happens. At this moment the user-interface has become *"unstable"*, this means that some user-interface functionality stops working. For example, the `FragmentManager` refuses at this point to add new Fragments because the Activity's `onSaveInstanceState()` method already has been called. If the `Callback` listener is not removed by the `TaskManager` before this point, then a `DialogFragment.show()` call will throw an exception when called in the `onPostExecute()` method. This is why the `Callback` listeners are removed when the Activity stops. |
| 130 | +
|
| 131 | +Although tasks are automatically retained, you will still need to provide a new `Callback` listener for each `Task`. You can easily do this by implementing (overriding) the `TaskActivityCompat` (or `TaskFragmentCompat`) `onPreAttachTask(Task)` method and return a `Callback` instance. At this point you can also use the `onPreAttachTask(Task)` method to restore the user-interface state according to the `Tasks` state. The `onPreAttachTask(Task)` method will be called for each task that isn't finished (didn't deliver). |
| 132 | + |
| 133 | +```java |
| 134 | +public class Main extends TaskActivityCompat implements Task.Callback { |
| 135 | + |
| 136 | + @Override |
| 137 | + public Task.Callback onPreAttach(Task<?, ?> task) { |
| 138 | + //Restore the user-interface based on the tasks state |
| 139 | + return this; //This Activity implements Task.Callback |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | + |
| 145 | +## 2. How it works |
| 146 | +--- |
| 147 | +####**How are tasks retained?** |
| 148 | +Tasks are are stored in `FragmentManagers` which are stored in a *"no-ui-fragment"* this fragment retained across configuration changes and is added to your Activity's `FragmentManager` the first time you call: |
| 149 | + |
| 150 | + - `getTaskManager()` in the following classes: |
| 151 | + - `TaskActivity` & `TaskActivityCompat` |
| 152 | + - `TaskFragment` & `TaskFragmentCompat` |
| 153 | + - `TaskManagerLifeCycleProxy.getTaskManager()` |
| 154 | + - `TaskManager.getActivityTaskManager()` <sub>(super-advanced usage)</sub> |
| 155 | + - `TaskManager.getFragmentTaskManager()` <sub>(super-advanced usage)</sub> |
| 156 | + |
| 157 | +Essentially any time you request a `TaskManager`. |
| 158 | + |
| 159 | +The *"no-ui-fragment"* is from that point on bound to the Activity's life-cycle and keeps track of all `TaskManager` instances. It also makes sure that all internal `TaskManagers` remove all `Callback` listeners as soon as the Activity is stopping (`onStop()`). It might also throw an exception if a `Fragment` `TaskManger` did not remove the `Callback` listeners, so that you (the developer) know you've messed up. |
| 160 | + |
| 161 | +####**What happens when a Task without callback finishes?** |
| 162 | +When a Task doesn't have a `Callback` listener to deliver it's results to it will skip the delivery and redeliver the results as soon as a new listener is attached. This happens for example when the `onPreAttachTask(Task)` method returns, the newly provided `Callback` listener will be fired and you need to be sure that the user-interface is ready. It might also happen if you manually call one of the `TaskManager.attach()` methods (advanced usage). |
| 163 | + |
| 164 | +**Important:** Only the `onPostExecute()` and `onCanceled()` methods will be redelivered, other methodes won't be redelivered. You can restore a tasks progress using the `Task.getLastKnownProgress()` method. |
| 165 | + |
| 166 | +####**What does the Task and Callback life-cycle look like?** |
| 167 | +A `Task` basically has four life-cycle methods *(its heavily based on Android's AsyncTask)*: |
| 168 | + |
| 169 | +* `onPreExecute()` *[ui-thread]* |
| 170 | +* `doInBackground()` *[executor-thread]* |
| 171 | +* `onProgressUpdate()` *[ui-thread]* |
| 172 | +* `onPostExecute()` or `onCanceled()` *[ui-thread]* |
| 173 | + |
| 174 | +A `Callback` listener has the same life-cycle methods as the`Task`. All `Callback` methods are executed on the user interface thread. When a `Callback` listener is attached to the task, both the `Callback` and the `Task` methods will be called. But when the listener is detached from the task only the tasks methods will be called. With exception of the `onPostExecute()` and `onCanceled()` methods which can be redelivered. |
| 175 | + |
| 176 | + |
| 177 | +## 3. Advanced usage |
| 178 | +--- |
| 179 | +Besides the basics there are some more advanced API's you will probably need. |
| 180 | + |
| 181 | +####**Getting task results** |
| 182 | +Unlike the default Android `AsyncTask` implementation you don't get `Task` results as a parameter, instead you will need to call the `Task.getResult()` method, which returns the tasks result. |
| 183 | + |
| 184 | +####**Getting the tasks current state** |
| 185 | +The Android `AsyncTask` API provides the `AsyncTask.getStatus()` method which returns an enum value which can be used to determinate the tasks current state. Instead of using that method combined with an enum you can use on of the following methods: |
| 186 | + |
| 187 | +* `isFinished()` |
| 188 | +* `isRunning()` |
| 189 | +* `isReady()` |
| 190 | +* `isResultDelivered()` |
| 191 | +* `isCanceled()` |
| 192 | + |
| 193 | +####**Getting the tasks last progress update** |
| 194 | +To get the tasks most recent progress update use the `getLastKnownProgress()` method. |
| 195 | + |
| 196 | +####**AdvancedCallback** |
| 197 | +If you need the `onProgressUpdated` and `onCanceled` callback methods you can implement the `AdvancedCallback` interface, which is an extension of the `Callback` interface. |
| 198 | + |
| 199 | +####**TaskExecutor & Executor** |
| 200 | +You can also execute tasks without using a `TaskManager` this means that you are responsible for removing and setting the `Callback` listener. Executing tasks without using the `TaskManager` is handy when you don't perse need to get any feedback to the user-interface. |
| 201 | + |
| 202 | +```java |
| 203 | +TaskExecutor.executeParallel(new ExampleTask()); |
| 204 | +``` |
| 205 | +```java |
| 206 | +TaskExecutor.executeSerial(new ExampleTask()); |
| 207 | +``` |
| 208 | +```java |
| 209 | +//Alias for calling executeParallel |
| 210 | +TaskExecutor.execute(new ExampleTask()); |
| 211 | +``` |
| 212 | + |
| 213 | +You can also use a custom java `Executor` to execute tasks with: |
| 214 | + |
| 215 | +```java |
| 216 | +TaskExecutor.executeOnExecutor(new ExampleTask(), yourExecutor); |
| 217 | +``` |
| 218 | + |
| 219 | +####**Using the TaskManagerLifeCycleProxy to mimic the TaskActivity** |
| 220 | +If you already use some custom Activity or Fragment implementation you might not be able to use the `TaskActivity` or `TaskFragment` class. To overcome this problem you can implement the behaviour of the `TaskActivity` yourself using the `TaskManagerLifeCycleProxy` class. |
| 221 | + |
| 222 | +Create a new `TaskManagerLifeCycleProxy` instance and let your Activity (or Fragment) implement the `TaskManagerOwner` interface. Override the`onStart()` and `onStop()` methods and proxy those together with the `getTaskManager()` method to the `TaskManagerLifeCycleProxy` instance. |
| 223 | + |
| 224 | +```java |
| 225 | +public class MyBaseActivity extends SomeActivity implements TaskManagerOwner { |
| 226 | + |
| 227 | + private TaskManagerLifeCycleProxy proxy = new TaskManagerLifeCycleProxy(this); |
| 228 | + |
| 229 | + @Override |
| 230 | + protected void onStart() { |
| 231 | + super.onStart(); |
| 232 | + proxy.onStart(); |
| 233 | + } |
| 234 | + |
| 235 | + @Override |
| 236 | + protected void onStop() { |
| 237 | + proxy.onStop(); |
| 238 | + super.onStop(); |
| 239 | + } |
| 240 | + |
| 241 | + @Override |
| 242 | + public TaskManager getTaskManager() { |
| 243 | + return proxy.getTaskManager(); |
| 244 | + } |
| 245 | + |
| 246 | + @Override |
| 247 | + public Task.Callback onPreAttach(@NonNull Task<?, ?> task) { |
| 248 | + return null; |
| 249 | + } |
| 250 | +} |
| 251 | +``` |
| 252 | + |
| 253 | +## 4. FAQ |
| 254 | + |
| 255 | +####**Why does the Task class still have the onPostExecute and onPreExecute etc. methods?** |
| 256 | + |
| 257 | +Although the `Callback` interface provides these methods sometimes you don't need any callback to the Activity's user-interface, at that moment the Task methods come in handy. It also gives a `Task` the change to modify it's state or store it's progress values, for example: |
| 258 | +```java |
| 259 | +private class VerySimpleTask extends Task<Integer, Integer> { |
| 260 | + |
| 261 | + private final ArrayList<Integer> progressValues = new ArrayList<>(); |
| 262 | + |
| 263 | + public ExampleTask(String tag){ |
| 264 | + super(tag); |
| 265 | + } |
| 266 | + |
| 267 | + @Override |
| 268 | + protected Boolean doInBackground() { |
| 269 | + for(int i = 0; i < 10; i++){ |
| 270 | + publishProgress(i); |
| 271 | + SystemClock.sleep(500); |
| 272 | + } |
| 273 | + return 10; |
| 274 | + } |
| 275 | + |
| 276 | + @Override |
| 277 | + protected void onProgressUpdate(Integer value) { |
| 278 | + progressValues.add(value); |
| 279 | + } |
| 280 | + |
| 281 | + public List<Integer> getProgressCache(){ |
| 282 | + // This method is safe to call on the ui-thread because the |
| 283 | + // onProgressUpdate method is executed on the same thread. |
| 284 | + return progressValues; |
| 285 | + } |
| 286 | +} |
| 287 | +``` |
| 288 | + |
| 289 | +## 5. To-Do |
| 290 | + |
| 291 | +*“As long as I am breathing, in my eyes, I am just beginning.”* |
| 292 | + |
| 293 | + - Add custom Executors to the TaskManager; |
| 294 | + - Finish documentation; |
| 295 | + - Write real tests for the library besides having only a demo app; |
0 commit comments