Data disappears when scrolling in recycler view - android

Good day. So I currently have data in my recycler view. It is for now only static data. I still have to do the code where I import. My problem however is I have a button that changes the background of a text view. This happens in my adapter. And when I scroll through my list the bg color change gets reverted back to what it was before the button click. I have read a lot of similar problems but could not really find one that explains clearly or work for me. From what I read the data gets reset to the static data because it is currently happening in my onBindViewHolder and I think this changes the data on every new data read(scrolling). I read that I should create a link or a listener and then call it. But It does not make sense to me because if a link is called the same amount of times as the code is executed then it will be the same will it not. Maybe having a condition listener but not sure if this is the way to go.
I am somewhat new to android and kotlin. Have been working with it for a month now. I dont know everything I am doing but I got given a deadline. So sadly there was no time to go and learn the basics. Thank you for any and all help. Please let me know if you need any additional code/information
my adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView)
}
private fun setHeaderBg(view: View) {
view.setBackgroundResource(R.drawable.table_header_cell_bg)
}
private fun setContentBg(view: View) {
view.setBackgroundResource(R.drawable.table_content_cell_bg)
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
// (TableViewAdapter.DataviewHolder) .bind()
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = Tripsheetlist[rowPos - 1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
// setContentBg(txtAddress)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.Company.toString()
// txtAddress.text = modal.Address.toString()
txtWeight.text = modal.Weight.toString()
txtbutton1.text = modal.Button1.toString()
txtbutton2.text = modal.Button2.toString()
txttvdone.text = modal.tvdone.toString()
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
txttvdone.setBackgroundResource(R.color.green)
txttvdone.setText("✓")
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
txttvdone.setBackgroundResource(R.color.orange)
txttvdone.setText("x")
}
}
}
class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val txttvdone:TextView = itemView.findViewById<TextView>(R.id.txttvdone)
val txtbutton1:Button = itemView.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
} class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){
var txtbutton1 = view.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
var txttvdone = view.findViewById<TextView>(R.id.txttvdone)
}
I tried (TableViewAdapter.DataviewHolder) .bind() doing this and creating another class as I saw that was done in another thread(Why do values ​disappear after scrolling in Recycler View?) Its a lot like my problem. I just can't seem to implement his solution to make mine work. ( don't understand his solution fully)
//I am also aware that I am using android extensions which will expire at the end of the year. But for now it works and once I have the code up and running I will start to move over to the newer versions of kotlin.

A RecyclerView, as its name implies, will recycle the views when they go off screen. This means that when the view for an item comes into view, it gets recreated and the onBindViewHolder() is called to fill in the details.
Your onClickListener inside your adapter changes the background of one of the subviews for your cell view. However, that cell will be redrawn if it leaves the screen and comes back.
To get around this, your onClickListener should be changing a property on the data item, and your onBindViewHolder should check that property to determine what background color to display for the subview:
enum class DataState {
Unselected,
Success,
Failure
}
data class DataItem(var state: DataState = DataState.Unselected)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var dataItems: List<DataItem> = emptyList()
fun updateData(data: List<DataItem>) {
dataItems = data
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val dataItem = dataItems[position]
holder.txttvdone.apply {
setBackgroundResource(when (dataItem.state) {
DataState.Unselected -> android.R.color.transparent
DataState.Success -> R.color.green
DataState.Failure -> R.color.orange
})
text = when (dataItem.state) {
DataState.Unselected -> ""
DataState.Success -> "✓"
DataState.Failure -> "x"
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
dataItem.state = DataState.Success
notifyDataSetChanged()
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
dataItem.state = DataState.Failure
notifyDataSetChanged()
}
}
}
}

Related

Kotlin recycler view cannot change background when later calling a function

