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.
Related
Please tell me how to transfer the ID (position) of the view element on recyclerview to another class?
class CardAdapter : RecyclerView.Adapter<CardAdapter.CardViewHolder>(), View.OnClickListener {
private var cardList = ArrayList<Card>()
private lateinit var card: Card
class CardViewHolder(
val binding: FragmentCardBinding
) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = FragmentCardBinding.inflate(inflater, parent, false)
return CardViewHolder(binding)
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
card = cardList[position]
..
}
override fun getItemCount(): Int = cardList.size
override fun onClick(v: View) {
when (v.id) {
R.id.root_card_template -> {
val intent = Intent(v.context, ProductActivity::class.java)
// need put id into ProductActivty
intent.putExtra("item", card.id)
v.context.startActivity(intent)
}
}
}
At the moment, it only passes the ID of the last generated element. For some reason there is almost no information on the Internet on this score
Just create an interface and implement that in the calling activity. While creating an instance of your adapter inside the activity, pass that interface along and on the click event of the view in the adapter class, call the interface's method with the data that you want to pass back to the activity.
interface OnItemClickListener{
fun onClick(pos: Int)
}
class YourActivity: AppCompatActivity(), OnItemClickListener {
override fun onStart() {
super.onStart()
//Create an instance of your adapter and pass the interface.
// Here #this context is being passed as Activity is implementing the interface.
val cardListAdapter = CardListAdapter(this)
}
override fun onClick(pos: Int) {
//Add your logic
}
}
// Then in your adapter class
class YourAdapter(private val itemClickListener: OnItemClickListener) :
RecyclerView.Adapter<CardListAdapter.CardViewHolder>() {
//Your code
override fun onBindViewHolder(holder: CardListAdapter.CardViewHolder, position: Int) {
yourView.setOnClickListener {
itemClickListener.onClick(position)
}
}
}
first you need to creat an interface
interface OnItemListener {
fun onItemSelect(position: Int)
}
then in your class that calls the recyclerview adapter, pass it to your recyclerView Adapter like this
var cardAdapter = CardAdapter(object :
OnItemListener {
override fun onItemSelect(position: Int) {
// you can handle your data here
// your position that you passed comes here
}
})
var layoutManager = GridLayoutManager(context, 2)
yourRecyclerViewId.adapter = cardAdapter
yourRecyclerViewId.layoutManager = layoutManager
finally in your adapter do like this
class CardAdapter(
private val onItemListener: OnItemListener
)
: RecyclerView.Adapter<CardAdapter.CardViewHolder>(), View.OnClickListener {
private var cardList = ArrayList<Card>()
private lateinit var card: Card
and in your on click event in the adapter call it as below:
onItemListener.onItemSelect(yourPosition)
I have a RecyclerView list, I want the color of the clicked item to be changed. But whenever I am clicking an item the RecyclerView items change randomly.
I have also taken a boolean in my data class to keep track of item selection.
data class
data class OrderAvailableDaysResponseItem(
val actual_date: String,
val day_name: String,
val date_name: String,
val day_num: Int,
// For item selection
var isDateSelected: Boolean)
Inside my fragment, checking if the clicked item matches the list item and updating the isDateSelected to true, then calling notifydatasetchanged.
private var availableDaysList: OrderAvailableDaysResponse = OrderAvailableDaysResponse()
orderAvailableDaysAdapter.setDateClickListener {
for(resItem in availableDaysList){
resItem.isDateSelected = resItem.date_name == it.date_name
}
orderAvailableDaysAdapter.notifyDataSetChanged()
}
Adapter clicklistener
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
Please refer to the attachment for a better understanding of the situation
As you can see on clicking on an item the items are changing randomly. Please help me. Am I missing something?
Edit 1:
Complete adapter code
class OrderAvailableDaysAdapter(var orderAvailableDaysResponseList: OrderAvailableDaysResponse) : RecyclerView.Adapter<OrderAvailableDaysAdapter.AvailableDaysViewHolder>() {
private lateinit var binding: ItemAvailableDaysBinding
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
binding = ItemAvailableDaysBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
override fun onBindViewHolder(holder: AvailableDaysViewHolder, position: Int) {
holder.setBindings(orderAvailableDaysResponseList[position])
}
override fun getItemCount(): Int {
return orderAvailableDaysResponseList.size
}}
remove private lateinit var binding: ItemAvailableDaysBinding from adapter, don' keep it "globally", initialisation is made only once, in onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
ItemAvailableDaysBinding binding = ItemAvailableDaysBinding.inflate(
LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
same naming of this "global" object and in AvailableDaysViewHolder inner class may confuse it and setBindings may be called on lastly initialised (global kept) object rather that this passed in constructor
What's the objective
Im currently working on an app which has a RecyclerView for the Settings menu. This menu serves to load other fragments. So i needed to implement an OnItemClick function: for this, i followed this video.
What's the probelm
Following the given tutorial, Android Studio flags val adapter = adapterSettings(settingsList), saying No value passed for parameter 'listener'. I suppose that im missing something, since without the code written in the tutorial, the RecyclerView works.
So, am i missing something? Are there any ways to fix this in an easy and clean way?
Code:
activitySettings.kt
class ndActSettings : AppCompatActivity(), adapterSettings.OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ndactivity_settings)
topToolbarBack.setNavigationOnClickListener {
finish()
}
var settingsList = listOf(
dataItemsSettings(getString(R.string.look), getString(R.string.lookdescription), R.drawable.ic_colored_color_lens),
dataItemsSettings(getString(R.string.reproduction), getString(R.string.reproductiondescription), R.drawable.ic_colored_view_carousel),
dataItemsSettings(getString(R.string.images), getString(R.string.imagesdscription), R.drawable.ic_colored_image),
dataItemsSettings(getString(R.string.audio), getString(R.string.audiodescription), R.drawable.ic_colored_volume_up),
dataItemsSettings(getString(R.string.about), getString(R.string.aboutdescription), R.drawable.ic_colored_info)
)
val adapter = adapterSettings(settingsList) //ERROR HERE!
rvSettings.adapter = adapter
rvSettings.layoutManager = LinearLayoutManager(this)
}
override fun OnItemClick(position: Int) {
//TODO
}
}
adapterSettings.kt
class adapterSettings(
var settingsList: List<dataItemsSettings>,
var listener: OnItemClickListener
) : RecyclerView.Adapter<adapterSettings.SettingsViewHolder>() {
inner class SettingsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(p0: View?) {
val position : Int = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.OnItemClick(position)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_settings, parent, false)
return SettingsViewHolder(view)
}
override fun getItemCount(): Int {
return settingsList.size
}
override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) {
holder.itemView.apply {
rvTitle.text = settingsList[position].stringTitle
rvDescription.text = settingsList[position].stringDescription
rvIcon.setImageResource(settingsList[position].itemIcon)
}
}
interface OnItemClickListener {
fun OnItemClick(position: Int)
}
}
The constructor of class adapterSettings is expecting two parameters
class adapterSettings(
var settingsList: List<dataItemsSettings>,
var listener: OnItemClickListener
)
However, you are instantiating the object with one parameter only:
val adapter = adapterSettings(settingsList)
So, you have to add a second parameter.. An object that implements OnItemClickListener. Since you activity already implements that interface, you can send the activity as second parameter:
val adapter = adapterSettings(settingsList, this)
I am working with MVP for the first time and I believe I get the idea of it but I am not sure about the RecyclerView. As far as I can say, MVP is about making views as passive as possible so all business logic goes to the Presenter but how can this be achieved for the Recycler View?
Here is my code so far:
Contract
public interface PhotosContract {
// View
interface View {//: IBaseActivity {
fun showPhotos(photos: ArrayList<Photo>)
fun showText(message: String)
}
// Presenter
interface Presenter {//: IBasePresenter<View> {
fun getPhotos()
}
}
Presenter
public class PhotosPresenter(var view: PhotosContract.View) :PhotosContract.Presenter {
var dataList = ArrayList<Photo>()
override fun getPhotos() {
//call for endpoint
val call : Call<ArrayList<Photo>> = ApiClient.getClient.getPhotos()
call.enqueue(object: Callback<ArrayList<Photo>> {
override fun onFailure(call: Call<ArrayList<Photo>>, t: Throwable) {
Log.d("FAIL","FAILED")
}
override fun onResponse(
call: Call<ArrayList<Photo>>,
response: Response<ArrayList<Photo>>
)
{
Log.d("SUCCESS","SUCCESSED")
dataList.addAll(response!!.body()!!)
Log.d("SIZELIST",dataList.size.toString())
view.showPhotos(dataList)
view.showText("SUCCESS")
}
})
}
}
RecyclerViewAdapter
class PhotosAdapter(private var dataList: List<Photo>, private val context: Context) : RecyclerView.Adapter<PhotosAdapter.PhotosViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotosAdapter.PhotosViewHolder {
return PhotosViewHolder(LayoutInflater.from(this.context).inflate(R.layout.list_item_home, parent, false))
}
override fun getItemCount(): Int {
return dataList.size
}
override fun onBindViewHolder(holder: PhotosAdapter.PhotosViewHolder, position: Int) {
val dataModel = dataList[position]
holder.titleTextView.text = dataModel.title
}
class PhotosViewHolder(itemLayoutView: View) : RecyclerView.ViewHolder(itemLayoutView){
var titleTextView: TextView = itemLayoutView.tv_title
}
}
Activity
class PhotosActivity : AppCompatActivity(),PhotosContract.View {
private lateinit var presenter: PhotosPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photos)
presenter = PhotosPresenter(this)
presenter.getPhotos()
}
override fun showPhotos(photos: ArrayList<Photo>) {
photosRecyclerView.layoutManager = LinearLayoutManager(this)
photosRecyclerView.adapter = PhotosAdapter(photos,this)
photosRecyclerView.adapter?.notifyDataSetChanged()
}
override fun showText(message: String) {
Toast.makeText(this,message,Toast.LENGTH_LONG).show()
}
}
Here's how I did it when I used MVP. In your contract, define an additional view named ItemView. The way I do it, each item view holder is a MVP view. The view is dumb, so it just calls the presenter whenever something happens, and the presenter calls it back.
interface MyContract {
interface View {
fun setTitle(title: String)
}
// Add this interface here
interface ItemView {
fun bindItem(item: Item)
}
interface Presenter {
fun attach(view: View)
fun detach()
val itemCount: Int
fun onItemClicked(pos: Int)
fun onBindItemView(itemView: ItemView, pos: Int)
}
}
The adapter is also dumb. When it needs to bind an item view holder, it calls the presenter to do it.
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
// How many items do we have? We don't know, ask the presenter.
override fun getItemCount() = presenter?.itemCount ?: 0
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// How to bind the item if we only have position? We don't know, ask the presenter.
presenter?.onBindItemView(holder, position)
}
// ...
}
The ViewHolder implements the MyContract.ItemView interface. Again, it's just a view so it has no responsibility by itself. It just delegates to the presenter.
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), MyContract.ItemView {
private val txv: TextView = view.findViewById(R.id.text_view)
init {
view.setOnClickListener {
// What to do here, we only have the item's position? Call the presenter.
presenter?.onItemClicked(adapterPosition)
}
}
override fun bindItem(item: Item) {
txv.text = item.text
}
}
And finally the presenter:
class MyPresenter : MyContract.Presenter {
private var view: View? = null
private val items = mutableListOf<Item>()
override fun attach(view: View) {
this.view = view
// ...
}
override fun detach() {
view = null
}
override val itemCount: Int
get() = items.size
override fun onItemClicked(pos: Int) {
val item = items[pos]
// ...
}
override fun onBindItemView(itemView: ItemView, pos: Int) {
itemView.bindItem(items[pos])
}
// ...
}
The view for completeness, but nothing new here:
class MyView : Fragment(), MyContract.View {
private var presenter: Presenter? = null
override fun onViewCreated(view: View) {
// Attach presenter
presenter = MyPresenter()
presenter?.attach(this)
}
override fun onDestroyView() {
super.onDestroyView()
// Detach the presenter
presenter?.detach()
presenter = null
}
// ...
}
That's just one way to do it, I'm sure there are a lot of others. I just like this one because all the responsibility belongs to the presenter, there's no business logic anywhere else.
Eventually, you'll want to make changes to your list and notify the adapter. For this, add a couple methods in your View contract like notifyItemInserted(pos: Int) and call them when needed from the presenter. Or, better yet, use DiffUtil so you don't have to manage it yourself!
Once you have a good understanding of MVP though, I strongly suggest you move to MVVM as it is the official architecture promoted by Google. Most people also find it a lot more convenient than MVP.
If you have any questions don't hesitate.
I am trying to build a Slack-like chat app following a tutorial in a course I am taking online.
In the tutorial the instructor is using a ListView and the OnItemClickListener method, but I am trying to do it with recycler view, and I am stuck with the onClickListener in the adapter.
I have tried to find answers in other questions but couldn't find one that solves my problem. The closest ones were this and this
My two problems are:
1. The app's main activity has on the top of the screen a title that states what channel is currently active. I have created a singleton that holds the "current channel" and the title's text is being pulled from that singleton.
I am having a hard time changing the value of that singleton on click.
The main activity also has all the channels in a list view in a drawer.
I am trying to close the drawer when a channel is clicked but that isn't happening either.
This is my current adapter:
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
holder.itemView.setOnClickListener {
ChannelName.activeChannel = channels[position]
holder.mainLayout.drawer_layout.closeDrawer(GravityCompat.START)
}
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This is the singleton
object ChannelName {
var activeChannel : Channel? = null
}
You can rewrite the setter for activeChanell variable and call a listener that has been added before to notify your Activity:
object ChannelName {
private val listeners = ArrayList<(Channel?) -> Unit>()
fun addChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.add(listener)
}
fun removeChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.remove(listener)
}
var activeChannel: Channel? = null
set(value) {
field = value
listeners.forEach { it.invoke(value) }
}
}
And inside the Activity add a listener like this:
ChannelName.addChannelNameChangedListener {
// Do your operation
}
The alternative solution is to use Observable utils like LiveData, so you shouldn't worry about the Android life cycle any more:
object ChannelName {
val activeChannel: MutableLiveData<ChannelName> = MutableLiveData()
}
To change the value inside your adapter simply call:
ChannelName.activeChannel.value = channels[position]
And inside your activity Observe to the variable by calling:
ChannelName.activeChannel.observe(this, Observer {
// Do your operation
})
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
private var itemClickListener: OnItemClickListener? = null
fun setItemClickListener(itemClickListener: OnItemClickListener) {
this.itemClickListener = itemClickListener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
override fun onClick(v: View?) {
val position = adapterPosition
itemClickListener?.let {
if (position != RecyclerView.NO_POSITION) {
it.onItemClick(position)
}
}
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This way you can setItemClickListener to the adapter in your activity and get callback from your recyclerView.
You should not set listener in onBind() method since it will be called more than your items' count.