LiveData not updating when objects in database change - android

I have following fragment:
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_favourite_books, container, false);
...
getActivity().getFavouriteBooks().observe(this, books -> booksAdapter.setBooks(books));
return view;
}
My Activity:
public LiveData<List<Book>> getFavouriteBooks() {
return viewModel.getFavouriteBooks();
}
My ViewModel (books are fetched from database):
LiveData<List<Book>> favouriteBooks;
public MainViewModel(#NonNull Application application) {
super(application);
favouriteBooks = booksRepository.getFavouriteBooks();
}
public LiveData<List<Book>> getFavouriteBooks() {
return favouriteBooks;
}
Problem:
When I change favourite flag outside this functionality (e.g. in another fragment), favouriteBooks in adapter are not refreshed. This fragment I have in TabLayout, and favouriteBooks are refreshed only when I click on another tab one more time. Any idea how to solve this? It is very simple code and I believe LiveData should support this updating outside itself, I believe this is for what LiveData has been created in the first place. Thanks for your help.
EDIT: Moving observer from onCreateView to onViewCreated does not make any difference.

I found the solution. In setBooks method of Adapter, I need to call notifyDataSetChanged();. With that, list is refreshed properly everytime the collection changes.
public void setBooks(List<Book> books) {
this.books = books;
notifyDataSetChanged();
}

Related

Reusing layout created on Fragment.onCreateView() to avoid inflation when showing multiple times

I am using a DialogFragment to display a 'modal' bottom sheet menu (more info here: https://material.io/develop/android/components/bottom-sheet-dialog-fragment/). Since it contains a kind of context menu for the items contained in a RecyclerView, it may be shown multiple times during runtime.
However, always DialogFragment.show() is called, Fragment.onCreateView() is also called, which leads to layout inflation, which can(?) be considered as a 'heavy' task to be computed in the UI thread, which I want to avoid for performance reasons. So to avoid layout inflation every time the DialogFragment is shown, I created a ViewGroup member object pointing to the View being returned Fragment.onCreateView() in order to be reused, like this:
public class BottomMenu extends BottomSheetDialogFragment {
private ViewGroup mLayout;
private TextView mLabel;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
if (mLayout == null) {
mLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.bottom_sheet, container, false);
mLabel = mLayout.findViewById(R.id.bottom_sheet_label);
}
return mLayout;
}
#Override
public void onDismiss(#NonNull DialogInterface dialog) {
super.onDismiss(dialog);
// The view cannot be reused if it's already attached to the previous parent view
((ViewGroup) mLayout.getParent()).removeView(mLayout);
}
public void setLabel(String label) {
mLabel.setText(label)
}
}
But once used for the first time, such view must be detached from the Fragment container view to be reused (see onDismissed() overriden method on posted snippet), which seems like a nasty workaround
So I post this question to check if anyone knows a better approach to reuse the layout for the same Fragment
More details here:
public class ActivityMain extends AppCompatActivity {
private BottomMenu mBottomMenu;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
[...]
mBottomMenu = new BottomMenu();
}
#Override
public boolean onLongClick(View v) {
mBottomSheet.setLabel(label);
// The following calls onCreateView() in Fragment, so try to return
// there the previously inflated layout, if any
mBottomSheet.show(getSupportFragmentManager(), "TAG?");
return true;
}
}
It is already a nice practice as long as you don't surrender to any possible bugs.. However there are one or two things I want to let you know about resuing dialogFragment.
public class BottomMenu extends BottomSheetDialogFragment {
private ViewGroup mLayout;
private TextView mLabel;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
if (mLayout == null) {
mLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.bottom_sheet, container, false);
mLabel = mLayout.findViewById(R.id.bottom_sheet_label);
} else if(mLayout.getParent()!=null) { // it's not a lot of code. just a few lines……
((ViewGroup)mLayout.getParent()).removeView(mLayout);
}
return mLayout;
}
}
One thing is about nested fragments. When the dialogFragment hold a viewpager and the viewpager have serveral sub-fragments, you must reset the viewpager's adapter on the reusing-call of onCreateView. The reason is that after closing the dialogFragment, the old fragmentManager returned by getChildFragmentManager() is no longer valid, and it should be updated.
... onCreateView(...)
if (mLayout == null) {
...
} else {
...
viewpager.setAdapter(new MyFragmentAdapter(getChildFragmentManager(), fragments));
}
If this step is omitted, you may observe strange behaviours when reusing the dialogFragment, such as recyclerviews in the sub-fragments stop updating in response to NotifyDatasetChanged, but if you scroll it, it will update.
Another thing is that I tend to use WeakRefernce to hold the dialogFragment to be reused. I even have an array of them.
In java applications, if you don't use similar mechanism, you can see rapid surge in memory usage when the user open and close the same dialog again and again. So at least it's not a bad practice to reuse dialogs when it's necessary.

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-Realm list with strict Model/View separation

