RecyclerView项目的视图模型

24

我的活动拥有一个Google的ViewModel,该ViewModel获取一些模型项。然后这些项被转换为RecyclerView的适配器项。同一个RecyclerView支持许多类型的适配器项。

我希望每个模型对象都有一个单独的视图模型对象,以便我可以将更复杂的逻辑封装在这个“小”视图模型中。

目前,当我有一些异步逻辑(需要在onCleared()中停止),与某些适配器项相关联时,我必须通过主视图模型以某种方式路由回调,以使所有东西都正确注销。

我正在考虑使用ViewModelProvider::get(key, modelClass),但是我的项随着时间的推移而改变,我找不到一种好的方法来“清除”旧项。

您如何处理项目中的这些情况?

编辑:为了增加关于我的担忧的更多信息,也许用不同的话说:我希望我的“小”ViewModel的生命周期与它所代表的模型项一样长。这意味着:

  • 我必须在父级接收到的相同场景下接收onCleared()回调
  • 当项目不再有效时,我必须接收onCleared()回调

编辑:请尝试将其与具有项作为片段的ViewPager进行比较。每个单独的模型项都表示为带有其ViewModel的Fragment。我希望为RecyclerView实现类似的功能。


1
我建议不要为每个项目使用ViewModels,因为ViewModels基本上与Activity / Fragment生命周期相关联,如果您尝试将其绑定到每个项目,它会破坏该目的(尽管上下文在整个RecyclerView中是相同的,但它将在项目之间共享)。除此之外,您可以通过VM公开LiveData到适配器,并通过lifecycleOwner引用在Adapter或ViewHolder中观察它。 - Jeel Vankhede
抱歉,我无法理解您的问题。您能用其他方式描述一下吗? - Mariusz
RecyclerView的项不是ViewModelStoreOwners,你可能不希望它们成为其中之一。ViewModel不是视图模型,它是跨配置更改的数据缓存。你绝对不需要为每个RecyclerView项创建一个ViewModel。 - EpicPandaForce
4个回答

46

androidx.lifecycle.ViewModel默认情况下不应在RecyclerView项上使用

为什么呢?

ViewModel是AAC (Android Architecture Component),其唯一目的是为了在Android Activity/Fragment 生命周期的配置更改中存活,以便数据可以通过ViewModel进行持久化。

这是通过将VM实例缓存在与托管activity相关联的存储中来实现的。

这就是为什么它不应直接用于RecyclerView (ViewHolder)项中,因为Item View本身将是Activity/Fragment的一部分,而且(RecyclerView/ViewHolder)没有任何特定的API来提供ViewModelStoreOwner(其中ViewModel基本上是针对给定Activity/Fragment实例派生的).

获取ViewModel的简单语法是:

ViewModelProvider(this).get(ViewModel::class.java)

& here this would be referred to Activity/Fragment context.

即使在RecyclerView项目中使用ViewModel,由于上下文可能是Activity/Fragment跨越RecyclerView的相同,因此它将为您提供相同的实例,这对我来说毫无意义。 因此,ViewModel对于RecyclerView是无用的,或者在这种情况下并没有太大贡献。


TL;DR

解决方案?

您可以直接在RecyclerView.Adapter类中传递需要从Activity/Fragment的ViewModel观察的LiveData对象。 您还需要提供LifecycleOwner,以便您的适配器开始观察给定的LiveData。

因此,您的Adapter类应如下所示:

class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
    
    init {
        liveDataToObserve.observe(lifecycleOwner) { t ->
            // Notify data set or something...
        }
    }

}

如果不是这种情况,而您想在ViewHolder类中拥有它,则可以在onCreateViewHolder方法中将LiveData对象传递给ViewHolder实例,并将lifecycleOwner一起传递。
额外加分!
如果您在RecyclerView项上使用数据绑定,则可以轻松从绑定类中获取lifecycleOwner对象。您需要做的就是在onCreateViewHolder()中设置它,如下所示:
class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
    
    override fun onCreateViewHolder: ViewHolder {
        // Some piece of code for binding
        binding.lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
        // Another piece of code and return viewholder
    }

}

