How to limit selectable checkboxes to 3 in recycler view - android

I have a recycler view which has checkboxes as items. I want to make sure there cannot be selected more than 3 checkboxes. If 3 checkboxes are selected other checkboxes should not be selectable, only selected checkboxes should be able to be deselected. I am not sure if I need to implement that behaviour in adapter or fragment which holds adapter. This is how my adapter looks like:
class HomeAdapter(
private val listener: OnCheckBoxClickListener
) : ListAdapter<String, HomeAdapter.SurveyViewHolder>(HOME_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SurveyViewHolder {
val bind = ItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SurveyViewHolder(bind)
}
override fun onBindViewHolder(holder: SurveyViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
inner class SurveyViewHolder(
val binding: ItemHomeBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(answer: String) = with(binding) {
checkBox.text = answer
}
init {
binding.checkBox.setOnCheckedChangeListener { _, isChecked ->
listener.onCheckBoxClickListener(
index = bindingAdapterPosition,
isChecked = isChecked,
answer = currentList[bindingAdapterPosition]
)
}
}
}
interface OnCheckBoxClickListener {
fun onCheckBoxClickListener(index: Int, isChecked: Boolean, answer: String)
}
companion object {
private val HOME_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
}
}
So far, I have tried something like this in fragment:
private fun countCheckedCheckBoxes() = with(binding) {
var num = 0
for (i in 0..recyclerView.childCount) {
(recyclerView.getChildAt(i) as? CheckBox).also {
if (it?.isChecked == true) {
num += 1
}
}
}
if (num == 3) {
// TODO do something here
}
}

Related

holder.bindingAdapterPosition returns -1

binding adapter position is returning -1 and i cannot figure out why, anyone has an idea of what i could be doing wrong ?
holder.bindingAdapterPosition in onCreateViewHolder.
Adapter and viewHolder
class CommentAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var likeClick: LikeClick? = null
private var disLikeClick: DisLikeClick? = null
inner class CommentHolder(view: View) : RecyclerView.ViewHolder(view) {
val nicknameTextView: TextView = itemView.findViewById(R.id.nickname_text_view)
val commentTextView: TextView = itemView.findViewById(R.id.comment_text_view)
val likeCount: TextView = itemView.findViewById(R.id.like_count_text_view)
val disLikeCount: TextView = itemView.findViewById(R.id.dislike_count_text_view)
val likeBtn: ImageButton = itemView.findViewById(R.id.like_image_btn)
val disLikeBtn: ImageButton = itemView.findViewById(R.id.disLike_Image_btn)
}
fun setLikeClickListener(clickListener: LikeClick) {
likeClick = clickListener
}
fun setDisLikeClickListener(clickListener: DisLikeClick) {
disLikeClick = clickListener
}
private val diffCallback = object : DiffUtil.ItemCallback<Comment>() {
override fun areItemsTheSame(oldItem: Comment, newItem: Comment): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Comment, newItem: Comment): Boolean {
return oldItem.id == newItem.id
}
}
private val diff = AsyncListDiffer(this, diffCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_comment, parent, false)
val holder: RecyclerView.ViewHolder = CommentHolder(view)
//set on click listener for like and dislike
Log.d("TESTLOG",diff.currentList.toString())
//TODO FIX
val commentItem = diff.currentList[holder.bindingAdapterPosition]
val likeBtn=view.findViewById<ImageButton>(R.id.like_image_btn)
val dislikeBtn=view.findViewById<ImageButton>(R.id.disLike_Image_btn)
likeBtn.setOnClickListener {
likeClick?.onLikeClick(commentItem.id,likeBtn)
}
dislikeBtn.setOnClickListener {
disLikeClick?.onDisLikeClick(commentItem.id,dislikeBtn)
}
return holder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val commentItem = diff.currentList[position]
val videoHolder = holder as CommentHolder
videoHolder.apply {
commentTextView.text = commentItem.comment
nicknameTextView.text = commentItem.nickname
likeCount.text = commentItem.commentlike.toString()
disLikeCount.text = commentItem.dislike.toString()
//check if user already liked/disliked this comment and change icon based on that
Utils.likes.forEach {
if(it == commentItem.id){
likeBtn.isSelected=true
}
}
Utils.disLikes.forEach {
if(it==commentItem.id){
disLikeBtn.isSelected=true
}
}
}
}
override fun getItemCount(): Int {
return diff.currentList.size
}
fun submitList(list: List<Comment>) {
diff.submitList(list)
}
interface LikeClick {
fun onLikeClick(commentId: Int, likeBtn:ImageButton)
}
interface DisLikeClick {
fun onDisLikeClick(commentId: Int, disLikeBtn:ImageButton)
}
}
Log.d in oncreateViewHolder
Shows what diff.currentlist contains.
D/TESTLOG: [Comment(comment=firstcomment
, commentlike=0, createdAt=2022-07-06T04:35:43.000Z, dislike=0, id=4, nickname=Alpac, videoId=170c3d0c-331f-4da9-974b-39703940f819)]
Where i set the adapter
i submit a list of comments to adapter, set like and dislike listeners then apply adapter to recyclerView
ChikiCommentsFetcher().fetchCommentsOfAVideo(videoId).observe(viewLifecycleOwner){
commentsRecyclerView.apply {
commentAdapter.submitList(it)
commentAdapter.setLikeClickListener(this#CommentsFragment)
commentAdapter.setDisLikeClickListener(this#CommentsFragment)
adapter=commentAdapter
}
}
Holder positions should be handled inside the onBindViewHolder method.
Also, move your clicks to the onBindViewHolder method.
holder.likeBtn.setOnClickListener {
likeClick?.onLikeClick(commentItem.id,likeBtn)
}
holder.dislikeBtn.setOnClickListener {
disLikeClick?.onDisLikeClick(commentItem.id,dislikeBtn)
}
check out this RecyclerView Tutorial

How To send request with different body to an Api with paging3 kotlin

Hi I have a function inside my viewModel which make a pager and return a flow:
fun loadLastMoviesList(genreId: Int) = Pager(config = PagingConfig(10)) {
LastMoviesPaging(repository, genreId)
}.flow.cachedIn(viewModelScope)
and a click listener in my HomeFragment which whenever user click it execute the function with different genreId:
genresAdapter.setOnItemClickListener { genre, name ->
lastMoviesTitle.text = "$name Movies"
genre.id?.let { id ->
lifecycleScope.launchWhenCreated {
viewModel.loadLastMoviesList(id).collect {
lastMoviesAdapter.submitData(it)
}
}
}
}
my problem is that when I click on an Item of recyclerView and it send new request with new body
the new itm's appear on the top of last item in recyclerView I want to update the new list with the last list in the recycler view when the genreId change how can I do it??
Adapter:
class LastMoviesAdapter #Inject constructor(): PagingDataAdapter<Data, LastMoviesAdapter.ViewHolder>(differCallBack) {
private lateinit var binding: ItemHomeMoviesLastBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LastMoviesAdapter.ViewHolder {
binding = ItemHomeMoviesLastBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder()
}
override fun onBindViewHolder(holder: LastMoviesAdapter.ViewHolder, position: Int) {
getItem(position)?.let { holder.setData(it) }
holder.setIsRecyclable(false)
}
inner class ViewHolder: RecyclerView.ViewHolder(binding.root){
fun setData(item: Data){
binding.apply {
movieNameTxt.text = item.title
movieRateTxt.text = item.imdbRating
movieCountryTxt.text = item.country
movieYearTxt.text = item.year
moviePosterImg.load(item.poster){
crossfade(true)
crossfade(800)
}
root.setOnClickListener{
onItemClickListener?.let {
it(item)
}
}
}
}
}
private var onItemClickListener: ((Data) -> Unit)? = null
fun setOnItemClickListener(listener: (Data) -> Unit){
onItemClickListener = listener
}
companion object{
val differCallBack = object : DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem == newItem
}
}
}
pagingSourceClase:
class LastMoviesPaging (private val repository: HomeRepository, private val genreId: Int): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key ?: 1
Log.d("paging3", "paging running genre: $genreId")
val responseData = repository.lastMoviesList(genreId, currentPage).run {
body()?.data?.toString()?.let { Log.d("paging3", it) }
body()?.data ?: emptyList()
}
LoadResult.Page(responseData, if (currentPage == 1) null else -1 , if (responseData.isEmpty()) null else currentPage + 1)
}catch (e: Exception){
LoadResult.Error(e)
}
}
and I also want to save the flow in viewModel instead of resiving it directly in the HomeFragment
so any one can suggest a solution for my problem? I will provide more information if needed

