Updating a part of the RecyclerView - java

i have a recycle view that contains
a description of a comment and a vote count when i press up i need the count to increase this is how it looks like at the moment
but the problem is when i press up the vote does increase but until i close that fragment and reopen it it does not update
what actually happens is when i press up vote it makes an API call which does some logic and sends it over to android which has a number of the vote for that particular item which was being clicked or upvoted
how can i update that particular record so that i can see the change without reopening the view
code:
CommentItem.java
public CommentItem(String id, String description, String voteCount)
{
this.id = id;
this.description= description;
this.voteCount = voteCount;
}
//getters and setters
CommentItemAdapter.java
...
private OnItemClickListener mlistener;
/*Interfaces*/
public interface OnItemClickListener{
void VoteUpClick(int position);
void VoteDownClick(int position);
...
}
...
CommentFragment.java
#Override
public void VoteUpClick(final int position) {
final CommentItem clickeedItem =
mitemList.get(position);
StringRequest strReq = new StringRequest(Request.Method.POST,
AppConfig.URL, new Response.Listener<String>() {
#Override
public void onResponse(String response) {
try {
JSONObject jObj = new JSONObject(response);
boolean error = jObj.getBoolean("error");
if (!error) {
String errorMsg = jObj.getString("msg");
Toast.makeText(getContext(), errorMsg,
Toast.LENGTH_SHORT).show();
} else {
String msg= jObj.getString("msg");
Toast.makeText(getContext(), msg,
Toast.LENGTH_SHORT).show();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {}
}) {
#Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<String, String>();
params.put("voteType", "up");
params.put("commentID", clickeedItem.getId().toString());
return params;
}
};
AppController.getInstance().addToRequestQueue(strReq, tag_string_req);
}

If I get it right, simply adapter.notifyDataSetChanged() will update your list. But There is a better way, using DiffUtils in recyclerView. That is, comparing old and new list, and updating only the changed item, not changing the whole data set.
Have a look at it out
https://medium.com/#iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00
Other approach, is to have the viewHolder reference from the Activity/Or Fragment, when onItemClick happens, by recyclerView.findViewHolderForAdapterPosition(position). Then change view by this view

A lot of people here had a nice advice to use DiffUtils. Thats quite a reasonable way to handle diffs inside dataset of RecycleView.
The best way to perform it right now is to use ListAdapter, which requires a DiffUtil.ItemCallback or AsyncDifferConfig. A really big pros of ListAdapter is that all differ logic is done on background, which in turns optimizes your UI.
Everything you need is to override this funs: areItemsTheSame() and areContentsTheSame(), additionally u have getChangePayload() - for detailed info about the changed item.
Don't use the notifyDataSetChanged() and other range update functions, all of that stuff is handled under-the-hood.
Yours case could be handled via different approaches. I prefer having an intermediate state which will notify user that something happens. So you can locally mark that comment with pending vote up, for example yellow arrow and when response is obtained from back-end you just need to refresh the data-list and the ItemCallback will do the diff trick for you. When response is retrieved and vote is applied it can be marked as green arrow. Those are just thoughts about the right flow.
In any case everything you need is to use the ListAdapter.sumbitList(newDataSet) and the internal differ of ListAdapter will use ItemCallback to compare old and new list.

Related

reading Firestore documents with AsyncTask

I have a database in Cloud Firestore where each document has a particular key "last update" with value, a String, representing a date in the form YYYY-MM-DD. Each time a document is updated, the value of "last update" is set as the date of the update.
Now, I want my activity to have a method that checks documents for their last update. As the documents contain fairly big lists of objects, this update check takes a few seconds. So I decided to defer it to an AsyncTask. The doInBackground method of the AsyncTask should create a DocumentReference, noteRef, for the document and read its "last update" with noteRef.get(), equipped with
onSuccess- and onFailure listeners, into a String, which is then returned by the method.
In order to test this, I have created a toy activity, MyTestActivity, which calls the above AsyncTask with String arguments "myCollection" and "myDocument" and
displays the value of this document's last update in a TextView. Now, instead of showing the actual value, "2019-10-03", the TextView displays the value, "1970-01-01", which is the one
used in doInBackground to initialize the String variable which is returned. It's as if doInBackground doesn't bother to wait until the document has been read. The code is as follows.
public class MyTestActivity extends AppCompatActivity{
private Button button;
private TextView textView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_test);
button = findViewById(R.id.update_button);
textView = findViewById(R.id.update_text_view);
}
public void buttonClicked(View view) throws ExecutionException, InterruptedException {
UpdateTask task = new UpdateTask(this, "myCollection", "myDocument");
String date = task.execute().get();
textView.setText("Last update on "+date);
}
private static class UpdateTask extends AsyncTask<Integer, Integer, String> {
private WeakReference<MyTestActivity> activityWeakReference;
String collection;
String document;
String lastUpdate;
UpdateTask(MyTestActivity activity, String collection, String document) {
activityWeakReference = new WeakReference<MyTestActivity>(activity);
this.collection = collection;
this.document = document;
lastUpdate = new String();
}
#Override
protected void onPreExecute() {
super.onPreExecute();
MyTestActivity activity = activityWeakReference.get();
if (activity == null || activity.isFinishing()) {
return;
}
}
#Override
protected String doInBackground(Integer... params) {
FirebaseFirestore db = FirebaseFirestore.getInstance();
DocumentReference noteRef = db.collection(collection).document(document);
lastUpdate = "1970-01-01";
noteRef.get()
.addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists()) {
Map<String, Object> map = documentSnapshot.getData();
lastUpdate = (String)map.get("last update");
activityWeakReference.get().textView.setText(lastUpdate);
} else {
lastUpdate = "Document doesn't exist";
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
lastUpdate = "Listener failed.";
}
});
return lastUpdate;
}
}
}
Can anyone explain what is going on here?
I have a database in Firebase Firestore where each document has a particular key "last update" with value, a String, representing a date in the form YYYY-MM-DD.
That's uncommon to store the date as a String, instead you should store it as a:
FieldValue.serverTimestamp()
As explained in my answer from the following post:
ServerTimestamp is always null on Firebase Firestore
So I decided to defer it to an AsyncTask.
The Cloud Firestore database client, already runs all network operations in a background thread. This means that all operations take place without blocking your main thread. Adding it in an AsyncTask does not give any any benefits at all.
Now, instead of showing the actual value, "2019-10-03", the TextView displays the value, "1970-01-01", which is the one used in doInBackground
This is happening because you are trying to return a message synchronously from a method that is asynchronous. That's not a good idea. You should handle the APIs asynchronously as intended.
A quick solve for this problem would be to use value of lastUpdate only inside the onSuccess() method, otherwise I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.
Ouch! That's bad design. Firestore calls are asynchronous, so you don't need to put them into asyncTask background method. Also, using an synctask wont execute your code faster. What you need is a "loading message" until your OnSuccessListener fires back.

