RecyclerView binding with LiveData displaying wrong values after item is removed - android

I'm using a RecyclerView with LiveData and databinding. The user can remove elements from the recyclerview, and sometimes, after an item is removed, the items in the list are reordered incorrectly and the wrong item is removed.
In printing the values in LogCat, the correct values/order are always printed, so it's somewhere in the display/binding that the items are getting (I believe) recycled improperly, but I haven't been able to resolve it.
Here's what I'm working with: Fragment, ViewModel, ItemPresenter, Adapter, list_item.xml
Below are what I believe to be the relevant parts of each file:
Fragment
viewModel
.personalFilesLiveData
.observe(viewLifecycleOwner, Observer {
personalFilesAdapter.bind(it)
if(personalFilesAdapter.itemCount == 0){
this.activity?.findViewById<ConstraintLayout>(R.id.no_results_container)?.visibility = View.VISIBLE
}
else{
this.activity?.findViewById<ConstraintLayout>(R.id.no_results_container)?.visibility = View.GONE
}
})
Presenter
class ItemPresenter(
private val openContentAction: (String) -> Unit,
private val removePersonalFileAction: (String) -> Unit) {
private lateinit var myPersonalFile: MyPersonalFileData
val name get() = myPersonalFile.name
val fileName get() = myPersonalFile.fileName
val month get() = myPersonalFile.monthDay
val year get() = myPersonalFile.year
fun bind(file: MyPersonalFileData) {
Log.d("MyFilesPersonalFileItemPresenter", "name: " + file.name)
this.myPersonalFile = file
}
fun contentClicked() {
openContentAction(myPersonalFile.contentId)
}
fun removeClicked() {
removePersonalFileAction(myPersonalFile.contentId)
}
}
Adapter
class Adapter(
private val openContentAction: (String) -> Unit,
private val removePersonalFileAction: (String) -> Unit
) : RecyclerView.Adapter<DataboundViewHolder<ViewMyPersonalFilesItemBinding>>() {
private var data = emptyList<MyPersonalFileData>()
fun bind(data: List<MyPersonalFileData>) {
val diff = RecyclerViewDiffHelper.simpleDiffUtil(this.data, data) {
first, second -> first.contentId == second.contentId
}
this.data = data
diff.dispatchUpdatesTo(this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataboundViewHolder<ViewMyPersonalFilesItemBinding> {
return DataboundViewHolder(
ViewMyPersonalFilesItemBinding.inflate(LayoutInflater.from(parent.context), parent, false).apply {
presenter = MyFilesPersonalFileItemPresenter(openContentAction, removePersonalFileAction)
}
)
}
override fun getItemCount() = data.count()
override fun onBindViewHolder(holder: DataboundViewHolder<ViewMyPersonalFilesItemBinding>, position: Int) {
AlternateBackgroundHelper.setBackground(holder.binding.root, position)
holder.binding.presenter?.bind(data[position])
}
}
class MyPersonalFileData(
val name: String,
val fileName: String,
val monthDay: String,
val year: String,
val contentId: String
) {
override fun toString(): String {
return "MyPersonalFileData(name='$name')"
}
}
list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="presenter"
type="com.storyslab.helper.myfiles.bookmarks.MyFilesBookmarkItemPresenter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:onClick="#{() -> presenter.contentClicked()}"
android:foreground="?selectableItemBackground"
tools:background="#313131">
<TextView
android:id="#+id/tv_content_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#android:color/white"
android:textStyle="bold"
android:text="#{presenter.name}"
app:fontFamily="#font/oswald_regular"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toStartOf="#id/tv_date_top_line"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="#id/tv_file_name"
tools:text="Content Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Any ideas?

I think the problem lies within this portion of your code in the adapter:
private var data = emptyList<MyPersonalFileData>()
fun bind(data: List<MyPersonalFileData>) {
val diff = RecyclerViewDiffHelper.simpleDiffUtil(this.data, data) {
first, second -> first.contentId == second.contentId
}
this.data = data // PROBLEM HERE!
diff.dispatchUpdatesTo(this)
}
You're replacing the main list of the adapter with the list that comes from your activity/fragment. In my opinion, we should never expose the same list reference to the adapter this way. Doing this may cause unexpected results like the one you're facing here.
Try like this and your problem should be resolved:
private val data = mutableListOf<MyPersonalFileData>()
fun bind(data: List<MyPersonalFileData>) {
val diff = RecyclerViewDiffHelper.simpleDiffUtil(this.data, data) {
first, second -> first.contentId == second.contentId
}
this.data.clear()
this.data.addAll(data)
diff.dispatchUpdatesTo(this)
}

Give it a try. Just replace the bind method and the data list with the following list.
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
fun bind(newList: List<Operator>) {
differ.submitList(newList)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Operator>() {
override fun areItemsTheSame(oldItem: MyPersonalFileData, newItem: MyPersonalFileData): Boolean =
oldItem.contentId == newItem.contentId
override fun areContentsTheSame(oldItem: Operator, newItem: Operator): Boolean {
return oldItem == newItem
}
}
}

Related

How can I select multiple items in RecyclerView and add them to an ArrayList(KOTLIN)?

I asked this question before but I could not get any answer. So I am asking it again but this time more detailed so you guys can understand my problem. I have a RecyclerView that gets items from my Firebase database. I want to select multiple items and add highlighted items to an arraylist of strings. FYI I am using a library called Groupie. I am not using custom adapter.
Hobbies Class
class Hobbies : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hobbies)
val database = FirebaseDatabase.getInstance()
tophobbies.layoutManager =
object : LinearLayoutManager(this, HORIZONTAL, false) {
override fun checkLayoutParams(lp: RecyclerView.LayoutParams): Boolean {
// force height of viewHolder here, this will override layout_height from xml
lp.height = recyclerlinear.height
return true
}
}
val adapter = GroupAdapter<GroupieViewHolder>()
val reference = database.getReference("Hobbies")
reference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (snap in snapshot.children) {
val hobbiesItem = snap.getValue(HobbiesClass::class.java)
if (hobbiesItem != null) {
adapter.add(HobbiesAdapter(hobbiesItem))
}
}
tophobbies.adapter = adapter
}
override fun onCancelled(error: DatabaseError) {
}
})
}
}
HobbiesAdapter
class HobbiesAdapter(val hobbyItem: HobbiesClass) : Item<GroupieViewHolder>() {
var list: ArrayList<String> = ArrayList()
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemView.hobbynameTV.text = hobbyItem.hobbyName
Picasso.get().load(hobbyItem.imageUrl).into(viewHolder.itemView.hobbyImageView)
viewHolder.itemView.setOnClickListener {
if (viewHolder.itemView.isSelected){
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackground)
viewHolder.itemView.isSelected = false
}else {
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackgroundselected)
viewHolder.itemView.isSelected = true
}
}
}
override fun getLayout(): Int {
return R.layout.row
}
}
HobbiesClass
#Parcelize
class HobbiesClass(val hobbyName: String, val imageUrl: String,var isSelected:Boolean) : Parcelable {
constructor() : this("", "",false)
}
My item row xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="164dp"
android:layout_height="70dp"
android:layout_marginStart="5dp"
android:background="#android:color/transparent"
xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:id="#+id/frameHobby"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/hobbiesbackground">
<TextView
android:id="#+id/hobbynameTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Camping"
android:textColor="#000000"
android:fontFamily="#font/extralight"
android:layout_gravity="center|right"
android:layout_marginEnd="15dp"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="#+id/hobbyImageView"
android:layout_width="54dp"
android:layout_height="47dp"
android:layout_gravity="center|left"
android:layout_marginStart="12dp"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
I can change items background with
viewHolder.itemView.setOnClickListener {
if (viewHolder.itemView.isSelected){
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackground)
viewHolder.itemView.isSelected = false
}else {
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackgroundselected)
viewHolder.itemView.isSelected = true
}
}
What I want to do is: Add the selected elements names to my arraylist. If I use the code provided under it adds the name string to the array but when I click to another one the previous one gets deleted.(It looks like the array is not saved, it resets everytime I press an item) Also if I click on multiple items rapidly then it adds them. But if I click on an item split second later all previous items from the arraylist get deleted. How can I fix this? I have looked on multiple videos but still could not get any answers.
viewHolder.itemView.setOnClickListener {
list.add(viewHolder.itemView.hobbynameTV.text.toString())
}
Thank you in advance!
I have found the solution. I knew the problem that was making my arraylist reset everytime was that the bind method runs everytime it is clicked. So I made a mutable list called selectedList in my Hobbies class where the recyclerview is stored. Then I past it as a parameter in my HobbiesAdapter. Then in My Hobbies class I changed the adapter from adapter.add(HobbiesAdapter(hobbiesItem))
to adapter.add(HobbiesAdapter(hobbiesItem,selectedItems))
Here is how my code looks like now. I hope it will help others in the future
Hobbies class
class Hobbies : AppCompatActivity(){
private val selectedItems = mutableListOf<String>()
#SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hobbies)
val database = FirebaseDatabase.getInstance()
tophobbies.layoutManager =
object : LinearLayoutManager(this, HORIZONTAL, false) {
override fun checkLayoutParams(lp: RecyclerView.LayoutParams): Boolean {
// force height of viewHolder here, this will override layout_height from xml
lp.height = recyclerlinear.height
return true
}
}
val adapter = GroupAdapter<GroupieViewHolder>()
val reference = database.getReference("Hobbies")
reference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (snap in snapshot.children) {
val hobbiesItem = snap.getValue(HobbiesClass::class.java)
if (hobbiesItem != null) {
adapter.add(HobbiesAdapter(hobbiesItem,selectedItems))
}
}
tophobbies.adapter = adapter
}
override fun onCancelled(error: DatabaseError) {
}
})
letsgobtn.setOnClickListener {
Toast.makeText(this,"Selected item is ${selectedItems.toString()}",Toast.LENGTH_SHORT).show()
}
}
}
HobbiesAdapter class
class HobbiesAdapter(val hobbyItem: HobbiesClass,val selectedItems: MutableList<String>) : Item<GroupieViewHolder>() {
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemView.hobbynameTV.text = hobbyItem.hobbyName
Picasso.get().load(hobbyItem.imageUrl).into(viewHolder.itemView.hobbyImageView)
viewHolder.itemView.setOnClickListener {
if (viewHolder.itemView.isSelected){
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackground)
viewHolder.itemView.isSelected = false
selectedItems.remove(viewHolder.itemView.hobbynameTV.text.toString())
}else {
viewHolder.itemView.frameHobby.setBackgroundResource(R.drawable.hobbiesbackgroundselected)
viewHolder.itemView.isSelected = true
selectedItems.add(viewHolder.itemView.hobbynameTV.text.toString())
}
}
}
override fun getLayout(): Int {
return R.layout.row
}
}
HobbiesClass class
#Parcelize
class HobbiesClass(val hobbyName: String, val imageUrl: String,var isSelected:Boolean) : Parcelable {
constructor() : this("", "",false)
}

