So I created mvvm project in kotlin using tmdb api. I was requested to load the movies using paging 3 and for each movie load a list of tags describing all of it's genres.I tried to copy the items with the same ids from the genre list and the movies list into a new list and send it to the child's adapter but it did not work. I don't see the parent's list or the child's list on screen but everything else is working fine. Can anyone help?
Thank you
ParentAdapter class:
class MoviesAdapter(
var list:MutableList<Genre>
) : PagingDataAdapter<Movies,MoviesAdapter.MoviesViewHolder>(differCallback){
companion object {
private val differCallback = object : DiffUtil.ItemCallback<Movies>() {
//Returns true if there are duplicate items
override fun areItemsTheSame(oldItem: Movies, newItem: Movies) =
oldItem.id == newItem.id
//Returns true if content is same
override fun areContentsTheSame(oldItem: Movies, newItem: Movies) =
oldItem == newItem
}
}
inner class MoviesViewHolder(val viewDataBinding: MovieItemBinding) : RecyclerView.ViewHolder(viewDataBinding.root){
lateinit var genreAdapter:GenreAdapter
var i=0
var genreList= mutableListOf<Genre>()
fun bind(movie:Movies,position:Int){
viewDataBinding.apply{
movieTitle.text = movie.title.toString()
releaseDate.text=movie.releaseDate.toString()
Glide.with(itemView).load(Constants.IMG_PATH+movie.posterPath).into(movieImg)
while(movie.genreIds.size>i) {
for(j in 0 until list.size)
if (movie.genreIds[i]==list[j].id){
genreList.add(i,list[j])
}
i++
}
genreAdapter= GenreAdapter(list=list)
recycler.apply {
adapter=genreAdapter
layoutManager=LinearLayoutManager(context,LinearLayoutManager.HORIZONTAL,false)
genreAdapter.notifyDataSetChanged()
}
}
genreList.clear()
}
}
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
val currentItem=getItem(position)
currentItem?.let { holder.bind(movie= it, position = position) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
val binding=MovieItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return MoviesViewHolder(binding)
}
}
ChildAdapter class:
class GenreAdapter(var list:MutableList<Genre>):RecyclerView.Adapter<GenreAdapter.GenreViewHolder>() {
inner class GenreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var btn = itemView.findViewById<Button>(R.id.genreType)
fun bind(genre: Genre) {
btn.text = genre.name.toString()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.genre_item, parent, false)
return GenreViewHolder(view)
}
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
// holder.bind(genre = list[position % list.size])
holder.bind(genre = list[position])
}
override fun getItemCount(): Int {
return list.size
}
}
Related
I try to show data form api in Custom recycler view by using view binding and diffUtil
I try to use DiffUtil with Binding view in RecyclerView Adapter
but when I send the data from fragment differ.submitList(the DATA)
nothing the show in the list
when I debugging I found the data come from API and go to differ
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ItemViewHolder>() {
private val differCallBack = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallBack)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_article_preview, parent, false)
)
}
inner class ItemViewHolder(private val view: View) :
RecyclerView.ViewHolder(view) {
var binding =
ItemArticlePreviewBinding.bind(view)
fun bind(article: Article) {
Glide.with(view).load(article.urlToImage).into(binding.ivArticleImage)
binding.tvTitle.text = article.title
binding.tvSource.text = article.source.name
binding.tvDescription.text = article.description
binding.tvPublishedAt.text = article.publishedAt
}
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
holder.bind(article)
setOnClickListener {
onItemClickListener?.let {
it(article)
}
}
}
}
private var onItemClickListener: ((Article) -> Unit)? = null
fun setOnClickListener(listener: (Article) -> Unit) {
onItemClickListener = listener
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Inside ItemViewHolder, the itemView you are binding data to, and the itemView you are passing to the recyclerView are two different views.
The view which you have passed to ItemViewHolder after creating from onCreateViewHolder is being passed to the RecyclerView, but you are binding data to a different view that you are inflating using the viewBinding of the ItemArticlePreviewBinding.
You can change ViewHolder's constructor to accept a binding instance instead of a view and then bind data to that binding object.
inner class ItemViewHolder(private val binding: ItemArticlePreviewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(article: Article) {
Glide.with(view).load(article.urlToImage).into(binding.ivArticleImage)
binding.tvTitle.text = article.title
binding.tvSource.text = article.source.name
binding.tvDescription.text = article.description
binding.tvPublishedAt.text = article.publishedAt
}
}
Inside onCreateViewHolder inflate a ViewBinding object and pass that to the ItemViewHolder's constructor
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemArticlePreviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
I am replacing a regular RecyclerView with a ListAdapter.
I am trying to implement a delete on swipe on the list within a fragment.
I have created the following ListAdapter class, the code for which is as follows:
class NewsAdapter(private val listener: OnItemClickListener) : ListAdapter<Article, NewsAdapter.ArticleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
val binding = ItemArticlePreviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ArticleViewHolder(binding)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val currentArticle = getItem(position)
holder.bind(currentArticle)
}
inner class ArticleViewHolder(private val binding: ItemArticlePreviewBinding): RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
root.setOnClickListener {
val position = adapterPosition
if(position != RecyclerView.NO_POSITION) {
val article = getItem(position)
listener.onItemClick(article)
}
}
}
}
fun bind(article: Article) {
binding.apply {
Glide.with(itemView.context).load(article.urlToImage).into(ivArticleImage)
tvSource.text = article.source.name
tvTitle.text = article.title
tvDescription.text = article.description
tvPublishedAt.text = article.publishedAt
}
}
}
/*
* Had to create my own function here since for some reason,
* newsAdapter.currentList is not working in any fragment
* */
fun getArticleAt(position: Int): Article {
return getItem(position)
}
interface OnItemClickListener {
fun onItemClick(article: Article)
}
class DiffCallback : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
}
Within the fragment where this list resides, I have the following:
...
lateinit var newsAdapter: NewsAdapter
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val article = newsAdapter.getArticleAt(position) // this works, but it is not what I want
viewModel.deleteArticle(article)
Snackbar.make(view, "Article successfully deleted.", Snackbar.LENGTH_LONG)
.apply {
setAction("Undo") {
viewModel.saveArticle(article)
}
show()
}
}
}).attachToRecyclerView(rvSavedNews)
...
}
...
Notice above, that I had to call:
val article = newsAdapter.getArticleAt(position)
within the fragment, since for some reason, I cannot use currentList as follows:
val article = newsAdapter.currentList[position]
The delete on swipe works just fine from what I can tell, however, I don't think I am always getting the most current list. Note: calling submitList on my adapter works just fine for other purposes. currentList seems to not be callable and I cannot figure out why.
How should I initialize viewHolder? I have this error:
What I need to do is to get selected item in recyclerView but without using onClick method. When I get this selected item I need to show Toast message. Item is data class. Is it possible to pass some value from adapter to activity? Like I need to pass actual items from Data Class.
Process: com.pors.coopreaderlast, PID: 7862
kotlin.UninitializedPropertyAccessException: lateinit property viewHolder has not been initialized
at com.pors.coopreaderlast.features.polozka.PolozkaAdapter.getViewHolder(PolozkaAdapter.kt:18)
at com.pors.coopreaderlast.features.polozka.PolozkaAdapter.getCurrentItem(PolozkaAdapter.kt:46)
at com.pors.coopreaderlast.features.polozka.PolozkaActivity.onStart(PolozkaActivity.kt:213)
this is for line where viewHolder is set in Adapter:
lateinit var viewHolder: PolozkaViewHolder
This is Adapter
class PolozkaAdapter(val chosen_item: Int, private val listener: OnItemClickListener): ListAdapter<Polozka, PolozkaAdapter.PolozkaViewHolder>(DiffCallback()){
var selectedItemPosition: Int = chosen_item
lateinit var viewHolder: PolozkaViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PolozkaViewHolder {
val binding = PolozkyItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
//return PolozkaViewHolder(binding)
viewHolder = PolozkaViewHolder(binding)
return viewHolder
}
override fun onBindViewHolder(holder: PolozkaViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
if (selectedItemPosition == position){
holder.itemView.setBackgroundColor(Color.parseColor("#DA745A"))
} else
{
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
fun getCurrentItem(): Polozka = super.getItem(viewHolder.bindingAdapterPosition)
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
inner class PolozkaViewHolder(private val binding: PolozkyItemBinding): RecyclerView.ViewHolder(binding.root){
init {
binding.root.setOnClickListener{
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION){
val item = getItem(position)
if (item != null){
listener.onItemClick(item, position)
}
}
notifyItemChanged(selectedItemPosition)
selectedItemPosition = bindingAdapterPosition
notifyItemChanged(selectedItemPosition)
}
}
fun bind(polozkaPolozka: Polozka){
binding.apply {
tvREG.text = polozkaPolozka.reg
tvVB.text = polozkaPolozka.veb.toString()
}
}
}
interface OnItemClickListener{
fun onItemClick(polozkaDoklad: Polozka, position: Int)
}
class DiffCallback: DiffUtil.ItemCallback<Polozka>(){
override fun areItemsTheSame(oldItem: Polozka, newItem: Polozka) =
oldItem.pvp06pk == newItem.pvp06pk
override fun areContentsTheSame(oldItem: Polozka, newItem: Polozka) =
oldItem == newItem
}
}
This is onCreate method but it can be in onCreate method also.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityPolozkaBinding.inflate(layoutInflater)
idPositionItem = intent.getIntExtra("positionItem",0)
val itemAdapter = PolozkaAdapter(idPositionItem, this)
binding.apply {
recyclerView.apply {
adapter = itemAdapter
layoutManager = LinearLayoutManager(this#ItemActivity)
}
itemViewModel.getall(index,idExp.toString() ).observe(this#PolozkaActivity){
itemAdapter.submitList(it)
}
}
val selectedItem = itemAdapter.getCurrentItem()
Toast.makeText(this, "Reg vybrane polozky je ${selectedItem.reg}", Toast.LENGTH_LONG).show()
I have similar question here: Similar question but here I use binding.
The reason you're getting this exception is that viewHolder has not been initalized at the moment you want to access it. And since it's a lateinit var, it expects to be initialized every time you access it. (see https://kotlinlang.org/docs/properties.html#late-initialized-properties-and-variables)
Instead of using a lateinit var for viewHolder, you can return the instance you created in the onCreateViewHolder() so no need to have an extra field in the adapter for it.
I believe you use viewHolder to find the selected item. In that case I suggest using a boolean (for instance you can name it selected) in your model object Polozka to indicate if the item is selected or not. And in you getCurrentItem() method I would write getCurrentList().find { it.selected } to find the currently selected item. In this case you need to update your list every time you select a new item and mark only that as selected.
class PolozkaAdapter(private val listener: OnItemClickListener): ListAdapter<Polozka, PolozkaAdapter.PolozkaViewHolder>(DiffCallback()){
lateinit var viewHolder: PolozkaViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PolozkaViewHolder {
val binding = PolozkyItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PolozkaViewHolder(binding)
}
override fun onBindViewHolder(holder: PolozkaViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
if (currentItem.selected){
holder.itemView.setBackgroundColor(Color.parseColor("#DA745A"))
} else
{
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
fun getCurrentItem(): Polozka = currentList.find { it.selected }
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
//Model object would look like
data class Polozka(
val selected: Boolean,
//rest of the fields
)
And in the observe you should do it like this.
itemViewModel.getall(index,idExp.toString() ).observe(this#PolozkaActivity){
// mark the item selected
val updatedList = it.mapIndexed { index, item ->
if (index == idPositionItem) {
item.copy(selected = true)
} else {
item
}
}
itemAdapter.submitList(updatedList)
}
}
val selectedItem = itemAdapter.getCurrentItem()
Toast.makeText(this, "Reg vybrane polozky je ${selectedItem.reg}", Toast.LENGTH_LONG).show()
And every time a new item gets selected you need to update your list.
I create an app to fetch some data from the REST API. I had some issues with recycler view scrolling. When I scroll down and then scroll up all the items will be messed up. I searched in stack overflow and find an answer. The answer said that we should add setHasStableIds = true to our adapter and add getItemViewType to our adapter class. I did and it worked fine but the new problem came.
the image of the items should load sequentially. For instance, if the user scrolled down to item 200 he/she should wait until all previous items load it images.
This is my adapter code
class MovieAdapter : PagingDataAdapter<Movie,MovieAdapter.MovieViewHolder>(DiffUtilCallback()) {
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
try {
holder.bind(getItem(position)!!)
}catch (e : Exception){
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.movie_list_item,parent,false)
return MovieViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return position
}
/////
class MovieViewHolder(view : View) : RecyclerView.ViewHolder(view){
private val imgMoviePoster = view.findViewById<ImageView>(R.id.imgImagePoster)
private val txtMovieTitle = view.findViewById<TextView>(R.id.txtMovieTitle)
private val txtMovieReleaseDate = view.findViewById<TextView>(R.id.txtMovieReleaseDate)
fun bind(movie : Movie){
txtMovieTitle.text = movie.title
txtMovieReleaseDate.text = movie.releaseDate
val moviePosterUrl = POSTER_BASE_URL + movie.posterPath
/*Glide.with(imgMoviePoster)
.load(moviePosterUrl)
.into(imgMoviePoster)*/
Picasso.get().load(moviePosterUrl).into(imgMoviePoster)
}
}
/////
class DiffUtilCallback : DiffUtil.ItemCallback<Movie>(){
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return newItem.id == oldItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return newItem == oldItem
}
}
}
This is my code for recycler view and view model (inside main activity)
private fun initRecyclerView(){
recyclerViewPopMovie.apply {
layoutManager = GridLayoutManager(this#MainActivity,2)
movieAdapter = MovieAdapter()
adapter = movieAdapter
}
}
private fun initViewModel(){
lifecycleScope.launchWhenCreated {
viewModel.getContent().collectLatest {
movieAdapter.submitData(it)
movieAdapter.setHasStableIds(true)
}
}
}
I was following this tutorial and tried to use multiple ViewHolders. An example use case would be a messaging app where the recyclerview has to display a sent message and a receiving message with different layouts. I got it working with the following solution:
class MyEntityDiffCallback : DiffUtil.ItemCallback<MyEntity>() {
override fun areItemsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem == newItem
}
}
class MyAdapter :
ListAdapter<MyEntity, MyAdapter.MyAbstractViewHolder>(
MyEntityDiffCallback()
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyAbstractViewHolder {
if (viewType == 1){
return MyViewHolder1.from(parent)
}else{
return MyViewHolder2.from(parent)
}
}
override fun onBindViewHolder(holder: MyAbstractViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if(item.someCounter > 5){
return 1
}else{
return 0
}
}
abstract class MyAbstractViewHolder(val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: MyEntity)
}
class MyViewHolder2 private constructor(binding: Item2Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item2Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder2 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item2Binding.inflate(layoutInflater, parent, false)
return MyViewHolder2(binding)
}
}
}
class MyViewHolder1 private constructor(binding: Item1Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item1Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder1 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item1Binding.inflate(layoutInflater, parent, false)
return MyViewHolder1(binding)
}
}
}
}
Is this the way it should be implemented? I am not sure about the cast of the binding value in the bind method. Or are there other good practices to solve this with the Listadapter and the Databinding?