Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What design concept to use to update the UI async

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:

enter image description here

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:

  1. the Timeline is built by populating all days with PlaceholderItems (a TimelineItem with just a Date).
  2. Events are loaded from the DB and transformed to TimelineItems
  3. 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.

enter image description here

When scrolling away and returning to the range of days that was displayed incorrectly before, all is good:

enter image description here

enter image description here

like image 609
kazume Avatar asked Jan 17 '16 16:01

kazume


1 Answers

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:

  1. Make a copy of the original data (mTimelineItems) and send it to the AsyncTask.
  2. Change the copy asynchronously in doInBackground() and log all changes.
  3. Return the changed data and logs to the UI thread.
  4. 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.

  1. 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.

  2. 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);
like image 63
Andrew0x1 Avatar answered Oct 04 '22 01:10

Andrew0x1