使用 ViewModel 和 LiveData 从 RecyclerView 中的一个元素更新数据

Update data from one element in RecyclerView using ViewModel and LiveData

我正在尝试使用各种架构 components/concepts 编写一个应用程序,但现在我不确定 best/preferred 我的一个问题的解决方案是什么。

我在应用程序中有一个片段,它有一个 RecyclerView 并在列表中显示数据库中的元素。每个项目都有一个删除和一个编辑按钮。现在我必须实现这些功能,但我对如何做到这一点有点不知所措。 我可以更新数据库并监听那里的更改以更新 RecyclerView,或者我可以先更新 RecyclerView 并在片段为 destroyed/paused 时更新数据库。但我正在努力实施这些方法中的任何一种。

当元素已更改(显示新标题等)或从列表中删除时,需要更新视图。如果我将 onClickListener 放在 ViewHolder 中,我将无法访问完整的数据集。如果我将它放在 onCreateViewHolder 中,我将无法访问被单击的项目。在 OnBindViewHolder 中,我无权访问该按钮。

在 onClickListener 中,我要么需要更改 RecycleViewer 显示的数据(并在 Fragment 为 destroyed/paused 时更新数据库),要么直接调用访问数据库的 ViewModel 中的方法。我也不知道如何从适配器访问 ViewModel 及其方法(也许这通常不应该这样做)因为 ViewModelProvider(?)[PlacesViewModel::class.java] 需要 ViewModelStoreOwner 或 ViewModelStore 和我真的不明白其中一个或如何从适配器访问它们。

我错过了什么? best/proper 的方法是什么?这可能是非常明显的事情,但在所有这些新概念之间我感到有点迷茫。正如我所说,我是 Android 的新手,只是开始使用这里的大部分概念(LiveData、ViewModel 等)。可能是我没有正确使用它们,整个方法不正确。

这是片段:

class PlacesFragment : Fragment() {

    private var _binding: FragmentPlacesBinding? = null
    private val binding get(): FragmentPlacesBinding = _binding!!
    private lateinit var mViewModel: PlacesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewModel = ViewModelProvider(this)[PlacesViewModel::class.java]
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        _binding = FragmentPlacesBinding.inflate(inflater, container, false)

        // set the adapter for the recyclerview to display the list items
        val recyclerView = _binding!!.recyclerViewPlaces
        mViewModel.places.observe(viewLifecycleOwner){ places ->
            recyclerView.adapter = RecyclerViewPlacesAdapter(places)
        }

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

视图模型:

class PlacesViewModel(app: Application): AndroidViewModel(app) {

    private var _places: MutableLiveData<MutableList<Place>> = MutableLiveData()
    val places get() = _places

    init {
        viewModelScope.launch {
            try{
                val db = AppDatabase.getInstance(getApplication())
                _places.value = db.placeDao().getAll().toMutableList()
            } catch(e: Exception){
                Log.e("Error", e.stackTraceToString())
            }
        }
    }

