Jada
Jada

Reputation: 385

RecylcerView adapter with viewModel and Live data

I am starting to use android jetpack arch components and have run into some confusion. Also please note data binding is not a option.

I have an activity that has a RecylcerView. I have a ViewModel that looks like the following

public class Movie extends ViewModel {


public Movie movie;
public URL logoURL;

private MutableLiveData<Drawable> logo;

public MutableLiveData<Drawable> getLogo() {

    if (logo == null) {
        logo = new MutableLiveData<>();
    }
    return logo;
}


public PikTvChannelItemVM(Movie movie, URL logo) {

    this.movie = movie;
    this.logoURL = logoURL;


}

public Bitmap getChannelLogo() {

  //Do some network call to get the bitmap logo from the url
 }

}

The above is all fine although in my I recyclerview have the following code below. Although in onbindviewholder when I try to observe for the returned image from the viewmodels live data it needs a life cycle owner reference which I don't have in my recycler view. Please help thanks

public class MovieRecyclerViewAdapter extends RecyclerView
    .Adapter<MovieRecyclerViewAdapter.MovieItemViewHolder> {

public List<MovieViewModel> vmList;


public MovieRecyclerViewAdapter(List<MovieViewModel> vmList) {

    this.vmList = vmList;
    setHasStableIds(true);
}

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

    MovieView itemView = new MovieView(parent.getContext(), null);
    itemView.setLayoutParams(new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
    ));
    return new MovieItemViewHolder(itemView);
}

@Override
public void onBindViewHolder(@NonNull MovieItemViewHolder holder, int position) {


    vmList.get(position).observe(?????, users -> {

  // As you can see I have no reference to the life cycle owner
       holder.imageView.setimage(some drawable returned by the View Model)
    });
}


@Override
public long getItemId(int position) {

    return position;
}

@Override
public int getItemViewType(int position) {

    return position;
}


@Override
public int getItemCount() {

    return vmList.size();
}

class MovieItemViewHolder extends RecyclerView.ViewHolder {

    private MovieView channelView;

    public MovieItemViewHolder(View v) {

        super(v);
        channelView = (MovieView) v;
    }

    public MovieView getChannelView() {

        return channelView;
    }
 }

}

Upvotes: 5

Views: 8181

Answers (4)

Ivan Marchuk
Ivan Marchuk

Reputation: 11

The following scheme is usually proposed as a solution:

  1. The Model calls a method from the ViewModel;
  2. The ViewModel saves the variable in List and sends the update to LiveData;
  3. The View sees the updated List in LiveData and updates the RecyclerView.

