In the app I've been working on, I have a custom class DeviceListAdapter
extending BaseAdapter
which gets passed to my ListView
. In my DeviceListAdapter
class, I keep my own ArrayList<Device>
which I use to generate the list's views with View getView(... )
. Whenever the app causes a change in the data, I use custom methods in DeviceListAdapter
to update the ArrayList<Device>
to reflect the changes. I've used the debugger and many print statements to check that the data does get changed how I expect it to, adding and removing Device
objects as specified. However, after each change to the data I also call notifyDataSetChanged()
, but on the UI none of the elements get updated. In the debugger, I found that after calling notifyDataSetChanged()
, the getView(... )
method was not being called, which explains why the ListView
wasn't being redrawn. To figure out why, I used the debugger's "step into" function to trace where the program execution went into the android framework since I have the SDK sources downloaded. What I found was very interesting. The path of execution went like this:
DeviceListAdapter.notifyDataSetChanged()
BaseAdapter.notifyDataSetChanged()
DataSetObservable.notifyChanged()
AbsListView.onInvalidated()
Rather calling the onChanged()
method, it jumped tracks and executed the onInvalidated()
method once it reached AbsListView
. Initially I thought this was an error with the debugger perhaps reading the wrong line number, but I restarted my Android Studio as well as totally uninstalled and reinstalled the app, but the result is the same. Can anybody tell me if this is legitimately a problem with Android's framework or if the debugger is unreliable for tracing execution outside of my own project files?
More on my implementation of notifyDataSetChanged()
... I created the local method to override BaseAdapter
's notifyDataSetChanged()
so that I could set a boolean flag mForceRedraw
inside of my DeviceListAdapter
as to whether I should force redraw my list entries. In the getView(... )
method, I typically check if the second parameter, View convertView
is null, if it is then I redraw the view and if not then I pass convertView through and return it. However, when 'mForceRedraw' is true, I never return convertView
, I explicitly redraw the view. The problem that arises is caused by my earlier concern, which is that getView()
is not called after I execute notifyDataSetChanged()
.
EDIT: Here's a code snippet of my DeviceListAdapter
:
/**
* Serves data about current Device data to the mDeviceListView. Manages the dynamic and
* persistent storage of the configured Devices and constructs views of each individual
* list item for placement in the list.
*/
private class DeviceListAdapter extends BaseAdapter {
private boolean mForceRedraw = false;
/**
* Dynamic array that keeps track of all devices currently being managed.
* This is held in memory and is readily accessible so that system calls
* requesting View updates can be satisfied quickly.
*/
private List<Device> mDeviceEntries;
private Context mContext;
public DeviceListAdapter(Context context) {
this.mContext = context;
this.mDeviceEntries = new ArrayList<>();
populateFromStorage();
}
/**
* Inserts the given device into storage and notifies the mDeviceListView of a data update.
* @param newDevice The device to add to memory.
*/
public void put(Device newDevice) {
Preconditions.checkNotNull(newDevice);
boolean flagUpdatedExisting = false;
for (Device device : mDeviceEntries) {
if (newDevice.isVersionOf(device)) {
int index = mDeviceEntries.indexOf(device);
if(index != -1) {
mDeviceEntries.set(index, newDevice);
flagUpdatedExisting = true;
break;
} else {
throw new IllegalStateException();
}
}
//If an existing device was not updated, then this is a new device, add it to the list
if (!flagUpdatedExisting) {
mDeviceEntries.add(newDevice);
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* If the given device exists in storage, delete it and remove it from the mDeviceListView.
* @param device
*/
public void delete(Device device) {
Preconditions.checkNotNull(device);
//Remove device from mDeviceEntries
Iterator iterator = mDeviceEntries.iterator();
while(iterator.hasNext()) {
Device d = (Device) iterator.next();
if(device.isVersionOf(d)) {
iterator.remove();
}
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* Retrieves Device entries from persistent storage and loads them into the dynamic
* array responsible for displaying the entries in the listView.
*/
public void populateFromStorage() {
List<Device> temp = Preconditions.checkNotNull(TECDataAdapter.getDevices());
mDeviceEntries = temp;
notifyDataSetChanged();
}
public int getCount() {
if (mDeviceEntries != null) {
return mDeviceEntries.size();
}
return 0;
}
public Object getItem(int position) {
return mDeviceEntries.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null || mForceRedraw) //Regenerate the view
{
/* Draws my views */
} else //Reuse the view
{
view = (LinearLayout) convertView;
}
return view;
}
@Override
public void notifyDataSetChanged() {
mForceRedraw = true;
super.notifyDataSetChanged();
mForceRedraw = false;
}
}
You are in the adapter and calling notify dataset changed.This would ideally not even be needed.Because you are modifying the dataset which is used internally by your adapter.The getView method of your adapter will always be called whenever a view needs to be rendered.
The convertView approach is to solely recycle a view(not the data).It merely provides you an alternative to the expensive process of view inflation.
So what your code should be :
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null) //Regenerate the view
{
/* inflate Draws my views */
} else
{
view = (LinearLayout) convertView;
}
//repopulate this view with the data that needs to appear at this position using getItem(position)
return view;
}