I am building a simple movie app in Kotlin (to practice live coding for interviews) from this tutorial: Build a Movie App using Retrofit and MVVM Architecture with Kotlin. Everything happens in the Main Activity onCreate and I wanted to move the functionality to use fragments. I'm currently getting the error "E/RecyclerView: No adapter attached; skipping layout".
I have looked over many similar posts without success. Here is my code below. All the log calls are happening except where indicated. I will update question if more info is needed.
Adapter:
package com.example.interviewpracticemvvm.adapter
import android.content.ContentValues.TAG
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.interviewpracticemvvm.databinding.MovieLayoutBinding
import com.example.interviewpracticemvvm.model.Result
class MovieAdapter : RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
private var movieList = ArrayList<Result>()
fun setMovieList(movieList: List<Result>) {
this.movieList = movieList as ArrayList<Result>
notifyDataSetChanged()
Log.d(TAG, "setMovieList called in adapter")
}
class ViewHolder(val binding: MovieLayoutBinding) : RecyclerView.ViewHolder(binding.root) {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
MovieLayoutBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
Log.d(TAG, "onCreateViewHolder called") //Not being called
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Glide.with(holder.itemView)
.load("https://image.tmdb.org/t/p/w500" movieList[position].poster_path)
.into(holder.binding.movieImage)
holder.binding.movieName.text = movieList[position].title
Log.d(TAG, "onBindViewHolder called") // Not being called
}
override fun getItemCount(): Int {
return movieList.size
}
}
Fragment:
package com.example.interviewpracticemvvm
import android.content.ContentValues.TAG
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.example.interviewpracticemvvm.adapter.MovieAdapter
import com.example.interviewpracticemvvm.databinding.FragmentMovieListBinding
import com.example.interviewpracticemvvm.viewmodel.MovieViewModel
/**
* A simple [Fragment] subclass.
* Use the [MovieListFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class MovieListFragment : Fragment() {
private lateinit var binding: FragmentMovieListBinding
private lateinit var viewModel: MovieViewModel
private lateinit var movieAdapter: MovieAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
Log.d(TAG,"onCreateView: called")
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_movie_list, container, false
)
return inflater.inflate(R.layout.fragment_movie_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG,"attaching Adapter")
prepareRecyclerView()
Log.d(TAG,"prepareRecyclerView called")
viewModel = ViewModelProvider(this)[MovieViewModel::class.java]
viewModel.getPopularMovies()
Log.d(TAG,"getPopularMovies called")
viewModel.observeMovieLiveData().observe(viewLifecycleOwner, Observer { movieList ->
movieAdapter.setMovieList(movieList)
Log.d(TAG,"movieAdapter.setMovieList(movieList) called")
})
}
private fun prepareRecyclerView() {
movieAdapter = MovieAdapter()
binding.rvMovies.apply {
layoutManager = GridLayoutManager(activity, 2) //Tried "context"; appCompatActivity used in MainActivity wouldn't work
adapter = movieAdapter
}
Log.d(TAG,"adapter attached")
}
}
Viewmodel:
package com.example.interviewpracticemvvm.viewmodel
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.interviewpracticemvvm.RetrofitInstance
import com.example.interviewpracticemvvm.model.Movies
import com.example.interviewpracticemvvm.model.Result
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MovieViewModel : ViewModel() {
private var movieLiveData = MutableLiveData<List<Result>>()
fun getPopularMovies() {
RetrofitInstance.api.getPopularMovies("69d66957eebff9666ea46bd464773cf0")
.enqueue(object : Callback<Movies> {
override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
if (response.body() != null) {
movieLiveData.value = response.body()!!.results
}
}
override fun onFailure(call: Call<Movies>, t: Throwable) {
Log.d("TAG", t.message.toString())
}
})
}
fun observeMovieLiveData() : LiveData<List<Result>> {
return movieLiveData
}
}
Fragment XML:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MovieListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@ id/rv_movies"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/movie_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I had a series of errors with using databinding and safeargs. I haven't found a post that points to a glaring problem. The error seems to be unspecific enough that it can be caused by many things in the project.
CodePudding user response:
Just pass requireContext()
instead of activity
and try like below I hope your problem solve.
private fun prepareRecyclerView() {
movieAdapter = MovieAdapter()
binding.rvMovies.apply {
layoutManager = GridLayoutManager(requireContext(), 2) //Tried "context"; appCompatActivity used in MainActivity wouldn't work
adapter = movieAdapter
}
Log.d(TAG,"adapter attached")
}
CodePudding user response:
You're inflating two copies of your layout - one is getting used by your binding
object, the other is what's actually being displayed:
// calling an 'inflate' function twice
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_movie_list, container, false
)
// this is the actual inflated View that gets displayed (because it's the one you return)
return inflater.inflate(R.layout.fragment_movie_list, container, false)
So there are two different RecyclerView
s - the one in the layout all your binding
references point to, and the one that's actually being displayed on the screen. And when you call prepareRecyclerView
, the one that gets set up isn't the one on the screen:
binding.rvMovies.apply {
layoutManager = GridLayoutManager(activity, 2)
adapter = movieAdapter
}
That means when the displayed one gets its layout pass (i.e. it's being told it's getting drawn) it doesn't have an adapter or layout manager, because you haven't set them on that RecyclerView
. And that's why you get your error.
You need to inflate one copy of your layout, pass that back for display, and also bind to the same instance so you have access to its views. There are two ways you can do it:
// use the Data Binding stuff to inflate the view and get your binding object
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_movie_list, container, false
)
// pass the inflated layout (its root view) back for display
return binding.root
or
// inflate the view hierarchy yourself
val view = inflater.inflate(R.layout.fragment_movie_list, container, false)
// bind to it (like the inflate call, but without inflating anything -
// just does all the lookups using the view hierarchy you've passed in)
binding = DataBindingUtil.bind(view)
// return the view for display
return view
The first approach (inflating through the binding class, passing back root
) is neater and is the recommended way of doing things, and works with Activities too. bind
can be useful when you already have an existing view hierarchy, and you just want to wire up all the views to the references in a binding object.
Basically, if you ever do layout inflation more than once that's a big warning sign - usually you wouldn't do that, and having two different and completely separate sets of views around can lead to bugs. Usually this kind of thing, where the one being configured and the one being displayed are different, so things on the screen "don't work"