BindingAdapter has lost binding with unknown reason - android

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.

Related

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>

RecyclerView binding with LiveData displaying wrong values after item is removed

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
}
}
}

How to add items to desired ViewHolder position when using multiple viewtypes in RecyclerView?

I drew what I'm trying to implement.
To explain the function, it is a workout log app.
Image means that both RoutineItem and DetailItem can be added and deleted by button.
(I didn't include a picture of a button that adds a RoutineItem in the image!)
I made a function that adds both RoutineItem and DetailItem, but I couldn't create a function that adds DetailItem to each RoutineItem last.
In other words, when the button of each RoutineItem is pressed, a DetailItem should be added to the end of each RoutineItem, but my current implementation is that any RoutineItem button is always added to the end of the List when the button is pressed.
I'm not sure how to figure out the end of each RoutineItem because I'm implementing it using Multiple ViewHolder.
How do I know the last DetailItem listed after each RoutineItem?
RoutineItem.kt
sealed class RoutineItem() {
data class RoutineModel(
val workout: String, // excercise
val unit: String, // unit (kg or lbs)
var routineDetail: List<DetailModel> = listOf()
) : RoutineItem()
data class DetailModel(
val set: String,
val reps: String = "1",
val weight: String
) : RoutineItem()
}
ViewHolder.kt
sealed class RoutineItemViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
class RoutineViewHolder(
private val binding: ItemRoutineBinding,
private val addDetailClicked: (Int) -> Unit,
val deleteDetailClicked: (Int) -> Unit
)
: RoutineItemViewHolder(binding) {
init {
binding.add.setOnClickListener {
addDetailClicked(adapterPosition)
}
}
fun bind(item : RoutineItem.RoutineModel) {
binding.workout.text = item.workout
}
}
class RoutineDetailViewHolder(private val binding: ItemRoutineDetailBinding)
: RoutineItemViewHolder(binding) {
fun bind() {
// EMPTY
}
}
}
Adapter
class RoutineItemAdapter(
private val addDetailClicked: (Int) -> Unit,
private val deleteDetailClicked: (Int) -> Unit) :
ListAdapter<RoutineItem, RoutineItemViewHolder>(RoutineDiffCallback2()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoutineItemViewHolder {
return when(viewType) {
R.layout.item_routine -> RoutineItemViewHolder.RoutineViewHolder(
ItemRoutineBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
addDetailClicked,
deleteDetailClicked
)
R.layout.item_routine_detail -> RoutineItemViewHolder.RoutineDetailViewHolder(
ItemRoutineDetailBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException("Invalid ViewType Provided")
}
}
override fun onBindViewHolder(holder: RoutineItemViewHolder, position: Int) {
when(holder) {
is RoutineItemViewHolder.RoutineViewHolder -> holder.bind(currentList[position] as RoutineItem.RoutineModel)
is RoutineItemViewHolder.RoutineDetailViewHolder -> holder.bind()
}
}
override fun getItemCount(): Int = currentList.size // footer 때문에 +1
override fun getItemViewType(position: Int): Int {
return when(currentList[position]) {
is RoutineItem.RoutineModel -> R.layout.item_routine
is RoutineItem.DetailModel -> R.layout.item_routine_detail
else -> throw IllegalArgumentException("Invalid ViewType Provided")
}
}
}
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<List<RoutineItem>> = MutableLiveData(listOf())
private val routines = mutableListOf<RoutineItem>()
val items: LiveData<List<RoutineItem>> = _items
fun addRoutine(workout: String) {
routines.add(RoutineItem.RoutineModel(workout, "TEST"))
routines.add(RoutineItem.DetailModel("1","3","3"))
_items.postValue(routines)
}
fun addDetail(pos: Int) {
routines.add(RoutineItem.DetailModel("1","3","3"))
_items.postValue(routines)
}
}
I don't like the way you have handled this because these things do not belong to ViewModel and they belong to the Adapter, but in your code, you can add this function to your ViewModel to achieve what you want this method get the position of RoutineViewHolder and add a DetailViewHolder to end of it:
fun addDetailItemToEndOfRoutine(parentRoutineViewHolderPosition : Int){
var addingPosition:Int = parentRoutineViewHolderPosition
for(position in (parentRoutineViewHolderPosition + 1) .. routines.size()){
if(routines.getOrNull(position) is RoutineItem.DetailModel){
continue
}
addingPosition = position - 1
break
}
//now you have found the position and you can add the detail model you want
items.add(addingPosition + 1, RoutineItem.DetailModel("1","3","3"))
_items.postValue(routines)
}
this method adds a new detail veiwholder to the end of the Routine viewholder.
you can test it directly from where you use your viewmodel and for using this method in viewholder pass this method to viewholder and in viewHolder,
set it in the onClickListener you want and pass getAdapterPosition() to it.

