Data binding + LiveData is not working with complex nested objects - android

I have encountered some unexpected behaviour with LiveData and data binding libraries.
I had implemented CustomLiveData as in this answer https://stackoverflow.com/a/48194074/13321296, so I just can call notifyChange() inside parent class to update UI.
I have parent object(some methods omitted for brevity):
class Day(val tasks: MutableList<RunningTask>,
state: DayState = DayState.WAITING,
var dayStartTime: Long = 0L,
currentTaskPos: Int = 0): BaseObservable() {
var state: DayState = state
set(value) {
field = value
notifyChange()
}
var currentTaskPos: Int = currentTaskPos
set(value) {
field = value
notifyChange()
}
fun start() {
dayStartTime = System.currentTimeMillis()
state = DayState.ACTIVE
resetTasks()
tasks[currentTaskPos].start()
notifyChange()
}
}
Child object:
class RunningTask(
startTime: Long,
var name: String = "",
private val originalDuration: Long = 0L,
val sound: String
): BaseObservable() {
var startTime: Long = startTime
set(value) {
field = value
uiStartTime = convertMillisToStringFormat(value)
}
#Bindable
var uiStartTime: String = convertMillisToStringFormat(startTime)
set(value) {
field = value
notifyPropertyChanged(BR.uiStartTime)
}
var duration: Long = originalDuration
set(value) {
field = value
}
var state: State = State.WAITING
var progress: Long = 0L
set(value) {
field = value
}
var timePaused: Long = 0L
var timeRemain: String = convertMillisToStringFormat(duration)
enum class State {
WAITING, ACTIVE, COMPLETED, DISABLED
}
fun start() {
state = State.ACTIVE
}
}
The problem is what data binding from item_main_screen_task.xml is not updated when I change items inside of Day's tasks field, e.g. calling method start(), but other fields, such as state, do update correctly, so I guess the problem is with list inside of it.
fragment_main_screen.xml, recyclerview is populated with Day class field tasks:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="viewmodel"
type="com.sillyapps.meantime.ui.mainscreen.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorPrimary">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/tasks"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="#{viewmodel.noTemplate ? View.GONE : View.VISIBLE}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="#+id/play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/constraintLayout"
tools:listitem="#layout/item_main_screen_task"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
item_main_screen_task, taskState attribute is just basically BindingAdapter what sets background drawable according to Day's state enum:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="task"
type="com.sillyapps.meantime.data.RunningTask" />
<variable
name="taskAdapterPosition"
type="Integer" />
<variable
name="clickListener"
type="com.sillyapps.meantime.ui.ItemClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:taskState="#{task.state}"
android:onClick="#{() -> clickListener.onClickItem(taskAdapterPosition)}">
<TextView
android:id="#+id/time"
style="#style/TimeItemStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="#{task.uiStartTime}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/enter_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="17:00" />
<TextView
android:id="#+id/enter_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="#{task.name}"
app:layout_constraintBottom_toBottomOf="#+id/time"
app:layout_constraintEnd_toStartOf="#+id/progress"
app:layout_constraintStart_toEndOf="#+id/time"
app:layout_constraintTop_toTopOf="#+id/time"
tools:text="Свободное время" />
<TextView
android:id="#+id/progress"
style="#style/TimeItemStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="#{task.timeRemain}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/enter_name"
app:layout_constraintTop_toTopOf="#+id/time"
tools:text="01:00" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Thanks in advance.

Turns out that solution was very simple, but somewhat unexpected
The child class should extend BaseObservable, and call notifyChange() on setters of every data-binded fields, something like that:
class RunningTask(
startTime: Long,
var name: String = "",
private val originalDuration: Long = 0L,
val sound: String
): BaseObservable() {
var startTime: Long = startTime
set(value) {
field = value
uiStartTime = convertMillisToStringFormat(value)
}
#Bindable
var uiStartTime: String = convertMillisToStringFormat(startTime)
set(value) {
field = value
notifyPropertyChanged(BR.uiStartTime)
}
var state: State = State.WAITING
set(value) {
field = value
notifyChange()
}
...
}
Appears that I'd already implemented this in uiStartTime before coming up with question, but I just didn't know exact reason why it's worked

Related

Can a Kotlin entity with a constructor that has default non null values become null at runtime?

