Referencing properties of Observable class in Android Data Binding layout - android

What is the type of the Observable class property which getter is annotated as #Bindable in the Android Data Binding framework?
For example, let the Observable class be defined as follows:
class Localization() : BaseObservable() {
var translation: (key: String) -> String by Delegates.observable(defaultTranslation) { _, _, _ ->
notifyPropertyChanged(BR.translation)
}
#Bindable get
}
The layout XML will be then something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="translation"
type="WHAT IS THE TYPE OF TRANSLATION?" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{translation.invoke(stringKey)}" />
</FrameLayout>
</layout>
The question is, what to put in the type attribute of variable "translation".
I've tried:
type="kotlin.jvm.functions.Function1<String, String>"
It compiles, but the TextView is not updated when translation property changes.
I can achieve the desired behavior by introducing localization variable in the layout XML and then calling localization.translation.invoke() in the binding expression. I am just not comfortable with this and want to know if I can reference translation directly.

The Localization extends BaseObservable while Function1 is not observable at all. So using the Localization gives you an interface for observing the changes to the properties.
If you bind the translation, it's a simple field that gets set. If you want to update it, you'd have to call setTranslation() again.

Related

How to pass a sealed class through data binding

I have the following sealed class:
sealed class Pot(
val ball: Ball,
val potType: PotType,
val potAction: PotAction
) {
class HIT(hitBall: Ball) : Pot(hitBall, PotType.HIT, PotAction.CONTINUE)
object SAFE : Pot(Ball.NOBALL, PotType.SAFE, PotAction.SWITCH)
object MISS : Pot(Ball.NOBALL, PotType.MISS, PotAction.SWITCH)
class FOUL(foulBall: Ball, foulAction: PotAction): Pot(foulBall, PotType.FOUL, foulAction)
class REMOVERED(removeBall: Ball): Pot(removeBall, PotType.REMOVERED, PotAction.CONTINUE)
object ADDRED: Pot(Ball.RED, PotType.ADDRED, PotAction.CONTINUE)
}
I want to pass this from the xml to the view model as such:
<data>
<import type="com.example.snookerscore.fragments.game.Pot"/>
// other variables
</data>
Then I use lambdas in the views I need to pass the information to the click handler:
<TextView
android:id="#+id/game_btn_act_safe"
style="#style/temp_btn"
android:onClick="#{() -> gameViewModel.updateFrame(Pot.SAFE)}"
// Other view Properties
/>
I get this error:
Could not find identifier 'Pot'. Check that the identifier is spelled correctly, and that no or tags are missing.
I've also tried importing Pot.SAFE directly, but it still doesn't work
Data binding uses Java to generate codes and in your XMLs you have to code in Java (e.g. ternary conditions are written with condition ? A : B instead of Kotlin if/else)
So you have to access those object using Java syntax, something like:
android:onClick="#{v -> gameViewModel.updateFrame(Pot.SAFE.INSTANCE)}"
used;
<variable
name = "sealedName"
type="com.example.snookerscore.fragments.game.Pot"/>
Removed
<import type="com.example.snookerscore.fragments.game.Pot"/>
final;
android:onClick="#{() -> gameViewModel.updateFrame(sealedName.SAFE)}"

Why doesn't data bing use LiveData or Observable fields?