Why would currentList be unavailable on this ListAdapter?

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.

RecyclerView Duplicates (Insert)

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!

Using RecyclerView with DiffUtil for multiple data types

I know how to implement DiffUtil if the adapter receive single data type objects.
But in my case I have an adapter which get two data types from a fragment.
So how can I use DiffUtil with multiple data types?
Here is my adapter code:
class VisitorsAdapter(val listener: VisitorsViewHolder.OnVisitorClicked) :
RecyclerView.Adapter<BaseViewHolder<*>>() {
private var visitorsData = mutableListOf<Any>()
private var isOwner = true
fun setData(visitorsData: List<Any>) {
Log.e("TAG", "setData called")
this.visitorsData.clear()
var currentVisitors = mutableListOf<CurrentVisitorResponseItem>()
var leavedVisitors = mutableListOf<LeavedVisitorsResponseItem>()
for (visitor in visitorsData) {
when (visitor) {
is CurrentVisitorResponseItem -> currentVisitors.add(visitor)
is LeavedVisitorsResponseItem -> leavedVisitors.add(visitor)
}
}
currentVisitors = currentVisitors.sortedBy { it.roomNumber?.toInt() }.toMutableList()
leavedVisitors = leavedVisitors.sortedBy { it.room }.toMutableList()
this.visitorsData.addAll(currentVisitors)
this.visitorsData.addAll(leavedVisitors)
notifyDataSetChanged()
}
fun isOwner(chooser: Boolean) {
this.isOwner = chooser
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return VisitorsViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.visitor_item, parent, false)
)
}
override fun getItemCount() = visitorsData.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val dataPositioned = visitorsData[position]
when (holder) {
is VisitorsViewHolder -> {
holder.bind(dataPositioned, position, listener, isOwner)
}
}
}
}
Note: I have 2 types of data:
1- CurrentVisitorResponseItem
2- LeavedVisitorsResponseItem
I found the solution!
First of all, if we try to make the type of ItemCallback (Any) we will face a problem
says 'Suspicious equality check: equals() is not implemented in Object DiffUtilEquals'
The solution for this is to make an interface that override equals function
and implement that interface on the data classes that we want to pass. Like this:
interface Equatable {
override fun equals(other: Any?): Boolean
}
data class CurrentVisitorResponseItem() : Equatable
data class LeavedVisitorsResponseItem() : Equatable
Then, the adapter class will be like this:
class VisitorsAdapter(val listener: VisitorsViewHolder.OnVisitorClicked) :
RecyclerView.Adapter<BaseViewHolder<*>>() {
private var isOwner = true
private val differCallBack = object : DiffUtil.ItemCallback<Equatable>() {
override fun areItemsTheSame(oldItem: Equatable, newItem: Equatable): Boolean {
Log.e("TAG", "I'm in the areItemsTheSame")
return when {
oldItem is CurrentVisitorResponseItem && newItem is CurrentVisitorResponseItem -> {
oldItem.id == newItem.id
}
oldItem is LeavedVisitorsResponseItem && newItem is LeavedVisitorsResponseItem -> {
oldItem.id == newItem.id
}
else -> false
}
}
override fun areContentsTheSame(oldItem: Equatable, newItem: Equatable): Boolean {
return when {
oldItem is CurrentVisitorResponseItem && newItem is CurrentVisitorResponseItem -> {
oldItem == newItem
}
oldItem is LeavedVisitorsResponseItem && newItem is LeavedVisitorsResponseItem -> {
oldItem == newItem
}
else -> false
}
}
}
val differ = AsyncListDiffer(this, differCallBack)
fun isOwner(chooser: Boolean) {
this.isOwner = chooser
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return VisitorsViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.visitor_item, parent, false)
)
}
override fun getItemCount() = differ.currentList.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val dataPositioned = differ.currentList[position]
when (holder) {
is VisitorsViewHolder -> {
holder.bind(dataPositioned, position, listener, isOwner)
}
}
}
}

Categories

Resources