如果我的 RecyclerView 适配器被重新初始化几次会怎样?

What happens if my RecyclerView adapter is reinitialized several times?

我在一个函数中有一些回收器视图代码,该函数在扫描蓝牙设备时被调用多次。我的代码正在运行,但我想知道将我的回收程序视图初始化代码放在一个重复很多的函数中会产生什么看不见的影响?我最终想更新列表而不是通过 notifyDataSetChanged() 替换它,但我不确定如何使用我当前的代码结构来做到这一点。如有任何帮助,我们将不胜感激!

@SuppressLint("MissingPermission", "NotifyDataSetChanged")
    fun displayDevices(
        scannedDevicesStrings: TreeSet<String>,
        deviceMap: HashMap<String, String>
    ) {
        
        val sortedDeviceMap = deviceMap.toSortedMap()

        Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")

        // Set linear layout manager for the widget.
        val linearLayoutManager = LinearLayoutManager(applicationContext)
        binding.recyclerviewDevices.layoutManager = linearLayoutManager

        // Specify an adapter.
        listAdapter = CustomAdapter(scannedDevicesStrings.toList(), sortedDeviceMap, bluetoothManager)
        binding.recyclerviewDevices.adapter = listAdapter

        listAdapter.notifyDataSetChanged()

        // Notify the view to update when data is changed.
        if ( binding.recyclerviewDevices.isAttachedToWindow) {
            binding.progressBarCyclic.visibility = GONE
        }
    }

此代码调用我的 CustomAdapter() class,它看起来像这样:

class CustomAdapter(
    private val treeSet: List<String>,
    private var hashMap: SortedMap<String, String>,
    private val bluetoothManager: BluetoothManager
    ) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.textview_list_item)
        val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
        val context: Context = view.context
        val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.text_device_row_item, parent, false)

        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val deviceList = hashMap.keys.toList()
        val macAddressList = hashMap.values.toList()
        holder.textView.text = deviceList.elementAt(position)
        holder.textView2.text = macAddressList.elementAt(position)
        val selectedDeviceString = deviceList.elementAt(position).toString()
        val selectedDevice = bluetoothManager.adapter.getRemoteDevice(hashMap[selectedDeviceString])
        val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return
            with(sharedPreferences.edit()) {
                putString("selectedDeviceString", selectedDevice.toString())
                apply()
            }
        holder.listLayout.setOnClickListener {
            val intent = Intent(holder.context, DeviceActivity::class.java)
            intent.putExtra("btDevice", selectedDevice)
            intent.putExtra("btDeviceName", selectedDeviceString)
            sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
            sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
            sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
            holder.context.startActivity(intent)
        }
    }

    override fun getItemCount() = treeSet.size
}

应避免多次设置适配器。这样做会导致其滚动位置丢失并重置到顶部,并导致它必须重新膨胀其所有视图和 ViewHolder。相反,您应该更新适配器指向的模型并对其进行 notifyDataSetChanged() (或者更好的是,使用 DiffUtil 更新单个项目)。

设置新的适配器会使 RecyclerView 重新初始化自身,并且它会再次创建所有 ViewHolder,等等。您真的希望避免这种情况。这通常是您更新它的方式:

class CustomAdapter(
    private var data: List<Thing>
    ...
) {

    fun setData(data: List<Thing>) {
        // store the data and do any other setup work
        this.data = data
        // make sure the RecyclerView updates to show the new data
        notifyDataSetChanged()
    }

}

然后您只需要在 onCreate 或其他任何地方首次创建适配器时保留对适配器的引用,并在获得新内容时调用 theAdapter.setData(newData)。您只需创建一个适配器来处理在列表中显示数据的设置,然后在需要更新时将数据交给它。

实际的“事物如何更新”逻辑在 setData 中 - 适配器的内部工作方式是适配器的业务,您知道吗?现在这是最基本的 notifyDataSetChanged() 调用,即“刷新所有内容”,但您可以稍后更改它 - 尽管外界不需要关心它。


我在 onBindViewHolder 中注意到您正在这样做:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val deviceList = hashMap.keys.toList()
    val macAddressList = hashMap.values.toList()

那个函数 运行s 每次 a ViewHolder 需要显示一些新信息(基本上就在它滚动到屏幕上之前)所以你每次滚动时都会创建很多列表。真的你应该这样做一次,当数据被设置时 - 从源中导出你的列表并将它们保存在周围:

class CustomAdapter(
    initialData: List<Thing> // not a var now - see init block
    ...
) {

    // these might need to be lateinit since you're init-ing through a function
    private var data: List<Thing>
    private var deviceList: List<String>
    private var macAddressList: List<String>

    init {
        setData(initialData)
    }

    fun setData(data: List<Thing>) {
        // Now you're storing the data and deriving the other lists
        // You might not even need to store the 'data' object if you're not using it?
        this.data = data
        deviceList = data.keys.toList()
        macAddressList = data.values.toList()

        // make sure the RecyclerView updates to show the new data
        notifyDataSetChanged()
    }

}

