Armands9186
Armands9186

Reputation: 130

RecyclerView is not updated until swiped

I am working on the Movies app where I want to load movies and save as cache in local db. I was following the Udacity's "Building Andoid apps with Kotlin" course "Mars real estate" example. My app is using Live data, MVVM, databinding, Room and Retrofit. Each time new data is loaded, it is saved in db, and is the single source of 'truth'.

This is the main layout:

<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="viewModel"
            type="com.example.moviesapp.presentation.main.MainViewModel"/>
    </data>

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

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/movies_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            android:background="@android:color/black"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:listData="@{viewModel.movies}"
            app:spanCount="3"
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Fragment:

class MainFragment : DaggerFragment(), SharedPreferences.OnSharedPreferenceChangeListener {

    /**
    //     * Lazily initialize [MainViewModel].
    //     */
    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this, providerFactory).get(MainViewModel::class.java)
    }

    @Inject
    lateinit var providerFactory: ViewModelProviderFactory

    @Inject
    lateinit var sharedPreferences: SharedPreferences

    override fun onResume() {
        super.onResume()
        sharedPreferences.registerOnSharedPreferenceChangeListener(this)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {

        val binding = FragmentMainBinding.inflate(inflater)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel
        binding.moviesGrid.adapter = MainAdapter(MainAdapter.OnClickListener {
            viewModel.displayMovieDetails(it)
        })

        viewModel.navigateToSelectedMovie.observe(this, Observer {
            if (null != it) {
                this.findNavController().navigate(MainFragmentDirections.actionShowDetail(it.id))
                viewModel.displayMovieDetailsComplete()
            }
        })

        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.main, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_settings -> {
                this.findNavController().navigate(
                    MainFragmentDirections.actionShowSettings()
                )
                true
            }
            R.id.action_search -> {
                this.findNavController().navigate(
                    MainFragmentDirections.actionMainFragmentToSearchFragment()
                )
                true
            }
            R.id.action_back -> {
                viewModel.paginateBack()
                true
            }
            R.id.action_forward -> {
                viewModel.paginateForward()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
    }

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        viewModel.onPreferencesChanged()
    }
}

View Model:

class MainViewModel @Inject constructor(application: Application, private val getMoviesUseCase: GetMoviesUseCase) :
    AndroidViewModel(application) {

    private var currentPage = 1

    private val totalPages: Int by lazy {
        with (PreferenceManager.getDefaultSharedPreferences(application)) {
            getInt(TOTAL_PAGES, 0)
        }
    }

    // get movies saved in local db
    val movies = getMoviesUseCase.allMovies()

    init {
        loadMovies()
    }

    private fun loadMovies() {
        if (isInternetAvailable(getApplication()))
            viewModelScope.launch {
                getMoviesUseCase.refreshMovies(currentPage)
            }
    }

    private val _navigateToSelectedMovie = MutableLiveData<Movie>()
    val navigateToSelectedMovie: LiveData<Movie>
        get() = _navigateToSelectedMovie

    fun displayMovieDetails(movie: Movie?) {
        if (movie != null) _navigateToSelectedMovie.value = movie
    }

    fun displayMovieDetailsComplete() {
        _navigateToSelectedMovie.value = null
    }

Adapter:

class MainAdapter(private val onClickListener: OnClickListener) :
    ListAdapter<Movie, MainAdapter.MovieViewHolder>(DiffCallback) {

    class MovieViewHolder(private var binding: GridViewItemBinding):
            RecyclerView.ViewHolder(binding.root ) {
        fun bind(movie: Movie) {
            binding.movie = movie
            binding.executePendingBindings()
        }
    }

    companion object DiffCallback : DiffUtil.ItemCallback<Movie>() {
        override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
            return oldItem.id == newItem.id
        }
    }

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

    override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
        val movie = getItem(position)
        holder.itemView.setOnClickListener {
            onClickListener.onClick(movie)
        }
        holder.bind(movie)
    }

    class OnClickListener(val clickListener: (movie: Movie) -> Unit) {
        fun onClick(movie: Movie) = clickListener(movie)
    }
}

Data binding adapters:

@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<Movie>?) {
    val adapter = recyclerView.adapter as MainAdapter
    adapter.submitList(data)
}

@BindingAdapter("posterImageUrl")
fun bindPosterImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = (IMAGE_BASE_URL + POSTER_SIZE + imgUrl).toUri().buildUpon().build()
        Glide.with(imgView.context)
            .load(imgUri)
            .apply(RequestOptions().placeholder(R.drawable.loading_animation).error(R.drawable.ic_broken_image))
            .into(imgView)
    }
}

I now data new Live data appears in my View model, but the Recyclerview doesn't get updated until I swipe to refresh. The viewmodel instance is passed to the recyclerview via databinding. I now in fragments we call the observe method on Live data, but don't know how its is done in this example. In the Mars real estate example it appears to be working. I cannot find where is the difference in my code.

Thanks in advance, Armands

Upvotes: 0

Views: 378

Answers (1)

Yorick
Yorick

Reputation: 13

// in viewModel, is the type of movies LiveData<List<Movie>>?
val movies = getMoviesUseCase.allMovies()

If the type of movies returned from getMoviesUseCase.allMovies() is LivaData<List<Movie>> type, the observers(Recyclerview) can receive the changes.

If the type of movies is List<Movie>, the observers cannot receive the changes, because no one to notify the observers. You should, in this case, change it to the LiveData<List<Movie>>.

private val _movies = MutableLiveData<List<Movie>>().apply { value = emptyList() }
val movies: LiveData<List<Movie>> = _movies

and try to init the _movies like this:

viewModelScope.launch {
    _movies.value = getMoviesUseCase.allMovies()
}

Upvotes: 1

Related Questions