The right way to deal with a few network requests

I'm preety new in java/android.
I'm writing android app, which takes data from online api.
The problem is, I'm not sure if my concept is correct.
So my app send first request, but I need some of respond data to start next request.
Here is some example more or less how it looks right now:
public class MainActivity extends AppCompatActivity{
private int key = 0;
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new DownloadTask().execute("url of 1st request");
}
private class DownloadTask extends AsyncTask<String, Void, String> {
#Override
protected String doInBackground(String... params) {
try {
return downloadContent(params[0]);
} catch (IOException e) {
return "Unable to retrieve data. URL may be invalid.";
}
}
#Override
protected void onPostExecute(String result) {
if(key == 0){
// I know it's 1st request because my key == 0
JSONObject json = new JSONObject(result);
String id = json.getString("id");
key++;
new DownloadTask().execute( "url of 2nd request/" + id );
}else{
// I know it's 2st request because my key != 0
//here I'm getting data i need
// and I'm going to rest of my app
end(result);
}
}
}
private void end(String result){
//rest of my app
}
}
Code is working fine, but I wanted to know if it's proper way to do it.
Maybe you know another way to do that, I'm not asking for completly new code, but maybe some topic I should find and read.
If you want to do it asynchronously, i will recommend you using CompletableFuture's. It is very simple and look like that:
CompletableFuture
.supplyAsync(() -> yourFirstrequest())
.thenApplyAsync(yourInstance::getResponse)
.thenAcceptAsync(nextRequestClass::sendNextrequest);
or you can separate them by Future's like:
CompletableFuture firstRequest = CompletableFuture.supplyAsync(() -> yourFirstrequest());
CompletableFuture getResponse = firstRequest.thenApply(response -> someActionWithResponse(response));
it is very powerful and convenient framework.
Take a look at the java docs or android specific docs

Chat application's "Send" button not working