class ViewHolder(private val someLiveData: LiveData<T>, binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root) {
    
    init {
        someLiveData.observe(requireNotNull(binding.lifecycleOwner)) { t->
            // set your UI by live data changes here
        }
    }
}

所以,是的,你可以使用包装类为你的ViewHolder实例提供LiveData,但如果包装类扩展了ViewModel类,我会不建议这样做。
一旦关注模拟ViewModelonCleared()方法,你可以在包装类上创建一个方法,在ViewHolder通过方法onViewRecycled()onViewDetachedFromWindow()从窗口中回收或分离时调用它。

编辑针对@Mariusz的评论:关于使用Activity/Fragment作为LifecycleOwner的担忧是正确的。但是,读到这篇文章可能会有一些误解。

只要在给定的RecyclerViewHolder项目中使用lifecycleOwner观察LiveData,就可以这样做,因为LiveData是生命周期感知组件,它在内部处理订阅生命周期,因此安全使用。即使你想要显式地删除观察,也可以使用onViewRecycled()onViewDetachedFromWindow()方法。

关于ViewHolder内部的异步操作:

  1. 如果你正在使用协程,那么可以使用来自lifecycleOwnerlifecycleScope调用你的操作,然后将数据提供回特定观察的LiveData,而不需要显式处理清除情况LifecycleScope会为你处理)

  2. 如果不使用协程,则仍然可以进行异步调用,并将数据提供回观察的LiveData,而不必担心在onViewRecycled()onViewDetachedFromWindow()回调期间清除异步操作。这里重要的是LiveData尊重给定LifecycleOwner的生命周期,而不是正在进行的异步操作。


@Mariusz编辑了帖子,请检查一下,让我知道它是否解决了您的问题。 - Jeel Vankhede
1
谢谢您的更新。我看到您建议将异步操作范围限定在生命周期(所有者)或onViewRecycled / DetachedFromWindow()中,但我希望我的异步操作即使在配置更改时也能继续进行,并且根据我对您评论的理解,这是无法实现的。 - Mariusz
那我是不是应该“取消实现”先前为静态屏幕创建的视图模型,因为谷歌无法提供一键绑定、生命周期和范围管理?顺便说一下奖金是好的,谢谢!理想情况下,我们应该提供视图模型<->相应作用域的映射表,回收器应该维护所有绑定。但是再次强调,作用域仅是UI优化的一部分。因此,UI只应该使用普通的ViewModel列表工作,在其中通过某个映射函数定义适当视图的类型。 - aeracode
1
我同意并且完全不介意。通常情况下,当有人询问关于MVVM或者Android中的视图模型时,他们使用的是由AndroidX提供的库,这可能会导致混乱。 - Jeel Vankhede
1
很棒的实现,谢谢。 - Nicolas Mage
显示剩余7条评论

5

不确定Google是否支持嵌套的ViewModel,看起来似乎不支持。

幸运的是,在需要应用MVVM模式时我们并不需要坚持使用androidx.lifecycle.ViewModel。这里有一个我决定编写的小例子:

Fragment,不做任何更改:

    @Override public void onCreate(@Nullable Bundle savedInstanceState) {
        final ItemListAdapter adapter = new ItemListAdapter();
        binding.getRoot().setAdapter(adapter);

        viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
        viewModel.getItems().observe(getViewLifecycleOwner(), adapter::submitList);
    }

ItemListAdapter 除了填充视图之外,还负责通知项目的观察者 - 它们是否应继续监听。在我的示例适配器中,使用的是扩展 RecyclerView.Adapter 的 ListAdapter,因此会接收项目列表。这是无意中的,我只是编辑了一些我已经有的代码。但为了演示目的,这也是可以接受的。

    @Override public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new Holder(parent);
    }

    @Override public void onBindViewHolder(Holder holder, int position) {
        holder.lifecycle.setCurrentState(Lifecycle.State.RESUMED);
        holder.bind(getItem(position));
    }

    @Override public void onViewRecycled(Holder holder) {
        holder.lifecycle.setCurrentState(Lifecycle.State.DESTROYED);
    }

    // Idk, but these both may be used to pause/resume, while bind/recycle for start/stop.
    @Override public void onViewAttachedToWindow(Holder holder) { }
    @Override public void onViewDetachedFromWindow(Holder holder) { }

