I am using the RealmRecyclerView from this post:https://realm.io/news/android-realm-listview/
In this ToDo app, when item is swiped, it is automatically deleted from RecyclerView as well as Realm Database.
I want to get notified when an item is deleted, also which item is deleted so I can perform some action with that item.
I tried using the Realm Change Listener, but that is invoked every time when a Realm Transaction is committed. So it is invoked even when a new item is added.
How do I do this? Is it possible with normal RecyclerView?
At this moment (with version V1.1.0), Realm does not provide a callback when a RealmObject is deleted. This is true for all types of data/views (listView, RecyclerView, RealmObject, RealmResults etc), also true if you query data asObservable or use a change listener.
However it does make sense to send an empty object, when the queried object is deleted from Realm, but since its a breaking change in Realm, we will have to wait for V2.0.
More details - https://github.com/realm/realm-java/issues/3138
According to the answer, they have not yet added this feature. But if you want to achieve this, you can use normal ReacyclerView using the existing RealmAdapter and everything as it is.
Here's how to do it:-
Remove the RealmRecyclerView and add the normal RecyclerView:-
1. Add the normal RecyclerView from the support library.
2. Initialize the recyclerview with the existing adapter i.e the adapter class that extends RealmBasedRecyclerViewAdapter, no need to make a new adapter
recyclerView=(RecyclerView)findViewById(R.id.realm_recyeler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new FilterAdapter(this,results,true,true);
recyclerView.setAdapter(adapter);
3. Next, we will use the ItemTouchHelper class to implement the swipe to dismiss for the RecyclerView :-
ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView,RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
adapter.remove(viewHolder.getAdapterPosition(),alarmintent);
}
};
I've made a method in my adapter to remove item(shown below), you can do it here as well
The viewHolder.getAdapterPositon() gives the position of item swiped, it is passed to delete the RealmObject from Realm DB at given position(shown below)
0 -> drag flag - since I am not implement drag to move items, I've kept it as zero
ItemTouchHelper.RIGHT - swipe flags - These say in which direction the swipe to dismiss is set
ItemTouchHelper.RIGHT - swipe in right direction to dismiss
ItemTouchHelper.LEFT - swipe in left direction to dismiss
To support both directions, pass- ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);
Create a new ItemTouchHelper object with above callback
Attach the ItemTouchHelper to RecyclerView
4. Here's how to remove the item (below is the code of my remove method of adapter):-
public void remove(int position)
{
RealmConfiguration configuration = new RealmConfiguration.Builder(context).deleteRealmIfMigrationNeeded().build();
realm = Realm.getInstance(configuration);
realm.beginTransaction();
realmResults.deleteFromRealm(position);
realm.commitTransaction();
notifyItemRemoved(position);
}
deleteFromRealm method is used to delete item at given position
call the notifyItemRemoved(position) to indicate item is removed at specified position
5. That's it, very easy and no need to create new adapters etc.
While Realm may not provide a callback for when an item is deleted, there is a way to know when items are deleted when using the RealmRecyclerView from this post.
The adapter (RealmBasedRecyclerViewAdapter) will call onItemSwipedDismiss(int position) whenever an item is swiped for dismissal. In your subclass of this adapter, you can override this method to add some extra logic.
For example, in my Recycler View, I want to give users the option to Undo a deletion. So, I override onItemSwipedDismiss(int position) and access the fields of the object being deleted. (In my case, this object is fairly small -- only three fields -- so this isn't too unwieldy). Then I call the super method: super.onItemSwipedDismiss(position); which will animate the deletion and remove it from Realm.
Then, I create a Snackbar with an action that re-creates the Realm object from the saved fields. Once it's created, it immediately goes back into the recycler view.
Here's a skeleton of the implementation of this method override:
#Override
public void onItemSwipedDismiss(int position) {
// Gather the object's fields, if you want:
YourObject objectToDelete = realmResults.get(position);
final String title = objectToDelete.getTitle();
final long timestamp = objectToDelete.timestamp;
// Perform delete and animation:
super.onItemSwipedDismiss(position);
// Add code here depending on what you want to do
// (for example, you could add a Snackbar that undoes
// the deletion by "resurrecting" your deleted object)
}
Related
I need to refresh / rebind ListView or RecyclerView contents without refreshing the header item itself.
Any tips on how to achieve this?
Thanks.
Yes, you can do this. Generally, your header has 0 position in the list, so the header places on the top of your list. So, for your lists, e.g. RecyclerView you must initialize the adapter (in case of RecyclerView you must create accessor of RecyclerView.Adapter class) and this adapter has a lot of methods for updating data in adapter (notifyDataSetChanged(), notifyItemInserted() etc.) and you can use one of this methods, depends on your purpose. So, in your case you can use notifyItemRangeChanged(int positionStart, int itemCount).
You can find more information about these methods in the official documentation
Assuming the header is at position 0:
Lets say you want to bind the header once and then stop binding it after refresh:
When you call notifyDataSetChanged() to reload, the onBindViewHolder() method in the adapter gets called again to refresh the data, keeping that in mind. You can set a boolean so that you bind your header once. So that eventhough the onBindViewHolder() is called multiple times the header would bind once.
class Adapter extends ...........{
//use a boolean as a flag
private boolean bindHeader = true;
........
.......
.......
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
..........
if(position == 0 && bindHeader){
//bind the header only for the first time
......
......
//then stop binding after refresh
bindHeader = false;
}else if(position > 0){
//bind the reset of the items here
}
}
..............
..............
}
I have a problem using FirebaseRecyclerAdapater, at first it was working fine but now this adapter is firing twice. The database reference is only referring one child, but it is always firing twice. The Toast with text "counter" will appear twice
FirebaseRecyclerAdapter<RequestVisit, RequestViewHolder> requestAdapter =
new FirebaseRecyclerAdapter<RequestVisit, RequestViewHolder>(
RequestVisit.class,
R.layout.seekerrequests_layout,
RequestViewHolder.class,
requestDatabase.child("2DBwmhGplGMoAlLy6337HZEShi93")
) {
#Override
protected void populateViewHolder(final RequestViewHolder viewHolder, RequestVisit model, int position) {
Toast.makeText(getContext(), "counter" +
viewHolder.getAdapterPosition(), Toast.LENGTH_SHORT).show();
}
};
requestVisitList.setAdapter(requestAdapter);
A Firebase*Adapter shows a list of items, the child nodes under the location that you attach it to.
If populateViewHolder gets called with two different positions, that means there are two children under requestDatabase.child("2DBwmhGplGMoAlLy6337HZEShi93").
Keep in mind that if 2DBwmhGplGMoAlLy6337HZEShi93 is a child node with two properties, then your approach will call populateViewHolder for each of those properties.
If you want to show only a single item in the RecyclerView, you can create a simple query with:
FirebaseRecyclerAdapter<RequestVisit, RequestViewHolder> requestAdapter =
new FirebaseRecyclerAdapter<RequestVisit, RequestViewHolder>(
RequestVisit.class,
R.layout.seekerrequests_layout,
RequestViewHolder.class,
requestDatabase.orderByKey().equalTo("2DBwmhGplGMoAlLy6337HZEShi93")
)
I have a simple recyclerview with items (tips) and a loading spinner at the bottom.
here's how the item count and item view type methods look:
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) { // last position
return LOADING_FOOTER_VIEW_TYPE;
}
else {
return TIP_VIEW_TYPE;
}
}
#Override
public int getItemCount() {
return tips.size() + 1; // + 1 for the loading footer
}
basically, i just have a loading spinner under all my items.
I create the adapter once like so:
public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
this.tipsActivity = tipsActivity;
this.tips = tips;
}
and then once i have fetched additional items, i call add like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeInserted(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeChanged(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
Nor does it happen if i simply call notifyDataSetChanged() like so:
public void addTips(List<Tip> tips) {
this.tips.addAll(tips);
notifyDataSetChanged();
}
Here's the code for setting the adapter in my Activity:
public void setAdapter(#NonNull ArrayList<Tip> tips) {
if (!tips.isEmpty()) { // won't be empty if restoring state
hideProgressBar();
}
tipsList.setAdapter(new TipsListAdapter(this, tips));
}
public void addTips(List<Tip> tips) {
hideProgressBar();
getAdapter().addTips(tips);
restorePageIfNecessary();
}
private TipsListAdapter getAdapter() {
return (TipsListAdapter) tipsList.getAdapter();
}
Note:
I don't manually set scroll position anywhere.
I call setAdapter() in onResume()
addTips() is called after I fetch items from the server
Let me know if you need any additional parts of my code.
This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.
When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.
On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.
On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.
My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.
Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);
Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..
So either you setAdapter in onCreate using new operator to create reference for adapter or,
in onResume use mAdapter field variable reference..
I know there are lot of ways to have an empty view for a RecyclerView. But my question is for FirebaseRecyclerView.
My layout is:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<ProgressBar
android:id="#+id/feed_loading"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
So I am showing Loading ProgressBar before the RecyclerView fills its items. Now what if server doesn't have any item. In this situation my RecyclerView is empty always and my Loading ProgressBar always visible to user.
So instead of showing the ProgressBar for indefinite period, I want to show some empty layout. E.g. "No Data Found" or something similar message.
The final result should be: ProgressBar should be shown until data is loaded to RecyclerView and once data is loaded ProgressBar should be invisible. But if no data is present in server, some empty layout should be shown instead of ProgressBar.
In normal RecyclerView, we have a dataset (some ArrayList, etc) and if it is empty then we can show that empty layout. But in case of FirebaseRecyclerAdapter, I dont have the reference of Snapshot in my Activity or Context. Nor I have any callback which tells me that no data is present in server.
Any workaround will help a lot.
Here is what I would try. First check out the accepted answer to the question linked below. It provides some very good insight into how Firebase queries work. I'd consider the info trusted since the answer is by someone on the Firebase team:
How to separate initial data load from incremental children with Firebase?
So, based on the answer to the question linked above and the fact that the FirebaseRecyclerAdapter is backed by a FirebaseArray which is populated using a ChildEventListener I would add a Single value event listener on the same database reference used to populate your FirebaseRecyclerAdapter. Something like this:
//create database reference that will be used for both the
//FirebaseRecyclerAdapter and the single value event listener
dbRef = FirebaseDatabase.getInstance().getReference();
//setup FirebaseRecyclerAdapter
mAdapter = new FirebaseRecyclerAdapter<Model, YourViewHolder>(
Model.class, R.layout.your_layout, YourViewHolder.class, dbRef) {
#Override
public void populateViewHolder(YourViewHolder holder, Model model, int position){
//your code for populating each recycler view item
};
mRecyclerView.setAdapter(mAdapter);
//add the listener for the single value event that will function
//like a completion listener for initial data load of the FirebaseRecyclerAdapter
dbRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
//onDataChange called so remove progress bar
//make a call to dataSnapshot.hasChildren() and based
//on returned value show/hide empty view
//use helper method to add an Observer to RecyclerView
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
That would handle the initial setup of the RecyclerView. When onDataChange is called on the single value event listener use a helper method to add an observer to the FirebaseRecyclerAdapter to handle any subsequent additions/deletions to database location.
mObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
//perform check and show/hide empty view
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
//perform check and show/hide empty view
}
};
mAdapter.registerAdapterDataObserver(mObserver);
Answer from: https://stackoverflow.com/a/40204298/2170278
the FirebaseUI adapters nowadays have an onDataChanged() method that you can override to detect when they're done loading a set of data.
See the source code on github. From there:
This method will be triggered each time updates from the database have been completely processed. So the first time this method is called, the initial data has been loaded - including the case when no data at all is available. Each next time the method is called, a complete update (potentially consisting of updates to multiple child items) has been completed.
You would typically override this method to hide a loading indicator (after the initial load) or to complete a batch update to a UI element.
The FirebaseUI sample app overrides onDataChanged() to hide its "loading" indicator:
public void onDataChanged() {
// If there are no chat messages, show a view that invites the user to add a message.
mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
I have implemented my RecyclerView and even added an onscrolllistener to support infinity scrolling and now I'm stuck with a, hopefully, easy problem: How can I add the newly loaded data to the existing dataset?
My current approach: I create a new array with the length of the existing dataset + the length of the newly loaded data. I System.arraycopy my existing dataset and add the new content with a for-loop.
This works but the list is always reset (scrolls back to the top) and I assume my way to add additional content is overly complicated/wrong, though the tutorials I have looked at seem to pass over this "detail".
Update: I'm currently calling "scrollToPosition" on the UI-Thead after the data has been loaded, but I doubt this is the correct way of doing this or am I wrong?
You shouldn't be adding stuff to your dataset, you will sooner or later run out of memory. What you can do is return a big number (I used Short.MAX_VALUE) item in getItemCount inside your adapter and in the method that requests a view for postion you should do position % list.size();
It is not a truly endless RecyclerView this way, but good enough. I will paste some code tomorrow, I don't have it here now :/
I think you have to add items inside your adapter. Let`s say
class Adapter extends Recycler.Adapter<Recycler.ViewHolder>{
List<YourCustomObject> list;
public Adapter(){
list = new ArrayList<>();
}
public void addItem(YourCustomObject item){
list.add(item);
notifyItemDateSetChanged(); //This method for adapter to notice that list size have been changed
}
// Here your views
}
There is implementation of Your fragment or Activity where you retrieve data from internet.Let` say
class MainActivity extends AppCompactActivity{
Adapter adapter = new Adapter();
List<YourCustomObjects> objects;
public void onCreateView(){
//////// Something yours
}
public void onLoadMore(){
///// Your operation to retrieve data and init it to your list objects
for(YourCustomObject object : objects){
adapter.addItem(object);
}
}
}