this is my first time asking question on stackoverflow.
I'll just go straight to the point, I'm currently developing an application which involve creating a chat room using firebase, I found this
https://www.youtube.com/watch?v=uX6_w6yhj4E
and decided to follow him. Everything was going smoothly until i finish the code and tries to run it. Everything functioned as expected except the send button that was suppose to display my message in the chat room. My send button didn't respond even though i tap on it. I can't seem to find the problem.
Here is my code for the chat_room:
public class Chat_Room extends AppCompatActivity {
private Button btn_send_msg;
private EditText input_msg;
private TextView chat_conversation;
private String user_name, room_name;
private DatabaseReference root;
private String temp_key;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.chat_room);
btn_send_msg = (Button) findViewById(R.id.btn_send);
input_msg = (EditText) findViewById(R.id.msg_input);
chat_conversation = (TextView) findViewById(R.id.textview);
user_name = getIntent().getExtras().get("user_name").toString();
room_name = getIntent().getExtras().get("room_name").toString();
setTitle(" Room - "+room_name);
root = FirebaseDatabase.getInstance().getReference().child(room_name);
btn_send_msg.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View view) {
Map<String,Object> map = new HashMap<String, Object>();
temp_key = root.push().getKey();
root.updateChildren(map);
DatabaseReference message_root = root.child(temp_key);
Map<String, Object> map2 = new HashMap<String, Object>();
map2.put("name",user_name);
map2.put("msg",input_msg.getText().toString());
//message_root.updateChildren(map2);
message_root.setValue(map2);
}
});
root.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
append_chat_conversation(dataSnapshot);
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
append_chat_conversation(dataSnapshot);
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
private String chat_msg, chat_user_name;
private void append_chat_conversation(DataSnapshot dataSnapshot){
Iterator i = dataSnapshot.getChildren().iterator();
while (i.hasNext()){
chat_msg = (String) ((DataSnapshot)i.next()).getValue();
chat_user_name = (String) ((DataSnapshot)i.next()).getValue();
chat_conversation.append(chat_user_name +" : "+chat_msg+"\n");
}
}
}
Let me know if I did something wrong or if the information provided was not enough, really appreciate for the help as I'm still a rookie in both android studio and firebase. Thanks in advance.
Welcome to stack!! :)
Update only updates data, if there is not data to update in the database then nothing will happen.
Your first update will do nothing.
Map<String,Object> map = new HashMap<String, Object>();
temp_key = root.push().getKey();
root.updateChildren(map);
The map is empty and nothing will be updated because, well, the map is empty.
The second update, will also do nothing and this is why
DatabaseReference message_root = root.child(temp_key);
Map<String, Object> map2 = new HashMap<String, Object>();
map2.put("name",user_name);
map2.put("msg",input_msg.getText().toString());
message_root.updateChildren(map2);
Here you made a reference to the child with the push key. Push keys are unique and no two push keys will ever be the same. So when you make a databaseReference to the 'message_root'. That Database reference does not exist at that point. So the attributes, "name" and "msg" do not exist in the database reference and therefore cannot be updated.
That is why you see nothing. To fix it, try this
//message_root.updateChildren(map2);
message_root.setValue(map2);
Instead of updating the message_root. Set the value instead. You have to set the value because the message_root does not exist and by calling setting value you set its existence if that makes sense.
[EDIT]
Try confirming that at least something can be posted to your database.
In your on create method create a map and push that map to your database.
If nothing is reflected, I can only assume that one of two things has happened. You have set up Firebase incorrectly(have you added your google-services json) or your security rulesare set to only alound writes from authenticated users.Make sure your security rules are open. I think the security rules are the most likely cause.
Go to the Firebase console. Go to the database and click on Rules. Make sure your rules look like this.
{
"rules": {
".read": true,
".write": true
}
}
Copy and pase those rules and post them in directly.

Android Volley Request - within request populating custom class not working

