Recycler View recycle issue - android-recyclerview

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

Related

Jetpack Compose Textfield value not getting updated

I'm trying to create a display where users can input a duration composed of hours, minutes and seconds. I manage the state of the duration with a class i wrote called TimeData. I set the value for the textfield to the state values, however this does not get updated when it changes. I have tried alot and i can't seem to figure out why this does not work, as similar implementations work just fine.
I save the state in viewmodel, inject it into the screen (composable) the screen passes fields of the TimeData to the three composables for the hours, seconds and minutes. These components handle displaying the textfields and changing the values
I've tried changing state directly, saving state in screen file with by remember instead of in viewmodel with State, i've tried changing val to var and back in the TimeData object and it's children. And much more.
p.s. this is my first question here, so if anything is not clear, please let me know.
TimeData class (saved as state in viewmodel)
data class TimeData(
val hours: TimeUnit = TimeUnit(TimeUnits.HOURS),
val mins: TimeUnit = TimeUnit(TimeUnits.MINS),
val secs: TimeUnit = TimeUnit(TimeUnits.SECS),
) {
fun isDataEmpty() = hours.value == 0L && mins.value == 0L && secs.value == 0L
}
fun TimeData.toISOString(): String = "PT" +
"${hours.value}${hours.unit.firstLetter}" +
"${mins.value}${mins.unit.firstLetter}" +
"${secs.value}${secs.unit.firstLetter}"
fun TimeData.toDuration(): Duration = Duration.parse(this.toISOString())
TimeUnit class (this value is updated)
data class TimeUnit(
var unit: TimeUnits
) {
var value: Long = 0
set(value) {
val parsedValue = value
val min = 0
val max = when (unit) {
TimeUnits.HOURS -> 99
TimeUnits.MINS -> 60
TimeUnits.SECS -> 60
}
if (parsedValue in min..max) {
field = value
}
}
override fun toString(): String {
return if (value in 1..9) "0$value" else value.toString()
}
}
enum class TimeUnits(val firstLetter: String) {
HOURS("h"),
MINS("m"),
SECS("s")
}
State in viewmodel
private val _timeState = mutableStateOf(TimeData())
val timeState: State<TimeData> = _timeState
Texfield
BasicTextField(
modifier = Modifier.widthIn(1.dp),
value = time.value.toString(),
onValueChange = {
if (it.isNotBlank()) {
//changing state directly
time.value = it.toLong()
//letting viewmodel handel change
viewModel.onEvent(AddEditExerciseEvents.DurationValueChanged(time))
}
},
singleLine = true,
textStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Medium,
color = textColor,
textAlign = TextAlign.Center
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Changing state value in viewmodel
when (event) {
is AddEditExerciseEvents.DurationValueChanged -> {
when (event.value.unit) {
TimeUnits.HOURS -> {
_timeState.value = timeState.value.copy(
hours = event.value
)
}
TimeUnits.MINS -> {
_timeState.value = timeState.value.copy(
mins = event.value
)
}
TimeUnits.SECS -> {
_timeState.value = timeState.value.copy(
secs = event.value
)
}
}
}

Filter Observed ViewModel data for Spinner based on another Spinners selection (LiveData)

I have two Spinners, each populated by ArrayList, which is observed in the ViewModel from the Fragment as below:
InventoryAddEdit Fragment
// Observe ProductGroups and populate Spinner
businessViewModel.allAppDataProductGroups.observe(viewLifecycleOwner, { productGroupArrayList -> // ArrayList<ProductGroupObject>
if (!productGroupArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productGroupArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, { productTypeArrayList -> // ArrayList<ProductTypeObject>
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productTypeArrayList)
binding.inventoryAddEditProductTypeSpinner.adapter = adapter
binding.inventoryAddEditProductTypeSpinner.setSelection(17) // Sets default value
}
})
I am trying to avoid keeping data in the Fragment (MVVM), so I am wondering how best to filter the second ArrayList based on the selection of the first.
I thought I could use the onItemSelected method on the first spinner to cancel the observer and reattach, but then filter the newly observed ArrayList by a selection of the first spinner. However, this seems a bit clunky. Another idea was to create another filtered list in the ViewModel, but that will mean more data in the ViewModel.
Is there another option I am missing, please?
For info, the ProductGroupObject and ProductTypeObject look like this:
ProductGroupObject
#IgnoreExtraProperties
data class ProductGroupObject (
//ProductGroup fields (1 fields)
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productGroupID: String = ""
) : Serializable {
override fun toString(): String {
return productGroup
}
}
ProductTypeObject
#IgnoreExtraProperties
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
fun detailsText(): String {
val detailsString = StringBuilder()
if(productTypeID.isNotEmpty()) detailsString.append("$productTypeID\n")
if(productType.isNotEmpty()) detailsString.append("$productType\n")
if(productGroup.isNotEmpty()) detailsString.append("$productGroup\n")
return detailsString.toString()
}
}
So the best solution I came up with was to create a 'full list' and 'filtered list' for each Spinner data set in the ViewModel + 'current selection' object for each Spinner (also kept in the ViewModel).
The 'full list' is populated by the Cloud Database on startup, the 'filtered lists' are filtered depending on the Spinner selection by way of the following code:
binding.inventoryAddEditProductGroupSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent:AdapterView<*>?, view: View?, position: Int, id: Long){
val productGroupObject = parent?.selectedItem as ProductGroupObject
if (productGroupObject.productGroupID.isNotEmpty()){
businessViewModel.updateCurrentProductGroupVMLiveData(productGroupObject.productGroupID)
}
}
This updates the 'filtered lists' (filtering the 'full list') which hold data to any of the linked Spinners as below:
fun updateCurrentProductGroupVMLiveData (currentProductGroupId: String) {
val newProductGroup = allAppDataProductGroups.value?.find { productGroup -> productGroup.productGroupID == currentProductGroupId }
_currentProductGroup.value = newProductGroup
if(newProductGroup?.productGroup != null) {
val filteredProductsList = allAppDataProductTypes.value?.filter { productTypeObject -> productTypeObject.productGroup == newProductGroup.productGroup} as ArrayList<ProductTypeObject>
_filteredAppDataProductTypes.value = filteredProductsList
}
// UPDATE OTHER SPINNERS HERE
Log.d(TAG, "updateCurrentProductGroupVMLiveData(): '_currentProductGroupId.value' updated ($currentProductGroupId)")
}
One issue I faced was that the View was nullable because of how LiveData works, which was solved in the following post: Spinner Listener LiveData Issue

