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.
Related
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
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}"
I'm currently using bindings to dynamically set the texts of various text views using the android view models. At the moment the view models look something like this:
class MyViewModel(
resources: Resources,
remoteClientModel: Model = Model()
) : ObservableViewModel() {
init {
observe(remoteClientModel.liveData) {
notifyChange()
}
fun getTextViewTitle(): String = when {
someComplicatedExpression -> resources.getString(R.string.some_string, null)
else -> resources.getString(R.string.some_other_string)
}
}
And the xml layout:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="my.app.signature.MyViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{viewModel.textViewTitle}"
android:textAlignment="center"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
However I would like to remove the "resources: Resources" that is injected into the view model, since the resources are coupled with the Activity. The code now simply returns the string resource id instead:
fun getTextViewTitle(): Int = when {
someComplicatedExpression -> R.string.some_string
else -> R.string.some_other_string
}
Hence I've removed the activity dependency. The compiler thinks this is fine but it crashes in runtime with the following exception: android.content.res.Resources$NotFoundException: String resource ID #0x0.
This happens when trying to attach the lifeCycleOwner to the binding using:
override fun onActivityCreated(savedInstanceState: Bundle?) {
// Some more code....
binding.lifecycleOwner = activity
// Some more code....
I'm not sure how to remove the resource dependency from the view model without having it crash in runtime.
EDIT:
For clarification: The ObservableViewModel in my example is the very same one as the one found here:
https://developer.android.com/topic/libraries/data-binding/architecture
Used to perform notifyChange.
The issue here is the code is trying to call textView.setText(0) which results in an error since there is no string resource with id 0x0. This is happening because getTextViewTitle() return an Int and the view binding functionality will make it default as 0 (when initializing).
https://developer.android.com/topic/libraries/data-binding/expressions#property_reference
From the docs
Avoiding null pointer exceptions
Generated data binding code automatically checks for null values and avoid null pointer exceptions. For example, in the expression #{user.name}, if user is null, user.name is assigned its default value of null. If you reference user.age, where age is of type int, then data binding uses the default value of 0.
Maybe something like this could work,
android:text='#{viewModel.textViewTitle == 0 ? "" : #string{viewModel.textViewTitle}}'
or
android:text='#{viewModel.textViewTitle, default=""}'
To solve this simply make a context available in the view, so that you can call context.getString(...) in your view.
<data>
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewModel"
type="my.application.path.SomeViewModel" />
</data>
<....
....
android:text="#{context.getString(viewModel.textResource)}"
...
/>
Just convert your int value to String to avoid this crush
android:text='#{String.valueOf(viewModel.profile.walletBalance)}'
In some cases your binding variable itself can be null
<data>
<variable
name="viewModel"
type="SomeViewModel"
/>
</data>
<TextView
android:text="#{viewModel == null ? "" : viewModel.textViewTitle}"
/>
First, this question is not a case of 'onClick' event parameter passing.
I have DateUtil class with one method as below :
public static String formatDate(long date) {
SimpleDateFormat dateFormat;
dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
Calendar c = Calendar.getInstance();
dateFormat.setTimeZone(TimeZone.getDefault());
c.setTimeInMillis(date);
return dateFormat.format(c.getTimeInMillis());
}
My model CommentEntity has following attributes :
private int id;
private int productId;
private String text;
private Date postedAt;
Now in one of layout I'm displaying the Comments.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="comment"
type="com.example.entity.CommentEntity"/>
<variable
name="dateUtil"
type="com.example.util.DateUtil"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
//This line gives error for data binding
android:text="#{dateUtil.formatDate(comment.postedAt.time)}"/>
</layout>
The error I'm getting is :
cannot find method
formatDate(com.example.util.DateUtil) in class
long
Now for the same if I modify formatDate() method as it will take current time by default, hence removing the parameter passing in data binding, it will work perfectly.
So am I doing something wrong or is it a bug?
Please provide solution for problem to pass the parameter to method in data binding.
Try below approach:
Don't take your DateUtil class object direct from data binding in xml
Make one BindingAdapter method in your CommentEntity model class like this:
#BindingAdapter("android:text")
public static void setPaddingLeft(TextView view, long date) {
view.setText(DateUtil.formatDate(long));
}
Then use like below for xml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
android:text="#{comment.postedAt.time}"/>
Explanation :
When you wanted to apply some custom logic for your view based on data model, you need to use BindingAdapter to do that job. So, you can provide some custom tag or use any default android: tag on which you need to set logic.
I denied you using DateUtil for binding adapter because, you might be using it's methods to somewhere else too. So suggested to make new method in your model instead for that logic, so that core logic remains untouched. (You can use your DateUtils for this logic though, you just need to make it as BindingAdapter).
since you want to use a static method in DateUtil you should import it:
<data>
<variable ... />
<import type="foo.bar.DateUtil"/>
</data>
and in TextView:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
//use DateUtil directly-
android:text="#{DateUtil.formatDate(comment.postedAt.time)}"/>
your error was trying to use it as a variable - this tells databinding to expect this class instance to be bound somewhere in your UI class (Fargment/Activity)
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.