Handling positions in FirebaseRecyclerAdapter - java

I have fiddled with the FirebaseRecyclerAdapter for quite some time now. It's really a great tool to populate a custom list/recycler view very fast with all of its features. However, one thing that I would like to ask is how to handle positions of items inside the adapter itself.
So for example, I want to mimic this small feature that WhatsApp has in their chats.
So, in a group chat setting, if a person sends more than one consecutive message in a row, the display name of that particular person will be invisible.
The logic behind it according to my understanding: if the person who sends the message is the same for (position - 1), then I will just make the EditText invisible for (position). This is, of course, to prevent a very long stream of text with minimum amounts of repetitive information.
Let's say the JSON tree from Firebase database is as follows.
{
"messages" : {
"pushed_id_1" : {
"message_sender" : "AppleJuice",
"message_text" : "Are you free?"
},
"pushed_id_2" : {
"message_sender" : "AppleJuice",
"message_text" : "On Saturday I mean..."
}
}
}
The FirebaseRecyclerAdapter would look like this.
FirebaseRecyclerAdapter<Message, MessageViewHolder> adapter = new FirebaseRecyclerAdapter<Message, MessageViewHolder>(Message.class, R.layout.message_item, MessageViewHolder.class, myRef) {
#Override
protected void populateViewHolder(MyBookingsViewHolder viewHolder, Booking model, int position) {
viewHolder.messageSender.setText(model.getMessage_sender());
viewHolder.messageText.setText(model.getMessage_text());
//put some code here to implement the feature that we need
}
};
messages_recycler_menu.setAdapter(adapter);
The furthest I have gone is to use getItemCount() method in the FirebaseRecyclerAdapter, but I am still unable to achieve the feature that mimics that of Whatsapp's that I was talking about previously.
Is there a method that can achieve this? Or am I missing something very important in this example?