In my mind, One-way or Two-way data bing use either LiveData or Observable fields.
The following code is from the project https://github.com/enpassio/Databinding
The attribute android:text="#={viewModel.toyBeingModified.toyName}" of the control android:id="#+id/toyNameEditText" bind to viewModel.toyBeingModified.toyName with Two-way data bing.
I'm very strange why viewModel.toyBeingModified is neither LiveData or Observable fields, could you tell me?
fragment_add_toy.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 class="AddToyBinding">
<variable
name="viewModel"
type="com.enpassion.twowaydatabindingkotlin.viewmodel.AddToyViewModel" />
<import type="com.enpassion.twowaydatabindingkotlin.utils.BindingUtils"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/margin_standard">
<androidx.cardview.widget.CardView
android:id="#+id/cardEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/margin_standard"
app:cardBackgroundColor="#color/skin_rose"
app:cardCornerRadius="#dimen/card_corner_radius"
app:cardElevation="#dimen/card_elevation"
app:contentPadding="#dimen/padding_standard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/toyNameLayout"
style="#style/Widget.Enpassio.TextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/toy_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="#+id/guidelineET"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteY="418dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/toyNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:text="#={viewModel.toyBeingModified.toyName}"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
AddToyViewModel.kt
class AddToyViewModel(private val mRepo: ToyRepository, private val chosenToy: ToyEntry?) : ViewModel() {
val toyBeingModified: ToyEntry
private var mIsEdit: Boolean = false
init {
if (chosenToy != null) {
//This is edit case
toyBeingModified = chosenToy.copy()
mIsEdit = true
} else {
/*This is for adding a new toy. We initialize a ToyEntry with default or null values
This is because two-way databinding in the AddToyFragment is designed to
register changes automatically, but it will need a toy object to register those changes.*/
toyBeingModified = emptyToy
mIsEdit = false
}
}
private fun insertToy(toy: ToyEntry) {
mRepo.insertToy(toy)
}
...
}
ToyEntry.kt
data class ToyEntry(
var toyName: String,
var categories: Map<String, Boolean>,
var gender: Gender = Gender.UNISEX,
var procurementType: ProcurementType? = null,
#PrimaryKey(autoGenerate = true) val toyId: Int = 0
): Parcelable{
/*This function is needed for a healthy comparison of two items,
particularly for detecting changes in the contents of the map.
Native copy method of the data class assign a map with same reference
to the copied item, so equals() method cannot detect changes in the content.*/
fun copy() : ToyEntry{
val newCategories = mutableMapOf<String, Boolean>()
newCategories.putAll(categories)
return ToyEntry(toyName, newCategories, gender, procurementType, toyId)
}
}
In fact, we use LiveData or Observable fields when we need to do something as soon as they changed, search bar can be a good example. But in this case, we don't care when the user is changing the properties of the selected toy (I haven't seen the UI but I'm assuming there is a Save button or something like that). In other words, we don't want to do anything while user is typing b, bo, boa and finally boat.
We just need that data to be once set while the viewmodel is set to binding, let the user change it to whatever and when we want to do the saving process, we want our field to be what user had entered.
In addition, if you use LiveData in your binding (as long as the lifecycleOwner is set) you're adding an observer to you LiveData which can be a point of concern for some geeks 😂.
TL;DR
We use LiveData when we want to observe it (which is not required in the example you provided). It's an option not a must. Data binding can set/get data for nearly everything.
I would suggest to start with 1-way data binding first and as soon as this works, extend it to 2-way data binding. What you are doing wrong right now is the following:
android:text="#={viewModel.toyBeingModified.toyName}"
This line of code means that you pass a ToyEntry object to a setText() method of the TextView. That means the TextView would need to have a method with the signature: setText(entry: ToyEntry).
Of course, this method does not exist (yet). So to make this data binding work, you have to define this method yourself by creating a BindingAdapter:
#BindingAdapter("toyEntry")
fun setToyEntry(textView: TextView, toyEntry: ToyEntry) {
// in here you define what to do with the textView. For example:
textView.text = toyEntry.toyName
}
You can create this BindingAdapter in any file without the need to put it into a class.
You can give this method any name you want
The first parameter of this method is the kind of View in the xml that you want to bind the toyEntry to
The second parameter of this method os the object that you set in your xml via #{...}
Now when you write a 1-way databinding like this: binding:toyEntry="#{viewModel.toyBeingModified.toyName}"
The binding namespace can be craeted by AndroidStudio automatically. You can name this anything you want (but not android, since this is already defined)
The toyEntry is what connects this line of xml to your BindingAdapter from the previous step (it corresponds to the same string that you set in the annotation #BindingAdapter(...)
Now, the generated code knows about your binding adapter and calls its method setToyEntry when it computes this data binding. You can also delete the line android:text="#={viewModel.toyBeingModified.toyName}", because it is not used anymore.
Go from there to setup 2-way data binding. Here you also have to create #InverseBindingAdapter as explained here: https://developer.android.com/reference/android/databinding/InverseBindingAdapter
Some more comments: Depending on your gradle version, you have to enable databinding and also make sure to have all dependencies and gradle plugins setup.
More on that here: https://developer.android.com/jetpack/androidx/releases/databinding?hl=en

Data Binding Compiler cannot find custom binding adapter provided

I have made a binding adapter available statically inside my Fragment which basically change my button appearance from "Stop" to "Play" and vice-versa.
companion object {
#BindingAdapter("playState")
fun Button.setPlayState(item: UIState) {
item.let {
if (it.isPlaying) {
setText("Stop")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorStop))
} else {
setText("Play")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorPlay))
}
}
}
}
Here is my layout file. I have provided a data class for it.
<?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>
<!-- stuff here -->
<variable
name="viewmodel"
type="com.mypackage.ui.ViewModel"/>
<variable
name="uistate"
type="com.mypackage.ui.UIState" />
</data>
<!-- layout, buttons, and more stuff here. Just pay attention to this following button -->
<Button
android:id="#+id/play_button"
android:layout_width="150sp"
android:layout_height="75sp"
android:layout_marginTop="20sp"
android:onClick="#{() -> viewmodel.onPlayClicked()}"
android:text="#string/play_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/minus_layout"
app:layout_constraintVertical_bias="0.026"
app:playState="#{uistate}"/>
</layout>
UIState itself is pretty self-explanatory.
data class UIState(var isPlaying: Boolean)
and the () -> viewmodel.onPlayClicked() flips the Boolean at UIState.
After compiling, Data Binding Compiler throws this error:
Cannot find a setter for <android.widget.Button app:playState>
that accepts parameter type 'com.mypackage.ui.UIState'
I have tried:
Rebuilding the project by removing .gradle folder
Looking for answer here and here.
Removed #JvmStatic annotation at the extension function
Moved the extension function to top level instead of Fragment's companion object.
I think you missed to add kotlin plugin in your gradle
apply plugin: 'kotlin-kapt'
You don't have to use #JvmStatic because you are using Kotlin extension feature.
You need to add the view reference as a paramater to your BindingAdapter method.
#BindingAdapter("playState")
fun setPlayState(button:Button,item: UIState) {
//do your work here
}
Your namespace
xmlns:app="http://schemas.android.com/apk/res-auto"
is wrong for custom binding adapters. Please use the namespace
xmlns:app="http://schemas.android.com/tools"
since app:playState is not in the namespace you have given its not working properly