    fun deletePlace(place: Place){
        viewModelScope.launch {
            try{
                val db = AppDatabase.getInstance(getApplication())
                _places.value?.remove(place)
                //db.placeDao().delete(place)
            } catch(e: Exception){
                Log.e("Error", e.stackTraceToString())
            }
        }
    }
}

和适配器:

class RecyclerViewPlacesAdapter(private val dataSet: List<Place>) :
    RecyclerView.Adapter<RecyclerViewPlacesAdapter.ViewHolder>() {

    class ViewHolder(itemBinding: ListTilePlacesBinding) : RecyclerView.ViewHolder(itemBinding.root) {
        private val placeTitle: TextView = itemBinding.placeTitle
        private val placeAddress: TextView = itemBinding.placeAddress
        var place: Place? = null

        fun setValues(){
            if(place != null){
                placeTitle.text = place!!.title
                placeAddress.text = place!!.address //TODO either address or lat/long
            }
        }

        init {
            // Define click listener for the ViewHolder's View.
            itemBinding.buttonDelete.setOnClickListener { 
                // TODO remove item from dataset and in RecyclerView
            }
            itemBinding.buttonEdit.setOnClickListener {
                // TODO edit item and display changes in RecyclerView
            }
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ViewHolder {
        val itemBinding = ListTilePlacesBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        return ViewHolder(itemBinding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.place = dataSet[position]
        holder.setValues()
    }

    override fun getItemCount() = dataSet.size
}

更新: 正如@cactustictacs 所建议的,我向适配器添加了一个 deleteItem() 方法,该方法从数据集中删除了项目并调用了 notifyItemRemoved 来更新视图。我还将 ViewModel 传递给适配器以调用其方法。我之前不确定这是否是将其传递给另一个方法的正确方法。

  1. 创建一个 PlaceListener class
class PlaceListener (val clickListener: (p: Place) -> Unit) {
    fun onClick (p: Place) = clickListener (p)
}
  1. 更换适配器
class RecyclerViewPlacesAdapter (
private val deleteClickListener: PlaceListener,
 private val updateClickListener: PlaceListener,
): ListAdapter <Place, RecyclerViewPlacesAdapter.ViewHolder> (PlaceDiffCallback ()) {

    override fun onCreateViewHolder (parent: ViewGroup, viewType: Int): RecyclerViewPlacesAdapter.ViewHolder {
       return RecyclerViewPlacesAdapter.ViewHolder.from (parent)
    }

    override fun onBindViewHolder (holder: RecyclerViewPlacesAdapter.ViewHolder, position: Int) {
        val item = getItem (position)
        return holder.bind (item, deleteClickListener, updateClickListener)
    }

    class ViewHolder private constructor (private val binding: ListTilePlacesBinding):
        RecyclerView.ViewHolder (binding.root) {

        fun bind (item: Place, deleteliCkListener: PlaceListener, updateClickListener: PlaceListener) {
            binding.apply {
                   buttonDelete.setOnClickListener {
                 deleteClickListener.onClick(item)
}
              buttonEdit.setOnClickListener {
           updateClickListener.onClick(item)
}
            }
        }

       companion object {
            fun from (parent: ViewGroup): RecyclerViewPlacesAdapter.ViewHolder {
                val layoutInflater = LayoutInflater.from (parent.context)
                val binding = ListTilePlacesBinding.inflate (layoutInflater, parent, false)
                return RecyclerViewPlacesAdapter.ViewHolder (binding)
            }
        }
    }

    class PlaceDiffCallback: DiffUtil.ItemCallback <Place> () {
        override fun areItemsTheSame (oldItem: Place, newItem: Place): Boolean {
            return oldItem == newItem
        }
        override fun areContentsTheSame (oldItem: Place, newItem: Place): Boolean {
// example of control of 3 properties
            return (
                    oldItem.property1 == newItem.property1 &&
                            oldItem.property2 == newItem.property2 &&
                            oldItem.property3 == newItem.property3
                    )
        }
    }
}
  1. 片段中:
 private lateinit var adapter: RecyclerViewPlacesAdapter

// after you initialize the binding
adapter = RecyclerViewPlacesAdapter (
PlaceListener {place -> mViewModel.deletePlace (place)},
PlaceListener {place -> mViewModel.updatePlace (place)},
 )
val recyclerView = _binding!!.recyclerViewPlaces
recyclerView.adapter = adapter
      mViewModel.places.observe (viewLifecycleOwner) {places ->
            adapter.submitList (places)
            adapter.notifyDataSetChanged ()
        }

如果您不喜欢使用 adapter.submitList(位置)和 adapter.notifyDataSetChanged (),您始终可以在构造函数中将列表传递给它们(在这种情况下,在适配器构造函数中您将拥有Place 对象和 2 个 placeClickListener 对象的列表)

从广义上讲,这个想法是您的 ViewModel 是 UI 层(视图)与之交谈的内容。它公开了您的 UI observe 的数据状态(例如通过 LiveData 对象),这就是控制 UI 显示的内容。观察者得到一些新数据 -> 你显示它。

UI 还会通知 VM 用户交互 - 按钮按下等。所以在你的例子中,你有 deletePlace 函数,它基本上告诉虚拟机“嘿,用户刚刚决定删除它,做你需要做的”。因为 VM 代表当前状态,所以当发生任何事情时你都需要更新它(并且它在内部处理诸如保持该状态之类的事情)

那是两条完全独立的道路,彼此隔绝。请记住,UI 显示 VM 告诉它的内容 ,因此它不需要对用户点击删除按钮做出反应。那只是被转发到虚拟机。 如果 VM 确定状态已更改,那么它将更新其 LiveData,UI 将观察新数据,然后 然后 它会更新。您基本上是将 VM 视为事实来源,并将更新逻辑保留在视图层之外。


这大大简化了事情,因为当您的 UI 只需要观察 VM 中的某些状态时,每次更新都是相同的。片段加载,填充列表?观察LiveData,得到一个结果,显示出来。从后台恢复的应用程序,可能有一个被破坏的进程?观察事物,每当有更新时显示。用户删除事物或更新事物?您正在观察数据,当存储更改时,它会更新。 ETC!每次都是一样的逻辑。无需担心在片段被销毁时保存状态 - 所有状态都在 VM 中!

(这可能有一些例外,例如,如果删除需要网络调用,可能需要一些时间,但您希望更改立即在视图中发生。但同样,这可能只是一个实现细节在 VM 中 - 它可以更新其本地数据状态,并让远程更新在后台进行)


就实施而言,我个人觉得让 ViewHolderAdapter 上调用方法更干净,比如 deleteItem(index: Int) 或其他任何东西 - 只是因为 VH 是一个 UI 组件,而适配器是位于数据集和用于显示它的 UI 之间的东西。感觉更像是适配器的工作来处理“用户单击删除此项目”等,如果这有意义的话。不过不是很重要。

通常将 Adapter 设为其父级 Fragmentinner class - 这样,如果 Fragment 具有视图模型引用,适配器就可以看到它。否则,您可以在设置期间将其作为 属性 在适配器上 pass/set。 ViewModel 是共享资源,因此无需担心传递它