How do I use ViewHolder to get a reference to items in the RecyclerView? - android-recyclerview

I am working with the LeDeviceListAdapter class in Android Studio. Specifically, I don't know how to call the ViewHolder for my RecyclerView in the getView function of this class.
override fun getView(i: Int, view: View?, viewGroup: ViewGroup?): View? {
var view: View? = view
val viewHolder: ViewHolder
// General ListView optimization code.
if (view == null) {
view = mInflator.inflate(R.layout.listitem_device, null)
viewHolder = ItemAdapter.ItemViewHolder(view)
viewHolder.device_name = view.findViewById(R.id.device_address) as TextView
viewHolder.device_address = view.findViewById(R.id.device_name) as TextView
view.setTag(viewHolder)
} else {
viewHolder = view.tag as ViewHolder
}
val device = mLeDevices[i]
val deviceName = device.name
if (deviceName != null && deviceName.length > 0) viewHolder.deviceName.setText(deviceName) else viewHolder.deviceName.setText(
R.string.unknown_device
)
viewHolder.deviceAddress.setText(device.address)
return view
}
I am getting an error where it says...
viewHolder = ViewHolder( )
The error says I can't instantiate an abstract class. I have tried to extend the ViewHolder class, and I have tried to access the ViewHolder through the RecyclerView.
How can I access the ViewHolder from this class?
Let me know if you need to see more of my project or hear more specifics.
UPDATE: I moved these two lines...
view = mInflator.inflate(R.layout.listitem_device, null)
viewHolder = ItemAdapter.ItemViewHolder(view)
...to the top of the function, and it resolved my problem with the ViewHolder.
For some reason, though, the line that says...
if (deviceName != null && deviceName.length > 0)
viewHolder.deviceName.setText(deviceName)
...cannot access the deviceName property of the ViewHolder the same way that I could above.

Related

Can you change the color of a textview in a recyclerview adapter after a certain condition is met in Main Activity?

