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:
image
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:
image
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

Also popular now: