Monthly Archives: January 2017

Android: Pros and Cons of Loaders

Loaders are weird. The idea seems simple, but just doesn’t seem to stick with you. Perfectly capable Android developers show no shame in saying they don’t understand Loaders. So I took some time to look into them (again) and write it down for future reference.

Simplest implementation is AsyncTaskLoader. Example in documentation is MSDN-style, meaning it’s comprehensive, but you won’t understand anything. So here’s the most basic implementation.

protected void onCreate(Bundle savedInstanceState) {
    getSupportLoaderManager().initLoader(0, null, callback);
}

private LoaderManager.LoaderCallbacks callback = 
        new LoaderManager.LoaderCallbacks() {
    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new MyLoader(SecondActivity.this);
    }

    @Override
    public void onLoadFinished(Loader loader, Integer data) { }

    @Override
    public void onLoaderReset(Loader loader) { }
};

private static class MyLoader extends AsyncTaskLoader {

    public MyLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    @Override
    public Integer loadInBackground() {
        return 10;
    }
}

In this example:

  1. Activity calls its LoaderManager to register loader with id=0 and given callback
  2. Callback then creates the loader when necessary
  3. Loader does loadInBackground(), supposedly doing an expensive operation

One confusion I have every time, is it’s necessary to call forceLoad() manually, or the Loader won’t do anything. This is apparently because there could be some tricky scenario where you already have data to return even before the loader ran for the first time. In most simple case, you just need to know that onStartLoading() is called at the right moment to start the background thing.

Here’s how it works.

D/test: Activity.onCreate()
I/test: initLoader()
I/test: onCreateLoader
I/test: MyLoader()
I/test: MyLoader.onStartLoading()
I/test: MyLoader.onForceLoad()
I/test: MyLoader.onCancelLoading()
I/test: MyLoader.loadInBackground()
I/test: onLoadFinished 10
(rotate)
D/test: Activity.onDestroy()
D/test: Activity.onCreate()
I/test: initLoader()
I/test: onLoadFinished 10
(return)
I/test: MyLoader.onStopLoading()
I/test: onLoaderReset
I/test: MyLoader.onReset()
D/test: Activity.onDestroy()

So this is very intuitive: Loader survived activity rotation, delivered same result to the recreated activity, and then reset itself when activity was about to destroy.

Now let’s make the background operation longer.

public Integer loadInBackground() {
    try { Thread.sleep(5000); } catch (Exception ex) {}
    return 10;
}

Results:

D/test: Activity.onCreate()
I/test: onCreateLoader
I/test: MyLoader.loadInBackground() started
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: onLoadFinished 10
(return)
I/test: onLoaderReset
D/test: Activity.onDestroy()

When Loader has time to load everything, it just delivered result normally and then died with activity.

D/test: Activity.onCreate()
I/test: onCreateLoader
I/test: MyLoader.loadInBackground() started
(rotate)
D/test: Activity.onDestroy()
D/test: Activity.onCreate()
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: onLoadFinished 10

Activity was recreated while Loader was working. It just continued and then delivered the result to the new activity.

D/test: Activity.onCreate()
I/test: onCreateLoader
I/test: MyLoader.loadInBackground() started
(return)
D/test: Activity.onDestroy()
I/test: MyLoader.loadInBackground() finished, isReset=true

Activity was closed while Loader was working. It didn’t stop magically (because we didn’t check any flag anywhere), but on completion it didn’t deliver any events and apparenly just died silently.

One more interesting test is to deliver “content changed” event and see how Loader does.

public MyLoader(Context context) {
    super(context);
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            MyLoader.this.onContentChanged();
        }
    }, 7000);
}

public Integer loadInBackground() {
    try { Thread.sleep(5000); } catch (Exception ex) {}
    return new Random().nextInt();
}

Results:

D/test: Activity.onCreate()
I/test: onCreateLoader
I/test: MyLoader.loadInBackground() started
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: onLoadFinished 1303887722
D/test: Handler.run()
I/test: MyLoader.loadInBackground() started
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: onLoadFinished 1662614671

So without anything going on in the Activity, Loader received “onContentChanged”, restarted its background thread, and delivered new result. Now let’s deliver this event while first background thread is still running.

D/test: Activity.onCreate()
I/test: onCreateLoader
I/test: MyLoader.loadInBackground() started
D/test: Handler.run() isReset=false isAbandoned=false
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: MyLoader.loadInBackground() started
I/test: MyLoader.loadInBackground() finished, isReset=false
I/test: onLoadFinished 474091183

