How to pass a sealed class through data binding - android

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)}"

Related

How to properly use two way binding in Kotlin-Multiplatform?

I'm trying to use a String variable to bind it into my view.
When I use a model object with a String property, it works well. But if I use the String variable alone, it only works with one way binding.
ViewModel:
class SampleModel(var data : String = "")
var myModel : SampleModel = SampleModel()
var myVariable : String = ""
XML:
<data>
<variable
name="model"
type="MyViewModel.SampleModel" />
<variable
name="variable"
type="String" />
</data>
<!-- Two way works fine -->
<EditText
android:text="#={model.data}"/>
<!-- Only one way works -->
<EditText
android:text="#={variable}"/>
The string in the SampleModel works well with two way binding but the String variable does not.
I think it is because the imported String in xml is java.lang.String but the String in the model is kotlin.String. And I'm unable to use the kotlin.String in xml.
Is there any solution to fix this? Or is there any proper way of two way binding in Kotlin-Multiplatform projects?
It looks like you have added a wrong variable in the xml file. In your view model you have created a variable named myVariable of type String but in your xml file you are creating one more variable here :-
<variable
name="variable"
type="String" />
so these both variables are different. You don't need to import anything in your xml file just create a viewModel variable which you have already done here :-
<variable
name="model"
type="MyViewModel.SampleModel" />
and now simply use this like :- android:text="#={model. myVariable}"
UPDATE :- Here in this you need to use the String variable which i created in your viewModel because it used kotlin.String and in xml you have java.lang.String. You can simply use the variable which is created in your viewModel For eg :- android:text="#={viewModel.yourVariable}"

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

How to reference ArrayList<Custom Class> type variables and objects in it with an index in Kotlin, with MVVM pattern and Data Binding applied?

I'm building an app, with MVVM pattern applied.
I've added all the required dependencies.
Also, I know how to pass variables to my xml layout files,
and how to use a function using them as parameters.
But I just don't know how to handle ArrayList type of variables.
This is the data class declaration part from my activity_main.xml
<variable
name="vm"
type="com.example.mvvm.MainActivityViewModel" />
<variable
name="codeBlock"
type="java.util.ArrayList"/>
<variable
name="block"
type="com.example.mvvm.CodeBlock" />
And binded the data like this in MainActivity
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
//binds data
binding.vm = mMainActivityViewModel
binding.lifecycleOwner = this
binding.codeBlock = mMainActivityViewModel.getBlockButton()
so I did pass the ArrayList of custom classes, each of them is called CodeBlock. And here's what I've tried :
<TextView
android:id="#+id/bt_move"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/move"
android:textAllCaps="false"
android:textColor="#color/Black"
android:onClick="#{vm.addNewBlock(codeBlock[0])}"/>
or not [0], but .get(0)
and I think, or I'm sure it is because that the type of codeBlock[0] varaiable is ambiguous. In MainActivity, this thing gives me an error sign.
var ff : CodeBlock = binding.codeBlock[0]
Type Mismatch , Required : CodeBlock Found : Any!
Why??? I gave it ArrayList and it received that?
Also, this gives me an error too.
var ff : ArrayList<CodeBlock> = binding.codeBlock
Smart cast to 'kotlin.collections.ArrayList<CodeBlock> /*= java.util.ArrayList<CodeBlock> */' is impossible, because 'binding.codeBlock' is a mutable property that could have been changed by this time
Well should I use kotlin.collections.ArrayList<CodeBlock>?
but in variable type in activity_main.xml,
it does not support that kind of type.
It seems it only support types from java.
I can't find kotlin.collections.ArrayList.
So how can I pass ArrayList type of variables to view?
And this ArrayList property? this is not gonna change. It is a list of blocks that the user can choose in a stage. So it shouldn't be Mutable.
When I try to run the code,
the error says
cannot find method addNewBlock(java.lang.Object) in class com.example.mvvm.MainActivityViewModel

How to use companion objects on xml layout?

