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
Related
I'm using a Recycler View to show all the images from the galley or the external storage of a device in a Grid Layout Manager. And I'm using a Radio Button to show if the image is selected or not.
PROBLEM
Whenever I select or deselect a Radio Button from the visible Views in the Recycler View some other Views which are outside the Visible Screen got selected or deselected.
It is like I'm pressing on the same View of the Recycler View, but the images are different.
PROBLEM
well that's because of the recycler view concept of reusing the views instead of creating new views every time you scroll.
you see if you have 100 items you want to show in a recycler view and only 20 of them could appear to the user, recycler view creates only 20 view holder to represent the 20 items, whenever the user scroll recycler view will still have 20 view holder only but will just switch the data stored in this view holders rather than create new view holders.
now to handle selection of your items there's two ways to do this.
the naive way
hold selection in a boolean array inside the recycle view adapter.
whenever the user scrolls, the adapter calls onBindViewHolder to update the visible viewholder with the proper data.
so when onBindViewHolder gets called just set the radio button selection according the boolean array using the position sent in the method call
at the end of your usage to the recycler view you can create a getter method in the adapter to get the selection array list of boolean and pass the data based on it
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_ClassType> data;
ArrayList<Boolean> dataSelected ;
public PhotosGalleryAdapter(ArrayList<Your_Data_ClassType> data) {
this.data = data;
dataSelected = new ArrayList<>(data.size()) ;
}
...
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
RadioButton radioButton = holder.getRadioButton()
radioButton.setChecked(dataSelected.get(position));
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
dataSelected.set(holder.getAbsoluteAdapterPosition() , isChecked) ;
}
});
...
}
}
the other way is to use a selection tracker and it should be the correct way to handle selections in a recycler view.
the problem with this way is it needs a lot of editing to the code and creating new classes to include as parameters in the selection tracker, but in the end you'll find it worth the time you spent on it.
in order to start with this way you need to do the following :
firstly, decide what should be a key (String-Long-Parcelable) so the tracker should use to differentiate between your data , the safest way is either String or Parcelable as I once tried Long and ended up with lots and lots of problems (in your case I will assume it's the photo's uri which will be of type string)
secondly, you need to create two new classes, one that extends ItemDetailsLookup, and the other extends ItemKeyProvider, and should use the key as their generic type (the type that is put between <> )
your two classes should look like this (that you might copy them straight forward)
the one that extends ItemKeyProvider :
public class GalleryItemKeyProvider extends ItemKeyProvider<String>{
PhotosGalleryAdapter adapter ;
/**
* Creates a new provider with the given scope.
*
* #param scope Scope can't be changed at runtime.
*/
public GalleryItemKeyProvider(int scope,PhotosGalleryAdapter m_adapter) {
super(scope);
this.adapter = m_adapter;
}
#Nullable
#Override
public String getKey(int position) {
return adapter.getKey(position);
}
#Override
public int getPosition(#NonNull String key) {
return adapter.getPosition(key);
}
}
the one that extends ItemDetailsLookup :
public class GalleryDetailsLookup extends ItemDetailsLookup<String> {
private final RecyclerView recView ;
public GalleryDetailsLookup(RecyclerView m_recView){
this.recView = m_recView;
}
#Nullable
#Override
public ItemDetails<String> getItemDetails(#NonNull MotionEvent e) {
View view = recView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);
if (holder instanceof PhotosGalleryViewHolder) {
return ((PhotosGalleryViewHolder) holder).getItemDetails();
}
}
return null;
}
}
thirdly, you should include this new two methods in your adapter to be used by the above classes
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
...
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
...
}
forthly (if there's an english word called forthly), you should initialize the tracker with all the above classes that were created before and he will handle the rest, the tracker takes as parameters
a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
the ItemKeyProvider that we created
the DetailsLookup that we created
a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
a Selection predicate, it's responsible to handle the way of selection you want to do, you want it to be able to (select only one item-multiple selection with no limits- based on a weird algorithm like even only or odd only), in my case I will use a default multiple selection one but if you want to alter it with another selection algorithm you should create a new class that extends SelectionPredicates and implement your way of selection, you could also just check the other default ones might be what you're looking for.
anyway, that's how the initialization should look (you should put this code wherever you initialize your recycler view at whether it's in fragment or activity method):
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
...
}
I didn't find a way to let me initialize the adapter with data and then create the tracker inorder to make the viewholders know about their selection or not, so in this case I firstly created the tracker and then made the adapter know about it's data using a setter and notifyDataSetChanged
what I mean by that is after creating the tracker instantly set the tracker and data to the adapter, so the initRecycleView should look like this
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
photosAdapter.setTracker(tracker);
photosAdapter.setData(data);
photosAdapter.notifyDataSetChanged();
...
}
Last but no least, you should handle how the view holders should know if they were selected or not, so you should let the adapter know about the tracker and its data by creating a setter method in it, that's how the adapter should look like in the end :
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_Class> data;
private SelectionTracker<String> tracker;
public PhotosGalleryAdapter() {
data = new ArrayList<>();
}
public ArrayList<Your_Data_Class> getData() {
return data;
}
public void setData(ArrayList<Your_Data_Class> m_data) {
this.data = m_data;
}
#Override
public ScheduleViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
...
}
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
boolean isSelected = tracker.isSelected(data.get(i).getUri());
RadioButton radioButton = holder.getRadioButton;
radioButton.setChecked(isSelected);
}
#Override
public int getItemCount() {
return data.size();
}
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
public void setTracker(SelectionTracker<String> m_tracker) {
this.tracker = m_tracker;
}
}
(as you may notice if you initialized the adapter with its data through the constructor, when he asks the tracker if there were an item selected or not, it will result in a NullPointerException as at the moment of initializing the adapter you still didn't initialize the tracker)
that way you could keep track of your selection the way google suggests in their documentation (which I honestly don't know why the made it very complicate like that).
if you want to know all the selected item in the end of your application/fragment use, you should call tracker.getSelection() which will return a Selection List for you to iterate on
There's a tiny problem/feature with the tracker that it won't start selecting the first item until you use a long press on it, that happens only in the first item you select, if you do want this feature (start selecting mode by long press) then leave it as it is
incase you don't want it you can make the tracker select a ghost key (any unique string key that means nothing to your data) at the beginning which should later enable the selection mode with a simple click on any photo
tracker.select("");
this also the way to make a default/old selection at the beginning, you could make a for loop and call tracker.select(Key) if you do want the tracker to start with few items being selected
N.B : incase you use the Ghost Key method you should watchout that the selection array that will get returned when you call tracker.getSelection() will also contain this Ghost Key.
at the end if you do have the curiosity of reading about selection tracker in the documentation follow this link
or maybe if you know how to read kotlin follow this two links
implementing-selection-in-recyclerview
a guide to recyclerview selection
I was stuck in the selection problem for days before I figure how to do all that so I hope you find your way through it.
Omar Shawky has covered the solutions.
With my answer I will stress on the reason why someone may face this sort of an issues with recycler views and how to avoid this common issue in the future (avoiding pitfalls).
Reason:
This issue happens because RecyclerView recycles views. So a RecyclerView item's view once inflated can get reused to show another off screen (to be scrolled to) item. This helps reduces re-inflation of views which otherwise can be taxing.
So if the radio button of an item's view is selected, and the same view gets reused to show some other item, then that new item can also have a selected radio button.
Solution:
The simplest solution for such issues is to have an if else logic in your ViewHolder to provide logic for both selected and de-selected cases. We also do not rely on information from radio button itself for initial setup (we do not use radioButton.isSelected() at the time of setup)
e.g code to write inside your ViewHolder class:
private boolean isRadioButtonChecked = false; // ViewHolder class level variable. Default value is unchecked
// Now while binding in your ViewHolder class:
// Setup Radio button (assuming there is just one radio button for a recyclerView item).
// Handle both selected and de-selected cases like below (code can be simplified but elaborating for understanding):
if (isRadioButtonChecked) {
radioButton.setChecked(true);
} else {
radioButton.setChecked(false);
}
radioButton.setOnCheckedChangeListener(
(radioButton, isChecked) -> isRadioButtonChecked = isChecked);
Do not do any of the following while setting up:
private boolean isRadioButtonChecked = false; // class variable
//while binding do not only handle select case. We should handle both cases.
if (isRadioButtonChecked) { // --> Pitfall
radioButton.setChecked(true);
}
radioButton.setOnCheckedChangeListener((radioButton, isChecked) -> isRadioButtonChecked = isChecked);
OR
// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}
I understand how a ViewHolder's onBindViewHolder works, however I'm unclear about how notifyItemRangeChanged(0, this.data.size()); works in this example and what it does exactly.
The data that is supplied to this adapter is in Json format.
The adapter is below:
public class AdapterQuestion extends RecyclerView.Adapter<AdapterQuestion.ViewQuestion>{
private LayoutInflater mLayoutInflater;
//this is an arrayList of questionData objects
private ArrayList<QuestionData> data =new ArrayList<>();
//Created the layoutInflator
public AdapterQuestion(Context context){
//get from context
mLayoutInflater=LayoutInflater.from(context);
}
public void setBloglist(ArrayList<QuestionData> data){
this.data =data;
notifyItemRangeChanged(0, this.data.size());
}
#Override
public ViewQuestion onCreateViewHolder(ViewGroup parent, int viewType) {
//inflates the customQuestion view or converts it to java code
View view= mLayoutInflater.inflate(R.layout.customquestion, null);
//We now want to convert the View into a ViewQuestion, view Question takes
//a view so we pass the view into view question and then return it.
ViewQuestion holder=new ViewQuestion(view);
return holder;
}
//ViewGroup parent and ViewType are not being assigned.
#Override
public void onBindViewHolder(ViewQuestion holder, int position) {
//here we need to bind the data to our view, there is currently no Data!
//We need to get the data from our JSON
//Parameters is a ViewHolder and a Position
//This gives us the current information object from the whole arraylist
//data.get(position) data is the arraylist and we are getting the current position or index;
//That current obj is of Type QuestionData
QuestionData currentObj= data.get(position);
//we are accessing the Inflated view, or saved view with holder
//holder.answerText is the textView in holder. We are then taking that current object
//We are getting the text of the current object and setting it to the AnswerText view
holder.answerText.setText(currentObj.getMtext());
holder.answerId.setText(currentObj.getId());
holder.mVotes.setText(currentObj.getVotes());
holder.mLikeButton.setTag(currentObj);
}
#Override
public int getItemCount() {
return data.size();
}
public class ViewQuestion extends RecyclerView.ViewHolder{
//once we create it once the reclycer view will automatically recycle it
private TextView answerText;
private TextView answerId;
private TextView mVotes;
private LikeButton mLikeButton;
public ViewQuestion (View itemView){
super(itemView);
//here we are finding the views by their ID
answerText=(TextView)itemView.findViewById(R.id.answerText);
answerId=(TextView)itemView.findViewById(R.id.answerId);
mVotes=(TextView)itemView.findViewById(R.id.VoteTextView);
mLikeButton= (LikeButton)itemView.findViewById(R.id.heart_buttons);
mLikeButton.setOnLikeListener(new OnLikeListener() {
#Override
public void liked(LikeButton likeButton) {
Voting vote = new Voting();
vote.onUpVote(convertToString(),
getAdapterPosition(),ViewQuestion.this);
System.out.print("Adapter Position"+getAdapterPosition());
}
#Override
public void unLiked(LikeButton likeButton) {
Voting onDown=new Voting();
onDown.onDownVote(convertToString(),
getAdapterPosition(), ViewQuestion.this);
}
});
}
public String getVoteView(){
String voteView=mVotes.getText().toString();
return voteView;
}
public String convertToString(){
String converted=answerId.getText().toString();
return converted;
}
public int convertToInt(){
String converted=answerId.getText().toString();
int ConvertedInt=Integer.parseInt(converted);
return ConvertedInt;
}
}
}
When the data that is to be set in RecyclerView is changed, the Adapter needs to get notified of the data change so that it can change the data in recyclerview.
The method
notifyItemRangedChanged(fromIndex,toIndex);
is used to notify the adapter that some set of data is changed among the whole data and it tells the adapter that adapter should refresh the data and reload it into the recyclerView starting from fromIndex to toIndex as passed into the method .
use this method if you have multiple data changed but not all , those changed data also are in cluster so that you can say from 5th to 10th index data are changed .
If all data are changed call :
notifyDataSetChanged();
if only one dataItem is changed then call :
notifyItemChanged(dataPosition);
Using notifyItemRangeChanged(0, this.data.size()) it’s bad practice.
Best way is using notifyItemChanged or notifyItemRangeChanged with payload.
Payload - optional parameter (key). That give you opportunity to check what kind of update do you need.
public void onBindViewHolder(/*...*/, List payloads) {
if (payloads.isEmpty()) {
setText(holder, position);
downloadBitmap(holder, position);
} else if (payloads.contains(SET_ONLY_TEXT)){
setText(holder, position);
}
}
In this example payloads used for checking when adapter should update only the text.
in your case you are not doing it(notifyItemRangeChanged) right as you might as well can call notifyDataSetChanged(); because you are telling the adapter that the entire list has changed and not specific position.
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;
}
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 have created my own adapter which extends BaseAdapter implements Filterable.
1. I am occasionally getting index out of bounds error, in getView method:
private ArrayList<ResultHolderData> originalData;
private ArrayList<ResultHolderData> arrayList;
private LayoutInflater inflater;
private ArrayList<ResultHolderData> suggestions;
public static class ResultHolderData {
public String symbol;
public String fullName;
public ResultHolderData(String a, String b) {
symbol=a;
fullName=b;
}
}
public static class ResultHolder {
public TextView symName;
public TextView symNameFull;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ResultHolder rh;
if(convertView==null) {
rh=new ResultHolder();
convertView=inflater.inflate(R.layout.two_line_dropdown_item, null);
rh.symName=(TextView) convertView.findViewById(R.id.autocompleteSym);
rh.symNameFull=(TextView) convertView.findViewById(R.id.autocompleteName);
convertView.setTag(rh);
} else {
rh=(ResultHolder) convertView.getTag();
}
//rh.symName.setTextColor(Color.GREEN);
/***THE BELLOW LINE THROWS THE ERROR***/
rh.symName.setText(arrayList.get(position).symbol);
rh.symNameFull.setText(arrayList.get(position).fullName);
//rh.symName.setText(arrayList.get(position));
return convertView;
}
The arrayList represents the filtered resultSet:
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint,FilterResults results) {
if(results.count>0 && results!=null) {
arrayList=(ArrayList<ResultHolderData>) results.values;
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
The error occurs sometimes, when you have 2 items in dropdown suggestions and when typing another letter causes the dropdown to only suggest one item. Then it says: Invalid index 1, size is 1. Or size is 0..
My opinion: It usually happens when i am typing in fast, so i assume that NotifyDataSetChanged is in progress, but the publishResults changes the content of the arrayList and this causes the error. But then again i would expect this to happen in more situations?
2. Also another error pops out: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread.
And the only place where i change the contents of adapter is in publishResults? Why is this happening then?
Its obvious i got something wrong here, or i dont completely understand how this works.
Since i posted the same question twice when i despair, here is the answer i gave: Errors with custom BaseAdapter for AutoCompleteTextView(indexOutOfBounds & content changed but no notification)
The problem was this line in publishResults:
arrayList=(ArrayList<ResultHolderData>) results.values;
which just pointed arrayList to those results, instead i made a "shallow copy" and cleared the list before it:
arrayList.clear();
for(ResultHolderData tempRhd : (ArrayList<ResultHolderData>)results.values)
arrayList.add(tempRhd);
and problem solved!