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;
}
There are many bugs with notifyDataSetChanged() and usually they appear if you try doing some complex work with your list data.
Mostly, it is because the method is lazy and can't distinguish changes, so to avoid this problem, test your code with this scenario:
delete changing rows
call notifyDataSetChanged()
add changed rows at their indexes
again call notifyDataSetChanged()
and, tell me if it did'nt solve your problem.
Edit: After adapter code is put, I saw the flaw in your code.
Sorry for late the response:
convertView is the view which you had populated before after initializing it.
When in method getView() you get an instance of convertView, you must populate it before returning it.
so to be clear, do something like this:
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) //Regenerate the view
{
/* Instantiate your view */
} else {
view = convertView;
}
// populate the elements in view like EditText, TextView, ImageView and etc
return view;
}
Related
Apologies if this is poorly explained, I am having difficulty in understanding it myself. If you point out anything you don't understand I will do my best to correct any issues. Okay so here we go.
Several classes. (D&D sheet, sheet has weapons user can equip, this is about equipping said weapons which is stored in a list)
A fragment activity - CombatFragment
The arrayadapter list which is declared in CombatFragment -
AttackListViewContentAdapter
The realm object - Weapon
The realm object where a list of Weapon is held - Sheet
A number of XML files (The code of which I won't paste here as SO has a limit on code. content_combat, attack_list_item
What I've gathered so far is that when I create a new attackListViewContentAdapter it loops at a rapid and continued pace. So much so that the screen does not respond to me touching any of the widgets. I've done things like log a number each time it passes so it shows when it's doing it again and again. If you need information on that I can show you where I put the logs and what shows in my Logcat when I add an additional view (row).
I believe that it's something to do with the onChangedListener which keeps being triggered, even if I found the reason why how do I then get to a stage where I can create a new view and have the listener so it can record changes.
Please note in the interests of space I will be using abbreviated code. I've ignored things like dialog boxes and widgets which aren't relevant. So if it seems like something missing or you need to view the classes, it's possibly in the file which I've linked above each one.
CombatFragment
public class CombatFragment extends Fragment {
#BindView(R.id.lv_attack_spellcasting_content)
ListView lv_attack_spellcasting_title;
#Nullable
#Override
public View onCreateView(final LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.content_combat, container, false);
RealmList<Weapon> weaponList = sheet.getWeaponList();
final AttackListViewContentAdapter attackListViewContentAdapter = new AttackListViewContentAdapter(getActivity(), sheet, realm, weaponList);
weaponList.addChangeListener(new RealmChangeListener<RealmList<Weapon>>() {
#Override
public void onChange(RealmList<Weapon> weapons) {
/* Gives the adaptor a kick to know that the weapon realm list has changed */
attackListViewContentAdapter.notifyDataSetChanged();
loopOnChanged++;
}
});
lv_attack_spellcasting_title.setAdapter(attackListViewContentAdapter);
playerInit();
return rootView;
}
// This is a fake method, this is just to show that the .add is in it's own method which is triggered by a button press and not in onCreate
public void buttonPress() {
sheet.getWeaponList().add(realm.createObject(Weapon.class));
}
} `
AttackListViewContentAdapter
public class AttackListViewContentAdapter extends ArrayAdapter<Weapon> {
public AttackListViewContentAdapter(Context context, Sheet sheet, Realm realm, List<Weapon> weaponList) {
super(context, 0, weaponList);
this.sheet = sheet;
this.realm = realm;
}
#Override
#NonNull
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null)
//Because you're returning the view (AttachToRoot is false) the ArrayAdaptor (This class) will handle adding the view to the list.
convertView =
LayoutInflater.from(getContext()).inflate(R.layout.attack_list_item, parent, false);
return convertView;
}
}
Weapon
public class Weapon extends RealmObject {
#PrimaryKey
int weaponID;
//properties, set get methods etc.
}
Sheet
public class Sheet extends RealmObject {
#PrimaryKey
private int sheetID;
private RealmList<Weapon> weaponList;
public RealmList<Weapon> getWeaponList() {
return weaponList;
}
public void setWeaponList(RealmList<Weapon> weaponList) {
this.weaponList = weaponList;
}
}
content_combat
<ListView
android:id="#+id/lv_attack_spellcasting_content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:columnCount="7"
android:rowCount="1" />
attack_list_item
Nothing really in there to include
Your problem is happening because of bad initialization of your spinner widgets within AttackListViewContentAdapter class.
You must set your spinners selection before setOnItemSelectedListener is set.
You must check whether your spinner selection is not equal to current selection, to avoid an infinite loop between onChange and onItemSelected methods. I mean, your spinners onItemSelected callbacks, execute a realm transanctions, then, those transanctions fire your onChange callback and finally, your onChange callback invokes notifyDataSetChanged() which make cycle start again going into an infinite loop.
To solve your problem, you should follow the next steps inside AttackListViewContentAdapter.java:
A) Remove the following lines from addWeaponToUI() method:
private void addWeaponToUI() {
et_name_value.setText(weapon.getWeaponName());
np_damage_number_of_die_value.setValue(weapon.getWeaponDamageNumberOfDie());
SheetEnum.Ability ability = SheetEnum.Ability.getEnumValue(weapon.getWeaponAbilityBonusInt());
tv_attack_bonus_value.setText(String.valueOf(sheet.getAbilityBonus(ability)));
// REMOVE below lines!
//s_damage_die_type_value.setSelection(weapon.getWeaponDamageDieTypeInt());
//s_damage_type_value.setSelection(weapon.getWeaponDamageTypeInt());
//s_ability_bonus_value.setSelection(weapon.getWeaponAbilityBonusInt());
}
B) Invoke spinner setSelection() before setOnItemSelectedListener(), then check selected item is not equal to selected position to avoid an infinite loop:
ArrayAdapter<CharSequence> damageDieTypeAdapter = ArrayAdapter.createFromResource(getContext(), R.array.die_type, android.R.layout.simple_spinner_dropdown_item);
s_damage_die_type_value.setAdapter(damageDieTypeAdapter);
//Set selection before listener
s_damage_die_type_value.setSelection(weapon.getWeaponDamageDieTypeInt());
s_damage_die_type_value.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View v, final int position, long id) {
//Check selected position is not equal to current position to avoid an infinite loop
if (position != weapon.getWeaponDamageDieTypeInt()) {
String[] value = getContext().getResources().getStringArray(R.array.die_type);
realm.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
weapon.setWeaponDamageDieType(position);
}
});
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
C) Repeat Step B for s_damage_type_value and s_ability_bonus_value spinners
So I'm working on a fairly simplistic SMS app to teach myself Android, and I seem to have run into the same problem described in this thread: ListView in ArrayAdapter order get's mixed up when scrolling
Unfortunately, the answer posted there doesn't seem to be working for me. I've been looking at this for a long time now, and I'm stumped. Note that since this is an SMS app, I'm using two different layouts, depending on who sent the message.
Also, in by debugging, I have found that the messages that I pass in my ArrayList are in the correct order, so that doesn't seem to be the problem. Any help would be amazing.
public class MessageAdapter extends ArrayAdapter {
public MessageAdapter(Context context, ArrayList<CryptoMessage> messages){
super(context, 0, messages);
}
/**
* {#inheritDoc}
*
* #param position
* #param convertView
* #param parent
*/
#Override
public View getView(int position, View convertView, ViewGroup parent) {
CryptoMessage current = (CryptoMessage) getItem(position);
if(convertView == null){
if(current.isSent()){
//This is message that the user sent. Inflate the view for a sent message.
convertView = LayoutInflater.from(getContext()).inflate(R.layout.sent_message, parent, false);
}
else{
//Otherwise, this is a message that the user received. Inflate the view for a received message.
convertView = LayoutInflater.from(getContext()).inflate(R.layout.recieved_message, parent, false);
}
}
if(current.isSent()){
//The current message is a sent message. Get the resources from the sent view.
TextView body = (TextView) convertView.findViewById(R.id.sentMessageText);
TextView date = (TextView) convertView.findViewById(R.id.sentMessageDate);
//Populate the data.
if(body != null) {
body.setText(current.getMessage());
}
if(date != null) {
date.setText(current.getDate());
}
}
else{
TextView body = (TextView) convertView.findViewById(R.id.recievedMessageText);
TextView date = (TextView) convertView.findViewById(R.id.recievedTimestampText);
if(body != null) {
body.setText(current.getMessage());
}
if(date != null) {
date.setText(current.getDate());
}
}
return convertView;
}
}
Problem #1: Never use LayoutInflater.from() for creating a UI for an activity or fragment. Always call something more specific, like getLayoutInflater() on the Activity. As it stands, your themes will be ignored.
Problem #2: If you are going to use multiple different layouts for rows, you must override getViewTypeCount() to return the number of layouts (in your case, 2). Also, you must override getItemViewType() to return a value (in your case, 0 or 1), indicating which type of row will be used for the supplied position. In your case, that would be based on isSent().
Problem #3: Get rid of the body != null and date != null. If you do not get your widgets for your rows, you want to crash during development, so you can start determining why you did not get your widgets. In this case, had you left those checks off, you would be crashing with NullPointerExceptions, indicating that you did not find the widgets, and perhaps would have had an easier time finding out what went wrong.
I have an arraylist and I want to update specific item.I am adding data to list with this line:
randomsList.add(new Random(result.get(i).getAsJsonObject(),0));
This is adding datas to 0,1,2,3,4... locations so when I try to update an item I don't know which object is where.
I am updating data with this line:
randomsList.set(position,new Random(user,1));
I think if I use the custom numbers for location I can update specific item.My prototype:
randomsList.add({USER_ID},new Random(result.get(i).getAsJsonObject(),0));
And if I want to update it then I use this line:
randomsList.set({USER_ID},new Random(user,1));
Is this a good approach ? If your answer is no,how should be ?
P.S. : I am using this arraylist with an adapter
As #itachiuchiha mentions, you should use a Map. The "custom numbers" you mention are your key (integers), and the value is the Random object.
As an aside, in response to your comment, below is an example of an Android Adapter that uses a Map as the underlying datasource.
public class RandomsAdapter extends BaseAdapter {
private Map<Integer, Random> randoms;
public RandomsAdapter(Map<Integer, Random> randoms) {
this.randoms = randoms;
}
public void updateRandoms(Map<Integer, Random> randoms) {
this.randoms = randoms;
notifyDataSetChanged();
}
#Override
public int getCount() {
return randoms.size();
}
#Override
public Random getItem(int position) {
return randoms.get(position);
}
#Override
public long getItemId(int position) {
// we won't support stable IDs for this example
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = createNewView();
}
update(view, songs.get(position));
return view;
}
private View createNewView() {
// TODO: create a new instance of your view and return it
return null;
}
private void update(View view, Random random) {
// TODO: update the rest of the view
}
}
Note the updateRandoms(Map<Integer, Random> randoms) method.
While you could expose a method in the adapter to update a single entry in the Map, it shouldn't be the responsibility of the Adapter to handle modifications to the map. I prefer passing the entire map again - it could still be a reference to the same object, but the Adapter doesn't know or care; it just knows: "my underlying datasource has been changed/modified, I need to tell my observers that they should refresh their views by calling notifyDataSetChanged()".
Alternatively, you could call notifyDataSetChanged() on the adapter externally when you modify the underlying Map (this tells the ListView that its data is out of date and to request its views from the adapter again).
I'm creating a custom adapter and using the getView method in attempt to display a default text only ONCE. Now I'm having a problem such that when I click the first Item in the list, the default text is kept but that doesn't hold for any other items? Any suggestions?
Thanks! (My code is a bit messy as I was just trying to debug)
boolean firstTime = true;
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (firstTime) {
firstTime = false;
TextView firstView = new TextView(ForgotPasswordActivity.this);
firstView.setText("Please select School");
return firstView;
}
TextView view = new TextView(ForgotPasswordActivity.this);
view.setText("Hello");
return view;
}
You must play with the getCount function :
#Override
public int getCount() {
return super.getCount() -1; // This makes the trick;
}
this trick will not show last item that you've added inside your spinner(so when you finish adding your text inside the spinner, add the text that will not be shown in the spinner, and by that it will be show as a default value before clicking the spinner).
Good luck
I'm not exactly sure what your trying to do but you could force the top row to always show the select message by checking if the position is 0. Also notice in the code below that I'm reusing the convertView if it is not null. It's faster to reuse the convertView if it is available than to recreate a new view every time.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
convertView = new TextView(ForgotPasswordActivity.this);
}
if(position == 0) {
convertView.setText("Please select School");
} else {
convertView.setText("Hello");
}
return convertView;
}
Also remember that by forcing position zero to show the select message you are not showing the actual data in the adapter at position 0. Make sure this is what you want to do or insert a dummy piece of data in the first position of the backing data array.
I have a ViewPager within a Fragment in my application. Within the ViewPager I have 6 list views which pull their data from a central data source (a Schedule, which each list view then pulls Lists of days from). Now I'm trying to update this schedule object by changing the schedule that is stored in my PagerAdapter class, and then updating the ArrayAdapter that serves as the adapter to all the list views. When one list view is switched to, a new array adapter is created with the correct data.
Unfortunately this doesn't work at all. I cannot see any data at any point in the application lifecycle. So I'm assuming I'm doing something fundamentally wrong in connecting my data to my ViewPager...
I've been all over the net, read up on all of the fragment stuff, a LOT of the view pager stuff...
Does anyone know the correct way to do this?
Heres the code for my DayAdapter (ViewPagerADapter)
public final class DayAdapter extends PagerAdapter {
//~Data Fields----------------------------------------//
/**
* The list adapter that the current list is being handed to.
*/
private Schedule schedule;
/**
* The current index of the day that is to be displayed.
*/
private int currentIndex;
/**
* ArrayAdpter to be used to connect data to all of the list views.
*/
private ArrayAdapter<Course> adapter;
//~Constructors---------------------------------------//
/**
* Constructor for the DayAdapter implementation of PagerAdapter. Takes in a
* Schedule object which is to be the data backing of the views displayed.
*
* #param theSChedule the Schedule object that is the backing for the ListViews.
*/
public DayAdapter(Schedule theSchedule) {
schedule = theSchedule;
currentIndex = schedule.getTodayIndex();
}
//~Methods--------------------------------------------//
public void setSchedule(Schedule theSchedule) {
schedule = theSchedule;
adapter.clear();
adapter.add(schedule.getDay(currentIndex).getList().get(0));
adapter.notifyDataSetChanged();
}
#Override
public Object instantiateItem(ViewGroup container, int index) {
LayoutInflater inflater = (LayoutInflater) container.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.schedule_list_view, null);
ListView view = new ListView(container.getContext());
adapter = new ArrayAdapter<Course>(container.getContext(),
R.layout.list_view_child, schedule.getDay(index).getList());
view.setAdapter(adapter);
//TEST CODE!!!! This does not yield any sort data items in the list views!?
adapter.add(new Course("test", "test", "test", "test", "test", "test", "test"));
currentIndex = index;
((ViewPager) container).addView(layout);
return layout;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
ViewPager pager = (ViewPager) container;
View view = (View) object;
pager.removeView(view);
}
/**
* Gets the pageTitle, which is the name of the day that is at position.
*
* #param position the index of the day selected.
* #return the name of the day the index refers to.
*/
#Override
public CharSequence getPageTitle(int position) {
return schedule.getDay(position).getThisDay();
}
#Override
public int getCount() {
return 6;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
If that is the full code for the instantiateItem method then it's normal that you don't see any data in the lists. You inflate the layout file named R.layout.schedule_list_view(in which you, most likely, have a ListView) and in the same instantiateItem method you create an independent ListView widget on which you set the data. As the data is binded to the ListView which is not in the layout file, you don't see anything as the original ListView remains empty.
The solution is to either set the data on the ListView that is in the inflated layout(R.layout.schedule_list_view) if you have it there, or add the created ListView with data to the inflate layout(layout(this is a View, so you would want to cast it to a ViewGroup or a subclass of ViewGroup)).
Instead of creating a new adapter with new data, try to change the data in the existing adapter.
Get the collection associated with the adapter.
Modify the collection as you need (should not create new instance of collection. either you can clear the collection and add the new contents or update the collection.)
call the notifyDataSetChanged() of the adapter.
Hope this will work...:)