Search code examples
androidkotlinandroid-recyclerviewonclicklistenerandroid-adapter

How to get the image and text from a view in the recyclerView?


I researched around 20 - 30 answers and still not found a working way to:

  1. get the image and text from --> view --> viewHolder --> recyclerview --> Fragment ( one image and one text fills 1 screen). ( NullPointerException)
  2. I need them as quote: String and imgRes: Int to input them in a viewModel DAO method to save them to favourites.
  3. Ideally if the user clicks the heart button, the present image + text goes in the favourites. If it is not possible, or overly complicated, then if the user clicks anywhere in the screen the image + text should go to favourites.
  4. In the adapter I need the context to get a random quote from the database (I understand that Context in Adapter is a big no-no) - can you recommend a cleaner way to do it?
  5. Please advise on how to implement the clicklistener so it gets the image and text! Thank you!

Adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.data.DataSource
import com.example.data.DataSource.prepareManualQuote
import com.example.databinding.SlidingItemBinding

class SlidingItemAdapter(context: Context) :
    RecyclerView.Adapter<SlidingItemAdapter.SlidingItemViewHolder>() {
    //    does NOT WORK TO PASS CONTEXT HERE, NEITHER IN DATABASE, OR VIEWMODEL,
    // but it is needed to get the drawable resources programatically
    // that is why we pass a private constructor
    private var imgList: MutableList<Int> = DataSource.getThemAll(context)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlidingItemViewHolder {
        val binding = SlidingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return SlidingItemViewHolder(binding)
    }

    //     the prepareManualQuote is still a random quote - but passed through Database this time, 
   // not viewmodel
    override fun onBindViewHolder(holder: SlidingItemViewHolder, position: Int) {

        // we take a random quote and a random image
        val randomPos: Int = (imgList.indices).random()
        val imgRes: Int = imgList[randomPos]
        val manualQuote: String = prepareManualQuote(holder.itemView.context)

        holder.binding.imageViewManual.setImageResource(imgRes)
        holder.binding.quoteManual.setText(manualQuote)

    }

    class SlidingItemViewHolder(val binding: SlidingItemBinding):
        RecyclerView.ViewHolder(binding.root)

    override fun getItemCount(): Int {
        return imgList.size
    }

}

SlidingFragment


class SlidingPhotosFragment : Fragment() {

    private var _bindingManual: FragmentManualSlidingRecyclerviewBinding? = null
    private val bindingManual get() = _bindingManual!!

    // Anything used in onCreate and OnviewCreated, etc - declare it here
    lateinit var isAuto: String

    /*** Standard implementation to work with databases */
    private val viewModel: SpiritViewModel by activityViewModels {
        SpiritViewModelFactory(
            (requireActivity().application as ConnectRoomDatabaseApplication).roomDatabase.itemDao()
        )
    }
    var randomPos: Int = 5
    var quote: String = ""

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
      _bindingManual = FragmentManualSlidingRecyclerviewBinding.inflate(inflater, container,false)
      return bindingManual.root
        }
    }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    bindingManual.lifecycleOwner = viewLifecycleOwner

    // slidesrecyclerView is the binding name gave to recyclerview id in  fragment_manual_sliding_recyclerview
    bindingManual.slidesRecyclerview.adapter = context?.let { SlidingItemAdapter(it) }

    // Extracting stuff from the recyclerview
    val linearManager = LinearLayoutManager(context)

    bindingManual.heartFavouriteButton.setOnClickListener {
        val position: Int = linearManager.findFirstCompletelyVisibleItemPosition()

        val row: View? = bindingManual.slidesRecyclerview.layoutManager!!.findViewByPosition(position)
        val quoteManual: TextView = row!!.findViewById(R.id.quoteManual)
        val imageViewManual: ImageView = row.findViewById(R.id.imageViewManual)
        println("dra in manual sliders were extracted this quote and this imageViewId" + quoteManual.text + imageViewManual.getDrawable())
            }

        // Snap implementation
        val pgSnaphelper: SnapHelper = PagerSnapHelper()

            // By using a pagerSnap Helper the snap goes strictly to only 1 slide - not scrolling through many more
        bindingManual.slidesRecyclerview.layoutManager?.let {
            bindingManual.slidesRecyclerview.layoutManager!!.getChildAt(0)
                ?.let { it1 -> pgSnaphelper.calculateDistanceToFinalSnap(it, it1) }
            }
        pgSnaphelper.attachToRecyclerView(bindingManual.slidesRecyclerview)
        }
    }

XML fragment_manual_sliding_recyclerview

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        android:navigationBarColor="@color/nav_icons_default"
        tools:context=".ui.fragments.SlidingPhotosFragment">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/slides_recyclerview"
            android:clipToPadding="false"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageButton
            android:id="@+id/heartFavouriteButton"
            style="@android:style/Widget.Holo.ImageButton"
            android:layout_width="33dp"
            android:layout_height="32dp"
            android:layout_marginEnd="250dp"
            android:adjustViewBounds="true"
            android:background="@color/material_on_primary_disabled"
            app:layout_constraintBottom_toTopOf="@+id/heartRedFavouriteButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:srcCompat="@drawable/ic_heart_svgrepo_com"
            android:contentDescription="@string/save_slider_to_favourites" />

        <ImageButton
            android:id="@+id/heartRedFavouriteButton"
            style="@android:style/Widget.Holo.ImageButton"
            android:layout_width="33dp"
            android:layout_height="32dp"
            android:layout_marginEnd="250dp"
            android:layout_marginBottom="100dp"
            android:adjustViewBounds="true"
            android:background="@color/material_on_primary_disabled"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:srcCompat="@drawable/ic_heart_svgrepo_com2"/>
<!--            tools:visibility="visible" />-->

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

<!--    tools:visibility="visible"-->

Solution

  • Like Tenfour04 says in the comments, really your Adapter is just there to display a set of data and pass back UI events to a listener. It shouldn't need to be compiling stuff like text, image URLs etc - that information should already be part of the data that was passed to the Adapter for display.

    It should be enough to just pass that item to the "hey this item was clicked" listener. That could call a setFavourite(item) function in your ViewModel, which can pass it to the DAO for handling. Since there's a database involved, you probably need some kind of itemId in Item so you can tell which item in the database it corresponds to. If you're just passing that Item around, everything has all the info it needs by looking at it.


    Nobody can write you a full implementation for all this, but here's the basics to get you started:

    // In your adapter
    
    var favClickedListener: ((Item) -> Unit)? = null
    
    // In onBindViewHolder
    
    val item = items[position]
    holder.favButton.setOnClickListener {
        favClickedListener?.invoke(item)
    }
    
    // In your fragment or whatever
    
    adapter.favClickedListener = { item ->
        // or maybe this should be a toggle - click the button, flip its status
        viewModel.setFavourite(item)
    }
    
    

    and then your ViewModel handles setting that favourite status, maybe pushing a new set of items with this changed status so it can be redisplayed, etc