Какую концепцию дизайна использовать для обновления UI async

я работаю над приложением, которое отображает рабочий график на временной линии.

это примерный макет того, как приложение разработано на данный момент:

enter image description here

данные хранятся в БД SQLite. Когда Timeline (одноэлементный объект) запрашивает данные из вспомогательного класса базы данных, он получает ArrayList Events (например, an Event может быть обязанность, начиная с 1 мая 2016 года в 03:00 и заканчивая 3 мая 2016 в 16:00). The Timeline затем преобразует эти Events до TimelineItems, класс, представляющий (часть)Event для определенного дня.

загрузка Events и преобразование Events до TimelineItems оба сделаны в AsyncTasks. Пока все хорошо.

теперь приходит часть, с которой я борюсь: обновление пользовательского интерфейса после новой выборки БД.

мой первый подход был передать обновленные ArrayList с TimelineItems к RecyclerView адаптер и пусть адаптер знает, что данные изменились с notifyDatasetChanged(). Проблема такого подхода заключается в том, что 1) выполняется много ненужной работы (потому что мы пересчитываем все события / TimelineItems, а не только те, которые были изменены) и 2) положение прокрутки на RecyclerView сбрасывается после каждой выборки DB

в моем 2-м подходе я реализовал некоторые методы для проверки того, какие события / TimelineItems изменились с момента последнего отображения с идеей изменения только этих TimelineItems, с notifyItemChanged(). Меньше работы делается, и не нужно беспокоиться о позициях прокрутки вообще. Хитрый бит заключается в том, что проверка того, какие элементы изменились, занимает некоторое время, поэтому ее также нужно выполнить асинхронно:

я попытался выполнить манипуляции с кодом в doInBackground() и обновление пользовательского интерфейса путем публикации событий шины otto в 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!
        }
    }
}

проблема в том, что каким-то образом прокрутка во время публикации этого прогресса полностью портит пользовательский интерфейс.

мой главный проблема: когда я обновляю определенный элемент в наборе данных (TimelineItems) адаптера, notifyItemChanged () изменяет элемент, но не помещает элемент в правильное положение.

вот:
/**
* 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();
        }
    }
}

и это метод, который запускается в TimelineFragment, когда изменение публикуется на шине событий:

@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");
}

следует отметить, что набор данных адаптера, кажется, отображается правильно после того, как я прокрутил от изменил данные достаточно далеко и вернуться на позицию после этого. Первоначально пользовательский интерфейс полностью испорчен.

EDIT:

я нашел, что добавление

mAdapter.notifyItemRangeChanged(position, mAdapter.getItemCount());

решает проблему, но-к сожалению-устанавливает позицию прокрутки на изменяемую: (

вот мой 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");
}

я сделал снимок экрана приложения после его первой загрузки. Я быстро объясню, что происходит. инициализация:

  1. временная шкала построена путем заполнения всех дней PlaceholderItems (TimelineItem только с датой).
  2. события загружаются из БД и преобразуются в TimelineItems
  3. всякий раз, когда новый TimelineItem изменился и готов, временная шкала тычет TimelineFragment через шину otto, чтобы обновить набор данных адаптера для этой конкретной позиции с новым TimelineItem.

вот скриншот о том, что происходит после начальной загрузки:

временная шкала загружается, но некоторые элементы вставляются в неправильном положении.

enter image description here

при прокрутке и возврате в диапазон дней, который был отображен неправильно раньше, все хорошо:

enter image description here

enter image description here

1 ответов


о вашем втором подходе. Вероятно, ваш код не работает, потому что у вас есть Гонки Данных on mTimelineItems и mCachedEvents. Я не вижу весь ваш код, но кажется, что вы используете mTimelineItems внутри doInBackground() одновременно с потоком пользовательского интерфейса без синхронизации.

я предлагаю вам смешать ваш первый и второй подходы:

  1. сделайте копию исходных данных (mTimelineItems) и отправить его на AsyncTask.
  2. изменить копию асинхронно в doInBackground() и регистрировать все изменения.
  3. верните измененные данные и журналы в поток пользовательского интерфейса.
  4. применить новые данные к RecyclerView с помощью журналов.

позвольте мне проиллюстрировать этот подход в код.

управление данными:

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);
    }
}

использование в 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();
            }
        });
    }
}

я поставил пример применение на GH, так можете проверить, если хотите.

обновление 1

О Гонке Данных.

  1. this.mTimelineItems = timelineItems; внутри TimelineAdapter() конструктор делает копию ссылки на ArrayList, но не копия!-Сама -13-->. Итак, у вас есть две ссылки:TimelineAdapter.mTimelineItems и Timeline.mTimelineItems, что оба относятся к одному и тому же