String lastSender=null; //or some random string
FirebaseRecyclerAdapter<Message, MessageViewHolder> adapter =
new FirebaseRecyclerAdapter<Message, MessageViewHolder>
(Message.class, R.layout.message_item, MessageViewHolder.class, myRef) {
#Override
protected void populateViewHolder(MyBookingsViewHolder viewHolder, Booking model, int position) {
if (model.getMessage_sender().equals(lastSender){ //check if the current sender is same as the last sender
viewHolder.messageText.setText(model.getMessage_text()); //setting only message text
viewHolder.messageSender.setVisibility(View.GONE); //if required
}else{
lastSender=model.getMessage_sender();//updating the lastSender value
viewHolder.messageSender.setVisibility(View.VISIBLE); //if required
viewHolder.messageSender.setText(model.getMessage_sender());
viewHolder.messageText.setText(model.getMessage_text());
}
//put some code here to implement the feature that we need
}
};
messages_recycler_menu.setAdapter(adapter);

As discussed in comments:
Let's suppose I received message and stored sender's name in constant String that should be static constant in some class i.e. AppConstants so that It can be accessed everywhere therefore after that:
in populateViewHolder or in your message receiver do something like this:
if (TextUtils.isEqual(storedSender,model.getMessage_sender())){
viewHolder.messageSender.setVisiblity(View.GONE)
}
else{
// do your normal flow
viewHolder.messageSender.setVisiblity(View.VISIBLE);
storedSender = model.getMessage_sender();
}
In this way automatically the last message's sender's name will be updated , this is exactly what you were trying to achieve by adapter position!

Related

How does Android Paging Library know to load more data?

Iʼm fairly new to developing Android apps and Iʼm trying to do everything “the right way.” So right now, Iʼm implementing the new Android Paging Library into my project, where I need to load a list of articles from a network server.
I have an ArticlesRepository class that returns an ArticleList class containing instances of ArticleListItem that I would like to display in a RecyclerView. The list of articles is paginated already on the server, so the repository sends a request for the first page and returns an ArticleList with the page property set to 1 and the articles property containing a List<ArticleListItem> of articles on the requested page. I donʼt know how many articles can be on one page.
Now, I was able to implement a PageKeyedDataSource<Integer, ArticleListItem>, but it only fetches the first page:
#Override
public void loadInitial(#NonNull LoadInitialParams<Integer> params, #NonNull LoadInitialCallback<Integer, ArticleListItem> callback) {
ArticleList list = load(1);
if (list != null) {
callback.onResult(list.articles, null, next(list));
}
}
#Override
public void loadBefore(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, ArticleListItem> callback) {
ArticleList list = load(previous(params.key));
if (list != null) {
callback.onResult(list.articles, previous(list));
}
}
#Override
public void loadAfter(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, ArticleListItem> callback) {
ArticleList list = load(next(params.key));
if (list != null) {
callback.onResult(list.articles, next(list));
}
}
The previous/next functions return an Integer with the previous/next page number or null if there isnʼt one.
In my ViewModel, I configure the PagedList like this:
PagedList.Config config = new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(1)
.setPageSize(1)
.setPrefetchDistance(1)
.build();
This way Iʼm able to load the first page, but when I scroll to the bottom of the RecyclerView (that is inside a NestedScrollView), nothing happens. Debugging shows that the PageKeyedDataSource.loadAfter method is not invoked.
Do I have to somehow tell the PagedList that the next page has to be loaded, or is it the RecyclerView/DataSource/GodKnowsWhatElseʼs job and Iʼm just doing something wrong? Thanks for any advice.
The paging library should know automatically when to load new items. The problem in your implementation is that the paged RecyclerView is inside a NestedScrollView and according to this issue the libary doesn't have built in support for that.
when you put recyclerview inside an infinite scrolling parent, it will
layout all of its children because the parent provides infinite
dimensions.
You'll need to create your own implementation of Nested Scroll View, there is actually one here in this gist that might be able to help you.
It is also suggested to add fillViewPort to this custom nested scroll view:
android:fillViewport="true" to scrollable container

Merging Local Data With Firebase Database and showing it in recyclerview

I have a chatting application which is made using firebase. It works fine. But some users send slangs or stuffs like that. I use Firebase Recyclerview. My connection is direct to database. So I can't verify if the message is good or bad before data gets added.
What I want to do is, I want to send that data to some other location. Then after verification using cloud functions, I want to add it to original chat room database.
Till that step I have no problems. But when user sends a message. I dont want the user to wait until data gets updates. Cloud Functions takes 2-3 seconds for me. I want to add that data locally with a sending status and then update it when it gets updated on the chat room database.
So What's the best way to do that..
There is nothing wrong with my code. It's just basic firebase adapter..
Query query = FirebaseDatabase.getInstance()
.getReference()
.child("rooms").child("Off-Topic").child("chat")
.limitToLast(500);
firebaseOptions = new FirebaseRecyclerOptions.Builder<ChatData>()
.setQuery(query, ChatData.class)
.build();
chatAdapter = new FirebaseRecyclerAdapter<ChatData, ChatHolder>(firebaseOptions) {
#Override
protected void onBindViewHolder(ChatHolder holder, int position, ChatData model) {
}
#Override
public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.sender_text_message, parent, false);
return new ChatHolder(v);
}
};
The simplest way for you to solve this would be to add a boolean variable to your ChatData class. This variable (let's call it isGood) would be false when the message is sent and would become true when it has been updated by your Cloud Function.
So your Adapter's onBindViewHolder would be:
#Override
protected void onBindViewHolder(ChatHolder holder, int position, ChatData model) {
if(model.isGood())
holder.textView.setText(model.getMessage()); //edit this line as needed
else
holder.textView.setText("sending..."); //replace textView with the name of your actual TextView
}

Should there be any logic on the activity class?

I was recently reading about design patterns and especially about low coupling and delegation.
I was wondering, whether there should be any logic on the Activity class or if it only serves the view.
E.g. I have an activity called BattleActivity and that is supposed to work as some kind of session between two players. A lot of Push Notifications happen there, also the class works as an Observer, so there is a lot of comminication going on there.
Right now I am trying to figure out what logic could I move to a separated object(and whether I should) and then just work with the activity.
Example of one of my methods on the activity:
private void postCastedSpell(final int spellId) {
Call call = StaticGlobalContainer.api.postSpellToBattle(Integer.parseInt(battleId), Integer.parseInt(MainActivity.CURRENT_USER_ID), spellId, 100);
call.enqueue(new Callback<User>() {
#Override
public void onResponse(Response<User> response, Retrofit retrofit) {
User user = response.body();
if (response.code() == 202) {
// 200
Log.i("Posting spell to battle", "Success");
Boolean affectedUserIsOpponent = isUserOpponent(user);
if (affectedUserIsOpponent && user.currentHp<1){
StaticGlobalContainer.battleOnResult(Constants.WON, getApplicationContext());
}else {
updateBattleLog(affectedUserIsOpponent, user, spellId);
}
// TODO: do something here
} else {
// 404 or the response cannot be converted to User.
Log.e("Posting spell to battle", "Error:" + response.errorBody());
}
}
#Override
public void onFailure(Throwable t) {
Log.i("HttpRequest-Post spell", "Failure");
}
});
}
It's not specifically bad to put a lot of logic in Activities, but you're right to try to keep it only view related things. If the app is relatively small, it might not be worth moving the logic out. Also, there is some overhead to using abstractions.
if your abstractions aren't supplying a significant benefit, you should avoid them
I try to keep any big data objects in a manager class, so given your example, it might worthwhile to create a Battle manager class to hold all the logic involved in it, like this postCastedSpell function. This way all the Battle information is self contained, and also can be used elsewhere in other activities.
Just keep in mind if you're use data manager classes and you want them to prompt some sort of interation with the UI, you'll have to use Callbacks or the Bus pattern since the Battle manager won't have access to your UI. For example, to call the postCastedSpell the call would look like:
BattleActivity:
BattleManager bm = BattleManager.getInstance(user1, user2);
onSpellClicked() {
bm.castSpell(spellId, user1, callback)
}
BasicCallback callback = new BasicCallback() {
#Override
onComplete() {
if (MyInfoFragment.this.isVisible()) {
[Update UI]
}
}
};
NOTE: When using callbacks like my example, when it finally gets called the activity may have already gone out of view and have been already garbage collected. So in the callback function you need to first make sure it is still visible before trying to modify the UI that possibly no longer exists.

setAdapter from ListView acts like addView instead than just adding text on the View

my name is Francesco and I was trying to create a chat-app, just to see how much time I would spend doing it.
Unluckily there's a thing that seems like a bug for me, but maybe I'm wrong.
I've 3 classes:
A Global class to share variables.
A FriendList class.
A ChatRoom class.
The "buggy" piece of code is into the ChatRoom class. It works really fine into the FriendList class, it just uses the method setAdapter from the ListView but when it has to use it, instead of adding text to the ListView, it adds 2 views(an autoCompleteTextView and a Button), that are the 2 views I'm using in the ChatRoom class to write and send messages...
The layout of the ChatRoom is the same of the FriendList with the difference that in the ChatRoom one there is an autoCompleteTextView, a Button to send messages and 2 TextViews instead of one, cause I wanted to put the different messages to the right or left depending on who wrote the message.
piece of the FriendList code:
friendList = (ListView) findViewById(R.id.mainListView);
list = new ArrayList<String>();
ArrayAdapter<String> adapter = new ArrayAdapter<String>(currentActivity, R.layout.friendlist_layout, R.id.textview, list);
friendList.setAdapter(adapter);
adapter.add("francesco");
adapter.add("funkyserver");
friendList.setAdapter(adapter);
piece of ChatRoom code:
public void onClick(View v) {
try {
String messageToSend = messageInput.getText().toString();
if (messageToSend != "" | messageToSend != null) {
chat.sendMessage(messageInput.getText().toString());
messageListAdapterR.add(messageToSend);
messageListAdapterL.add("");
currentActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
messageListView.setAdapter(messageListAdapterL);
messageListView.setAdapter(messageListAdapterR);
}
});
}
...
p.s. Both the codes are into a runOnUiThread.
Thanks in advance for the replies :)
When you modify the data attached to the adapter bound to a ListView, you do not call setAdapter() again to update the view. That is what notifyDataSetChanged() is for. Your first pseudo-code sample ought to look something more like this:
friendList = (ListView) findViewById(R.id.mainListView);
list = new ArrayList<String>();
ArrayAdapter<String> adapter = new ArrayAdapter<String>(currentActivity,
R.layout.friendlist_layout, R.id.textview, list);
friendList.setAdapter(adapter);
adapter.add("francesco");
adapter.add("funkyserver");
adapter.notifyDataSetChanged();
Actually, to be even more precise, even notifyDataSetChanged() shouldn't be necessary when adding items to the adapter directly…it should call that method internally.
With regards to your second example, ListView cannot display data from multiple adapters. When your code calls:
messageListView.setAdapter(messageListAdapterL);
messageListView.setAdapter(messageListAdapterR);
The second method call overrides the first and all you will see are items from messageListAdapterR. If you want the data from both "lists" to show in the view, they have to be combined into a single adapter.