Apparently Loader understands that content has changed while it was still running, and instead of delivering first result, it just re-runs the thread and just delivers the second one.

Also, you may have noticed that I’ve changed “10” to random number. This is because when Loader sees “10” in the result every time, it doesn’t deliver it. Apparently it thinks that Activity doesn’t need the result unless it’s changed.

One more important thing I found in testing, is Loader still contains a strong reference to Activity. So Activity can’t be dropped while background thread is running.

Conclusion:

You might remember how at first recreating activity on rotation was presented as The Shit. And then everyone started hatin on it, because it made it unreasonably difficult to manage data. So AsyncTaskLoader, in its simplest form, is essentially a hack over that. It allows us to manage background requests as if rotated activity never recreated and was the same object. In a perfect world, it would just be this way to begin with, and then we wouldn’t need it.

Suppose you have these conditions:

  •  you just need to load data once (like from DB)
  • reacting to someone else changing the data is not necessary
  • your activity does not recreate on rotation
  • or you have a fragment with retainInstance

Here, you don’t need anything other than a single thread started onCreate. Loader won’t give you anything on top of a simple AsyncTask. It just provides some small gimmicks, which are pretty easy to recreate with Activity.isDestroyed(). Remember that Loaders are hard, people don’t understand them, and you want to keep things simple.

On the other hand, in following situations you might want Loader:

  • you have classic rotating/reloading activity
  • background operation is long, data may change in the process, and you want to react correctly on change events

That’s where you actually have some not so straightforward cases of managing threads and results, and you don’t want spend time and rewrite what Loader already provides.

CursorLoader

I can’t just close this topic without due hating on CursorLoader. I am very much against the very concept. It seems like a natural evolution of a regular Loader, and almost the first intended use case for it. However, it does stuff which is very, very harmful for the code.

What CursorLoader does, is it gives UI classes direct access to a DB. It kinda tries to obfuscate that by running data through a ContentProvider first, but in the end it’s still the same DB access methods. End results of that:

  • Activities now know how your DB looks. You can’t ever change anything about your DB structure, because it would require changes in 50 Activities and related helper classes.
  • You have an intermediate ContentProvider class. It does essentially nothing but mapping CursorLoader logic to DB. IF YOU LUCKY. I’ve had cases for example where ContentProvider didn’t implement “delete” (because it was never needed), and I spent half a day debugging DB and trying to understand why a perfectly normal operation doesn’t work. And it’s a relatively simple example, it’s possible to break ContentProvider in a way you wouldn’t believe.
  • Despite of what you might expect, using CursorLoader doesn’t mean you magically get memory benefits. What Cursor does, is it first loads the entire data set into memory, and then just gives you a way to conveniently iterate over it.

At best, CursorLoader is a low-level optimization for low-powered devices. Initial idea was that you just read numbers straight from a DB row and put them directly into UI. Forget all that MVP crap, we’re old-school. This nice feature comes at a cost of a huge added complexity, and basically making sure only advanced developers will be able to work with your code. Because have you ever tried to work with a huge chunk of code which doesn’t even have any business objects and just passes god damn Cursors everywhere?

Instead, I usually create a “data manager” class, which completely isolates any access to DB. If you want any data from DB, you go through that class. It loads the same dataset CursorLoader would, but then converts it all into business objects and closes the cursor (releasing the underlying data). Am I rewriting ContentProvider? Yes. Yes I am. But let’s compare them.

ContentProvider:

  • Low level data model, presents data as tables of primitive values.
  • Users use Cursors to navigate the data. If they’re good, they then convert rows to business objects themselves. If they’re not, they either use primitive values straight from a Cursor, or worse pass Cursor somewhere else, completely ruining its lifecycle.
  • Returns just the rows user requested.
  • Built-in functions for changing and observing data, documented in SDK, require everyone to use this ContentProvider to access this DB.

Custom data manager:

  • Object level data model, presents data as collections of business objects.
  • Users receive List<BusinessObject>, which is usually end result of what they wanted.
  • In simplest implementation, returns the entire dataset for user to navigate.
  • Custom functions for changing and observing data, discoverable through code, require everyone to use this manager to access this DB.

So here I lose some on performance, but gain in presenting data to the outside users in a way which people will actually be able to understand. This approach is unfortunately completely incompatible with CursorLoaders, which makes me sad when I have to have discussions with people who like them.