17

Consider a layout data-bound to a viewModel. More specifically, the layout uses a layout variable for accessing this underlying viewModel. Whenever the binding is inflated, its viewModel and lifeCycleOwner are to be set. (Of course the viewModel contains some liveData, directly bound to some view properties).

A RecyclerView (created & set in an Activity) is passed a list of viewModels. For each viewModel, a ViewHolder is made via inflating a new copy of layout and its dataBinding.

The onBindViewHolder strategy is to

  • not touch the viewModels
  • set the ViewHolder.dataBinding.setViewModel(viewModels[position])
  1. but how to set the LifeCycleOwner?
  2. Is passing a LifeCycleOwner as an argument to the Adapter okay? Afterall, the adapter would only live as long as the RecyclerView which inturn will only be alive as long as the parent Activity.
  3. Is this a sensible way of using dataBinding in the context of RecyclerView?

fig1. layout_counter.xml :The layout of one single component that gets added to recyclerView.

layout

Codes(if needed)

  • 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>

lineage
  • 792
  • 1
  • 8
  • 20

5 Answers5

6

You can refer the below sample code:

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()
} }

Reason why we need to pass a lifecycle owner is because: ViewHolder is not a lifecycle owner and that is why it can not observe LiveData. The entire idea is to make Viewholder a lifecycle owner by implementing LifecycleOwner and after that start its lifecycle.

Android14
  • 1,045
  • 1
  • 11
  • 18
  • And yet, why is lifecycleOwner passed to SampleAdapter in your code if it is not used anywhere? – vitidev Jan 08 '21 at 03:23
4

If i understand correctly from this page it is not best to pass the lifeCycleOwner to a RecyclerView.Adapter binding item, since:

When a ViewHolder has been detached, meaning it is not currently visible on the screen, parentLifecycleOwner is still in the resumed state, so the ViewDataBindings are still active and observing the data. This means that when a LiveData instance is updated, it triggers the View to be updated, but the View is not currently being displayed! Not ideal.

Stephen Brewer seems to suggest some solutions but i did not test them

epic
  • 1,333
  • 1
  • 13
  • 27
  • This is correct, you will leak your scope to adapter if you do this. – paxcow Dec 21 '22 at 14:12
  • I've read Stephen Brewer's solution but I think the same could be achieved by just setting the lifecycleOwner to the view holder in `onViewAttachedToWindow()` and then unsetting it in `onViewDetachedFromWindow()`. What do you think? – 123db Jun 30 '23 at 23:40
1

Instead of passing LifeCycleOwner to RecyclerView. Try to use observeForever(Observer) in case of observing data from the RecyclerView. And remove observer after the work has been done using removeObserver(Observer).

Refer Documentation: https://developer.android.com/reference/androidx/lifecycle/LiveData#observeForever(androidx.lifecycle.Observer%3C?%20super%20T%3E)

Tarish
  • 468
  • 8
  • 8
0

You can pass lifecycleOwner to binding in onCreateViewHolder method.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    ...
    binding.lifecycleOwner = parent.findViewTreeLifecycleOwner()
    return ViewHolder(binding)
}
Emirhan Soylu
  • 681
  • 6
  • 12
  • Would this not ultimately have the same effect as just passing the lifecycleowner into the adapter? This does not provide an individual lifecycle to each ViewHolder, does it? – freddig May 28 '21 at 03:23
  • `findViewTreeLifecycleOwner` works for a for example a button but when it is used in the adapter, it returns null! Also, @freddig is right; what if you need the lifecycle in the viewholder? – Dr.jacky Oct 11 '22 at 09:19
0

As you can get lifecycle owner from Binding in the following;

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