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
Related
I read a few posts to convert from String to Integer and tried to do the same to convert string to double using two-way binding but was unable to do it.
SampleViewModel.kt
class SampleViewModel: ViewModel() {
val weight = MutableLiveData<Double>()
fun validateFields() {
Log.i("SAMPLE_VIEW_MODEL", "validateFields: ${weight.value}")
}
}
TypeConverters.kt
object TypeConverters {
#InverseMethod("stringToDouble")
#JvmStatic
fun doubleToString(value: Double?): String {
return value?.toString() ?: ""
}
#JvmStatic
fun stringToDouble(value: String): Double? {
if (TextUtils.isEmpty(value)) {
return null
}
return value.toDoubleOrNull()
}
}
fragment_sample.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.sampleapp.SampleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/til_weight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/_10sdp"
android:hint="#string/hint_weight"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="#+id/tilName">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/tiet_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={TypeConverters.doubleToString(viewModel.weight)}"/>
</com.google.android.material.textfield.TextInputLayout>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The example above does not let me enter the proper value on UI like "56.78". I tried to follow this post but doesn't work for me. Another way is to take string value and then convert it into double and vice versa accordingly. I would like to know which is the correct way.
In your TextInputEditText, update this line
android:text="#={String.valueOf(viewModel.weight)}"
See Results
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/tiet_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={String.valueOf(viewModel.weight)}"/>
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
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.
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
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.