1

Here's what I've been doing all this while to get data from my database - my fragment will call a dbhelper which will run the query and pass the resulting data in an object.

I should have used a loader so that the querying is not done in the UI thread, but previously I didn't bother since my app's database is very small. Moving forward, this was a bad idea since my app has grown larger and so do the database. So now I'm reading all I can about Loaders including CursorLoader and custom loaders, but I still have problem implementing it in my app.

So here's my current code (simplified, just to show the relevant parts):

public class CategoryEdit extends Fragment{

private DbControl dbc;
private ObjectCategory objCategory;
private int categoryID = 3;
private EditText etName;

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        dbc = new DbControl(getActivity());
        try{
            dbc.open();
            objCategory = new ObjectCategory();
            objCategory = dbc.getCategoryData(categoryID);
            dbc.close();
        }catch (SQLException e){
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }

        etName.setText(objCategory.name);
    }
}

public class DbControl {

    public static final String FILE_NAME = "DbControl.java";
    private SQLiteDatabase database;
    private MySQLiteHelper dbHelper;

    public DbControl(Context context) {
        dbHelper = new MySQLiteHelper(context);
    }

    public void open() throws SQLException {
        database = dbHelper.getWritableDatabase();
    }

    public void close() {
        dbHelper.close();
    }

    public ObjectCategory getCategoryData(int itemID) throws Exception{
        ObjectCategory objCategory = new ObjectCategory();
        Cursor cursor = database.query(MySQLiteHelper.TABLE_CATEGORY, new String[] {"name"},
                "id = " + itemID, null, null, null, null);
        if (cursor!=null && cursor.getCount()>0 && cursor.moveToFirst()) {
            objCategory.name = cursor.getString(cursor.getColumnIndex("name"));
        }
        return objCategory;
    }
}

Can anyone point me in the right direction, in implementing Loader the right way in my case here? If possible, I don't want to alter the codes inside class DbControl too much - especially the return data type of the functions inside it.

Btw amongst the tutorials about loaders that I've read is:

Anyway here's what I've done so far:

public class CategoryEdit extends Fragment implements LoaderManager.LoaderCallbacks<ObjectCategory> {

@Override
    public Loader<ObjectCategory> onCreateLoader(int id, Bundle args){
        try{
            dbc.open();
            objCategory = new ObjectCategory();
            objCategory = dbc.getCategoryData(categoryID);
            dbc.close();
        }catch (SQLException e){
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }

        return objCategory; //this returns error, it says required Loader<ObjectCategory> instead of objCategory, but I'm not sure how to do that here
    }

@Override
    public void onLoadFinished(Loader<ObjectCategory> cursorLoader, ObjectCategory cursor) {
//Since onCreateLoader has error above, I'm not sure how to implement this part
    }

@Override
    public void onLoaderReset(Loader<ObjectCategory> cursorLoader) {
        //Can I just leave this empty?
    }
}
imin
  • 4,504
  • 13
  • 56
  • 103

2 Answers2

1

I would say - don't use Loaders. I used CursorLoader in my app when I was new to Android, and it turned out to be a bad decision.

If CursorLoader is sufficient for your needs, then you're alright (though your Activities and Fragments will become polluted with DB related logic), but once you try to implement your own Loader, that should encapsulate DB related logic and just return a constructed object, you'll inevitably bump into a million issues with LoaderManager framework. @CommonsWare wrote a nice article about it - read it before you integrate Loaders into your app.

Nowadays I usually use "managers" for loading data from SQLite or internet. The general form of my "manager" is like this (please note that these "managers" are not Singletons):

public class TutorialManager {

    interface TutorialManagerListener {
        void onDataFetchCompleted(SomeData data);
    }
    private BackgroundThreadPoster mBackgroundThreadPoster;
    private MainThreadPoster mMainThreadPoster;


    private Set<TutorialManagerListener> mListeners = new HashSet<>(1);

    public TutorialManager(@NonNull BackgroundThreadPoster backgroundThreadPoster,
                           @NonNull MainThreadPoster mainThreadPoster) {
        mBackgroundThreadPoster = backgroundThreadPoster;
        mMainThreadPoster = mainThreadPoster;
    }

