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

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

Related

Font tag inside string tag is not read with Data Binding Android

String resource:
<string name="hint_dob_mandatory">Date of birth<font color='#FFFF0000'> *</font></string>
<!-- Already tried with '#FFFF0000', "#FFFF0000" -->
<string name="hint_dob">Date of birth</string>
Below piece of code is working fine:
android:hint="#string/hint_dob_mandatory"
Below is the actual output which is correctly fine: (with normal use of string res)
Here, issue with data binding:
android:hint="#{ANY_TRUE_CONDITION ? #string/hint_dob_mandatory : #string/hint_dob}
Below is the actual output which is having issue: (with using data binding of string res)
The key is to look at the signature of the methods.
If you are NOT using databinding, you are actually using this method:
fun EditText.setHint(resourceId: Int)
When using Databinding, #string/hint_dob_mandatory will be resolved to a String automatically, and, in fact, your are using this method:
fun EditText.setHint(hint: CharSequence)
This first one handles the font tag nicely, but the second will just ignore it. Basically, Databinding is trying to be smart but by doing so, uses the wrong method.
Now, to solve it you have 2 options:
1) Explicitly set a ResourceId for the hint
<data>
<variable
name="hintResource"
type="Integer" />
</data>
<EditText
android:hint="#{hintResource}" />
With this approach, you need to set hintResource on your binding
binding.hintResource = R.string.hint
This effectively ensures, that the setHint(res: Int) variant is being used
2) Import your R file
<data>
<import type="com.your.package.R" />
</data>
<EditText
app:resourceHint="#{ANY_TRUE_CONDITION ? R.string.hint_dob_mandatory : R.string.hint_dob}
By importing the R file, you can use it in the dataBinding expression. By using R.string.hint you indicate that you use a Resource. Whereas #string/hint will actually convert that resource into a String.
Finally, a combination of the two are also possible, as well as creating a custom BindingAdapter. But in the end it's all the same, as long as you make sure to use an #StringRes Int instead of a String

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

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 Do We Get SpannedString Objects From String Resources Using Data Binding?

Florina Muntenescu wrote up a cool post about using <annotation> in string resources for being able to have flexible markup that you can process in your app using custom spans. I am trying to leverage it in data binding, but I cannot quite figure out how to get a SpannedString edition of the string resource from data binding.
In my layout, I have app:thingy="#{#string/my_annotated_string}" as an attribute on a TextView. I have a binding adapter set up to handle thingy attributes. However, the data binding system seems to insist that my value is a String.
I have tried:
#BindingAdapter("thingy")
#JvmStatic
fun handleThingy(textView: TextView, thingy: SpannedString) { /* stuff goes here */ }
and:
#BindingAdapter("thingy")
#JvmStatic
fun handleThingy(textView: TextView, thingy: Spanned) { /* stuff goes here */ }
and:
#BindingAdapter("thingy")
#JvmStatic
fun handleThingy(textView: TextView, #StringRes thingy: Int) { /* stuff goes here */ }
In all cases, I get Cannot find the setter for attribute 'app:thingy' with parameter type java.lang.String on android.widget.TextView build errors.
If I use String or CharSequence for the thingy parameter type, it builds, but then I get passed a String and I do not have my annotation spans from the string resource.
So, how can I either:
Get the SpannedString corresponding to my string resource (i.e., what you get from getText() instead of getString()), or
Get the string resource ID of my string resource, so I can call getText() myself to get my SpannedString
As an expression, #string/my_annotated_string evaluates to a string. Eventhough it resembles a string resource reference in XML, it's actually only a String value.
It would be nice to have a #text/my_annotated_string version as well, but as of the documentation this is not available.
Instead you'd have to use the actual resource within your binding expression:
app:thingy="#{string.my_annotated_string}"
app:thingy="#{context.getText(string.my_annotated_string)}"
This is assuming the import of the string class:
<import type="path.to.R.string"/>
Here is a maybe slightly less icky way:
Define the annotated string.
<string name="my_annotated_string">A <annotation font="title_emphasis">cool</annotation> annotation <annotation font="title_emphasis">thingy</annotation>.</string>
Place a reference to that string resource into a TypedArray:
<resources>
<array name="annotated_text">
<item>#string/my_annotated_string</item>
</array>
</resources>
Reference the TypedArray in the layout:
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:thingy="#{#typedArray/annotated_text}" />
Finally, set a BindingAdapter to capture the SpannedString with the annotations:
#BindingAdapter("thingy")
public static void setThingy(TextView textView, TypedArray strings) {
SpannedString ss = (SpannedString) strings.getText(0);
Object spans[] = ss.getSpans(0, ss.length(), Object.class);
}
Although a little involved, this works. If there are multiple strings, the array can be expanded.
Use this binding adapter:
#BindingAdapter("thingy")
fun handleThingy(textView: TextView, #StringRes thingy: Int) { /* stuff goes here */ }
and instead of using app:thingy="#{#string/my_annotated_string}" pass resource into binding: thingy="#string/my_annotated_string" (without app:).
one can define just any data-type with <import>:
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
and then obtain the text from it:
android:text="#{sparse[index]}"
to define a thingy with data-type SpannedString (or one that extends SpannedString):
<data>
<import type="com.acme.model.BindableSpannedString"/>
<variable name="thingy" type="BindableSpannedString"/>
<variable name="index" type="int"/>
</data>
the getter annotated with #Bindable should be index access:
android:text="#{thingy.getSpanAt(index)}"
SpannedString lacks the getter required to bind it, nor can one add annotations to framework classes.

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