I'm new to Kotlin and so if you do find rubbish code here and poor practices, do let me know. Otherwise, here is the issue I am having.
I am writing a tiny app that presents users with multiple questions from which they have to select the correct answer. If they select the correct answer, the option is supposed to be highlighted green for 250ms and then they move on to the next question. Otherwise, select the incorrect answer. The logic for moving onto the next question is defined in the main activity, and the background change logic is defined in the adapter class. Below is what the adapter class looks like at the moment (I've only included that which I think is relevant just to add too much faff):
class QuestionOptionAdapter(
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener {
if (item == correctAnswer) {
runBlocking {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_correct)
delay(250)
}
onSelectedCorrectAnswer()
} else {
it.background =
ContextCompat.getDrawable(it.context, R.drawable.question_opt_incorrect)
onSelectedIncorrectAnswer()
}
}
}
}
I realised that although the code to changes the background is executed before onSelectedCorrectAnswer(), it won't change the background colour until the entire block has finished executing. Therefore, the user never sees the updated background.
Is there a way to show an update before the block finishes executing?
runBlocking doesn't work because it blocks. It will just wait for the whole time you delay and block the main thread so the device will be frozen and not show any visual changes until it returns.
You need to pass the Activity or Fragment's CoroutineScope into the adapter for the adapter to use. You can then launch a coroutine that won't block the main thread when you delay inside it.
Here I lifted the coroutine to encompass all your click listener logic. That will make it easier to modify the behavior later if you want.
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
delay(250)
onSelectedCorrectAnswer()
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}
Actually, you probably want to also prevent the user from clicking other options during that 250ms delay, so you should set a Boolean that can disable further clicking of items during the delay:
class QuestionOptionAdapter(
private val scope: CoroutineScope,
private val items: ArrayList<String>,
private val correctAnswer: String,
) : RecyclerView.Adapter<QuestionOptionAdapter.ViewHolder>() {
var onSelectedAnswer: (String) -> Unit = {}
var onSelectedCorrectAnswer: () -> Unit = {}
var onSelectedIncorrectAnswer: () -> Unit = {}
private var isLockClickListeners = false
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.tvOption.text = item
holder.tvOption.setOnClickListener { view ->
if (isLockClickListeners) {
return#setOnClickListener
}
scope.launch {
val isCorrect = item == correctAnswer
val colorDrawable =
if (isCorrect) R.drawable.question_opt_correct
else R.drawable.question_opt_incorrect
view.background = ContextCompat.getDrawable(view.context, colorDrawable)
if (isCorrect) {
isLockClickListeners = true
delay(250)
onSelectedCorrectAnswer()
isLockClickListeners = false
} else {
onSelectedIncorrectAnswer()
}
}
}
}
}

Recyclerview multiple item selector

I'm trying to create a calendar view for a reservation app. I need to show to the user which days are already in use.
For this i like to create a selector between continuous days like this:
For the calendar view i created a RecyclerView using java.util.Calendar as datasource, every day is a ViewHolder.
Adapter:
class CalendarAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var list = emptyArray<CalendarItem>()
override fun getItemViewType(position: Int): Int {
return list[position].viewType?.asInt ?: super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.CURRENT_DAY.asInt -> {
val binding = ViewCurrentDayBinding.inflate(inflater, parent, false)
CurrentDayHolder(binding)
}
ViewType.DAY_OF_MONTH.asInt -> {
val binding = ViewDayOfMonthBinding.inflate(inflater, parent, false)
DayOfMonthHolder(binding)
}
ViewType.DAY_OF_WEEK.asInt -> {
val binding = ViewDayOfWeekBinding.inflate(inflater, parent, false)
DayOfWeekHolder(binding)
}
ViewType.SELECTED_DAY.asInt -> {
val binding = ViewSelectedDayBinding.inflate(inflater, parent, false)
SelectedDayHolder(binding)
}
ViewType.MOCK.asInt -> {
val binding = ViewMockDayBinding.inflate(inflater, parent, false)
MockDayHolder(binding)
}
else -> {
val binding = ViewMockDayBinding.inflate(inflater, parent, false)
MockDayHolder(binding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
ViewType.CURRENT_DAY.asInt -> {
(holder as CurrentDayHolder).bind(list[position], position, callback)
}
ViewType.DAY_OF_MONTH.asInt -> {
(holder as DayOfMonthHolder).bind(list[position], position, callback)
}
ViewType.DAY_OF_WEEK.asInt -> {
(holder as DayOfWeekHolder).bind(list[position])
}
ViewType.SELECTED_DAY.asInt -> {
(holder as SelectedDayHolder).bind(list[position], position, callback)
}
ViewType.MOCK.asInt -> {
(holder as MockDayHolder).bind(list[position])
}
}
}
override fun getItemCount(): Int {
return list.size
}
private val callback: (index: Int) -> Unit = {
// find current selected index and unselect
val currentSelectedIndex =
list.indices.find { el -> list[el].viewType == ViewType.SELECTED_DAY }
if (currentSelectedIndex != null) {
list[currentSelectedIndex].viewType = list[currentSelectedIndex].defaultViewType
notifyItemChanged(currentSelectedIndex)
}
// select the new index
list[it].viewType = ViewType.SELECTED_DAY
notifyItemChanged(it)
}
}
ViewHolder:
class CurrentDayHolder(var binding: ViewCurrentDayBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
calendarItem: CalendarItem,
index: Int,
callback: (index: Int) -> Unit
) {
binding.day.text = calendarItem.label
binding.root.setOnClickListener {
callback.invoke( index)
}
}
}
The full project is avaible on GitHub
How can i archive my goal?
I also thought not to use a RecyclerView and create a custom view directly, with the obvious complexities of the case.
I'm sure there is a way to do this with the RecyclerView as well
With a RecyclerView you'd need to customise the item layout so you can display the different styles (normal item, start of selection, middle of selection, end of selection, all the stripey versions of those) and then calculate the state of each item in onBindViewHolder so you can style it correctly, e.g. by having different background drawables you can switch between. But since these are all separate views, you might have trouble getting those stripes to line up correctly where one view ends and the adjacent one starts.
Also you could just use a GridLayout or something for this - no need for a RecyclerView when you're displaying all the items at once. You might want to consider a custom view with a grid/table where each item is a TextView or a borderless Button (better!), where you have another View layer on top which is a custom view that draws the selections.
But since you'd have to draw the text as well (e.g. the white date over the orange highlight) you might find it easier to just make the whole thing as a custom view, where you're positioning all the text yourself. That's one of the benefits of custom views - you get more control over how it draws itself. You could always try subclassing an existing calendar widget! Use the work that's already been done
I solved the problem using your suggestions: I managed a state for each element of the recyclerview indicating the selection of start, destination and the intermediate value when I create the datasource of the elements. then in each viewholder I change the corresponding background.
Full solution available on the example code in the question.
Here a screen of the result.
thanks everyone for the help