E/RecyclerView: No adapter attached; skipping layout in Android, kotlin

I have implemented RecyclerView in my app with Kotlin using Refrofit, MVVM, DataBinding, Coroutines. The same code works fine in another fragment but not here.
*Note: The retrofit functions returns the commentsList successfully but only problem in displaying the list in a recyclerView.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val api = ApiRepository()
factory = CommentsViewModelFactory(api)
viewModel = ViewModelProvider(this, factory).get(CommentsViewModel::class.java)
viewModel.getComments(requireActivity())
viewModel.commentsList.observe(viewLifecycleOwner, Observer { comments ->
rvComment.also {
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
if (comments != null) {
it.adapter = HomeServicesCommentsAdapter(comments, this)
}
}
})
}
The ViewModel looks like this, i declared the comments as MutableLiveData, which returns the data successfully but the only issue is with the adapter attachment.
class CommentsViewModel(private val repository: ApiRepository) : ViewModel() {
var userComment: String? = null
private val comments = MutableLiveData<List<Comment>>()
private lateinit var job: Job
val commentsList: MutableLiveData<List<Comment>>
get() = comments
fun getComments(context: Context) {
job = CoroutinesIO.ioThenMain(
{
repository.getServices(context)
}, {
for (i in it!!.data.data)
comments.value = i.comments
}
)
}
Here is the adapter implementation
class HomeServicesCommentsAdapter(
private val comments: List<Comment>,
private val listenerService: RvListenerServiceComments
) : RecyclerView.Adapter<HomeServicesCommentsAdapter.ServicesViewHolder>() {
override fun getItemCount() = comments.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ServicesViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.custom_comment_layout,
parent,
false
)
)
override fun onBindViewHolder(holder: ServicesViewHolder, position: Int) {
holder.recyclerViewServicesBinding.comments = comments[position]
notifyDataSetChanged()
}
class ServicesViewHolder(
val recyclerViewServicesBinding: CustomCommentLayoutBinding
) : RecyclerView.ViewHolder(recyclerViewServicesBinding.root)
}
Let me know if you need the xml layout files.
Instead of giving layout manager at runtime while observing data ,
Define layoutmanager inside xml
eg:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvNews"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
tools:listitem="#layout/item_your_layout"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
Remove below lines from observer
it.layoutManager = LinearLayoutManager(requireContext())
EDIT:
Do not create instance of adapter while observing data because observing data is not on MainThread So make sure you set data on MainThread
val adapter = HomeServicesCommentsAdapter(arrayListOf(), this)
rvComment?.adapter = adapter
viewModel.getComments(requireActivity())
viewModel.commentsList.observe(viewLifecycleOwner, Observer { comments ->
comments?.let{adapter.setData(comments)}//define setData(list:ArrayList<Comments>) method in your adapter
})
HomeServicesCommentsAdapter.kt:
........
private var mObjects: MutableList<Comment>? = ArrayList()// top level declaration
fun setData(objects: List<Comment>?) {
this.mObjects = objects as MutableList<Comment>
this.notifyDataSetChanged()
}
......

Why do it still need launch notifyDataSetChanged() when I have used LiveData?

I'm learning Room with the sample project RoomWordsSample at https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin.
The following code are from the project.
In my mind, the LiveDate will update UI automatically when the data changed if it was observed.
But in the file WordListAdapter.kt, I find notifyDataSetChanged() is added to the function setWords(words: List<Word>), it's seems that it must notify UI manually when data changed.
Why do it still need launch notifyDataSetChanged() when I have used LiveData ?
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
words?.let { adapter.setWords(it) }
})
}
}
WordViewModel.kt
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
WordListAdapter.kt
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Actually, livedata will give you updated data in your activity. But now, it is your activity's job to update the ui. So, whenever live data gives you updated data, you will have to tell the ui to update the data. Hence, notifyDataSetChanged().
notifyDataSetChanged has nothing to do with LiveData, it's part of RecyclerView api.
LiveData - is way of receiving data in lifecycle-aware way, RecyclerView simply displays views.

Categories

Resources