Fragment ListView adapter notifyDataSetChanged() from MainActivity - android

First of all please correct me if my ideology is incorrect, it is my first time working with fragments and not that good at ListView adapters.
I am currently using DrawerLayout which consist of fragments as you'd expect, I have a FAB which upon clicking will add a new entry to the SQLite db entry and will refresh the ListView (data pulled from the db).
My thinking was that I could get the ListView adapter (which is in Profiles fragment class) in the ActivityMain (as the FAB is not part of the fragment and will have different actions depending on current fragment) and then call .notifyDataSetChanged()
The Profiles fragment class
public class Profiles extends Fragment{
public ProfileListAdapter adapter;
(...)
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DBHandler db = new DBHandler(getActivity().getBaseContext());
adapter = new ProfileListAdapter(getActivity().getBaseContext(), db.getAllProfiles());
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
ListView profileListView = (ListView)view.findViewById(R.id.profileListView);
profileListView.setAdapter(adapter);
}
ProfilesListAdapter (from here)
public class ProfileListAdapter extends ArrayAdapter<Profile> {
private static class ViewHolder {
TextView name;
}
public ProfileListAdapter(Context context, ArrayList<Profile> profileList) {
super(context, R.layout.profile_row, profileList);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
Profile profile = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
ViewHolder viewHolder; // view lookup cache stored in tag
if (convertView == null) {
// If there's no view to re-use, inflate a brand new view for row
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.profile_row, parent, false);
viewHolder.name = (TextView) convertView.findViewById(R.id.profileListRowValue);
// Cache the viewHolder object inside the fresh view
convertView.setTag(viewHolder);
} else {
// View is being recycled, retrieve the viewHolder object from tag
viewHolder = (ViewHolder) convertView.getTag();
}
// Populate the data into the template view using the data object
viewHolder.name.setText(profile.name);
// Return the completed view to render on screen
return convertView;
}
}
Inside the DBHelper class
// Getting All Profiles
public ArrayList<Profile> getAllProfiles() {
ArrayList<Profile> profileListItems = new ArrayList<Profile>();
// Select All Query
String selectQuery = "SELECT * FROM " + TABLE_PROFILE;
SQLiteDatabase db = this.getWritableDatabase();
Cursor cursor = db.rawQuery(selectQuery, new String[] {});
// looping through all rows and adding to list
if (cursor.moveToLast()) {
do {
Profile profile = new Profile();
profile.setId(Integer.parseInt(cursor.getString(0)));
profile.setName(cursor.getString(1));
// Adding contact to list
profileListItems.add(profile);
} while (cursor.moveToPrevious());
}
// return contact list
cursor.close();
return profileListItems;
}
And this is what I am trying to do upon FAB click controlled in MainActivity, magic number is just an int which starts at 0 and increments by one for me to test the db / ListView easier.
db.addProfile(new Profile("Test Profile " + magicNumber));
magicNumber++;
FragmentManager fm = getSupportFragmentManager();
Profiles fragment = (Profiles) fm.findFragmentById(R.id.fragmentProfiles);
//I gave the fragment xml main FrameLayout an ID
fragment.updateListView();
Inside the Fragment class
public void updateListView(){
adapter.notifyDataSetChanged();
}
I also tried returning the adapter from the Profiles fragment class which I feel is the wrong way to tackle this problem.
public ProfileListAdapter getAdapter(){
return adapter;
}
I always end up with
java.lang.NullPointerException: Attempt to invoke virtual method 'void layout.Profiles.updateListView()' on a null object reference
or
java.lang.NullPointerException: Attempt to invoke virtual method 'com.xxxxx.xxxxx.appName.ProfileListAdapter layout.Profiles.getAdapter()' on a null object reference
To my noobish eye it seems that the adapter cannot be access from outside the fragment Profiles class, not sure if that's what it is - even if so not sure how to work around this. Let me know if any other code / logcat is required. Thank you!

For anyone coming across this question, the solution I found was here: notifyDataSetChange not working from custom adapter (top answer worked well).
Although I did implement CodeCody's idea of putting the FABs in individual fragments I can safely revert to as it was earlier, not sure which option is better.

