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

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

Это приблизительная схема того, как приложение создано на данный момент:

Введите описание изображения здесь

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

Загрузка Event s и преобразование Event s в TimelineItem выполняются в AsyncTasks. Все идет нормально.

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

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

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

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

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

РЕДАКТИРОВАТЬ:

Я обнаружил, что добавление

 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 был изменен и готов, Timeline выгружает TimelineFragment через шину otto, чтобы обновить набор данных адаптера для этой конкретной позиции с помощью нового TimelineItem.

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

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

Введите описание изображения здесь

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

Введите описание изображения здесь

Введите описание изображения здесь

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

Я предлагаю вам составить сочетание ваших первых и вторых подходов:

  1. Сделайте копию исходных данных ( mTimelineItems ) и отправьте их в AsyncTask .
  2. Измените копию асинхронно в doInBackground() и 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); } } 

Использование в пользовательском интерфейсе:

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

Я положил приложение Example на GH, поэтому вы можете проверить его, если хотите.

Обновление 1

Информация о гонке данных.

  1. this.mTimelineItems = timelineItems; Внутри конструктора TimelineAdapter() делает копию ссылки на ArrayList , но не копию самого ArrayList . Таким образом, у вас есть две ссылки: TimelineAdapter.mTimelineItems и Timeline.mTimelineItems , которые оба относятся к одному объекту ArrayList . Пожалуйста, посмотрите на это .

  2. Гонка данных возникает, когда doInBackground() из рабочего onProgressUpdate() и onProgressUpdate() вызванного из onProgressUpdate() пользовательского интерфейса одновременно. Основная причина заключается в том, что publishProgress() не вызывает onProgressUpdate() синхронно . Вместо этого publishProgress() планирует вызов onProgressUpdate() в onProgressUpdate() пользовательского интерфейса в будущем. Вот хорошее описание проблемы.

Не по теме.

Эта:

 mTimelineItems.set(position, item); 

Должен быть быстрее, чем это:

 mTimelineItems.remove(position); mTimelineItems.add(position, item);