I have a nav_graph, where Fragment 1 and Fragment 2 are defined.
Fragment1 has view pager with 3 tabs and each tab has recyclerview.
How can i navigate to Fragment 2 on item click of recyclerview ?
Fragment->ViewPager->Recyclerview->ClickAction.
In recyclerview's fragment, simply call requireParentFragment().findNavController().navigate(/* destination */) to navigate to Fragment2.
Besides, you should pass a lambda into your recycler view adapter and then pass it into your view holder for using it (i recommend this way).
You can read more about this in the sample code below.
class YourRecyclerViewAdapter(..., private val onItemClick: () -> Unit) : RecyclerView.Adapter<YourViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup) = YourViewHolder(onItemClick)
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
holder.bind(...)
}
}
class YourViewHolder(..., private val onItemClick: () -> Unit) : RecyclerView.ViewHolder(...) {
fun bind(...) {
// Use onItemClick here...
}
}
class RecyclerViewFragment : Fragment() {
override fun onViewCreated(view: View, saveInstanceState: Bundle?) {
val adapter = YourRecyclerViewAdapter(...) {
requireParentFragment().findNavController().navigate(...)
}
yourRecyclerView.adapter = adapter
}
}
Related
I have recyclerView and after click of card I would like to replace fragments in activity. The problem is I have no access to activity. Here is my code in adapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itemsViewModel = mList[position]
holder.tagImage.setImageResource(itemsViewModel.tagImage)
holder.tagName.text = itemsViewModel.tagName
holder.tagDescription.text = itemsViewModel.tagDescription
holder.itemView.setOnClickListener {
Log.d(InTorry.TAG, itemsViewModel.tagName)
val fragment = ProductsFragment()
val transaction = activity?.supportFragmentManager?.beginTransaction()
transaction?.replace(R.id.homeFragmentsContainer, fragment)
//transaction?.disallowAddToBackStack()
transaction?.commit()
}
}
The above replace code works in fragment but in adapter there is "activity?" error.
Kind Regards
Jack
There are multiple ways to solve this problem.
Using Context
You can use the context from holder.itemView and cast it into an Activity.
This is probably the simplest way, however this can be problematic since a Context may represent an Activity, a Service, an Application, etc. in which case it may lead to a ClassCastException when used simply.
Using Callback
You can set up a callback from your Adapter to your Activity or Fragment and then replace your Fragment.
Use JetPack Navigation
This is my personal favorite as the latest versions allow you to access NavController from Activity, Fragment or any View in the hierarchy to navigate. This is just one of many benefits of using this library.
Here is a link to Jetpack Navigation.
The Simplest and Safer way to solve this is my using Callback from your holder to activity. Below is the step by step process :
Decalare an Interface
interface OnItemClick {
fun onClick()
}
Implement that interface in you Activity and put the desired code
class MainActivity : OnItemClick {
...
override onClick() {
// Do whatever you want
val fragment = ProductsFragment()
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.homeFragmentsContainer, fragment)
transaction.commit()
}
}
Create a variable of that Interface type in you Adapter and in your onBindViewHolder method invoke that interface
class MyAdapter(val listener : OnItemClick) {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
...
listener.onClick()
}
Finally pass that interface to your Adapter from you Activity
class MainActivity : OnItemClick {
val adapter = MyAdapter(this)
...
}
NOTE : Please don't pass activity to context here and there you will get unexpected result and most probably a crash.
After watching this video https://www.youtube.com/watch?v=WqrpcWXBz14
I managed to do it this way
In Adapter
class TagsAdapter(var mList: List<TagsViewModel>) :
RecyclerView.Adapter<TagsAdapter.ViewHolder>() {
var onItemClick: ((TagsViewModel) -> Unit)? = null//click listener STEP 1!!!
override fun onBindViewHolder(holder: TagsAdapter.ViewHolder, position: Int) {
holder.itemView.setOnClickListener {
onItemClick?.invoke(itemsViewModel)//click listener STEP 2!!!
}
}
}
And in Fragment
class TagsFragment : Fragment() {
private lateinit var tagsRecyclerView: RecyclerView
private var tagsArray = ArrayList<TagsViewModel>()
private lateinit var adapter: TagsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = TagsAdapter(tagsArray)
tagsRecyclerView.adapter = adapter
adapter.onItemClick = {//click listener STEP 3!!!
val fragment = ProductsFragment()
val transaction = activity?.supportFragmentManager?.beginTransaction()
transaction?.replace(R.id.homeFragmentsContainer, fragment)
//transaction?.disallowAddToBackStack()
transaction?.commit()
}
}
}
It looks very clean and easy. I don't know is a correct way but it works
I'm really confused about how the Kotlin lambdas work, specifically with click listeners. I had something that was working to do a single ViewModel function in my MainFragment but now I want multiple buttons on my adapter that do different things. At first I thought I would just have to pass all the necessary information including IDs for the different buttons to the callback then do a switch statement in my main fragment that does the appropriate ViewModel functions. As soon as I changed my input parameters the adapter no longer accepted my OnClickListener argument.
First I'll show the old OnClickListener that was working.
ItemAdapter
class ItemAdapter(private val context: Context, private val onClickListener: OnClickListener) : ListAdapter<SongWithRatings, ItemAdapter.SongViewHolder>(SongsComparator())
{
lateinit var isVisible: BooleanArray
override fun onCurrentListChanged(
previousList: List<SongWithRatings>,
currentList: List<SongWithRatings>
) {
super.onCurrentListChanged(previousList, currentList)
if(previousList.size != currentList.size) {
isVisible = BooleanArray(itemCount)
isVisible.fill(element = false)
}
}
class SongViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
val textViewBpm: TextView = view.findViewById(R.id.bpm)
val lastPlayed: TextView = view.findViewById(R.id.lastPlayed)
//new rating code 11/27/2021
val ratingBar: SeekBar = view.findViewById(R.id.ratingBar)
val ratingLabel: TextView = view.findViewById(R.id.ratingLabel)
val button: Button = view.findViewById(R.id.submitRating)
//expandable view 3/27/2022
val titleView: LinearLayout = view.findViewById(R.id.titleView)
val expand: ConstraintLayout = view.findViewById(R.id.expand)
//fragment launch buttons
val moreButton: Button = view.findViewById(R.id.moreButton)
val rateButton: Button = view.findViewById(R.id.rateButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
// create a new view
val adapterLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return SongViewHolder(adapterLayout)
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val item = getItem(position)
//temporary code for initial rating
val initialRating = item.recentPerformanceRating()
var newRating = 0
holder.textView.text = item.song.songTitle
holder.textViewBpm.text = context.resources.getString(R.string.BPM,item.song.bpm)
holder.ratingLabel.setBackgroundColor(getStatusColor(item.recentPerformanceRating()))
//holder.imageView.setImageResource(item.imageResourceID)
holder.ratingLabel.text = context.resources.getString(R.string.Rating, initialRating )
holder.lastPlayed.text = item.lastPlayedString()
//rating bar functionality
holder.ratingBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, rating: Int, fromUser: Boolean) {
holder.ratingLabel.text = context.resources.getString(R.string.Rating, rating)
newRating = rating
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
//Performance Rating button functionality
holder.button.setOnClickListener{
onClickListener.onClick(item.song.songTitle, newRating)
}
holder.moreButton.setOnClickListener { }
class OnClickListener(val clickListener: (songTitle: String, newRating: Int) -> Unit) {
fun onClick(songTitle: String,newRating: Int) = clickListener(songTitle, newRating)
}
}
From MainFragment
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
val adapter = ItemAdapter(requireContext(),
ItemAdapter.OnClickListener { songTitle, newRating ->
songViewModel.insertRating( Rating(System.currentTimeMillis(),songTitle,songViewModel.artistName, newRating )) }
)
Like I said, this all worked fine until I tried to use the OnClickListener with a SongsWithRatings parameter.
Am I even close here or do I have to redo my whole interface between the adapter, fragment and ViewModel?
You just have to make use of Interface for the purpose of providing listeners to the Fragment .
Step 1: Create an Interface Class.
interface ItemClickListener{
//You can include the parameters into the functions which you wish to be associated with the button. Suppose I want Title on Click of more Button, then I will pass it as a parameter
fun onButtonClick(val item : SongsWithRating)
fun onMoreButtonClicked(val title : String)
}
Step 2 : Create a listener variable in your adapter and call the functions in the onClick function of the respective buttons
class ItemAdapter(private val context: Context) : ListAdapter<SongWithRatings, ItemAdapter.SongViewHolder>(SongsComparator())
{
var listener : ItemClickListener ?= null
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val item = getItem(position)
//Calling buttonClick and passing the function defined in the interface
along with the parameters
holder.button.setOnClickListener{
listener?.onButtonClick(item)
}
//Similarly for morebutton
holder.moreButton.SetOnClickListener{
listener?.onMoreButtonClicked(item.song.songTitle)
}
}
}
Step 3 : Now the final Step : Go to the fragment wherein the recyclerView is implemented
//Extent the Fragment with the Interface and override the methods
class Fragment : Fragment(), ItemClickListener{
//define Adapter and attach the listener
val adapter = ItemAdapter(requireContext()
adapter.listener = this
}
You are good to go
I am creating Recyclerview using MVVM and data binding. Now I need to perform some network operation in Recycler view adapter. So how can we create ViewModel and Live data for adapter. How can adapter observe live data.
I have create ViewModel using activity context and but not working proper
class CartAdapter(cartList: ArrayList<ProductData>, context: BaseActivity) :
RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
private val itemList = cartList
private val activity = context
private var viewModel: CartAdapterViewModel =
ViewModelProvider(context).get(CartAdapterViewModel::class.java)
init {
initObserver()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.cart_item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = itemList[position]
holder.setData(item)
}
override fun getItemCount(): Int {
return itemList.size
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binding: CartItemLayoutBinding? = DataBindingUtil.bind(itemView)
fun setData(model: ProductData) {
binding?.item = model
}
}
private fun initObserver() {
viewModel.statusResponse.observe(activity, {
activity.hideLoader()
})
viewModel.serverError.observe(activity, {
activity.hideLoader()
})
}
}
You should not create a separate ViewModel for adapter.
The Classic way:
The adapter should expose an interface whose implementation would later handle e.g. clicks on an item in the RecyclerView.
class CartAdapter(
cartList: ArrayList<ProductData>,
private val itemClickListener: ItemClickListener // This is the interface implementation
// that will be provided for an item click in this example
) : RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
interface ItemClickListener {
fun onItemClick(position: Int)
}
override fun onBindViewHolder(holder: ScanResultViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
itemClickListener.onItemClick(position)
}
}
// this function would be useful for retrieving an item from the recyclerview
fun getItemAt(position: Int): ProductData = itemList[position]
...
}
Later on when instantiating the CartAdapter in Your Activity or Fragment You would have to provide that interface implementation:
private val cartAdapter: CartAdapter = CartAdapter(
cartList,
object : CartAdapter.ItemClickListener {
override fun onItemClick(position: Int) {
// this function will handle the item click on a provided position
doSomethingWithARecyclerViewItemFrom(position)
}
}
)
private fun doSomethingWithARecyclerViewItemFrom(position: Int) {
// get the adapter item from the position
val item = cartAdapter.getItemAt(position)
// later on You can use that item to make something usefull with Your ViewModel of an activity/fragment
...
}
This way the Adapter doesn't have to have any ViewModels - the corresponding actions on RecyclerView items can be handled by the Activity view model.
In my example this action is an item click but for a more specific action, You would have to update Your question with those details.
The more compact way:
You can implement the same functionality as above using even more compact and neat way by using function types:
class CartAdapter(
cartList: ArrayList<ProductData>,
private val itemClickListener: (productData: ProductData) -> Unit // notice here
) : RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
override fun onBindViewHolder(holder: ScanResultViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
itemClickListener(position) // slight change here also
}
}
}
My suggestion is using constructor to pass viewModel instance.
Without concerns of unhandled instance scope problem anyway.
Have a happy day.
I have a recycler view(Parent) and inside it, I have another recycler View (Child).
There are 2 operations in child recycler View which I want to get on Fragment Class and do some things dynamically.
Architecture: MVVM
Yes, you can achieve your desired behavior by following these steps:
I will use Lambda to refer to Higher Order Function.
Pass the Lambda function from Activity/Fragment -> Parent Adapter
Pass the Lambda function from Parent Adapter -> Child Adapter.
For example, this code shows how to get a callback from nested Recyclerview when a user clicks Error Item from child Recyclerview.
//In Activity/Fragment
private var errorClick: () -> Unit
parentAdapter.setErrorClick(errorClick)
//In Parent Adapter
private var errorClick: () -> Unit
childAdapter.setErrorClick(errorClick)
//In Child Adapter - Now use errorClick to callback methods to Activity/Fragment
private var errorClick: () -> Unit // Use IT!
Let me know if you have any questions. Thanks.
Here is an example for higher order function.
TextAdapter.kt class
class TextAdapter(
val onClick: (String) -> Unit
): RecyclerView.Adapter<TextAdapter.ViewHolder>() {
inner class ViewHolder(val binding: ItemTextBinding) :
RecyclerView.ViewHolder(binding.root)
private val list: ArrayList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder =
ViewHolder(
ItemTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val adapter = InnerAdapter {
onClick.invoke(it)
}
binding.recyclerViewList.adapter = adapter
binding.recyclerViewList.setData(list)
}
override fun getItemCount(): Int = list.size
fun setData(newList: ArrayList<String>) {
list.clear()
list.addAll(newList)
}
}
Inner adapter
class InnerAdapter(
val onClick: (String) -> Unit
): RecyclerView.Adapter<InnerAdapter.ViewHolder>() {
inner class ViewHolder(val binding: ItemText1Binding) :
RecyclerView.ViewHolder(binding.root)
private val list: ArrayList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder =
ViewHolder(
ItemText1Binding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
binding.tvTxt.text = list[position]
holder.itemView.setOnClickListener {
//change background color
onClick.invoke(list[position])
}
}
override fun getItemCount(): Int = list.size
fun setData(newList: ArrayList<String>) {
list.clear()
list.addAll(newList)
}
}
Now you can get the value of the item click on the setAdapter
Let's see how
The below function is called from the fragment class where the adapter is set
val adapter = TextAdapter {
showToast(it)
}
I am trying to trigger the start of a fragment B when a click is detected on an item from the Recycler view present in the Fragment A.
The way I did it is:
MainActivity start the Fragement A and display a list of CardView
Once the user click on one of the CardView, an interface call a click method implemented in the main Activity to start the Fragment B
MainActivity.kt
class MainActivity : AppCompatActivity(), OnLocationSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(savedInstanceState == null) { // initial transaction should be wrapped like this
--Start Fragment A
}
}
override fun onLocationSelected(id: String) {
replaceFragment(FragmentB(), R.id.listcontainer, id)
}
companion object {
val LOCATION_ID: String = "location_id"
}
}
The interface is defined in : OnLocationSelectedListener.kt
interface OnLocationSelectedListener {
fun onLocationSelected(id: String)
}
the listener must be called from the Adapter linked to Fragment B
class FragmentBAdapter(
var listOfLocations: List<RestaurantLocation>) : RecyclerView.Adapter<LocationsListAdapter.ViewHolder>() {
private lateinit var onLocationSelectedListener: OnLocationSelectedListener
override fun getItemCount(): Int {
return listOfLocations.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocationsListAdapter.ViewHolder {
return ViewHolder(
parent.context,
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.location_item,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindViewHolder(listOfLocations[position])
}
inner class ViewHolder(private val context: Context, private val viewDataBinding: LocationItemBinding) :
RecyclerView.ViewHolder(viewDataBinding.root) {
fun bindViewHolder(location: RestaurantLocation) {
viewDataBinding.locationName.text = location.name
viewDataBinding.cardItem.setOnClickListener {
onLocationSelectedListener.onLocationSelected(location.id)
}
}
}
}
I have an exception popping up because lateinit property onLocationSelectedListener has not been initialized
I do not understand how to initialize it?
Any idea?
Thanks
You need to pass the listener to the constructor of your FragmentBAdapter, like so:
class FragmentBAdapter(val onLocationSelectedListener: OnLocationSelectedListener)
Your Activity is a OnLocationSelectedListener, so in your Fragment where you create your adapter, probably in onCreateView(), you can just do this
adapter = FragmentBAdapter(activity as? OnLocationSelectedListener)
As you have onLocationSelectedListener's implementation in your MainActivity, you can pass MainActivity's object to FragmentBAdapter and initialize onLocationSelectedListener in FragmentBAdapter's constructor by MainActivity Object