Problem Implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview : adapter Recyclerview not update

I'm newbie use Kotlin on my dev apps android,
and now, I on step learn to implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview. The My Apps:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
I call function to get data from server on onCreateView HomeFragment, and observe this with show shimmer data on my Recylerview when is loading, try update RecyclerView when success, and show view Failed Load -button refresh when error.
The problem is:
Adapter Recyclerview not Update when success get Data from Server. Adapter still show shimmer data
With case error (no Internet), i show view Failed Load, with button refresh. Tap to refresh, i re-call function to get data server, but fuction not work correct. Recyclerview show last data, not show Failed Load again.
Bellow my code
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
NewsAdapter
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
I hope someone can help me to solve the problem. thanks, sorry for my English.
You have 3 different LiveDatas, right? One with a list of news data, one with a loading error message, and one with a loading state.
You set up an Observer for each of these, and that observer function gets called whenever the LiveData's value updates. That's important, because here's what happens when your request succeeds, and you get some new data:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
which means newsList updates, then loadError updates, then loading updates.
So your observer functions for each of those LiveDatas run, in that order. But you're not testing the new values (except for null checks), so the code in each one always runs when the value updates. So when your response is successful, this happens:
newsList observer runs, displays as successful, calls getDataNews
loadError observer runs, value is null so nothing happens
loading observer runs, value is false but isn't checked, displays as loading, calls getDataNewsShimmer
So even when the response is successful, the last thing you do is display the loading state
You need to check the state (like loading) before you try to display it. But if you check that loading is true, you'll have a bug in fetchNews - that sets loading = true, starts a coroutine that finishes later, and then immediately sets loading = false.
I'd recommend trying to create a class that represents different states, with a single LiveData that represents the current state. Something like this:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
And then you just need to observe that state:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
You also might want to break that display code out into separate functions (like showError(), showList(state.newsList) etc) and call those from the branches of the when, if that makes it more readable
I hope that makes sense! When you have a single value representing a state, it's a lot easier to work with - set the current state as things change, and make your observer handle each possible UI state by updating the display. When it's Loading, make it look like this. When there's an Error, make it look like this
That should help avoid bugs where you update multiple times for multiple things, trying to coordinate everything. I'm not sure why you're seeing that problem when you reload after an error, but doing this might help fix it (or make it easier to see what's causing it)

Recyclerview adapter onBindViewHolder payload is not working

I checked all the examples, but they don't work after all. As far as I know, even if payload is 'List', String or Int value can go into.
class RecordListAdapter (val context: Context, val layoutManager: LinearLayoutManager, private val canBeEdited: Boolean)
: RecyclerView.Adapter<RecordListAdapter.RecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var records: ArrayList<Record> = arrayListOf()
// Update ALL VIEW holder
override fun onBindViewHolder(holder: RecordViewHolder, position: Int) {
val current = records[position]
holder.autoCompleteTextView.text = SpannableStringBuilder(current.name)
holder.weightPicker.value = current.weight
holder.setPicker.value = current.set
holder.repsPicker.value = current.reps
if(position == itemCount - 1) holder.addBtn.visibility = View.VISIBLE
else holder.addBtn.visibility = View.GONE
if(canBeEdited) {
if(itemCount == 1) {
holder.deleteBtn.visibility = View.GONE
} else {
holder.deleteBtn.visibility = View.VISIBLE
holder.deleteBtn.setOnClickListener {
records.remove(current)
notifyItemRemoved(position)
}
}
} else
holder.deleteBtn.visibility = View.GONE
}
// Update only part of ViewHolder that you are interested in
override fun onBindViewHolder(holder: RecordViewHolder, position: Int, payloads: MutableList<Any>) {
Log.e("payload", "::$payloads")
if(payloads.isNotEmpty()) {
} else
super.onBindViewHolder(holder,position, payloads)
}
private fun addRecordDefault() {
this.records.add(Record("", 28, 5, 10))
notifyItemInserted(itemCount)
notifyItemRangeChanged(itemCount-1, 2, "PAYLOAD_ADD_BTN")
}
override fun getItemCount() = records.size
}
As above code, I set the Log.e to know whether the value is empty or not. The payload Log.e always say it's null.
E/payload: ::[]
Firstly, it seems you are just adding the item and want to change something in it with payloads right away.
Obviously, when you just add a new one, the whole item has to be drawn, thus no payloads needed.
Only then, when it is already drawn and you want to change some elements, you may use payloads with notifyItemChanged (if one item was changed) or notifyItemRangeChanged (if several items were changed).
Secondly, I am not sure regarding the range you use.
The first argument of notifyItemRangeChanged is the start index.
The second one is how many items you want to change.
Thirdly, it's not clear where do you call the addRecordDefault
So make sure you called notifyItemChanged or notifyItemRangeChanged with payloads.

