“Mobile is eating the world,” but few developers realize that mobile software is written very differently from desktop software. This leads to lots of mobile apps that simply don’t work well, suck up battery power, or can’t recover from being put into the background. I’ll discuss a few such apps on the Android platform, and explain how they should have been written to improve user experience, illustrating general mobile development principles by example.
3. Hint: If you know Android well, put
“Android” on your resume.
4. Key Differences
• Heterogeneous device capabilities
• Limited system resources
• Background tasks periodically killed
• Apps “stack” Activities
• Events handled by the OS
• Background processing common
• Blocking in UI thread yields an ANR
• Homogeneous virtual machine
• Lots of CPU and memory
• Lots of battery power
• Apps run in a dispatch loop
• Events handled within the app
• Background processing unusual
• Blocking in UI events semi-OK.
5. What happens when you get it wrong?
• Weird behavior when launching app
• App doesn’t function when phone sleeps
• Battery life and network issues
• Now you can’t access locks.
6. Common Pitfalls: Always Foreground
Problem: Android can kill background apps at any time to free resources.
Naïve solution #1: Ignore this.
“Why did I stop receiving
{mail, notifications, cat pictures} from your app?”
“It was supposed to alert me when ____, but didn’t”
“My music stopped playing in the middle of a song”
xkcd.com/937
7. When you know you’re doing it wrong
But don’t want to fix it
Can’t close this, ever… “…Or you’ll die in a car crash” So it kills your battery instead
8. The Right Way
Android apps can be killed any time they’re in the background. Accept this.
“Two hands clap and there is a sound.
What is the sound of one hand?”
9. The Right Way
Android will start your app again and restore its state when…
1. “Something interesting happens”, or
2. The user launches it again from history
onRestoreInstanceState()The catch: you have to write
The catch: you have to tell it what’s interesting!
10. Expressing Interest With Intents
PendingIntent
Intent
Intents will start your activity up again if it’s down.
PendingIntents tell Android to send them when certain things happen.
11. The Bad Way
package example;
import android.app.IntentService;
import android.content.Intent;
import java.util.concurrent.TimeUnit;
/**
* Prints out a message after 5 minutes - how not to do it.
*/
public class BadService extends IntentService {
public BadService() {
super("BadService");
}
@Override protected void onHandleIntent(Intent intent) {
try {
Thread.sleep(TimeUnit.MINUTES.toMillis(5));
} catch (InterruptedException e) {
// Because this is really bad code, we also ignore interrupts.
}
System.out.println("Your eggs are ready!");
}
}
12. The Right Way: Use Intents to Wake Your App
package example;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import java.util.Calendar;
/**
* Prints out a message after 5 minutes.
*/
public class GoodService extends IntentService {
public GoodService() {
super("GoodService");
}
public static void schedule(Context sender) {
AlarmManager alarmManager = (AlarmManager) sender.getSystemService(Context.ALARM_SERVICE);
Calendar alarmTime = Calendar.getInstance();
alarmTime.add(Calendar.MINUTE, 5);
Intent startGoodService = new Intent(sender, GoodService.class);
alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTime.getTimeInMillis(),
PendingIntent.getService(sender, 0, startGoodService, 0));
}
@Override protected void onHandleIntent(Intent intent) {
System.out.println("Your eggs are ready!");
}
}
13. Common Pitfalls: Work on the UI Thread
Draw! Draw!
I have to find this file first!
The UI thread is busy responding to the user. Never distract it.
Jeff Miracola, Wizards of the Coast – “Frantic Search”
14. The Bad Waypackage example;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import java.io.InputStream;
import java.net.URL;
/**
* Displays a cat picture to the user, but crashes with a NetworkOnMainThreadException first.
*/
public class CatPictureActivity extends Activity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
try {
drawCatPics();
} catch (Exception e) {
Log.e(getClass().getName(), "I haz an exception :(", e);
finish();
}
}
private void drawCatPics() throws Exception {
URL catApi = new URL("http://thecatapi.com/api/images/get");
InputStream catStream = (InputStream) catApi.getContent();
findViewById(R.id.cat_pics).setImageDrawable(Drawable.createFromStream(catStream, "Cats"));
}
}
15. The Right Way: AsyncTasks / Threads
package example;
import …;
/**
* Displays a cat picture to the user when the download finishes.
*/
public class CatPictureActivity extends Activity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
final ListenableFutureTask<Drawable> catDownloader = ListenableFutureTask.create(new CatCall());
catDownloader.addListener(new Runnable() {
@Override public void run() {
try {
findViewById(R.id.cat_pics).setImageDrawable(catDownloader.get());
} catch (Exception e) {
Log.e(getClass().getName(), "I haz an exception :(", e);
finish();
}
}
}, Executors.newSingleThreadExecutor());
}
private class CatCall implements Callable<Drawable> {
@Override public Drawable call() throws Exception {
URL catApi = new URL("http://thecatapi.com/api/images/get");
InputStream catStream = (InputStream) catApi.getContent();
return Drawable.createFromStream(catStream, "Cats");
}
}
}
16. Common Pitfalls: Assuming Network Reliability
package example;
import …;
/**
* Displays a cat picture to the user when the download finishes.
*/
public class CatPictureActivity extends Activity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
final ListenableFutureTask<Drawable> catDownloader = ListenableFutureTask.create(new CatCall());
catDownloader.addListener(new Runnable() {
@Override public void run() {
try {
findViewById(R.id.cat_pics).setImageDrawable(catDownloader.get());
} catch (Exception e) {
Log.e(getClass().getName(), "I haz an exception :(", e);
finish();
}
}
}, Executors.newSingleThreadExecutor());
}
private class CatCall implements Callable<Drawable> {
@Override public Drawable call() throws Exception {
URL catApi = new URL("http://thecatapi.com/api/images/get");
InputStream catStream = (InputStream) catApi.getContent();
return Drawable.createFromStream(catStream, "Cats");
}
}
}
What if Wifi is down?
No cat pics :(
17. The Right Way: Network State Intents
android.net.ConnectivityManager.CONNECTIVITY_ACTION
Android broadcasts an intent called
When the network state changes
and then you can check
ConnectivityManager cm =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
// Cat pics, here we come!
}
18. The Right Way
public class CatPictureActivity extends Activity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
registerReceiver(new NetReceiver(), new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
private void setupTask() {
final ListenableFutureTask<Drawable> catDownloader = ListenableFutureTask.create(new CatCall());
catDownloader.addListener(new Runnable() {
@Override public void run() {
try {
findViewById(R.id.cat_pics).setImageDrawable(catDownloader.get());
} catch (Exception e) {
Log.e(getClass().getName(), "I haz an exception :(", e);
finish();
}
}
}, Executors.newSingleThreadExecutor());
}
private class NetReceiver extends BroadcastReceiver {
@Override public void onReceive(Context context, Intent intent) {
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
unregisterReceiver(this); // Prevents a second run if the network goes down.
setupTask();
}
}
}
private class CatCall implements Callable<Drawable> {
@Override public Drawable call() throws Exception {
URL catApi = new URL("http://thecatapi.com/api/images/get");
InputStream catStream = (InputStream) catApi.getContent();
return Drawable.createFromStream(catStream, "Cats");
}
}
}