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)
}
}
}
Related
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
}
}
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.
In my app, I am trying to add an item to my recycling view. When I add 1 item and call notifyDataSetChanged () there is no problem, only when I want to add a 2nd item and then call the same method the two added items appear 2 times. If I add a 3rd item the 1st added item still appears 2 times the 2nd item now appears 3 times and the 3rd added item also appears 3 times. I'm working with an adapter class for my recyclerview.
I've tried working with NotifyItemInserted(position-1) but it did not work.
Here are some snippets of my code:
My adapter class
class CategoriesAdapter(var clickListener: CategorieListener): ListAdapter<CategorieModel, CategoriesAdapter.CategorieViewHolder>(CategorieDiffCallback()) {
override fun getItemCount(): Int {
return super.getItemCount()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategorieViewHolder {
return CategorieViewHolder.from(parent)
}
override fun onBindViewHolder(holder: CategorieViewHolder, position: Int) {
val reis = getItem(position)
holder.bind(reis, clickListener, position)
}
class CategorieViewHolder private constructor(val binding: LayoutCategorieItemBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(categorie: CategorieModel, clickListener: CategorieListener, position: Int?){
binding.categorie = categorie
var card = binding.catCard
itemView.setOnClickListener {
clickListener.onClick(categorie, card)
}
itemView.ib_delete.setOnClickListener {
clickListener.onDeleteClick(categorie, position!!)
}
binding.catCard.transitionName = categorie.naam
}
companion object {
fun from(parent:ViewGroup) : CategorieViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = LayoutCategorieItemBinding.inflate(layoutInflater, parent, false)
return CategorieViewHolder(binding)
}
}
}
class CategorieDiffCallback : DiffUtil.ItemCallback<CategorieModel>() {
override fun areItemsTheSame(oldItem: CategorieModel, newItem: CategorieModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CategorieModel, newItem: CategorieModel): Boolean {
return oldItem == newItem
}
}
class CategorieListener(val clicklistener: (categorie: CategorieModel, view:MaterialCardView?, position:Int?) -> Unit){
fun onClick(categorie: CategorieModel, view: MaterialCardView) = clicklistener(categorie, view, 0)
fun onDeleteClick(categorie: CategorieModel, position: Int) = clicklistener(categorie, null, position)
}
}
My Fragment behind the scenes
private fun setAdapter() {
val adap = CategoriesAdapter(CategoriesAdapter.CategorieListener{ categorie, cardview, position ->
if(cardview == null){
deleteCategorie(categorie, position!!)
}else{
findNavController().navigate(CategoriesFragmentDirections.actionCategoriesFragmentToItemTakenFragment(categorie.naam,categorie))
}
})
binding.categories.apply {
layoutManager = LinearLayoutManager(this.context)
adapterobj = adap
adapter = adap
}
}
private fun setCardButtons() {
binding.btnAdd.setOnClickListener {
val categorie = CategorieModel(binding.catName.editText?.text.toString(),null,null, userId,0)
newCategorie(categorie)
}
binding.btnCancel.setOnClickListener {
toggleKeyboard()
}
}
private fun newCategorie(cat: CategorieModel){
//Toast.makeText(requireContext(), categories.size.toString(), Toast.LENGTH_SHORT).show()
model.createCategorie(cat)
model.categorieCreate.observe(viewLifecycleOwner, Observer {response ->
response?.let {
if(response.isSuccessful){
afterCreateActions(response.body()?.result)
}else{
afterErrorActions()
}
}
})
}
private fun afterCreateActions(cat: CategorieModel?) {
categories.add(cat!!)
adapterobj.notifyDataSetChanged()
toggleKeyboard()
toggleForm()
Snackbar.make(binding.mainView, "${cat?.naam} werd succesvol toevoegd!", Snackbar.LENGTH_LONG)
.setAnimationMode(Snackbar.ANIMATION_MODE_FADE)
.show()
}
The problem was that I was Observing liveData in an clicklistener, that was the main problem! Thanks to Ramakrishna Joshi n!
Visit answer!
Adapter class, with DiffUtil implementation
class MyAdapter(private val context: Context, private val onItemClickListener:
(id:String)->Unit) :ListAdapter<SMS, MyAdapter.ViewHolder>(MyDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType:
Int):ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_view, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when(holder) {
is ViewHolder -> {
holder.bind(getItem(position),context, onItemClickListener)
}
}
}
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
private val msg = view.tvMsg
fun bind(
sms: SMS, context: Context,
onContactItemClickListener:(id:String)->Unit ) {
msg.text = sms.msgString
}
}
class MyDiffCallback : DiffUtil.ItemCallback<SMS>() {
override fun areItemsTheSame(oldItem: SMS, newItem: SMS): Boolean {
return oldItem.time == newItem.time
}
override fun areContentsTheSame(oldItem: SMS, newItem: SMS): Boolean {
return oldItem.time == newItem.time && oldItem.msgString == newItem.msgString
}
}
}
Oncreate() method in activity where I set the new list/livedata to the adapter
viewModel.data.observe(this, Observer { data->
data.let {
adapter.submitList(it)
}
}
The problem is I have to scroll to see new item that is being added to the list. how can I make sure that new item is visible without scrolling, like we see in whatsapp chat.
I have a RecyclerView and a FAB in a Fragment.
The FAB switches the layout of the RecyclerView between GRID and LINEAR.
I have a enum class in the ViewModel.
enum class LAYOUT { GRID, LINEAR }
private val _layout = MutableLiveData<LAYOUT>()
val layout: LiveData<LAYOUT>
get() = _layout
And the fragment observes the MutableLiveData of the layout value in ViewModel.
I now kind of get it working by having 2 ListAdapter, one for Grid and one for Linear.
Below is my code of the Fragment.
val gridAdapter = MovieGridAdapter()
viewModel.layout.observe(viewLifecycleOwner, Observer {
if (it == GRID) {
binding.recyclerViewMovie.adapter = gridAdapter
binding.recyclerViewMovie.layoutManager = GridLayoutManager(context, 3)
} else {
binding.recyclerViewMovie.adapter = linearAdapter
binding.recyclerViewMovie.layoutManager = LinearLayoutManager(context)
}
})
viewModel.trendingMovies.observe(viewLifecycleOwner, Observer {
it?.let {
gridAdapter.submitList(it)
linearAdapter.submitList(it)
}
})
I feel like this is not the best way to do it, as I now have 2 adapters in the fragment, and I need to submitList for both adapters.
Please let me know if there is a better way to do this, thanks!
You don't need to use two adapters one adapter can do the work
class ProductListAdapter(
private val listener: OnProductListener) : PagedListAdapter<Product, ProductViewHolder>( PRODUCT_COMPARATOR ) {
var layoutId: Int = R.layout.list_item_product
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
val product = getItem(position)
with(holder) {
bindTo(product,true)
product.let { product ->
itemView.setOnClickListener {
product.product_id?.let { it1 -> listener.onProductSelected(it1) }
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val view =
LayoutInflater.from(parent.context)
.inflate(layoutId, parent, false)
return ProductViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return position
}
fun setLayoutResourceId(layoutId: Int) {
this.layoutId = layoutId
}
companion object {
private val PRODUCT_COMPARATOR = object : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean =
TextUtils.equals(
oldItem.product_id,
newItem.product_id
)
override fun areContentsTheSame(
oldItem: Product,
newItem: Product
): Boolean =
oldItem == newItem
}
}
}
in you fragment define the adapter
private var productsAdapter: ProductListAdapter = ProductListAdapter(this)
viewModel.trendingMovies.observe(viewLifecycleOwner, Observer {
it?.let {
productsAdapter.submitList(it)
}
})
showList
private fun showList() {
rv_products.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
productsAdapter.setLayoutResourceId(R.layout.list_item_list_product)
rv_products.adapter = productsAdapter
}
showGrid
private fun showGrid() {
rv_products.layoutManager = GridLayoutManager(activity, 3)
productsAdapter.setLayoutResourceId(R.layout.list_item_grid_product)
rv_products.adapter = productsAdapter
}