Kotlin: unresolved reference for Updating UI using Observables - android

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.

Related

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

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.

Android Two-Way Data Binding with Double (Kotlin)

I have a ViewModel class defined as follows:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
}
That is bound to the following layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="it.kfi.lorikeetmobile.extras.Converter" alias="Converter"/
<variable
name="viewModel"
type="it.kfi.lorikeetmobile.stock.models.StockLoadTaskModel" />
<variable
name="view"
type="it.kfi.lorikeetmobile.stock.ui.movements.StockLoadTaskFragment
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_item_code"
android:text="#={viewModel.itemCode}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={Converter.doubleToString(d)}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_note"
android:lines="3"
android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls"
android:gravity="top"
android:inputType="textMultiLine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_note"
android:text="#={viewModel.selectedItem.detail.note}"/>
</com.google.android.material.textfield.TextInputLayout>
...
</LinearLayout>
And I have also the following Converter object:
object Converter {
#JvmStatic
#InverseMethod("stringToDouble")
fun doubleToString(value: Double?): String? {
if (value == null) {
return null
}
return DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).format(value)
}
#JvmStatic
fun stringToDouble(value: String?): Double? {
if (value == null) {
return null
}
val v = DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).parse(value)
return v.toDouble()
}
}
If I set: android:text="#={Converter.doubleToString(d)}" (two-way databinding), in the EditText with id et_quantity I get the following error:
...error: cannot find symbol
If I change it into a one-way databinding like: android:text="#{Converter.doubleToString(d)}", it works. It looks like the binding manager is not able to recognize the inverse method.
Can anybody help me? Thank you.
Why the error happens?
When you define two-way data binding like you have in your example android:text="#={Converter.doubleToString(d)}" the question is: what function/object will receive data that you get back passed from EditText as user types data in? Should data be passed to Converter.doubleToString or maybe some other static function of Converter? Maybe to the result of Converter.doubleToString(d) or to d variable?
You must be precise.
You expect it is d, the compiler expects it is the result of Converter.doubleToString(d). Actually, neither will work.
Another issue is that EditText does operate with characters. It knows nothing about double, int, float, byte, short, boolean or anything else that is not a string.
It means that in order to implement two-way data binding your source:
must return value of type String;
must be assignable.
How to fix the issue?
Android architecture components introduce us with ObservableField class. There are ready to use ObservableBoolean, ObservableChar, ObservableFloat and a few others. If you open the link from the previous sentence you should see all of the classes Observable... on the left pane.
There is no ObservableString but ObservableField accepts a generic type. So you can define a variable that is a part of data binding to be ObservableField<String>("defaultValueHere").
So what you should have is:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable = ObservableField<String>(d.toString())
}
The dataBindingVariable will always return you the contents of an EditText you bound it to. You can get that value and safely convert to double.
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable =
object: ObservableField<String>(d.toString()) {
override fun set(value: String?) {
super.set(value)
// a value has been set
d = value.toDoubleOrNull() ?: d
}
}
}
Layout declaration will look like that for input field:
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={viewModel.dataBindingVariable}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
And there will be no need for object Converter.
There is another way of doing two-way data binding I'm not talking about here because it was already answered. Here it is.

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

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