I have spent the last couple of weeks trying to rewrite an android app from a java to kotlin, from custom fragment navigation to navigation component and all other Jetpack bells and whistles.
Now I've encountered several bugs through this process but there's this specific one. I have a kotlin class with a default constructor as shown below
#Entity(tableName = Globals.FIREBASE_ITEM_NODE)
#Parcelize
class Item(
#PrimaryKey(autoGenerate = true)
var id: Int = 0 ,
var imageUri: String = "",
var isRead: Boolean = false,
var expanded: Boolean = false,
var favourite: Boolean = false,
var isSaved: Boolean = false,
var englishWord: String = "",
var topic: String = "",
var audioUri: String = "",
var rutooroWord: String = "",
var firebaseImageNode: String = "",
) : Parcelable
This is because I fetch data from firebase rtdb and cache it in room. I then collect a flow of this data submit it to a List Adapter and use databinding to bind it to my views.
the item Viewholder
inner class ItemViewHolder(private val b: RvLangItemBinding) : RecyclerView.ViewHolder(b.root) {
fun bind(position: Int) {
if (getItem(position) !is Item) return
b.item = getItem(position) as Item
if ((getItem(position) as Item).expanded) createPalette(
(getItem(position) as Item).imageUri,
b.parent,
b.tvEnglish,
b.tvRutooro
)
else b.parent.setBackgroundColor(
itemView.context.resources.getColor(
R.color.transparent,
itemView.context.theme
)
)
b.root.setOnClickListener {
(getItem(position) as Item).expanded = !(getItem(position) as Item).expanded
notifyItemChanged(position)
if (prevPosition != INITIAL_POSITION && prevPosition != position) {
(getItem(prevPosition) as Item).expanded = false
notifyItemChanged(prevPosition)
}
prevPosition = position
if ((getItem(position) as Item).expanded && (getItem(position) as Item).audioUri.isNotEmpty())
AudioUtil.playAudioFile(
itemView.context,
Uri.parse((getItem(position) as Item).audioUri)
)
}
b.favourite.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
(getItem(position) as Item).favourite = true
onItem.update(getItem(position) as Item)
}
override fun unLiked(likeButton: LikeButton) {
(getItem(position) as Item).favourite = false
onItem.update(getItem(position) as Item)
}
})
b.audioButton.setOnClickListener {
if ((getItem(position) as Item).audioUri.isNotEmpty())
AudioUtil.playAudioFile(
itemView.context,
Uri.parse((getItem(position) as Item).audioUri)
)
}
}
}
and this is the xml for the item
<?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>
<import type="android.view.View" />
<variable
name="item"
type="com.allez.san.learnrutooro.models.Item" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="18dp"
android:elevation="4dp"
android:padding="8dp"
app:cardCornerRadius="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="#+id/tv_english"
style="#style/TextAppearance.Material3.TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="#{item.englishWord}"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="#id/favourite"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Good Morning" />
<com.like.LikeButton
android:id="#+id/favourite"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="4dp"
app:icon_size="25dp"
app:icon_type="star"
app:liked="#{item.favourite}"
app:layout_constraintBottom_toBottomOf="#+id/tv_english"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/tv_english"
app:like_drawable="#drawable/ic_star_green"
app:unlike_drawable="#drawable/ic_star_white" />
<RelativeLayout
android:id="#+id/relativeLayout"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="4dp"
android:background="#android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="#id/downArrow"
app:layout_constraintEnd_toStartOf="#id/downArrow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#id/downArrow" />
<com.google.android.material.imageview.ShapeableImageView
android:id="#+id/downArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:src="#drawable/ic_arrow_drop_down"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/favourite" />
<com.google.android.material.textview.MaterialTextView
android:id="#+id/tv_rutooro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="#{item.rutooroWord}"
android:textSize="16sp"
android:visibility="#{item.expanded? View.VISIBLE:View.GONE}"
app:layout_constraintBottom_toTopOf="#id/item_image"
app:layout_goneMarginBottom="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/downArrow"
tools:text="oraire ota" />
<com.google.android.material.imageview.ShapeableImageView
android:id="#+id/item_image"
setImage="#{item.imageUri}"
setImageItemVisibility="#{item}"
android:layout_width="270dp"
android:layout_height="200dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tv_rutooro"
app:layout_goneMarginBottom="16dp"
tools:src="#drawable/lr_logo_light" />
<com.google.android.material.imageview.ShapeableImageView
android:id="#+id/audio_button"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:src="#drawable/ic_audio"
android:visibility="#{item.expanded? View.VISIBLE:View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/downArrow"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
and these are my binding adapters
#BindingAdapter("setImage")
fun setImage(view:ImageView, uri: String)=
Glide.with(view).load(uri).into(view)
#BindingAdapter("setImageItemVisibility")
fun setItemImageVisibility(view:ImageView, item: Item){
view.visibility = if(item.expanded && item.imageUri.isNotEmpty()) View.VISIBLE else View.GONE
}
and this is the error I've been getting.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.allez.san.myapplication, PID: 26721
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter item
at com.allez.san.learnrutooro.utils.BindingUtilsKt.setItemImageVisibility(Unknown Source:7)
at com.allez.san.learnrutooro.databinding.RvLangItemBindingImpl.executeBindings(RvLangItemBindingImpl.java:152)
at androidx.databinding.ViewDataBinding.executeBindingsInternal(ViewDataBinding.java:512)
at androidx.databinding.ViewDataBinding.executePendingBindings(ViewDataBinding.java:484)
at androidx.databinding.ViewDataBinding$7.run(ViewDataBinding.java:218)
at androidx.databinding.ViewDataBinding$8.doFrame(ViewDataBinding.java:320)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1106)
at android.view.Choreographer.doCallbacks(Choreographer.java:866)
at android.view.Choreographer.doFrame(Choreographer.java:792)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1092)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
I/Process: Sending signal. PID: 26721 SIG: 9
How is this possible and why??? I've been at it for a while now. reading as much as I could about the subject but no luck yet.
Any help will be appreciated. thanx in advance.
try to change the fields type to nullable and run it again, if you did not have the same error then the problem is that you defined entity all its fields are non-nullable, so when you are calling #Query("SELECT * FROM items") fun getAllItems(): Flow<List<Item>> you are trying to give a null value from database to one of the fields.
I think the problem was with my binding adapters
#BindingAdapter("setImage")
fun setImage(view: ImageView, uri: String) =
Glide.with(view).load(uri).into(view)
#BindingAdapter("setImageItemVisibility")
fun setItemImageVisibility(view: ImageView, item: Item) {
view.visibility = if (item.expanded && item.imageUri.isNotEmpty()) View.VISIBLE else View.GONE
}
Because when i switched to directly binding the image and setting the image visibility in the item viewholder everything is working just fine.
Glide.with(itemView).load((getItem(position) as Item).imageUri).into(b.itemImage)
b.itemImage.visibility = if ((getItem(position) as Item).expanded && (getItem(position) as Item).imageUri.isNotEmpty()) View.VISIBLE else View.GONE
I don't think this is a solution to that error coz I still don't know why I was getting it and I'd like to still use databinding to set the image and its visibility. If anyone has an explanation as to why this was happening I'd really appreciate it.

