将LifeCycleOwner传递给RecyclerView.Adapter是否安全?

17
考虑一个与viewModel数据绑定的布局。更具体地说,布局使用布局变量来访问底层的viewModel。每当绑定被填充时,它的viewModel和lifeCycleOwner都将被设置。(当然,viewModel包含一些liveData,直接绑定到某些视图属性)。
一个RecyclerView(在Activity中创建并设置)传递了一个viewModel列表。对于每个viewModel,通过填充新的布局副本和其数据绑定来创建ViewHolder。
onBindViewHolder策略是:
- 不触及viewModels - 设置ViewHolder.dataBinding.setViewModel(viewModels[position])
但是如何设置LifeCycleOwner?
将LifeCycleOwner作为参数传递给Adapter可以吗?毕竟,适配器只会像RecyclerView一样存在,而后者只会在父Activity存在期间保持活动状态。
在RecyclerView的上下文中使用dataBinding是否是合理的方法?
fig1. layout_counter.xml:添加到RecyclerView的单个组件的布局。

layout

代码(如有必要)

  • MainViewModel.java
  • Adapter.java
  • MainActivity.java
  • layout_counter.xml
  • layout_main.xml

MainViewModel.java

package com.gmail.example.rough;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

public class MainViewModel extends androidx.lifecycle.ViewModel {

    private MutableLiveData<String> date, time, name;

    public MainViewModel() {
        this.date = new MutableLiveData<>(LocalDateTime.now().toString());
        this.time = new MutableLiveData<>(LocalTime.now().toString());
        this.name = new MutableLiveData<>(UUID.randomUUID().toString().substring(0,10));
        Timer t=new Timer();
        t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                date.postValue(LocalDateTime.now().toString());
                time.postValue(LocalTime.now().toString());
                }
        }, 0, 1000);
    }

    public LiveData<String> getDate() {
        return date;
    }

    public LiveData<String> getTime() {
        return time;
    }

    public LiveData<String> getName() {
        return name;
    }
}

Adapter.java

package com.gmail.example.rough;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.gmail.example.rough.databinding.LayoutCounterBinding;
import java.util.List;

public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {

    public static class ViewHolder extends RecyclerView.ViewHolder {

        private LayoutCounterBinding binding;

        public ViewHolder(@NonNull View itemView, LayoutCounterBinding binding) {
            super(itemView);
            this.binding = binding;
        }

        public LayoutCounterBinding getBinding() {
            return binding;
        }
    }

    List<MainViewModel> vms;

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {


        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.layout_counter, parent, false);
        LayoutCounterBinding binding = DataBindingUtil.inflate(inflater, R.layout.layout_counter, parent, false);
        return new ViewHolder(view, binding);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.getBinding().setVm(vms.get(position));
        //how to set the LifeCycleOwner?
//        holder.getBinding().setLifecycleOwner(???);
    }

    @Override
    public int getItemCount() {
        return vms.size();
    }

   

    public Adapter(List<MainViewModel> vms) {
        this.vms = vms;
    }
}

MainActivity.java

package com.gmail.example.rough;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;

import android.os.Bundle;

import com.gmail.example.rough.databinding.LayoutMainBinding;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_main);
        LayoutMainBinding binding = LayoutMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        ArrayList<MainViewModel> vms = new ArrayList<>();
        vms.add(new MainViewModel());
        vms.add(new MainViewModel());
        vms.add(new MainViewModel());
        binding.recyclerView.setAdapter(new Adapter(vms));
        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));


    }


}

layout_counter.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
                name="vm"
                type="com.gmail.example.rough.MainViewModel" />



    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".logger.android.MainActivity">

        <TextView
                android:id="@+id/tvName"
                android:layout_width="wrap_content"
                android:layout_height="19dp"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="@{vm.name}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        <RelativeLayout
                android:id="@+id/layoutRealtive"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="8dp"
                android:gravity="center"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tvName">

            <TextView
                    android:id="@+id/tvTs"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignStart="@id/tvTime"
                    android:layout_alignTop="@id/tvTime"
                    android:text="@{vm.date}" />

            <TextView
                    android:id="@+id/tvTime"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"

                    android:background="?selectableItemBackground"
                    android:gravity="center"
                    android:textAlignment="center"
                    android:textSize="36sp"
                    android:textStyle="bold"

                    />


            <ImageView
                    android:id="@+id/ivReset"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignTop="@id/tvTime"
                    android:layout_alignBottom="@id/tvTime"
                    android:layout_alignParentEnd="true"

                    android:src="@drawable/ic_launcher_foreground"
                     />

        </RelativeLayout>


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

