Random An Android blog

17Apr/142

AsyncTask is bad and you should feel bad

There are a lot of mixed feelings about AsyncTask. All this stems from the fact that developers new to Android jump to AsyncTask when they need to do something on a thread, without fully understanding the Android lifecycle and how to properly handle it. This often leads to problems with leaked activities, state not updating, and a variety of other issues.
Because of this you'll likely get told from experienced developers that you should not be using AsyncTask.

In this article I'll give an example of how you can use AsyncTask and avoid these issues.

I know that the general consensus is that AsyncTask isn't meant for long-running tasks, but sometimes you just want a simple way to perform a simple task (no matter how long it might take). In my opinion, AsyncTask is perfect for this.

You're doing it wrong
A common way for new developers to use AsyncTask could be something like the following:

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  // Somewhere the AsyncTask is started

  public class MyAsyncTask extends AsyncTask<Void, Void, String> {

    @Override protected String doInBackground(Void... params) {
      // Do work
      return result;
    }

    @Override protected void onPostExecute(String result) {
      Log.d("MyAsyncTask", "Received result: " + result);
    }
  }
}

The problem with this is that the AsyncTask has an implicit reference to the enclosing Activity. If a configuration change happens the Activity instance that started the AsyncTask would be destroyed, but not GCd until the AsyncTask finishes. Since Activities are heavy this could lead to memory issues if several AsyncTasks are started. Another issue is that the result of the AsyncTask could be lost, if it's intended to act on the state of the Activity.

This leads to two issues we have to fix:
- Ensuring the Activity isn't kept in memory when destroyed by the framework
- Ensuring the result of the AsyncTask is delivered to the current Activity instance

There's some examples out there that uses Activity#onRetainNonConfigurationInstance (https://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance()) to pass the AsyncTask instance between Activities on configuration changes, and then set a callback field on the AsyncTask with the current Activity instance. I'm not particularly fond of that approach since then you have to keep track of your AsyncTasks to make sure they're all passed properly to the new instance. In a Fragment you'd use Fragment#setRetainInstance (https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)) to achieve the same, but you don't necessarily want to do that.

Otto to the rescue
My favorite approach is to use an event bus - I prefer Otto (http://square.github.io/otto/).

Basically what we're going to do is
- Move the AsyncTask out into its own class file
- Define an event that's posted once the AsyncTask completes
- Subscribe to said event in our Activity

Using Otto is quite simple. We have to use it as a singleton, so create a small helper class for this

public class MyBus {

  private static final Bus BUS = new Bus();

  public static Bus getInstance() {
    return BUS;
  }
}

For this example the result will just be a String, so the event we post is as simple as this

public class AsyncTaskResultEvent {

  private String result;

  public AsyncTaskResultEvent(String result) {
    this.result = result;
  }

  public String getResult() {
    return result;
  }
}

Then we have to create an AsyncTask that does some work, then posts the result to the event bus. In this case it'll sleep for a random amount of time.

public class MyAsyncTask extends AsyncTask<Void, Void, String> {

  @Override protected String doInBackground(Void... params) {
    Random random = new Random();
    final long sleep = random.nextInt(10);
    try {
      Thread.sleep(sleep * 1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "Slept for " + sleep + " seconds";
  }

  @Override protected void onPostExecute(String result) {
    MyBus.getInstance().post(new AsyncTaskResultEvent(result));
  }
}

Our activity layout will just be a button the starts an AsyncTask

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

  <Button
      android:id="@+id/button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Start AsyncTask"/>
</LinearLayout>

And finally we have our Activity that starts the AsyncTask and later receives the result. It's important to remember to unregister the Activity once it's destroyed, otherwise it'll be leaked as well.

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        new MyAsyncTask().execute();
      }
    });

    MyBus.getInstance().register(this);
  }

  @Override protected void onDestroy() {
    MyBus.getInstance().unregister(this);
    super.onDestroy();
  }

  @Subscribe public void onAsyncTaskResult(AsyncTaskResultEvent event) {
    Toast.makeText(this, event.getResult(), Toast.LENGTH_LONG).show();
  }
}

And that's it. The AsyncTask has no hard reference to the Activity, so there's no memory leaking. If a configuration change happens we'll automatically be notified of the result in the new Activity instance since it's registered to the event bus.

Now, you might ask: What happens if the AsyncTask finishes between onDestroy of on Activity and onCreate of the next?
While doInBackground can certainly finish at this time, onPostExecute is called by posting a message to the main thread Looper (or message queue). Since onDestroy and onCreate happens within the same message, the onPostExecute message can only be handled after both has happened - solving the issue.

Closing words
AsyncTask is not the only place where an event bus can come in handy. If you use services, it can be a hassle to get results back to the activity. An event bus solves this issue as well.

Filed under: Android 2 Comments
7Feb/125

What API level should I target?

There's a lot of confusion in this area. A common misconception is that if you want your app to run on everything from 2.1 to 4.0, you have to target 2.1. That's wrong.

This post will cover most of what you need to know to get started on making apps that not only supports an old API level, but also take advantage of what the new API level have to offer.

AndroidManifest.xml
You're most likely familiar with the <uses-sdk /> tag in your manifest. But that understanding does often not go further than the android:minSdkVersion attribute. First, a brief explanation of the attributes supported by uses-sdk.