Holder.它实现了LifecycleOwner接口,这使得自动取消订阅成为可能,它的源代码是从androidx.activity.ComponentActivity中复制过来的,所以一切应该都没问题:D

static class Holder extends RecyclerView.Holder implements LifecycleOwner {

    /*pkg*/ LifecycleRegistry lifecycle = new LifecycleRegistry(this);

    /*pkg*/ Holder(ViewGroup parent) { /* creating holder using parent's context */ }

    /*pkg*/ void bind(ItemViewModel viewModel) {
        viewModel.getItem().observe(this, binding.text1::setText);
    }

    @Override public Lifecycle getLifecycle() { return lifecycle; }
}

List view-model 是一种经典的 AndroidX ViewModel,但非常粗糙,还提供了嵌套的 View Model。请注意,在此示例中所有的 View Model 都会立即在构造函数中开始运行,直到父 View Model 被命令清除!请不要在家里尝试这样做!

public class ItemListViewModel extends ViewModel {

    private final MutableLiveData<List<ItemViewModel>> items = new MutableLiveData<>();

    public ItemListViewModel() {
        final List<String> list = Items.getInstance().getItems();

        // create "nested" view-models which start background job immediately
        final List<ItemViewModel> itemsViewModels = list.stream()
                .map(ItemViewModel::new)
                .collect(Collectors.toList());

        items.setValue(itemsViewModels);
    }

    public LiveData<List<ItemViewModel>> getItems() { return items; }

    @Override protected void onCleared() {
        // need to clean nested view-models, otherwise...
        items.getValue().stream().forEach(ItemViewModel::cancel);
    }
}

条目的视图模型,使用一些 rxJava 模拟后台工作和更新。故意不将其实现为 androidx....ViewModel,只是为了强调视图模型并不是 Google 命名的 ViewModel,而是表现为视图模型的东西。在实际程序中,它很可能会被扩展:

// Wow, we can implement ViewModel without androidx.lifecycle.ViewModel, that's cool!
public class ItemViewModel {

    private final MutableLiveData<String> item = new MutableLiveData<>();

    private final AtomicReference<Disposable> work = new AtomicReference<>();

    public ItemViewModel(String topicInitial) {
        item.setValue(topicInitial);
        // start updating ViewModel right now :D
        DisposableHelper.set(work, Observable
            .interval((long) (Math.random() * 5 + 1), TimeUnit.SECONDS)
                    .map(i -> topicInitial + " " + (int) (Math.random() * 100) )
                    .subscribe(item::postValue));
    }

    public LiveData<String> getItem() { return item; }

    public void cancel() {
        DisposableHelper.dispose(work);
    }

}

在此示例中有几点需要注意:

  • "父" ViewModel 存在于 activity 的作用域中,因此所有其数据(嵌套的 view models)也存在于该作用域中。
  • 在此示例中,所有 嵌套的视图模型都会立即开始运行。这不是我们想要的结果。我们需要相应地修改构造函数、onBind、onRecycle 和相关方法。
  • 请务必测试其是否存在内存泄漏问题。

1
谢谢你的回答!你的回答基本上解决了我的问题。我认为你唯一错过的是,当Holder的Activity被置于后台(但仍然存活)时,Holder的生命周期仍处于RESUMED状态,但可以通过额外的回调集进行改进。 - Mariusz
感谢这个留言:说实话,我没有检查过。但如果是这种情况,请看一下 onViewDetachedFromWindow - 它可能会有所帮助。 - aeracode
如果有人好奇LiveData如何依赖于生命周期,请查看此源代码:this - aeracode