I've got a RecyclerView backed by a Realm findAll(). I use a RealmChangeListener to notify the list about updates, and everything works remarkably well given the heavy use of the blunt instrument notifyDataSetChanged().
private RealmResults<Sale> allSales;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
....
// Update sales list whenever the AllSales result changes
allSales = getRealm().where(Sale.class).findAll();
allSalesListener = new RealmChangeListener<RealmResults<Sale>>() {
#Override
public void onChange(RealmResults<Sale> results) {
saleAdapter.notifyDataSetChanged();
}};
allSales.addChangeListener(allSalesListener);
....
However, I'd really like to have good MVVC structure, keeping all the Realm code in the ViewModel and out of my Fragments. The Realm examples don't do this. And probably for good reason -- I don't see an elegant way to notify the adapter appropriately of changes in the RealmResults. Databinding isn't there yet; it doesn't seem to support backing a RecyclerView with an ObservableCollection... and even if it did, a RealmResult isn't an ObservableCollection.
At this point, I'm thinking that I need to create a "ListChangedListener" interface in my Fragment, and manually maintain a collection of listeners for every List property in my ViewModel. But that seems like an awful lot of extra code just to maintain View/Model separation.
TLDR: I'm looking for an example of a Realm-backed ListView or RecyclerView with no Realm code whatsoever in the View code. Or even just reassurance that my custom "listener" interface is a good path forward.
UPDATE: I had somehow overlooked the RealmRecyclerViewAdapter. See my answer below.
The Realm library includes a RealmRecyclerViewAdapter base class, which I had somehow overlooked. No matter how good your MVVC intentions, the Adapter can't really be divorced from the model implementation, so it may as well be one that's intended for it.
Anyhow, it is very clean and compact. Do yourself a favour and review the example.
Here's a minimalist working implementation, with Android Databinding used for the row fields to make the Adapter and ViewHolder even cleaner and simpler:
private void setUpRecyclerView() {
// Called from your onCreateView(...)
recyclerView.setLayoutManager(new LinearLayoutManager(mainActivity));
recyclerView.setAdapter(new MyRecyclerViewAdapter(mainActivity, mainActivity.getDb().serialsRR));
recyclerView.setHasFixedSize(true);
}
public class MyRecyclerViewAdapter extends RealmRecyclerViewAdapter<Serial, MyRecyclerViewAdapter.SerialViewHolder> {
private final ActivityMain activity;
public MyRecyclerViewAdapter(ActivityMain activity, OrderedRealmCollection<Serial> data) {
super(activity, data, true);
this.activity = activity;
}
#Override
public SerialViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.serial_row, parent, false);
return new SerialViewHolder(itemView);
}
#Override
public void onBindViewHolder(SerialViewHolder holder, int position) {
SerialRowBinding rowBinding = holder.getBinding();
rowBinding.setSerial(getData().get(position));
}
class SerialViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
#Getter SerialRowBinding binding;
public SerialViewHolder(View view) {
super(view);
binding = DataBindingUtil.bind(view);
}
}
}

Accessing parent fragments methods from recyclerview's adapter

Hi guys I'm trying to access few methods and variables of fragment(containing a recycler view) from the recycler views adapter class.. Simplest way is to pass in the fragment reference along with the adapter which creating it. But I dont think passing the full adapter reference which creating the adapter is a good approach.
I'm using RxJava in my project and tried a lot of things with PublishSubject like creating a Subject in adapter, calling its onNext which an event is performed and subscribe to that subject in the fragment but it didnt work out..
So any good approach will be highly appreciated.
TIA...
I'd suggest to introduce EventBus in your app - pretty elegant way of communication between different components of the app.
Then it'd look like:
Fragment:
public class MyFragment extends Fragment {
private EventBus eventBus = EventBus.getDefault();
RecyclerViewAdapter viewAdapter;
#Override
public void onAttach(Context context) {
super.onAttach(context);
eventBus.register(this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_a, container, false);
if (viewAdapter == null) {
viewAdapter = new RecyclerViewAdapter(eventBus);
}
RecyclerView recyclerView = (RecyclerView)rootView.findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(viewAdapter);
return rootView;
}
#SuppressWarnings("unused") // invoked by EventBus
public void onEventMainThread(final DataRefreshedEvent event) {
// Do something!
}
#Override
public void onDetach() {
eventBus.unregister(this);
super.onDetach();
}
}
Adapter:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
EventBus eventBus;
public RecyclerViewAdapter(EventBus eventBus) {
this.eventBus = eventBus;
}
void sentSomethingToFragment() {
eventBus.post(new DataRefreshedEvent());
}
.....
}
Event: public final class DataRefreshedEvent {}
And just a note - with Dagger, it'd look even better.
I hope, it helps
My suggestion is go with interface approach.
1. Create one interface.
2. Fragment should implement that interface.
3. Pass that interface reference to the adapter.
4. Call the interface method from adapter
So that way you can communicate between fragment and adapter.

Databinding apply for one layout used by multiple activity/fragment

I am replacing existing code by databinding. But I face a problem.
I have some layout files shared by more than one activity/fragment. E.g there is a layout file layout_sub used by SubFragmentA and its extending class SubFragmentB. And the data model used in these two fragment are not the same.
The code looks like following.
public class SubFragmentA extends Fragment {
private DataA dataA;
#Override
public View onCreateView(Bundle Bundle) {
View v = LayoutInflator.from(getActivity()).inflate(R.layout.shared_layout);
initView(v, dataA);
return v;
}
private void initView(view v, DataA dataA) {
// use dataA to init v
}
}
public class SubFragmentB extends Fragment {
private DataB dataB;
#Override
public View onCreateView(Bundle Bundle) {
View v = LayoutInflator.from(getActivity()).inflate(R.layout.shared_layout);
initView(v, dataB);
return v;
}
private void initView(view v, DataB dataB) {
// use dataB to init v
}
}
So far, I think using DataA and DataB in layout_sub file at the same time is not a good idea, because it would require a lot of redundant code to decide which object to be used.
Please share your ideas on this problem.
Finally, I got a solution. The databinding is used for MVVM pattern. That means one layout corresponds to one ViewModel. And the ViewModel contains every data for UI layout. So I should prepare one ViewModel for each layout file. And every fragment/activity should just handle the ViewModel.

Categories

Resources