Android - Background tasks across screen orientation (AsyncTaskLoaders)

     One of the annoying tasks for new developers is to deal with the use case where in the activity is destroyed and re created in response to a configuration change like changing orientation from portrait to landscape or vice versa, changing device language, adding a keyboard accessory etc. Handling background tasks in this scenario makes it difficult as most of the ASyncTask implementations hold an explicit reference to the activity or the fragment. This hard dependency could be solved by a broadcast mechanism but then there is a potential race where in broadcast receiver might not be registered as the new instance of the activity might be in the process of recreation and the background task could have fired of the intent to notify the results. This could be solved by using the application context instead of using the activity context to register a broadcast receiver. This would need more work like relaying this event to an appropriate activity and could be complex in an application having multiple activities.

   Android's SDK offers AsyncTaskLoader and LoaderManager to ease this pain for application developers. But how exactly did it solve the underlying problem that application developers couldn't solve all the while or found it difficult? These APIs depend on the framework's ability to detect this scenario where in the activity is relaunched in response to an configuration change.

    Each application process has a data structure to keep track of the activity info, ActivityClientRecord. As and when the configuration changes, the desired activity client record is fetched and processed. One of the important updates is to a variable mChangingConfigurations in the Activity. This is a package private variable and access is restricted to the framework classes. Note that this is set to true in the instance of the activity that is going to be destroyed. So why bother doing this on a instance that is to be destroyed?

        r.activity.mChangingConfigurations = true;

  This serves as hint for the activity to save its loaders. It invokes doRetain on the current LoaderManager, if any. From this point on the activity is paused, its state is saved, and is destroyed.

        performPauseActivity(r.token, false, r.isPreHoneycomb());
       mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
       handleDestroyActivity(r.token, false, configChanges, true);

   As the activity is destroyed, the non configuration instances are saved.

     r.lastNonConfigurationInstances  = r.activity.retainNonConfigurationInstances();

The retainNonConfigurationInstances() has the logic to save existing loader managers.

    NonConfigurationInstances retainNonConfigurationInstances() {
        Object activity = onRetainNonConfigurationInstance();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        ArrayList<Fragment> fragments = mFragments.retainNonConfig();
        boolean retainLoaders = false;
        if (mAllLoaderManagers != null) {
            // prune out any loader managers that were already stopped and so
            // have nothing useful to retain.
            final int N = mAllLoaderManagers.size();
            LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
            for (int i=N-1; i>=0; i--) {
                loaders[i] = mAllLoaderManagers.valueAt(i);
            }
            for (int i=0; i<N; i++) {
                LoaderManagerImpl lm = loaders[i];
                if (lm.mRetaining) {
                    retainLoaders = true;
                } else {
                    lm.doDestroy();
                    mAllLoaderManagers.remove(lm.mWho);
                }
            }
        }
        if (activity == null && children == null && fragments == null && !retainLoaders) {
            return null;
        }
        
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = mAllLoaderManagers;
        return nci;
    }

   This non configuration instance is passed on to the new activity instance and its mAllLoaderManagers is restored to tracks existing Loaders. Activities typical usage of LoaderManager is something like this and it registers for a callback as and when the loader task is finished.

     getLoaderManager().initLoader(0, null, this);

getLoaderManager() is defined in the base Activity class as,

    LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) {
        if (mAllLoaderManagers == null) {
            mAllLoaderManagers = new ArrayMap<String, LoaderManagerImpl>();
        }
        LoaderManagerImpl lm = mAllLoaderManagers.get(who);
        if (lm == null) {
            if (create) {
                lm = new LoaderManagerImpl(who, this, started);
                mAllLoaderManagers.put(who, lm);
            }
        } else {
            lm.updateActivity(this);
        }
        return lm;
    }

   The LoaderManager has a reference to the activity and the callback provided by the application developer. This callback could too be an activity if the developer choose the activity to implement LoaderManager.LoaderCallbacks. So if the loader manager has a reference to an old instance of the activity, how can it possibly be reused? What is the point of saving and restoring? Wouldn't holding a reference to the old activity cause a memory leak?

   This is handled by passing on the responsibility to the application developers by the expected usage of LoaderManager API. As and when the new activity is created and it is expected to initialize loader once again. This call to initLoader() in the new activity instance provides the old loader manager with reference to the new callback and the implementation of getLoaderManager() ensures that the reference of the new activity is set on old Loader Managers via lm.updateActivity(this). From this point on as and when the Loader is finished, it would invoke the desired callback. It also handles the potential race where in the ASyncLoaderTask might complete before the new activity instance initializes the loader again. This works because Loader is not just a task executor, it also manages the data, listens for changes in underlying source etc. This is how Loaders work across configuration changes.

   But the important take away is for applications developers to call getLoaderManager() and not try to book keep it in a singleton helper or in the Application class.
   

No comments: