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

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.

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.

How to change the text on the second activity on button clicked from first activity? [KOTLIN]

I have to implement OTP validation on my application, I just want to get the mobile number from the previous activity and set it to the textview on the next activity once I clicked the Next Button intent from Activity 1 to Activity 2.
Here is how it looks like in view:
I would like to put the mobile number below the please enter the otp that has sent to..
First try: I called the ObjectSingleton.mobileNum since it carries the mobile # then I used mobileNum.setText(ObjectSingleton.mobileNum) but it ddint work.
My codes in 1st Activity where intent is happening to go to the next Activity;
class MobileNumberActivity : AppCompatActivity(), OtpInterface.MobileNumberViews {
lateinit var presenterMobileNumber:MobileNumberPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mobile_number)
presenterMobileNumber = MobileNumberPresenter()
presenterMobileNumber.mobileViews = this
close_icon2.setOnClickListener {
finish()
}
nxtBtn.setOnClickListener {
if (inputText.text.isNullOrEmpty()) {
inputText.error = "Please enter valid mobile number."
} else if (inputText.text.toString().length != 10) {
inputText.error = "Please enter valid mobile number."
} else{
val mobile = inputText.text.toString()
presenterMobileNumber.otpMobile(mobile)
}
}
}
override fun ifFailed(msg: String) {
errorMsg.setText(msg)
}
override fun ifSuccess(res: OtpData) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
startActivity(intent)
}
Here are the codes of the 2nd Acitivy where I want to change the text of mobile number display
class OtpValidationActivity : AppCompatActivity(), OtpValidateInterface.OtpValidateViews {
lateinit var otpPresenter: OtpPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_otp_verification)
otpPresenter = OtpPresenter()
otpPresenter.otpValidationViews = this
verifyOtpBtn.setOnClickListener {
if (otp_input.text.isNullOrEmpty()) {
otp_input.error = "Please enter OTP."
} else{
val otpNumber = otp_input.text.toString()
otpPresenter.otpValidate(ObjectSingleton.mobileNum,otpNumber)
}
}
}
override fun validateFailed(res: String) {
resend.setText("Failed")
}
override fun validateSuccess(msg: OtpValidationData) {
resend.setText("Success")
}
For your REFERENCE here's the XML file of the said PICTURE above where should I put the Mobile number.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/rectangle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/linearLayout6"
android:layout_width="372dp"
android:layout_height="wrap_content"
android:background="#drawable/otpbg"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.487"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.041">
<LinearLayout
android:id="#+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_30sdp"
android:text="OTP VERIFICATION"
android:textColor="#color/reply_black_800"
android:textSize="#dimen/_22sdp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</TextView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please enter the OTP that has sent to">
</TextView>
<TextView
android:id="#+id/mobileNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_22sdp"
android:layout_marginBottom="#dimen/_30sdp"
android:text="#string/_63"
android:textColor="#color/colorPrimary"
android:textSize="#dimen/_19sdp"
android:textStyle="bold">
</TextView>
</LinearLayout>
<com.mukesh.OtpView
android:id="#+id/otp_input"
android:layout_width="267dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:layout_marginLeft="#dimen/_10sdp"
android:layout_marginRight="#dimen/_10sdp"
android:inputType="number"
android:textAppearance="?attr/textAppearanceHeadline5"
android:textColor="#android:color/black"
app:OtpItemCount="6"
app:OtpItemWidth="#dimen/_30sdp"
app:OtpLineColor="#color/reply_black_800"
app:OtpViewType="line" />
<TextView
android:id="#+id/resend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_19sdp"
android:gravity="center"
android:textSize="#dimen/_14sdp"
android:text="#string/didn_t_recieve_the_code_resend_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/otp_view"
app:layout_constraintVertical_bias="0.039"
android:paddingBottom="#dimen/_30sdp"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/verifyOtpBtn"
android:layout_width="378dp"
android:layout_height="42dp"
android:background="#drawable/radius_btn"
android:backgroundTint="#color/colorPrimary"
android:text="Next"
android:textAllCaps="false"
android:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.484"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/linearLayout6"
app:layout_constraintVertical_bias="0.117" />
</androidx.constraintlayout.widget.ConstraintLayout>
I am new to Kotlin/Android Develepment I hope someone help me with this.
#JUniorDEV
UPDATE: Imma share to you my MobileNumberPresenter codes:
var mobileViews:OtpInterface.MobileNumberViews? = null
override fun otpMobile(mobile: String) {
Fuel.post("https://api.staging.riderko.com/riderko_be/public/api/riderSendRegisterOtp", listOf(
"mobile" to mobile
)).timeout(5000)
.header("Accept", "application/json")
.responseObject<OtpResponse>{request, response, result ->
when (result) {
is Result.Failure -> {
mobileViews?.ifFailed(result.error.response.statusCode.toString())
}
is Result.Success -> {
val (bytes, error) = result
if (bytes != null) {
val status = bytes.success
if (status){
mobileViews?.ifSuccess(bytes.data)
}else{
mobileViews?.ifFailed(bytes.message)
}
}
}
}
}
}
in OtpInterface.MobileNumberViews
add second parameter as string to ifSuccess
in MobileNumberPresenter
change mobileViews?.ifSuccess(bytes.data) to mobileViews?.ifSuccess(bytes.data, mobile)
and you ifSuccess method will be changed to
override fun ifSuccess(res: OtpData, mobileNum: String) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
intent.putExtra("MOBILE_NUMBER",mobileNum)
startActivity(intent)
}
And in OtpValidationActivity
val mobileNum = arguments?.getString("MOBILE_NUMBER")
Update the code of 1st activity to
override fun ifSuccess(res: OtpData) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
startActivity(intent)
intent.putExtra("MobileNum",mobile)
}
Now in the second activity after setContentView(R.layout.activity_otp_verification)
add this line:
val MobNo = intent.getExtra("MobileNum")
mobileNum.text = MobNo
Hope this will work
Here in the MobileNumberActivity inside the ifSuccess() before you do startActivity(intent) do intent.putExtra("IDENTIFER",mobileNum)
Now in the 2nd activity simply do val mobNO = intent.getStringExtra("IDENTIFER")
and now assign the mobNo to the desired textView.
This helps pass data between activities.
I was able to solve this simply by calling ObjectSingleton.mobNum assigned to a variable and displayed it into view I just learnt that once objectsingleton has been assigned to, the value will be carried over to any part of your app, this value will be removed automatically when you close your app.