tornadofx listview is creating one additional null listcellfragment than items in the list

I have a ViewModel for a ListView with 3 players in it:
object PlayerListViewModel : ViewModel() {
lateinit var players : ObservableList<Player>
init{
}
fun loadPlayers(){
players = Engine.selectedGame.players.asObservable()
}
}
class PlayerListView : View() {
private val vm = PlayerListViewModel
override val root = VBox()
init {
vm.loadPlayers()
root.replaceChildren {
style {
spacing = 25.px
alignment = Pos.CENTER
padding = box(0.px, 15.px)
}
listview(vm.players){
style{
background = Background.EMPTY
prefWidth = 300.px
}
isFocusTraversable = false
isMouseTransparent = true
cellFragment(PlayerCardFragment::class)
}
}
}
}
For some reason the listview is creating 4 PlayerCardFragments, with the first having a null item property and the last 3 having the correct Player item reference. This is the PlayerCardFragment definition:
class PlayerCardFragment : ListCellFragment<Player>() {
private val logger = KotlinLogging.logger { }
private val vm = PlayerViewModel().bindTo(this)
private lateinit var nameLabel : Label
private lateinit var scoreLabel : Label
override val root = hbox {
addClass(UIAppStyle.playerCard)
nameLabel = label(vm.name) { addClass(UIAppStyle.nameLabel) }
scoreLabel = label(vm.score) { addClass(UIAppStyle.scoreLabel) }
}
init {
logger.debug { "Initializing fragment for ${this.item} and ${vm.name.value}" }
EventBus.channel(EngineEvent.PlayerChanged::class)
.observeOnFx()
.subscribe() {
vm.rollback() //force viewmodel (PlayerViewModel) refresh since model (Player) does not implement observable properties
logger.debug { "${vm.name.value}'s turn is ${vm.myTurn.value}" }
root.toggleClass(UIAppStyle.selected, vm.myTurn)
}
}
When running the application, the PlayerCardFragment initializations print out "Initializing fragment for null and null" four times, but the list appears perfectly correctly with the 3 Player items. Later during execution, wnen there is an Engine.PlayerChanged event received, the Oberver function prints:
"null's turn is false"
"Adam's turn is false"
"Chad's turn is true"
"Kyle's turn is false"
These are the correct players, with the correct turn statuses. The listview appears perfectly well with the styling changes. I'm just not sure where that first null ListCellFragment is coming from.
It seems like you're trying to give ItemViewModel functionality to a view model by having everything around it change. Why not change the PlayerViewModel to have the functionality instead? Easiest way to imagine is to create bindings out of generic properties, then have them all changed and committed with by listening to the itemProperty:
class Player(var foo: String?, var bar: Int?)
class PlayerViewModel() : ItemViewModel<Player>() {
val foo = bind { SimpleStringProperty() }
val bar = bind { SimpleIntegerProperty() }
init {
itemProperty.onChange {
foo.value = it?.foo
bar.value = it?.bar
}
}
override fun onCommit() {
item?.let { player ->
player.foo = foo.value
player.bar = bar.value?.toInt()
}
}
}
Is it pretty? No. Does it keep you from having to implement an event system? Yes.

How do I bind a custom property to a textfield bidirectionally?

I have a complex object that I want to display in a textfield. This is working fine with a stringBinding. But I don't know how to make it two-way so that the textfield is editable.
package com.example.demo.view
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
class MainView : View("Hello TornadoFX") {
val complexThing: Int = 1
val complexProperty = SimpleObjectProperty<Int>(complexThing)
val complexString = complexProperty.stringBinding { complexProperty.toString() }
val plainString = "asdf"
val plainProperty = SimpleStringProperty(plainString)
override val root = vbox {
textfield(complexString)
label(plainProperty)
textfield(plainProperty)
}
}
When I run this, the plainString is editable and I see the label change because the edits are going back into the property.
How can I write a custom handler or what class do I need to use to make the stringBinding be read and write? I looked through a lot of the Property and binding documentation but did not see anything obvious.
Ta-Da
class Point(val x: Int, val y: Int) //You can put properties in constructor
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
if(string.isNullOrBlank()) return null //Empty strings aren't valid
val xy = string.split(",", limit = 2) //Only using 2 coordinate values so max is 2
if(xy.size < 2) return null //Min values is also 2
val x = xy[0].trim().toIntOrNull() //Trim white space, try to convert
val y = xy[1].trim().toIntOrNull()
return if(x == null || y == null) null //If either conversion fails, count as invalid
else Point(x, y)
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6) //Probably doesn't need to be its own member
val pointProperty = SimpleObjectProperty<Point>(point)
val pc = PointConverter()
override val root = vbox {
label(pointProperty, converter = pc) //Avoid extra properties, put converter in construction
textfield(pointProperty, pc)
}
}
I made edits to your converter to "account" for invalid input by just returning null. This is just a simple band-aid solution that doesn't enforce correct input, but it does refuse to put bad values in your property.
This can probably be done more cleanly. I bet there is a way around the extra property. The example is fragile because it doesn't do input checking in the interest of keeping it simple. But it works to demonstrate the solution:
class Point(x: Int, y: Int) {
val x: Int = x
val y: Int = y
}
class PointConverter: StringConverter<Point?>() {
override fun fromString(string: String?): Point? {
val xy = string?.split(",")
return Point(xy[0].toInt(), xy[1].toInt())
}
override fun toString(point: Point?): String {
return "${point?.x},${point?.y}"
}
}
class MainView : View("Hello TornadoFX") {
val point = Point(5, 6)
val pointProperty = SimpleObjectProperty<Point>(point)
val pointDisplayProperty = SimpleStringProperty()
val pointStringProperty = SimpleStringProperty()
val pc = PointConverter()
init {
pointDisplayProperty.set(pc.toString(pointProperty.value))
pointStringProperty.set(pc.toString(pointProperty.value))
pointStringProperty.addListener { observable, oldValue, newValue ->
pointProperty.set(pc.fromString(newValue))
pointDisplayProperty.set(pc.toString(pointProperty.value))
}
}
override val root = vbox {
label(pointDisplayProperty)
textfield(pointStringProperty)
}
}