I've got an Activity consisting of a buch of TextViews (fourteen) and two Buttons.
I have created a custom class named Lesson, wich basically has a constructor and getter methods for its variables.
Now, inside my onCreate() in my Activity I am calling two functions: 1.) populateLessonDetails(myURL) and 2.) populateLessonTextViews().
I have created a private Lesson mLesson; variable inside my Activity, above all the #Overrides, because I'm trying to use this variable to populate it later on.
So, populateLessonDetails(myURL) is basically making a JsonArrayRequest, getting all the data from the JSON inside the onResponse(), saving it to String variables still inside the onResponse() and then, still inside the onResponse() I am trying to populate the mLesson variable, by calling
mLesson = new Lesson(mName, mRoom, mExtra, mAddress, mPC, mCity, mStart, mDate, mRID, mMaxAtt, mCurrentAtt); - the variables used within the constructor are the String variables containing the JSON data.
I Log.i() the JSON data as well as the mLesson variables via its getter methods, and the data is there. Everything is fine.
Now, my populateLessonDetails() ends.
It returns to the onCreate() and continues with the next line of code, wich would be calling populateLessonTextViews().
This is where things went south...
As soon as the function is called I try to get the information stored inside mLesson via its getter methods to set it to the TextViews like so:
//Lesson Name Big
TextView lessonNameTextBig = (TextView) findViewById(R.id.text_activelesson_name_big);
lessonNameTextBig.setText(mLesson.getLessonName());
This is the proper way to do it, I've done it a bunch of times already, but my App crashes at the second line.
I have debugged it and I have noticed that mLesson is empty. My guess would be that me populating it inside the onResponse() of the JsonArrayRequest, which is inside the populateLessonDetails() is only valid for this particular function, the scope of the variable mLesson ends when the function returns to the onCreate() and the mLesson variable is empty again since it died with the function.
Now how can I fix this? Do I have to set mLesson as a parameter for the populateLessonDetails() and then also return it (currently the populate functions are void) ? Then save the return value into another variable of type Lesson and set this new variable as a parameter for the populateLessonTextViews() ?? I've tried a couple of those things but they didn't work, but maybe its just me not doing it right.
This is what my code looks like (the important part):
public class ActiveLesson extends AppCompatActivity {
// there are also some other variables up here
private Lesson mLesson;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_active_lesson);
requestQ = Volley.newRequestQueue(this);
Intent intent = getIntent();
Bundle extras = intent.getExtras();
mDatum = extras.getString("datum");
mRID = extras.getString("rid");
mVon = extras.getString("von");
myActiveLessonURLFiltered += "datum="+mDatum+"&rid="+mRID+"&von="+mVon;
populateLessonDetails(myActiveLessonURLFiltered);
populateLessonTextViews();
}
private void populateLessonDetails(String myActiveLessonURLFiltered) {
JsonArrayRequest lessonJAR = new JsonArrayRequest(myActiveLessonURLFiltered,
new Response.Listener<JSONArray>() {
#Override
public void onResponse(JSONArray response){
try{
for (int i=0; i < response.length(); i++)
{
JSONObject jsonObject = response.getJSONObject(i);
String mName = jsonObject.getString("Name");
String mRoom = jsonObject.getString("Raum");
String mExtra = jsonObject.getString("Zusatz");
String mAdresse = jsonObject.getString("Address");
String mPC = jsonObject.getString("PLZ");
String mCity = jsonObject.getString("City");
String mMaxAtt = jsonObject.getString("maxAnz");
String mCurrentAtt = jsonObject.getString("belegtAnz");
if(mExtra.length()==0 || mExtra == "null")
mExtra="";
if(mRoom.length()==0 || mRoom == "null")
mRoom="";
else
mRoom="Room: "+mRoom;
if(mName.length()==0 || mName == "null")
mName="";
mLesson = new Lesson(mName, mRoom, mExtra, mAdresse,
mPC, mCity, mVon, mDatum, mRID, mMaxAtt, mCurrentAtt);
Log.i("mmLesson"," Lesson with new = "+ mLesson.getLessonName()
+" "+mLesson.getLessonCity());
}
}catch (JSONException e){
e.printStackTrace();
}
}
},
new Response.ErrorListener(){
#Override
public void onErrorResponse(VolleyError error){
error.printStackTrace();
Toast.makeText(ActiveLesson.this, "No Lessons Available",
Toast.LENGTH_LONG).show();
}
}){
#Override
public Map<String, String> getHeaders() throws AuthFailureError {
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("Accept", "application/json");
return headers;
}
};
requestQ.add(lessonJAR);
}
private void populateLessonTextViews(Lesson mLesson) {
//Lesson Name Big
TextView lessonNameTextBig = (TextView) findViewById(R.id.text_activelesson_name_big);
lessonNameTextBig.setText(mLesson.getLessonName());
// there are others lines of code like these two,
// but I've left them out, since they are all the same
}
If some could help me out I would appreciate it. Thank you!
The onResponse() method is a callback that is called later when the network request returned a value. The server does not respond with any delay. This means the populateLessonDetails(..) method get called from onCreate triggers an network request and return immedietly to the onCreate() call of this function and steps forward.
You have to take this in consideration. The best way to do this, call inside the onResponse the populateLessonTextViews() method. Then you can be sure that the content has been loaded.

Paging library returns empty list initially

