1

I have a simple search app. The API call gets the objects correctly and loads them into movies.list. I observe this list from the viewModel and pass it to the adapter. ( most of the times is empty) The adapter is then connected to the recyclerview and.. it doesn't show anything.

I tried tweaking with the ViewModel initialization, using viewmodelFactory- it may be that the list loads slower than the adapter publishes it. I tried making a loading animation while waiting but it does not work

ResultsFragment

package com.dragos.moviesearch.ui.main

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.dragos.moviesearch.databinding.FragmentResultsBinding
import com.dragos.moviesearch.viewmodel.MainViewModel

class ResultsFragment : Fragment() {

    private val viewModel: MainViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

    private var myadapter = ResultsAdapter()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val binding = FragmentResultsBinding.inflate(inflater)
        binding.lifecycleOwner = this

        viewModel.moviesList.observe(viewLifecycleOwner) {
            if (it != null){
                println("dra avem urmatoarele filme atasate la adaptor: " + it)
                myadapter.info = it
                myadapter.notifyDataSetChanged()
                for(i in 0..5){
                    println("dra avem urmatoarele filme in resultsfragment la onCreate: " + (viewModel.moviesList.value?.get(i)))
                }
            }
        }

        binding.resultsRecyclerview.adapter = myadapter
        binding.resultsRecyclerview.layoutManager = LinearLayoutManager(context)

        return binding.root
    }
}

MainViewModel

package com.dragos.moviesearch.viewmodel

import androidx.lifecycle.*
import com.dragos.moviesearch.data.Movie
import com.dragos.moviesearch.data.MoviesList
import com.dragos.moviesearch.repository.DataRepository
import kotlinx.coroutines.launch
enum class MovieApiStatus { LOADING, ERROR, DONE }

class MainViewModel : ViewModel() {

    private val repository: DataRepository = DataRepository
    private var _moviesList = MutableLiveData<List<Movie>>()
    val moviesList: LiveData<List<Movie>>
        get() = _moviesList

    private val _status = MutableLiveData<MovieApiStatus>()
    val status: LiveData<MovieApiStatus> = _status

    private var _query = MutableLiveData<String>()
    val query: LiveData<String> = _query

    fun getMovieInfo() {
        viewModelScope.launch {
            _status.value = MovieApiStatus.LOADING
            try {
                _moviesList.value = _query.value?.let { repository.getMovieInfo(it)}
                _status.value = MovieApiStatus.DONE
                println("dra here we have the movie list at done: " + _moviesList.value)
            } catch (e: Exception) {
                _status.value = MovieApiStatus.ERROR
                _moviesList.value = ArrayList()
            }
        }
    }

    fun updateQuery(rawInputQuery: String) {
        _query.value = rawInputQuery.replace(" ","+")
        println("dra query-ul este: " + _query.value)
    }
}

Binding Adapters

package com.dragos.moviesearch.utils

import android.os.Build.VERSION_CODES.M
import android.view.View
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.databinding.BindingAdapter
import com.dragos.moviesearch.R
import coil.load
import com.dragos.moviesearch.viewmodel.MovieApiStatus

@BindingAdapter("showPhoto")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
        imgView.load(imgUri) {
            placeholder(R.drawable.loading_animation)
            error(R.drawable.ic_broken_image)
        }
    }
}

@BindingAdapter("movieApiStatus")
fun bindStatus(statusImageView: ImageView, status: MovieApiStatus?) {
    when (status) {
        MovieApiStatus.LOADING -> {
            statusImageView.visibility = View.VISIBLE
            statusImageView.setImageResource(R.drawable.loading_animation)
        }
        MovieApiStatus.ERROR -> {
            statusImageView.visibility = View.VISIBLE
            statusImageView.setImageResource(R.drawable.ic_connection_error)
        }

        else -> {statusImageView.visibility = View.GONE}
    }}

ResultsAdapter

package com.dragos.moviesearch.ui.main

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.dragos.moviesearch.data.Movie
import com.dragos.moviesearch.data.MoviesList
import com.dragos.moviesearch.databinding.MovieItemBinding
import com.dragos.moviesearch.utils.bindImage

class ResultsAdapter: RecyclerView.Adapter<ResultsAdapter.MovieHolder>() {

    var info: List<Movie> = listOf()

    class MovieHolder(private var binding: MovieItemBinding): RecyclerView.ViewHolder(binding.root) {

        fun bind(movie: Movie){
            binding.itemTitle.text = movie.show.name
            binding.itemGenres.text = movie.show.genres.joinToString(separator = " ")
            bindImage(binding.movieImg, movie.show.image.medium)
            binding.executePendingBindings()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieHolder {
        return MovieHolder(MovieItemBinding.inflate(LayoutInflater.from(parent.context)))
    }

    override fun onBindViewHolder(holder: MovieHolder, position: Int) {
        holder.bind(info[position])
    }

    override fun getItemCount() = info.size
}

fragment_results.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">

    <data>
        <variable
            name="viewModel"
            type="com.dragos.moviesearch.viewmodel.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/secondaryTextColor">

        <EditText
            android:id="@+id/searchMovie"
            style="@style/editTextStyle"
            android:layout_marginTop="20dp"
            android:layout_marginEnd="17dp"
            android:background="@drawable/round_corners_shape_empty"
            android:hint="@string/search"
            android:textColor="@color/whiteText"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:id="@+id/magnifierButton"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginStart="5dp"
            app:layout_constraintBottom_toBottomOf="@+id/searchMovie"
            app:layout_constraintStart_toEndOf="@+id/searchMovie"
            app:layout_constraintTop_toTopOf="@+id/searchMovie"
            app:srcCompat="@drawable/baseline_search_24" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/resultsRecyclerview"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginTop="10dp"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/searchMovie" />

        <ImageView
            android:id="@+id/status_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:movieApiStatus="@{viewModel.status}" />

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

Thank you!

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
  • Nothing jumps out at me - have you tried just setting some dummy test data on the adapter to see if it works, without the ViewModel being involved? Personally I prefer having a `setData(movies)` function in the adapter, so you can pass it *what you want it to display* and it can internally call `notifyDataSetChanged` (or whatever is appropriate) - having a function like that would make it easy to debug so you can see *when* it's called and with what data. Are your layouts ok? I'm assuming `searchMovie` isn't filling the screen, is `movie_item.xml` ok, everything visible? `wrap_content` height? – cactustictacs Mar 05 '23 at 20:04

1 Answers1

0

For standard implementation:

As far as I'm aware, in your MainViewModel class, you need to call ResultsAdapter.notifyDataSetChanged() after your Data gets pulled from the API...

You can also (preferably use the DiffUtil to be more efficient.

For LiveData:

You can do it via an observer which receives changes and then updates the list for the RecyclerView. There are multiple guides and blogs on the topic including update RecyclerView with Android LiveData

A P
  • 2,131
  • 2
  • 24
  • 36
  • ViewModels shouldn't know about and be directly poking at specific parts of the UI like that, the observer pattern is about *publishing* data so that observers (e.g. the one in the `Fragment` in the OP's case) can *react* to updates. Keeping things separate makes it a lot easier to work with! That's why it's designed this way – cactustictacs Mar 05 '23 at 20:08