Why is populating the BindingAdapter empty / null with complex case. MVVM

When I run the app, the fragments content is blank. Even though the log statements show, the list is populated. I tried implementing a favorite post feature. You can add/remove a favorite post to your list. This works fine.
The goal:
I want to display the favorite posts in FavoritePostsOverViewFragment. Using a recyclerView.
I'm also trying to follow MVVM architecture. Using a Room database. (no API at this point)
The problem(s):
Working with the 2 different objects seems a bit weird the way I do it right now. But it is populated at the moment
Please refer to the part "How I am getting the posts based on if they have been favorite by a user" Is there a less complex way of writing this?
The Binding Adapter is null / empty, not displaying the posts.
I am using the Adapter already in another fragment, it works fine there. I can see a list of posts and use the click listeners. So In my thoughts, I eliminated the adapter as a problem for this case.
The two data classes used:
data class Post(
var Id: Long = 0L,
var Text: String = "",
var Picture: Bitmap? = null,
var Link: String = "",
var UserId: String = "",
var UserEmail: String = ""
)
data class Favorite(
var Id: Long = 0L,
var UserId: String = "",
var PostId: Long = 0L
)
The Adapter
lass PostAdapter(val clickListener: PostListener, val favoriteListener: FavoriteListener) :
ListAdapter<Post, ViewHolder>(PostDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(clickListener, favoriteListener, item)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
}
class ViewHolder(val binding: PostListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(clickListener: PostListener, favoriteListener: FavoriteListener, item: Post) {
binding.post = item
binding.clickListener = clickListener
binding.favoriteListener = favoriteListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
println(layoutInflater.toString())
val binding = PostListItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem.Id == newItem.Id
}
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem == newItem
}
}
class PostListener(val clickListener: (post: Post) -> Unit) {
fun onClick(post: Post) = clickListener(post)
}
class FavoriteListener(val clickListener: (post: Post) -> Unit) {
fun onClick(post: Post) = clickListener(post)
}
How I am getting the posts based on if they have been favorite by a user.
class PostRepository(private val faithDatabase: FaithDatabase) {
suspend fun getUserFavs(): List<Long> {
return withContext(Dispatchers.IO) {
faithDatabase.favoriteDatabaseDao.getUserFavorites(CredentialsManager.cachedUserProfile?.getId()!!)
}
}
suspend fun getFavos(): LiveData<List<Post>> {
val _items: MutableLiveData<List<Post>> = MutableLiveData(listOf())
val items: LiveData<List<Post>> = _items
val postIds: List<Long>
var dbPost: DatabasePost
withContext(Dispatchers.IO) {
postIds = getUserFavs()
}
for (id in postIds) {
withContext(Dispatchers.IO) {
dbPost = faithDatabase.postDatabaseDao.get(id)
}
val post = Post(
Text = dbPost.Text,
UserId = dbPost.UserId,
UserEmail = dbPost.UserEmail,
Link = dbPost.Link,
Picture = dbPost.Picture,
Id = dbPost.Id
)
_items.value = _items.value?.plus(post) ?: listOf(post)
}
Timber.i("items= " + items.value!!.size)
/*this logs=
I/PostRepository: items= 2*/
return items
}
My FavoritePostOverViewModel
class FavoritePostsOverviewViewModel(val database: PostDatabaseDao, app: Application) :
AndroidViewModel(app) {
private val db = FaithDatabase.getInstance(app.applicationContext)
private val postRepository = PostRepository(db)
var posts: LiveData<List<Post>>? = null
init {
viewModelScope.launch {
posts = repository.getFavos()
Timber.i(posts!!.value.toString())
/* this logs=
I/FavoritePostsOverviewViewModel: [Post(Id=1, Text=Name, Picture=android.graphics.Bitmap#ef3b553, Link=Add your link here, UserId=auth0|62cc0d4441814675a5906130, UserEmail=jdecorte6#gmail.com), Post(Id=4, Text=test, Picture=android.graphics.Bitmap#35ae90, Link=www.google.com, UserId=auth0|62cc0d4441814675a5906130, UserEmail=jdecorte6#gmail.com)]*/
}
}
my FavoritePostsOverViewFragment
class FavoritePostsOverViewFragment : Fragment() {
lateinit var binding: FragmentFavoritePostsBinding
private lateinit var favoritePostsOverviewViewModel: FavoritePostsOverviewViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// setup the db connection
val application = requireNotNull(this.activity).application
val dataSource = FaithDatabase.getInstance(application).postDatabaseDao
// create the factory + viewmodel
val viewModelFactory = FavoritePostsOverviewViewModelFactory(dataSource, application)
favoritePostsOverviewViewModel =
ViewModelProvider(this, viewModelFactory)[FavoritePostsOverviewViewModel::class.java]
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_favorite_posts, container, false)
// Giving the binding access to the favoritePostsOverviewViewModel
binding.favoritePostsOverviewViewModel = favoritePostsOverviewViewModel
// Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
binding.lifecycleOwner = this
// Sets the adapter of the PostAdapter RecyclerView with clickHandler lambda that
// tells the viewModel when our property is clicked
binding.postList.adapter = PostAdapter(PostListener {
favoritePostsOverviewViewModel.displayPropertyDetails(it)
}, FavoriteListener {
favoritePostsOverviewViewModel.FavoriteClick(it)
})
return binding.root
}
I have a Binding Adapter
#BindingAdapter("listData")
fun bindRecyclerViewPost(recyclerView: RecyclerView, data: List<Post>?) {
if (data.isNullOrEmpty()) {
return
}
val adapter = recyclerView.adapter as PostAdapter
adapter.submitList(data)
}
Used in the XML
<?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="favoritePostsOverviewViewModel"
type="com.example.ep3_devops_faith.ui.post.favorites.FavoritePostsOverviewViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/post_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="6dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:listData="#{favoritePostsOverviewViewModel.posts}"
tools:listitem="#layout/post_list_item"
tools:itemCount="16"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
referenced articles:
Android BindingAdapter order of execution?
LiveData Observer in BindingAdapter
https://developer.android.com/topic/architecture
https://developer.android.com/topic/libraries/data-binding/binding-adapters
https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/01_Introduction
try changing this line
app:listData="#{favoritePostsOverviewViewModel.posts}"
to
app:listData="#{favoritePostsOverviewViewModel.posts.value}"
I guess, you are binding list of posts in your binding adapter and you are passing LiveData<List>

How to populate Recyclerview with images retrieved from firebase storage?

How to populate a Recyclerview with images retrieved from Firebase storage?
I'm trying to use a pattern I learned in one of the courses, but it doesn't seem to apply
Here's the adapter:
class RecyclerViewAdapter (
private val onClickListener: OnClickListener) :
ListAdapter<GiftData, RecyclerViewAdapter.DataViewHolder>(DiffCallback)
{
class DataViewHolder(private var binding: GridViewItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(GiftData: GiftData) {
binding.data = GiftData
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<GiftData>() {
override fun areItemsTheSame(oldItem: GiftData, newItem: GiftData): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: GiftData, newItem: GiftData): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DataViewHolder {
return DataViewHolder(GridViewItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: DataViewHolder, position: Int) {
val data = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(data)
}
holder.bind(data)
}
fun interface OnClickListener {
fun onClick(data: GiftData)
}
}
The Binding adapter:
#BindingAdapter("giftData")
fun bindGiftRecyclerView(recyclerView: RecyclerView, data: List<GiftData>?) {
val adapter = recyclerView.adapter as RecyclerViewAdapter
adapter.submitList(data)
}
the Recyclerview xml:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/gift_photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="6dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:giftData="#{viewModel.data}"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="#layout/grid_view_item" />
The call to firebase which returns string urls for the images:
fun getImagesUrl(callback: (String)-> Unit){
var url: String
val storageRef = Firebase.storage.reference.child("images")
storageRef.listAll().addOnSuccessListener { listResult ->
listResult.items.forEach { result ->
result.downloadUrl.addOnSuccessListener {
url = it.toString()
callback.invoke(url)
}
}
}
}
The viewmodel: Here I'm trying to set the value of [Data] a mutablelivedata list of the data class that's holding the images url which then I'm going to load into the view item, but the returned is a list of strings while the required type is a list of the data class holding the url
class OverviewViewModel : ViewModel() {
private val _status = MutableLiveData<NetworkStatus>()
val status: LiveData<NetworkStatus>
get() = _status
private val _data = MutableLiveData<List<GiftData>>()
val data: LiveData<List<GiftData>>
get() = _data
init {
getData()
}
private fun getData(){
viewModelScope.launch {
_status.value = NetworkStatus.LOADING
try {
getImagesUrl {
_data.value = listOf(it)
_status.value = NetworkStatus.DONE
}
}catch (e: Exception){
Log.e("failed", e.toString())
_data.value = ArrayList()
_status.value = NetworkStatus.ERROR
}
}
}
}
From the question what I understand is that you need to fetch the image in the recycler view item.
So to this kindly follow the steps below:
// add the dependency of Glide in build.gradle file (app level)
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
and in the onBindViewHolder you can simply pass the link to the glide to load it in the item's imageView
Glide.with(this)
.load(MEDIA_LINK)
.into(YOUR_IMAGE_VIEW);
and further you can also attach listeners to it if you need.
Feel Free to ask if something is unclear and if it helps you kindly mark it as the correct answer.

BindingAdapter has lost binding with unknown reason

The binding adapter has lost binding to the view model. However I have no idea what is the reason. The SpinnerTextView in the code is a textview popping an alert dialog for selecting value from a list. Setting the title will set The textview's text as the String value. The binding lost cause the textview does not show the new value, is there any solution?
I have put some breakpoints, and I found that the pickedQuantity = "0" worked and also pickedQuantity.value = quantities.value!![index] has been run too. However, in the BindingAdapter.kt only pickedQuantity = "0" triggered the setTitle function.
Therefore, my TextView will always shows 0 but not changing when I select value.
BindingAdapter.kt
#BindingAdapter("spinnerTitle")
fun<T> SpinnerTextView<T>.setTitle(str: String) {
title = str
}
#BindingAdapter("spinnerAlertTitle")
fun<T> SpinnerTextView<T>.setAlertTitle(str: String) {
alertTitle = str
}
#BindingAdapter("spinnerItems")
fun<T> SpinnerTextView<T>.setItems(list: List<T>) {
items = list
}
#BindingAdapter("spinnerItemHandler")
fun<T> SpinnerTextView<T>.setHandler(handler: (Int) -> Unit) {
valueChanged = handler
}
TicketTypeViewModel.kt
class TicketTypeViewModel : BaseViewModel() {
val ticketId = MutableLiveData<Int>()
val ticketName = MutableLiveData<String>()
val ticketPrice = MutableLiveData<String>()
val pickedQuantity = MutableLiveData<String>()
val quantities = MutableLiveData<List<String>>()
val spinnerTitle = MutableLiveData<String>()
val spinnerHandler = MutableLiveData<(Int) -> Unit>()
fun bind(ticket: TicketType, onClick: (Int) -> Unit) {
ticketId.value = ticket.ticketTypeId
ticketName.value = ticket.ticketTypeName
ticketPrice.value = "$" + ticket.price
pickedQuantity.value = "0"
spinnerTitle.value = ""
val temp = mutableListOf<String>()
for (i in 0 until ticket.quota) {
temp.add(i.toString())
}
quantities.value = temp.toList()
spinnerHandler.value = { index ->
pickedQuantity.value = quantities.value!![index]
onClick(index)
}
}
}
TicketTypeAdapter.kt
class TicketTypeAdapter(val onClick: (Int) -> Unit): RecyclerView.Adapter<TicketTypeAdapter.ViewHolder>() {
private var ticketList: MutableList<TicketType> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketTypeAdapter.ViewHolder {
val binding: EventTicketTypeListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.event_ticket_type_list_item, parent, false)
return ViewHolder(binding, onClick)
}
override fun onBindViewHolder(holder: TicketTypeAdapter.ViewHolder, position: Int) {
holder.bind(ticketList[position])
}
override fun getItemCount(): Int {
return ticketList.size
}
fun refreshTicketList(ticketList: List<TicketType>){
this.ticketList.clear()
this.ticketList.addAll(ticketList)
notifyDataSetChanged()
}
class ViewHolder(private val binding: EventTicketTypeListItemBinding, val onClick: (Int) -> Unit): RecyclerView.ViewHolder(binding.root){
private val viewModel = TicketTypeViewModel()
fun bind(ticket: TicketType){
viewModel.bind(ticket, onClick)
binding.ticketType = viewModel
}
}
}
In .xml
<com.cityline.component.SpinnerTextView
android:id="#+id/spinner_quantity"
spinnerAlertTitle="#{ticketType.spinnerTitle}"
spinnerItemHandler="#{ticketType.spinnerHandler}"
spinnerItems="#{ticketType.quantities}"
spinnerTitle="#{ticketType.pickedQuantity}"
android:layout_width="20dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="20dp"
android:background="#drawable/edit_text_bg_bottom_line"
android:gravity="center"
android:textSize="16sp" />
You're missing a call to binding.setLifecycleOwner(this)
Sets the LifecycleOwner that should be used for observing changes of
LiveData in this binding. If a LiveData is in one of the binding expressions
and no LifecycleOwner is set, the LiveData will not be observed and updates to it
will not be propagated to the UI.
So either set the lifecycle owner or use ObservableField instead, which is better fitting.
As adapters work differently in regards of data updates, you might want to propagate changes to the adapter data set instead and call notifyDataSetChanged() or a similar one to update the bindings.

Generic RecyclerView adapter

I want to have generic RecyclerView to be able to reuse it. In my case I have 2 models: CategoryImages and Category. While trying to add constructor() it brings the following errors. I know the second one is because it understands like both primary and secondary constructor are same.
Is it possible to do such kind of thing? If yes, then how? if no - thank you.
Here is CategoryImage:
class CategoryImage {
#SerializedName("url")
private var url: String? = null
fun getUrl(): String? {
return url
}
}
And here is Category:
class Category {
#SerializedName("_id")
var id: String? = null
#SerializedName("name")
var name: String? = null
#SerializedName("__v")
var v: Int? = null
#SerializedName("thumbnail")
var thumbnail: String? = null
}
Here is the part of RecyclerViewAdapter's constructor:
class RecyclerViewAdapter(var arrayList: ArrayList<CategoryImage>?, var fragment: Int): RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
constructor(arrayList: ArrayList<Category>, fragment: Int): this(arrayList, fragment)
}
I want to have generic RecyclerView to be able to reuse it.
That's nice intention, then why you haven't made your adapter generic?
I think you can adopt the approach outlined by Arman Chatikyan in this blog post. After applying some Kotlin magic you'll only need following lines of code in order to setup your RecyclerView:
recyclerView.setUp(users, R.layout.item_layout, {
nameText.text = it.name
surNameText.text = it.surname
})
And if you need to handle clicks on RecyclerView items:
recyclerView.setUp(users, R.layout.item_layout, {
nameText.text = it.name
surNameText.text = it.surname
}, {
toast("Clicked $name")
})
Now the adapter of the RecyclerView is generic and you are able to pass list of any models inside setup() method's first argument.
In this section I will copy-paste sources from the blog post, in order to be evade from external sources deprecation.
fun <ITEM> RecyclerView.setUp(items: List<ITEM>,
layoutResId: Int,
bindHolder: View.(ITEM) -> Unit,
itemClick: ITEM.() -> Unit = {},
manager: RecyclerView.LayoutManager = LinearLayoutManager(this.context)): Kadapter<ITEM> {
return Kadapter(items, layoutResId, {
bindHolder(it)
}, {
itemClick()
}).apply {
layoutManager = manager
adapter = this
}
}
class Kadapter<ITEM>(items: List<ITEM>,
layoutResId: Int,
private val bindHolder: View.(ITEM) -> Unit)
: AbstractAdapter<ITEM>(items, layoutResId) {
private var itemClick: ITEM.() -> Unit = {}
constructor(items: List<ITEM>,
layoutResId: Int,
bindHolder: View.(ITEM) -> Unit,
itemClick: ITEM.() -> Unit = {}) : this(items, layoutResId, bindHolder) {
this.itemClick = itemClick
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.bindHolder(itemList[position])
}
override fun onItemClick(itemView: View, position: Int) {
itemList[position].itemClick()
}
}
abstract class AbstractAdapter<ITEM> constructor(
protected var itemList: List<ITEM>,
private val layoutResId: Int)
: RecyclerView.Adapter<AbstractAdapter.Holder>() {
override fun getItemCount() = itemList.size
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = itemList[position]
holder.itemView.bind(item)
}
protected abstract fun onItemClick(itemView: View, position: Int)
protected open fun View.bind(item: ITEM) {
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
Assuming CategoryImage means a Category with image.
You can express this relationship with inheritance:
open class Category(
val name: String
)
class CategoryImage(
name: String,
val image: String
) : Category(name)
class RecyclerViewAdapter(
val arr: List<Category>,
val fragment: Int
) {
fun bind(i: Int) {
val item = arr[i]
val name: String = item.name
val image: String? = (item as? CategoryImage)?.image
}
}
Another options it to have a common interface (which removes that ugly cast):
interface CategoryLike {
val name: String
val image: String?
}
class Category(
override val name: String
) : CategoryLike {
override val image: String? = null
}
class CategoryImage(
override val name: String,
override val image: String
) : CategoryLike
class RecyclerViewAdapter(private var arr: List<CategoryLike>, var fragment: Int) {
fun bind(i: Int) {
val item = arr[i]
val name: String = item.name
val image: String? = item.image
}
}
In both cases the following works (just to see that it can be compiled):
fun testCreation() {
val cats: List<Category> = listOf()
val catImages: List<CategoryImage> = listOf()
RecyclerViewAdapter(cats, 0)
RecyclerViewAdapter(catImages, 0)
}
Tip: don't use ArrayList, List (listOf(...)) or MutableList (mutableListOf(...)) should be enough for all your needs.
Tip: try to use val as much as you can, it helps prevent mistakes.
Wish: Next time please also include some relevant parts of your code in a copy-able form (not screenshot), so we don't have to re-type it and have more context. See https://stackoverflow.com/help/mcve
One "terrible" way of doing it is to simply have 1 constructor taking an ArrayList of Objects and perform an instanceof on the objects.
Both methods have the same signature, because type parameters are not considered as different types (for Java Virtual Machine both are just ArrayLists). You also need to be aware of type erasure.
Check this repository https://github.com/shashank1800/RecyclerGenericAdapter
lateinit var adapter: RecyclerGenericAdapter<AdapterItemBinding, TestModel>
...
val clickListener = ArrayList<CallBackModel<AdapterItemBinding, TestModel>>()
clickListener.add(CallBackModel(R.id.show) { model, position, binding ->
Toast.makeText(context, "Show button clicked at $position", Toast.LENGTH_SHORT)
.show()
})
adapter = RecyclerGenericAdapter(
R.layout.adapter_item, // layout for adapter
BR.testModel, // model variable name which is in xml
clickListener // adding click listeners is optional
)
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
adapter.submitList(viewModel.testModelList)
Recycler adapter item R.layout.adapter_item XML.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="testModel"
type="com.packagename.model.TestModel" />
</data>
...
VERY IMPORTANT NOTE: I'm using same layout for all my screens.
//********Adapter*********
// include a template parameter T which allows Any datatype
class MainAdapter<T : Any>(var data: List<T>) : RecyclerView.Adapter<MainViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val view = parent.inflateLayout()
return MainViewHolder(view)
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
}
override fun getItemCount(): Int = data.size
class MainViewHolder(private val binding: MainItemsListBinding) :
RecyclerView.ViewHolder(binding.root) {
// do the same for for bind function on Viewholder
fun <T : Any> bind(item: T) {
// Extension function see code below
binding.appInfo.mySpannedString(item)
}
}
}
//Cast Item to type
fun <T : Any> TextView.mySpannedString(item: T) {
when (item.javaClass.simpleName) {
"AaProgram" -> {
item as AaProgram
this.text = buildSpannedString {
appInfo(item.numero, item.principio)
}
}
"AppContent" -> {
item as AppContent
this.text = buildSpannedString {
appInfo(item.title, item.coment, item.footnote)
}
}
"AutoDiagnostic" -> {
item as AppContent
this.text = buildSpannedString {
appInfo(item.title, item.coment, item.footnote)
}
}
"GroupDirectory" -> {}
"ReflexionsBook" -> {}
"County" -> {}
"States" -> {}
"Towns" -> {}
}
}

Categories

Resources