android - 使用什么设计理念来异步更新UI

标签 android android-asynctask

我正在开发一款在时间线上显示工作时间表的应用。

这是目前应用程序设计的粗略布局:

enter image description here

数据存储在 SQLite 数据库中。当 Timeline(一个单例对象)从数据库助手类请求数据时,它会得到一个包含 Event 的 ArrayList(例如,一个 Event 可以是从 2016 年 5 月 1 日 03:00 开始到 2016 年 5 月 3 日 16:00 结束的职责)。 Timeline 然后将这些 Event 转换为 TimelineItem,一个表示(部分)Event 的类特定的一天。

Event 的加载和 EventTimelineItem 的转换都在 AsyncTasks 中完成。到目前为止一切顺利。

现在是我正在苦苦挣扎的部分:在获取新的数据库后更新 UI。

我的第一种方法是将更新的 TimelineItems 的 ArrayList 传递给 RecyclerView 适配器,并让适配器知道数据已通过 notifyDatasetChanged() 更改。这种方法的问题是 1) 正在做很多不必要的工作(因为我们正在重新计算所有事件/时间线项目,而不仅仅是那些改变的)和 2) RecyclerView 上的滚动位置在每次 DB 获取后重置

在我的第二种方法中,我实现了一些方法来检查自上次显示以来哪些事件/TimelineItems 发生了变化,其想法是使用 notifyItemChanged() 仅更改那些 TimelineItems。完成的工作更少,根本无需担心滚动位置。棘手的一点是检查哪些项目已更改确实需要一些时间,因此也需要异步完成:

我尝试在 doInBackground() 中进行代码操作,并通过在 onProgressUpdate() 中发布奥托总线事件来更新 UI。

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!
        }
    }
}

问题是,不知何故,在发布此进度时滚动会完全弄乱 UI。

我的主要问题是:当我更新适配器的数据集 (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");
}

需要注意的是,在我滚动离开更改的数据足够远并返回到之后的位置后,适配器的数据集似乎正确显示。不过,最初 UI 完全乱七八糟。

编辑:

我发现添加

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 就会通过 otto 总线调用 TimelineFragment,以使用新的 TimelineItem 为该特定位置更新适配器的数据集。

这是初始加载后发生的情况的屏幕截图:

时间轴已加载,但某些项目插入了错误的位置。

enter image description here

当滚动离开并返回到之前显示不正确的日期范围时,一切都很好:

enter image description here

enter image description here

最佳答案

关于您的第二种方法。您的代码可能无法正常工作,因为您在 mTimelineItemsmCachedEvents 上有Data Race。我看不到您的所有代码,但您似乎在 doInBackground() 中同时使用 mTimelineItems 与 UI 线程,但没有任何同步。

我建议您混合使用第一种和第二种方法:

  1. 复制原始数据 (mTimelineItems) 并将其发送到 AsyncTask
  2. doInBackground() 中异步更改副本并记录所有更改。
  3. 将更改的数据和日志返回到 UI 线程。
  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();
            }
        });
    }
}

我把Example GH 上的应用程序,因此您可以根据需要进行测试。

更新 1

关于数据竞赛。

  1. this.mTimelineItems = timelineItems;TimelineAdapter() 构造函数中复制了对 ArrayList 的引用,但不是ArrayList 本身的副本。因此,您有两个引用:TimelineAdapter.mTimelineItemsTimeline.mTimelineItems,它们都引用相同的 ArrayList 对象。请看this .

  2. 当从 Worker Thread 调用 doInBackground() 和从 UI Thread 调用 onProgressUpdate() 时发生数据竞争同时。主要原因是 publishProgress() 没有同步调用onProgressUpdate()。相反,publishProgress()计划在将来UI 线程 上调用onProgressUpdate()Here很好地描述了问题。

题外话。

这个:

mTimelineItems.set(position, item);

应该比这更快:

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

关于android - 使用什么设计理念来异步更新UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34840470/

相关文章:

android - 如何在 ListView 中显示数据库中的数据?

android - 您如何检测声音文件何时完成?

android - 适用于 Android 应用程序的 RESTful Web 服务

android - 致命异常 : AsyncTask #1, java.lang.RuntimeException:执行 doInBackground() 时发生错误

java - 在 ListView 中加载图像时加载错误的图像

java - 无法使用firebase云消息传递在mysql数据库中注册 token

android - 在 onPostExecute 中调用 setAdapter 返回空异常

android - AsynchTask 中的 progressBar 导致崩溃, "Unable to add window -- token null is not for an application"

安卓线程问题

java - 从android webview中的base64 url​​下载文件