I have a recyclerView that shows a list of cart items, Every item is clickable and opens details fragment for that item, I'm Updating the item layout to have a delete button inside, the delete button suppose to call a method inside the fragment viewModel. I believe that making the viewModel a constructor in the adapter is not the best practice since separation of concern is important as I develop my skills.
I'm doing so with dataBinding and I've searched so much and couldn't find an answer.
CartListItem.xml
<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">
<data>
<variable
name="makeupItem"
type="com.melfouly.makeupshop.model.MakeupItem" />
<variable
name="viewModel"
type="com.melfouly.makeupshop.makeupcart.CartViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cart_card_item"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="4dp"
android:backgroundTint="#color/primary"
app:cardCornerRadius="8dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="6">
<ImageView
android:id="#+id/item_image"
loadImage="#{makeupItem}"
android:layout_gravity="center"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="fitCenter"
tools:src="#tools:sample/avatars" />
<TextView
android:id="#+id/item_name"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:fontFamily="#font/aclonica"
android:gravity="center"
android:text="#{makeupItem.name}"
android:textColor="#color/black"
android:textStyle="bold"
tools:text="Item name" />
<TextView
android:id="#+id/item_price"
loadPrice="#{makeupItem}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:fontFamily="#font/aclonica"
android:gravity="center"
android:textColor="#color/black"
tools:text="5$" />
<Button
android:id="#+id/delete_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="#{() -> viewModel.deleteItemFromCart(makeupItem)}"
app:icon="#drawable/ic_baseline_delete_outline_24"
app:iconGravity="end" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
CartAdapter
class CartAdapter(private val clickListener: (MakeupItem) -> Unit) :
ListAdapter<MakeupItem, CartAdapter.CartViewHolder>(DiffCallback()) {
class CartViewHolder(private val binding: CartListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(makeupItem: MakeupItem) {
binding.makeupItem = makeupItem
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CartListItemBinding.inflate(layoutInflater, parent, false)
return CartViewHolder(binding)
}
override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
val makeupItem = getItem(position)
holder.itemView.setOnClickListener {
clickListener(makeupItem)
}
holder.bind(makeupItem)
}
class DiffCallback : DiffUtil.ItemCallback<MakeupItem>() {
override fun areItemsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
return oldItem == newItem
}
}
CartViewModel delete function
fun deleteItemFromCart(makeupItem: MakeupItem) {
viewModelScope.launch {
Log.d(TAG, "DeleteItemFromCart method in viewModel called")
repository.deleteItemFromCart(makeupItem)
}
}
I've came to an answer.
DataBinding of a viewModel with a recyclerView item won't work since we're not declaring the adapter in this viewModel, so you should make a callBack inside your adapter and receive it in your fragment then call your viewModel function.
Here is CartAdapter after modifying a callBack for your delete button onClick and use the same way for your cardItem
class CartAdapter(
private val cartItemClickListener: CartItemClickListener,
private val deleteItemClickListener: DeleteItemClickListener
) :
ListAdapter<MakeupItem, CartAdapter.CartViewHolder>(DiffCallback()) {
class CartViewHolder(private val binding: CartListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
makeupItem: MakeupItem,
cartItemClickListener: CartItemClickListener,
deleteItemClickListener: DeleteItemClickListener
) {
binding.makeupItem = makeupItem
binding.cartItemClickListener = cartItemClickListener
binding.deleteItemClickListener = deleteItemClickListener
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CartListItemBinding.inflate(layoutInflater, parent, false)
return CartViewHolder(binding)
}
override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
val makeupItem = getItem(position)
holder.bind(makeupItem, cartItemClickListener, deleteItemClickListener)
}
class DiffCallback : DiffUtil.ItemCallback<MakeupItem>() {
override fun areItemsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
return oldItem == newItem
}
}
class CartItemClickListener(val clickListener: (makeupItem: MakeupItem) -> Unit) {
fun onClick(makeupItem: MakeupItem) = clickListener(makeupItem)
}
class DeleteItemClickListener(val deleteItemClickListener: (makeupItem: MakeupItem) -> Unit) {
fun onClick(makeupItem: MakeupItem) = deleteItemClickListener(makeupItem)
}
}
As for CartListItem add two dataBinding one itemClickListener and the other for deleteButtonClickListener and use android:onClick and a lambda expression inside it
<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">
<data>
<variable
name="makeupItem"
type="com.melfouly.makeupshop.model.MakeupItem" />
<variable
name="cartItemClickListener"
type="com.melfouly.makeupshop.makeupcart.CartAdapter.CartItemClickListener" />
<variable
name="deleteItemClickListener"
type="com.melfouly.makeupshop.makeupcart.CartAdapter.DeleteItemClickListener" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cart_card_item"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="4dp"
android:backgroundTint="#color/primary"
android:onClick="#{() -> cartItemClickListener.onClick(makeupItem)}"
app:cardCornerRadius="8dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="6">
<ImageView
android:id="#+id/item_image"
loadImage="#{makeupItem}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:scaleType="fitCenter"
tools:src="#tools:sample/avatars" />
<TextView
android:id="#+id/item_name"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:fontFamily="#font/aclonica"
android:gravity="center"
android:text="#{makeupItem.name}"
android:textColor="#color/black"
android:textStyle="bold"
tools:text="Item name" />
<TextView
android:id="#+id/item_price"
loadPrice="#{makeupItem}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:fontFamily="#font/aclonica"
android:gravity="center"
android:textColor="#color/black"
tools:text="5$" />
<Button
android:id="#+id/delete_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="#{() -> deleteItemClickListener.onClick(makeupItem)}"
app:icon="#drawable/ic_baseline_delete_outline_24"
app:iconGravity="end" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
Once you get to your fragment and declaring your adapter pass the adapter parameters which will do a certain viewModel function or whatever you need to implement on every click of your cardItem and delete button
adapter = CartAdapter(CartAdapter.CartItemClickListener { makeupItem ->
viewModel.onMakeupItemClicked(makeupItem)
}, CartAdapter.DeleteItemClickListener { makeupItem ->
viewModel.deleteItemFromCart(makeupItem)
}
)
I hope this is the best practice answer and helps everyone has the same issue.
Related
So I'm doing a project and I'm looking to see if my RecyclerView works. Here's what I got so far
The data class:
#Parcelize
#Entity(tableName = "asteroid_feed")
data class Asteroid(val id: Long, val codename: String, val closeApproachDate: String,
val absoluteMagnitude: Double, val estimatedDiameter: Double,
val relativeVelocity: Double, val distanceFromEarth: Double,
val isPotentiallyHazardous: Boolean) : Parcelable
The Adapter
class AsteroidViewAdapter (private val list: MutableList<Asteroid>) : ListAdapter<Asteroid, AsteroidViewAdapter.AsteroidViewHolder>(DiffCallback) {
companion object DiffCallback : DiffUtil.ItemCallback<Asteroid>() {
override fun areItemsTheSame(oldItem: Asteroid, newItem: Asteroid): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Asteroid, newItem: Asteroid): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AsteroidViewHolder {
return AsteroidViewHolder(AsteroidListContainerBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: AsteroidViewHolder, position: Int) {
holder.bind(getItem(position))
}
class AsteroidViewHolder (private val binding: AsteroidListContainerBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Asteroid){
binding.value = item
}
}
}
The Fragment
class MainFragment : Fragment() {
private lateinit var manager: RecyclerView.LayoutManager
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val binding = FragmentMainBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
val mutableList: MutableList<Asteroid> = ArrayList()
mutableList.add(Asteroid(1, "fgnuugrhrg", "bihagtyjerwailgubivb", 4.0, 8.0,3.0, 9.0, false))
mutableList.add(Asteroid(2, "fguk.nuugrhrg", "bidjswjyhagrwailgubivb", 3.0, 90.0,355.0, 9.0, true))
mutableList.add(Asteroid(3, "fgnssuugrhrg", "bshjtihagrwailgubivb", 4.0, 33.0,33.0, 9.0, false))
mutableList.add(Asteroid(4, "fgnuw4suugrhrg", "bjsryjihagrwailgubivb", 6.0, 8.0,11.0, 9.0, true))
mutableList.add(Asteroid(5, "fgnuugrudkdkhrg", "bihjjkkuagrwailgubivb", 4.0, 5.0,77.0, 9.0, false))
manager = LinearLayoutManager(this.context)
binding.asteroidRecycler.apply {
adapter = AsteroidViewAdapter(mutableList)
layoutManager = manager
}
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main_overflow_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return true
}
}
XML Files:
The Layout
<?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">
<data>
<variable
name="viewModel"
type="com.udacity.asteroidradar.main.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/app_background">
<FrameLayout
android:id="#+id/activity_main_image_of_the_day_layout"
android:layout_width="match_parent"
android:layout_height="220dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/activity_main_image_of_the_day"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:srcCompat="#drawable/placeholder_picture_of_day"/>
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="#android:color/white"
android:textStyle="bold"
android:textSize="20sp"
android:layout_gravity="bottom"
android:background="#55010613"
android:text="#string/image_of_the_day" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/asteroid_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activity_main_image_of_the_day_layout"
app:layout_constraintVertical_bias="0.0" />
<ProgressBar
android:id="#+id/status_loading_wheel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The Container:
<?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">
<data>
<variable
name="value"
type="com.udacity.asteroidradar.Asteroid" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/app_background">
<TextView
android:id="#+id/codename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textColor="#color/default_text_color"
android:textSize="20sp"
android:textStyle="bold"
android:text="#{value.codename}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="#string/codename" />
<TextView
android:id="#+id/date_field"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textColor="#color/default_text_color"
android:text="#{value.closeApproachDate}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="#+id/codename"
app:layout_constraintTop_toBottomOf="#+id/codename"
tools:text="#tools:sample/date/mmddyy" />
<ImageView
android:id="#+id/danger_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="#+id/codename"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="#drawable/ic_status_potentially_hazardous" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
As you can see, I was using a MutableList to fill in the fields and having that list viewed on my RecyclerView.ListAdapter in the fragment_main but it is not showing anything. Again this is just checking to see if The RecyclerView is working. Thanks for the Help.
The issue that you send the list to the adapter, but didn't submit it using submitList()
You can do that in init{} of the adapter
class AsteroidViewAdapter(private val list: MutableList<Asteroid>) :
ListAdapter<Asteroid, AsteroidViewAdapter.AsteroidViewHolder>(DiffCallback) {
init {
submitList(list)
}
// omitted code
Based on my understanding list view doesn't know what size of the items in the list to populate. So you need to override item list count by below inside list adapter class.
override fun getItemCount(): Int {
return items.size
}
I have a recyclerView and every elements from it has a ratingBar. I try to take the rating the user will give for every elements.
I am using MVVM architecture.
This is my code now:
<RatingBar
android:id="#+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:stepSize="1"
android:onClick="#{(view) -> listener.rateMovie(view, model)}"
app:layout_constraintTop_toBottomOf="#id/movie_poster"
app:layout_constraintEnd_toEndOf="#id/movie_poster"
app:layout_constraintStart_toStartOf="#id/movie_poster"
/>
override fun rateMovie(
ratingBar: View,
pickedMovieItemViewModel: PickedMovieItemViewModel
) {
ratingBar as RatingBar
val stars: Int = ratingBar.numStars
pickedMovieItemViewModel.rating.set(stars)
}
This is where I initilized the listener:
#BindingAdapter("rated_movies", "listener")
#JvmStatic
fun setItems(
recyclerView: RecyclerView,
items: List<PickedMovieItemViewModel>?,
rateListener: RateListener?
) {
var adapter = recyclerView.adapter
if (adapter == null) {
adapter = RateMoviesAdapter()
recyclerView.adapter = adapter
val layoutManager: RecyclerView.LayoutManager =
LinearLayoutManager(recyclerView.context, LinearLayoutManager.HORIZONTAL, false)
recyclerView.layoutManager = layoutManager
}
if (items != null) {
adapter as RateMoviesAdapter
adapter.updateItems(items)
}
if (items != null && rateListener != null) {
adapter as RateMoviesAdapter
adapter.updateListener(rateListener)
}
}
And this is the adapter for recycler-view
class RateMoviesAdapter : RecyclerView.Adapter<RateMoviesAdapter.RateMoviesViewHolder>() {
private lateinit var movies: List<PickedMovieItemViewModel>
private lateinit var listener: RateListener
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RateMoviesViewHolder {
val binding: ItemMovieRatedBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_movie_rated,
parent,
false
)
return RateMoviesViewHolder(binding)
}
override fun onBindViewHolder(holder: RateMoviesViewHolder, position: Int) {
val item = movies[position]
holder.bind(item, listener)
}
override fun getItemCount(): Int {
return movies.size
}
fun update(
movies: List<PickedMovieItemViewModel>,
rateListener: RateListener
) {
this.movies = movies
this.listener = rateListener
notifyDataSetChanged()
}
class RateMoviesViewHolder(private val binding: ItemMovieRatedBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: PickedMovieItemViewModel, rateListener: RateListener) {
binding.model = item
binding.listener = rateListener
}
}
}
This is the layout where recycler view is
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.moviepicker.presentation.viewmodel.RateMoviesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.activity.RateMoviesActivity">
<TextView
android:id="#+id/rate_textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin"
android:gravity="center"
android:text="#string/please_give_us_a_review_for_movies_you_have_picked"
android:textColor="#color/red_dark"
android:textSize="#dimen/title_size"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_pickedMovies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/top_margin"
app:listener="#{viewModel::rateMovie}"
app:rated_movies="#{viewModel.pickedMovies}"
android:foregroundGravity="center"
app:layout_constraintTop_toBottomOf="#id/rate_textView" />
<Button
android:id="#+id/login_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/top_margin"
android:layout_marginTop="#dimen/button_top_margin"
android:layout_marginEnd="#dimen/top_margin"
android:layout_marginBottom="#dimen/top_margin"
android:background="#drawable/ic_red_button"
android:onClick="#{() -> viewModel.goMainPage()}"
android:shadowColor="#color/black"
android:shadowDx="1.5"
android:shadowDy="1.3"
android:shadowRadius="10"
android:text="#string/next"
android:textAllCaps="false"
android:textColor="#color/white"
android:textSize="#dimen/text_size"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#id/rv_pickedMovies" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The method rateMovie is never called.
Where am I wrong?
OnClick event is not getting fired on RatingBar view, so I have used setOnRatingBarChangeListener() method to get callback from RatingBar. And also remove android:onClick="#{(view) -> listener.rateMovie(view,model)}" from ItemViewRateBinding. Like:
fun bind(item: PickedMovieItemViewModel, rateListener: RateListener) {
binding.model = item
//binding.listener = rateListener
// Click listener is not working on Rating Bar.
binding.rating.setOnRatingBarChangeListener { ratingBar, rating, fromUser ->
rateListener.rateMovie(ratingBar,item)
}
}
item_view_rate.xml
<layout>
<data>
<variable
name="model"
type="com.adhoc.testapplication.PickedMovieItemViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RatingBar
android:id="#+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:stepSize="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!--
android:onClick="#{(view) -> listener.rateMovie(view,model)}"
-->
You are defining different listener in recycler's adapter , you need to pass listener from your fragment/videmodel which is implemented and overridden there.
YourFragment : Fragment(), RateListener {
val yourAdapter = RateMoviesAdapter(this)
}
where "this" represents implemented listener(Interface), and apply that listener in adapter.
Should work then
Hi I am using a recycler view with a card view, I have no idea why it is not scrolling smoothly. it stuck for a few seconds and shows the next content.
This is my xml for list item
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="#color/colorAccent"
app:cardCornerRadius="10dp"
app:cardElevation="10dp" >
<LinearLayout
android:id="#+id/product_list_item_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/product_name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="name"
android:textColor="#FFFFFF"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:id="#+id/product_catagory_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="catagory"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="#+id/product_end_date_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="End Date"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
this is my recyclerViewAdapter
class ProductsRecyclerViewAdapter(private val clickListener: (Product) -> Unit): RecyclerView.Adapter<ProductsMyViewHolder>() {
private val productsList = ArrayList<Product>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductsMyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding: ProductsListItemBinding =
DataBindingUtil.inflate(layoutInflater, R.layout.products_list_item, parent, false)
return ProductsMyViewHolder(binding)
}
override fun getItemCount(): Int {
return productsList.size
}
override fun onBindViewHolder(holder: ProductsMyViewHolder, position: Int) {
holder.bind(productsList[position], clickListener)
}
fun setList(products: List<Product>) {
productsList.clear()
productsList.addAll(products)
}
}
class ProductsMyViewHolder(val binding: ProductsListItemBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(product: Product, clickListener: (Product) -> Unit) {
binding.productNameTextView.text = product.name
binding.productCatagoryTextView.text = product.catagory
binding.productEndDateTextView.text = convertLongToTime(product.end_date)
binding.productListItemLayout.setOnClickListener {
clickListener(product)
}
}
fun convertLongToTime(time: Long): String {
val date = Date(time)
val format = SimpleDateFormat("dd MMM yyyy")
return format.format(date)
}
}
This is my fragment where i initialise the ProductsRecyclerViewAdapter
private fun initRecyclerView() {
binding.productsRecyclerView.layoutManager = LinearLayoutManager(context)
adapter = ProductsRecyclerViewAdapter ({ selectedItem: Product -> listItemClicked(selectedItem)})
binding.productsRecyclerView.adapter = adapter
displayProductssList()
}
private fun displayProductssList() {
productsViewModel.products.observe(viewLifecycleOwner, Observer {
Log.i("MYTAG", it.toString())
adapter.setList(it)
adapter.notifyDataSetChanged()
})
}
any thoughts what I might be doing wrong here
your suggestion would be very helpful
thanks in advance
R
The only thing I see in your code is creating new SimpleDateFormat on every bind, it is definitely not free. Create single instance in adapter.
Also, maybe your products observer executes too often?
I have created the adapter below which shows two different data models in the recycler view.
However, I am not sure how to do the bindings in the bind functions written in the ViewHolders. I have two seperate xml files which I would like to bind when this "bind" function is called but how do I set the data?
My code is as follows:
class HomeAdapter(
private val context: Context
) :
RecyclerView.Adapter<HomeAdapter.BaseViewHolder<*>>() {
private var homeList: List<Any> = emptyList()
companion object {
private const val TYPE_VISIT = 0
private const val TYPE_WASH = 1
}
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(item: T)
}
inner class VisitViewHolder(itemView: View) : BaseViewHolder<HomeVisitLabel>(itemView) {
override fun bind(item: HomeVisitLabel) {
//Do your view assignment here from the data model
}
}
inner class WashViewHolder(itemView: View) : BaseViewHolder<HomeWashLabel>(itemView) {
override fun bind(item: HomeWashLabel) {
//Do your view assignment here from the data model
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when (viewType) {
TYPE_VISIT -> {
val view = LayoutInflater.from(context)
.inflate(R.layout.reward_label_visit_card, parent, false)
VisitViewHolder(view)
}
TYPE_WASH -> {
val view = LayoutInflater.from(context)
.inflate(R.layout.reward_label_wash_card, parent, false)
WashViewHolder(view)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val element = homeList[position]
when (holder) {
is VisitViewHolder -> holder.bind(element as HomeVisitLabel)
is WashViewHolder -> holder.bind(element as HomeWashLabel)
else -> throw IllegalArgumentException()
}
}
override fun getItemViewType(position: Int): Int {
val comparable = homeList[position]
return when (comparable) {
is HomeVisitLabel -> TYPE_VISIT
is HomeWashLabel -> TYPE_WASH
else -> throw IllegalArgumentException("Invalid type of data " + position)
}
}
override fun getItemCount(): Int {
return homeList.size
}
}
One of the two XML files
<?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">
<data>
<variable
name="VisitLabel"
type="com.modelz.HomeVisitLabel" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="5dp"
android:layout_margin="5dp"
app:cardBackgroundColor="#color/colorPrimary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="Visit:"
android:gravity="left"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:textStyle="bold"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="#{VisitLabel.Name}"
android:gravity="left"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:textStyle="bold"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Redeem"
android:visibility="invisible"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="0.5"
android:gravity="left"
android:text="#{VisitLabel.descript}"
android:textSize="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="0.5"
android:gravity="left"
android:text="Progress:"
android:textSize="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="0.9"
android:text="#{VisitLabel.countUser}"
android:gravity="right"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="0.5"
android:text="#{VisitLabel.countSet}"
android:gravity="left"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>
if you want to use data binding, you need to inflate your layouts from Binding class as follow:
val binding = RewardLabelVisitCardBinding.inflate(layoutInflater, parent, false)
and same for the other layout.
You can get layoutInflator using:
val layoutInflater = LayoutInflater.from(parent.context)
And then in the bind function you need to use that binding variable to assign attributes. like,
binding.textView.text = item.name
The overall code for better understanding is given below:
class VisitViewHolder(private val binding: RewardLabelVisitCardBinding) : BaseViewHolder<HomeVisitLabel>(binding.root) {
override fun bind(item: HomeVisitLabel) {
binding.VisitLabel = item
binding.executePendingBindings()
}
}
Just pass binding variable to ViewHolder after inflating the layout in onCreateViewHolder
return VisitViewHolder(binding)
Hope, that answers your question!
You will need to complete the bind functions for both the viewholders:
override fun bind(item: HomeVisitLabel) {
// textView.text = item.name // example
}
You should be able to assign the values in the function above as demonstrated by the example, this should work as in onBindViewHolder, the bind function is called depending on the type of data at the position in the array, allowing the data to be bound to different xml layout files.
You will have to complete the bind functions for both viewholders, setting text views to item data strings for example or setting images resources for image views, corresponding to the correct xml layout file for the data type.
I'm in the process of fiddling my way to a working app using Kotlin and I have hit a roadblock when trying to implement OnClickListeners for my three buttons. I have my RecyclerView populate properly, but despite following the recommendations on this SO post (except in Kotlin) and following the documentation, though I am still having trouble getting the implementation to work.
The code below is my adapter class for the implementation.
class BrowseHabitsAdapter(private val habits: ArrayList<Habit>) :
RecyclerView.Adapter<BrowseHabitsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.habit_card, parent, false)
return ViewHolder(itemView, object: HabitClickListener {
override fun onDecrease(position: Int) {
val streak = itemView.dayCounter.text.toString().toInt()
itemView.dayCounter.text = streak.dec().toString()
}
override fun onIncrease(position: Int) {
val streak = itemView.dayCounter.text.toString().toInt()
itemView.dayCounter.text = streak.inc().toString()
}
override fun onEdit(position: Int) {
TODO("Change Activity to Edit")
}
})
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = habits[position]
holder.habitTitle.text = currentItem.title
holder.streak.text = currentItem.streak.toString()
}
override fun getItemCount() = habits.size
class ViewHolder(itemView : View, listener : HabitClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
val habitTitle: TextView = itemView.habitTitle
val streak: TextView = itemView.dayCounter
val decreaseCounterButton : Button = itemView.decreaseCounterButton
val increaseCounterButton : Button = itemView.increaseCounterButton
val listener = listener
init {
decreaseCounterButton.setOnClickListener(this)
increaseCounterButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (itemView.id) {
itemView.decreaseCounterButton.id -> listener.onDecrease(this.layoutPosition)
itemView.increaseCounterButton.id -> listener.onIncrease(this.layoutPosition)
}
}
}
interface HabitClickListener {
fun onDecrease(position : Int)
fun onIncrease(position : Int)
fun onEdit(position : Int)
}
}
and the following is my XML code defining one of my cards:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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"
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
app:cardBackgroundColor="#eeeeee"
app:cardCornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="#+id/cardHeader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="#+id/habitTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:text="#string/default_card_title"
android:textSize="18sp" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1" />
<ImageView
android:id="#+id/settingsIcon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="bottom"
android:layout_marginRight="10dp"
app:srcCompat="#android:drawable/ic_menu_manage" />
</LinearLayout>
<LinearLayout
android:id="#+id/cardControls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="#+id/decreaseCounterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="-"
android:textAllCaps="false"
android:textSize="30sp" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="#+id/dayCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:fontFamily="sans-serif-medium"
android:text="0"
android:textAlignment="center"
android:textSize="30sp"
android:textStyle="bold" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="#+id/increaseCounterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="+"
android:textSize="30sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Any additional explanation that can be provided as to what I did wrong and what is going on in detail would be really appreciated!
You are in kotlin so need to implement View.OnClickListener you can directly use setOnClickListener on any view.
Inside your ViewHolder Class:
itemView.increaseCounterButton.setOnClickListener{
listener.onIncrease(this.layoutPosition)
}
itemView.decreaseCounterButton.setOnClickListener{
listener.onDecrease(this.layoutPosition)
}
It should be view?.id instead of itemView.id
override fun onClick(v: View?) {
when (v?.id) {
itemView.decreaseCounterButton.id -> listener.onDecrease(this.layoutPosition)
itemView.increaseCounterButton.id -> listener.onIncrease(this.layoutPosition)
}
}
Additionally, your code with bugs. You handle HabitClickListener only update UI, when you scroll your data will be update base on habits. It means it will be revert when you scroll. Make sure streak of model Habit is var
return ViewHolder(itemView, object: HabitClickListener {
override fun onDecrease(position: Int) {
habits[position].streak = habits[position].streak.dec()
itemView.dayCounter.text = shabits[position].streak.toString()
}
override fun onIncrease(position: Int) {
habits[position].streak = habits[position].streak.inc()
itemView.dayCounter.text = shabits[position].streak.toString()
}
override fun onEdit(position: Int) {
TODO("Change Activity to Edit")
}
})