layout_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="409dp"
                android:layout_height="729dp"
                android:layout_marginStart="1dp"
                android:layout_marginTop="1dp"
                android:layout_marginEnd="1dp"
                android:layout_marginBottom="1dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>


你找到解决问题的方法了吗?我也遇到了同样的问题。 - dxtr
@dxtr 很抱歉回复晚了...我还没有找到解决方案。 - lineage
5个回答

6

您可以参考以下示例代码:

class SampleAdapter(private var list: List<String>,
                private val viewmodel: SampleViewModel,
                private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<SampleAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val binding: ItemRowBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_row, parent, false)
    val holder= ViewHolder(binding, lifecycleOwner)
    binding.lifecycleOwner=holder
    holder.lifecycleCreate()
    return holder
}

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

override fun getItemCount(): Int {
    return list.size
}

override fun onViewAttachedToWindow(holder: ViewHolder) {
    super.onViewAttachedToWindow(holder)
    holder.attachToWindow()
}

inner class ViewHolder(private val binding: ItemRowBinding,
                       private var lifecycleOwner: LifecycleOwner)
    : RecyclerView.ViewHolder(binding.root),LifecycleOwner {
    private val lifecycleRegistry = LifecycleRegistry(this)
    private var paused: Boolean = false

    init {
        lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
    }
    fun lifecycleCreate() {
        lifecycleRegistry.currentState = Lifecycle.State.CREATED
    }
    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }

    fun bind() {
        lifecycleOwner = this@SampleAdapter.lifecycleOwner
        binding.viewmodel = viewmodel
        binding.executePendingBindings()
    }

    fun attachToWindow() {
        if (paused) {
            lifecycleRegistry.currentState = Lifecycle.State.RESUMED
            paused = false
        } else {
            lifecycleRegistry.currentState = Lifecycle.State.STARTED
        }
    }
}

fun setList(list: List<String>) {
    this.list = list
    notifyDataSetChanged()
} }

我们需要传递生命周期所有者的原因是: ViewHolder 不是生命周期所有者,所以无法观察 LiveData。 整个想法是通过实现 LifecycleOwner 来使 Viewholder 成为生命周期所有者,然后启动它的生命周期。

然而,如果lifecycleOwner在您的代码中没有被使用,为什么要将其传递给SampleAdapter? - vitidev

4

如果我从这篇文章中理解正确,将lifeCycleOwner传递给RecyclerView.Adapter绑定项并不是最佳选择,因为:

当ViewHolder已分离时,即它当前不可见于屏幕上时,父LifecycleOwner仍处于恢复状态,因此ViewDataBindings仍然是活动的,正在观察数据。这意味着当LiveData实例被更新时,会触发View的更新,但是View当前并未被显示!这并不理想。

Stephen Brewer似乎提出了一些解决方案,但我没有测试过。


如果这样做,你将会把作用域泄漏到适配器中。这是正确的。 - paxcow
我已经阅读了Stephen Brewer的解决方案,但是我认为可以通过在onViewAttachedToWindow()中将lifecycleOwner设置为视图持有者,然后在onViewDetachedFromWindow()中取消设置来实现相同的效果。你怎么看? - 123db

1

这确实很有帮助。谢谢!但是你如何移除观察者? - oyeraghib

0

你可以从以下绑定中获取生命周期所有者;

inner class ViewHolder(binding: List....): RecyclerView.ViewHolder(binding.root) {
     private val lifecycleOwner by lazy{
       binding.root.context as? LifecycleOwner 
     }
}

0

你可以在onCreateViewHolder方法中将lifecycleOwner传递给绑定(binding)。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    ...
    binding.lifecycleOwner = parent.findViewTreeLifecycleOwner()
    return ViewHolder(binding)
}

这样做最终不会产生与将lifecycleowner传递到适配器中的效果相同吗?这不会为每个ViewHolder提供单独的生命周期,对吧? - freddig
findViewTreeLifecycleOwner 对于例如按钮之类的控件有效,但是当在适配器中使用时,它返回 null!此外,@freddig 是正确的;如果您需要在视图保持者中使用生命周期怎么办? - Dr.jacky

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