I'm using Paging library to paginate a list of items I'm retrieving from my server. Initially, when my fragment is loaded, it returns an empty list. But after changing fragments and going back to that fragment, I can see the list loaded. After debugging I saw that data was actually being fetched, but an empty list was passed to my fragment.
ItemDataSource:
#Override
public void loadInitial(#NonNull LoadInitialParams<Integer> params, #NonNull LoadInitialCallback<Integer, Item> callback) {
apiService.getItems(OFFSET)
.enqueue(new Callback<ItemWrapper>() {
#Override
public void onResponse(#NonNull Call<ItemWrapper> call,#NonNull Response<ItemWrapper> response) {
callback.onResult(response.body().getItems(), null, OFFSET + 25);
}
#Override
public void onFailure(#NonNull Call<ItemWrapper> call,#NonNull Throwable t) {
t.printStackTrace();
}
});
}
#Override
public void loadBefore(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Item> callback) {
}
#Override
public void loadAfter(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Item> callback) {
apiService.getItems(params.key)
.enqueue(new Callback<ItemWrapper>() {
#Override
public void onResponse(#NonNull Call<ItemWrapper> call,#NonNull Response<ItemWrapper> response) {
Integer key = response.body().getItems().isEmpty() ? null : params.key + 25;
callback.onResult(response.body().getItems(), key);
}
#Override
public void onFailure(#NonNull Call<ItemWrapper> call,#NonNull Throwable t) {
t.printStackTrace();
}
});
}
ItemDataSourceFactory:
#Override
public DataSource create() {
ItemDataSource itemDataSource = new ItemDataSource();
itemLiveDataSource.postValue(itemDataSource);
return itemDataSource;
}
public MutableLiveData<ItemDataSource> getItemLiveDataSource() {
return itemLiveDataSource;
}
ItemViewModel:
private LiveData<ItemDataSource> liveDataSource;
private LiveData<PagedList<Item>> itemPagedList;
private ItemViewModel(Application application) {
ItemDataSourceFactory factory = new ItemDataSourceFactory();
liveDataSource = factory.getItemLiveDataSource();
PagedList.Config config = (new PagedList.Config.Builder())
.setEnablePlaceholders(false)
.setPageSize(ItemDataSource.LIMIT).build();
itemPagedList = (new LivePagedListBuilder(factory, config)).build();
}
public LiveData<PagedList<Item>> getItems() {
return itemPagedList;
}
Fragment:
ItemViewModel itemViewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
itemViewModel.getItems.observe(this, items -> {
adapter.submitList(items);
})
Not 100% sure, but I think this is because you are running an asynchronous request. try to change it to run synchronously for loadInitial() like so request.execute()
I also had this problem once and I still can't figure it out why it does not work for some fragments. The solution I found, aldo being more like a fast sketchy fix is to load the fragment twice.
Yassin Ajdi is right. loadinitial() calls immediately on the same thread where PagedList is created on. As your API is async, the method runs empty for the first time
If, like me, anyone is using an asynchronous RxJava/Kotlin call in loadInitial. I finally figured out a solution after many painful hours.
I tried using a delayed Handler (500ms) in the Observer method but it was fickle and didn't work in every scenario. No matter how much I tried to make it synchronous using setFetcher and Rx observeOn it wouldn't consistently work.
My solution was to use .blockingSubscribe in my Observable. My data fetcher was using a Socket library that had its own concurrency out of my scope, so I couldn't guarantee that I could make the process wholly synchronous as Paging requires. (A process which needs better documentation IMO). Anyway, here's my solution, hopefully it helps others with same issue:
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, ResultItem>
) {
mySocketClientRxRequest()
.subscribe ({
callback.onResult(it.resultItems 1, 2)
},{
it.printStackTrace()
})
}
to
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, ResultItem>
) {
mySocketClientRxRequest()
.blockingSubscribe ({
callback.onResult(it.resultItems 1, 2)
},{
it.printStackTrace()
})
}
Just as in the question I was loading the data asynchronously inside loadInitial, but the data did not come to the adapter when callback was called.
Solved just by updating the library version from
androidx.paging:paging-runtime-ktx:2.1.2
to androidx.paging:paging-runtime-ktx:3.0.0.
I've been having this same problem as well for a few days until finally I found the solution. But just to note I used Paging version 3, so this might not answer this question directly, but might help anyone who's still struggling in blank recyclerView initially
In your fragment where you applied PagingAdapter, use loadStateFlow on the adapter to observe changes in loadState. You can place this in onCreateView for Fragment
val pagingAdapter = PagingAdapter()
lifecycleScope.launch {
pagingAdapter.loadStateFlow.collect { loadState ->
if (loadState.prepend.endOfPaginationReached) {
// Apply this if PagingDataAdapter start binding data in view holder
println("APPLYING ADAPTER")
binding.recyclerView.adapter = pagingAdapter
cancel() // Cancel this flow after applying adapter
}
}
}

Categories