I have reviewed other similar posts before posting this, mine is different
I am current retrieving a list of download urls from my Firestore data base, then trying to download those images from my firebase storage to display them in a gridview.
This is my code so far:
final Query chatRoomMsgs = db.collection("chatrooms").document(chatRoomID).collection("Messages").whereEqualTo("sentby", firebaseAuth.getUid());
chatRoomMsgs.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
ArrayList<String> sentPicsURLS = new ArrayList<String>();
for(QueryDocumentSnapshot documentSnapshot: queryDocumentSnapshots){
for(int i = 0; i < queryDocumentSnapshots.size(); i++) {
sentPicsURLS.add(documentSnapshot.get("image").toString());
if(i == (queryDocumentSnapshots.size()-1)){
//now download the images and place them into the proper view
for(int z = 0; z < sentPicsURLS.size(); z++){
}
}
}
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
}
});
This is where the images should be pulled and pushed into a gridview:
for(int z = 0; z < sentPicsURLS.size(); z++){
//right here
}
But I am having trouble creating an adapter that can handle this. I have a valid gridview in the activity and I have a layout file that contains an imageview with a ID.
final ArrayAdapter arrayAdapter = new ArrayAdapter(Chatroom.this, R.layout.chatroom_sent_images,R.id.sent_iv);
sentPics.setAdapter(arrayAdapter);
sentPics.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//empty for now
}
});
The part I am missing (seems to be) where I actually loop through sentPicsURLS and then add them to the adapter... maybe with something like arrayAdapter.addAll(sentPicsURLS); inside the //right here for loop?
Right now the gridview is showing empty without even the default image view included in R.layout.chatroom_sent_images. I feel like I am so close, what am I missing? Thanks!
Edit
Here is my chatroom database structure, every chatroom and chatroom message is structured the same way.
As I see in your screenshot, your document hols only a single image. So to solve this, there is no need for an extra inner for-loop. To create a list of those images, please use the following lines of code:
Query chatRoomMsgs = db.collection("chatrooms").document(chatRoomID)
.collection("Messages").whereEqualTo("sentby", firebaseAuth.getUid());
chatRoomMsgs.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
ArrayList<String> sentPicsURLS = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
sentPicsURLS.add(document.getString("image"));
}
//Do what you need to do with your list
Log.d(TAG, "List size: " + sentPicsURLS.size());
}
}
});
Be also aware that Firebase APIs are asynchronous and you can use that list only inside the callback. Now, the result in your logcat will be size of your list.
Related
In an android app I have made, the home page shows a list a games in a listview, each row being custom made to include a textview and an imageview and some buttons. The imageview's src comes from downloading th image from my google cloud firebase, and the functionality of this works well, but when the listview is scrolled through there is an issue. The images seem to be unloaded when scrolled away from, which causes a bit of lag when they are reloaded once scrolled back to. I imagine this is built in to prevent a listview from loading many high resolution images and keeping them loaded, but for my list, keeping the images loaded won't be a problem. Is there a way I can turn this off and just keep the images loaded? Here is a video of the problem:
https://www.youtube.com/watch?v=FYYJWZcvBy4&feature=youtu.be
here is the code of the listview and getting the image from firebase:
public void showData(DataSnapshot dataSnapshot){
TextView toolbarTitle = findViewById(R.id.toolbarTitle);
toolbarTitle.setText("Popular Games");
ArrayList<GameInformation> PopularGames = new ArrayList<>();
Iterator<DataSnapshot> items = dataSnapshot.child("Games").getChildren().iterator();
PopularGames.clear();
while(items.hasNext()){
GameInformation game = new GameInformation();
DataSnapshot item = items.next();
game.setGameName(item.child("Name").getValue(String.class));
game.setActiveLobbies(Integer.parseInt(item.child("Live Lobbies").getValue().toString()));
game.setPicturePath(item.child("FilePathName").getValue().toString());
Iterator<DataSnapshot> itemsDeep1 = item.child("consoles").getChildren().iterator();
while(itemsDeep1.hasNext()){
DataSnapshot itemDeep = itemsDeep1.next();
game.setConsoles(itemDeep.getValue(String.class));
}
Iterator<DataSnapshot> itemsDeep2 = item.child("genres").getChildren().iterator();
while(itemsDeep2.hasNext()){
DataSnapshot itemDeep = itemsDeep2.next();
game.setGenres(itemDeep.getValue(String.class));
}
if (game.getActiveLobbies() == 1){
PopularGames.add(game);
}
}
Log.d(TAG, "Hello Here" + PopularGames.size());
ArrayAdapter adapter = new CustomListAdapter(this, PopularGames);
mListView.setAdapter(adapter);
}
getting the image:
storageRef.child("Games/"+singleGame.getPicturePath()+".jpg").getDownloadUrl().addOnSuccessListener(new OnSuccessListener<Uri>() {
#Override
public void onSuccess(Uri uri) {
Glide.with(getContext()).load(uri).into(gameImageID);
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// Handle any errors
}
});
The delay you're facing is because there are 2 operations (each one takes a few secs to complete) being performed when you get each image:
The image is read from Firebase Storage using getDownloadUrl()
The image is displayed with Glide
I recommend that when you store the images, save their Download URL to the database instead of saving their path. This way, you'd no longer need to call for getDownloadURL(). You'd simply load the image with a single operation:
Glide.with(getContext()).load(singleGame.getPicturePath()).into(gameImageID);
Not related to the question:
You can simplify your code by using a forEach loop (instead of while) in your Iterators:
PopularGames.clear();
for(DataSnapshot item : dataSnapshot.child("Games").getChildren()){
GameInformation game = new GameInformation();
game.setGameName(item.child("Name").getValue(String.class));
game.setActiveLobbies(Integer.parseInt(item.child("Live Lobbies").getValue().toString()));
game.setPicturePath(item.child("FilePathName").getValue().toString());
for(DataSnapshot itemDeep: item.child("consoles").getChildren()){
game.setConsoles(itemDeep.getValue(String.class));
}
for(DataSnapshot itemDeep: item.child("genres").getChildren()){
game.setGenres(itemDeep.getValue(String.class));
}
if (game.getActiveLobbies() == 1){
PopularGames.add(game);
}
}
Log.d(TAG, "Hello Here" + PopularGames.size());
This question already has answers here:
How to paginate Firestore with Android?
(3 answers)
Closed 4 years ago.
On working around to learn firebase firestore for an example from GitHub friendly eat app
I thought to implement pagination to limiting nodes for 10
private static final int LIMIT = 10;
in the firestore example app the mAdapter loads data/nodes as below
mFirestore = FirebaseFirestore.getInstance();
// Get ${LIMIT} restaurants
mQuery = mFirestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT);
// RecyclerView
mAdapter = new RestaurantAdapter(mQuery, this) {
#Override
protected void onDataChanged() {
// Show/hide content if the query returns empty.
if (getItemCount() == 0) {
mRestaurantsRecycler.setVisibility(View.GONE);
mEmptyView.setVisibility(View.VISIBLE);
} else {
mRestaurantsRecycler.setVisibility(View.VISIBLE);
mEmptyView.setVisibility(View.GONE);
}
}
#Override
protected void onError(FirebaseFirestoreException e) {
// Show a snackbar on errors
Snackbar.make(findViewById(android.R.id.content),
"Error: check logs for info.", Snackbar.LENGTH_LONG).show();
}
};
mRestaurantsRecycler.setLayoutManager(new LinearLayoutManager(this));
mRestaurantsRecycler.setAdapter(mAdapter);
// Filter Dialog
mFilterDialog = new FilterDialogFragment();
}
and
#Override
public void onStart() {
super.onStart();
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn();
return;
}
// Apply filters
onFilter(mViewModel.getFilters());
// Start listening for Firestore updates
if (mAdapter != null) {
mAdapter.startListening();
}
}
on firestore docs says about to paginate
// Construct query for first 25 cities, ordered by population
Query first = db.collection("cities")
.orderBy("population")
.limit(25);
first.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
// ...
// Get the last visible document
DocumentSnapshot lastVisible =
documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
// get the next 25 cities.
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
// Use the query for pagination
// ...
}
});
combining those above codes how should I implement paginate to load more than 10
nodes to load when I scroll to the bottom of the recycler view
// Use the query for pagination
// ...
Update: I am working based on firestore doc about Paginate query and taking look at a possible duplicate of another question I did not get it to done working
Thank you
Here I found solution around but not better one, if is there any better way please post
by saving RecyclerView state before loading more nodes and reloading RecyclerView state after increasing the limit
private static final int LIMIT = 10;
Changed to
private int LIMIT = 10;
when recyclerView is scrolled to the bottom
mRestaurantsRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
final int mLastVisibleItemPosition = mManager.findLastVisibleItemPosition();
if ( mLastVisibleItemPosition == (LIMIT-1)) {
LIMIT = LIMIT*2;
showSpotDialog();
// save RecyclerView state
mBundleRecyclerViewState = new Bundle();
Parcelable listState = mRestaurantsRecycler.getLayoutManager().onSaveInstanceState();
mBundleRecyclerViewState.putParcelable(KEY_RECYCLER_STATE, listState);
loadMore(query);
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
// restore RecyclerView state
if (mBundleRecyclerViewState != null) {
Parcelable listState = mBundleRecyclerViewState.getParcelable(KEY_RECYCLER_STATE);
mRestaurantsRecycler.getLayoutManager().onRestoreInstanceState(listState);
}
hideSpotDialog();
}
}, 500);
}
}
});
it looks very unusual when nodes are loaded after the limit, but no way for now...
and yes I am looking for loading more nodes without flaws
I have extended the FirestoreAdapter as followed:
Keep track DocumentSnapshots identifier.
private Set<String> mIdentifier = new HashSet<>();
Add a new public method for pagination, as i expect the rest of the query to remain the same, the given query does not need to be changed
/**
* Extends the query to load even more data rows. This method will do nothing if the query has
* not yet been set.
* #param limit the new limit
*/
public void paginate(long limit) {
if (mQuery != null) {
if (mRegistration != null) {
mRegistration.remove();
mRegistration = null;
}
// Expect the query to stay the same, only the limit will change
mQuery = mQuery.limit(limit);
startListening();
}
}
Clear the identifier in the setQuery(Query) method by calling mIdentifier.clear()
Adopt the onDocumentAdded(DocumentChange) and the onDocumentRemoved(DocumentChange) as followed
protected void onDocumentAdded(DocumentChange change) {
if (!mIdentifier.contains(change.getDocument().getId())) {
mSnapshots.add(change.getNewIndex(), change.getDocument());
mIdentifier.add(change.getDocument().getId());
notifyItemInserted(change.getNewIndex());
}
}
protected void onDocumentRemoved(DocumentChange change) {
mSnapshots.remove(change.getOldIndex());
mIdentifier.remove(change.getDocument().getId());
notifyItemRemoved(change.getOldIndex());
}
For the onScrolling listener i stick to this guide: Endless Scrolling with AdapterViews and RecyclerView
When a button is pressed, a dialog appears asking user for message, with the option of attaching an image (from url). The problem I'm having is once the recyclerview is filled with enough items to scroll, when the user scrolls quickly for some reason random images start popping up in seemingly random list items.
I know the problem has to come from when the image is actually placed into the imageview, since I can tell the link is added to the firebase db just fine.
When the image link is submitted, it's sent to /posts/$uid/$post-id in a HashMap. Kind of like this:
final Map<String, String> postMap = new HashMap<String, String>();
imagebutton.setOnClickListener((view) -> {
AlertDialog.Builder = new...
LayoutInflater in = ...
View dialogLayout = ...inflate(r.layout...., null);
build.setView(dialogLayout);
EditText imgText = ...
Button submit = ...
AlertDialog a = build.create();
submit.setOnClickListener((View) -> {
...
postMap.put("imgLink", imgText.getText().toString());
a.dismiss();
...
urlDialog.show();
Then a few more items are added to the map and pushed to firebase.
Firebase postRef = ref.child("posts").child(auth.getUid());
postMap.put("author", ...);
postMap.put("content", ...);
postRef.push().setValue(postMap);
But like I said, I'm almost 100% sure the problem is not in posting the information, just populating the recview
Here's my code for the list itself:
RecyclerView feed = (RecyclerView)findViewById(R.id.recycler);
if (ref.getAuth() != null) {
FirebaseRecyclerAdapter<TextPost, PostViewHolder> adapter = new FirebaseRecyclerAdapter<TextPost, PostViewHolder>(TextPost.class, R.layout.list_item, PostViewHolder.class, ref.child("posts").child(uid)) {
#Override
protected void populateViewHolder(final PostViewHolder postViewHolder, final TextPost textPost, int i) {
postViewHolder.content.setText(textPost.getContent());
postViewHolder.author.setText(textPost.getAuthor());
postViewHolder.score.setText(textPost.getScore());
postViewHolder.time.setText(textPost.getTime());
if (textPost.getImgLink() != null && !textPost.getImgLink().equals("")) {
Log.i(TAG, "Setting image");
new Thread(new Runnable() {
#Override
public void run() {
try {
final Bitmap pic = bitmapFromUrl(textPost.getImgLink());
postViewHolder.img.post(new Runnable() {
#Override
public void run() {
postViewHolder.img.setImageBitmap(pic);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
feed.setAdapter(adapter);
I just started learning how to work with worker threads for network activities off the main UI thread so I assume I messed that up? I've gone through the logic over and over in my head and i can't seem to figure out what's going wrong here.
EDIT: I tried using AsyncTask instead of Threads and the problem persists. sos
All I had to do was set the ImageView drawable to null before populating the ImageView.
Like this:
#Override
protected void populateViewHolder(final ViewHolder v, final Object o, int i) {
//populate views
v.content.setText("...");
//Set imageview to null
v.imageview.setImageDrawable(null);*
if (o.getImageLink() != null && !o.getImageLink.equals("")) {
// Start AsyncTask to get image from link and populate imageview
new DownloadImageTask().execute(o.getImgLink(), v.imageview);
}
}
I have a recyclerview which works as expected. I have a button in the layout that fills the list. The button is supposed to make a async call, and on result, I change the button's look. All this happens fine.
But when I click on the button and scroll down the list fast, the async call's result updates the new view's button(the view that is in place of the old one). How do I handle this? Can I have a handle on when a particular view gets reused?
Update :
Code piece of the adapter class that does the async call and the updation of ui.
#Override
public void onBindViewHolder(CommentsViewHolder holder, int position) {
try {
Comments comment = comments.get(position);
holder.bindView(comment,position);
}
catch(Exception ex){ex.printStackTrace();}
}
#Override
public int getItemCount() {
if(comments==null)
{return 0;}
return comments.size();
//return comments.length();
}
public class CommentsViewHolder extends RecyclerView.ViewHolder {
TextView score ;
TextView commentText;
TextView commentTime;
TextView avatarId;
ImageButton minusOne;
ImageButton plusOne;
ParseObject model;
public CommentsViewHolder(View itemView) {
super(itemView);
//itemView.setBackgroundColor(Color.DKGRAY);
minusOne =(ImageButton)itemView.findViewById(R.id.decScore);
plusOne =(ImageButton)itemView.findViewById(R.id.incScore);
commentText = (TextView)itemView.findViewById(R.id.comment);
score = (TextView)itemView.findViewById(R.id.commentScore);
commentTime =(TextView)itemView.findViewById(R.id.commentTime);
avatarId = (TextView)itemView.findViewById(R.id.ivUserAvatar);
}
public void bindView(Comments comment, int position) {
commentText.setText(comment.getCommentText());
score.setText(Integer.toString(comment.getScore()));
String timeText = DateUtils.getRelativeTimeSpanString( comment.getCreatedAt().getTime(), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString();
timeText = timeText.replace("hours","hrs");
timeText = timeText.replace("seconds","secs");
timeText = timeText.replace("minutes","mins");
commentTime.setText(timeText);
int commentHandler = comment.getCommenterHandle();
String commenterNumber = "";
if(commentHandler==0)
{
commenterNumber = "OP";
}
else{
commenterNumber = "#"+commentHandler;
}
avatarId.setText( commenterNumber);
model = comment;
String choice = "none";
minusOne.setEnabled(true);
plusOne.setEnabled(true);
minusOne.setVisibility(View.VISIBLE);
plusOne.setVisibility(View.VISIBLE);
for (ParseObject choiceIter : choices) {
if ((choiceIter.getParseObject("comment").getObjectId()).equals(comment.getObjectId())) {
choice = choiceIter.getString("userChoice");
break;
}
}
Log.i("debug",comment.getCommentText()+" "+comment.getScore()+" "+choice);
switch (choice) {
case "plusOne":
Log.i("darkplus","setting darkplus");
plusOne.setImageResource(R.drawable.ic_add_circle_black_18dp);
plusOne.setOnClickListener(reversePlusOneOnClickListener);
//minusOne.setOnClickListener(minusOneOnClickListener);
minusOne.setVisibility(View.GONE);
break;
case "minusOne":
Log.i("darkminus","setting darkminus");
minusOne.setImageResource(R.drawable.ic_remove_circle_black_18dp);
minusOne.setOnClickListener(reverseMinusOneOnClickListener);
//plusOne.setOnClickListener(plusOneOnClickListener);
plusOne.setVisibility(View.GONE);
break;
case "none":
Log.i("darkregular","setting regular");
minusOne.setImageResource(R.drawable.ic_remove_black_18dp);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setOnClickListener(minusOneOnClickListener);
break;
}
}
View.OnClickListener reversePlusOneOnClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
if (!FourUtils.isConnected(v.getContext())) {
return;
}
minusOne.setEnabled(false);
plusOne.setEnabled(false);
model.increment("plusOne", -1);
model.increment("score", -1);
model.saveEventually(new SaveCallback() {
#Override
public void done(ParseException e) {
if (e == null) {
ParseQuery<ParseObject> query = ParseQuery.getQuery("CommentChoice");
query.whereEqualTo("user", ParseUser.getCurrentUser());
query.whereEqualTo("comment", model);
query.fromPin(Four.COMMENT_CHOICE);
query.getFirstInBackground(new GetCallback<ParseObject>() {
#Override
public void done(ParseObject parseObject, ParseException e) {
if (e == null) {
if (parseObject == null) {
parseObject = ParseObject.create("CommentChoice");
parseObject.put("comment", model);
parseObject.put("user", ParseUser.getCurrentUser());
}
parseObject.put("userChoice", "none");
parseObject.pinInBackground(Four.COMMENT_CHOICE, new SaveCallback() {
#Override
public void done(ParseException e) {
if (e == null) {
score.setText(Integer.toString(model.getInt("score")));
//votes.setText((model.getInt("minusOne") + model.getInt("plusOne")) + " votes");
minusOne.setVisibility(View.VISIBLE);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setEnabled(true);
plusOne.setEnabled(true);
// minusOne.setOnClickListener(minusOneOnClickListener);
BusProvider.getInstance().post(new NewCommentChoicesAdded());
} else {
e.printStackTrace();
}
}
});
}
else{e.printStackTrace();}
}
});
} else {
e.printStackTrace();
Log.i("plus1 error", e.getMessage());
}
}
});
}
};
When the async code is done, you should update the data, not the views. After updating the data, tell the adapter that the data changed. The RecyclerView gets note of this and re-renders your view.
When working with recycling views (ListView or RecyclerView), you cannot know what item a view is representing. In your case, that view gets recycled before the async work is done and is assigned to a different item of your data.
So never modify the view. Always modify the data and notify the adapter. bindView should be the place where you treat these cases.
Chet Haase from Google discusses your exact issue in this DevBytes video.
In short, the framework need to be notified that one of the Views is in "transient" state. Once notified, the framework will not recycle this View until its "transient" flag cleared.
In your case, before you execute the async action, call setHasTransientState(true) on the child View that should change when the async action completes. This View will not be recycled until you explicitly call setHasTransientState(false) on it.
Offtopic:
It looks like you might be manipulating UI elements from background threads. Don't do that! If you can have a reference to Activity then use its runOnUiThread(Runnable action) API instead. If getting a reference to Activity is difficult, you can obtain UI thread's Handler and use its post(Runnable action) API.
Without code to look at, this is going to be difficult (if not impossible) for people to provide an exact answer. However, based on this description it sounds as though your async network loading (using an AsyncTask or custom Loader?) is not specifically tied to an element being tracked by your adapter. You'll need to have some way of tying the two together since the child View objects shown by the RecyclerView are re-used to be more efficient. This also means that if a View is being reused and there is an active async operation tied to it, that async operation will need to be canceled. Otherwise you'll see what you see now: the wrong child View being updated with content from an older async call.
I have an app in which you can upvote things, similar to Facebook likes or Reddit upvotes. You get the picture.
Well, I figured it would just be simple. I have an array column in Parse. For every message that gets an upvote, the user's name gets added to the array in that specific post's row. Except I need to prevent the user from voting an infinite amount of times. Here is what I tried after reading
final ParseQuery<ParseObject> query3 = ParseQuery.getQuery("Messages");
query3.whereContains("objectId", objectId);
query3.whereContains("PostVoteUsers", mCurrentUser.getUsername());
query3.findInBackground(new FindCallback<ParseObject>() {
List<String> list11 = new ArrayList<String>();
public void done(List<ParseObject> messages, ParseException e) {
if (e == null) {
ParseObject p = messages.get(0);
if(p.getList("PostVoteUsers").contains(mCurrentUser.getUsername())==false)
list11 = p.getList("ParseVoteUsers");
if(list11.contains(mCurrentUser.getUsername()))
uparrow.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
ParseQuery<ParseObject> query2 = ParseQuery.getQuery("Messages");
query2.getInBackground(objectId, new GetCallback<ParseObject>() {
public void done(ParseObject messages, ParseException e) {
if (e == null) {
int votes = messages.getInt("PostVotes") + 1;
messages.add("PostVoteUsers", mCurrentUser.getUsername());
messages.put("PostVotes", votes);
postvote.setText("" + votes);
messages.saveInBackground();
}
}
});
}
});
}}});
It doesn't work. Everything I try gets me with a nullpointer exception. I've tried a few other things too, but I figured I may be looking at the wrong questions.
In the class "Messages," I have an array column called "PostVoteUsers."
Once and for all, how can I find out if the user is in a specific row's array?
http://i.stack.imgur.com/a2AEq.jpg
Write a function that determines whether the user is in your object's array similar to this:
public boolean canBeLiked() {
String ids = getJSONArray("likedBy").toString();
if (ids.contains(ParseUser.getCurrentUser().getObjectId())) {
return true;
}
return false;
}
But change that to work for you. It works for me. Get the JSONArray of your ParseObject, make it a string, and then call contains(ParseUser.getCurrent().getObjectId()) on that string.