Why can I bind either String or LiveData<string> varaiable to android:text in Android Studio?

I'm learning data binding, the following code is from the project.
The android:text in plain_activity_solution_3.xml bind to SimpleViewModel with name which is String.
The android:text in solution.xml bind to SimpleViewModelSolution with name which is LiveData<String>.
Why can either String or LiveData<string> be bind to android:text? In my mind , only one is allowed to be bind to android:text.
SimpleViewModel.kt
class SimpleViewModel : ViewModel() {
val name = "Grace"
val lastName = "Hopper"
var likes = 0
private set // This is to prevent external modification of the variable.
...
}
plain_activity_solution_3.xml
<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="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/plain_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="128dp"
android:text="#{viewmodel.name}"
...
}
SimpleViewModelSolution.kt
class SimpleViewModelSolution : ViewModel() {
private val _name = MutableLiveData("Ada")
private val _lastName = MutableLiveData("Lovelace")
private val _likes = MutableLiveData(0)
val name: LiveData<String> = _name
val lastName: LiveData<String> = _lastName
val likes: LiveData<Int> = _likes
...
}
solution.xml
<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="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModelSolution"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- A simple binding between a TextView and a string observable in the ViewModel -->
<TextView
android:id="#+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="128dp"
android:text="#{viewmodel.name}"
...
}
As the document says:
Any plain-old object can be used for data binding, but modifying the object doesn't automatically cause the UI to update. Data binding can be used to give your data objects the ability to notify other objects, known as listeners, when its data changes.
LiveData<string> is observable, too.
This is by design, and discussed in documentation. The binder will accept either the native data type, or something that's "Observable", like LiveData. If an observable object is provided, the binder will subscribe to the object's changes and bind those to the view, saving you lines of code.

Why doesn't android:text display latest LiveData value?

The following code is based the project.
I modified a few code.
The android:text="#{viewmodel.name}" displays the LiveData value of the name.
The fun onLike() will change LiveData value of the name.
I think android:text="#{viewmodel.name}" will display latest value "My new" after I click the button (android:id="#+id/like_button").
But in fact, android:text="#{viewmodel.name}" keep to display "Ada", why?
SimpleViewModelSolution.kt
class SimpleViewModelSolution : ViewModel() {
private var _name = MutableLiveData("Ada") // I modified from private val _name = MutableLiveData("Ada")
val name: LiveData<String> = _name
...
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
_name = MutableLiveData("My new") // I added
}
}
solution.xml
<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="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModelSolution"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{viewmodel.name}"
..."/>
<Button
android:id="#+id/like_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:onClick="#{() -> viewmodel.onLike()}"
android:text="#string/like"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Change your onLike() to this:
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
_name.value = "My new"
}
Also, you can declare _name as val instead of var

