Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling button clicks in recyclerview adapter (Kotlin)?

I have an adapter in which the items each have 3 buttons, that generate a dialog that then performs an action. I have a sense that this should be removed from the adapter (I have view models available), but it works and I am wondering: Should I move logic to the fragment, to the view model, do I need to move it at all (is the code below bad practice and if so why)? Any help/input would be greatly appreciated.

Here is the adapter code:

class ViewRecipesAdapter(val context: Context, private val recipes: List<Recipe>, private val parentFragment: Fragment) :
        RecyclerView.Adapter<ViewRecipesAdapter.RecipeViewHolder>()
{

    private var listToUse: List<Recipe> = recipes
    private lateinit var recipesViewModel: RecipesViewModel
    private var isView = false


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder
    {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: ViewRecipesItemBinding =
                DataBindingUtil.inflate(layoutInflater, R.layout.view_recipes_item, parent, false)

        return RecipeViewHolder(binding, context)
    }

    override fun getItemCount() = listToUse.size

    override fun onBindViewHolder(holder: RecipeViewHolder, position: Int)
    {

        val recipe = listToUse[position]
        // to delete and edit items
        val dao = RecipesDatabase.getInstance(context).recipeDao()

        val repository = RecipeRepository(dao)
        recipesViewModel = RecipesViewModel(repository)
        //display data on list item
        holder.bind(recipe)
        Glide.with(context).load(recipe.imageOne)
                .into(holder.binding.imageViewItemImage)
        //tried to handle clicks here through the viewModel but I could not get it working from fragment
        //the function call after viewModel calls is what works and it seems to work well
        holder.binding.imageButtonItemdelete.setOnClickListener {
            recipesViewModel.setIsDelete(true)
            recipesViewModel.setPositionFromAdapter(position)
            startDeleteDialog(position)
        }
        holder.binding.imageButtonItemedit.setOnClickListener {
            recipesViewModel.setIsView(false)
            recipesViewModel.setPositionFromAdapter(position)
            isView = false
            startEditOrViewDialog(position)
        }
        holder.binding.imageButtonItemview.setOnClickListener {
            recipesViewModel.setIsView(true)
            recipesViewModel.setPositionFromAdapter(position)
            isView = true
            startEditOrViewDialog(position)
        }

    }

    fun setList(newList: List<Recipe>)
    {
        listToUse = newList
    }

    //dialog functions for the edit, delete, and view buttons on each item
    private fun startDeleteDialog(position: Int)
    {
        AlertDialog.Builder(context)
                .setTitle("Delete recipe?")
                .setPositiveButton("Yes") { _, _ ->
                    recipesViewModel.deleteRecipe(recipes[position])
                    notifyItemRemoved(position)
                }
                .setNegativeButton("No") { dialog, _ ->
                    dialog.dismiss()
                }.show()
    }

    private fun startEditOrViewDialog(position: Int)
    {
        when (isView)
        {
            true ->
            {
                AlertDialog.Builder(context).setTitle("View recipe?")
                        .setPositiveButton("Yes") { _, _ ->
                            //get relevant data from current recipe
                            val recipe = recipes[position]
                            //create a dialog that shows this data in an inflated layout
                            val viewDialog = AlertDialog.Builder(context)
                            val inflater = LayoutInflater.from(context)
                            val view = inflater.inflate(R.layout.fragment_edit_or_view, null)

                            view.editText_editrecipe_directions.setText(recipe.directions)
                            view.editText_editrecipe_ingredients.setText(recipe.ingredients)
                            view.editText_editrecipe_notes.setText(recipe.notes)
                            view.editText_editrecipe_title.setText(recipe.title)
                            view.textView_date_edit.text = recipe.date
                            view.editText_editrecipe_title.keyListener = null
                            view.editText_editrecipe_directions.keyListener = null
                            view.editText_editrecipe_ingredients.keyListener = null
                            view.editText_editrecipe_notes.keyListener = null
                            if (recipe.rating != null)
                            {
                                view.ratingBar_edit.rating = recipe.rating
                            }
                            Glide.with(context)
                                    .load(recipe.imageOne)
                                    .into(view.imageView_addphoto_edit)
                            viewDialog.setView(view).show()
                        }
                        .setNegativeButton("No") { dialog, _ ->
                            dialog.dismiss()
                        }.show()
            }
            false ->
            {
                AlertDialog.Builder(context).setTitle("Edit recipe?")
                        .setPositiveButton("Yes") { _, _ ->
                            //get relevant data from current recipe
                            val recipe = recipes[position]
                            val idString = recipe.id.toString()
                            recipesViewModel.setId(idString)
                            recipesViewModel.getRecipeById2(idString)
                            notifyDataSetChanged()

                            val controller = parentFragment.findNavController()
                            controller.navigate(
                                ViewRecipesFragmentDirections.actionNavViewrecipesToNavAddrecipe(
                                    recipe.id.toString()
                                )
                            )
                        }
                        .setNegativeButton("No") { dialog, _ ->
                            dialog.dismiss()
                        }.show()
            }
        }
    }

    override fun getItemId(position: Int): Long
    {
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int
    {
        return position
    }


    class RecipeViewHolder(val binding: ViewRecipesItemBinding, val context: Context) :
            RecyclerView.ViewHolder(binding.root)
    {

        fun bind(recipe: Recipe)
        {
            if (recipe.isLeftover == true)
            {
                binding.tvIsLeftovers.visibility = View.VISIBLE
            }
            binding.textViewItemTitle.text = recipe.title

            if (recipe.date != null)
            {
                binding.textViewItemDate.text = recipe.date
            }
            if (recipe.rating != null)
            {
                binding.ratingBar2.rating = recipe.rating
            }
            binding.root.animation = AlphaAnimation(0.0f, 1.0f).apply {
                duration = 1000
            }
        }
    }
}

This is the view model, with live data variables set up that I could not get working in the fragment that this RecyclerView is in:

class RecipesViewModel(private val repository: RecipeRepository) : ViewModel()
{
    val recipesList = repository.getAllRecipes()

    private val _isView = MutableLiveData<Boolean>()
    val isView: MutableLiveData<Boolean> = _isView

    private val _isEdit = MutableLiveData<Boolean>()
    val isEdit: MutableLiveData<Boolean> = _isEdit

    private val _positionFromAdapter = MutableLiveData<Int>()
    val positionFromAdapter: MutableLiveData<Int> = _positionFromAdapter

    private val _isDelete = MutableLiveData<Boolean>()
    val isDelete: MutableLiveData<Boolean> = _isDelete

    private val _recipesListFromSearch = MutableLiveData<List<Recipe>>()
    val recipesListFromSearch: LiveData<List<Recipe>> = _recipesListFromSearch

    private val _recipe = MutableLiveData<Recipe>()

    val recipe: LiveData<Recipe> = _recipe

    lateinit var searchString: String

    val savedId = MutableLiveData<String>()

    fun setPositionFromAdapter(position: Int)
    {
        _positionFromAdapter.value = position
    }

    fun setIsView(isView: Boolean)
    {
        _isView.value = isView
    }

    fun setIsDelete(isDelete: Boolean)
    {
        _isView.value = isDelete
    }

    fun setIsEdit(isEdit: Boolean)
    {
        _isEdit.value = isEdit
    }

    fun setId(id: String)
    {
        savedId.value = id
    }

    fun insertRecipe(recipe: Recipe)
    {
        CoroutineScope(Dispatchers.IO).launch {
            repository.insertRecipe(recipe)
        }
    }

    fun getRecipesFromQuery(query: String)
    {
        CoroutineScope(Dispatchers.IO).launch {
            val list = repository.getRecipesSearch(query)
            MainScope().launch { _recipesListFromSearch.value = list }
        }
    }

    fun saveUserRecipeToDb(
        title: String?,
        ingredients: String?,
        directions: String?,
        notes: String?,
        uriToSave: String?,
        rating: Float?,
        date: String?,
        isLeftover: Boolean,
        loadedId: String
    ): Boolean
    {
        val recipeToSave = Recipe(
            title,
            ingredients,
            directions,
            notes,
            uriToSave,
            null,
            null,
            rating,
            date,
            isLeftover
        )
        if (loadedId != "666")
        {
            recipeToSave.id = loadedId.toInt()
        }
        insertRecipe(recipeToSave)
        return false
    }

    fun getRecipeById2(id: String) = repository.getRecipeByIdLive(id)

    fun deleteRecipe(recipe: Recipe)
    {
        CoroutineScope(Dispatchers.IO).launch {
            repository.deleteRecipe(recipe)
        }
    }
}
like image 663
Matt Grier Avatar asked Sep 14 '25 03:09

Matt Grier


1 Answers

How to implement onClick in the RecyclerView. Let's assume that in Your Recycler every view is a visualization of some item and when You click on it You want to do something with that item:

  1. Create class: ClickListener:
class ClickListener(
    val clickListener: (itemId: Int) -> Unit,
)
{
    fun onClick(item: ItemClass) = clickListener(item.id)
}
  1. Now in Your RecylerViewAdapter pass as an argument this Listener:
class RecylerViewAdapter(
    private val clickListener: ClickListener
)
  1. In onBindViewHolder pass this Listenner as argument
override fun onBindViewHolder(holder: ViewHolder, position: Int)
{
    holder.bind(getItem(position)!!, clickListener)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
{
    return ViewHolder.from(
        parent
    )
}
  1. In Your ViewHolder class:
class ViewHolder private constructor(private val binding: ItemRecyclerBinding) :
        RecyclerView.ViewHolder(binding.root)
{
    companion object
    {
        fun from(parent: ViewGroup): ViewHolder
        {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = ItemRecyclerBinding.inflate(layoutInflater, parent, false)
            return ViewHolder(
                binding
            )
        }
    }


    fun bind(
        item : Item,
        clickListener: ClickListener
    )
    {
        binding.item = item
        binding.clickListener = clickListener
        binding.executePendingBindings()
    }
}
  1. In Your item layout (which has to be converted to data binding layout) add this:
<data>
    <variable
        name="item"
        type="com.example.sth.database.Item" /> // path to `Item`
    <variable
        name="clickListener"
        type="com.example.sth.ui.adapter.ClickListener" /> // Path to `ClickListener`
</data>
  1. Now You can add onClick method to Button:
android:onClick="@{() -> clickListener.onClick(item)}"
  1. When You create Adapter in fragment or Activity You have to pass clickListenner as a parameter. In this way You can handle everything from fragment and RecyclerView doesn't care about what You do in this function.
val clickListenner = ClickListenner(
    { id -> viewModel.clickItemWithid(id) }, // click. This function from ViewModel will be executed when You click on item in recycler View
)

val adapter = RecylerViewAdapter (
    clickListenner
)

This method is based on Google developers codelabs on Udacity.
Here You can check whole codelabs. It is free.
And here is just one video with implementing click listenner

like image 151
iknow Avatar answered Sep 16 '25 17:09

iknow