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}"
/>
Related
I am trying to find out how to bind both the list items, and the selected value/index of an Android Spinner (I am pretty new to Android / Kotlin)
I have the following
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.app.Modes" />
<variable
name="viewModel"
type="com.example.app.MainActivityViewModel" />
</data>
....
<Spinner
android:layout_row="17"
android:layout_column="2"
android:id="#+id/spinner1"
android:layout_width="1200px"
android:entries="#{viewModel.devicesDescriptions}"
app:selectedValue="#={viewModel.devicePosition}"
android:layout_height="wrap_content"
android:background="#android:drawable/btn_dropdown"
android:spinnerMode="dropdown"/>
and in the View Model
val devicesDescriptions = ObservableArrayList<String>()
var devices = listOf<MidiDeviceInfo>()
fun setFoundDevices(d: MutableList<MidiDeviceInfo>) {
devices = d
for (dev in devices)
devicesDescriptions.add(dev.toString())
}
By trial and error I could set just strings to the Spinner items (the MidiDeviceInfo would have been better, but string will do)
However, I cannot get a binding to get the selectedItem to work.
I have tried many things, but with the above, I have the error
Found data binding error(s):
[databinding] {"msg":"Cannot find a getter for \u003candroid.widget.Spinner app:selectedValue\u003e that accepts parameter type \u0027java.lang.String\u0027\n\nIf a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.","file":"app\\src\\main\\res\\layout\\activity_main.xml","pos":[{"line0":334,"col0":4,"line1":343,"col1":39}]}
Anyone know a way to do this?
Try using android:selectedItemPosition="#={viewModel.devicePosition}" instead of app:selectedValue="#={viewModel.devicePosition}".
in databinding of android simply i want to control view visibility by checking viewmodel parameter as profilePicUrl
Solution 1:
when profilePicUrl is empty view should be gone, otherwise that should be visible, for example:
<data>
<import type="android.view.View"/>
<import type="android.text.TextUtils"/>
<variable name="viewModel" type="xx.xxxxx.xxxxxxx.MyViewModel"/>
</data>
...
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{TextUtils.isEmpty(viewModel.profilePicUrl) ? View.GONE : View.VISIBLE}"
>
i get this error:
error: '#{TextUtils.isEmpty(viewModel.profilePicUrl)? View.GONE : View.VISIBLE' is incompatible with attribute visibility (attr) enum [gone=2, invisible=1, visible=0].
Solution 2:
after getting this error i try to test another solution to approach that, for example:
BindingAdapters class:
object BindingAdapters {
#BindingAdapter("visibleIf")
#JvmStatic
fun changeVisibility(#NonNull imageView: ImageView, visible: Boolean) {
if (visible) {
imageView.visibility = View.VISIBLE
} else {
imageView.visibility = View.GONE
}
}
}
xml layout:
<data>
<import type="android.view.View"/>
<import type="android.text.TextUtils"/>
<variable name="viewModel" type="xx.xxxxx.xxxxxxx.MyViewModel"/>
</data>
...
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:visibleIf="#{TextUtils.isEmpty(viewModel.profilePicUrl)}"
>
i get this error:
error: attribute visibleIf (aka xxx.xxxxx.xxxxxxxx:visibleIf) not found.
Are you sure you have enabled data-binding properly? Your code is correct, so there is no reason why it shouldn't work.
Please make sure this code is added in the Gradle file of your app module:
dataBinding {
enabled = true
}
In addition, I would recommend adding this to the gradle.properties file:
android.databinding.enableV2=true
Here you can see a similar question.
Regarding the second solution, sometimes importing the object that contains the adapter methods helps:
<import type="com.your.package.name.BindingAdapters" />
Also, please make sure you have added this attribute on your layout tag:
xmlns:app="http://schemas.android.com/apk/res-auto"
I'm trying to set an OnClickListener for an <include>d layout, but receive a data binding error at compile time stating that data binding "Cannot find the setter for attribute 'android:onClick' with parameter type android.view.View.OnClickListener".
Context here is that I'm using data binding to inflate the included layout, so that I can pass values into it from a viewModel that I've bound to the including layout.
I've tried various syntax for the data binding expression:
#{viewModel::onClickFunction}
#{viewModel.onClickFunction}
#{() -> viewModel.onClickFunction()}
#{(view) -> viewModel.onClickFunction()}
#{_ -> viewModel.onClickFunction()}
I've tried all of the above with onClickFunction as a function, and also as an OnClickListener object.
Other related questions on Stack Overflow seem to solve this issue by cleaning the project to regenerate the databinding files, but that hasn't worked for me.
Relevant code snippets below:
viewModel
class MyViewModel() {
val onClickFunctionAsAListener: OnClickListener = object:OnClickListener{
override fun onClick(v: View?) {
//Do something
}
}
fun onClickFunction() {
//Do something
}
}
Including layout
<layout>
<data>
<variable name="viewModel" type="full.package.name.MyViewModel"/>
</data>
<LinearLayout>
<include
layout="#layout/included_layout"
android:onClick="#{viewModel.onClickListener}"
app:customAttribute="#{`someText`}/>
</LinearLayout>
</layout>
Included layout
<layout>
<data>
<variable name="customAttribute" type="String"/>
</data>
<TextView
layout="#layout/included_layout"
android:text="#{customAttribute}"/>
</layout>
It seems that you can't actually assign an OnClick handler to an <include> tag directly. I managed to get it to work by adding another variable to IncludedLayouts data binding class, and then assigning the OnClickListener to IncudedLayouts root view in XML.
After the changes, my files looked like this:
viewModel
class MyViewModel() {
val onClickFunction: OnClickListener = object:OnClickListener{
override fun onClick(v: View?) {
//Do something
}
}
}
Including layout
<layout>
<data>
<variable name="viewModel" type="full.package.name.MyViewModel"/>
</data>
<LinearLayout>
<include
layout="#layout/included_layout"
app:onClick="#{viewModel.onClickListener}"
app:customAttribute="#{`someText`}/>
</LinearLayout>
</layout>
Included layout
<layout>
<data>
<variable name="customAttribute" type="String"/>
<variable name="onClick" type="android.view.View.OnClickListener"/>
</data>
<TextView
layout="#layout/included_layout"
android:text="#{customAttribute}"
android:onClick="#{onClick}"/>
</layout>
include tag does not support onClick method directly. While the selected answer is correct, instead of passing onClickLister to include layout (or having custom #BindingAdapter, which is also another solution), I would just wrap my include inside a ViewGroup and onClick on ViewGroup.
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="#{()-> viewModel.yourFunction()}">
<include layout="#layout/custom_layout"/>
It's a workaround, but works as charm.
I have a ViewModel with a List auf MutableLiveData<Data> in my Fragment Layout I set the data variable of my CustomView with one of the data elements from the List.
This works fine when it first loads but it doesn't update when I change a value in my data object.
Not really sure how to do this, until now I just used two-way data binding with EditText and MutableLiveData for example.
CustomView Layout:
<data>
<variable
name="data"
type="androidx.lifecycle.LiveData<Data>"/>
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="#{data.color}"
app:cardCornerRadius="16dp">
Class:
var data: MutableLiveData<Data>? = null
set(value) {
binding.data = value
}
Fragment Layout:
<data>
<variable
name="viewModel"
type=".ViewModel" />
</data>
<CustomView
.
.
.
app:data="#{viewModel.data[1]}" />
The reason for the update only happening the first time the screen is loaded is that the XML is used to inflate the View and then the initial item is used and set to the CustomView.
Then when the item in the list is updated, it does not trigger an update in the CustomView.
What you might be looking for is #BindingAdapter
#BindingAdapter("enableButton")
internal fun Button.enableButton(enabled: Boolean?) = enabled?.let { isEnabled = it } ?: also { isEnabled = false }
And then using it in the following way:
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button Text"
app:enableButton="#{viewModel.observeStatus()}" /> // <- Observes Boolean
A good walk-through might be at the following link: BindingAdapter
Note: The example is only for a Boolean observation, but it can simply be changed to match whatever object is observed.
Via data-binding I am setting the visibility of a text field. The visibility is depending on a string being null or empty or nothing of the both.
<?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">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="com.example.viewModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
<TextView
android:id="#+id/textField1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.data.text}"
android:visibility="#{(viewModel.data.text == null || viewModel.data.text.empty) ? View.GONE : View.VISIBLE}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
Is it possible to create an import in the data element so that I can use the isNullOrBlank() function from the kotlin.text.Strings.kt class?
I was hoping to be able to use it like this: android:visibility="#{(viewModel.data.title.isNullOrBlank() ? View.GONE : View.VISIBLE}"
Android data binding still generates the Java code from the XML instead of the Kotlin code, once data binding will be migrated to generating Kotlin code instead of Java I believe we will be able to use Kotlin extension function in the XML, which will be really cool.
I am sure that gonna happen real soon as Google is pushing to Kotlin heavily. But for now, you have below
TextUtils.isEmpty() as mentioned by #Uli Don't forget to write an import.
The reason why you can't you use StringKt.isNullOrBlack in xml:
Below is the code from Kotlin String.kt
#kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this#isNullOrEmpty != null)
}
return this == null || this.length == 0
}
As you can see it is annotated with #kotlin.internal.InlineOnly which says the Java generated code of this method will be private.
InlineOnly means that the Java method corresponding to this Kotlin
function is marked private so that Java code cannot access it (which
is the only way to call an inline function without actual inlining
it).
It means it can't be called from Java and as data binding generated code is in JAVA it can't be used in data binding either. Thumb rule is what you can access from JAVA you can use that in data binding if not just use the old Java way I would say.
TextUtils.isEmpty() should do what you want.