I have a basic function that displays the elapsed time every time the button is pressed. I cannot get the logic in MainActivity to transfer to the recyclerview adapter. I simply want the text output color to change to red after the time passes 5 seconds. I have tried to research how to do this for the past week and I cannot find the exact answer. I'm hoping someone can help.
I have tried it with and without the boolean in the data class. I wasn't sure if that was required.
Here is my code:
Main Activity:`
class MainActivity : AppCompatActivity() {
var startTime = SystemClock.elapsedRealtime()
var displaySeconds = 0
private lateinit var binding: ActivityMainBinding
private val secondsList = generateSecondsList()
private val secondsAdapter = Adapter(secondsList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
recyclerView.adapter = secondsAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(false)
binding.button.setOnClickListener {
getDuration()
addSecondsToRecyclerView()
}
}
fun getDuration(): Int {
val endTime = SystemClock.elapsedRealtime()
val elapsedMilliSeconds: Long = endTime - startTime
val elapsedSeconds = elapsedMilliSeconds / 1000.0
displaySeconds = elapsedSeconds.toInt()
return displaySeconds
}
private fun generateSecondsList(): ArrayList<Seconds> {
return ArrayList()
}
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
Adapter:
var adapterSeconds = MainActivity().getDuration()
class Adapter(
private val rvDisplay: MutableList<Seconds>
) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
class AdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView1: TextView = itemView.tv_seconds
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
val myItemView = LayoutInflater.from(parent.context).inflate(
R.layout.rv_item,
parent, false
)
return AdapterViewHolder(myItemView)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
val currentDisplay = rvDisplay[position]
currentDisplay.isRed = adapterSeconds > 5
holder.itemView.apply {
val redColor = ContextCompat.getColor(context, R.color.red).toString()
val blackColor = ContextCompat.getColor(context, R.color.black).toString()
if (currentDisplay.isRed) {
holder.textView1.setTextColor(redColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
} else {
holder.textView1.setTextColor(blackColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
}
}
}
override fun getItemCount() = rvDisplay.size
}
Data Class:
data class Seconds(
var rvSeconds: Int,
var isRed: Boolean
)
when you call secondsList.add(addSeconds) then the data that is already inside secondsList should be updated too.
you could do something like
private var secondsList = generateSecondsList() // make this var
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
if ( /* TODO check if time has passed */) {
secondsList = secondsList.map { it.isRed = true }
secondsAdapter.rvDisplay = secondsList // TODO also make rvDisplay a var
secondsAdapter.notifyDatasetChanged() // also need to tell rv to redraw the all views
} else {
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
that might work, but to be honest it looks bad... There is already a lot of logic inside Activity. Read about MVVM architecture and LiveData, there should be another class called ViewModel that would keep track of time and the data. Activity should be as simple as possible, because it has lifecycle, so if you rotate the screen, all your state will be lost.
Your code isn't really working because of this:
var adapterSeconds = MainActivity().getDuration()
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
...
currentDisplay.isRed = adapterSeconds > 5
...
}
You're only setting adapterSeconds right there, so it never updates as time passes. I assume you want to know the moment 5 seconds has elapsed, and then update the RecyclerView at that moment - in that case you'll need some kind of timer task that will fire after 5 seconds, and can tell the adapter to display things as red. Let's deal with that first:
class Adapter( private val rvDisplay: MutableList ) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
private var displayRed = false
set(value) {
field = value
// Refresh the display - the ItemChanged methods mean something about the items
// has changed, rather than a structural change in the list
// But you can use notifyDataSetChanged if you want (better to be specific though)
notifyItemRangeChanged(0, itemCount)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
if (displayRed) {
// show things as red - you shouldn't need to store that state in the items
// themselves, it's not about them - it's an overall display state, right?
} else {
// display as not red
}
}
So with that setter function, every time you update displayRed it'll refresh the display, which calls onBindViewHolder, which checks displayRed to see how to style things. It's better to put all this internal refreshing stuff inside the adapter - just pass it data and events, let it worry about what needs to happen internally and to the RecyclerView it's managing, y'know?
Now we have a thing we can set to control how the list looks, you just need a timer to change it. Lots of ways to do this - a CountdownTimer, a coroutine, but let's keep things simple for this example and just post a task to the thread's Looper. We can do that through any View instead of creating a Handler:
// in MainActivity
recyclerView.postDelayed({ secondsAdapter.displayRed = true }, 5000)
That's it! Using any view, post a delayed function that tells the adapter to display as red.
It might be more helpful to store that runnable as an object:
private val showRedTask = Runnable { secondsAdapter.displayRed = true }
...
recyclerView.postDelayed(showRedTask, 5000)
because then you can easily cancel it
recyclerView.removeCallbacks(showRedTask)
Hopefully that's enough for you to put some logic together to get what you want. Set displayRed = false to reset the styling, use removeCallbacks to cancel any running task, and postDelayed to start a new countdown. Not the only way to do it, but it's pretty neat!
I finally figured it out using a companion object in Main Activity with a boolean set to false. If the time exceeded 5 seconds, then it set to true.
The adapter was able to recognize the companion object and change the color of seconds to red if they exceeded 5.

How to change a function argument value [duplicate]

I'm implementing a SpinnerAdapter in Android project. So I have to override getView(i: Int, convertView: View, parent: ViewGroup) method. So convertView is here in order to reuse existing view and reduce memory usage and GC occurrences. So if it is null I have to create view and use already created otherwise.
So in fact I have to write something like this (officially recomended by google):
if (view == null) {
view = View.inflate(context, R.layout.item_spinner, parent)
view.tag(Holder(view))
} else {
(view.tag as Holder).title.text = getItem(i)
}
But Kotlin does not allow to write to param.
What I found on the internet is an official blog post that says that it is not possible since Feb, 2013.
So I'm wondering if there is any workaround ?
There are two issues here.
First, you are mistakenly assuming that modifying view in Java does anything outside of the current function scope. It does not. You setting that parameter to a new value affects nothing outside of the local function scope.
View getView(int i, View view, ViewGroup parent) {
// modify view here does nothing to the original caller reference to view
// but returning a view does do something
}
Next, in Kotlin all parameters are final (JVM modifier, also same as final modifier in Java). The Kotlin if statement version of this code would be:
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return if (view == null) {
val tempView = View.inflate(context, R.layout.item_spinner, parent)
tempView.tag(Holder(tempView))
tempView
} else {
(view.tag as Holder).title.text = getItem(i)
view
}
}
or avoiding the new local variable:
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return if (view == null) {
View.inflate(context, R.layout.item_spinner, parent).apply {
tag(Holder(this)) // this is now the new view
}
} else {
view.apply { (tag as Holder).title.text = getItem(i) }
}
}
or
fun getView(i: Int, view: View?, parent: ViewGroup): View {
if (view == null) {
val tempView = View.inflate(context, R.layout.item_spinner, parent)
tempView.tag(Holder(tempView))
return tempView
}
(view.tag as Holder).title.text = getItem(i)
return view
}
or using the ?. and ?: null operators combined with apply():
fun getView(i: Int, view: View?, parent: ViewGroup): View {
return view?.apply {
(tag as Holder).title.text = getItem(i)
} ?: View.inflate(context, R.layout.item_spinner, parent).apply {
tag(Holder(this))
}
}
And there are another 10 variations, but you can experiment to see what you like.
It is considered less-than-a-good practice (but allowed) to shadow variables by using the same name, that is why it is a compiler warning. And why you see a change in the variable name above from view to tempView
Mutable parameters are not supported in Kotlin.
I would like to refer you to this discussion in kotlinlang.org
There's a dirty but useful way to achieve that.
fun a(b: Int) {
var b = b
b++ // this compiles
}
Officially speaking, you are not allowed to override a method param. The best you can do is "shadow" the param variable.
So you can do it similar to (not sure why you would want to shadow though but you can)
getView(i: Int, view: View?, parent: ViewGroup) {
val view = view ?: View.inflate(context, R.layout.item_spinner, parent)
.apply { tag(Holder(view)) }
(view.tag as Holder).title.text = getItem(i)
}

Why can't I use this as paramter of observe event when I use LiveData with Kotlin?

I use LiveData in my layout file, and add observe event for some LiveData variable , you can see Code C.
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
2: I think mHomeViewModel.listVoiceBySort.observe(this) {... } in Code B can work well, but in fact, it failed, why?
Code A
binding.lifecycleOwner = this.viewLifecycleOwner //It can work well
binding.lifecycleOwner = this //It can work well
Code B
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) { //It can work well
mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
Code C
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
//binding.lifecycleOwner = this //It can work well
binding.aHomeViewModel = mHomeViewModel
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) {
//mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
myAdapter.submitList(it)
}
...
return binding.root
}
private fun showActionMenu() {
val view = mActivity.findViewById<View>(R.id.menuMoreAction) ?: return
PopupMenu(mContext, view).run {
menuInflater.inflate(R.menu.menu_option_action, menu)
for (item in menu){
if (item.itemId == R.id.menuMoreActionShowCheckBox){
mHomeViewModel.displayCheckBox.observe(this#FragmentHome){
//mHomeViewModel.displayCheckBox.observe(this#FragmentHome.viewLifecycleOwner){ //It can work well
if (it){
item.title =mContext.getString(R.string.menuMoreActionHideCheckBox)
}else{
item.title =mContext.getString(R.string.menuMoreActionShowCheckBox)
}
}
}
}
...
}
}
...
}
viewLifecycleOwner is available only after view of the Fragment is being inflated. Referencing the Fragment this for lifecycleOwner will use the Fragment lifecycle.
You can use this (referencing the Fragment) before view of the fragment is inflated, in onAttach() and onCreateView(). While viewLifecycle can be used in and after any point after view is completely created, after onViewCreated().
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
Because they are both LifecycleOwner. Although you generally want to use viewLifecycleOwner from onViewCreated, otherwise you can get bugs on fragments that are in "limbo state" (replace().addToBackStack()'d).
2.) private fun showActionMenu() { ... observe(
You generally shouldn't "start observing" things that are triggered by side-effects, otherwise you will end up with observers that don't unregister even after your PopupMenu is dismissed, for example.

Negative offset causes RecyclerView item to disappear

In my RecyclerView, I have a header image for each group of CardView. The image extends below the first card view to create a nice visual effect. I achieved this by creating an image header view holder, and set the offset of the first CardView in each group to be negative (using ItemDecoration). However, as you can see at the bottom of the screen, the CardView disappears before it goes completely offscreen.
My custom item decoration
class OffsetItemDecoration(val offset: Int, val itemPositions: Array<Int>) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
val layoutManager = parent.layoutManager
if (!itemPositions.contains(position) || layoutManager !is LinearLayoutManager) {
return
}
outRect.top = offset
}
}
In my MainActivity:
val itemOffset = OffsetItemDecoration(-150, arrayOf(1, 5))
recycler_view.addItemDecoration(itemOffset)
Recommendations of other ways to achieve the same effect would be appreciated as well

Recycler View recycle issue

I have a recyclerView. When I do the pull to refresh, if the new data is just one list item, then the recycler view loads the item perfectly. But if the updated data contains 2 or more, then I think the view is not recycled properly. In the actionContainer, there should only one item to be added for each of the updated list item. But during pull to refresh, ONLY WHEN there are 2 or more list items to be updated, the actionContainer shows 2 data where it should be only one. Can someone help me to fix this?
override fun onBindViewHolder(holder: HistoryListAdapter.ViewHolder?, position: Int) {
info("onBindViewHolder =>"+listAssets.size)
info("onBindViewHolder itemCount =>"+itemCount)
info("onBindViewHolder position =>"+position)
val notesButton = holder?.notesButton
val notesView = holder?.notesTextView
val dateTime = listAssets[position].date
val location = listAssets[position].location
val sessionId = listAssets[position].id
holder?.sessionID = sessionId
holder?.portraitImageView?.setImageDrawable(listAssets[position].image)
holder?.titleTextView?.text = DateTimeFormatter.getFormattedDate(context, dateTime)
val timeString = DateTimeFormatter.getFormattedTime(context, dateTime)
if (location.length != 0) {
holder?.subtitleTextView?.text = "$timeString # $location"
} else {
holder?.subtitleTextView?.text = "$timeString"
}
val data = listAssets[position].data
for (actionData in data) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val parent = inflater.inflate(R.layout.history_card_action, null)
val icon = parent?.findViewById(R.id.historyActionIcon) as ImageView
val title = parent?.findViewById(R.id.historyActionTitle) as TextView
val subtitle = parent?.findViewById(R.id.historyActionSubtitle) as TextView
var iconDrawable: Drawable? = null
when(actionData.type) {
ActionType.HEART -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.heart)
}
ActionType.LUNGS -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.lungs)
}
ActionType.TEMPERATURE -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.temperature)
}
}
icon.setImageDrawable(iconDrawable)
val titleString = actionData.title
titleString?.let {
title.text = titleString
}
val subtitleString = actionData.subtitle
subtitleString?.let {
subtitle.text = subtitleString
}
holder?.actionContainer?.addView(parent)
}
val notes = listAssets[position].notes
notesView?.text = notes
if (notes.length == 0) {
notesButton?.layoutParams?.width = 0
} else {
notesButton?.layoutParams?.width = toggleButtonWidth
}
if (expandedNotes.contains(sessionId)) {
notesView?.expandWithoutAnimation()
} else {
notesView?.collapseWithoutAnimation()
}
notesButton?.onClick {
notesView?.toggleExpansion()
}
}
data class ListAssets(val id: String,
val date: Date,
val location: String,
val notes: String,
val image: Drawable,
val data: ArrayList<ListData>)
data class ListData(val type: ActionType,
val title: String?,
val subtitle: String?)
override fun onViewRecycled(holder: HistoryListAdapter.ViewHolder?) {
super.onViewRecycled(holder)
if (holder != null) {
holder.actionContainer.removeAllViewsInLayout()
holder.actionContainer.removeAllViews()
val notesTextView = holder.notesTextView
if (notesTextView != null) {
if (notesTextView.expandedState) {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch == null) {
expandedNotes.add(sessionID)
}
}
} else {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch != null) {
expandedNotes.remove(sessionSearch)
}
}
}
}
}
}
First, you should probably not override onViewRecycled() unless you have to perform some very particular resources cleanup.
The place where you want to setup your views before display is onBindViewHolder().
Second, you don't need not add or remove views dynamically in a RecyclerView item, it's simpler and more efficient to only switch the visibility of the view between VISIBLE and GONE. In cases where this is not enough because views are too different, you should declare different view types, which will create separate ViewHolders for each view type.
You should not remove or add any view while overriding onBindViewHoder() method of RecyclerView Adapter because next time when a recycled layout is used, the removed views will not be found. Instead of this you can use show/hide on a view.
If you add any view to the layout dynamically, later on when this layout is recycled, it also contains the extra view which you have added before.
Similarly, if you remove any view from the layout dynamically, later on when this layout is recycled, it does not contain the view which you have removed earlier.
I have implemented a RecyclerView and Retrofit,it has the SwipeView layout (Pull to Refresh).Here is the link to the repisitory.
https://github.com/frankodoom/Retrofit-RecyclerVew