Kotlin: unresolved reference for Updating UI using Observables

I am learning kotlin and databinding for android. I am able to run function of databinding. While I am working with Observable, I am getting unresolve reference for BR.property
here is my model class:
data class FruitModel(var fruitImage: String?, var fruitName: String?) : BaseObservable() {
var imageUrl: String? = fruitImage
get() = field
set(value) {
field = value
notifyPropertyChanged(BR.imageUrl)
}
var nameValue: String? = fruitName
get() = field
set(value) {
field = value
notifyPropertyChanged(BR.fruitModel)
}
}
I am able to get BR.fruitModel instead of above two. Here is my xml:
<data>
<variable name="onClickItem"
type="com.wings.kotlintest1.interfaces.FruitAdapterInterface"/>
<variable name="fruitModel"
type="com.wings.kotlintest1.model.FruitModel"/>
<variable name="position"
type="int"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
card_view:cardCornerRadius="5dp"
android:onClick="#{() -> onClickItem.onClickItemListener(position)}">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="#+id/ivFruitImage"
android:layout_width="50dp"
android:layout_height="50dp"
app:loadImageWithGlide="#{fruitModel.fruitImage}"/>
<TextView
android:id="#+id/tvFruitName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:textColor="#color/colorAccent"
android:textSize="18sp"
android:text="#{fruitModel.fruitName}"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
what is reason that BR class is not generating properties? Am I doing
something wrong?
I think you need to use #get:Bindable
data class FruitModel(var fruitImage: String?, var fruitName: String?) : BaseObservable() {
#get:Bindable
var imageUrl: String? = fruitImage
get() = field
set(value) {
field = value
notifyPropertyChanged(BR.imageUrl) // **unresolved reference : BR.imageUrl**
}
#get:Bindable
var nameValue: String? = fruitName
get() = field
set(value) {
field = value
notifyPropertyChanged(BR.nameValue) // **unresolved reference : BR.nameValue**
}
}
I think you need to add the #Bindable property to the get() of those fields. See
https://developer.android.com/topic/libraries/data-binding/observability
Your full layout must be inside <layout> ... </layout>.
Also, try to clean and build your project.
You can also trace your wrong line in stacktrace under build window.

A Kotlin property is in looping when getting the value ($field)

When I bind the value in the 'name' property in xml, the getter seems to be in a loop and the value inside it, is concatenating in the screen until I stop the app.
1 - I don't know with sure yet if I need to use notifyPropertyChanged() or the anotations #set and #get;
2 - If I set the get without the concatenating string, it's works nicelly: get() = field;
3 - If I try to return the get value inside braces, the problem keeps to occour: get(){return "Field: $field"};
This is the model:
class ContactModel : BaseObservable(){
#set:Bindable
#get:Bindable
var name: String = ""
get() = "Field: $field"
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#set:Bindable
#get:Bindable
var email: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
#set:Bindable
#get:Bindable
var phone: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.phone)
}
}
Here's the activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var contactModel: ContactModel = ContactModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
contactModel = ContactModel(/*"Rômulo", "romulocoviel#gmail.com", "(19):98421-0821"*/)
contactModel.name = "Rômulo"
contactModel.email = "romulocoviel#gmail.com"
contactModel.phone = "(19):98421-0821"
binding.contactModel = contactModel
binding.setLifecycleOwner(this)
}
fun changeSignatures(view: View) {
Log.e("TESTING", "Testando!" + contactModel.name)
val nameList: ArrayList<ContactModel> = ArrayList()
contactModel.name = "asdasd"
contactModel.email = "asdasda"
contactModel.phone = "asdasd"
}
}
And here's the XML that I have a button that changes the values when tapped and the binding views:
<data>
<variable
name="contactModel"
type="com.example.romulo.bindingmetricsconversor.ContactModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:text="#={contactModel.name}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvName" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<TextView
android:text="#={contactModel.email}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvEmail" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvName" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<TextView
android:text="#={contactModel.phone}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvPhone" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvEmail" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<Button
android:text="Change"
android:layout_width="wrap_content"
android:layout_height="49dp"
android:id="#+id/btChange" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp" android:onClick="changeSignatures"/>
</android.support.constraint.ConstraintLayout>
The result in the screen always is:
Field: asdasd
Field:Field: asdasd
Field:Field: asdasd
Field:Field:Field: asdasd
Field:Field:Field:Field: asdasd
... to the infinite
Just for the sake of completeness:
it seems like whenever the text view is updated by the property change listener it detects a change in its own content and thus tries to save back to the observable, triggering a loop, since you're using two-way binding.
The problem can be solved by using one-way binding instead (#{}), as upon changing its text the text view would trigger its own listeners and attempt to modify the observable, sending it into an infinite recursion.

Categories

Resources