MVVM和适配器中的元素选择

关于第三个自写适配器,在其中必须实现记住所选元素的逻辑之后,我曾想过应该有一些已经包含所有必要内容的解决方案。 特别是在开发过程中,如果您不得不更改仅选择一项以进行多项选择的功能。


在研究了MVVM方法并将其完全浸入其中之后,上述问题就更加明显地出现了。 此外,适配器本身处于View级别,而对于ViewModel ,有关所选元素的信息通常是极其必要的。


也许我没有花足够的时间在Internet上寻找答案,但是无论如何,我都没有找到现成的解决方案。 但是,在一个项目中,我提出了一个可能是通用的实现想法,所以我想分享一下。


备注 。 尽管Android上的MVVM在LiveData的情况下进行实现是合乎逻辑的,但在现阶段,我还不准备使用它编写代码。 所以这只是为了未来。 但是最终的解决方案证明没有Android依赖项,这有可能使它可以在kotlin可以运行的任何平台上使用。


选择管理器


为了解决此问题,编译了常规的SelectionManager接口:


 interface SelectionManager { fun clearSelection() fun selectPosition(position: Int) fun isPositionSelected(position: Int): Boolean fun registerSelectionChangeListener(listener: (position: Int, isSelected: Boolean) -> Unit): Disposable fun getSelectedPositions(): ArrayList<Int> fun isAnySelected(): Boolean fun addSelectionInterceptor(interceptor: (position: Int, isSelected: Boolean, callback: () -> Unit) -> Unit): Disposable } 

默认情况下,已经有3种不同的实现:


  • MultipleSelection该对象使您可以从列表中选择任意数量的元素;
  • SingleSelection一个对象仅允许您选择一个元素;
  • NoneSelection对象完全不允许选择元素。

可能是后者,所有问题中大多数都会出现,因此我将尝试在一个示例上进行展示。


转接头


应该通过构造函数将SelectionManager对象作为依赖项添加到适配器。


 class TestAdapter(private val selectionManager: SelectionManager) : RecyclerView.Adapter<TestHolder>() { //class body } 

在示例中,我不会理会处理元素单击的逻辑,因此我们只同意持有人(无详细信息)完全负责单击侦听器的任命。


 class TestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: Any, onItemClick: () -> Unit) { //all bind logic } } 

此外,为了使此魔术起作用,适配器必须执行以下3个步骤:


1. onBindViewHolder


将回调传递给持有者的bind方法,该方法将为显示的元素调用selectionManager.selectPosition(position) 。 同样在这里,您很可能需要根据当前是否选中该项目来更改显示(通常仅是背景)-您可以为此调用selectionManager.isPositionSelected(position)


 override fun onBindViewHolder(holder: TestHolder, position: Int) { val isItemSelected = selectionManager.isPositionSelected(position) //do whatever you need depending on `isItemSelected` value val item = ... //get current item by `position` value holder.bind(item) { selectionManager.selectPosition(position) } } 

2. registerSelectionChangeListener


为了使适配器及时更新按下的元素,您必须订阅适当的操作。 并且不要忘记必须保存订阅方法返回的结果。


 private val selectionDisposable = selectionManager.registerSelectionChangeListener { position, isSelected -> notifyItemChanged(position) } 

我注意到在这种情况下, isSelected参数的isSelected并不重要,因为任何更改都会改变元素的外观。 但是,没有什么可以阻止您添加其他处理,对此值很重要。


3. selectionDisposable


在上一步中,我不仅仅是说必须保存该方法的结果-返回一个清除预订的对象,以避免泄漏。 完成工作后,必须咨询该对象。


 fun destroy() { selectionDisposable.dispose() } 

视图模型


对于魔术适配器就足够了,我们将传递给ViewModel 。 初始化SelectionManager非常简单:


 class TestViewModel: ViewModel() { val selectionManager: SelectionManager = SingleSelection() } 

在这里,与适配器类似,您可以订阅更改(例如,当未选择任何项目时,使“删除”按钮不可用),但是您也可以单击摘要按钮(例如,“下载所选内容”)以获取所有已选择内容的列表。 。


 fun onDownloadClick() { val selectedPositions: ArrayList<Int> = selectionManager.getSelectedPositions() ... } 

这是我解决方案的缺点之一:在当前阶段,对象只能存储元素的位置。 也就是说,为了准确地获取选定的对象而不是它们的位置,将需要使用连接到适配器的数据源来附加逻辑(阿拉斯语,仅到目前为止)。 但希望您能解决。


此外,仅保留将适配器与视图模型连接。 这已经处于活动级别。


 class TestActivity: AppCompatActivity() { private lateinit var adapter: TestAdapter private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //`TestViewModel` initialization adapter = TestAdapter(viewModel.selectionManager) } } 

柔韧性


对于某些人来说,这可能是可以理解的,但是我想特别指出的是,通过这种实现方式,可以很容易地控制适配器中的选择方法。 现在,适配器只能选择一个元素,但是如果TestViewModelTestViewModel更改selectionManager属性的初始化,则其余代码将“以新方式”工作,而无需进行任何更改。 也就是说,设置val selectionManager: SelectionManager = MultipleSelection() ,现在适配器允许您选择任意多个元素。


而且,如果您有整个应用程序的某种基本适配器类,则可以不必担心以相同的方式包含SelectionManager 。 确实,特别是对于根本不暗示选择元素的适配器,有一个NoneSelection的实现-不管您做什么,它都不会拥有选定的元素,也永远不会调用任何侦听器。 不,他不会抛出异常-他只是忽略所有调用,但是适配器完全不需要知道这一点。


拦截器


在某些情况下,元素的选择更改会伴随其他操作(例如,加载详细信息),而无法成功完成更改操作会导致错误状态。 特别是对于这些情况,我添加了一种拦截机制。


要添加拦截器,您需要调用addSelectionInterceptor方法(同样,您需要保存结果并在完成后访问它)。 callback: () -> Unit示例中拦截器的参数之一callback: () -> Unit直到被调用,更改才会应用。 也就是说,在没有网络的情况下,无法成功完成从服务器的详细信息的加载;因此,所使用的selectionManager的状态将不会更改。 如果这正是您要争取的行为,则需要此方法。


 val interceptionDisposable = selectionManager.addSelectionInterceptor { position: Int, isSelected: Boolean, callback: () -> Unit -> if(isSelected) { val selectedItem = ... //get current item by `position` value val isDataLoadingSuccessful: Boolean = ... //download data for `selectedItem` if(isDataLoadingSuccessful) { callback() } } } 

如有必要,您可以连接任意数量的拦截器。 在这种情况下,第一个拦截器的callback()调用开始处理第二个拦截器。 并且只有最后一个中的callback()会最终导致selectionManager状态发生变化。


前景展望


  1. 使用Disposable清除订阅很有效,但不如LiveData方便。 在线上的第一个改进是使用android.arch.lifecycle的功能来进行更方便的工作。 很有可能,这将是一个单独的项目,以免在当前项目中增加平台依赖性。
  2. 就像我说的那样,获取选定对象的列表很不方便。 我还想尝试实现可以​​以相同方式与数据容器一起使用的对象。 同时,它可能是适配器的数据源。

参考文献


您可以在链接找到源代码-GitHub
该项目也可以通过gradle实施ru.ircover.selectionmanager:core:1.0.0

Source: https://habr.com/ru/post/zh-CN483552/


All Articles