所以现在 setData 获取一些源数据,导出您需要使用的列表并存储这些列表,然后调用更新函数。因为必须完成该设置,所以每次设置源数据时都需要调用此函数 - 包括首次创建适配器时。这就是为什么构造函数中的数据参数不是 var,它只是用于初始化,传递给 setData 调用。

或者,根本不在构造函数中传递任何数据 - 只需创建适配器,然后立即对其调用 setData。将变量初始化为 emptyList() 等,然后您根本不需要处理“构建时设置”的情况!


另外一些提示 - 我不知道 treeSet 的用途,但您在 getItemCount 中使用了它的大小。你不应该这样做,它通常应该反映你实际显示的数据集的大小,即 hashSet 的内容 - 当你在 onBindViewHolder 中得到 position 时,您正在查找 hashSet 中的一个元素,而不是 treeSet,因此这应该是您的项目数

的来源

另一件事是,onBindViewHolder 中的所有内容...您正在做大量的设置工作,这些工作实际上只应在实际单击该项目时发生。通常,您会在 onCreateViewHolder 中设置一次点击侦听器,然后在绑定时在查看器上设置一个字段,告诉它当前正在显示哪个 position。如果点击侦听器触发,然后你可以在数据中查找当前的position,创建Intent

即使您不将其移入 VH,至少也应将设置代码移入 onClickListener,这样就不会 运行 每次有新项目滚动到视图中时。 sharedPreferences 位尤其是一个问题 - 每次绑定新项目时都会被覆盖(并且它们仍然可以绑定 off-screen)所以它可能没有设置为您期望的

我完成了代码更新,效果很好!添加新数据时,数据不再跳到顶部。以为我会 post 任何感兴趣的人的代码。

这是我的适配器:

class CustomAdapter(
    private val bluetoothManager: BluetoothManager
    ) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    private var sortedMap = emptyMap<String, String>()
    private var deviceList = emptyList<String>()
    private var macAddressList = emptyList<String>()

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.textview_list_item)
        val listLayout: FrameLayout = view.findViewById(R.id.item_layout)
        val context: Context = view.context
        val textView2: TextView = view.findViewById(R.id.textview_list_item_address)
    }

    @SuppressLint("NotifyDataSetChanged")
    fun setData(sortedMap: SortedMap<String, String>) {
        this.sortedMap = sortedMap
        deviceList = sortedMap.keys.toList()
        macAddressList = sortedMap.values.toList()

        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.text_device_row_item, parent, false)

        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        holder.textView.text = deviceList.elementAt(position)
        holder.textView2.text = macAddressList.elementAt(position)

        holder.listLayout.setOnClickListener {
            val selectedDeviceString = deviceList.elementAt(position).toString()
            val selectedDevice = bluetoothManager.adapter.getRemoteDevice(sortedMap[selectedDeviceString])
            val sharedPreferences = holder.context.getSharedPreferences("mSharedPrefs", Context.MODE_PRIVATE) ?: return@setOnClickListener
            with(sharedPreferences.edit()) {
                putString("selectedDeviceString", selectedDevice.toString())
                apply()
            }

            val intent = Intent(holder.context, DeviceActivity::class.java)
            intent.putExtra("btDevice", selectedDevice)
            intent.putExtra("btDeviceName", selectedDeviceString)
            sharedPreferences.edit().putString("loadedFrom", "loadedFromCustomAdapter").apply()
            sharedPreferences.edit().putString("selectedDeviceName", selectedDeviceString).apply()
            sharedPreferences.edit().putString("selectedDeviceString", selectedDevice.toString()).apply()
            holder.context.startActivity(intent)
        }
    }

    override fun getItemCount() = sortedMap.size
}

和 activity onCreate() 代码:

// Specify an adapter.
listAdapter = CustomAdapter(bluetoothManager)
binding.recyclerviewDevices.adapter = listAdapter

我更新数据的函数:

@SuppressLint("MissingPermission", "NotifyDataSetChanged")
    fun displayDevices(
        deviceMap: HashMap<String, String>
    ) {
        
        val sortedDeviceMap = deviceMap.toSortedMap()
        listAdapter.setData(sortedDeviceMap)

        Log.d(TAG, "displayDevices: ${sortedDeviceMap.entries}")

        // Notify the view to update when data is changed.
        if ( binding.recyclerviewDevices.isAttachedToWindow) {
            binding.progressBarCyclic.visibility = GONE
        }
    }