I have a ListView in which there several rows containing two buttons and a ProgressBar (Visibility:GONE) each.
My purpose is to display the ProgressBar upon click on the buttons and after completing a certain set of background operations remove that row entirely.
The problem here is that after removing the item from the ArrayList which the ListView is created upon and calling notifyDataSetChanged the row is removed successfully but the ProgressBar remains visible.
Shouldn't it be removed along with it's parent view?
Checkout the following record to see the problem in action.
Here is the source of my entire fragment:
public class FriendRequestFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "FriendRequestFragment";
ArrayList<FriendRequest> friendRequests;
#InjectView(R.id.friendRequestList)
ListView mListView;
#InjectView(R.id.noRequestsText)
TextView noRequestsText;
#InjectView(R.id.swipe)
SwipeRefreshLayout swipeRefreshLayout;
// NotificationHandler nh;
/**
* The Adapter which will be used to populate the ListView/GridView with
* Views.
*/
private FriendRequestAdapter mAdapter;
private Context c;
private boolean isProcessing = false;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public FriendRequestFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Util.trackFragment(this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_friendrequest_list, container, false);
ButterKnife.inject(this, view);
c = getActivity();
friendRequests = new ArrayList<>();
swipeRefreshLayout.setOnRefreshListener(this);
mAdapter = new FriendRequestAdapter(getActivity(), friendRequests);
mListView.setAdapter(mAdapter);
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int topRowVerticalPosition =
(view == null || view.getChildCount() == 0) ?
0 : view.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0);
Log.d(TAG, "SwipeRefresh: " + String.valueOf(firstVisibleItem == 0 && topRowVerticalPosition >= 0));
}
});
loadRequests();
return view;
}
private void loadRequests() {
// nh = new NotificationHandler(getActivity());
swipeRefreshLayout.setRefreshing(true);
Log.d(TAG, "loading requests init");
HashMap<String, Integer> params = new HashMap<>();
params.put("profile_id", Util.getCurrentProfileID(c));
final String uniqueID = Util.getCurrentProfileID(c) + String.valueOf(System.currentTimeMillis() / 1000 / 1200);
new ApiRequest(Util.URL_GET_FRIEND_REQUESTS, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String result, AjaxStatus status) {
super.callback(url, result, status);
ApiResponse apiResponse = new ApiResponse(url, result, uniqueID);
Log.d(TAG, "Friend Requests Response: " + result);
if (apiResponse.isSuccessful()) {
JSONArray jsonArray = apiResponse.getDataJSONArray();
try {
for (int i = 0; i < jsonArray.length(); i++) {
friendRequests.add(new FriendRequest(jsonArray.getJSONObject(i)));
}
mAdapter.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
mListView.setVisibility(View.VISIBLE);
} else if (apiResponse.getErrorMessage().equals("request_not_found")) {
noRequestsText.setVisibility(View.VISIBLE);
}
swipeRefreshLayout.setRefreshing(true);
}
}).setUniqueID(uniqueID).execute();
}
#Override
public void onRefresh() {
loadRequests();
}
private void acceptRequest(final int position, final View rootView) {
if (isProcessing) {
CustomToast.makeToast(getActivity(), CustomToast.TYPE_ALERT, getString(R.string.please_wait), CustomToast.LENGTH_SHORT);
return;
}
rootView.findViewById(R.id.loading).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.GONE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.GONE);
isProcessing = true;
Log.d("FriendRequest", "accepting:" + position);
FriendRequest request = friendRequests.get(position);
HashMap<String, Integer> params = new HashMap<>();
params.put("request_id", request.getRequestID());
params.put("profile_id", ProfilesSingleton.getInstance().getCurrentProfile().getProfileID());
new ApiRequest(Util.URL_ACCEPT_REQUEST, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String object, AjaxStatus status) {
super.callback(url, object, status);
ApiResponse apiResponse = new ApiResponse(object);
if (apiResponse.isSuccessful()) {
friendRequests.remove(position);
CustomToast.makeToast(getActivity(), CustomToast.TYPE_DEFAULT,
getString(R.string.you_are_now_friends_with) + " " + friendRequests.get(position).getFullName(),
CustomToast.LENGTH_SHORT);
mAdapter.notifyDataSetChanged();
}else {
rootView.findViewById(R.id.loading).setVisibility(View.GONE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.VISIBLE);
}
isProcessing = false;
}
}).execute();
}
private void denyRequest(final int position, final View rootView) {
if (isProcessing) {
CustomToast.makeToast(getActivity(), CustomToast.TYPE_ALERT, getString(R.string.please_wait), CustomToast.LENGTH_SHORT);
return;
}
rootView.findViewById(R.id.loading).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.GONE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.GONE);
Log.d("FriendRequest", "denying:" + position);
FriendRequest request = friendRequests.get(position);
HashMap<String, Integer> params = new HashMap<>();
params.put("request_id", request.getRequestID());
params.put("profile_id", ProfilesSingleton.getInstance().getCurrentProfile().getProfileID());
new ApiRequest(Util.URL_DENY_REQUEST, params, new AjaxCallback<String>() {
#Override
public void callback(String url, String object, AjaxStatus status) {
super.callback(url, object, status);
ApiResponse apiResponse = new ApiResponse(object);
if (apiResponse.isSuccessful()) {
friendRequests.remove(position);
mAdapter.notifyDataSetChanged();
}else {
rootView.findViewById(R.id.loading).setVisibility(View.GONE);
rootView.findViewById(R.id.acceptBtn).setVisibility(View.VISIBLE);
rootView.findViewById(R.id.denyBtn).setVisibility(View.VISIBLE);
}
}
}).execute();
}
public class FriendRequestAdapter extends ArrayAdapter<FriendRequest> {
public FriendRequestAdapter(Context context, ArrayList<FriendRequest> objects) {
super(context, 0, objects);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rootView = convertView;
final ViewHolder holder;
final FriendRequest friendRequest = getItem(position);
if (rootView == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rootView = inflater.inflate(R.layout.friend_request_item, parent, false);
holder = new ViewHolder();
holder.profilePhoto = (RoundedImageView) rootView.findViewById(R.id.profilePhoto);
holder.fullName = (TextView) rootView.findViewById(R.id.fullName);
holder.acceptBtn = (ImageView) rootView.findViewById(R.id.acceptBtn);
holder.denyBtn = (ImageView) rootView.findViewById(R.id.denyBtn);
holder.loading = (ProgressBar) rootView.findViewById(R.id.loading);
rootView.setTag(holder);
} else {
holder = (ViewHolder) rootView.getTag();
}
holder.fullName.setText(friendRequest.getFullName());
if (friendRequest.getFullPhotoPath().equals("")) {
ImageUtil.replaceWithInitialsView(getContext(), holder.profilePhoto, friendRequest.getInitials());
} else {
Util.aQuery.id(holder.profilePhoto).image(friendRequest.getFullPhotoPath(), false, true, 50, R.drawable.avatar_profile, null, AQuery.FADE_IN);
}
final View finalRootView = rootView;
holder.acceptBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
acceptRequest(position, finalRootView);
}
});
holder.denyBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
denyRequest(position, finalRootView);
}
});
return rootView;
}
public class ViewHolder {
RoundedImageView profilePhoto;
TextView fullName;
ImageView acceptBtn, denyBtn;
ProgressBar loading;
}
}
}
Add a field in your FriendRequest class that saves the current state of the progress bar. based on it set the visibility of the progress bar.
The same view row has been sent to another row. in your getView method you must always set the progress bar visibility based on its status.
Code Sample:
final View finalRootView = rootView;
if (friendRequest.acceptingRequestInProgress())
holder.loading.setVisibility(View.Visibile);
else
holder.loading.setVisibility(View.Gone);
holder.acceptBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
friendRequest.setAcceptingInProgress(true);
acceptRequest(position, finalRootView);
}
});
holder.denyBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
denyRequest(position, finalRootView);
}
});
Another place to modify:
if (apiResponse.isSuccessful()) {
friendRequest.setAcceptingInProgress(false);
friendRequests.remove(position);
mAdapter.notifyDataSetChanged();
}
Note: this is also handles the case when the user scrolls the list
view and the row view in progress is no longer visible. this will
hands the view to another row. But since we check the row state the
progress bar will be stopped. and when user scrolls back to the row
view in progress and hands it a reusable view the progress bar will be
visible again if accepting is still in progress.
Views are getting reused by the ListView and in the getView() method you are not cleaning up the reused view, that's why the progress bar will become visible for an item that shouldn't display it.
Similarly if an item would be removed some items with progress bars visible would loose their progress bar, handing them over to an item that didn't need it.
In getView(), after initializing the holder, you should check if progress bar is necessary.
Start with storing progress bar values at the beginning:
private ArrayList<Integer> progresses = new ArrayList<Integer>();
Update these values every time the list changes (when list changes in loadRequests and when value changes not sure where).
And in getView()
if (progresses.get(position) == 100) {
holder.loading.setVisibility(View.GONE);
} else {
holder.loading.setVisibility(View.VISIBLE);
holder.loading.setProgress(progresses.get(position));
}
The problem is due to visibility of progressbar is VISIBLE default so in getView() after you call notifyDataSetChanged(), the progressbar becomes visible to row position (i - 1).
holder.loading = (ProgressBar) rootView.findViewById(R.id.loading);
holder.loading.setVisibility(View.GONE);
Set progressbar visibility to GONE in getView() and this problem will not come
Related
The problem is that in my tablayout when im switching between tabs my list duplicating. So i need to remove list on onStop() to recreate it then. Or might be other better solution.
I have tried the following solutions
https://code-examples.net/en/q/1c97047
How to reset recyclerView position item views to original state after refreshing adapter
Remove all items from RecyclerView
My code of adapter
public class OnlineUsersAdapter extends RecyclerView.Adapter<OnlineUsersAdapter.OnlineUserViewHolder> {
private List<OnlineUser> onlineUsers = new ArrayList<>();
private OnItemClickListener.OnItemClickCallback onItemClickCallback;
private OnItemClickListener.OnItemClickCallback onChatClickCallback;
private OnItemClickListener.OnItemClickCallback onLikeClickCallback;
private Context context;
public OnlineUsersAdapter(Context context) {
this.onlineUsers = new ArrayList<>();
this.context = context;
}
#NonNull
#Override
public OnlineUsersAdapter.OnlineUserViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
context = parent.getContext();
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_user, parent, false);
return new OnlineUsersAdapter.OnlineUserViewHolder(v);
}
#Override
public void onBindViewHolder(#NonNull OnlineUsersAdapter.OnlineUserViewHolder holder, int position) {
OnlineUser user = onlineUsers.get(position);
Log.d("testList", "rating " + user.getRating() + " uid " + user.getUid());
holder.bind(user, position);
}
#Override
public int getItemCount() {
return onlineUsers.size();
}
class OnlineUserViewHolder extends RecyclerView.ViewHolder {
RelativeLayout container;
ImageView imageView, likeBtn, chatBtn;
TextView name, country;
private LottieAnimationView animationView;
OnlineUserViewHolder(View itemView) {
super(itemView);
context = itemView.getContext();
container = itemView.findViewById(R.id.item_user_container);
imageView = itemView.findViewById(R.id.user_img);
likeBtn = itemView.findViewById(R.id.search_btn_like);
chatBtn = itemView.findViewById(R.id.search_btn_chat);
name = itemView.findViewById(R.id.user_name);
country = itemView.findViewById(R.id.user_country);
animationView = itemView.findViewById(R.id.lottieAnimationView);
}
void bind(OnlineUser user, int position) {
ViewCompat.setTransitionName(imageView, user.getName());
if (FirebaseUtils.isUserExist() && user.getUid() != null) {
new FriendRepository().isLiked(user.getUid(), flag -> {
if (flag) {
likeBtn.setBackground(ContextCompat.getDrawable(context, R.drawable.ic_favorite));
animationView.setVisibility(View.VISIBLE);
} else {
likeBtn.setBackground(ContextCompat.getDrawable(context, R.drawable.heart_outline));
animationView.setVisibility(View.GONE);
}
});
}
if (user.getUid() != null) {
chatBtn.setOnClickListener(new OnItemClickListener(position, onChatClickCallback));
likeBtn.setOnClickListener(new OnItemClickListener(position, onLikeClickCallback));
}
imageView.setOnClickListener(new OnItemClickListener(position, onItemClickCallback));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
if (user.getImage().equals(Consts.DEFAULT)) {
Glide.with(context).load(context.getResources().getDrawable(R.drawable.default_avatar)).into(imageView);
} else {
Glide.with(context).load(user.getImage()).thumbnail(0.5f).into(imageView);
}
country.setText(user.getCountry());
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(500);
animator.addUpdateListener(valueAnimator ->
animationView.setProgress((Float) valueAnimator.getAnimatedValue()));
if (animationView.getProgress() == 0f) {
animator.start();
} else {
animationView.setProgress(0f);
}
}
}
public OnlineUsersAdapter(OnItemClickListener.OnItemClickCallback onItemClickCallback,
OnItemClickListener.OnItemClickCallback onChatClickCallback,
OnItemClickListener.OnItemClickCallback onLikeClickCallback) {
this.onItemClickCallback = onItemClickCallback;
this.onChatClickCallback = onChatClickCallback;
this.onLikeClickCallback = onLikeClickCallback;
}
public void addUsers(List<OnlineUser> userList) {
int initSize = userList.size();
onlineUsers.addAll(userList);
// notifyItemRangeInserted(onlineUsers.size() - userList.size(), onlineUsers.size());
}
public String getLastItemId() {
return onlineUsers.get(onlineUsers.size() - 1).getUid();
}
public void clearData() {
List<OnlineUser> data = new ArrayList<>();
addUsers(data);
notifyDataSetChanged();
}
My code in fragment
#Override
public void onStop() {
super.onStop();
firstUid = "";
stopDownloadList = false;
List<OnlineUser> list = new ArrayList<>();
mAdapter.addUsers(list);
mAdapter.notifyDataSetChanged();
}
`users are added after callback
#Override
public void addUsers(List<OnlineUser> onlineUsers) {
if (firstUid.equals("")){
firstUid = onlineUsers.get(0).getUid();
}
if (!firstUid.equals("") && onlineUsers.contains(firstUid)){
stopDownloadList = true;
}
if (!stopDownloadList){
mAdapter.addUsers(onlineUsers);
}
setRefreshProgress(false);
isLoading = false;
isMaxData = true;
}
The line mAdapter.addUsers(onlineUsers); from addUsers method gets called twice. Looks like your asynchronous operation gets triggered twice (e. g. from repeating lifecycle methods like onCreate/onCreateView/onViewCreated).
Solution #1: request users a single time
Move your user requesting machinery to onCreate or onAttach. This will save network traffic but could lead to showing outdated data.
Solution #2: replaceUsers
Your clearData calls mAdapter.addUsers(new ArrayList<>()); (btw, take a look at Collections.emptyList()). Looks like you're trying to replace adapter data but appending instead. Replacement method could look like
public void replaceUsers(List<OnlineUser> userList) {
int oldSize = userList.size();
onlineUsers = userList;
notifyItemRangeRemoved(0, oldSize);
notifyItemRangeInserted(0, userList.size);
}
This version still requeses users every time your fragment gets focused but shows fresher data.
Gonna do some background so you can picture better what I need to accomplish.
Well the thing is, I'm reading data from a DataBase, this data changes relatively fast, let's say every 30 seconds, I need to scroll the recycler to check the new data and press some buttons, every time I press the button, it changes its color, but 2 things are happening.
1- I refresh the recycler with a timer every second so, when I'm scrolling it and the refresh comes, it will go all the way to the first item and that can't happen, so I need a way to prevent this.
2- every data that comes from the Database needs some adjustment and for that, I press a button, this button changes it´s color when I press it so I know the action for that button is done but, everytime the timer refreshes the recycler it not only goes to the first item but also turns the buttons to the original color because is re-creating the entire recycler with the data in the database, and that Can Not Happen, when the timer refreshes the recycler, the old items should remain the same and only add the NEW IF there's any new.
well here's my code for the adapter and for the Activity, thanks in advance for the help.
Here's the Adapter
public class ComandaAdapter extends
RecyclerView.Adapter<ComandaAdapter.ComandaAdapterViewHolder>
implements View.OnClickListener{
private ArrayList<Comanda> list_comandas;
private Context context;
private View.OnClickListener listener;
public ComandaAdapter(Context con, ArrayList<Comanda> list) {
this.context = con;
this.list_comandas = list;
}
#Override
public ComandaAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row, parent, false);
return new ComandaAdapterViewHolder(itemView);
}
#Override
public void onBindViewHolder(ComandaAdapterViewHolder holder, int position) {
Integer anchomanda = Math.round(context.getResources().getDimension(R.dimen.parents_size));
// Llenar la información de cada item
Comanda comanda = list_comandas.get(position);
comanda.setNro_comanda(position+"");
String cadena = comanda.getOrden();
Integer tope = cadena.length();
Boolean tijera_categoria=false;
Boolean tijera_articulo=true;
Boolean tijera_contorno=true;
Boolean tijera_cambio =true;
Integer indisup;
Integer indiin =0;
char apuntador;
String Buscado ="";
String Buscado_contorno="";
String Buscado_categoria="";
Integer id=0;
holder.txt_comanda.setText(position+"");
holder.txt_mesa.setText(comanda.getMesa());
for (int i = 0; i < tope ; i++) {
apuntador = cadena.charAt(i);
if (Buscado.equals("Bebidas")){
break;
}
else
{
if (apuntador == '$')
{
break;
}
else
{
//CUERPO PRINCIPAL DE EJECUCION
if (apuntador == '#' && !tijera_categoria)
{
if (i==0) {
indiin = i + 1;
}
}
if (apuntador == '!' && !tijera_categoria)
{
tijera_categoria=true;
tijera_articulo=false;
indisup=i;
id=i;
Buscado=cadena.substring(indiin,indisup);
indiin=indisup+1;
Buscado_categoria=Buscado;
holder.b[id].setId(id);
}
if (apuntador == '%' && !tijera_articulo)
{
indisup=i;
tijera_articulo=true;
tijera_contorno=false;
Buscado=cadena.substring(indiin,indisup);
indiin=indisup+1;
holder.b[id].setLayoutParams(new LinearLayout.LayoutParams(anchomanda, LinearLayout.LayoutParams.WRAP_CONTENT));
holder.b[id].setTextSize((context.getResources().getDimension(R.dimen.txt_size)) / 2);
if (Buscado_categoria.equals("Fondos")) {
holder.b[id].setBackgroundTintList(context.getResources().getColorStateList(R.color.fondos, null));
}
if (Buscado_categoria.equals("Entradas")) {
holder.b[id].setBackgroundTintList(context.getResources().getColorStateList(R.color.entradas, null));
}
if (Buscado_categoria.equals("Postres")) {
holder.b[id].setBackgroundTintList(context.getResources().getColorStateList(R.color.postres, null));
}
holder.b[id].setText(Buscado);
holder.lyocomanda.addView(holder.b[id]);
}
if (apuntador == '*' && !tijera_contorno)
{
indisup=i;
tijera_cambio=false;
Buscado=cadena.substring(indiin,indisup);
indiin=indisup+1;
if (!Buscado.equals("")) {
Buscado_contorno=Buscado;
holder.t[i].setText(Buscado);
holder.t[i].setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
holder.t[i].setTextSize((context.getResources().getDimension(R.dimen.txt_size)) / 2);
holder.l[id].addView(holder.t[i]);
}
}
if (apuntador == '#' && !tijera_cambio)
{
indisup=i;
tijera_contorno=true;
tijera_cambio=true;
tijera_categoria=false;
Buscado=cadena.substring(indiin,indisup);
indiin=indisup+1;
if (!Buscado_contorno.equals("")) {
holder.l[id].setLayoutParams(new LinearLayout.LayoutParams(anchomanda, ViewGroup.LayoutParams.WRAP_CONTENT));
holder.l[id].setOrientation(LinearLayout.VERTICAL);
holder.l[id].setBackground(context.getDrawable(customborder));
holder.lyocomanda.addView(holder.l[id]);
}
}
//FIN CUERPO PRINCIPAL DE EJECUCION
} //EJECUCION DE DESCARTE DE FINAL DE CADENA
} //EJECUCION DE DESCARTE DE BEBIDAS
}
}
#Override
public int getItemCount() {
return list_comandas.size();
}
public void removeItem(int position) {
list_comandas.remove(position);
notifyItemRemoved(position);
}
#Override
public void onClick(View view) {
if (listener != null)
{
listener.onClick(view);
}
}
public void setOnClickListener(View.OnClickListener listener)
{
this.listener = listener;
}
public class ComandaAdapterViewHolder extends RecyclerView.ViewHolder {
TextView txt_comanda, txt_mesa;
private LinearLayout lyocomanda;
private Integer cant_platos =500;
private TextView[] t = new TextView[(cant_platos*8)];
private LinearLayout[] l = new LinearLayout[cant_platos];
private Button[] b = new Button[cant_platos];
public ComandaAdapterViewHolder(View itemView) {
super(itemView);
// Inicializamos los controles
lyocomanda = (LinearLayout) itemView.findViewById(R.id.lyocomanda);
txt_comanda = (TextView) itemView.findViewById(R.id.txt_comanda);
txt_mesa = (TextView) itemView.findViewById(R.id.txt_mesa);
for (int i = 0; i <cant_platos; i++) {
/////////////////////////////CONFIGURACION DEL BOTON///////////////////////////
b[i] = new Button(itemView.getContext());
b[i].setOnClickListener(listener);
///////////////////////////CONFIGURACION DEL CONTORNO///////////////////////////
l[i] = new LinearLayout(itemView.getContext());
t[i] = new TextView(itemView.getContext());
}
}
}
}
Here´s the Activity
public class MainActivity extends AppCompatActivity {
private ComandaAdapter mComandaAdapter;
ArrayList<Comanda> lista_Comanda;
RecyclerView rec_Lista;
public int counter = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rec_Lista = (RecyclerView) findViewById(R.id.rec_lista);
new CountDownTimer(10000, 100){
#Override
public void onTick(long millisUntilFinished) {
counter++;
}
#Override
public void onFinish() {
try{
loadRetrofitComanda();
counter = 0;
start();
}
catch (Exception e){
mostrarMensaje("Error: " + e.getMessage());
}
}
}.start();
}
#Override
protected void onResume() {
super.onResume();
loadRetrofitComanda();
}
private void loadRetrofitComanda()
{
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://192.168.0.20:3000")
.addConverterFactory(GsonConverterFactory.create())
.build();
IRequestComanda request = retrofit.create(IRequestComanda.class);
Call<ArrayList<Comanda>> call = request.getJSONComandas();
call.enqueue(new Callback<ArrayList<Comanda>>() {
#Override
public void onResponse(Call<ArrayList<Comanda>> call, Response<ArrayList<Comanda>> response) {
ArrayList<Comanda> lista = response.body();
lista_Comanda = lista;
// Refresh recyclerview
setAdapter();
configurarOrientacionLayout();
}
#Override
public void onFailure(Call<ArrayList<Comanda>> call, Throwable t) {
mostrarMensaje("Error: " + t.getMessage());
}
});
}
private void mostrarMensaje(String mensaje)
{
Toast.makeText(getApplicationContext(), mensaje, Toast.LENGTH_SHORT).show();
}
private void setAdapter()
{
mComandaAdapter = new ComandaAdapter(getApplicationContext(), lista_Comanda);
mComandaAdapter.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
mostrarMensaje("ejecutar accion");
}
});
rec_Lista.setAdapter(mComandaAdapter);
}
private void configurarOrientacionLayout()
{
rec_Lista.setLayoutManager(new L LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
}
}
a little update, I have managed to keep the recyclerview state when new data arrives with 2 lines of code :
recyclerViewState = rec_Lista.getLayoutManager().onSaveInstanceState();//save
setAdapter();
configurarOrientacionLayout();
rec_Lista.getLayoutManager().onRestoreInstanceState(recyclerViewState);//restore
by saving and restoring the layout manager befor and after calling the setAdapter method, so 1 more problem to go.. the scroll position for each recycler item.
Solved the final Issue with this.
mComandaAdapter.notifyItemInserted(lista_Comanda.size());
so i check if new items has arrived, if so, I just isolate the item and add it to the recycler and everything else remains unchanged.. Im not setting the adapter everytime new data arrives now.
hope this can help anyone.
RecyclerView does this out of the box when using a ListAdapter, which in turn uses DiffUtil. You then just have to inform the adapter of the new list via submitList(newList).
DiffUtil takes an old list and a new list and figures out what's different. It finds which items were added, removed, or changed, and RecyclerView can use that information to update those items, which is much more efficient than redoing the entire list.
The RecyclerView takes care of presenting it smoothly to the user.
I have an app that calling an API its resulting around 500 rows creation.In my app the row content can update in the detail page. So after update is there any possibility to update the row in the Recycler View without calling the API again.
Activity
AsyncHttpClient client = new AsyncHttpClient();
client.get("URL", new AsyncHttpResponseHandler() {
#Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
try {
String jsonStr = new String(responseBody, "UTF-8");
if (jsonStr != null) {
try {
JSONArray jsonArray = new JSONArray(jsonStr);
JSONObject cStatus = jsonArray.getJSONObject(jsonArray.length()-1);
for (int i = 0; i < jsonArray.length(); i++)
{
JSONObject c = jsonArray.getJSONObject(i);
String firstName = c.getString("firstName");
String subDistributerId = c.getString("subDistributerId");
SubdistributorItem item = new SubdistributorItem();
item.setSubDistributorName(firstName);
item.setSubDistributorId(subDistributerId);
ubdistributorItemList.add(item);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
Collections.sort(subdistributorItemList, new Comparator<SubdistributorItem>() {
public int compare(SubdistributorItem o1, SubdistributorItem o2) {
return o1.getSubDistributorName().compareToIgnoreCase(o2.getSubDistributorName());
}
});
adapter = new SubdistributorRecyclerAdapter(getActivity(),subdistributorItemList);
mRecyclerView.setAdapter(adapter);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
});
Adapter Class
public class SubdistributorRecyclerAdapter extends RecyclerView.Adapter<SubdistributorListRowHolder> {
private List<SubdistributorItem> subdistributorItems;
private Context mContext;
private ArrayList<SubdistributorItem> arraylist;
public SubdistributorRecyclerAdapter(Context context, List<SubdistributorItem> subdistributorItems) {
this.subdistributorItems = subdistributorItems;
this.mContext = context;
this.arraylist = new ArrayList<SubdistributorItem>();
this.arraylist.addAll(subdistributorItems);
}
#Override
public SubdistributorListRowHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.subdistributor_list_item, null);
SubdistributorListRowHolder mh = new SubdistributorListRowHolder(v);
layout_subdistributor.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
Intent i = new Intent(mContext, SubdistributorDetail.class);
Log.e("Tag shop ", "ShopKeeper Detail called");
i.putExtra("subDistributorStatus", txt_RechargeSubdistributor.getText().toString());
i.putExtra("subDistributorId", txt_subDistributorId.getText().toString());
mContext.startActivity(i);
}
});
return mh;
}
#Override
public void onBindViewHolder(SubdistributorListRowHolder subDistributorListRowHolder, int i) {
SubdistributorItem subdistributorItem = subdistributorItems.get(i);
Log.e("Tag ", "adapter "+ subdistributorItem.getSubDistributorName());
}
#Override
public int getItemCount() {
return (null != subdistributorItems ? subdistributorItems.size() : 0);
}
}
So can any one please help me to update a single row in a list without calling the API again.
Try using this method, to update a single row.
notifyItemChanged(int position)
Update item in your list subdistributorItems. Then call adapter.notifyItemChanged(int position). You can get a position in ClickListener using
layout_subdistributor.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (mh.getAdapterPosition() != RecyclerView.NO_POSITION) {
int position = mh.getAdapterPosition();
// edit your object by calling subdistributorItems.get(position)
}
}
});
Change this
public static class SubdistributorListRowHolder extends RecyclerView.ViewHolder {
private TextView textView_alphabet;
private TextView textView_name;
private TextView textView_tag;
private ImageView imageViewUserImage;
private ImageView imageViewMoreButton;
private LinearLayout linearLayoutMainContent;
public ViewHolder(View itemLayoutView) {
super(itemLayoutView);
textView_alphabet = (TextView) itemLayoutView.findViewById(R.id.textView_alphabet);
textView_name = (TextView) itemLayoutView.findViewById(R.id.textView_name);
textView_tag = (TextView) itemLayoutView.findViewById(R.id.textView_tag);
imageViewUserImage = (ImageView) itemLayoutView.findViewById(R.id.imageViewUserImage);
linearLayoutMainContent = (LinearLayout) itemLayoutView.findViewById(R.id.linearLayoutMainContent);
}
}
#Override
public void onBindViewHolder(SubdistributorListRowHolder subDistributorListRowHolder, int i) {
SubdistributorItem subdistributorItem = subdistributorItems.get(i);
Log.e("Tag ", "adapter "+ subdistributorItem.getSubDistributorName());
subDistributorListRowHolder.itemLayoutView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.e("Tag ", "Position "+ i);
}
});
}
Over here itemLayoutView is the mail layout which is clickable
Modify the SubdistributorListRowHolder like this put all your layout component and find view by id.
getViewForPosition(int position)
Obtain a view initialized for the given position. This method should be used by RecyclerView.LayoutManager implementations to obtain views to represent data from an RecyclerView.Adapter.
The Recycler may reuse a scrap or detached view from a shared pool if one is available for the correct view type. If the adapter has not indicated that the data at the given position has changed, the Recycler will attempt to hand back a scrap view that was previously initialized for that data without rebinding.
description here
Its simple.
Make the changes in the ArrayList that is bonded with the recycler view and call notifyDataSetChanged() on the Recycler View.
I'm working on an app that displays a working schedule on a time line.
This is a rough layout of how the app is designed at the moment:
The data is stored in an SQLite DB. When the Timeline (a singleton object) requests the data from the database helper class, it gets an ArrayList of Events (e.g. an Event could be a duty starting at the 1st of May 2016 at 03:00 and ending at the 3rd of May 2016 at 16:00). The Timeline then transforms these Events to TimelineItems, a class representing (part of) an Event for a particular day.
The loading of Events and the transformation of Events to TimelineItems both are done in AsyncTasks. So far so good.
Now comes the part I'm struggling with: updating the UI after a new DB fetch.
My first approach was to pass the updated ArrayList of TimelineItems to the RecyclerView adapter and let the the adapter know the data has changed with notifyDatasetChanged(). The problem with this approach is that
1) a lot of unnecessary work is being done (cause we're recalculating all Events/TimelineItems, not only the ones changed) and
2) the scroll position on the RecyclerView is reset after every DB fetch
In my 2nd approach, I've implemented some methods to check which Events/TimelineItems have changed since the last display with the idea of only changing those TimelineItems, with notifyItemChanged(). Less work is being done and no need to worry about scroll positions at all. The tricky bit is that checking which items have changed does take some time, so it needs to be done async as well:
I tried to do the code manipulations in doInBackground() and the UI updating by posting otto bus events in onProgressUpdate().
private class InsertEventsTask extends AsyncTask<Void, Integer, Void> {
#Override
protected Void doInBackground(Void... params) {
ArrayList<Event> events = mCachedEvents;
// if mChangedEvents is not null and not empty
if (events != null && !events.isEmpty()) {
// get the list of pairs for the events
ArrayList<TimelineItemForDateTimePair> listOfPairs = convertEventsToPairs(events);
// insert the TimelineItems from the pairs into the Timeline
for (int i = 0; i < listOfPairs.size(); i++) {
// get the last position for the DateTime associated with the pair
int position = findLastPositionForDate(listOfPairs.get(i).dateTime);
// if position is -1, the events started on a day before the timeline starts
// so keep skipping pairs until position > -1
if (position > -1) {
// if the item is a PlaceholderItem
if (mTimelineItems.get(position).isPlaceholderItem) {
// remove the PlaceholderItem
mTimelineItems.remove(position);
// and add the TimelineItem from the pair at the position the PlaceholderItem was at
mTimelineItems.add(position, listOfPairs.get(i).timelineItem);
// update the UI on the UI thread
publishProgress(position, TYPE_CHANGED);
} else { // if the item is not a PlaceholderItem, there's already an normal TimelineItem in place
// place the new item at the next position on the Timeline
mTimelineItems.add(position + 1, listOfPairs.get(i).timelineItem);
publishProgress(position, TYPE_ADDED);
}
}
}
}
return null;
}
/**
* onProgressUpdate handles the UI changes on the UI thread for us. Type int available:
* - TYPE_CHANGED
* - TYPE_ADDED
* - TYPE_DELETED
*
* #param values value[0] is the position as <code>int</code>,
* value[1] is the type of manipulation as <code>int</code>
*/
#Override
protected void onProgressUpdate(Integer... values) {
int position = values[0];
int type = values[1];
// update the UI for each changed/added/deleted TimelineItem
if (type == TYPE_CHANGED) {
BusProvider.getInstance().post(new TimelineItemChangedNotification(position));
} else if (type == TYPE_ADDED) {
BusProvider.getInstance().post((new TimelineItemAddedNotification(position)));
} else if (type == TYPE_DELETED) {
// TODO: make delete work bro!
}
}
}
The problem is, that somehow, scrolling while this progress is being posted messes up the UI completely.
My main problem is: when I update a specific item in the data set (TimelineItems) of the adapter, notifyItemChanged() does change the item but doesn't put the item at the correct position.
Here's my adapter:
/**
* A custom RecyclerView Adapter to display a Timeline in a TimelineFragment.
*/
public class TimelineAdapter extends RecyclerView.Adapter<TimelineAdapter.TimelineItemViewHolder> {
/*************
* VARIABLES *
*************/
private ArrayList<TimelineItem> mTimelineItems;
/****************
* CONSTRUCTORS *
****************/
/**
* Constructor with <code>ArrayList<TimelineItem></code> as data set argument.
*
* #param timelineItems ArrayList with TimelineItems to display
*/
public TimelineAdapter(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// Create new views (invoked by the layout manager)
#Override
public TimelineItemViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_timeline, parent, false);
// set the view's size, margins, paddings and layout parameters
// ...
return new TimelineItemViewHolder(v);
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(TimelineItemViewHolder holder, int position) {
// - get element from your data set at this position
// - replace the contents of the view with that element
// if the item is a ShowPreviousMonthsItem, set the showPreviousMonthsText accordingly
if (mTimelineItems.get(position).isShowPreviousMonthsItem) {
holder.showPreviousMonthsText.setText(mTimelineItems.get(position).showPreviousMonthsText);
} else { // otherwise set the showPreviousMonthsText blank
holder.showPreviousMonthsText.setText("");
}
// day of month & day of week of the TimelineItem
if (mTimelineItems.get(position).isFirstItemOfDay) {
holder.dayOfWeek.setText(mTimelineItems.get(position).dayOfWeek);
holder.dayOfMonth.setText(mTimelineItems.get(position).dayOfMonth);
} else {
holder.dayOfWeek.setText("");
holder.dayOfMonth.setText("");
}
// Event name for the TimelineItem
holder.name.setText(mTimelineItems.get(position).name);
// place and goingTo of the TimelineItem
// if combinedPlace == ""
if(mTimelineItems.get(position).combinedPlace.equals("")) {
if (mTimelineItems.get(position).isFirstDayOfEvent) {
holder.place.setText(mTimelineItems.get(position).place);
} else {
holder.place.setText("");
}
if (mTimelineItems.get(position).isLastDayOfEvent) {
holder.goingTo.setText(mTimelineItems.get(position).goingTo);
} else {
holder.goingTo.setText("");
}
holder.combinedPlace.setText("");
} else {
holder.place.setText("");
holder.goingTo.setText("");
holder.combinedPlace.setText(mTimelineItems.get(position).combinedPlace);
}
if(mTimelineItems.get(position).startDateTime != null) {
holder.startTime.setText(mTimelineItems.get(position).startDateTime.toString("HH:mm"));
} else {
holder.startTime.setText("");
}
if(mTimelineItems.get(position).endDateTime != null) {
holder.endTime.setText(mTimelineItems.get(position).endDateTime.toString("HH:mm"));
} else {
holder.endTime.setText("");
}
if (!mTimelineItems.get(position).isShowPreviousMonthsItem) {
if (mTimelineItems.get(position).date.getDayOfWeek() == DateTimeConstants.SUNDAY) {
holder.dayOfWeek.setTextColor(Color.RED);
holder.dayOfMonth.setTextColor(Color.RED);
} else {
holder.dayOfWeek.setTypeface(null, Typeface.NORMAL);
holder.dayOfMonth.setTypeface(null, Typeface.NORMAL);
holder.dayOfWeek.setTextColor(Color.GRAY);
holder.dayOfMonth.setTextColor(Color.GRAY);
}
} else {
((RelativeLayout) holder.dayOfWeek.getParent()).setBackgroundColor(Color.WHITE);
}
holder.bindTimelineItem(mTimelineItems.get(position));
}
// Return the size of the data set (invoked by the layout manager)
#Override
public int getItemCount() {
return mTimelineItems.size();
}
// replace the data set
public void setTimelineItems(ArrayList<TimelineItem> timelineItems) {
this.mTimelineItems = timelineItems;
}
// replace an item in the data set
public void swapTimelineItemAtPosition(TimelineItem item, int position) {
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
notifyItemChanged(position);
}
// the ViewHolder class containing the relevant views,
// also binds the Timeline item itself to handle onClick events
public class TimelineItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
protected TextView dayOfWeek;
protected TextView dayOfMonth;
protected TextView showPreviousMonthsText;
protected TextView name;
protected TextView place;
protected TextView combinedPlace;
protected TextView goingTo;
protected TextView startTime;
protected TextView endTime;
protected TimelineItem timelineItem;
public TimelineItemViewHolder(View view) {
super(view);
view.setOnClickListener(this);
this.dayOfWeek = (TextView) view.findViewById(R.id.day_of_week);
this.dayOfMonth = (TextView) view.findViewById(R.id.day_of_month);
this.showPreviousMonthsText = (TextView) view.findViewById(R.id.load_previous_data);
this.name = (TextView) view.findViewById(R.id.name);
this.place = (TextView) view.findViewById(R.id.place);
this.combinedPlace = (TextView) view.findViewById(R.id.combined_place);
this.goingTo = (TextView) view.findViewById(R.id.going_to);
this.startTime = (TextView) view.findViewById(R.id.start_time);
this.endTime = (TextView) view.findViewById(R.id.end_time);
}
public void bindTimelineItem(TimelineItem item) {
timelineItem = item;
}
// handles the onClick of a TimelineItem
#Override
public void onClick(View v) {
// if the TimelineItem is a ShowPreviousMonthsItem
if (timelineItem.isShowPreviousMonthsItem) {
BusProvider.getInstance().post(new ShowPreviousMonthsRequest());
}
// if the TimelineItem is a PlaceholderItem
else if (timelineItem.isPlaceholderItem) {
Toast.makeText(v.getContext(), "(no details)", Toast.LENGTH_SHORT).show();
}
// else the TimelineItem is an actual event
else {
Toast.makeText(v.getContext(), "eventId = " + timelineItem.eventId, Toast.LENGTH_SHORT).show();
}
}
}
And this is the method that is triggered in the TimelineFragment when a change is posted on the event bus:
#Subscribe
public void onTimelineItemChanged(TimelineItemChangedNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
mAdapter.notifyItemChanged(position);
Log.d(TAG, "Item for position " + position + " swapped");
}
A thing to note is that the data set of the adapter seems to display correctly after I scrolled away from the changed data far enough and return to the position after that. Initially the UI is totally messed up though.
EDIT:
I found that adding
mAdapter.notifyItemRangeChanged(position, mAdapter.getItemCount());
resolves the issue but - unfortunately - sets the scroll position to the one being changed :(
Here's my TimelineFragment:
/**
* Fragment displaying a Timeline using a RecyclerView
*/
public class TimelineFragment extends BackHandledFragment {
// DEBUG flag and TAG
private static final boolean DEBUG = false;
private static final String TAG = TimelineFragment.class.getSimpleName();
// variables
protected RecyclerView mRecyclerView;
protected TimelineAdapter mAdapter;
protected LinearLayoutManager mLinearLayoutManager;
protected Timeline mTimeline;
protected MenuItem mMenuItemScroll2Today;
protected MenuItem mMenuItemReload;
protected String mToolbarTitle;
// TODO: get the value of this boolean from the shared preferences
private boolean mUseTimelineItemDividers = true;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get a handle to the app's Timeline singleton
mTimeline = Timeline.getInstance();
setHasOptionsMenu(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
rootView.setTag(TAG);
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.timeline_list);
mRecyclerView.hasFixedSize();
// LinearLayoutManager constructor
mLinearLayoutManager = new LinearLayoutManager(getActivity());
// set the layout manager
setRecyclerViewLayoutManager();
// adapter constructor
mAdapter = new TimelineAdapter(mTimeline.mTimelineItems);
// set the adapter for the RecyclerView.
mRecyclerView.setAdapter(mAdapter);
// add lines between the different items if using them
if (mUseTimelineItemDividers) {
RecyclerView.ItemDecoration itemDecoration =
new TimelineItemDivider(this.getContext());
mRecyclerView.addItemDecoration(itemDecoration);
}
// add the onScrollListener
mRecyclerView.addOnScrollListener(new TimelineOnScrollListener(mLinearLayoutManager) {
// when the first visible item on the Timeline changes,
// adjust the Toolbar title accordingly
#Override
public void onFirstVisibleItemChanged(int position) {
mTimeline.mCurrentScrollPosition = position;
try {
String title = mTimeline.mTimelineItems
.get(position).date
.toString(TimelineConfig.TOOLBAR_DATE_FORMAT);
// if mToolbarTitle is null, set it to the new title and post on bus
if (mToolbarTitle == null) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is null - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
} else { // if mToolbarTitle is not null
// only post on the bus if the new title is different from the previous one
if (!title.equals(mToolbarTitle)) {
if (DEBUG)
Log.d(TAG, "mToolbarTitle is NOT null, but new title detected - posting new title request on bus: " + title);
mToolbarTitle = title;
BusProvider.getInstance().post(new ChangeToolbarTitleRequest(mToolbarTitle));
}
}
} catch (NullPointerException e) {
// if the onFirstVisibleItemChanged is called on a "ShowPreviousMonthsItem",
// leave the title as it is
}
}
});
return rootView;
}
/**
* Set RecyclerView's LayoutManager to the one given.
*/
public void setRecyclerViewLayoutManager() {
int scrollPosition;
// If a layout manager has already been set, get current scroll position.
if (mRecyclerView.getLayoutManager() != null) {
scrollPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition();
} else {
scrollPosition = mTimeline.mFirstPositionForToday;
}
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.scrollToPositionWithOffset(scrollPosition, 0);
}
// set additional menu items for the Timeline fragment
#Override
public void onPrepareOptionsMenu(Menu menu) {
// scroll to today
mMenuItemScroll2Today = menu.findItem(R.id.action_scroll2today);
mMenuItemScroll2Today.setVisible(true);
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
mMenuItemScroll2Today.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
// get today's position
int todaysPosition = mTimeline.mFirstPositionForToday;
// scroll to today's position
mLinearLayoutManager.scrollToPositionWithOffset(todaysPosition, 0);
return false;
}
});
// reload data from Hacklberry
mMenuItemReload = menu.findItem(R.id.action_reload_from_hacklberry);
mMenuItemReload.setVisible(true);
mMenuItemReload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem item) {
// stop scrolling
mRecyclerView.stopScroll();
//
mTimeline.reloadDBForCurrentMonth();
mTimeline.loadEventsFromUninfinityDBAsync(mTimeline.mTimelineStart, mTimeline.mTimelineEnd);
return false;
}
});
super.onPrepareOptionsMenu(menu);
}
#Override
public void onResume() {
super.onResume();
// if the Timeline has been invalidated, let AllInOneActivity know it needs to replace
// this Fragment with a new one
if (mTimeline.isInvalidated()) {
Log.d(TAG, "posting TimelineInvalidatedNotification on the bus ...");
BusProvider.getInstance().post(
new TimelineInvalidatedNotification());
}
// fetch today's menu icon
if (mMenuItemScroll2Today != null) {
if (DEBUG) Log.d(TAG, "fetching scroll2today menu icon");
mMenuItemScroll2Today.setIcon(Timeline.getIconForDateTime(new DateTime()));
}
}
// from BackHandledFragment
#Override
public String getTagText() {
return TAG;
}
// from BackHandledFragment
#Override
public boolean onBackPressed() {
return false;
}
#Subscribe
public void onHacklberryReloaded(HacklberryLoadedNotification notification) {
resetReloading();
}
// handles ShowPreviousMonthsRequests posted on the bus by the TimelineAdapter's ShowPreviousMonthsItem onClick()
#Subscribe
public void onShowPreviousMonthsRequest(ShowPreviousMonthsRequest request) {
// create an empty OnItemTouchListener to prevent the user from manipulating
// the RecyclerView while it loads more data (would mess up the scroll position)
EmptyOnItemTouchListener listener = new EmptyOnItemTouchListener();
// add it to the RecyclerView
mRecyclerView.addOnItemTouchListener(listener);
// load the previous months (= add the required TimelineItems)
int newScrollToPosition = mTimeline.showPreviousMonths();
// pass the new data set to the TimelineAdapter
mAdapter.setTimelineItems(mTimeline.mTimelineItems);
// notify the adapter the data set has changed
mAdapter.notifyDataSetChanged();
// scroll to the last scroll (updated) position
mLinearLayoutManager.scrollToPositionWithOffset(newScrollToPosition, 0);
}
#Subscribe
public void onTimelineItemChanged(TimelineItemChangeNotification notification) {
int position = notification.position;
Log.d(TAG, "TimelineItemChanged detected for position " + position);
mAdapter.swapTimelineItemAtPosition(mTimeline.mTimelineItems.get(position), position);
//mAdapter.notifyItemRangeChanged(position, position);
Log.d(TAG, "Item for position " + position + " swapped");
}
I've taken a screenshot of the app after it first loads. I'll explain real quick what happens on initialisation:
the Timeline is built by populating all days with PlaceholderItems (a TimelineItem with just a Date).
Events are loaded from the DB and transformed to TimelineItems
Whenever a new TimelineItem has changed and is ready, the Timeline pokes the TimelineFragment via the otto bus to update the data set of the adapter for that particular position with the new TimelineItem.
Here's a screenshot of what happens after the initial load:
the Timeline is loaded but certain items are inserted at the wrong position.
When scrolling away and returning to the range of days that was displayed incorrectly before, all is good:
About your second approach. Probably your code is not workind because you have Data Race on mTimelineItems and mCachedEvents. I can't see all of your code, but it seems that you using mTimelineItems inside doInBackground() simultaneously with the UI thread without any synchronization.
I propose you to make a mix of your first and second approaches:
Make a copy of the original data (mTimelineItems) and send it to the AsyncTask.
Change the copy asynchronously in doInBackground() and log all changes.
Return the changed data and logs to the UI thread.
Apply the new data to the RecyclerView by using logs.
Let me illustrate this approach in code.
Data management:
public class AsyncDataUpdater
{
/**
* Example data entity. We will use it
* in our RecyclerView.
*/
public static class TimelineItem
{
public final String name;
public final float value;
public TimelineItem(String name, float value)
{
this.name = name;
this.value = value;
}
}
/**
* That's how we will apply our data changes
* on the RecyclerView.
*/
public static class Diff
{
// 0 - ADD; 1 - CHANGE; 2 - REMOVE;
final int command;
final int position;
Diff(int command, int position)
{
this.command = command;
this.position = position;
}
}
/**
* And that's how we will notify the RecyclerView
* about changes.
*/
public interface DataChangeListener
{
void onDataChanged(ArrayList<Diff> diffs);
}
private static class TaskResult
{
final ArrayList<Diff> diffs;
final ArrayList<TimelineItem> items;
TaskResult(ArrayList<TimelineItem> items, ArrayList<Diff> diffs)
{
this.diffs = diffs;
this.items = items;
}
}
private class InsertEventsTask extends AsyncTask<Void, Void, TaskResult>
{
//NOTE: this is copy of the original data.
private ArrayList<TimelineItem> _old_items;
InsertEventsTask(ArrayList<TimelineItem> items)
{
_old_items = items;
}
#Override
protected TaskResult doInBackground(Void... params)
{
ArrayList<Diff> diffs = new ArrayList<>();
try
{
//TODO: long operation(Database, network, ...).
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
//Some crazy manipulation with data...
//NOTE: we change the copy of the original data!
Random rand = new Random();
for(int i = 0; i < 10; i ++)
{
float rnd = rand.nextFloat() * 100.0f;
for(int j = 0; j < _old_items.size(); j++)
{
if(_old_items.get(j).value > rnd)
{
TimelineItem item = new TimelineItem("Item " + rnd, rnd);
//Change data.
_old_items.add(j, item);
//Log the changes.
diffs.add(new Diff(0, j));
break;
}
}
}
for(int i = 0; i < 5; i ++)
{
int rnd_index = rand.nextInt(_old_items.size());
//Change data.
_old_items.remove(rnd_index);
//Log the changes.
diffs.add(new Diff(2, rnd_index));
}
//...
return new TaskResult(_old_items, diffs);
}
#Override
protected void onPostExecute(TaskResult result)
{
super.onPostExecute(result);
//Apply the new data in the UI thread.
_items = result.items;
if(_listener != null)
_listener.onDataChanged(result.diffs);
}
}
private DataChangeListener _listener;
private InsertEventsTask _task = null;
/** Managed data. */
private ArrayList<TimelineItem> _items = new ArrayList<>();
public AsyncDataUpdater()
{
// Some test data.
for(float i = 10.0f; i <= 100.0f; i += 10.0f)
_items.add(new TimelineItem("Item " + i, i));
}
public void setDataChangeListener(DataChangeListener listener)
{
_listener = listener;
}
public void updateDataAsync()
{
if(_task != null)
_task.cancel(true);
// NOTE: we should to make the new copy of the _items array.
_task = new InsertEventsTask(new ArrayList<>(_items));
_task.execute();
}
public int getItemsCount()
{
return _items.size();
}
public TimelineItem getItem(int index)
{
return _items.get(index);
}
}
Using in UI:
public class MainActivity extends AppCompatActivity
{
private static class ViewHolder extends RecyclerView.ViewHolder
{
private final TextView name;
private final ProgressBar value;
ViewHolder(View itemView)
{
super(itemView);
name = (TextView)itemView.findViewById(R.id.tv_name);
value = (ProgressBar)itemView.findViewById(R.id.pb_value);
}
void bind(AsyncDataUpdater.TimelineItem item)
{
name.setText(item.name);
value.setProgress((int)item.value);
}
}
private static class Adapter extends RecyclerView.Adapter<ViewHolder>
implements AsyncDataUpdater.DataChangeListener
{
private final AsyncDataUpdater _data;
Adapter(AsyncDataUpdater data)
{
_data = data;
_data.setDataChangeListener(this);
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
return new ViewHolder(v);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.bind(_data.getItem(position));
}
#Override
public int getItemCount()
{
return _data.getItemsCount();
}
#Override
public void onDataChanged(ArrayList<AsyncDataUpdater.Diff> diffs)
{
//Apply changes.
for(AsyncDataUpdater.Diff d : diffs)
{
if(d.command == 0)
notifyItemInserted(d.position);
else if(d.command == 1)
notifyItemChanged(d.position);
else if(d.command == 2)
notifyItemRemoved(d.position);
}
}
}
private AsyncDataUpdater _data = new AsyncDataUpdater();
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv_content = (RecyclerView)findViewById(R.id.rv_content);
rv_content.setLayoutManager(new LinearLayoutManager(this));
rv_content.setAdapter(new Adapter(_data));
Button btn_add = (Button)findViewById(R.id.btn_add);
btn_add.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
_data.updateDataAsync();
}
});
}
}
I put Example application on GH, so you can test it if you want.
Update 1
About Data Race.
this.mTimelineItems = timelineItems; inside TimelineAdapter() constructor makes a copy of the reference to the ArrayList, but not the copy of the ArrayList itself. So you have two references: TimelineAdapter.mTimelineItems and Timeline.mTimelineItems, that both refer to the same ArrayList object. Please, look at this.
The data race occurs when doInBackground() called from Worker Thread and onProgressUpdate() called from UI Thread simultaneously. The main reason is that publishProgress() does not call onProgressUpdate() synchronously. Instead, publishProgress() plans the call of onProgressUpdate() on UI Thread in the future. Here is a good description of the problem.
Off topic.
This:
mTimelineItems.set(position, item);
should be faster than this:
mTimelineItems.remove(position);
mTimelineItems.add(position, item);
I'm using TouchImageView to load images in full screen and have zoom/pinch capability.
The images are pulled from URL via a web service. The response is in JSON. At this part, I'm using Volley+GSON to obtain the response and use a custom adapter to populate the Pager.
Initially, the images are shown in a listview. When a user click an item, it will go full screen showing the chosen item.
However, in my case, this doesn't happen. Whatever position the user chose, it will still show the image at index 0.
Here is how I do it:
In this adapter, a user will click an item and it will pass the position to another activity.
public class ItemPhotoAdapter extends BaseAdapter {
private ArrayList<ItemImageModel> arrItemImage;
#Override
public ItemImageModel getItem(int i) {
return arrItemImage.get(i);
}
...
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder vh;
lf = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(view == null){
vh = new ViewHolder();
view = lf.inflate(R.layout.row_photo_grid, null);
vh.item_image = (ImageView) view.findViewById(R.id.img_item);
view.setTag(vh);
} else {
vh = (ViewHolder) view.getTag();
}
ItemImageModel iim = arrItemImage.get(i);
Picasso.with(context) //
.load(iim.getResized()) //
.placeholder(R.drawable.placeholder) //
.error(R.drawable.error)
.into(vh.item_image);
vh.item_image.setOnClickListener(new OnImageClickListener(i));
return view;
}
...
private class OnImageClickListener implements View.OnClickListener {
int _position;
public OnImageClickListener(int position) {
this._position = position;
}
#Override
public void onClick(View view) {
Intent i = new Intent(context, PhotoGalleryActivity.class);
i.putExtra("position", _position);
context.startActivity(i);
}
}
}
In PhotoGalleryActivity, will load all the images back, but it doesn't start with the item the user chose.
public class PhotoGalleryActivity extends FragmentActivity {
private ArrayList<ItemImageModel> arrItemImages;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo_gallery);
ViewPager mViewPager = (ViewPager) findViewById(R.id.pager);
pb = (ProgressBar) findViewById(R.id.loading);
ActionBar actionBar = getActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
arrItemImages = new ArrayList<ItemImageModel>();
Intent i = getIntent();
final int position = i.getIntExtra("position", 1);
user_id = i.getStringExtra("user_id");
item_id = i.getStringExtra("item_id");
// Log.d(TAG, "Image position: " + position);
mAdapter = new PhotoGalleryAdapter(arrItemImages, getApplicationContext(), this);
mViewPager.setAdapter(mAdapter);
mViewPager.setCurrentItem(position);
// ViewPagerIndicator
CirclePageIndicator titleIndicator = (CirclePageIndicator)findViewById(R.id.titles);
titleIndicator.setViewPager(mViewPager);
loadPhotos();
}
...
private void loadPhotos() {
mRequestQueue = Volley.newRequestQueue(this);
String url = Constants.ITEM_DETAILS;
GsonRequest<ItemDetailContainer> myReq = new GsonRequest<ItemDetailContainer>(
Request.Method.GET, url, ItemDetailContainer.class,
createMyReqSuccessListener(), createMyReqErrorListener());
mRequestQueue.add(myReq);
}
...
}
I've found the solution.
To use SetCurrentItem correctly, in this case, I have to put it after all the images is loaded. Eg. after notifysetdatachanged.
In Volley response listener function,
private Response.Listener<ItemDetailContainer> createMyReqSuccessListener() {
return new Response.Listener<ItemDetailContainer>() {
#Override
public void onResponse(ItemDetailContainer response) {
try {
...
mAdapter.notifyDataSetChanged();
mViewPager.setCurrentItem(position); // this
} catch (Exception e) {
e.printStackTrace();
}
};
};
}
If you want to retain the tab index through screen rotation, you can save the value into a member variable first:
if (savedInstanceState != null) {
mLastTabIndex = savedInstanceState.getInt(KEY_TAB_INDEX, 0);
} else {
mLastTabIndex = -1;
}
And only after the tabs data have been loaded into tab adapter, would you check the member variable and set current tab index if present:
mPostTypes = result.getTypes();
mAdapter.notifyDataSetChanged();
if (mLastTabIndex > 0) {
mViewPager.setCurrentItem(mLastTabIndex);
}