    @UiThread
    public void registerListener(@NonNull TutorialManagerListener listener) {
        mListeners.add(listener);
    }

    @UiThread
    public void unregisterListener(@NonNull TutorialManagerListener listener) {
        mListeners.remove(listener);
    }

    @UiThread
    private void notifyFetchCompleted(SomeData data) {
        for (TutorialManagerListener listener : mListeners) {
            listener.onDataFetchCompleted(data);
        }
    }

    /**
     * Call to this method will fetch data on background thread and then notify all registered
     * listeners about new data on UI thread.
     */
    public void fetchData() {
        mBackgroundThreadPoster.post(new Runnable() {
            @Override
            public void run() {

                // logic that loads the data on background thread goes here
                // final SomeData data = ... ;

                mMainThreadPoster.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyFetchCompleted(data);
                    }
                });
            }
        });
    }
}

Implementation of MainThreadPoster:

/**
 * This class should be used in order to execute {@link Runnable} objects on UI thread
 */
public class MainThreadPoster {

    private final Handler mMainHandler;

    public MainThreadPoster() {
        mMainHandler = new Handler(Looper.getMainLooper());
    }

    /**
     * Post {@link Runnable} for execution on main thread
     */
    public void post(Runnable runnable) {
        mMainHandler.post(runnable);
    }

}

Implementation of BackgroundThreadPoster is up to you.I usually use the following simple implementation (unless some fine tuning is required):

/**
 * This class should be used in order to execute {@link Runnable} objects on background threads
 */
public class BackgroundThreadPoster {

    private final ExecutorService mBackgroundExecutor;

    public BackgroundThreadPoster() {
        mBackgroundExecutor = Executors.newCachedThreadPool();
    }

    /**
     * Post {@link Runnable} for execution on a random background thread
     */
    public void post(Runnable runnable) {
        mBackgroundExecutor.execute(runnable);
    }

}

I inject MainThreadPoster and BackgroundThreadPoster objects into "managers" in order to:

  1. Make "managers" unit-testable
  2. Perform centralized management and optimization of background threading across entire application
Vasiliy
  • 16,221
  • 11
  • 71
  • 127
  • wow thanks for another perspective on Loaders and another approach.. I might look at this later. Btw IINM I read some place suggesting that RxJava is another better approach compared to Loaders... you thought on this? – imin Nov 02 '16 at 05:52
  • @imin, I'm not an expert on RxJava, but I think that it is more than just an alternative to loaders - it is entirely different architectural style for application. From what I understand, RxJava will be somewhat similar to the "managers" approach I described in this answer - it is based on Observables and Notifications (aka. listeners). – Vasiliy Nov 02 '16 at 06:36
1

Can anyone point me in the right direction, in implementing Loader the right way in my case here?

You have to use an AsyncTaskLoader like this:

public class CategoryEdit extends Fragment implements LoaderManager.LoaderCallbacks<ObjectCategory> {

    @Override
    public Loader<ObjectCategory> onCreateLoader(int id, Bundle args) {
        return new AsyncTaskLoader<ObjectCategory>(getActivity()) {
            @Override
            public ObjectCategory loadInBackground() {
                try {
                    dbc.open();
                    objCategory = new ObjectCategory();
                    objCategory = dbc.getCategoryData(categoryID);
                    dbc.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return objCategory;
            }

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

    }

    @Override
    public void onLoadFinished(Loader<ObjectCategory> loader, ObjectCategory data) {
        //do some stuff here
    }

    @Override
    public void onLoaderReset(Loader<ObjectCategory> loader) {
        //Yes, you can leave it empty
    }
}
kws
  • 926
  • 7
  • 11
  • I tried using your method, but the codes inside loadInBackground is not called. I added forceload as suggested here http://stackoverflow.com/questions/8606048/asynctaskloader-doesnt-run but this doesn't seem like a good idea. – imin Nov 02 '16 at 05:44
  • OK I added `@Override protected void onStartLoading() { if (takeContentChanged()) forceLoad(); } @Override protected void onStopLoading() { cancelLoad(); }` and it works now. – imin Nov 02 '16 at 06:32