I'm using databiding into a RecyclerView in my layout like below :
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/topup_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:items="#{viewModel.items}"
tools:listitem="#layout/list_item_content" />
and the list and binding adapters like :
#BindingAdapter("app:items")
fun setItems(listView: RecyclerView, items: List<ListItem>?) {
items?.let {
(listView.adapter as MyListAdapter).submitList(items)
}
}
//------------------
class MyListAdapter() :
ListAdapter<ListItem, ViewHolder>(myListItemDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(private val binding: ListItemContentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ListItem) {
binding.item = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TopUpListItemContentBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class myListItemDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
return oldItem.label == newItem.label
}
override fun areContentsTheSame(
oldItem: ListItem,
newItem: ListItem
): Boolean {
return oldItem == newItem
}
}
But it's not the same case using a simple ListView. For many reasons, I prefer using a listview and I tried but I don't know how to customize my adapter for databiding like I used in RecycleView.
Below is My adapter for Listview :
class MyListAdapter(context: Context, items: List<ListItem>) :
ArrayAdapter<ListItem>(context, 0, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = ListItemContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
binding.item = getItem(position)
return binding.root
}
}
My Item class Model :
data class ListItem(
val iconResId: Int,
val label: String,
val action: () -> Unit
)
Any one has a clue ?
Try to update your adapter like below:
class MyListAdapter(context: Context, var items: List<ListItem> = arrayListOf()) :
ArrayAdapter<ListItem>(context, 0, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = ListItemContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
binding.item = items[position]
return binding.root
}
override fun getCount(): Int {
return items.size
}
fun submitList(items: List<ListItem>) {
this.items = items
notifyDataSetChanged()
}
}
Related
So I created mvvm project in kotlin using tmdb api. I was requested to load the movies using paging 3 and for each movie load a list of tags describing all of it's genres.I tried to copy the items with the same ids from the genre list and the movies list into a new list and send it to the child's adapter but it did not work. I don't see the parent's list or the child's list on screen but everything else is working fine. Can anyone help?
Thank you
ParentAdapter class:
class MoviesAdapter(
var list:MutableList<Genre>
) : PagingDataAdapter<Movies,MoviesAdapter.MoviesViewHolder>(differCallback){
companion object {
private val differCallback = object : DiffUtil.ItemCallback<Movies>() {
//Returns true if there are duplicate items
override fun areItemsTheSame(oldItem: Movies, newItem: Movies) =
oldItem.id == newItem.id
//Returns true if content is same
override fun areContentsTheSame(oldItem: Movies, newItem: Movies) =
oldItem == newItem
}
}
inner class MoviesViewHolder(val viewDataBinding: MovieItemBinding) : RecyclerView.ViewHolder(viewDataBinding.root){
lateinit var genreAdapter:GenreAdapter
var i=0
var genreList= mutableListOf<Genre>()
fun bind(movie:Movies,position:Int){
viewDataBinding.apply{
movieTitle.text = movie.title.toString()
releaseDate.text=movie.releaseDate.toString()
Glide.with(itemView).load(Constants.IMG_PATH+movie.posterPath).into(movieImg)
while(movie.genreIds.size>i) {
for(j in 0 until list.size)
if (movie.genreIds[i]==list[j].id){
genreList.add(i,list[j])
}
i++
}
genreAdapter= GenreAdapter(list=list)
recycler.apply {
adapter=genreAdapter
layoutManager=LinearLayoutManager(context,LinearLayoutManager.HORIZONTAL,false)
genreAdapter.notifyDataSetChanged()
}
}
genreList.clear()
}
}
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
val currentItem=getItem(position)
currentItem?.let { holder.bind(movie= it, position = position) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
val binding=MovieItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return MoviesViewHolder(binding)
}
}
ChildAdapter class:
class GenreAdapter(var list:MutableList<Genre>):RecyclerView.Adapter<GenreAdapter.GenreViewHolder>() {
inner class GenreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var btn = itemView.findViewById<Button>(R.id.genreType)
fun bind(genre: Genre) {
btn.text = genre.name.toString()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.genre_item, parent, false)
return GenreViewHolder(view)
}
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
// holder.bind(genre = list[position % list.size])
holder.bind(genre = list[position])
}
override fun getItemCount(): Int {
return list.size
}
}
I try to show data form api in Custom recycler view by using view binding and diffUtil
I try to use DiffUtil with Binding view in RecyclerView Adapter
but when I send the data from fragment differ.submitList(the DATA)
nothing the show in the list
when I debugging I found the data come from API and go to differ
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ItemViewHolder>() {
private val differCallBack = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallBack)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_article_preview, parent, false)
)
}
inner class ItemViewHolder(private val view: View) :
RecyclerView.ViewHolder(view) {
var binding =
ItemArticlePreviewBinding.bind(view)
fun bind(article: Article) {
Glide.with(view).load(article.urlToImage).into(binding.ivArticleImage)
binding.tvTitle.text = article.title
binding.tvSource.text = article.source.name
binding.tvDescription.text = article.description
binding.tvPublishedAt.text = article.publishedAt
}
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
holder.bind(article)
setOnClickListener {
onItemClickListener?.let {
it(article)
}
}
}
}
private var onItemClickListener: ((Article) -> Unit)? = null
fun setOnClickListener(listener: (Article) -> Unit) {
onItemClickListener = listener
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
Inside ItemViewHolder, the itemView you are binding data to, and the itemView you are passing to the recyclerView are two different views.
The view which you have passed to ItemViewHolder after creating from onCreateViewHolder is being passed to the RecyclerView, but you are binding data to a different view that you are inflating using the viewBinding of the ItemArticlePreviewBinding.
You can change ViewHolder's constructor to accept a binding instance instead of a view and then bind data to that binding object.
inner class ItemViewHolder(private val binding: ItemArticlePreviewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(article: Article) {
Glide.with(view).load(article.urlToImage).into(binding.ivArticleImage)
binding.tvTitle.text = article.title
binding.tvSource.text = article.source.name
binding.tvDescription.text = article.description
binding.tvPublishedAt.text = article.publishedAt
}
}
Inside onCreateViewHolder inflate a ViewBinding object and pass that to the ItemViewHolder's constructor
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemArticlePreviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
i'm confused, how can i update my adapter views from synthetic to viewbinding!
here is my code, how define viewbinding to viewHolder?
class PlayersListAdapter(
var items: ArrayList<MatchPlayer>
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflate: View =
LayoutInflater.from(parent.context)
.inflate(R.layout.item_team_player, parent, false)
return MatchSubstitutionHolder(inflate)
}
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
viewHolder.itemView.lblPlayerName.text = "name"
}
class MatchSubstitutionHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var view = itemView
}
override fun getItemCount(): Int {
return items.size
}
}
it's so easy like define viewbinding in fragment or activity
class PlayersListAdapter(
var items: ArrayList<MatchPlayer>
) :
RecyclerView.Adapter<TeamPlayersListAdapter.MatchSubstitutionHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchSubstitutionHolder {
val binding= ItemTeamPlayerBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return MatchSubstitutionHolder(binding)
}
override fun onBindViewHolder(viewHolder: MatchSubstitutionHolder, position: Int) {
viewHolder.binding.lblPlayerName.text = "name"
}
class MatchSubstitutionHolder(val binding: ItemTeamPlayerBinding) : RecyclerView.ViewHolder(binding.root)
override fun getItemCount(): Int {
return items.size
}}
I think this is best for you
class PlayersListAdapter(var items: ArrayList<MatchPlayer>) :
RecyclerView.Adapter<TeamPlayersListAdapter.MatchSubstitutionHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchSubstitutionHolder {
val binding = ItemTeamPlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MatchSubstitutionHolder(binding)
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(viewHolder: MatchSubstitutionHolder, position: Int) {
viewHolder.bind(position)
}
class MatchSubstitutionHolder(val binding: ItemTeamPlayerBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(position: Int) {
val matchPlayer = items[position]
binding.lblPlayerName.text = "name"
}
}
}
I'm trying to update the below recycler view to show the new data that input from the form in the second activity. However, its only showing the original list that I had in there. What am I doing wrong here?
Here is the main activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
var workoutList = mutableListOf(
Workout("a","d","d","d"),
Workout("a","d","d","d"),
Workout("a","d","d","d")
)
val adaptor = WorkoutAdaptor(workoutList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnNext.setOnClickListener {
Intent(this, SecondActivity::class.java).also {
startActivity(it)
}
}
binding.recyclerView.adapter = adaptor
binding.recyclerView.layoutManager = LinearLayoutManager(this)
}
override fun onRestart() {
super.onRestart()
var workout = intent.getSerializableExtra("EXTRA_WORKOUT") as Workout
workoutList.add(workout)
adaptor.notifyDataSetChanged()
}
}
Here is the adapter:
class WorkoutAdaptor (
var workouts: List<Workout>
) : RecyclerView.Adapter<WorkoutAdaptor.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val workoutCardBinding = WorkoutCardBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return ViewHolder(workoutCardBinding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(workouts[position])
}
override fun getItemCount(): Int {
return workouts.size
}
inner class ViewHolder(private val workoutCardBinding: WorkoutCardBinding) :
RecyclerView.ViewHolder(workoutCardBinding.root) {
fun bind(workout: Workout) {
workoutCardBinding.tvWorkoutCard.text = workout.toString()
}
}
}
And here is the activity where I get the workout object from:
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
lateinit var spWorkoutsPosition: String
lateinit var spIncrementsPosition: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.spWorkoutTypes.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
spWorkoutsPosition = p0?.getItemAtPosition(p2) as String
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
binding.spIncrements.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, position: Int, id: Long) {
spIncrementsPosition = adapterView?.getItemAtPosition(position) as String
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
binding.btnSave.setOnClickListener {
val workoutName = binding.tvWorkoutName.text.toString()
val workoutType = spWorkoutsPosition.toString()
val workoutIncrement = spIncrementsPosition.toString()
val workoutRestTime = binding.etRestTime.text.toString()
val workout = Workout(workoutName, workoutType, workoutIncrement, workoutRestTime)
Intent(this, MainActivity::class.java).also {
it.putExtra("EXTRA_WORKOUT", workout)
startActivity(it)
}
}
}
}
As #John Doe commented you should probably use ActivityResultLauncher. refer here.
and as for the update issue the recommended way of implementing recyclerView's adapter is androidx.recyclerview.widget.ListAdapter. The ListAdapter handles addition and removal without the need to redraw the entire view, and even animate those changes.
here's the implementation for your adapter:
class WorkoutAdaptor :
ListAdapter<Workout, WorkoutAdaptor.ItemViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemAutocompleteSearchResultBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(position)
}
inner class ItemViewHolder(val binding: ItemAutocompleteSearchResultBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(position: Int) {
binding.apply {
workoutCardBinding.tvWorkoutCard.text = getItem(position).toString()
}
}
}
private class DiffCallback : DiffUtil.ItemCallback<Workout>() {
override fun areContentsTheSame(oldItem: Workout, newItem: Workout): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: Workout, newItem: Workout): Boolean {
return oldItem.param1 == newItem.param1 &&
oldItem.param2 == newItem.param2 &&
oldItem.param3 == newItem.param3 &&
oldItem.param4 == newItem.param4
}
}
}
then you only have to call,
adapter.submitList(workoutList)
after you update the workoutList and the ListAdapter handles the rest.
and also call the adapter.submitList() in onStart() or onResume() methods.
I was following this tutorial and tried to use multiple ViewHolders. An example use case would be a messaging app where the recyclerview has to display a sent message and a receiving message with different layouts. I got it working with the following solution:
class MyEntityDiffCallback : DiffUtil.ItemCallback<MyEntity>() {
override fun areItemsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem == newItem
}
}
class MyAdapter :
ListAdapter<MyEntity, MyAdapter.MyAbstractViewHolder>(
MyEntityDiffCallback()
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyAbstractViewHolder {
if (viewType == 1){
return MyViewHolder1.from(parent)
}else{
return MyViewHolder2.from(parent)
}
}
override fun onBindViewHolder(holder: MyAbstractViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if(item.someCounter > 5){
return 1
}else{
return 0
}
}
abstract class MyAbstractViewHolder(val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: MyEntity)
}
class MyViewHolder2 private constructor(binding: Item2Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item2Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder2 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item2Binding.inflate(layoutInflater, parent, false)
return MyViewHolder2(binding)
}
}
}
class MyViewHolder1 private constructor(binding: Item1Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item1Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder1 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item1Binding.inflate(layoutInflater, parent, false)
return MyViewHolder1(binding)
}
}
}
}
Is this the way it should be implemented? I am not sure about the cast of the binding value in the bind method. Or are there other good practices to solve this with the Listadapter and the Databinding?