Data binding + LiveData is not working with complex nested objects

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

Trying to display an entire collection from my Firestore database using RecyclerView and Fragments - Kotlin

I have tried many different tutorials and haven't been able to relate any to my application. My application in a gist displays a user's medication that they are taking. Here is my data class...
import java.util.HashMap
class LocalMedication {
var m_medicationName: String? = null
var m_medicationQty: String? = null
var m_medicationType: String? = null
var m_medicationExpDate: String? = null
var m_medicationStatus: Boolean = false
constructor() {}
constructor(medicationName: String, medicationQty: String, medicationType: String, medicationExpDat : String, medicationStatus : Boolean) {
this.m_medicationName = medicationName
this.m_medicationType = medicationType
this.m_medicationQty = medicationQty
this.m_medicationExpDate = medicationExpDat
this.m_medicationStatus = medicationStatus
}
fun toMap(): Map<String, Any> {
val result = HashMap<String, Any>()
result.put("medicationName", m_medicationName!!)
result.put("medicationType", m_medicationType!!)
result.put("medicationQty", m_medicationQty!!)
result.put("medicationExpDate", m_medicationExpDate!!)
result.put("medicationStatus", m_medicationStatus!!)
return result
}
}
Here is my view holder class
package com.example.home_med.viewHolder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.home_med.R
class medicationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var medicationName: TextView
var medicationType: TextView
var medicationQty: TextView
init {
medicationName = view.findViewById(R.id.rv_medicationName)
medicationType = view.findViewById(R.id.rv_medicationType)
medicationQty = view.findViewById(R.id.rv_medicationQty)
}
}
Here is my fragment
class LocalMedication : Fragment() {
private var adapter: FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>? = null
private var firestoreDB: FirebaseFirestore? = null
private var firestoreListener: ListenerRegistration? = null
private var medList = mutableListOf<LocalMedication>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
firestoreDB = FirebaseFirestore.getInstance()
val recyclerView = recyclerview as? RecyclerView
val mLayoutManager = LinearLayoutManager(context)
recyclerView?.layoutManager = mLayoutManager
recyclerView?.itemAnimator = DefaultItemAnimator()
loadMedication()
firestoreListener = firestoreDB!!.collection("notes")
.addSnapshotListener(EventListener { documentSnapshots, e ->
if (e != null) {
Log.e(TAG, "Listen failed!", e)
return#EventListener
}
medList = mutableListOf()
if (documentSnapshots != null) {
for (doc in documentSnapshots) {
val note = doc.toObject(LocalMedication::class.java)
note.m_medicationName = doc.id
medList.add(note)
}
}
adapter!!.notifyDataSetChanged()
recyclerView?.adapter = adapter
})
}
override fun onDestroy() {
super.onDestroy()
firestoreListener!!.remove()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentLocalMedicationBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_local_medication, container, false)
binding.viewMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToViewMedication())
}
binding.addMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToAddMedication())
}
binding.homeButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToHome2())
}
setHasOptionsMenu(true)
return binding.root
}
private fun loadMedication() {
val query = firestoreDB!!.collection("notes")
val response = FirestoreRecyclerOptions.Builder<LocalMedication>()
.setQuery(query, LocalMedication::class.java)
.build()
adapter = object : FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>(response) {
override fun onBindViewHolder(holder: medicationViewHolder, position: Int, model: LocalMedication) {
val note = medList[position]
holder.medicationName.text = note.m_medicationName
holder.medicationType.text = note.m_medicationType
holder.medicationQty.text = note.m_medicationQty
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): medicationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return medicationViewHolder(view)
}
override fun onError(e: FirebaseFirestoreException) {
Log.e("error", e!!.message)
}
}
adapter!!.notifyDataSetChanged()
recyclerview?.adapter = adapter
}
public override fun onStart() {
super.onStart()
adapter!!.startListening()
}
public override fun onStop() {
super.onStop()
adapter!!.stopListening()
}
}
Here is my recyclerViewItem XML file
<LinearLayout 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"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/rv_medicationName"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationQty"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toEndOf="#+id/rv_medicationName"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationType"
style="#style/word_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/rv_medicationQty"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Here is my localMedications XML file
<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"
tools:context=".LocalMedication">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:text="LOCAL MEDICATION"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.478"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="543dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="#android:color/darker_gray"
app:layout_constraintBottom_toTopOf="#+id/homeButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView"
tools:listitem="#layout/recyclerview_item" />
<Button
android:id="#+id/viewMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100dp"
android:layout_marginBottom="100dp"
android:text="View Med"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="#+id/addMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:layout_marginBottom="100dp"
android:text="Add Medication"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/homeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="59dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="103dp"
android:text="Home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/viewMedicationButton"
app:layout_constraintStart_toEndOf="#+id/addMedicationButton"
app:layout_constraintTop_toBottomOf="#+id/addMedicationButton"
app:layout_constraintVertical_bias="0.972" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Here is what my database looks like...
Datatbase
Any help or guidance would be great. It runs the application, but in the Logs, it says "RecyclerView: No adapter attached; skipping layout"
The problem in your code lies in the fact that the names of the fields in your LocalMedication class are different than the name of the properties in your database. You have in your LocalMedication class a field named m_medicationName while in your database I see it as medicationName and this is not correct. The names must match. Behind the scene, Kotlin is creating a Java class with a getter named getM_medicationName() so Firebase is looking in the database for a field named m_medicationName and not medicationName.
There are two ways in which you can solve this problem. The first one would be to remove the data in your database and add it again using field names (m_medicationName, m_medicationQty etc) that exist in your LocalMedication class.
If you are not allowed to use the first solution, then the second approach will be to use annotations in front of your public fields. So you should use the PropertyName annotation in front of every field. So in your LocalMedication class, a field should look like this:
#get:PropertyName("medicationName")
#set:PropertyName("medicationName")
public var m_medicationName: String? = null
As explained in my answer from the following post:
I am trying to get the correct reference to my Firebase Database child and set the values in my RecyclerView but I am having some issues
It's for Firebase realtime database but the same rules apply to Cloud Firestore.

