AdamMc331
AdamMc331

Reputation: 16730

Calling initLoader() only works once when using callbacks for CursorAdapter

Inside of my Activity I have a listview that represents doctors. Each row has the doctor's name and a checkbox. I have implemented a callback interface so that when a doctor is selected, all other doctors are removed from the listview and only the selected doctor remains.

It appears to work, because once I select a doctor, all others are removed. When I uncheck a doctor, everyone is added back. But, if I now select a different doctor, the original one stays and all others are removed.

To better explain the issue, let's say when I start the activity I have two doctors in the listview, Joel and Sam. I think I want to select Joel, so I do, and Sam is removed from the list. Then, I realized I was wrong, so I unselect Joel, and I now see both Joel and Sam in the list. Last, I select Sam. However, Sam is removed from the list and only Joel remains again.

Here is some code snippets from the adapter class:

@Override
public void bindView(View view, Context context, Cursor cursor) {
    ViewHolder viewHolder = (ViewHolder) view.getTag();

    final long id = cursor.getLong(cursor.getColumnIndex(DoctorEntry._ID));
    String firstName = cursor.getString(cursor.getColumnIndex(DoctorEntry.COLUMN_FIRSTNAME));

    viewHolder.nameView.setText(firstName);

    viewHolder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if(mCallbacks != null){
                if(isChecked){
                    mCallbacks.onDoctorChecked(id);
                } else{
                    mCallbacks.onDoctorUnchecked();
                }
            }
        }
    });
}

public void onRegisterCallbacks(DoctorAdapterCallbacks activity){
    mCallbacks = activity;
}

public static interface DoctorAdapterCallbacks{
    void onDoctorChecked(long id);

    void onDoctorUnchecked();
}

And in my activity I have the following implementations:

@Override
public void onDoctorChecked(long id) {
    Bundle args = new Bundle();
    args.putLong(SELECTED_DOCTOR_ID, id);
    getSupportLoaderManager().initLoader(SELECTED_DOCTOR_LOADER, args, this);
}

@Override
public void onDoctorUnchecked() {
    getSupportLoaderManager().initLoader(DOCTOR_LOADER, null, this);
}

The DOCTOR_LOADER is a CursorLoader that represents all doctor's in the table. SELECTED_DOCTOR_ID is a CursorLoader for only a single doctor.

If I had to guess, my issue resides in the bindView method, because I declare the id variable as final. The reason I did that is because otherwise I receive a compiler error:

error: local variable id is accessed from within inner class; needs to be declared final

Is declaring the variable final causing trouble? Does anyone see a problem?

EDIT

I've added log statements to both the onCheckedChangedListener in the adapter and in the onDoctorSelected of the Activity. Using the same example above, I see the following output:

> onCheckedChanged : Selecting doctor id: 1 // Joel
> onDoctorSelected : Selecting doctor id: 1 // Joel
> onCheckedChanged : Selecting doctor id: 2 // Sam
> onDoctorSelected : Selecting doctor id: 2 // Sam

So, it appears that it does see that I select Sam, with id 2, and it passes the id 2 into the initLoader() method, but yet only Joel (doctor id 1) is displayed in the listview, because I selected him first.

EDIT 2

Per request, here is a snippet of the CursorLoader methods:

@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
    switch(i){
        case DOCTOR_LOADER:
            return new CursorLoader(
                    this,
                    DoctorEntry.CONTENT_URI,
                    DOCTOR_COLUMNS,
                    null,
                    null,
                    null
            );
        case SELECTED_DOCTOR_LOADER:
            long _id = bundle.getLong(SELECTED_DOCTOR_ID);
            return new CursorLoader(
                    this,
                    DoctorEntry.buildDoctorUri(_id),
                    DOCTOR_COLUMNS,
                    DoctorEntry._ID + " = '" + _id + "'",
                    null,
                    null
            );
        default:
            throw new UnsupportedOperationException("Unknown loader id: " + i);
    }
}

@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
    switch(cursorLoader.getId()){
        case DOCTOR_LOADER:
        case SELECTED_DOCTOR_LOADER:
            mDoctorAdapter.swapCursor(cursor);
            break;
        default:
            throw new UnsupportedOperationException("Unknown loader id: " + cursorLoader.getId());
    }
}

EDIT 3

I added another log statement inside onCreateLoader to see which ID the loader was being created with. Then, I saw this output:

> onCheckedChanged : Selecting doctor id: 2
> onDoctorSelected : Selecting doctor id: 2
> onCreateLoader : Using id: 2
> // Unchecked, and now check doctor one
> onCheckedChanged : Selecting doctor id: 1
> onDoctorSelected : Selecting doctor id: 1

That is not a typo. It appears that onCreateLoader is not called the second time a doctor is selected. I have tried calling destroyLoader() inside onDoctorChecked but that did not seem to make a difference.

Upvotes: 2

Views: 678

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 200100

Per the initLoader() documentation:

Ensures a loader is initialized and active. If the loader doesn't already exist, one is created and (if the activity/fragment is currently started) starts the loader. Otherwise the last created loader is re-used.

In either case, the given callback is associated with the loader, and will be called as the loader state changes. If at the point of call the caller is in its started state, and the requested loader already exists and has generated its data, then callback onLoadFinished(Loader, D) will be called immediately (inside of this function), so you must be prepared for this to happen.

initLoader() only initializes a certain loader ID once, reusing the data from then on. If you'd like to instead throw out and recreate a new loader (i.e., with a new CursorLoader), use restartLoader() in its place (note: restartLoader() will initalize the loader just like initLoader() the first time, so you don't need any special logic around the first run).

Upvotes: 5

Related Questions