Android how to interact with nested recyclerView from fragment

How does one properly send data to child adapter in a fragment?
I'm basically trying to implement an Instagram like comments-section, e.g. a bunch of comments that can each have more comments (replies).
To do that, I use one main recyclerView + main adapter, which instances are retained in my fragment, and within the main adapter I bind the children comments (recyclerView + adapter).
Adding comments to the main adapter is easy since the object is always available in the fragment, so I just call mainAdapter.addComments(newComments):
MainAdapter
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
notifyItemRangeInserted(0, newComments.size)
}
But how to call addComments of one particular nested-rV? I read I should not save the adapter instances and only use positions.
I'm trying to do that in my Fragment as follows:
val item = rVComments.findViewHolderForItemId(mAdapter.itemId)!!.itemView
val adapt = item.rVReplies.adapter as ChildCommentsAdapter
adapt.addComment(it.data.comment)
But that doesn't work very well: since we have only RecyclerViews, that particular ViewHolder is often already recycled if the user scrolled after posting or fetching items, which leads to a NullPointerException.
Hence the initial question: how does one properly interact with nested recyclerviews and their adapter? If the answer is via Interface, please provide an example as I've tried it without success since I shouldn't save adapter objects.
You can achieve that using a single multi-view type adapter by placing the comments
as part of the parent item, with that, you add the child items below the parent item and call notifyItemRangeInserted.
That way you don't have to deal with most of the recycling issues.
When you want to update a comment you just update the comment inside the parent item and call notifyItemChanged.
If you want I created a library that can generate that code for you in compile time.
It supports the exact case you wanted and much more.
Using #Gil Goldzweig's suggestion, here is what I did: in case of an Instagram like comments' system with replies, I did use a nested recyclerView system. It just makes it easier to add and remove items. However, as for the question How does one properly send data to child adapter in a fragment? You don't. It gets super messy. From my fragment, I sent the data to my mainAdapter, which in turn sent the data to the relevant childAdapter. The key to make it smooth is using notifyItemRangeInserted when adding a comment to the mainAdapter and then notifyItemChanged when adding replies to a comment. The second event will allow sending data to the child adapter using the payload. Here's the code in case other people are interested:
Fragment
class CommentsFragment : androidx.fragment.app.Fragment(), Injectable,
SendCommentButton.OnSendClickListener, CommentsAdapter.Listener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(CommentsViewModel::class.java)
}
private val searchViewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(SearchViewModel::class.java)
}
private val mAdapter = CommentsAdapter(this)
private var contentid: Int = 0 //store the contentid to process further posts or requests for more comments
private var isLoadingMoreComments: Boolean = false //used to check if we should fetch more comments
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comments, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//hide the action bar
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.GONE
contentid = arguments!!.getInt("contentid") //argument is mandatory, since comment is only available on content
ivBackArrow.setOnClickListener{ activity!!.onBackPressed() }
viewModel.initComments(contentid) //fetch comments
val layoutManager = LinearLayoutManager(this.context)
layoutManager.stackFromEnd = true
rVComments.layoutManager = layoutManager
mAdapter.setHasStableIds(true)
rVComments.adapter = mAdapter
setupObserver() //observe initial comments response
setupSendCommentButton()
post_comment_text.setSearchViewModel(searchViewModel)
setupScrollListener(layoutManager) //scroll listener to load more comments
iVCancelReplyTo.setOnClickListener{
//reset ReplyTo function
resetReplyLayout()
}
}
private fun loadMoreComments(){
viewModel.fetchMoreComments(contentid, mAdapter.itemCount)
setupObserver()
}
/*
1.check if not already loading
2.check scroll position 0
3.check total visible items != total recycle items
4.check itemcount to make sure we can still make request
*/
private fun setupScrollListener(layoutManager: LinearLayoutManager){
rVComments.addOnScrollListener(object: RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = rVComments.childCount
val totalItemCount = layoutManager.itemCount
val pos = layoutManager.findFirstCompletelyVisibleItemPosition()
if(!isLoadingMoreComments && pos==0 && visibleItemCount!=totalItemCount && mAdapter.itemCount%10==0){
//fetch more comments
isLoadingMoreComments = true
loadMoreComments()
}
}
})
}
private fun setupSendCommentButton() {
btnSendComment.setOnSendClickListener(this)
}
override fun onSendClickListener(v: View?) {
if(isInputValid(post_comment_text.text.toString())) {
val isReply = mAdapter.commentid!=null
viewModel.postComment(post_comment_text.text.toString(), mAdapter.commentid?: contentid, isReply) //get reply ID, otherwise contentID
observePost()
post_comment_text.setText("")
btnSendComment.setCurrentState(SendCommentButton.STATE_DONE)
}
}
override fun postCommentAsReply(username: String) {
//main adapter method to post a reply
val replyText = "${getString(R.string.replyingTo)} $username"
tVReplyTo.text = replyText
layoutReplyTo.visibility=View.VISIBLE
post_comment_text.requestFocus()
}
override fun fetchReplies(commentid: Int, commentsCount: Int) {
//main adapter method to fetch replies
if(!isLoadingMoreComments){ //load one series at a time
isLoadingMoreComments = true
viewModel.fetchReplies(commentid, commentsCount)
viewModel.replies.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
isLoadingMoreComments = false
}
Resource.Status.SUCCESS -> {
isLoadingMoreComments = false
mAdapter.addReplies(mAdapter.replyCommentPosition!!, it.data)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}
}
})
}
}
private fun isInputValid(text: String): Boolean = text.isNotEmpty()
private fun observePost(){
viewModel.postComment.observe(this, Observer<Resource<PostCommentResponse>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
if(it.data.asReply){
//dispatch comment to child adapter via main adapter
mAdapter.addReply(mAdapter.replyCommentPosition!!, it.data.comment)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}else{
mAdapter.addComment(it.data.comment)
}
resetReplyLayout()
//showProgressBar(false)
}
}
})
}
private fun setupObserver(){
viewModel.comments.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
isLoadingMoreComments = false
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
mAdapter.addComments(it.data)
isLoadingMoreComments = false
//showProgressBar(false)
}
}
})
}
private fun resetReplyLayout(){
layoutReplyTo.visibility=View.GONE
mAdapter.replyCommentPosition = null
mAdapter.commentid = null
}
override fun onStop() {
super.onStop()
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.VISIBLE
}
}
MainAdapter
class CommentsAdapter(private val listener: Listener) : RecyclerView.Adapter<CommentsAdapter.ViewHolder>(), ChildCommentsAdapter.ChildListener {
//method from child adapter
override fun postChildReply(replyid: Int, username: String, position: Int) {
commentid = replyid
replyCommentPosition = position
listener.postCommentAsReply(username)
}
interface Listener {
fun postCommentAsReply(username: String)
fun fetchReplies(commentid: Int, commentsCount: Int=0)
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
private var comments = mutableListOf<Comment>()
private var repliesVisibility = mutableListOf<Boolean>() //used to store visibility state for replies
var replyCommentPosition: Int? = null //store the main comment's position
var commentid: Int? = null //used to indicate which comment is replied to
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val comment = comments[position]
with(holder.view) {
//reset visibilities (rebinding purpose)
rVReplies.visibility = View.GONE
iVMoreReplies.visibility = View.GONE
tVReplies.visibility = View.GONE
content.loadUserPhoto(comment.avatarThumbnailURL)
text.setCaptionText(comment.username!!, comment.comment)
tvTimestamp.setTimeStamp(comment.timestamp!!)
val child = ChildCommentsAdapter(
//we pass parent commentid and position to child to be able to pass it again on click
this#CommentsAdapter, comments[holder.adapterPosition].id!!, holder.adapterPosition
)
val layoutManager = LinearLayoutManager(this.context)
rVReplies.layoutManager = layoutManager
rVReplies.adapter = child
//initial visibility block when binding the viewHolder
val txtMore = this.resources.getString(R.string.show_more_replies)
if(comment.repliesCount>0) {
tVReplies.visibility = View.VISIBLE
if (repliesVisibility[position]) {
//replies are to be shown directly
rVReplies.visibility = View.VISIBLE
child.addComments(comment.replies!!)
tVReplies.text = resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
} else {
//replies all hidden
val txt = txtMore + " (${comment.repliesCount})"
tVReplies.text = txt
}
}
//second visibility block when toggling with the show more/hide textView
tVReplies.setOnClickListener{
//toggle child recyclerView visibility and change textView text
if(holder.view.rVReplies.visibility == View.GONE){
//show stuff
if(comment.replies!!.isEmpty()){
Timber.d(holder.adapterPosition.toString())
//fetch replies if none were fetched yet
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!)
}else{
//load comments into adapter if not already
if(comment.replies!!.size>child.comments.size){child.addComments(comment.replies!!)}
}
repliesVisibility[position] = true
holder.view.rVReplies.visibility = View.VISIBLE
holder.view.tVReplies.text = holder.view.resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size && comment.replies!!.isNotEmpty()) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
}else{
//hide replies and change text
repliesVisibility[position] = false
holder.view.rVReplies.visibility = View.GONE
holder.view.iVMoreReplies.visibility = View.GONE
val txt = txtMore + " (${comment.repliesCount})"
holder.view.tVReplies.text = txt
}
}
tvReply.setOnClickListener{
replyCommentPosition = holder.adapterPosition
commentid = comments[holder.adapterPosition].id!!
listener.postCommentAsReply(comments[holder.adapterPosition].username!!)
}
iVMoreReplies.setOnClickListener{
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!, layoutManager.itemCount) //pass amount of replies too
}
}
}
#Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.isNotEmpty()){
//add reply to child adapter
with(holder.view){
Timber.d(payloads.toString())
val adapter = rVReplies.adapter as ChildCommentsAdapter
if(payloads[0] is Comment){
adapter.addComment(payloads[0] as Comment)
}else{
//will be of type List<Comment>
adapter.addComments(payloads[0] as List<Comment>)
val comment = comments[position]
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}else{
iVMoreReplies.visibility = View.GONE
}
}
}
}else{
super.onBindViewHolder(holder,position, payloads) //delegate to normal binding process
}
}
override fun getItemCount(): Int = comments.size
//add multiple replies to child adapter at pos 0
fun addReplies(position: Int, newComments: List<Comment>){
comments[position].replies!!.addAll(0, newComments)
notifyItemChanged(position, newComments)
}
//add a single reply to child adapter at last position
fun addReply(position: Int, newComment: Comment){
comments[position].replies!!.add(newComment)
comments[position].repliesCount += 1 //update replies count in case viewHolder gets rebinded
notifyItemChanged(position, newComment)
}
//add a new comment to main adapter at last position
fun addComment(comment: Comment){
comments.add(comment) //new comment just made goes to the end
repliesVisibility.add(false)
notifyItemInserted(itemCount-1)
}
//add multiple new comments to main adapter at pos 0
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
repliesVisibility.addAll(0, List(newComments.size) { false })
notifyItemRangeInserted(0, newComments.size)
}
}
The childAdapter is very basic and has nearly 0 logic.

Categories

Resources