I am trying to use a companion object property inside the layout but the compiler doesn't recognise it.
Kotlin Class
class MyClass {
companion object {
val SomeProperty = "hey"
}
}
XML Layout
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fancy="http://schemas.android.com/tools">
<data>
<import type="package.MyClass"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.Companion.SomeProperty}"/>
</layout>
And I got this error:
e: java.lang.IllegalStateException: failed to analyze: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:Could not find accessor package.MyClass.Companion.SomeProperty file:/path/to/my/layout.xml loc:21:67 - 21:103 ****\ data binding error ****
at org.jetbrains.kotlin.analyzer.AnalysisResult.throwIfError(AnalysisResult.kt:57)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:138)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:154)
...
Caused by: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:Could not find accessor package.MyClass.Companion.SomeProperty file:/path/to/my/layout.xml loc:21:67 - 21:103 ****\ data binding error ****
at android.databinding.tool.processing.Scope.assertNoError(Scope.java:112)
at android.databinding.annotationprocessor.ProcessDataBinding.doProcess(ProcessDataBinding.java:101)
at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:65)
...
I've tried to use companion instead of Companion, but no luck.
Is it possible to use companion objects on xml layout with databinding? How can I proceed? Thanks in advance for any help :)
In order to access Companion object attributes and methods, it is NOT required to have an instance of the Parent object.
Companion object are already instantiated, therefore you can access the instance directly.
Instead of using <import> (which is the natural translation from Java), we need to use <variable>, because we actually want to use the (already instantiated) Companion object into our XML Layout.
Import your Companion object as follow
Given Kotlin class:
package com.example.project
class MyViewModel {
companion object {
// it is only working with val and var
// const val wouldn't work
val MAX_LENGTH = 10
}
}
Layout:
<data>
<!-- Declare your "variable" that hold the Companion object itself -->
<variable name="myViewModelStatic" type="com.example.project.MyViewModel.Companion" />
</data>
<!-- then use the myViewModelStatic to access "static" properties of MyViewModel -->
<EditText
...
android:maxLength="#{ myViewModelStatic.MAX_LENGTH }"
/>
</layout>
Fragment:
class MyFragment {
...
onViewCreated(...) {
// now bind the companion object to the variable declared in the XML
binding.myViewModelStatic = TransferUseCase.Companion
}
...
}
You can get rid of Companion keyword if you annotate your method/proprty with #JvmStatic
In the XML just add Companion before the name of the field, for example:
In ViewModel
package com.example.project
class MyViewModel {
companion object {
var leText = "text"
}
var leColor = ...
}
In XML
<data>
<import type="android.view.View" />
<variable
name="context"
type="com.example.project.MyViewModel" />
</data>
<TextView
...
android:text="#{context.Companion.leText}"
android:color="#{context.leColor}"/>
To get access to your property, do the following:
Annotate you companion object property with
#JvmStatic
:
class MyClass {
companion object {
#JvmStatic
val SomeProperty = "hey"
}
}
then go ahead and remove 'Companion' from your binding TextView:
Change from
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.Companion.SomeProperty}"/>
to
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.SomeProperty}"/>
TL;DR:
Replace
android:text="#{MyClass.Companion.SomeProperty}"
with
android:text="#{MyClass.Companion.getSomeProperty()}"
Explanation:
Your problem is that you're trying to reference the Kotlin object exactly as it is named but that's not how the Kotlin compiler will generate the property in Java. Instead, it will convert it using the Java convention of being a "get" function.
You can find out what this name will look like by decompiling the Kotlin Bytecode.
Open the Kotlin class you want to see the bytecode for.
Open Tools > Kotlin > Show Kotlin Bytecode
In the side panel that opens up, click the Decompile button in the top-left.
This will show you the Java equalvalent of the Kotlin class, including the full name of the companion property.
Bonus:
That said, you probably would prefer to reference the field as a property, in which case you can just append the const keyboard to the property declaration.
const val SomeProperty = "hey"
With that the compiler will generate the field as a public static field, outside of the Companion, and you can update your xml to be simply:
android:text="#{MyClass.SomeProperty}"
Which is pretty much how you'd do it in Java.
Hope that helps!
There is another way:
in class
const val SomeProperty = "hey"
class MyClass {}
in XML
<data>
<import type="package.MyClassKt"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClassKt.SomeProperty}"/>
I went through all of this answers and I had to come up with my own solution.
Mark the constant with const val modifiers (const is the key) and just import the "parent" class (MyClass in your case) and you don't need the word Companion
class MyClass {
companion object {
const val SomeProperty = "hey"
}
}
XML
<data>
<import type="package.MyClass"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.SomeProperty}"/>
</layout>

Referencing properties of Observable class in Android Data Binding layout

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.

Categories

Resources