That is, when you change the data, you retransmit the List via LiveData, redefining its value. However, this updates the entire RecyclerView. Every time. (Alternatively, you can make a complex tracking mechanism in the View that checks for List changes and calls notify. But it's inconvenient.)

I wanted to use the standard notify methods from the RecyclerView.Adapter. The main problem with LiveData is that you cannot subscribe to every element of a dynamically changing list, or LiveData will be constantly created and deleted along with the elements of the list. But what if we want to load 10K items at once, how much memory will it take?

So I decided to just put the List in a wrapper class, with the ability to assign listeners to add to the List:

class ObservableArrayList<Type> : ArrayList<Type>() {

    // interface for the change listener
    abstract class OnListUpdateListener<Type>(val looper: Looper) {
        abstract fun onItemAdded(position: Int, element: Type)
        abstract fun onItemRemoved(position: Int)
        abstract fun onItemUpdated(position: Int, element: Type)
    }
    
    // listeners of changes in the List
    private val listeners = mutableListOf<OnListUpdateListener<Type>>()

    fun addOnListUpdateListener(listener: OnListUpdateListener<Type>) {
        listeners.add(listener)
    }

    fun removeOnListUpdateListener(listener: OnListUpdateListener<Type>) {
        listeners.remove(listener)
    }

    // data changing

    override fun add(element: Type): Boolean {
        val result = super.add(element)
        // notifying listeners
        if (result) listeners.forEach { it.onItemAdded(super.size, element) }
        return result
    }

    override fun add(index: Int, element: Type) {
        super.add(index, element)
        listeners.forEach { it.onItemAdded(super.size, element) }
    }

    override fun remove(element: Type): Boolean {
        val position = super.indexOf(element)
        val result = super.remove(element)
        if (result) listeners.forEach { it.onItemRemoved(position) }
        return result
    }

    override fun removeAt(index: Int): Type {
        val result = super.removeAt(index)
        listeners.forEach { it.onItemRemoved(index) }
        return result
    }

    override fun set(index: Int, element: Type): Type {
        val result = super.set(index, element)
        listeners.forEach { it.onItemUpdated(index, element) }
        return result
    }

    // can be called from a background thread
    // (for pure MVVM, it can only be called in ViewModel methods)

    fun postAdd(element: Type): Boolean {
        val result = super.add(element)
        // notifying listeners
        if (result) {
            listeners.forEach {
                Handler(it.looper).post {
                    it.onItemAdded(super.size, element)
                }
            }
        }
        return result
    }

    fun postRemoveAt(index: Int): Type {
        val result = super.removeAt(index)
        // notifying listeners
        listeners.forEach {
            Handler(it.looper).post {
                it.onItemRemoved(index)
            }
        }
        return result
    }

    fun postSet(index: Int, element: Type): Type {
        val result = super.set(index, element)
        // notifying listeners
        listeners.forEach {
            Handler(it.looper).post {
                it.onItemUpdated(index, element)
            }
        }
        return result
    }

    // ... You can override the rest of the methods ... 
}

This class is very easy to use. I have created a LiveData variable in the ViewModel:

val logsList = MutableLiveData(ObservableArrayList<LogEntry>())

And when I received information from the Model, I just saved it to the List:

logsList.value!!.postAdd(LogEntry((/*...*/))

In View, I subscribe to receive the List, and then subscribe View to changes in the List

viewModel.logsList.observe(viewLifecycleOwner) { list ->
    // assign a sheet to RecyclerView 1 time (on list init)
    recyclerView.adapter = LogsAdapter(list, resources)

    // subscribe to the List updates
    list.addOnListUpdateListener(object :
        ObservableArrayList.OnListUpdateListener<LogEntry>(Looper.getMainLooper()) {

        override fun onItemAdded(position: Int, element: LogEntry) {
            recyclerView.adapter!!.notifyItemInserted(position)
        }

        override fun onItemRemoved(position: Int) {
            recyclerView.adapter!!.notifyItemRemoved(position)
        }

        override fun onItemUpdated(position: Int, element: LogEntry) {
            recyclerView.adapter!!.notifyItemChanged(position)
        }
    })
}

Upvotes: 0

alexrnov
alexrnov

Reputation: 2534

You can do something like this:

ViewModel:

class MyViewModel : ViewModel() {
    private val items = MutableLiveData<List<String>>()

    init {
        obtainList()
    }

    fun obtainList() { // obtain list from repository
        items.value = listOf("item1", "item2", "item3", "item4")
    }

    fun getItems(): LiveData<List<String>> {
        return items
    }
}

Your Fragment (or Activity):

public class ContentFragment extends Fragment {
    private MyViewModel viewModel;
    private RecyclerView recyclerView;
    private MyAdapter adapter;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(this).get(MyViewModel.class);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 
            Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_main, container, false);
        recyclerView = root.findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 4));
       
        // add observer 
        viewModel.getItems().observe(this, items -> {
            adapter = new MyAdapter(items); // add items to adapter
            recyclerView.setAdapter(adapter);
        });

        return root;
    }

Adapter:

class MyAdapter(val list: List<String>) : RecyclerView.Adapter<MyAdapter.TextViewHolder {
    ...
    override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
        holder.textView.text = list[position] // assign titles from the list.
        ...
    }
}

Any custom objects can be used instead of String objects.

Upvotes: 0

Mustafa Kuloğlu
Mustafa Kuloğlu

Reputation: 1230

Your adapter list type should not be viewmodel. If you want to use live data without binding, you want auto ui change when your list update.

You can do it like this:

First of all, your movie class must be a model class, not be a viewmodel.

Do it adapter like normal. Just add a setList(Movie) method. This method should update the adapter list. Do not forget notify adapter after update list.

Then create livedata list like

MutableLiveData<List<Movie>> movieLiveData = 
    new MutableLiveData<>();

where you want.

Observe this list in the activity and call adapters setList(Movie)method inside observe{}.

After all this if your list updated, setList(Movie) medhod will be triggered then your ui will be update.

Upvotes: 1

Batz
Batz

Reputation: 372

I would try to send a list to the adapter to display it. I would observe the data outside of the adapter, as it is not the adapter's responsibility to observe the data but rather to display it.

Upvotes: 1

Related Questions