How to use companion objects on xml layout? - android

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>

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

2 way data binding error for a livedata variable with proper xml binding

I have a loginViewModel for my login Activity, in activity_login.xml
<?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>
<variable
name="loginViewModel"
type="com.example.test.ui.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/login_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="#+id/mobile_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:editable="true"
android:ems="10"
android:hint="#string/mobile_number_string"
android:inputType="phone"
android:textAlignment="center"
android:text="#={loginViewModel.phoneNumber}"
app:layout_constraintBottom_toTopOf="#+id/otp_input"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my loginViewModel i have defined my livedata as
class LoginViewModel : ViewModel(){
private val _phoneNumber = MutableLiveData<String>()
val phoneNumber : LiveData<String>
get() = _phoneNumber
}
Now, while building I am getting the following error
The expression \u0027loginViewModelPhoneNumber.getValue()\u0027 cannot be inverted, so it cannot be used in a two-way binding\n\nDetails: There is no inverse for method getValue, you must add an #InverseMethod annotation to the method to indicate which method should be used when using it in two-way binding expressions
All articles that I am reading suggesting this way to implement.
Can someone tell me what am I doing wrong here?
Unfortunately for two-way data binding you need to use MutableLiveData.
You should remove private on _phoneNumber.
Then change xml to use it android:text="#={loginViewModel._phoneNumber}".
change from
private val _phoneNumber = MutableLiveData<String>()
to
public val _phoneNumber = MutableLiveData<String>()
You are binding phoneNumber which is a LiveData that does not have any interface for writing value.
Consider removing phoneNumber, and using Kotlin-based approach, with just a public property
There are several issues with the code.
you try to use val phoneNumber that is not mutable in two way binding, thus is has only getter so binding class can only read value from field, to receive data from the UI - binding class wants to use setter, but there is no setter present, thus the phoneNumber won't change.
for the property that binds phoneNumber you try to use LiveData which is not mutable - you should use MutableLiveData in case you need it to be able to change.
if you want to listen to the changes of phoneNumber you need to add LiveData Observer like
phoneNumber.observe{
val value = it
}
Hope it helps.

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

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

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