RecyclerView和适配器数据更新

23

这是一个关于RecyclerView内部行为的问题,针对了解其机制或愿意深入源代码的人。我希望得到一份有源代码支持的答案。

原始问题

(向下滚动到“换言之”以获取更专注的问题)

我需要了解如何排队 notify* 操作(例如,notifyItemInserted())。想象一下,我有一个由此列表支持的适配器:

ArrayList<String> list = Arrays.asList("one", "three", "four");

我想要添加缺失的值zerotwo

示例1

list.add(1, "two");
// notify the view
adapter.notifyItemInserted(1);


// Seconds later, I go on with zero
list.add(0, "zero");
// notify the view
adapter.notifyItemInserted(0);

这很简单明了,没有什么需要解释的。

示例2

但如果这两个动作非常接近,并且它们之间没有布局传递怎么办?

list.add(1, "two");
list.add(0, "zero”);

我现在该做什么?

adapter.notifyItemInserted(1);
adapter.notifyItemInserted(0);

或者也许

adapter.notifyItemInserted(2);
adapter.notifyItemInserted(0);

从适配器的角度来看,列表立即从one, three, four切换到zero, one, two, three, four,因此第二个选项似乎更合理。

示例3

list.add(0, “zero”);
adapter.notifyItemInserted(0);
list.add(2, “two”);
adapter.notifyItemInserted(...)

现在怎么样?1还是2?列表在立即更新后,但我确定它们之间没有布局传递。

问题

你已经了解了主要问题,我想知道在这些情况下应该如何行事。实际情况是我有多个异步任务最终会在一个insert()方法中结束。我可以排队他们的操作,但:

  1. 如果已经有一个内部队列,我不想那样做,而且肯定有
  2. 如果两个动作之间没有布局传递,例如示例3,我不知道会发生什么。

换句话说

为了更新回收站,必须执行4个操作:

  1. 我实际上改变了数据模型(例如,将某些内容插入备份数组中)
  2. 我调用adapter.notify*()
  3. 回收站接收到调用
  4. 回收站执行操作(例如,在适配器上调用getItem*()onBind()),并布置更改。

当没有并发时很容易理解它们按顺序发生:

1. => 2. => 3. => 4. => (new update) 1. => 2. => 3. => 4. ...

让我们看看步骤之间会发生什么。

  • 步骤1步骤2之间:我认为开发者有责任在修改数据后立即调用notify()。这没问题。
  • 步骤2步骤3之间:这会立即发生,没有问题。
  • 步骤3步骤4之间:这并不是立即发生的!据我所知。因此,在上一次更新步骤3步骤4之间可能会出现新的更新(步骤12)。

我想了解在这种情况下会发生什么。 我们应该怎么做? 我应该确保上一次更新步骤4已经完成,然后再插入新的内容吗?如果是,如何操作?


1
例如2:您可以使用notifyitemrange方法。例如3:使用notifyiteminserted(2)。 - Divyesh Patel
@Divyesh 我的测试结果与你的不同,因此我希望得到一个有代码支持的答案。另外,我使用小索引(0和1)来使事情更清晰,但请将它们视为随机的(例如0和25)。此外,如果您查看源代码,您会发现 notifyItemInserted(position) 总是被视为 notifyItemRangeInserted(position, 1)。在这里使用哪一个并没有区别。 - natario
对于多个操作,我认为您应该先进行所有插入,然后使用notifydatasetchnged。 - Divyesh Patel
4个回答

8

我之前想过类似的问题,我的决定是:

  1. If I want to insert more than 1 item directly to end of list and want to get a animation for all, I should:

    list.add("0");
    list.add("1");
    adapter.notifyItemRangeInserted(5, 2); // Suppose there were 5 items before so "0" has index of 5 and we want to insert 2 items.
    
  2. If I want to insert more than 1 item directly to end of list, but want to get separated animation for each inserted item, I should:

    list.add("0");
    list.add("1");
    adapter.notifyItemInserted(0);
    mRecyclerView.postDelayed(new Runnable() {
        @Override
        public void run() {
            // before this happens, Be careful to call other notify* methods. Never call notifyDataSetChanged.
            adapter.notifyItemInserted(1); 
        }
    }, mRecyclerView.getItemAnimator().getAddDuration());
    
  3. If I want to insert more than 1 item to different position of list, similar as 2.

希望这可以帮助到您。


4

让我们从一些介绍开始,了解RecyclerView如何与notify items一起工作。它可以与其他保存ViewGroup项的列表(例如ListView)相当简单地配合使用。

RecyclerView拥有已经绘制的View Items队列,并且在没有调用notify(...)方法的情况下不知道任何更新。当您添加新项目并通知RecyclerView时,它会开始循环逐个检查所有视图。

RecyclerView contains and drawn next objects
View view-0 (position 0), view-1 (position 1), View-2 (position 2)

// Here is changes after updating
You added Item View view-new into (position 1) and Notify
RecyclerView starts loop to check changes
RecyclerView received unmodified view-0(position-0) and left them;
RecyclerView found new item view-new(position 1)
RecyclerView removing old item view-1(position 1)
RecyclerView drawing new item view-new(position 1)

// In RecyclerView queue in position-2 was item view-2, 
// But now we replacing previous item to this position
RecyclerView found new item view-1 (new position-2)
RecyclerView removing old item view-2(position 2)
RecyclerView drawing new item view-1(position 2)

// And again same behavior 
RecyclerView found new item view-3 (new position-3)
RecyclerView drawing new item view-1(position 2)

// And after all changes new RecyclerView would be
RecyclerView contains and drawn next objects
View view-0 (position 0), view-new (position 1) view-1 (position 2), View-2 (position 3)

这只是工作通知功能的主要流程,但需要知道所有这些操作都发生在UI线程、主线程上,即使您可以从异步任务中调用更新。回答您的两个问题 - 您可以随时调用RecyclerView的通知,并确保您的操作将在正确的队列上执行。
RecyclerView在任何情况下都能正常工作,更复杂的问题可能涉及到Adapter的工作。首先,您需要同步Adapter的操作,例如添加或删除项目,并完全避免使用索引。例如,在您的示例3中,最好这样做。
Item firstItem = new Item(0, “zero”);
list.add(firstItem);
adapter.notifyItemInserted(list.indexOf(firstItem));
//Other action...
Item nextItem = new Item(2, “two”);
list.add(nextItem);
adapter.notifyItemInserted(list.indexOf(nextItem))
//Other actions

更新 |

RecyclerView.Adapter文档相关,您可以看到与notifyDataSetChanged()相同的函数。此外,此RecyclerView.Adapter使用android.database.Observable扩展调用子项,请参见关于Observable。访问此可观察持有者是同步的,直到RecyclerView中的View元素释放使用。

另请参阅支持库版本25.0的RecyclerView,行号为9934-9988;


我的问题是,如果我在RecyclerView能够执行最后一个notify()操作之前(例如你所说的循环),修改了支持数组(list.add)并通知RecyclerView,会发生什么? - natario
@natario 我已经回答过了。即使你使用处理程序任务来启动你的操作并且每秒通知一百次,Recycler Queue 也会与更新数据同步,并且按顺序执行所有操作。 - GensaGames
关于这个问题(请参见上文,我添加了一个部分以使问题更清晰),您告诉我步骤4会立即、同步地在步骤3之后发生。您能否提供源代码的引用来支持这一点?在我看来,这并不是真的。请参考android.support.v7.widget.AdapterHelper类。 - natario

3
如果您在布局过程之间进行多次更新,这不应该成为问题。RecyclerView旨在处理(并优化)此情况:

RecyclerView在RecyclerView.Adapter和RecyclerView.LayoutManager之间引入了一种额外的抽象级别,以能够在布局计算期间批量检测数据集更改。[...] RecyclerView有两种与位置相关的方法:

  • layout position:项在最新布局计算中的位置。这是从LayoutManager的角度来看的位置。
  • adapter position:项在适配器中的位置。这是从Adapter的角度来看的位置。

这两个位置相同,除了在分派adapter.notify*事件和计算更新布局之间的时间内

在您的情况下,步骤如下:
  1. 您更新数据层

  2. 您调用adapter.notify*()

  3. recyclerview记录更改(如果我正确理解代码,则为AdapterHelper.mPendingUpdates)。这个更改将反映在ViewHolder.getAdapterPosition()中,但尚未反映在ViewHolder.getLayoutPosition()中。

  4. 在某些时候,recyclerView会应用记录的更改,基本上协调布局的观点和适配器的观点。似乎这可以在布局之前发生。
只要2.紧随1.(并且两者都在主线程上运行),1.2.3.顺序就可以发生任意次数。
(1. => 2. => 3.) ... (1. => 2. => 3.) ... 4. 

并且两者都在主线程上发生 为什么第一件事应该在主线程上发生?除此之外,我很快会回来向您询问澄清。 - natario
否则不能保证2)紧随1)之后立即发生,会有延迟。我认为实际顺序应该是0)在后台线程上收集数据,然后通过事件总线或处理程序发布结果,并在主线程上以原子方式执行1)和2)。 - bwt
适配器所看到的数据应立即更改,切换应在主线程上发生。 - bwt
是的,我的意思是“只要2紧随1之后(且两者都在主线程上发生)”更正确,而不是“只要2紧随1之后(且1和2都在主线程上发生)”。 - natario

-2
Item firstItem = new Item(0, “zero”);

list.add(firstItem);

adapter.notifyItemInserted(list.indexOf(firstItem));

//Other action...

Item nextItem = new Item(2, “two”);

list.add(nextItem);

adapter.notifyItemInserted(list.indexOf(nextItem))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接