4
虽然Android在Android Architecture Components中使用ViewModels,但这并不意味着它们仅仅是AAC的一部分。实际上,ViewModels是MVVM Architecture Pattern的组成部分,而该模式并非仅与Android相关。因此,ViewModel的实际目的不是为了在Android的生命周期更改时保留数据。然而,由于其在没有View引用的情况下公开其数据,使其成为Android特定情况的理想选择,在该情况下,View可以重新创建而不影响保存其状态的组件(ViewModel)。尽管如此,它还有其他好处,例如促进关注点的分离等。
需要翻译的内容:

同时,需要注意的是,您的情况无法与ViewPager-Fragments情况完全相比较,因为主要区别在于ViewHolders将在项目之间被回收。即使ViewPagerFragments被销毁并重新创建,它们仍将表示具有相同数据的相同Fragment。这就是为什么它们可以安全地绑定已有ViewModel提供的数据。但是,在ViewHolder的情况下,当它被重新创建时,它可能代表一个全新的项目,因此其应该提供的ViewModel提供的数据可能是不正确的,引用旧项目。

话虽如此,您可以轻松地使ViewHolder成为ViewModelStoreOwner

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewModelStoreOwner {

    private var viewModelStore: ViewModelStore = ViewModelStore()

    override fun getViewModelStore(): ViewModelStore = viewModelStore
}

如果ViewModel提供的数据独立于ViewHolder的项目(所有项目之间共享状态),那么仍然可以使用此方法。但是,如果不是这种情况,则需要通过调用viewModelStore.clear()来使ViewModelStore无效,并在ViewHolderonViewRecycled中创建一个新的ViewModel实例。您将失去在任何视图生命周期中保持状态的优势,但有时仍然可以作为遵循关注点分离的方法。

最后,关于使用LiveData实例来控制状态的选项,无论它是由ViewHolder共享或特定的ViewModel提供,还是通过Adapter传递,您都需要一个LifecycleOwner来观察它。更好的方法是仅使用特定ViewHolder的实际生命周期来使用当前FragmentActivity生命周期,因为它们实际上是被创建和销毁的,通过使它们实现LifecycleOwner接口。我创建了一个小型library,正是这样做的。


谢谢你的回答。1)你说得对,我的与ViewPager的比较是不正确的——Fragment需要创建自己的ViewModel。2)关于你回答中的ViewModel和ViewModel——我的项的ViewModel应该与其父ViewModelStore在同一个ViewModelStore中。我不希望在像onViewRecycled()这样的方法中调用onCleared(),因为即使视图不可见,我仍然可能想保留一些长时间运行的操作或其状态。3)关于你的库——它没有处理Activity/Fragment主机的视图ViewHolder暂停的情况。 - Mariusz

0
我按照aeracode的这个精彩答案HERE,只有一个例外。我使用了Rx BehaviourSubject代替ViewModel,对我来说完美地工作。 在协程的情况下,您可以使用StateFlow作为替代方案。
clas MyFragment: Fragment(){

   private val listSubject = BehaviorSubject.create<List<Items>>()
   ...
   private fun observeData() {
        viewModel.listLiveData.observe(viewLifecycleOwner) { list ->
            listSubject.onNext(list)
        }
   }
}

RecyclerView

class MyAdapter(
    private val listObservable: BehaviorSubject<List<Items>>
) : RecyclerView.Adapter<MyViewHolder>() {
   [...]
   override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bindToData(getItem(position))
    }

    override fun onViewRecycled(holder: MyViewHolder) {
        holder.onViewRecycled()
    }
    ...
    class MyViewHolder(val binding: LayoutBinding) :
        RecyclerView.ViewHolder(binding.root) {

        private var disposable: Disposable? = null

        fun bindToData(item: Item) = with(binding) {
            titleTv.text = item.title
            disposable = listObservable.subscribe(::setItemList) <- Here You listen
        }

        fun onViewRecycled() {
            disposable?.dispose()
        }
}

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