Related

How to build an Arraylist of objects from toggle OnClickListeners inside RecyclerView Adapter's items

I'm building an Android app of media, and trying to add a Playlist feature to it, the user will be able to create a playlist of his own and modify it.
I'm using a RecyclerView to show the user list of songs which he can choose from.
The problem is I don't understand how to pass the Arraylist of chosen songs from the adapter to the fragment.
I've tried to use the Observer pattern but the don't know how to use that information.
This is my Fragment for creating the playlist:
public class CreatePlaylistFragment extends Fragment implements PlaylistAdapterInterface {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_create_playlist, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ArrayList<ProgramsData> dataArrayList = ProgramsReceiver.getPrograms();
ArrayList<ProgramsData> sortedList = new ArrayList<>(dataArrayList);
adapter = new CreatePlaylistAdapter(dataArrayList, view.getContext(), this);
adapter.adapterInterface = this;
ivCreatePlaylist.setOnClickListener(v -> {
Toast.makeText(v.getContext(), "Creating Playlist!", Toast.LENGTH_SHORT).show();
new PlaylistsJsonWriter(playlistArrayList,getContext()).execute();
});
}
#Override
public void OnItemClicked(ArrayList<ProgramsData> programs) {
programsToCreate = programs;
String s = etListName.getText().toString();
playlistArrayList.add(new Playlist(s, programsToCreate));
}
}
This is the Recycler Adapter with ViewHolder as inner class:
public class CreatePlaylistAdapter extends RecyclerView.Adapter<CreatePlaylistViewHolder> {
List<ProgramsData> programsDataList;
Context context;
public PlaylistAdapterInterface adapterInterface = null;
public CreatePlaylistAdapter(List<ProgramsData> programsDataList, Context context , PlaylistAdapterInterface adapterInterface) {
this.programsDataList = programsDataList;
this.context = context;
this.adapterInterface = adapterInterface;
}
#NonNull
#Override
public CreatePlaylistViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.chose_program_to_playlist_item, viewGroup, false);
return new CreatePlaylistViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CreatePlaylistViewHolder holder, int i) {
ProgramsData programsData = programsDataList.get(i);
holder.tvProgramName.setText(programsData.getProgramName());
if (programsData.getStudentName() != null)
holder.tvStudentName.setText(programsData.getStudentName());
else holder.tvLine.setText(""); //if there is no student the line won't be printed
holder.ivProfilePic.setImageResource(programsData.getProfilePic());
holder.programsData = programsData;
// holder.mAdapterInterface = adapterInterface;
adapterInterface.OnItemClicked(holder.programs);
}
#Override
public int getItemCount() {
return programsDataList.size();
}
}
class CreatePlaylistViewHolder extends RecyclerView.ViewHolder {
TextView tvProgramName;
TextView tvStudentName;
TextView tvLine;
CircleImageView ivProfilePic;
ToggleButton tbCheck;
ProgramsData programsData;
ArrayList<ProgramsData> programs;
PlaylistAdapterInterface mAdapterInterface;
public CreatePlaylistViewHolder(#NonNull View itemView) {
super(itemView);
tvProgramName = itemView.findViewById(R.id.tvProgramName);
tvStudentName = itemView.findViewById(R.id.tvStudentName);
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
tvLine = itemView.findViewById(R.id.tvLine);
tbCheck = itemView.findViewById(R.id.tbCheck);
programs= new ArrayList<>();
tbCheck.setOnClickListener(v -> {
if (tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_radio_button_checked);
programs.add(programsData);
} else if (!tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_check);
programs.remove(programsData);
}
});
}
}
And this is the interface for the Observer Pattern:
public interface PlaylistAdapterInterface {
void OnItemClicked(ArrayList<ProgramsData> programs);
}
I know it's a lot of code, but I just don't understand how to pass the data from the adapter back to the fragment...
I don't understand exactly what are you trying to do.
The code contains several errors that I'll try to explain.
A clear error that you have made stays in onBindViewholder where you call the listener at the creation of every item instead than after clicking on it.
You have simply add an onClickListener in the viewHolder.getItemView() or in a specific view of the viewholder and then perform the operation you need to do once an item is clicked.
If you set a listener inside onBindViewHolder, you also have a method called
holder.getAdapterPosition() that you can use to understand which item are you clicking on.
The viewholder should be used only to setup the views accordingly to the data you are binding and nothing else. For this reason, you should not pass any object or listener to it and instead use the approach above.
If you have just to retrieve the selected songs after an user confirms it's playlist you can just add a public method on your adapter
public List<ProgramsData> getSelectedSongs()
that you can call from your fragment when an user click a confirm button.
In order to have a list of all selected song, you can have another list
ArrayList<ProgramsData> selectedPrograms;
that you are going to fill after the click.
The content of the listener inside the onBindViewHolder could be
ProgramsData currentProgram = programs.get(holder.getAdapterPosition());
if(selectedPrograms.contains(currentProgram){
selectedPrograms.remove(currentProgram);
}else{
selectedPrograms.add(currentProgram);
}
notifyItemChanged(holder.getAdapterPosition); //You can use this to update the view of the selected item
Then inside the onBindViewHolderMethod you can check whether the items you are binding are part of the selectedList and update the views accordingly.
You can use callback method. Maintain list of selected items in array list and send back to fragment when done button is clicked or any other button you have placed for complete action.
Follow these steps
-Create an Interface with list parameter.
-Fragment should implement this interface.
-Then when you initialize Recyclerview adapter pass this interface object.
-When done is clicked call overridden method of this interface and send selected songs list as argument.

Android - notifyDataSetChanged not working with Custom View

I have a list of questions, and on each item there is a yes and no checkbox. This is created using an abstract class (because there are lots of lists), a child class, and the array adapter. Here is the abstract class code creating the list:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
List<Question> questions = getQuestions(1L);
setContentView(R.layout.activity_questions);
items = (ListView) findViewById(R.id.items);
adapter = new QuestionsAdapter(this, getCurrentContext(), questions, 1L, getDbData());
items.setAdapter(adapter);
}
Here is the QuestionsAdapter:
public View getView(int position, final View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
row = inflater.inflate(R.layout.row_questions, parent, false);
holder = new QuestionHolder();
holder.question = (TextView) row.findViewById(R.id.question);
holder.yes = (CheckBox) row.findViewById(R.id.yes);
holder.no = (CheckBox) row.findViewById(R.id.no);
row.setTag(holder);
} else {
holder = (QuestionHolder) row.getTag();
}
Question question = questions.get(position);
holder.question.setText(question.getQuestion());
setStateCheckboxes(holder, question);
holder.yes.setTag(getItem(position));
holder.no.setTag(getItem(position));
holder.yes.setOnCheckedChangeListener(listen);
holder.no.setOnCheckedChangeListener(listen);
return row;
}
I have to create the holder to be able to have a listview with checkboxes. Up to this point, everything is working fine.
I then have a view for each element on the list. It is very basic:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dbData = new DbData(this);
this.setContentView(R.layout.single_question);
CheckBox yes = (CheckBox) findViewById(R.id.yes_single);
CheckBox no = (CheckBox) findViewById(R.id.no_single);
}
In this view I can change the status of the checkboxes. This change is reflected in the db, but when I return to the main list, it is only reflected on refresh. I have overridden the onRestart():
#Override
protected void onRestart() {
// Change this
questions = getQuestions(1L);
adapter.notifyDataSetChanged();
super.onRestart();
}
The adapter is pulling the data from the questions ArrayList, so I am repolling it and the notifying the adapter that the data has changed, but this does not change my view. If I refresh the view, then the current state of everything is updated. I know this is a long question, but any help would be appreciated.
Use the onResume() method to call through to notifyDataSetChanged.
You will need code to manage whether the adapter has been created. You don't want to be calling it too early in the process or you run the risk of exceptions.
Generally I do this through a method that is responsible for initialising the adapter and adding it to the listview, which makes it safer to call this irrespective of whether the activity/fragment is starting for the first time or being returned to from another activity (e.g. through the back button), as well as avoiding recreating the adapter or replacing an existing adapter on the listview.
Also better to call the super.onRestart() before your own code.

