Обновление элемента RecyclerView + асинхронный сетевой вызов
У меня есть recyclerview, который работает так, как ожидалось. У меня есть кнопка в макете, которая заполняет список. Предполагается, что кнопка должна выполнить асинхронный вызов, и по результату я изменил внешний вид кнопки. Все это происходит нормально.
Но когда я нажимаю на кнопку и быстро прокручиваю список, результат асинхронного вызова обновляет новую кнопку просмотра (представление, которое вместо старого). Как я могу справиться с этим? Могу ли я иметь ручку при повторном использовании определенного вида?
Обновление:
Кодовая часть класса адаптера, которая выполняет асинхронный вызов и обновление 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());
}
}
});
}
};
Ответы
Ответ 1
Когда асинхронный код завершен, вы должны обновить данные, а не представления. После обновления данных сообщите адаптеру, что данные изменены. RecyclerView принимает это к сведению и повторно отображает ваше мнение.
При работе с видами просмотра (ListView или RecyclerView) вы не можете знать, какой элемент представляет представление. В вашем случае это представление перерабатывается до выполнения асинхронной работы и назначается другому элементу ваших данных.
Поэтому никогда не изменяйте представление. Всегда изменяйте данные и уведомляйте адаптер. bindView должен быть местом, где вы рассматриваете эти случаи.
Ответ 2
Chet Haase из Google обсуждает вашу точную проблему в этом видео DevBytes.
Вкратце, структура должна быть уведомлена о том, что одно из Представлений находится в "переходном" состоянии. После уведомления структура не будет перерабатывать этот вид до тех пор, пока его флаг "переходный" не будет очищен.
В вашем случае перед выполнением асинхронного действия вызовите setHasTransientState(true)
в дочернем представлении, которое должно измениться при завершении действия async. Этот вид не будет переработан, пока вы явно не назовете setHasTransientState(false)
на нем.
Offtopic:
Похоже, вы можете манипулировать элементами пользовательского интерфейса из фоновых потоков. Не делай этого! Если вы можете иметь ссылку на Activity
, тогда используйте его API runOnUiThread(Runnable action)
. Если получить ссылку на Activity
сложно, вы можете получить поток пользовательского интерфейса Handler
и использовать его API post(Runnable action)
.
Ответ 3
Без кода для просмотра, это будет сложно (если не невозможно) для людей, чтобы дать точный ответ. Однако на основе этого описания звучит так, как если бы ваша асинхронная загрузка сети (с использованием AsyncTask
или custom Loader
?) Не была привязана специально к элементу, отслеживаемому вашим адаптером. Вам нужно будет связать их друг с другом, так как дочерние объекты View
, показанные RecyclerView
, будут использоваться более эффективно. Это также означает, что если a View
повторно используется, и к нему привязана активная операция async, эта асинхронная операция должна быть отменена. В противном случае вы увидите, что вы видите сейчас: неправильный дочерний View
обновляется с помощью контента из более старого асинхронного вызова.