3

I am trying to load some data into a layout. I am loading the data via an AsyncTask, which will load data from an SQLite datasource then insert that data into the layout on completion. I'd like to show a progress bar during this time, by adding it to the layout when the task starts, and removing it from the layout when the task completes.

Here's a rough outline:

private class ItemLoader extends AsyncTask<Void, Void, List<Item>> {

    private Context context;
    private LayoutInflater inflater;
    private View loading;

    public DataLoader(Context context) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
        this.loading = createLoading();
    }

    private View createLoading() {
        return inflater.inflate(R.layout.loading, null);
    }

    @Override
    public void onPreExecute() {
        // Insert 'loading' view into the 'items' layout.
        LinearLayout layout = (LinearLayout) findViewById(R.id.items);
        layout.addView(loading);
    }

    @Override
    protected List<Item> doInBackground(Void... params) {
        return loadItemsFromDataSource();
    }

    private List<Item> loadItemsFromDataSource() {
        ItemsSource d = new ItemsDataSource(context);
        d.open();
        try {
            return d.lookupItems();
        } finally {
            d.close();
        }       
    }

    @Override
    public void onPostExecute(List<Item> items) {
        LinearLayout layout = (LinearLayout) findViewById(R.id.items);
        // Remove 'loading' view from the 'items' layout.
        layout.removeView(loading);
        // Add items to the 'items' layout.
        for (Item item: items) {
            addItemToLayout(item, layout);
        }
    }

My loading layout is copied from the Android API docs for ProgressBar:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@android:style/Widget.ProgressBar.Small"
        android:layout_marginRight="5dp" />
   <TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/loading" />

</LinearLayout>

The AsyncTask is called during the activity's onCreate() method:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.itemsLayout);
    ...
    new ItemLoader(this).execute();
}

I can see that the 'loading' spinner and text are added to my layout while the data is loaded in the background, and that this spinner/text are removed when the data is added to the layout.

But the spinner is not spinning when the doInBackground() method is running. I'm adding and removing the progress bar in the UI thread (in the onPreExecute() and onPostExecute() methods).

Interestingly, if a add a sleep() call in doInBackground() before the data is loaded from the data source, then I can see the spinner is spinning. But once the data source call is made, the spinner stops spinning:

@Override
protected void doInBackground(Void... params) {
    sleepForFiveSeconds(); // spinner is spinning
    List<Item> items = loadItemsFromDataSource(); // spinner stops spinning
    sleepForFiveSeconds(); // spinner still not spinning
    return items;
}

And it's not just a spinner manipulated within an AsyncTask. I notice the same behavior when a ProgressBar is added to the activity, either by defining it in the activity's layout or adding it dynamically in the activity's onCreate() method. In these cases, the spinner is spinning until the data source lookup is performed. It will start spinning again once the data source lookup returns.

So it seems that the spinners are accessed within the UI thread, but data source operations in a background thread are causing these spinners to stop.

Any suggestions?

Edited to add: this is with my Nexus 7 (Android 4.2) and HTC phone (Android 2.3.4), with the android manifest setting minimum SDK version to 10.

John Q Citizen
  • 321
  • 1
  • 6
  • 15
  • What does the source of `ItemsDataSource` and `lookupItems` look like? Is your activity doing any other database lookups? – twaddington Nov 26 '12 at 02:20

1 Answers1

0

Instead of adding and removing the spinner from the layout, try including it in the base itemsLayout file and just toggling the visibility. So, something like findViewById(R.id.loading).setVisibility(View.VISIBLE);. This should take less time than inflating a whole new layout, which is what you're doing now.

twaddington
  • 11,607
  • 5
  • 35
  • 46
  • Thanks for the reply. I added the loading layout directly to the activity's layout, and set the loading layout to VISIBLE during `onPreExecute` and GONE during `onPostExcecute`. The spinner appears and disappears correctly, but is still not spinning during data source operations. – John Q Citizen Nov 26 '12 at 02:11
  • How long is your background task taking? Have you tried timing it? It's possible it's just completing too fast for the spinner to start up effectively. You might also try setting the `android:indeterminate` attribute on the spinner to `true`. – twaddington Nov 26 '12 at 02:17
  • The data loading can range from almost instantaneous to quite a few seconds. Note that I also put a `sleep()` call into the `doInBackground()` method of the background task after the data loading had finished and still the spinner did not spin. I'll try explicitly setting `android:indetermite` to `true`, but a spinner is indeterminate by default according to the docs. – John Q Citizen Nov 26 '12 at 02:23
  • Yeah, it seems like the problem is not with your `AsyncTask` but your `loadItemsFromDataSource` code. It's possible you're running into any number of issues, including database contention. – twaddington Nov 26 '12 at 02:39
  • But the data loading is on a separate thread. Not sure why that would affect a widget on the UI thread? – John Q Citizen Nov 26 '12 at 02:57
  • Well, even though it's accessing the database on a background Thread, it could cause the main thread to wait, if the main thread also tries to access the database at the same time. This is called contention. So, if you have any other database operations, make sure they're also running on a background thread. Also, if your background task is grossly inefficient, it can cause the whole device to start to run slower. If you remove the call to `loadItemsFromDataSource` your progress indicator works correct? So that leads me to believe the problem is in your database code. – twaddington Nov 26 '12 at 04:25
  • I've just checked my code: all datasource operations are contained within an `AsyncTask` and called via `doInBackground()`. The datasource uses an `SqliteOpenHelper` and the `loadItems()` method on that datasource performs an open/fetch/close operation on the database. The fetch operation is a join of two tables (both tables are quite large). – John Q Citizen Nov 26 '12 at 05:24
  • This is likely a more complicated problem than it appears on the surface. Unfortunately, I don't have any more ideas for you at the moment. It might help others if you post more of your code. – twaddington Nov 26 '12 at 05:28
  • 1
    The data loading code opens the datasource, invokes an SQL operation on the datasource, then closes the datasource. I replaced the SQL operation with a `Thread.sleep(2000)` and the spinner functioned correctly. So something in the actual SQLite calls / cursor-to-object conversion is causing the issue. – John Q Citizen Nov 26 '12 at 07:01
  • 1
    I'm using an `ArrayAdapter` in the `ListView` that shows items fetched from the datastore. So the data store performs an SQL query and then iterates over the cursor to construct an array of objects. It's this operation (or at least the first such operation if breaking up the query into chunks) that chews up processor cycles and stops the progress bar spinner animation. I'll try using a `CursorAdapter` instead of an `ArrayAdapter` and see if there's any improvement. – John Q Citizen Nov 27 '12 at 00:43
  • Sounds like a great solution! A `CursorAdapter` should load your content in batches, which will make things a lot more efficient. – twaddington Nov 27 '12 at 01:39