Updating ArrayAdapter issues

I have a weird situation with a custom ArrayAdapter.
When I try to update the adpater with new data, instead of the data being updated, the new data are inserted to the beginning of the listview and the old data are remaining and visible once you scroll the listview.
UPDATE
It seems that the problem is caused by the ArrayList from the fragment bundle.
If I don't set the listview in the onCreateView from the fragment bundle, my update code works fine, but now I'm puzzled why this:
ArrayList<Collection> cityStoresList = fragmentBundle.getParcelableArrayList("stores");
mStoresList.addAll(cityStoresList);
is causing the items to always remain on the list?
END OF UPDATE
Here are parts of the code: (Collection is a custom object model class)
ArrayList<Collection> mStoresList = new ArrayList<Collection>();
/** List Adapter */
private StoresListAdapter mListAdapter;
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
boolean attach = false;
if (container == null) {
attach = true;
}
Bundle fragmentBundle = getArguments();
ArrayList<Collection> cityStoresList = fragmentBundle.getParcelableArrayList("stores");
mStoresList.addAll(cityStoresList);
//inflater code not added here, but is present
mListAdapter = new StoresListAdapter(getActivity(), mStoresList);
mListView.setAdapter(mListAdapter);
return layout;
}
My custom adapter is as follows:
public class StoresListAdapter extends ArrayAdapter<Collection> {
public StoresListAdapter(Context c, ArrayList<Collection> array) {
super(c, 0, array);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// View from recycle
View row = convertView;
// Handle inflation
if (row == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
row = inflater.inflate(R.layout.row_store, null);
}
// Get the Store
Collection store = getItem(position);
//rest of code follows
return row;
}
}
Now when I want to update my adapter I use the following:
public void updateAdapter(ArrayList<Collection> storesList, final int listIndex) {
mStoresList.clear();
mStoresList.addAll(storesList);
mListAdapter.notifyDataSetChanged();
}
And this creates the issue I mentioned. The new items appear fine, but the previous ones are still visible and added after the new ones.
It's like adding the new items in the ArrayList as the first items, instead of just replacing the old ones.
Any ideas, suggestions?
Ok, finally found the problem.
Because the whole thing is within a fragment, the oncreateView is actually called when I'm attaching the array, so what happens is that my updateAdapter method is called, the items are added and displayed, before the view is actually visible.
Then the oncreateView method is fired and the original bundle items are being added to the Arraylist....

