why does my listview not update when I call notifyDatasetChanged() ?
the only way I can make it display the data is, to call setAdatper() on the ListView again... i also tried to call it via runOnUIThread() which did not change anything
The Adapter
/**
* Adapter to provide the data for the online scores
*
* #author soh#zolex
*
*/
public class OnlineScoresAdapter extends BaseAdapter {
private Context context;
private List<ScoreItem> scores = new ArrayList<ScoreItem>();
/**
* Constructor
*
* #param Context context
*/
public OnlineScoresAdapter(Context context) {
this.context = context;
}
/**
* Add an item to the adapter
*
* #param item
*/
public void addItem(ScoreItem item) {
this.scores.add(item);
}
/**
* Get the number of scores
*
* #return int
*/
public int getCount() {
return this.scores.size();
}
/**
* Get a score item
*
* #param int pos
* #return Object
*/
public Object getItem(int pos) {
return this.scores.get(pos);
}
/**
* Get the id of a score
*
* #param in pos
* #retrn long
*/
public long getItemId(int pos) {
return 0;
}
/**
* Get the type of an item view
*
* #param int pos
* #return int
*/
public int getItemViewType(int arg0) {
return arg0;
}
/**
* Create the view for a single list item.
* Load it from an xml layout.
*
* #param int pos
* #param View view
* #param ViewGroup viewGroup
* #return View
*/
public View getView(int pos, View view, ViewGroup group) {
LinearLayout layout;
if (view == null) {
layout = (LinearLayout)View.inflate(this.context, R.layout.scoreitem, null);
} else {
layout = (LinearLayout)view;
}
TextView position = (TextView)layout.findViewById(R.id.pos);
TextView time = (TextView)layout.findViewById(R.id.time);
TextView player = (TextView)layout.findViewById(R.id.player);
TextView createdAt = (TextView)layout.findViewById(R.id.created_at);
ScoreItem item = (ScoreItem)getItem(pos);
player.setText(item.player);
position.setText(String.valueOf(new Integer(item.position)) + ".");
time.setText(String.format("%.4f", item.time));
createdAt.setText(item.created_at);
return layout;
}
/**
* Get the number of different views
*
* #return int
*/
public int getViewTypeCount() {
return 1;
}
/**
* Return wheather the items have stable IDs or not
*
* #return boolean
*/
public boolean hasStableIds() {
return false;
}
/**
* Return wheather the list is empty or not
*
* #return boolean
*/
public boolean isEmpty() {
return this.scores.size() == 0;
}
/**
* No need of a data observer
*
* #param DataSetObserver arg0
* #return void
*/
public void registerDataSetObserver(DataSetObserver arg0) {
}
/**
* No need of a data observer
*
* #param DataSetObserver arg0
* #return void
*/
public void unregisterDataSetObserver(DataSetObserver arg0) {
}
/**
* No item should be selectable
*
* #return boolean
*/
public boolean areAllItemsEnabled() {
return false;
}
/**
* No item should be selectable
*
* #param int pos
* #return boolean
*/
public boolean isEnabled(int arg0) {
return false;
}
}
The Activity
The XMLLoaderThread works fine, it's just notifyDatasetChanged seems to do nothing...
/**
* Obtain and display the online scores
*
* #author soh#zolex
*
*/
public class OnlineScoresDetails extends ListActivity {
WakeLock wakeLock;
OnlineScoresAdapter adapter;
boolean isLoading = false;
int chunkLimit = 50;
int chunkOffset = 0;
#Override
/**
* Load the scores and initialize the pager and adapter
*
* #param Bundle savedInstanceState
*/
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "racesow");
adapter = new OnlineScoresAdapter(this);
setListAdapter(adapter);
this.loadData();
setContentView(R.layout.listview);
getListView().setOnScrollListener(new OnScrollListener() {
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (totalItemCount > 0 && visibleItemCount > 0 && firstVisibleItem + visibleItemCount >= totalItemCount) {
if (!isLoading) {
loadData();
}
}
}
});
}
public void loadData() {
final ProgressDialog pd = new ProgressDialog(OnlineScoresDetails.this);
pd.setProgressStyle(ProgressDialog.STYLE_SPINNER);
pd.setMessage("Obtaining scores...");
pd.setCancelable(false);
pd.show();
isLoading = true;
String mapName = getIntent().getStringExtra("map");
XMLLoaderThread t = new XMLLoaderThread("http://racesow2d.warsow-race.net/map_positions.php?name=" + mapName + "&offset=" + this.chunkOffset + "&limit=" + this.chunkLimit, new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
// network error
case 0:
new AlertDialog.Builder(OnlineScoresDetails.this)
.setMessage("Could not obtain the maplist.\nCheck your network connection and try again.")
.setNeutralButton("OK", new OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
finish();
overridePendingTransition(0, 0);
}
})
.show();
break;
// maplist received
case 1:
pd.dismiss();
InputStream xmlStream;
try {
xmlStream = new ByteArrayInputStream(msg.getData().getString("xml").getBytes("UTF-8"));
XMLParser parser = new XMLParser();
parser.read(xmlStream);
NodeList positions = parser.doc.getElementsByTagName("position");
int numPositions = positions.getLength();
for (int i = 0; i < numPositions; i++) {
Element position = (Element)positions.item(i);
ScoreItem score = new ScoreItem();
score.position = Integer.parseInt(parser.getValue(position, "no"));
score.player = parser.getValue(position, "player");
score.time = Float.parseFloat(parser.getValue(position, "time"));
score.created_at = parser.getValue(position, "created_at");
adapter.addItem(score);
}
adapter.notifyDataSetChanged();
chunkOffset += chunkLimit;
isLoading = false;
} catch (UnsupportedEncodingException e) {
new AlertDialog.Builder(OnlineScoresDetails.this)
.setMessage("Internal error: " + e.getMessage())
.setNeutralButton("OK", null)
.show();
}
break;
}
pd.dismiss();
}
});
t.start();
}
/**
* Acquire the wakelock on resume
*/
public void onResume() {
super.onResume();
this.wakeLock.acquire();
}
/**
* Release the wakelock when leaving the activity
*/
public void onDestroy() {
super.onDestroy();
this.wakeLock.release();
}
/**
* Disable animations when leaving the activity
*/
public void onBackPressed() {
this.finish();
this.overridePendingTransition(0, 0);
}
}
A bit late but the answer is you should not implement
public void registerDataSetObserver(DataSetObserver arg0) {
}
public void unregisterDataSetObserver(DataSetObserver arg0) {
}
I just had a simple BaseAdapter working as intended, who stop working after adding those two methods. I asume that "someone" need to observe data changes and such :)
I am not really sure if your implementation of Custom BaseAdapter is correct.
Try changing
public long getItemId(int pos) {
return 0;
}
to
public long getItemId(int pos) {
return pos;
}
I also found this simple tutorial that might be helpful on how to implement BaseAdapter. After you got this down, you can try notifyDataSetChanged() again.
You should call adapter.notifyDataSetChanged() after every manipulation of your dataset. If you're adding items in a batch (e.g. a for-loop), this means you have to put .notifyDataSetChanged in your loop, like so:
for(int i = 0; i < numPositions; i++) {
....
adapter.addItem(score);
adapter.notifyDataSetChanged();
}
Make sure you call adapter.notifyDataSetChanged() from your UI-thread.
If you rather update your adapter once, store your ScoreItems in an ArrayList and after the loop call:
adapter.addAll(scoreList);
adapter.notifyDataSetChanged();
But then again, as far as I'm aware there's really no reason to do that.
Maybe it will be helpful to someone. For the method to work correctly, you should make sure that the following conditions are met:
1) getCount() should return correct items size, e.g.
#Override
public int getCount() {
return allMonthDays.size();
}
2) getItemId(int position) should return different id if item was changed, so it may be not enough to return just position here, e.g.
#Override
public long getItemId(int position) {
return allMonthDays.get(position).getTimestamp();
}
3) getView(int position, View convertView, ViewGroup parent) should return needed View, you should make sure that you update it if you want to reuse old convertView
Related
Objective: I have a ListView with a BaseAdapter I want to populate with values from my Firebase Database.
This is my BaseAdapter I named MessagesAdapter.java:
public class MessagesAdapter extends BaseAdapter {
private Context context;
private List<Message> messages;
private LayoutInflater inflater;
boolean showMoreClicked = false;
public MessagesAdapter(Context context, List<Message> messages) {
this.context = context;
this.messages = messages;
inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
return messages.size();
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
public View getView(final int position, final View convertView,final ViewGroup parent) {
View itemView = convertView;
MessagesAdapter.ViewHolder holder;
if (convertView == null) {
itemView = inflater.inflate(R.layout.item_message, null);
holder = new ViewHolder();
// set image based on selected text
holder.message = itemView.findViewById(R.id.tv_msg);
holder.showMoreBtn = itemView.findViewById(R.id.bt_show_more);
holder.datetime = itemView.findViewById(R.id.tv_ride_time);
itemView.setTag(holder);
} else
holder = (MessagesAdapter.ViewHolder) itemView.getTag();
final Message msg = messages.get(position);
/* editing item views */
holder.datetime.setText(msg.getDatetime());
holder.message.setText(msg.getContent());
holder.showMoreBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(context, "All Message: "+msg.getContent(), Toast.LENGTH_SHORT).show();
showMoreClicked = !showMoreClicked;
}
});
DatabaseReference profileRef = FirebaseDatabase.getInstance().getReference("profile");
profileRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Profile sender = dataSnapshot.child(msg.getSender_id()+"").getValue(Profile.class);
ViewHolder h = new ViewHolder();
h.userImg = parent.getChildAt(position).findViewById(R.id.iv_profile_image);
h.userName = parent.getChildAt(position).findViewById(R.id.tv_sender_name);
h.userName.setText(sender.getFirstname()+" "+sender.getLastname()); /*** Error shows up here ***/
getImageFromDB(sender.getPicpath(), h.userImg);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return itemView;
}
private static class ViewHolder {
CircleImageView userImg;
TextView userName;
TextView datetime;
TextView message;
Button showMoreBtn;
}
private void getImageFromDB(String firebasePath, ImageView imageView) {
StorageReference mStorageRef = FirebaseStorage.getInstance()
.getReferenceFromUrl("gs://iveridemap.appspot.com")
.child(firebasePath);
// Load the image using Glide
Glide.with(context)
.using(new FirebaseImageLoader()) // cannot resolve method using!
.load(mStorageRef)
.into(imageView);
}
}
This adapter is used to fetch messages sent to the current user from firebase and show them in my ListView.
I instantiate this BaseAdapter and ListView inside a fragment I called MessagesFragment.java:
public class MessagesAdapter extends BaseAdapter {
private Context context;
private List<Message> messages;
private LayoutInflater inflater;
boolean showMoreClicked = false;
public MessagesAdapter(Context context, List<Message> messages) {
this.context = context;
this.messages = messages;
inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
return messages.size();
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
public View getView(final int position, final View convertView,final ViewGroup parent) {
View itemView = convertView;
MessagesAdapter.ViewHolder holder;
if (convertView == null) {
itemView = inflater.inflate(R.layout.item_message, null);
holder = new ViewHolder();
// set image based on selected text
holder.message = itemView.findViewById(R.id.tv_msg);
holder.showMoreBtn = itemView.findViewById(R.id.bt_show_more);
holder.datetime = itemView.findViewById(R.id.tv_ride_time);
itemView.setTag(holder);
} else
holder = (MessagesAdapter.ViewHolder) itemView.getTag();
final Message msg = messages.get(position);
/* editing item views */
holder.datetime.setText(msg.getDatetime());
holder.message.setText(msg.getContent());
holder.showMoreBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(context, "All Message: "+msg.getContent(), Toast.LENGTH_SHORT).show();
showMoreClicked = !showMoreClicked;
}
});
DatabaseReference profileRef = FirebaseDatabase.getInstance().getReference("profile");
profileRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Profile sender = dataSnapshot.child(msg.getSender_id()+"").getValue(Profile.class);
ViewHolder h = new ViewHolder();
h.userImg = parent.getChildAt(position).findViewById(R.id.iv_profile_image);
h.userName = parent.getChildAt(position).findViewById(R.id.tv_sender_name);
h.userName.setText(sender.getFirstname()+" "+sender.getLastname());
getImageFromDB(sender.getPicpath(), h.userImg);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return itemView;
}
private static class ViewHolder {
CircleImageView userImg;
TextView userName;
TextView datetime;
TextView message;
Button showMoreBtn;
}
private void getImageFromDB(String firebasePath, ImageView imageView) {
StorageReference mStorageRef = FirebaseStorage.getInstance()
.getReferenceFromUrl("gs://iveridemap.appspot.com")
.child(firebasePath);
// Load the image using Glide
Glide.with(context)
.using(new FirebaseImageLoader()) // cannot resolve method using!
.load(mStorageRef)
.into(imageView);
}
}
Problem: When I scroll down my ListView my code returns a NullPointerException at the line:
h.userName.setText(sender.getFirstname()+" "+sender.getLastname());
In the BaseAdapter, I pointed it out in the code above. Apprently something is wrong with initiating my profile instance I fetch from Firebase.
More Info: This error doesn't show up when there is fewer items in the list and I don't have to scroll down..
I am ready to post more code upon demand, if anyone needs to see my XML file or database structure just say so!
Thanks alot for your time.
I recommend using a RecyclerView, to use firebase items there is some great libraries to make everything more simple
Here is a great library to parcel your items
compile 'org.parceler:parceler-api:1.1.9'
With parceler you can make your activity like this example
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import com.firebase.client.Firebase;
import com.firebase.client.Query;
import org.parceler.Parcels;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private final static String SAVED_ADAPTER_ITEMS = "SAVED_ADAPTER_ITEMS";
private final static String SAVED_ADAPTER_KEYS = "SAVED_ADAPTER_KEYS";
private Query mQuery;
private MyAdapter mMyAdapter;
private ArrayList<MyItem> mAdapterItems;
private ArrayList<String> mAdapterKeys;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handleInstanceState(savedInstanceState);
setupFirebase();
setupRecyclerview();
}
// Restoring the item list and the keys of the items: they will be passed to the adapter
private void handleInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null &&
savedInstanceState.containsKey(SAVED_ADAPTER_ITEMS) &&
savedInstanceState.containsKey(SAVED_ADAPTER_KEYS)) {
mAdapterItems = Parcels.unwrap(savedInstanceState.getParcelable(SAVED_ADAPTER_ITEMS));
mAdapterKeys = savedInstanceState.getStringArrayList(SAVED_ADAPTER_KEYS);
} else {
mAdapterItems = new ArrayList<MyItem>();
mAdapterKeys = new ArrayList<String>();
}
}
private void setupFirebase() {
Firebase.setAndroidContext(this);
String firebaseLocation = getResources().getString(R.string.firebase_location);
mQuery = new Firebase(firebaseLocation);
}
private void setupRecyclerview() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
mMyAdapter = new MyAdapter(mQuery, MyItem.class, mAdapterItems, mAdapterKeys);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(mMyAdapter);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
return item.getItemId() == R.id.action_settings || super.onOptionsItemSelected(item);
}
// Saving the list of items and keys of the items on rotation
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(SAVED_ADAPTER_ITEMS, Parcels.wrap(mMyAdapter.getItems()));
outState.putStringArrayList(SAVED_ADAPTER_KEYS, mMyAdapter.getKeys());
}
#Override
protected void onDestroy() {
super.onDestroy();
mMyAdapter.destroy();
}
}
Here is the item Model
import org.parceler.Parcel;
#Parcel
public class MyItem {
String name;
long age;
public MyItem() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getAge() {
return age;
}
public void setAge(long age) {
this.age = age;
}
}
Here is the adapter
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.firebase.client.Query;
import java.util.ArrayList;
import java.util.List;
public class MyAdapter extends FirebaseRecyclerAdapter<MyAdapter.ViewHolder, MyItem> {
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView textViewName;
TextView textViewAge;
public ViewHolder(View view) {
super(view);
textViewName = (TextView) view.findViewById(R.id.textview_name);
textViewAge = (TextView) view.findViewById(R.id.textview_age);
}
}
public MyAdapter(Query query, #Nullable ArrayList<MyItem> items,
#Nullable ArrayList<String> keys) {
super(query, items, keys);
}
#Override public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false);
return new ViewHolder(view);
}
#Override public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
MyItem item = getItem(position);
holder.textViewName.setText(item.getName());
holder.textViewAge.setText(String.valueOf(item.getAge()));
}
#Override protected void itemAdded(MyItem item, String key, int position) {
Log.d("MyAdapter", "Added a new item to the adapter.");
}
#Override protected void itemChanged(MyItem oldItem, MyItem newItem, String key, int position) {
Log.d("MyAdapter", "Changed an item.");
}
#Override protected void itemRemoved(MyItem item, String key, int position) {
Log.d("MyAdapter", "Removed an item from the adapter.");
}
#Override protected void itemMoved(MyItem item, String key, int oldPosition, int newPosition) {
Log.d("MyAdapter", "Moved an item.");
}
}
And here is the adapter that is extending the main adapter so it can work with firebase
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.ViewGroup;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.Query;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
/**https://firebase.google.com/support/guides/firebase-android.
* <p>
* This class is a generic way of backing an Android RecyclerView with a Firebase location.
* It handles all of the child events at the given Firebase location.
* It marshals received data into the given class type.
* Extend this class and provide an implementation of the abstract methods, which will notify when
* the adapter list changes.
* <p>
* This class also simplifies the management of configuration change (e.g.: device rotation)
* allowing the restore of the list.
*
* #param <T> The class type to use as a model for the data contained in the children of the
* given Firebase location
*/
public abstract class FirebaseRecyclerAdapter<ViewHolder extends RecyclerView.ViewHolder, T> extends RecyclerView.Adapter<ViewHolder> {
private Query mQuery;
private ArrayList<T> mItems;
private ArrayList<String> mKeys;
/**
* #param query The Firebase location to watch for data changes.
* Can also be a slice of a location, using some combination of
* <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>.
*/
public FirebaseRecyclerAdapter(Query query) {
this(query, null, null);
}
/**
* #param query The Firebase location to watch for data changes.
* Can also be a slice of a location, using some combination of
* <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>.
* #param items List of items that will load the adapter before starting the listener.
* Generally null or empty, but this can be useful when dealing with a
* configuration change (e.g.: reloading the adapter after a device rotation).
* Be careful: keys must be coherent with this list.
* #param keys List of keys of items that will load the adapter before starting the listener.
* Generally null or empty, but this can be useful when dealing with a
* configuration change (e.g.: reloading the adapter after a device rotation).
* Be careful: items must be coherent with this list.
*/
public FirebaseRecyclerAdapter(Query query,
#Nullable ArrayList<T> items,
#Nullable ArrayList<String> keys) {
this.mQuery = query;
if (items != null && keys != null) {
this.mItems = items;
this.mKeys = keys;
} else {
mItems = new ArrayList<T>();
mKeys = new ArrayList<String>();
}
query.addChildEventListener(mListener);
}
private ChildEventListener mListener = new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
String key = dataSnapshot.getKey();
if (!mKeys.contains(key)) {
T item = getConvertedObject(dataSnapshot);
int insertedPosition;
if (previousChildName == null) {
mItems.add(0, item);
mKeys.add(0, key);
insertedPosition = 0;
} else {
int previousIndex = mKeys.indexOf(previousChildName);
int nextIndex = previousIndex + 1;
if (nextIndex == mItems.size()) {
mItems.add(item);
mKeys.add(key);
} else {
mItems.add(nextIndex, item);
mKeys.add(nextIndex, key);
}
insertedPosition = nextIndex;
}
notifyItemInserted(insertedPosition);
itemAdded(item, key, insertedPosition);
}
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
String key = dataSnapshot.getKey();
if (mKeys.contains(key)) {
int index = mKeys.indexOf(key);
T oldItem = mItems.get(index);
T newItem = getConvertedObject(dataSnapshot);
mItems.set(index, newItem);
notifyItemChanged(index);
itemChanged(oldItem, newItem, key, index);
}
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
String key = dataSnapshot.getKey();
if (mKeys.contains(key)) {
int index = mKeys.indexOf(key);
T item = mItems.get(index);
mKeys.remove(index);
mItems.remove(index);
notifyItemRemoved(index);
itemRemoved(item, key, index);
}
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
String key = dataSnapshot.getKey();
int index = mKeys.indexOf(key);
T item = getConvertedObject(dataSnapshot);
mItems.remove(index);
mKeys.remove(index);
int newPosition;
if (previousChildName == null) {
mItems.add(0, item);
mKeys.add(0, key);
newPosition = 0;
} else {
int previousIndex = mKeys.indexOf(previousChildName);
int nextIndex = previousIndex + 1;
if (nextIndex == mItems.size()) {
mItems.add(item);
mKeys.add(key);
} else {
mItems.add(nextIndex, item);
mKeys.add(nextIndex, key);
}
newPosition = nextIndex;
}
notifyItemMoved(index, newPosition);
itemMoved(item, key, index, newPosition);
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.e("FirebaseListAdapter", "Listen was cancelled, no more updates will occur.");
}
};
#Override
public abstract ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);
#Override
public abstract void onBindViewHolder(ViewHolder holder, final int position);
#Override
public int getItemCount() {
return (mItems != null) ? mItems.size() : 0;
}
/**
* Clean the adapter.
* ALWAYS call this method before destroying the adapter to remove the listener.
*/
public void destroy() {
mQuery.removeEventListener(mListener);
}
/**
* Returns the list of items of the adapter: can be useful when dealing with a configuration
* change (e.g.: a device rotation).
* Just save this list before destroying the adapter and pass it to the new adapter (in the
* constructor).
*
* #return the list of items of the adapter
*/
public ArrayList<T> getItems() {
return mItems;
}
/**
* Returns the list of keys of the items of the adapter: can be useful when dealing with a
* configuration change (e.g.: a device rotation).
* Just save this list before destroying the adapter and pass it to the new adapter (in the
* constructor).
*
* #return the list of keys of the items of the adapter
*/
public ArrayList<String> getKeys() {
return mKeys;
}
/**
* Returns the item in the specified position
*
* #param position Position of the item in the adapter
* #return the item
*/
public T getItem(int position) {
return mItems.get(position);
}
/**
* Returns the position of the item in the adapter
*
* #param item Item to be searched
* #return the position in the adapter if found, -1 otherwise
*/
public int getPositionForItem(T item) {
return mItems != null && mItems.size() > 0 ? mItems.indexOf(item) : -1;
}
/**
* Check if the searched item is in the adapter
*
* #param item Item to be searched
* #return true if the item is in the adapter, false otherwise
*/
public boolean contains(T item) {
return mItems != null && mItems.contains(item);
}
/**
* ABSTRACT METHODS THAT MUST BE IMPLEMENTED BY THE EXTENDING ADAPTER.
*/
/**
* Called after an item has been added to the adapter
*
* #param item Added item
* #param key Key of the added item
* #param position Position of the added item in the adapter
*/
protected void itemAdded(T item, String key, int position) {
}
/**
* Called after an item changed
*
* #param oldItem Old version of the changed item
* #param newItem Current version of the changed item
* #param key Key of the changed item
* #param position Position of the changed item in the adapter
*/
protected void itemChanged(T oldItem, T newItem, String key, int position) {
}
/**
* Called after an item has been removed from the adapter
*
* #param item Removed item
* #param key Key of the removed item
* #param position Position of the removed item in the adapter
*/
protected void itemRemoved(T item, String key, int position) {
}
/**
* Called after an item changed position
*
* #param item Moved item
* #param key Key of the moved item
* #param oldPosition Old position of the changed item in the adapter
* #param newPosition New position of the changed item in the adapter
*/
protected void itemMoved(T item, String key, int oldPosition, int newPosition) {
}
/**
* Converts the data snapshot to generic object
*
* #param snapshot Result
* #return Data converted
*/
protected T getConvertedObject(DataSnapshot snapshot) {
return snapshot.getValue(getGenericClass());
}
/**
* Returns a class reference from generic T.
*/
#SuppressWarnings("unchecked")
private Class<T> getGenericClass() {
return (Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[1];
}
}
Useful links:
Generic Firebase recyclerview list for Android
Hope it helps
In Recyclerview with multiple layouts I override the getItemViewType() method by which I decide which layout to display. Strange issue occurred when on scrolling duplicate items appeared and change their position too.
When I hardcode the things on the basis of the position then no duplicate's like below code sample.
#Override
public int getItemViewType (int position)
{
switch (position)
{
case 0:
return TYPE_HEADER;
case 8:
return TYPE_HEADER;
default:
return TYPE_ITEMS;
}
}
But duplicate's start when I change it like below code and make it dynamic instead of static positions.
String tempDate = "";
List<String> items = new ArrayList<>();
items.add("2017-01-01");
items.add("2017-01-01");
items.add("2017-01-02");
items.add("2017-01-02");
items.add("2017-01-02");
items.add("2017-01-03");
items.add("2017-01-03");
items.add("2017-01-03");
items.add("2017-01-04");
#Override
public int getItemViewType (int position)
{
if(!tempDate.equalsIgnoreCase(items.get(position)){
tempDate = items.get(position);
return TYPE_HEADER;
} else{
tempDate = items.get(position);
return TYPE_ITEMS;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
switch (viewHolder.getItemViewType()) {
case TYPE_HEADER:
//Make your header view visible
initialize the view resources of HeaderLayout xml
break;
case TYPE_ITEM:
//Make your second header view visible
initialize the view resources of ItemLayout xml
break;
}
}
The other methods onBindViewHolder(), onCreateViewHolder() are fine as per my knowledge. Any help is appreciated.
I think simple is better here:
private List<String> items = new ArrayList<>();
#Override
public int getItemViewType (int position) {
if (position == 0) {
return TYPE_HEADER;
}
String textForPosition = items.get(position);
String textForPrevPosition = items.get(position - 1);
if (textForPosition.equalsIgnoreCase(textForPrevPosition)) {
return TYPE_HEADER;
}
return TYPE_ITEM;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
// Use dateText instead of tempDate
String dateText = items.get(i);
switch (viewHolder.getItemViewType()) {
case TYPE_HEADER:
//Make your header view visible initialize the view resources of HeaderLayout xml
break;
case TYPE_ITEM:
//Make your second header view visible initialize the view resources of ItemLayout xml
break;
}
}
I think the problem is in that code section:
if(!tempDate.equalsIgnoreCase(items.get(position)){
tempDate = items.get(position);
return TYPE_HEADER;
}
else{
tempDate = items.get(position);
return TYPE_ITEMS;
}
I'd recomment to create another list in your adapter which called headers.
I already implemented a class which extends a baseadapter which i am using for a navigation list with headers and items.
Maybe you can take some ideas for your problem:
/**
* Adapter class for the navigation list. It handles items and section header items.
*/
public class CDMNavListAdapter extends BaseAdapter {
/**
* Constructor
*
* #param p_Context the context
*/
public CDMNavListAdapter(Context p_Context) {
m_Inflater = (LayoutInflater) p_Context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
* Adds an item to the list
*
* #param p_Item the item to add
*/
public void addItem(CDMNavigationItem p_Item) {
m_Data.add(p_Item);
notifyDataSetChanged();
}
/**
* Adds a section header item to the list
*
* #param p_Item the section head item
*/
public void addSectionHeaderItem(String p_Item) {
// CDMNavigationItem is just a wrapper: CDMNavigationItem(int p_iType , String p_Title, int p_iResIcon, String p_Count)
m_Data.add(new CDMNavigationItem(ADMNavigationTypes.iSECTION_HEADER, p_Item, -1, "0"));
m_SectionHeader.add(m_Data.size() - 1);
notifyDataSetChanged();
}
#Override
public int getItemViewType(int p_iPosition) {
return m_SectionHeader.contains(p_iPosition) ? iTYPE_SEPARATOR : iTYPE_ITEM;
}
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getCount() {
return m_Data.size();
}
#Override
public CDMNavigationItem getItem(int p_iPosition) {
return m_Data.get(p_iPosition);
}
#Override
public long getItemId(int p_iPosition) {
return p_iPosition;
}
#Override
public View getView(int p_iPosition, View p_ConvertView, ViewGroup p_Parent) {
int l_iRowType = getItemViewType(p_iPosition);
// sets the text to the item / section head item
CDMNavigationItem l_NavItem = m_Data.get(p_iPosition);
switch(l_iRowType) {
// item
case iTYPE_ITEM:
// item layout code
break;
// section header
case iTYPE_SEPARATOR:
// section header layout code
break;
}
return p_ConvertView;
}
/**
* Returns true if the item on the position is a section header item
*
* #param p_iPosition the position
* #return true if the item on the position is a section header item
*/
public boolean isSectionHeader(int p_iPosition) {
return getItemViewType(p_iPosition) == iTYPE_SEPARATOR;
}
/**
* Gets the position without header sections
* #param p_iPosition the position
* #return int the position without header sections
*/
public int getPositionWithoutHeaderSections(int p_iPosition) {
int l_iPositionWithoutHeaderSections = -1;
for(int i = 0; i <= p_iPosition; i++) {
if(!isSectionHeader(i))
l_iPositionWithoutHeaderSections++;
}
return l_iPositionWithoutHeaderSections;
}
/**
* Clears the data
*/
public void clear() {
m_Data.clear();
m_SectionHeader.clear();
notifyDataSetChanged();
}
private static final int iTYPE_ITEM = 0;
private static final int iTYPE_SEPARATOR = 1;
private List<CDMNavigationItem> m_Data = new ArrayList<>();
private Set<Integer> m_SectionHeader = new TreeSet<>();
private LayoutInflater m_Inflater;
}
You can use this code
#Override
public int getItemViewType(int position) {
String tem = "";
for (int i = 0; i < items.size(); i++) {
if (!tem.equals(items.get(i))) {
tem=items.get(i);
if (i == position) {
return TYPE_HEADER;
}
} else {
if (i == position) {
return TYPE_items;
}
}
}
return -1;
}
Hello guys I'm still learning android things and this is my first time that I 'm building a list using retrofit to populate it from server. I receive no error, app doesn't crash, and my log show that I receive data from server.
My Adapter for listview:
public class TransactionsAdapter extends BaseAdapter {
ArrayList<Transactions> transactions;
public TransactionsAdapter(ArrayList<Transactions> transactions) {
this.transactions=transactions;
}
#Override
public int getCount() {
return 0;
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view=null;
ViewHolder viewHolder = null;
if(convertView == null)
{
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.izvjestaji_item,parent,false);
viewHolder = new ViewHolder(view);
view.setTag(viewHolder);
}
else{
view = convertView;
viewHolder= (ViewHolder) view.getTag();
}
Transactions transactions = (Transactions) getItem(position);
if(transactions != null) {
viewHolder.datum.setText(transactions.getPurchaseDate());
viewHolder.partner.setText(transactions.getMerchantName());
viewHolder.iznos.setText(transactions.getTransactionMoneyAmount().toString());
viewHolder.brojbodova.setText(transactions.getSalesAmount().toString());
}
return view;
}
private class ViewHolder{
TextView datum;
TextView partner;
TextView iznos;
TextView brojbodova;
public ViewHolder(View view) {
this.datum = (TextView) view.findViewById(R.id.izvjestaji_datum);
this.partner = (TextView) view.findViewById(R.id.izvjestaji_partner);
this.iznos = (TextView) view.findViewById(R.id.izvjestaji_iznos);
this.brojbodova=(TextView)view.findViewById(R.id.izvjestaji_brojbodova);
}
}
}
Model that server return generated via jsonschema2pojo.org
import javax.annotation.Generated;
#Generated("org.jsonschema2pojo")
public class Transactions {
private Integer Id;
private String PurchaseDate;
private Integer Month;
private Integer Year;
private Double SalesAmount;
private Object SpecialSalesActionId;
private Integer PosTerminalId;
private Integer CardId;
private String MerchantName;
private Integer TransactionPointsAmount;
private Double TransactionMoneyAmount;
private Boolean IsDeleted;
private Boolean IsVoucher;
private Integer LoyaltyLevel;
private Integer CategoryId;
private Object CardNo;
private Integer MerchantId;
/**
*
* #return
* The Id
*/
public Integer getId() {
return Id;
}
/**
*
* #param Id
* The Id
*/
public void setId(Integer Id) {
this.Id = Id;
}
/**
*
* #return
* The PurchaseDate
*/
public String getPurchaseDate() {
return PurchaseDate;
}
/**
*
* #param PurchaseDate
* The PurchaseDate
*/
public void setPurchaseDate(String PurchaseDate) {
this.PurchaseDate = PurchaseDate;
}
/**
*
* #return
* The Month
*/
public Integer getMonth() {
return Month;
}
/**
*
* #param Month
* The Month
*/
public void setMonth(Integer Month) {
this.Month = Month;
}
/**
*
* #return
* The Year
*/
public Integer getYear() {
return Year;
}
/**
*
* #param Year
* The Year
*/
public void setYear(Integer Year) {
this.Year = Year;
}
/**
*
* #return
* The SalesAmount
*/
public Double getSalesAmount() {
return SalesAmount;
}
/**
*
* #param SalesAmount
* The SalesAmount
*/
public void setSalesAmount(Double SalesAmount) {
this.SalesAmount = SalesAmount;
}
/**
*
* #return
* The SpecialSalesActionId
*/
public Object getSpecialSalesActionId() {
return SpecialSalesActionId;
}
/**
*
* #param SpecialSalesActionId
* The SpecialSalesActionId
*/
public void setSpecialSalesActionId(Object SpecialSalesActionId) {
this.SpecialSalesActionId = SpecialSalesActionId;
}
/**
*
* #return
* The PosTerminalId
*/
public Integer getPosTerminalId() {
return PosTerminalId;
}
/**
*
* #param PosTerminalId
* The PosTerminalId
*/
public void setPosTerminalId(Integer PosTerminalId) {
this.PosTerminalId = PosTerminalId;
}
/**
*
* #return
* The CardId
*/
public Integer getCardId() {
return CardId;
}
/**
*
* #param CardId
* The CardId
*/
public void setCardId(Integer CardId) {
this.CardId = CardId;
}
/**
*
* #return
* The MerchantName
*/
public String getMerchantName() {
return MerchantName;
}
/**
*
* #param MerchantName
* The MerchantName
*/
public void setMerchantName(String MerchantName) {
this.MerchantName = MerchantName;
}
/**
*
* #return
* The TransactionPointsAmount
*/
public Integer getTransactionPointsAmount() {
return TransactionPointsAmount;
}
/**
*
* #param TransactionPointsAmount
* The TransactionPointsAmount
*/
public void setTransactionPointsAmount(Integer TransactionPointsAmount) {
this.TransactionPointsAmount = TransactionPointsAmount;
}
/**
*
* #return
* The TransactionMoneyAmount
*/
public Double getTransactionMoneyAmount() {
return TransactionMoneyAmount;
}
/**
*
* #param TransactionMoneyAmount
* The TransactionMoneyAmount
*/
public void setTransactionMoneyAmount(Double TransactionMoneyAmount) {
this.TransactionMoneyAmount = TransactionMoneyAmount;
}
/**
*
* #return
* The IsDeleted
*/
public Boolean getIsDeleted() {
return IsDeleted;
}
/**
*
* #param IsDeleted
* The IsDeleted
*/
public void setIsDeleted(Boolean IsDeleted) {
this.IsDeleted = IsDeleted;
}
/**
*
* #return
* The IsVoucher
*/
public Boolean getIsVoucher() {
return IsVoucher;
}
/**
*
* #param IsVoucher
* The IsVoucher
*/
public void setIsVoucher(Boolean IsVoucher) {
this.IsVoucher = IsVoucher;
}
/**
*
* #return
* The LoyaltyLevel
*/
public Integer getLoyaltyLevel() {
return LoyaltyLevel;
}
/**
*
* #param LoyaltyLevel
* The LoyaltyLevel
*/
public void setLoyaltyLevel(Integer LoyaltyLevel) {
this.LoyaltyLevel = LoyaltyLevel;
}
/**
*
* #return
* The CategoryId
*/
public Integer getCategoryId() {
return CategoryId;
}
/**
*
* #param CategoryId
* The CategoryId
*/
public void setCategoryId(Integer CategoryId) {
this.CategoryId = CategoryId;
}
/**
*
* #return
* The CardNo
*/
public Object getCardNo() {
return CardNo;
}
/**
*
* #param CardNo
* The CardNo
*/
public void setCardNo(Object CardNo) {
this.CardNo = CardNo;
}
/**
*
* #return
* The MerchantId
*/
public Integer getMerchantId() {
return MerchantId;
}
/**
*
* #param MerchantId
* The MerchantId
*/
public void setMerchantId(Integer MerchantId) {
this.MerchantId = MerchantId;
}
}
Fragment in which I want to include list:
public class Izvjestaji extends Fragment {
ListView list;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
// Defines the xml file for the fragment
return inflater.inflate(R.layout.izvjestaji, parent, false);
}
// This event is triggered soon after onCreateView().
// Any view setup should occur here. E.g., view lookups and attaching view listeners.
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
list=(ListView)view.findViewById(R.id.izvjestaji_list);
showList();
// Setup any handles to view objects here
// EditText etFoo = (EditText) view.findViewById(R.id.etFoo);
}
public void showList(){
NetworkSDK.getInstance().getTransactions(new Callback<List<Transactions>>() {
#Override
public void onResponse(Call<List<Transactions>> call, Response<List<Transactions>> response) {
if(response.isSuccess()){
Log.d("Data", String.valueOf(response.isSuccess()));
TransactionsAdapter transactionsAdapter=new TransactionsAdapter((ArrayList<Transactions>)response.body());
list.setAdapter(transactionsAdapter);
}
}
#Override
public void onFailure(Call<List<Transactions>> call, Throwable t) {
Log.d("Error","Def error");
}
});
}
}
2 xml files (one is from fragment and other is single item in list )
Fragment layout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/izvjestaji_list" />
</LinearLayout>
Item_Layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Datum"
android:id="#+id/izvjestaji_datum"
android:gravity="center"
android:background="#a9a8a8"
android:layout_weight="1"
android:textSize="20dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/izvjestaji_partner"
android:text="Partner"
android:gravity="center"
android:background="#a9a8a8"
android:layout_weight="1"
android:textSize="20dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/izvjestaji_iznos"
android:text="Iznos"
android:background="#a9a8a8"
android:gravity="center"
android:layout_weight="1"
android:textSize="20dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/izvjestaji_brojbodova"
android:text="Broj Bodova"
android:gravity="center"
android:background="#a9a8a8"
android:layout_weight="1"
android:textSize="20dp" />
</LinearLayout>
In my preview I only get empty fragment
In TransactionAdapter class you have not implemented the overriding methods
use these
#Override
public int getCount() {
return transactions.size();
}
#Override
public Object getItem(int position) {
return transactions.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
Change to this
#Override
public int getCount() {
return transactions.size();
}
#Override
public Object getItem(int position) {
return transactions.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
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);
when I execute the project it works eventually it stop
java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(2131361944, class android.widget.ListView) with Adapter(class com.pocketpharmacist.adapter.DrugClassesListAdapter)]
the problem it i cant used runOnUiThread bexause of the class axtend
#Override
public void onConversationMessage(String target)
{
Conversation conversation = server.getConversation(target);
if (conversation == null) {
// In an early state it can happen that the conversation object
// is not created yet.
return;
}
MessageListAdapter adapter = deckAdapter.getItemAdapter(target);
while(conversation.hasBufferedMessages()) {
Message message = conversation.pollBufferedMessage();
if (adapter != null && message != null) {
adapter.addMessage(message);
int status;
switch (message.getType())
{
case Message.TYPE_MISC:
status = Conversation.STATUS_MISC;
break;
default:
status = Conversation.STATUS_MESSAGE;
break;
}
conversation.setStatus(status);
}
}
if (dots != null) {
dots.invalidate();
}
}
for (Conversation conversation : mConversations) {
mAdapter = deckAdapter.getItemAdapter(conversation.getName());
if (mAdapter != null) {
mAdapter.addBulkMessages(conversation.getBuffer());
conversation.clearBuffer();
}
public class MessageListAdapter extends BaseAdapter
{
private final LinkedList<TextView> messages;
private final Context context;
private final int historySize;
/**
* Create a new MessageAdapter
*
* #param channel
* #param context
*/
public MessageListAdapter(Conversation conversation, Context context)
{
LinkedList<TextView> messages = new LinkedList<TextView>();
// Render channel name as first message in channel
if (conversation.getType() != Conversation.TYPE_SERVER) {
Message header = new Message(conversation.getName());
header.setColor(Message.COLOR_RED);
messages.add(header.renderTextView(context));
}
// Optimization - cache field lookups
LinkedList<Message> mHistory = conversation.getHistory();
int mSize = mHistory.size();
for (int i = 0; i < mSize; i++) {
messages.add(mHistory.get(i).renderTextView(context));
}
// XXX: We don't want to clear the buffer, we want to add only
// buffered messages that are not already added (history)
conversation.clearBuffer();
this.messages = messages;
this.context = context;
historySize = conversation.getHistorySize();
}
/**
* Add a message to the list
*
* #param message
*/
public void addMessage(Message message)
{
messages.add(message.renderTextView(context));
if (messages.size() > historySize) {
messages.remove(0);
}
notifyDataSetChanged();
}
/**
* Add a list of messages to the list
*
* #param messages
*/
public void addBulkMessages(LinkedList<Message> messages)
{
LinkedList<TextView> mMessages = this.messages;
Context mContext = this.context;
int mSize = messages.size();
for (int i = mSize - 1; i > -1; i--) {
mMessages.add(messages.get(i).renderTextView(mContext));
if (mMessages.size() > historySize) {
mMessages.remove(0);
}
}
notifyDataSetChanged();
}
/**
* Get number of items
*
* #return
*/
#Override
public int getCount()
{
return messages.size();
}
/**
* Get item at given position
*
* #param position
* #return
*/
#Override
public TextView getItem(int position)
{
return messages.get(position);
}
/**
* Get id of item at given position
*
* #param position
* #return
*/
#Override
public long getItemId(int position)
{
return position;
}
/**
* Get item view for the given position
*
* #param position
* #param convertView
* #param parent
* #return
*/
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
return getItem(position);
}
}
Based on the error message, the problem is likely that you are modifying the contents of your adapter within the callback "onConversationMessage." This is not likely running on the main thread and so cannot be used to update your adapter. What you could do to fix this is move your methods that deal with the adapter into a Runnable and then get the Activity context from within onConversationMessage and run on UI thread from there.
Runnable updateAdapter = new Runnable(){
#Override
public void run(){
//Move all your actions updating adapter here
}
}
Then within your onConversationMessage if you're within a fragment
getActivity().runOnUiThread(updateAdapter);
If you're in an activity you can use that as your context
ActivityName.this.runOnUiThread(updateAdapter);
You will have to change your structure slightly to make this work. For example, you're adapter will likely need to be an instance variable so that you end up updating the same adapter you're applying to the list.