Cannot find the getter for attribute 'android:checked'

data binding error ****msg:Cannot find the getter for attribute 'android:checked' with value type java.lang.Boolean on android.widget.CheckedTextView.
I have a Kotlin Android app and one of the XML layouts contains a CheckedTextView and I want to two-way bind the checked property to the checked value of the ViewModel. The idea is that the checked property in the viewModel will represent the one on the view itself. This fails with the error message above. Now I wonder whether this is because checked is a boolean value and the getter is called isChecked. Can Databinding not recognize that? So I tried extending it with a getChecked function, but that didn't resolve the error. Maybe because while Kotlin supports extension functions, Java does not. Any ideas how this can be solved?
XML file:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel"
type="lehrbaum.de.onenightcomps.view.SimpleCheckableListItemViewModel"/>
</data>
<CheckedTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/textView"
android:padding="#dimen/text_margin"
android:gravity="center_vertical"
android:textStyle="bold"
android:checkMark="?android:attr/listChoiceIndicatorSingle"
android:checkMarkTint="#color/colorPrimary"
android:checked="#={viewModel.checked}"
android:text="#{viewModel.text}"/>
</layout>
ViewModel class:
class SimpleCheckableListItemViewModel {
val checked : MutableLiveData<Boolean> = MutableLiveData()
val text : MutableLiveData<String> = MutableLiveData()
}
Extension function:
fun CheckedTextView.getChecked(): Boolean {
return this.isChecked
}
There might be different reasons for this error but in my case, the problem raised up because I didn't add apply plugin: 'kotlin-kapt' And apply plugin: 'kotlin-android-extensions' in my Gradle.
After adding these plugins you have to replaced your annotationProcessors with kapt.
After that, every thing might be going well.

Android mvvm livedata and databinding

I am experimenting the new architecture components from Google trying to achieve more reactive code using ViewModel, LiveData and DataBinding.
Basically my idea around ViewModel is to have only one field of type Model(user for the record since we are representing a user profile scree). So my ViewModel class is :
class ViewModel : ViewModel() {
var model = MutableLiveData<User>()
and my Model class is :
class User(var name: String, var lastName: String, var age: Int)
In my layout file, I am trying to bind the fields on my Model into the view using DataBinding plugin. The problem is that since my ViewModel has a MutableLiveData<User> I can't access (from xml binding) the fields inside the User class (name, lastName...).
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.github.andromedcodes.mvvmtutorial.ViewModel" />
</data>
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="#{viewModel.user.name}"/>
</RelativeLayout>
</layout>
Is it even possible to do that? And which is better, having a ViewModel with separate Fields (String, Int, Whatever...) or re-using a Model?
You can see my repository where I did everything using DataBinding and MVVM pattern . Just visit https://github.com/xyarim/android-architecture

Categories

Resources