Android Kotlin Calling ViewModel function from View with Parameters

I am building an Android app.
I have a layout that contains a button 'saveButton'. When the user clicks the button, the onClick should call a function in my ViewModel, onSave(). This function requires 2 parameters: the text contents of 2 EditText views that are also present in the same layout.
Basically, the user has edited the name and/or the synopsis and now wants to have the ViewModel update the object's data in the database.
(Part of) my UI (.xml fragment layout):
<?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"
tools:context=".ui.CreateEditRelationFragment">
<data>
<variable
name="createEditRelationViewModel"
type="be.pjvandamme.farfiled.viewmodels.CreateEditRelationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/saveButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/saveText"
android:onClick="#{() -> createEditRelationViewModel.onSave(relationNameEditText.getEditText().getText().toString(), relationSynopsisEditText.getEditText().getText().toString())}" />
<Button
android:id="#+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="#string/cancelText"
android:onClick="#{() -> createEditRelationViewModel.onCancel()}" />
<EditText
android:id="#+id/relationSynopsisEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine" />
<EditText
android:id="#+id/relationNameEditText"
android:layout_width="#dimen/relationNameEditWidth"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
(Part of) the ViewModel:
class CreateEditRelationViewModel (
private val relationKey: Long?,
val database: RelationDao,
application: Application
): AndroidViewModel(application){
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var relation = MutableLiveData<Relation?>()
private var _navigateToRelationDetail = MutableLiveData<Boolean>()
val navigateToRelationDetail: LiveData<Boolean>
get() = _navigateToRelationDetail
fun onSave(name: String, synopsis: String){
Timber.i("Got name: " + name + " and synopsis: " + synopsis)
if(relationKey == null){
uiScope.launch{
relation.value = Relation(0, name, synopsis, false)
insert(relation.value!!)
}
}
else{
uiScope.launch{
relation.value?.name = name
relation.value?.synopsis = synopsis
update(relation.value)
}
}
_navigateToRelationDetail.value = true
}
private suspend fun insert(newRelation: Relation){
withContext(Dispatchers.IO){
database.insert(newRelation)
}
}
private suspend fun update(relation: Relation?){
if(relation != null) {
withContext(Dispatchers.IO) {
database.update(relation)
}
}
}
}
I would want this thing to compile so that the onSave() function is called and the contents of the EditTexts passed as parameters.
I cannot manage to pass the text contents of these EditTexts. The compiler throws this error:
[databinding] {"msg":"cannot find method getEditText() in class android.widget.EditText","file":"D:\\ etc.
It does the same thing when I try to access using the .text property directly.
Does anyone know what I'm doing wrong? I'm tearing my hair out.
getEditText() method doesn't exist and you can remove it.
instead of;
relationNameEditText.getEditText().getText().toString()
you can do
relationNameEditText.getText().toString()`
i.e.
https://github.com/dgngulcan/droid-feed/blob/e0d0d5f4af07c5375d42b74e42c55b793319a937/app/src/main/res/layout/fragment_newsletter.xml#L120

Categories

Resources