
Effective client-server interaction in Android
StrictMode and the fight against ANR
Starting with the version of Gingerbread, Google has added a mechanism to Android that allows you to track long-term operations performed in the UI thread. The name of this mechanism is StrictMode. Why was this done?
Each of you has probably come across an Application Not Responding dialog box or ANR in applications. This happens when the application does not respond to input events (keystrokes, touch on the screen) for 5 seconds. To monitor processes blocking the UI thread, the StrictMode mechanism was introduced. And, starting with Android 3.0, StrictMode suppresses attempts to fulfill a network request in a UI thread.
So, what kind of mechanisms does Android provide for accessing the network outside the UI thread?
AsyncTask
Probably one of the most common mechanisms that help you perform network operations is AsyncTask. Consider an example:

At first glance, everything is fine, but there are several disadvantages:
- When you change the orientation of the device, the network thread (Worker Thread) will lose the Activity context within which it was launched. And upon receiving the result - IllegalStateException. Usually, to solve this problem, cancel the network stream in the Activity.onStop method (as an option in onPause) and run it again in Activity.onStart (onResume). But with this approach, the user can simply not wait for the result, not to mention the increase in traffic.
- The need to save the query result between configuration changes.
- If the application is in the background, the system can complete it along with all its threads.
Starting from 3.0, we come to the rescue mechanism that allows us to solve most of the problems described above. But what if the platform requirements are lower than 3.0? Do not despair and use the support-library to which this mechanism has been backported.
Loaders
Why is this mechanism so good:
- Available from Activity and Fragment
- Monitors the current state of the application (visible to the user, located in the background, etc.)
- Provides the ability to asynchronously load data
- Controls the data source
- Automatically returns the result of the last query when changing the configuration. So no need to re-request
Let's look at how you can use Loaders to request data over the network:

