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;
}
Related
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);
Do you guys have any best practices regarding using realm with a recyclerview ?
I know it's generic question but I found nothing on it on the internet. For example I run into a lot of troubles trying to implement a simple color change on a row . For example consider this typical usage:
public class User extends RealmObject {
#PrimaryKey
String name;
boolean isSelected;
...
constructor, getter and setters
}
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<User> users;
public UserAdapter(RealmResults<User> users) {
this.users = users;
}
...
public void markAsSelected(int position){
// get the old selected user and deselect it
notifyItemChanged(? how do i get the position given my User has no index ?);
// mark as selected the new user at position
}
I ran into a lot of issues since I couldn't find anything on the internet. I know this is because I don't know how to properly use realm. But finding the right way is a struggle in itself . I read all their documentation but to no avail.
EDIT : Since I was asked to --> Instead of saying "I have a bunch of issues with [that]", describe your issue(s) and we'll try to provide insights and answers to your incomprehensions.
So my problem is simple :
I have a RealmUser :
public class RealmUser extends RealmObject {
#PrimaryKey
private String key;
private String name;
private boolean isSelected;
private boolean editMode;
private RealmList<RealmItemList> lists;
public RealmUser() {}
public RealmUser(String name, RealmList<RealmItemList> lists, boolean isSelected , boolean editMode) {
this.key = UUID.randomUUID().toString();
this.name = name;
this.isSelected = isSelected;
this.editMode = editMode;
if (lists ==null){
this.lists = new RealmList<RealmItemList>();
}else{
this.lists = lists;
}
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
public boolean isEditMode() {
return editMode;
}
public void setEditMode(boolean editMode) {
this.editMode = editMode;
}
public RealmList<RealmItemList> getLists() {
return lists;
}
public void setLists(RealmList<RealmItemList> lists) {
this.lists = lists;
}
}
Which I put in a RealmResults array using :
RealmResults users = realm.where(RealmUser.class).findAll();
I pass my user array to my custom user adapter :
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<RealmUser> users;
public UserAdapter(RealmResults<RealmUser> users) {
this.users = users;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if(viewType == 1){
View v = inflater.inflate(R.layout.detail_user, parent, false);
return new UserHolder(v);
}else if(viewType == 2){
View v = inflater.inflate(R.layout.edit_user, parent, false);
return new editUserHolder(v);
}else {
return null;
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
RealmUser user = users.get(position);
String userName = user.getName();
boolean isSelected = user.isSelected();
if (holder instanceof UserHolder ){
UserHolder uHolder = (UserHolder) holder;
uHolder.userText.setText(userName);
if (isSelected){
uHolder.userContainer.setBackgroundColor(Color.parseColor("#607D8B"));
}
}else if(holder instanceof editUserHolder){
editUserHolder eUserHolder = (editUserHolder) holder;
eUserHolder.userEditContainer.setBackgroundColor(Color.parseColor("#eeeeee"));
}
}
#Override
public int getItemViewType(int position) {
RealmUser user = users.get(position);
if (user.isEditMode()){
return 2;
}else {
return 1;
}
}
#Override
public int getItemCount() {
return users.size();
}
public void markAsSelected(int position, DrawerLayout mDrawerLayout , Toolbar toolbar, Realm realm){
// Here is my problem : How do I get the already selected user asuming there is one in my db and notify the UI that I changed that item.
}
That has a custom click Listener : that gets recyclerview item that was clicked using :
public class UserClickListener implements RecyclerView.OnItemTouchListener{
public static interface OnItemClickListener{
public void onItemClick(View v, int position);
}
private OnItemClickListener mListener;
private GestureDetector mGestureDetector;
public UserClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
{
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null)
{
mListener.onItemClick(childView, recyclerView.getChildPosition(childView));
return true;
}
return false;
}
});
}
#Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null && mGestureDetector.onTouchEvent(e))
{
mListener.onItemClick(childView, view.getChildPosition(childView));
}
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
}
Which I add to my recyclerView with addOnItemTouchListener :
mListRecycler.addOnItemTouchListener(new UserClickListener(getActivity(), mListRecycler, new UserClickListener.OnItemClickListener(){
#Override
public void onItemClick(View view, int position)
{
UserAdapter myadapter = (UserAdapter) mListRecycler.getAdapter();
myadapter.markAsSelected(position, mDrawerLayout , mToolbar, realm);
}
}));
ANSWER FOR 0.89.0 AND ABOVE
For the latest versions, you should use RealmRecyclerViewAdapter in the realm-android-adapters repository.
Versions:
Use 1.5.0 up to 2.X
Use 2.1.1 up to 4.X
Use 3.0.0 above 5.X
OLD ANSWER FOR OLD VERSIONS:
I made this RealmRecyclerViewAdapter based on the implementation of RealmBaseAdapter.
This is for v0.89.0 AND ABOVE
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected OrderedRealmCollection<T> adapterData;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, OrderedRealmCollection<T> data) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.adapterData = data;
this.inflater = LayoutInflater.from(context);
this.listener = new RealmChangeListener<RealmResults<T>>() {
#Override
public void onChange(RealmResults<T> results) {
notifyDataSetChanged();
}
};
if (data != null) {
addListener(data);
}
}
private void addListener(OrderedRealmCollection<T> data) {
if (data instanceof RealmResults) {
RealmResults realmResults = (RealmResults) data;
realmResults.addChangeListener(listener);
} else if (data instanceof RealmList) {
RealmList realmList = (RealmList) data;
realmList.realm.handlerController.addChangeListenerAsWeakReference(listener);
} else {
throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
}
}
private void removeListener(OrderedRealmCollection<T> data) {
if (data instanceof RealmResults) {
RealmResults realmResults = (RealmResults) data;
realmResults.removeChangeListener(listener);
} else if (data instanceof RealmList) {
RealmList realmList = (RealmList) data;
realmList.realm.handlerController.removeWeakChangeListener(listener);
} else {
throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
}
}
/**
* Returns how many items are in the data set.
*
* #return the number of items.
*/
#Override
public int getItemCount() {
if (adapterData == null) {
return 0;
}
return adapterData.size();
}
/**
* Get the data item associated with the specified position in the data set.
*
* #param position Position of the item whose data we want within the adapter's
* data set.
* #return The data at the specified position.
*/
public T getItem(int position) {
if (adapterData == null) {
return null;
}
return adapterData.get(position);
}
/**
* Get the row id associated with the specified position in the list. Note that item IDs are not stable so you
* cannot rely on the item ID being the same after {#link #notifyDataSetChanged()} or
* {#link #updateData(OrderedRealmCollection)} has been called.
*
* #param position The position of the item within the adapter's data set whose row id we want.
* #return The id of the item at the specified position.
*/
#Override
public long getItemId(int position) {
// TODO: find better solution once we have unique IDs
return position;
}
/**
* Updates the data associated with the Adapter.
*
* Note that RealmResults and RealmLists are "live" views, so they will automatically be updated to reflect the
* latest changes. This will also trigger {#code notifyDataSetChanged()} to be called on the adapter.
*
* This method is therefore only useful if you want to display data based on a new query without replacing the
* adapter.
*
* #param data the new {#link OrderedRealmCollection} to display.
*/
public void updateData(OrderedRealmCollection<T> data) {
if (listener != null) {
if (adapterData != null) {
removeListener(adapterData);
}
if (data != null) {
addListener(data);
}
}
this.adapterData = data;
notifyDataSetChanged();
}
}
This is for v0.84.0 AND ABOVE, BUT OLDER THAN v0.89.0 (updated for v0.87.5):
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected RealmResults<T> realmResults;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.realmResults = realmResults;
this.inflater = LayoutInflater.from(context);
this.listener = (!automaticUpdate) ? null : new RealmChangeListener() {
#Override
public void onChange() {
notifyDataSetChanged();
}
};
if (listener != null && realmResults != null) {
realmResults.realm.handlerController.addChangeListenerAsWeakReference(listener);
}
}
/**
* Returns how many items are in the data set.
*
* #return count of items.
*/
#Override
public int getItemCount() {
if (realmResults == null) {
return 0;
}
return realmResults.size();
}
/**
* Returns the item associated with the specified position.
*
* #param i index of item whose data we want.
* #return the item at the specified position.
*/
public T getItem(int i) {
if (realmResults == null) {
return null;
}
return realmResults.get(i);
}
/**
* Returns the current ID for an item. Note that item IDs are not stable so you cannot rely on the item ID being the
* same after {#link #notifyDataSetChanged()} or {#link #updateRealmResults(RealmResults)} has been called.
*
* #param i index of item in the adapter.
* #return current item ID.
*/
#Override
public long getItemId(int i) {
// TODO: find better solution once we have unique IDs
return i;
}
/**
* Updates the RealmResults associated to the Adapter. Useful when the query has been changed.
* If the query does not change you might consider using the automaticUpdate feature.
*
* #param queryResults the new RealmResults coming from the new query.
*/
public void updateRealmResults(RealmResults<T> queryResults) {
if (listener != null) {
// Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm
if (this.realmResults != null) {
this.realmResults.realm.removeChangeListener(listener);
}
if (queryResults != null) {
queryResults.realm.addChangeListener(listener);
}
}
this.realmResults = queryResults;
notifyDataSetChanged();
}
public void addChangeListenerAsWeakReference(RealmChangeListener realmChangeListener) {
if(realmResults != null) {
realmResults.realm.handlerController.addChangeListenerAsWeakReference(realmChangeListener);
}
}
}
This is for OLDER THAN 0.84.0:
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected RealmResults<T> realmResults;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) {
if(context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.realmResults = realmResults;
this.inflater = LayoutInflater.from(context);
this.listener = (!automaticUpdate) ? null : new RealmChangeListener() {
#Override
public void onChange() {
notifyDataSetChanged();
}
};
if(listener != null && realmResults != null) {
realmResults.getRealm()
.addChangeListener(listener);
}
}
#Override
public long getItemId(int i) {
// TODO: find better solution once we have unique IDs
return i;
}
public T getItem(int i) {
if(realmResults == null) {
return null;
}
return realmResults.get(i);
}
public void updateRealmResults(RealmResults<T> queryResults) {
if(listener != null) {
// Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm
if(this.realmResults != null) {
realmResults.getRealm().removeChangeListener(listener);
}
if(queryResults != null) {
queryResults.getRealm().addChangeListener(listener);
}
}
this.realmResults = queryResults;
notifyDataSetChanged();
}
#Override
public int getItemCount() {
if(realmResults == null) {
return 0;
}
return realmResults.size();
}
}
Some of the answers above include reflection, not to mention that a sectioned RecyclerView would cause complications. They also do not support adding and removing items. Here is my version of the RecyclerView Adapter that works with Realm, supports a sectioned RecyclerView, also adds and removes items at arbitrary positions if need be
Here is our AbstractRealmAdapter that takes care of all the low level stuff, displaying headers, footers, items, loading data inside RealmResults, managing item types
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmResults;
public abstract class AbstractRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
public static final int HEADER_COUNT = 1;
public static final int FOOTER_COUNT = 1;
//Our data source
protected RealmResults<T> mResults;
public AbstractRealmAdapter(Realm realm) {
//load data from subclasses
mResults = loadData(realm);
notifyDataSetChanged();
}
public int getHeaderCount() {
return hasHeader() ? HEADER_COUNT : 0;
}
public int getFooterCount() {
return hasFooter() ? FOOTER_COUNT : 0;
}
public boolean isHeader(int position) {
if (hasHeader()) {
return position < HEADER_COUNT;
} else {
return false;
}
}
public boolean isFooter(int position) {
if (hasFooter()) {
return position >= getCount() + getHeaderCount();
} else {
return false;
}
}
#Override
public long getItemId(int i) {
return i;
}
#Override
public final int getItemViewType(int position) {
if (isHeader(position)) {
return ItemType.HEADER.ordinal();
} else if (isFooter(position)) {
return ItemType.FOOTER.ordinal();
} else {
return ItemType.ITEM.ordinal();
}
}
/**
* #param position the position within our adapter inclusive of headers,items and footers
* #return an item only if it is not a header or a footer, otherwise returns null
*/
public T getItem(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
return mResults.get(position - getHeaderCount());
}
return null;
}
#Override
public final int getItemCount() {
return getHeaderCount() + getCount() + getFooterCount();
}
public final int getCount() {
return mResults.size();
}
public abstract boolean hasHeader();
public abstract boolean hasFooter();
public void setData(RealmResults<T> results) {
mResults = results;
notifyDataSetChanged();
}
protected abstract RealmResults<T> loadData(Realm realm);
public enum ItemType {
HEADER, ITEM, FOOTER;
}
}
To add items by some method or remove items by swipe to delete, we have an extension in the form of AbstractMutableRealmAdapter that looks as shown below
import android.support.v7.widget.RecyclerView;
import io.realm.Realm;
import io.realm.RealmObject;
public abstract class AbstractMutableRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends AbstractRealmAdapter<T, VH> implements OnSwipeListener {
private Realm realm;
public AbstractMutableRealmAdapter(Realm realm) {
//call the superclass constructor to load data from subclasses into realmresults
super(realm);
this.realm = realm;
}
public void add(T item, boolean update) {
realm.beginTransaction();
T phraseToWrite = (update == true) ? realm.copyToRealmOrUpdate(item) : realm.copyToRealm(item);
realm.commitTransaction();
notifyItemRangeChanged(0, mResults.size());
}
#Override
public final void onSwipe(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
int itemPosition = position - getHeaderCount();
realm.beginTransaction();
T item = mResults.get(itemPosition);
item.removeFromRealm();
realm.commitTransaction();
notifyItemRemoved(position);
}
}
}
Notice the use of the interface OnSwipeListener which looks like this
public interface OnSwipeListener {
/**
* #param position the position of the item that was swiped within the RecyclerView
*/
void onSwipe(int position);
}
This SwipeListener is used to perform a Swipe to delete inside our TouchHelperCallback which in turn is used to delete the objects from Realm directly and looks as follows
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public class TouchHelperCallback extends ItemTouchHelper.Callback {
private final OnSwipeListener mSwipeListener;
public TouchHelperCallback(OnSwipeListener adapter) {
mSwipeListener = adapter;
}
/**
* #return false if you dont want to enable drag else return true
*/
#Override
public boolean isLongPressDragEnabled() {
return false;
}
/**
* #return true of you want to enable swipe in your RecyclerView else return false
*/
#Override
public boolean isItemViewSwipeEnabled() {
return true;
}
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//We want to let the person swipe to the right on devices that run LTR and let the person swipe from right to left on devices that run RTL
int swipeFlags = ItemTouchHelper.END;
return makeMovementFlags(0, swipeFlags);
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mSwipeListener.onSwipe(viewHolder.getAdapterPosition());
}
}
The full implementation demo is available here for review https://github.com/slidenerd/SpamWordList/tree/spamphraser_with_realmresults_base Feel free to suggest any improvements
I replaced the notifyXXX methods with notifyDataSetChanged, RealmResults objects are live objects which means they automatically change when the data is updated, I tried calling notifyXXX methods and they caused an RecyclerView inconsistency exception, I am well aware of the fact that notifyDataSetChanged() would mess with animations, will keep you guys updated on a solution that overcomes the inconsistency error and at the same time provides a good adapter experience
Now that with Realm 0.88.2 we can make a RecyclerView adapter that updates the RecyclerView with more precision than using notifyDataSetChanged() every time. This can be accomplished by using the new ability create custom methods.
Overriding the equals method, in the realm object that will be used with the recycler adapter, is all that will be needed. (You don't actually need to override equals... but you may find that realm objects do not equal each other when you expect them to. This will lead to unnecessary recyclerview updates after running diff)
Then add Google's java-diff-utils to your gradle dependencies
compile 'com.googlecode.java-diff-utils:diffutils:1.3.0'
Using this RealmRecyclerViewAdapter implementation a copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update the RecyclerView as appropriate
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
protected RealmResults<T> realmResults;
protected List<T> lastCopyOfRealmResults;
int maxDepth = 0;
private RealmChangeListener realmResultsListener;
Realm realm;
public RealmRecyclerViewAdapter(RealmResults<T> realmResults, boolean automaticUpdate) {
this(realmResults, automaticUpdate, 0);
}
/**
*
* #param realmResults
* #param automaticUpdate
* #param maxDepth limit of the deep copy when copying realmResults. All references after this depth will be {#code null}. Starting depth is {#code 0}.
* A copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update
* the RecyclerView as appropriate
*/
public RealmRecyclerViewAdapter(RealmResults<T> realmResults, boolean automaticUpdate, int maxDepth) {
this.realmResultsListener = (!automaticUpdate) ? null : getRealmResultsChangeListener();
if (realmResultsListener != null && realmResults != null) {
realmResults.addChangeListener(realmResultsListener);
}
this.realmResults = realmResults;
realm = Realm.getDefaultInstance();
this.maxDepth = maxDepth;
lastCopyOfRealmResults = realm.copyFromRealm(realmResults, this.maxDepth);
}
#Override
public int getItemCount() {
return realmResults != null ? realmResults.size() : 0;
}
/**
* Make sure this is called before a view is destroyed to avoid memory leaks do to the listeners.
* Do this by calling setAdapter(null) on your RecyclerView
* #param recyclerView
*/
#Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
if (realmResultsListener != null) {
if (realmResults != null) {
realmResults.removeChangeListener(realmResultsListener);
}
}
realm.close();
}
/**
* Update the RealmResults associated with the Adapter. Useful when the query has been changed.
* If the query does not change you might consider using the automaticUpdate feature.
*
* #param queryResults the new RealmResults coming from the new query.
* #param maxDepth limit of the deep copy when copying realmResults. All references after this depth will be {#code null}. Starting depth is {#code 0}.
* A copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update
* the RecyclerView as appropriate
*/
public void updateRealmResults(RealmResults<T> queryResults, int maxDepth) {
if (realmResultsListener != null) {
if (realmResults != null) {
realmResults.removeChangeListener(realmResultsListener);
}
}
realmResults = queryResults;
if (realmResults != null && realmResultsListener !=null) {
realmResults.addChangeListener(realmResultsListener);
}
this.maxDepth = maxDepth;
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,this.maxDepth);
notifyDataSetChanged();
}
public T getItem(int position) {
return realmResults.get(position);
}
public int getRealmResultsSize(){
return realmResults.size();
}
private RealmChangeListener getRealmResultsChangeListener() {
return new RealmChangeListener<RealmResults<T>>() {
#Override
public void onChange(RealmResults<T> element) {
if (lastCopyOfRealmResults != null && !lastCopyOfRealmResults.isEmpty()) {
if (realmResults.isEmpty()) {
// If the list is now empty, just notify the recyclerView of the change.
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
notifyDataSetChanged();
return;
}
Patch patch = DiffUtils.diff(lastCopyOfRealmResults, realmResults);
List<Delta> deltas = patch.getDeltas();
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
if (!deltas.isEmpty()) {
List<Delta> deleteDeltas = new ArrayList<>();
List<Delta> insertDeltas = new ArrayList<>();
for (final Delta delta : deltas) {
switch (delta.getType()){
case DELETE:
deleteDeltas.add(delta);
break;
case INSERT:
insertDeltas.add(delta);
break;
case CHANGE:
notifyItemRangeChanged(
delta.getRevised().getPosition(),
delta.getRevised().size());
break;
}
}
for (final Delta delta : deleteDeltas) {
notifyItemRangeRemoved(
delta.getOriginal().getPosition(),
delta.getOriginal().size());
}
//item's should be removed before insertions are performed
for (final Delta delta : insertDeltas) {
notifyItemRangeInserted(
delta.getRevised().getPosition(),
delta.getRevised().size());
}
}
} else {
notifyDataSetChanged();
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
}
}
};
}
}
Your post does not even contain a real question.
Have you checked out this post: http://gradlewhy.ghost.io/realm-results-with-recyclerview/ ?
Not sure why you wouldn't just use an ArrayList in your adapter and add all elements from the RealmResult to that list though. Could anyone explain why the solution in the blog post would be better?
Implementing the Realm Add-on from Thorben Primke is a very convenient method
for handling Recycler View applications with Realm databases. His github has good examples of the ways that it can be implemented.
I'll include mine here so you have an example. First modify your project build gradle for jitpack.io:
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
Then your module gradle to point to the library: (note , check for latest version)
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.github.thorbenprimke:realm-recyclerview:0.9.20'
Create the xml layout for a recycler view using the RealmRecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<co.moonmonkeylabs.realmrecyclerview.RealmRecyclerView
android:id="#+id/realm_recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rrvIsRefreshable="true"
app:rrvEmptyLayoutId="#layout/empty_view"
app:rrvLayoutType="LinearLayout"
app:rrvSwipeToDelete="true"
/>
</RelativeLayout>
Now in your RealmRecycler Fragment obtain a Realm query result of RealmObjects, inflate and define a Primke RealmAdapter:
Log.i(TAG, " Obtain Filtered List");
final RealmResults <Session> realmResults = queryD.findAllSorted(
"sessionId", Sort.DESCENDING);
Log.i(TAG, " Inflate realm List");
View view = inflater.inflate(R.layout.realm_card_recycler2, null);
Log.i(TAG, " Define and configure SessionRealmAdapter");
SessionRealmAdapter sessionRealmAdapter =
new SessionRealmAdapter(getActivity(), realmResults, true, true);`enter code here`
RealmRecyclerView realmRecyclerView =
(RealmRecyclerView) view.findViewById(R.id.realm_recycle_view);
realmRecyclerView.setAdapter(sessionRealmAdapter);
Finally configure the Realm Adapter for whatever you want for actions. I've got a couple for clicks and turned on the swipe to delete for deleting realm records.
public class SessionRealmAdapter
extends RealmBasedRecyclerViewAdapter<Session, SessionRealmAdapter.ViewHolder> {
public class ViewHolder extends RealmViewHolder {
public TextView sessionTextView;
public ViewHolder(FrameLayout container) {
super(container);
this.sessionTextView = (TextView) container.findViewById(R.id.session_text_view);
}
}
public SessionRealmAdapter(
Context context,
RealmResults<Session> realmResults,
boolean automaticUpdate,
boolean animateResults) {
super(context, realmResults, automaticUpdate, animateResults);
}
#Override
public ViewHolder onCreateRealmViewHolder(ViewGroup viewGroup, int viewType) {
View v = inflater.inflate(R.layout.session_simple_view, viewGroup, false);
return new ViewHolder((FrameLayout) v);
}
#Override
public void onBindRealmViewHolder(ViewHolder viewHolder, int position) {
final Session singleSession = realmResults.get(position);
viewHolder.sessionTextView.setText(singleSession.gettMethod());
viewHolder.sessionTextView.setOnClickListener(
new View.OnClickListener(){
#Override
public void onClick(View v){
selectSession(singleSession);
showMessage(" Selected "+singleSession.gettMethod());
}
}
);
viewHolder.sessionTextView.setOnLongClickListener(
new View.OnLongClickListener(){
#Override
public boolean onLongClick(View v){
showInformationDialog(singleSession);
showMessage("Long click selected for "
+singleSession.getSessionTitle());
return true;
}
}
);
}
}
My question is about filtering in Android. Both Ebay and Foursquare have what looks to be a dialog fragment that slides in from the right. Within this fragment are several nested listviews that open and expand to fill the fragment. When a nested listview is opened a back arrow appears at the top as well.
Once filters are selected they show as the listview text under each category name within the top level fragment. (see the different colored text for "Disney" and "HTC" in the ebay screenshot)
I'm wondering is there a library to implement nested listviews like this? Is this considered best practice for filtering search results?
I've included screenshots to hopefully show what i'm talking about.
You able to do this via android Expandable ListView with custom cell.
Here is one example about Expandable ListView with custom cell.
Thanks
May can achieve by android Recyclerview and some logic. I have handled a similar situation in the following manner.
FilterViewAdapter - Adapter of Recycler.
FilterListPresenter - class used to separate business logic.
Eight different type of cells used to generate filter list dynamically.
static final int FILTER_HEADER = 0;
static final int FILTER_DROPDOWN_HEADER = 1;
static final int FILTER_DROPDOWN_ITEM_RADIO = 2;
static final int FILTER_DROPDOWN_ITEM_CHECKBOX = 3;
static final int FILTER_DROPDOWN_ITEM_RADIO_LAST = 22;
static final int FILTER_DROPDOWN_ITEM_CHECKBOX_LAST = 33;
static final int FILTER_DUEDATE = 4;
static final int FILTER_PROGRESS = 5;
getItemViewType() provide the type of cell based on the logic. Example if a filter is opened then provide a different type of cell than usual filter header.
#Override
public int getItemViewType(int position) {
return filterListPresenter.getItemViewType(position);
}
Find my Adapter class
public class FilterViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements FilterListPresenter.AdapterCallBacks {
FilterListPresenter filterListPresenter;
Context context;
public FilterViewAdapter(Context context, FilterListPresenter filterListPresenter) {
this.filterListPresenter = filterListPresenter;
this.context = context;
filterListPresenter.setAdapterCallback(this);
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(filterListPresenter.getLayoutForView(viewType), parent, false);
return filterListPresenter.getViewHolder(view,viewType);
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
filterListPresenter.onBindCallLogRowViewAtPosition(position,holder,context);
}
#Override
public int getItemViewType(int position) {
return filterListPresenter.getItemViewType(position);
}
#Override
public int getItemCount() {
return filterListPresenter.getRowsCount();
}
#Override
public void notifyDataSetChanged1() {
notifyDataSetChanged();
}
}
Find my Presenter class
public class FilterListPresenter<T extends RecyclerView.ViewHolder> implements FilterListener {
static final int FILTER_HEADER = 0;
static final int FILTER_DROPDOWN_HEADER = 1;
static final int FILTER_DROPDOWN_ITEM_RADIO = 2;
static final int FILTER_DROPDOWN_ITEM_CHECKBOX = 3;
static final int FILTER_DROPDOWN_ITEM_RADIO_LAST = 22;
static final int FILTER_DROPDOWN_ITEM_CHECKBOX_LAST = 33;
static final int FILTER_DUEDATE = 4;
static final int FILTER_PROGRESS = 5;
Context context;
SessionData sessionData;
int openPosition = -1;
List<FIlterData> fIlterDatas ;
int numberOfRows = 5;
String[] filterNAmes = {"Checkin Template", "Employee", "Due Date", "Percentage of progress", "Status"}; //Main filters
int[] filterICons = {R.drawable.outline_view_agenda_black_24,
R.drawable.outline_person_black_24 ,
R.drawable.outline_calendar_today_black_24,
R.drawable.outline_trending_up_black_24,
R.drawable.outline_check_circle_black_24};
private String checkBoxValue;
public FilterListPresenter(Context context, SessionData sessionData) {
this.context = context;
this.sessionData = sessionData;
initilizeData(sessionData);
}
private void initilizeData(SessionData sessionData) {
fIlterDatas = new ArrayList<>();
//add data
}
public void onBindCallLogRowViewAtPosition(int position, T rowView, Context context) {
int itemViewType = getItemViewType(position);
switch (itemViewType) {
case FILTER_HEADER:
FilterHeaderHolder filterHeaderHolder = (FilterHeaderHolder) rowView;
filterHeaderHolder.setListener(this);
filterHeaderHolder.setTitle(getFilterTitle(position));
filterHeaderHolder.setImageView(getIcon(position), context);
break;
case FILTER_DROPDOWN_HEADER:
//add logic for each type cell
break;
case FILTER_DROPDOWN_ITEM_CHECKBOX:
case FILTER_DROPDOWN_ITEM_CHECKBOX_LAST:
break;
case FILTER_DROPDOWN_ITEM_RADIO:
case FILTER_DROPDOWN_ITEM_RADIO_LAST:
break;
case FILTER_PROGRESS:
break;
case FILTER_DUEDATE:
break;
}
}
private int getIcon(int position) {
if((openPosition == -1) || (position<=openPosition))
return filterICons[position];
else
return filterICons[position - getExtraRowCountForFIlter(openPosition)];
}
private String getFilterTitle(int position) {
if((openPosition == -1) || (position<=openPosition))
return filterNAmes[position];
else
return filterNAmes[position - getExtraRowCountForFIlter(openPosition)];
}
public int getRowsCount() {
if(openPosition == -1) return numberOfRows;
else return numberOfRows+ getExtraRowCountForFIlter(openPosition);
}
private int getExtraRowCountForFIlter(int position) {
switch (position){ //Additional one for header
case 0: return ((List<CheckinTemplate>) fIlterDatas.get(0).getFilterData()).size()+1;
case 1: try{return ((List<Employee>) fIlterDatas.get(1).getFilterData()).size()+1;}catch (Exception exc){
return 0;
}
case 4: return ((List<Status>) fIlterDatas.get(4).getFilterData()).size()+1;
default: return 1;
}
}
public int getItemViewType(int position) { //Complex logic to determine my cell type
if(openPosition == -1 || (position<=openPosition))
return FILTER_HEADER;
else {
int extraRowsForOpenFilter = getExtraRowCountForFIlter(openPosition);
if(position > (openPosition+extraRowsForOpenFilter))
return FILTER_HEADER;
switch (openPosition){
case 0:
if(openPosition+1 == position) return FILTER_DROPDOWN_HEADER;
else if(openPosition+extraRowsForOpenFilter == position) return FILTER_DROPDOWN_ITEM_RADIO_LAST;
else return FILTER_DROPDOWN_ITEM_RADIO;
case 1: if(openPosition+1 == position) return FILTER_DROPDOWN_HEADER;
else if(openPosition+extraRowsForOpenFilter == position) return FILTER_DROPDOWN_ITEM_CHECKBOX_LAST;
else return FILTER_DROPDOWN_ITEM_CHECKBOX;
case 4:
if(openPosition+1 == position) return FILTER_DROPDOWN_HEADER;
else if(openPosition+extraRowsForOpenFilter == position) return FILTER_DROPDOWN_ITEM_CHECKBOX_LAST;
else return FILTER_DROPDOWN_ITEM_CHECKBOX;
case 2:
return FILTER_DUEDATE;
case 3:
return FILTER_PROGRESS;
default: return FILTER_HEADER;
}
}
}
public int getLayoutForView(int viewType) {
switch (viewType) {
case FILTER_HEADER:
return R.layout.a_filter_list_item_header;
case FILTER_DROPDOWN_HEADER:
return R.layout.a_filter_list_item_dropdown_header;
case FILTER_DROPDOWN_ITEM_RADIO:
return R.layout.a_filter_list_item_dropdown_item_radio;
case FILTER_DROPDOWN_ITEM_RADIO_LAST:
return R.layout.a_filter_list_item_dropdown_item_radio_last;
case FILTER_DROPDOWN_ITEM_CHECKBOX:
return R.layout.a_filter_list_item_dropdown_item_chekbox;
case FILTER_DROPDOWN_ITEM_CHECKBOX_LAST:
return R.layout.a_filter_list_item_dropdown_item_chekbox_last;
case FILTER_DUEDATE:
return R.layout.a_filter_list_item_duedate;
case FILTER_PROGRESS:
return R.layout.a_filter_list_item_progress;
}
return FILTER_HEADER;
}
#Override
public void onFilterItemClick(int position, int viewTypeId) {
switch (viewTypeId) {
case FILTER_HEADER:
if(openPosition!=-1) minimizeFilters();
else {
openPosition = position;
adapterCallback.notifyDataSetChanged1();
}
case FILTER_DROPDOWN_HEADER:
break;
case FILTER_DROPDOWN_ITEM_RADIO:
case FILTER_DROPDOWN_ITEM_RADIO_LAST:
break;
case FILTER_DROPDOWN_ITEM_CHECKBOX:
case FILTER_DROPDOWN_ITEM_CHECKBOX_LAST:
break;
case FILTER_DUEDATE:
break;
case FILTER_PROGRESS:
break;
}
}
#Override
public RecyclerView.ViewHolder getViewHolder(View view, int viewType) {
switch (viewType) {
case FILTER_HEADER:
return new FilterHeaderHolder(view);
case FILTER_DROPDOWN_HEADER:
return new FilterListDropdownHeader(view);
case FILTER_DROPDOWN_ITEM_RADIO:
case FILTER_DROPDOWN_ITEM_RADIO_LAST:
return new FilterListItemRadioButton(view);
case FILTER_DROPDOWN_ITEM_CHECKBOX:
case FILTER_DROPDOWN_ITEM_CHECKBOX_LAST:
return new FilterListItemCheckBox(view);
case FILTER_DUEDATE:
return new FilterListItemDueDate(view);
case FILTER_PROGRESS:
return new FilterListItemProgress(view);
default: return new FilterHeaderHolder(view);
}
}
private void minimizeFilters() {
openPosition = -1;
adapterCallback.notifyDataSetChanged1();
}
AdapterCallBacks adapterCallback;
public void setAdapterCallback(FilterViewAdapter adapterCallback) {
this.adapterCallback = adapterCallback;
}
public String getDropdownHeader() {
if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof CheckinTemplate){
return "Select Template";
}else if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof Employee){
return "Select Employees";
}else if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof Status){
return "Select status";
}
return "";
}
public String getCheckBoxValue(int pos) {
if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof CheckinTemplate){
return ((List<CheckinTemplate>) fIlterDatas.get(openPosition).getFilterData()).get(pos).getName();
}else if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof Employee){
return ((List<Employee>) fIlterDatas.get(openPosition).getFilterData()).get(pos).getEmpName();
}else if( ((List<?>) fIlterDatas.get(openPosition).getFilterData()).get(0) instanceof Status){
return ((List<Status>) fIlterDatas.get(openPosition).getFilterData()).get(pos).getName();
}
return "";
}
public interface AdapterCallBacks{
void notifyDataSetChanged1();
}
}
Similar way you can construct your filter. Add your thoughts.
I want to create a ListView for a chat application in Android , In the listview I want to show Text layout if user type a text and Image Layout if user sends a image , I have designed two layouts as sms_row.xml and sms_chat_row.xml , the app works fine if I use just Text and Just Image only , by making my condition manually true and false , but If i am trying to do that dynamically getItemViewType(int position) shows error and Image crashed , here is my code of Adapter class -->
DevAdapter.java
package com.example.imageinsertdynamic;
import java.util.ArrayList;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.LinearLayout.LayoutParams;
public class DevAdapter extends BaseAdapter {
private ArrayList<Message> list;
private Context mCont;
LayoutParams lp;
public Message ob;
int type;
public DevAdapter(Context context, ArrayList<Message> msg) {
super();
this.mCont = context;
this.list = msg;
}
#Override
public int getCount() {
return list.size();
}
#Override
public Object getItem(int position) {
return list.get(position);
}
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getItemViewType(int position) {
if(true)//if ob.issetStatus() true if user types and image if user selects an image from the directory. but getting nullPointerException here.
{
return 0;
}
else
{
return 1;
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ob=(Message) this.getItem(position);
ViewHolder holder;
type=getItemViewType(position);//1 for case one and 2 for case 2 , need getItemViewtype() to bring this 1 and 2 but app crashes doing so need help in this function only
/*
*
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*
* HHHHHHHHEEEEEEEEEEEELLLLLLLLLLLPPPPPPPPPPP HERE !!
*
*
* \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
*/
if(convertView == null)
{
holder = new ViewHolder();
switch (type) {
case 0:
convertView = LayoutInflater.from(mCont).inflate(R.layout.sms_row_chat, parent, false);
holder.text = (TextView) convertView.findViewById(R.id.message_text);
convertView.setTag(holder);
break;
case 1:
convertView = LayoutInflater.from(mCont).inflate(R.layout.sms_row, parent, false);
holder.message = (ImageView) convertView.findViewById(R.id.message);
convertView.setTag(holder);
break;
}
}
else
holder = (ViewHolder) convertView.getTag();
switch(type){
case 0:
holder.text.setText(ob.getmsg());
System.out.println("value of isStatus image or text-->");
lp = (LayoutParams) holder.text.getLayoutParams();
break;
case 1:
holder.message.setImageBitmap(ob.getMessage());
System.out.println("value of isStatus image or text-->");
lp = (LayoutParams) holder.message.getLayoutParams();
break;
}
//Check whether message is mine to show green background and align to right
if(ob.isMine())
{ switch(type){
case 0:
holder.text.setBackgroundResource(R.drawable.speech_bubble_orange);
lp.gravity = Gravity.LEFT;
break;
case 1:
holder.message.setBackgroundResource(R.drawable.speech_bubble_orange);
lp.gravity = Gravity.LEFT;
break;
}
}
//If not mine then it is from sender to show orange background and align to left
else
{
switch(type){
case 0:
holder.text.setBackgroundResource(R.drawable.speech_bubble_green);
lp.gravity = Gravity.RIGHT;
break;
case 1:
holder.message.setBackgroundResource(R.drawable.speech_bubble_green);
lp.gravity = Gravity.RIGHT;
break;
}
}
switch(type){
case 0:
holder.text.setLayoutParams(lp);
break;
case 1:
holder.message.setLayoutParams(lp);
break;
}
//holder.message.setTextColor(R.color.textColor);
return convertView;
}
private static class ViewHolder
{
ImageView message;
TextView text;
}
#Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}
}
I have changed the return to 0 and 1 , but at the if Statement in getItemViewType(int position)
i have , the ob object is defined in getView so cant use it in getItemView(int position) what to do here to make it properly working ?
public int getItemViewType(int position) {
if(ob.isStatusMessage)//if ob.issetStatus() true if user types and image if user selects an image from the directory. but getting nullPointerException here.
{
return 0;
}
else
{
return 1;
}
}
isStatusMessage is from -->
Message.java
package com.example.imageinsertdynamic;
import android.graphics.Bitmap;
public class Message {
/**
* The content of the message
*/
Bitmap message;
String msg;
/**
* boolean to determine, who is sender of this message
*/
boolean isMine;
/**
* boolean to determine, whether the message is a status message or not.
* it reflects the changes/updates about the sender is writing, have entered text etc
*/
boolean isStatusMessage;
/**
* Constructor to make a Message object
*/
public Message(Bitmap message, boolean isMine,boolean status) {
super();
this.message = message;
this.isMine = isMine;
this.isStatusMessage = status;
}
/**
* Constructor to make a status Message object
* consider the parameters are swaped from default Message constructor,
* not a good approach but have to go with it.
*/
public Message(boolean status, String message,boolean isMine) {
super();
this.msg = message;
this.isMine = isMine;
this.isStatusMessage = status;
}
public Bitmap getMessage() {
return message;
}
public void setMessage(Bitmap message) {
this.message = message;
}
public String getmsg() {
return msg;
}
public void setMessage(String text) {
this.msg = text;
}
public boolean isMine() {
return isMine;
}
public void setMine(boolean isMine) {
this.isMine = isMine;
}
public boolean isStatusMessage() {
return isStatusMessage;
}
public void setStatusMessage(boolean isStatusMessage) {
this.isStatusMessage = isStatusMessage;
}
}
instead of returning 1 and 2, returns 0 and 1. It is like the index for an Array. If you create an array of two position and try to access array[2] you will get ArrayIndexOutBoundException. That's nearly what is happening to you
#Override
public int getItemViewType(int position) {
if(true)//if ob.issetStatus() true if user types and image if user selects an image from the directory. but getting nullPointerException here.
{
return 0;
}
else
{
return 1;
}
Edit:
Tipically the condiotion is associated with the dataset (ArrayList<Message>). getItemViewType has as paramter the position. So you have to retrieve the item at that position, and check if the condition is met
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