Passing parameters to the view

In my RCP application, I have a View with a TreeViewer for Navigation on the left side and a Folder for my views on the right side. The Perspective looks like this:
public void createInitialLayout(IPageLayout layout) {
layout.setEditorAreaVisible(false);
layout.setFixed(false);
layout.addStandaloneView(NavigationView.ID, false, IPageLayout.LEFT, 0.7f, layout.getEditorArea());
right = layout.createFolder("right", IPageLayout.RIGHT, 0.3f, "com.my.app.views.browser.navigation");
layout.getViewLayout(WallpaperView.Id).setCloseable(false);//dummy view to keep the folder from closing
layout.getViewLayout(WallpaperView.Id).setMoveable(false);
right.addView(WallpaperView.Id);
//add some placeholders for the potential views
right.addPlaceholder(DefaultAdminView.ID+":*");
}
I would like to open different views, based on what the user selects in the navigation tree. Figured that wouldn't be to hard. My Navigation Tree view:
tree = new TreeViewer(composite);
tree.setContentProvider(new BrowserNavigationTreeContentProvider());
tree.setLabelProvider(new BrowserNavigationTreeLabelProvider());
tree.setInput(UserProfileAdvisor.getProject());
//register Mouselistener for doubleclick events
tree.addDoubleClickListener(new IDoubleClickListener(){
#Override
public void doubleClick(DoubleClickEvent event) {
TreeSelection ts = (TreeSelection) event.getSelection();
Object selectedItem = ts.getFirstElement();
String viewId = DefaultAdminView.ID;
//set viewId depending on the selectedItem.class
try {
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().showView(viewId, String.valueOf(++viewCounter), IWorkbenchPage.VIEW_ACTIVATE);
} catch (PartInitException e) {
ILogHelper.error("The view for the selected object could not be opened", e);
}
}
});
This seems to work fine. There's just one tiny problem:
I need to pass the object (let's say the selectedItem) to my view somehow, in order to let the user interact with its content. How do I do that?
I've seen some examples where some of my colleagues wrote an own View which they placed on the right side. Then they added a CTabFolder, instantiated the views and added them manually. Is there a smarter solution?
Create a new interface, giving it a method like accept( Object parameter ) and make your views implement it.
Then, when you do PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().showView(viewId, String.valueOf(++viewCounter), IWorkbenchPage.VIEW_ACTIVATE) the method showView returns an IViewPart. Cast this return to your interface and call the accept method.
Use the SelectionService for that, please refer to Eclipse RCP let two views communicate
Implement the SelectionProvider in your "Navigation" and in the opened view you can ask for the selected object from the selection-service (see article)
HTH Tom

Categories

Resources