How does it work? Inside the Loader, a network stream starts, the result is parsed and sent to the ContentProvider, which saves them in a DataStorage (this can be memory, file system, sqlite database) and notifies Loader that the data has been changed. Loader, in turn, polls the ContentProvider for new data and returns it to Activity (Fragment). If we replace the network stream with a service, we can guarantee that the user will receive data even if the application has been minimized (since Service has a higher priority than background process).
What is the advantage of this approach:
- Ability to implement query caching
- Transparent network layer
- The ability to easily exclude the network layer and get an offline application.
- Regardless of the format of the returned data , the data that needs to be visualized is important to us directly .
- One entry point for accessing data
Implementation example
public abstract class AbstractRestLoader extends Loader {
private static final int CORE_POOL_SIZE = Android.getCpuNumCores() * 4;
private static final int MAX_POOL_SIZE = CORE_POOL_SIZE * 4;
private static final int POOL_KEEP_ALIVE = 1;
private static final BlockingQueue sPoolWorkQueue;
private static final ThreadFactory sThreadFactory;
private static final ExecutorService sThreadPoolExecutor;
private static final AsyncHttpClient sDefaultHttpClient;
private static final Handler sUiHandler;
static {
sPoolWorkQueue = new LinkedBlockingQueue(CORE_POOL_SIZE * 2);
sThreadFactory = new LoaderThreadFactory();
sThreadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE,
POOL_KEEP_ALIVE, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory
);
sDefaultHttpClient = new AsyncHttpClient();
sUiHandler = new Handler(Looper.getMainLooper());
}
private final AsyncHttpClient mHttpClient;
private final HttpMethod mRestMethod;
private final Uri mContentUri;
private final ContentObserver mObserver;
private String[] mProjection;
private String mWhere;
private String[] mWhereArgs;
private String mSortOrder;
private boolean mLoadBeforeRequest;
private FutureTask mLoaderTask;
private AsyncHttpRequest mRequest;
private Cursor mCursor;
private boolean mContentChanged;
public AbstractRestLoader(Context context, HttpMethod request, Uri contentUri) {
super(context);
mHttpClient = onInitHttpClient();
mRestMethod = request;
mContentUri = contentUri;
mObserver = new CursorObserver(sUiHandler);
}
public Uri getContentUri() {
return mContentUri;
}
public AbstractRestLoader setProjection(String[] projection) {
mProjection = projection;
return this;
}
public AbstractRestLoader setWhere(String where, String[] whereArgs) {
mWhere = where;
mWhereArgs = whereArgs;
return this;
}
public AbstractRestLoader setSortOrder(String sortOrder) {
mSortOrder = sortOrder;
return this;
}
public AbstractRestLoader setLoadBeforeRequest(boolean load) {
mLoadBeforeRequest = load;
return this;
}
@Override
public void deliverResult(Cursor cursor) {
final Cursor oldCursor = mCursor;
mCursor = cursor;
if (mCursor != null) {
mCursor.registerContentObserver(mObserver);
}
if (isStarted()) {
super.deliverResult(cursor);
mContentChanged = false;
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.unregisterContentObserver(mObserver);
oldCursor.close();
}
}
@Override
protected void onStartLoading() {
if (mCursor == null || mContentChanged) {
forceLoad();
} else {
deliverResult(mCursor);
}
}
@Override
protected void onForceLoad() {
cancelLoadInternal();
if (mLoadBeforeRequest) {
reloadCursorInternal();
}
restartRequestInternal();
}
@Override
protected void onReset() {
cancelLoadInternal();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
protected AsyncHttpClient onInitHttpClient() {
return sDefaultHttpClient;
}
protected void onCancelLoad() {
}
protected void onException(Exception e) {
}
protected void deliverResultBackground(final Cursor cursor) {
sUiHandler.post(new Runnable() {
@Override
public void run() {
deliverResult(cursor);
}
});
}
protected void deliverExceptionBackground(final Exception e) {
sUiHandler.post(new Runnable() {
@Override
public void run() {
onException(e);
}
});
}
protected abstract void onParseInBackground(HttpHead head, InputStream is);
protected Cursor onLoadInBackground(Uri contentUri, String[] projection, String where, String[] whereArgs,
String sortOrder) {
return getContext().getContentResolver().query(contentUri, projection, where, whereArgs, sortOrder);
}
private void reloadCursorInternal() {
if (mLoaderTask != null) {
mLoaderTask.cancel(true);
}
mLoaderTask = new FutureTask(new Callable() {
@Override
public Void call() throws Exception {
deliverResultBackground(onLoadInBackground(mContentUri, mProjection, mWhere, mWhereArgs, mSortOrder));
return null;
}
});
sThreadPoolExecutor.execute(mLoaderTask);
}
private void restartRequestInternal() {
if (mRequest != null) {
mRequest.cancel();
}
mRequest = mHttpClient.execute(mRestMethod, new AsyncHttpCallback() {
@Override
public void onSuccess(HttpHead head, InputStream is) {
onParseInBackground(head, is);
}
@Override
public void onException(URI uri, Exception e) {
deliverExceptionBackground(e);
}
});
}
private void cancelLoadInternal() {
onCancelLoad();
if (mLoaderTask != null) {
mLoaderTask.cancel(true);
mLoaderTask = null;
}
if (mRequest != null) {
mRequest.cancel();
mRequest = null;
}
}
private static final class LoaderThreadFactory implements ThreadFactory {
private final AtomicLong mId = new AtomicLong(1);
@Override
public Thread newThread(Runnable r) {
final Thread thread = new Thread(r);
thread.setName("LoaderThread #" + mId.getAndIncrement());
return thread;
}
}
private final class CursorObserver extends ContentObserver {
public CursorObserver(Handler handler) {
super(handler);
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (isStarted()) {
reloadCursorInternal();
} else {
mContentChanged = true;
}
}
}
}
Usage example
public class MessageActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks {
private ListView mListView;
private CursorAdapter mListAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.ac_message_list);
mListView = (ListView) findViewById(android.R.id.list);
mListAdapter = new CursorAdapterImpl(getApplicationContext());
getSupportLoaderManager().initLoader(R.id.message_loader, null, this);
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
return new AbstractRestLoader(getApplicationContext(), new HttpGet("API URL"), null) {
@Override
protected void onParseInBackground(HttpHead head, InputStream is) {
try {
getContext().getContentResolver().insert(
Messages.BASE_URI,
new MessageParser().parse(IOUtils.toString(is))
);
} catch (IOException e) {
deliverExceptionBackground(e);
}
}
@Override
protected void onException(Exception e) {
Logger.error(e);
}
}.setLoadBeforeRequest(true);
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
mListAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader loader) {
mListAdapter.swapCursor(null);
}
}
PS
Developing Android REST client applications
Android Developers - Loaders
Android Developers - Processes and Threads