TornadoFX how to add validation while editing TableView

Consider folowing example:
class Item(name: String, number: Int) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val numberProperty by lazy { SimpleIntegerProperty(number) }
var number by numberProperty
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).makeEditable(NumberStringConverter())
enableCellEditing()
}
}
}
How can I add a validator while editing cells? Is the only way to do that is to add rowExpander with some textfield and try to validate a model there?
You can either implement your own cellfactory and return a cell that shows a textfield bound to a ViewModel when in edit mode and an label if not. Alternatively, if you're fine with always displaying a textfield, you can use cellFormat and bind the current item to an ItemModel so you can attach validation:
class ItemModel(item: Item) : ItemViewModel<Item>(item) {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFormat {
val model = ItemModel(rowItem)
graphic = textfield(model.number, NumberStringConverter()) {
validator {
if (model.number.value == 123) error("Invalid number") else null
}
}
}
}
}
}
It will look like this:
While it works, it's sort of wasteful since the nodes are recreated frequently. I would recommend approach number one if performance is a concern, until we get cellFragment support for TableView like we have for ListView.
EDIT: I implemented cellFragment support, so it's possible to create a more robust solution which will show a label when not in edit mode and a validating textfield when you enter edit mode.
class ItemModel : ItemViewModel<Item>() {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFragment(NumberEditor::class)
}
}
}
class NumberEditor : TableCellFragment<Item, Number>() {
// Bind our ItemModel to the rowItemProperty, which points to the current Item
val model = ItemModel().bindToRowItem(this)
override val root = stackpane {
textfield(model.number, NumberStringConverter()) {
removeWhen(editingProperty.not())
validator {
if (model.number.value == 123L) error("Invalid number") else null
}
// Call cell.commitEdit() only if validation passes
action {
if (model.commit()) {
cell?.commitEdit(model.number.value)
}
}
}
// Label is visible when not in edit mode, and always shows committed value (itemProperty)
label(itemProperty) {
removeWhen(editingProperty)
}
}
// Make sure we rollback our model to avoid showing the last failed edit
override fun startEdit() {
model.rollback()
}
}
This will be possible starting from TornadoFX 1.7.9.