ListView, and ArrayAdapter issue, How do I proceed?

I have a Product Class, Which has three fields:-
id
name
price
In my code I create a List<Product> productList = Collections.synchronizedList(new ArrayList<Product>());
This product list is used to create a ArrayAdapter<Product> adapter = ArrayAdapter<Product>(), I fill the productList in a separate thread and adapter is notified accordingly. It is working absolutely fine.
Now,
I want to change the color of the some specific products (say for price < 1000).
Each row of ListView should contain 4 elements product image,name, desc and price.
When User clicks the Product, in a context menu options i.e. buy Product, View Product should be displayed.
I have read few blogs and threads related to that. Still I cant decide where to begin, I read about the customization of the ArrayAdapter, overriding getView(), custom list filters etc. Which way will be the best for my requirement... in other words How can custom adapters and list filters benefit me ?
You should extend BaseAdapter and provide your own layout for each item (getView()). Don't forget to manage the view recycling and maybe use the ViewHolder paradigm.
EDIT
I didn't use a lot the ListAdpater, because it binds to a ListView only. Sometimes I need an adapter for a GridView, and the BaseAdapter gives me enough freedom for all use cases.
Example of BaseAdapter:
public class FanAdapter extends BaseAdapter {
private List<Fan> mFans;
private Activity mContext;
public FanAdapter(Activity context, List<Fan> fans) {
mContext = context;
mFans = fans;
}
private class ViewHolder {
public ImageView image;
public TextView firstName;
public TextView lastName;
}
#Override
public View getView(int position, View view, ViewGroup container) {
if (view == null) {
view = LayoutInflater.from(mContext).inflate(R.layout.fan_item, container, false);
}
ViewHolder viewHolder = (ViewHolder) view.getTag();
if(viewHolder == null){
viewHolder = new ViewHolder();
viewHolder.image = (ImageView) view.findViewById(R.id.image);
viewHolder.firstName = (TextView) view.findViewById(R.id.firstname);
viewHolder.lastName = (TextView) view.findViewById(R.id.lastname);
view.setTag(viewHolder);
}
// setting here values to the fields of my items from my fan object
viewHolder.firstName.setText(fan.getFirstName());
(...)
return view;
}
#Override
public int getCount() {
if (mFans != null) {
return mFans.size();
} else {
return 0;
}
}
#Override
public Object getItem(int position) {
return mFans.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
}
You can use it with an Activity containing a ListView or a ListActivity (having in its layout a ListView with a special id):
<ListView
android:id="#id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="#android:color/transparent" />
This way, your ListActivity that will inflate the view will be able to make a findViewById() call and getListView() will return this internal listView. It's a small hack, you can put your own listView with another id and make the findViewById() yourself. For The ListActivity, there's another hack: if the ListActivity finds an empty view with again a special id, it will be shown when the list is empty:
<include
android:id="#+id/empty"
layout="#layout/empty"
android:visibility="gone"
android:layout_gravity="center" />
Then on your listView, whether you used an Activity or ListActivity, you can set your adapter on the ListView:
getListView().setAdapter(new FanAdapter(this, myFanDataArray)));
in getView(...) method you have to check price and set color of row...
see this customized listview..
http://samir-mangroliya.blogspot.in/p/android-customized-listview.html
i set row color as per odd and even row and
you can set checking price...
if(price < 1000){
row.setBackgroundColor(Color.RED);
}else{
row.setBackgroundColor(Color.Yellow);
}