android:minSdkVersion
Like the name suggests, defines the minimum API level your application works on. This is used by Play Store to detect if the application is compatible with the users device. So, if your minSdkVersion is 7, Play Storewill show the application to devices from API level 7 (2.1) and up. This attribute should be set by all apps!

android:maxSdkVersion
This defines the maximum API level your application works on. Again, Play Store uses this to detect if an application is compatible with a device. Please note, that there in most cases is no reason to use this! it is perfectly possible to create a single APK which works on every API level from your minSdkVersion and up!

android:targetSdkVersion
Unlike the other two attributes, this is used by the Android platform itself. This attribute should be set to the maximum API level you tested your application on. If not set, this will default to your minSdkVersion.

Not many new users know about this attribute, but it's rather important.

As the Android platform evolves, features are added and even removed. To remain backwards compatible, the system needs to apply certain compatibility behaviors. A recent example of this is the menu button. With ICS, this was removed. If the system was not able to apply compatibility behaviors, this would render every app built before ICS useless, since there was no way of getting to settings, or whatever feature is available in the menu. Instead, it can look at the targetSdkVersion and see that the app is tested against e.g. 2.3, in which case it needs to offer an alternative way on ICS devices for accessing the menu. On the Galaxy Nexus, it does this by drawing a menu button next to the regular onscreen buttons.

Compatibility is also applied when it comes to themes. HoneyComb introduced a new set of themes called "Holo". If these were simply applied to older apps (in case these apps did not manually define a theme in the manifest), it could break their functionality, e.g. draw black text on a black background. Instead, it checks the targetSdkVersion and decides which theme to apply.

Yet another example, and a very good reason to set this attribute, is the move to sd functionality. This was added in API level 8 (2.2). If your android:minSdkVersion is set to API level 7, and android:targetSdkVersion is not defined, devices which run API level 8 and up would not be able to use this function.

Setting android:targetSdkVersion higher than android:minSdkVersion does not break the app on your minimum API level. It will still work perfectly fine.

Build target
Build target is the API level Eclipse/IntelliJ/whatever IDE you're using is building against. This is simply used by the IDE/build system to know which APIs to offer you. If you build against API level 14, the application will still be able to run on API level 7, providing you don't call any APIs that are not available on API level 7.

I mostly set the build target to the same as android:targetSdkVersion, though this is not required.

How to take advantage of new APIs, and still remain compatible with older API levels
Any modern app will most certainly want to do this. Technically, there's no reason to limit yourself just to the APIs available in your android:minSdkVersion.

As a simple example, let's take a look at the Editor class. In API level 9, a new method named apply() was introduced. The old commit() method synchronously wrote the changes to the internal storage before returning, which could possibly take a while. apply() will commit its changes to memory, and asynchronously write the changes to the internal stroage. This will speed it up a bit.

When deciding which of the two methods to call, we have to check the API level of the device. Luckily, Android offers a simple way to do this. The Build class has everything we need.

First there's Build.VERSION.SDK_INT. This is simply an integer indicating the devices API level. If you're running your app on API level 7, this will be 7.

Secondly, there's Build.VERSION_CODES, which is just the API levels as ints. This makes it more readable.

What we'll do is save the value "lol" to the key "cat".

// Get our preferences
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
// Get an Editor object
Editor editor = settings.edit();
// Add our new value
editor.putString("cat", "lol");

// Check if we're running on GingerBread or above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
    // If so, call apply()
    editor.apply();
// if not
} else {
    // Call commit()
    editor.commit();
}

What happens is, we check the current SDK_INT against the INT for Gingerbread, which is when the apply() method was introduced. If the SDK_INT is 9 (the value for Build.VERSION_CODES.GINGERBREAD) or above, call apply(). If not, call commit(). Even though Build.VERSION_CODES.GINGERBREAD was not added until API9, this will not crash on API7. This is because the value is static final, and will be inlined when you build your application.

It's that simple.

This approach only works for Android 2.1 (API level 7) and above. If you support 1.6 (API level 4), the editor.apply() call will have to be wrapped in a static class. These days, very few apps have to support that old versions of Android, so I will not provide an example for this.

But wait! What if I forget to wrap a function call?

Well, first off, ADTr17 brought a new check that warns you if you are using APIs that were made available in a later version than your minSdkVersion. This should make it fairly obvious when errors occour.

Secondly, testing! If your android:minSdkVersion is 7, make sure to actually test it on API level 7. Likewise, if your android:targetSdkVersion is 14, make sure to actually test it on API level 14. Test on tablets, test on phones, test on as many configurations as you can.

It might seem like a hassle that there are so many versions of Android, and so many device configurations. But if done right, it should be pretty trivial.

TL;DR

  • Set android:minSdkVersion to the minimum API level you support.
  • Set android:targetSdkVersion to the highest API level you tested your app on.
  • Do not use android:maxSdkVersion.
  • Set your build target to whatever (I recommend the same as your android:targetSdkVersion), it does not affect the final APK.
  • Wrap calls to new APIs in an if where you check the API level.
  • Test your apps!

Additional reading
Google posted a great article on how to use the Holo themes on HoneyComb and above, while staying compatible with pre-HoneyComb devices. You can find it here: Holo Everywhere

Filed under: Android 5 Comments