Android: ListActivity containing custom rows and invoking ContextMenu

I have a ListView in a LitsActivity consisting of rows which are inflated from a separate XML file. The rows are populated by convertView method in my custom adapter for this ListView. I'm trying to invoke a context menu on each row. Normally, we do this by calling
registerForContextMenu(ourListViewInstance);
in onCreate method. But it doesn't work for me, onCreateContextMenu method is not called because there is no list rows at this point, they appear a bit later. I tried to use
registerForContextMenu(row);
in getView method of my custom list adapter so that each row gets registered for "long clicks" and it works, but for some reasons it's inacceptable and the usual way is required.
This is my ItemsAdapter which creates instances of ListView rows:
class ItemsAdapter extends ArrayAdapter<ItemsModel> {
public ItemsAdapter(ArrayList<ItemsModel> list) {
super(Items.this, R.layout.custom_row_view, list);
}
private ItemsModel getModel(int position) {
return (((ItemsAdapter) itemsList.getAdapter()).getItem(position));
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View row = convertView;
final ItemsModel currentItemModel = getModel(position); // Model class storing data for all the rows.
ItemsResourceManager resourceManager = null; // class used to easily get and set row views.
if (row == null) {
row = View.inflate(getBaseContext(), R.layout.custom_row_view, null);
row.setClickable(true);
row.setFocusable(true);
row.setBackgroundResource(android.R.drawable.menuitem_background);
resourceManager = new ItemsResourceManager(row);
row.setTag(resourceManager);
} else {
resourceManager = (ItemsResourceManager) row.getTag(); //class used to easily get and set row views.
}
registerForContextMenu(row); // works for each separate LisView row
//... skipped setText actions for this row
}
return row;
}
}
Also I tried to completely clean up my custom_row_view.xml from any focusable elements but it didn't help.
The issue was in the onClickListener in my custom adapter class. It was preventing contextMenu handling because the "short" click was invoked every time. Now both clickListener and ContextMenu handler are located in ListActivity class and it works just fine.

Categories

Resources