I want to create an "inflateWhen" BindingAdapter and attach it to a viewstub to have it inflate when a boolean value is true. However, the BindingAdapter keeps trying to operate on the root view of the viewstub, causing it to fail to compile. Is there any way to do this as a bindingadapter rather than having to do it programmatically in the activity?
Here's what I have so far:
#BindingAdapter("inflateWhen")
fun inflateWhen(viewstub: ViewStub, inflate: Boolean) {
if (inflate) {
viewstub.inflate()
} else {
viewstub.visibility = View.GONE
}
}
This is what I have, but when attached to a viewstub like
<ViewStub
android:id="#+id/activity_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:inflateWhen="#{viewmodel.userid != 0}" />
it fails to compile. The error is:
ActivityMyAccountSectionedBindingImpl.java:1087: error: cannot find symbol
if (this.pageFooter.isInflated()) this.pageFooter.getBinding().setVariable(BR.inflateWhen, viewmodelRatingInt0);
Looks like it's trying to apply the binding to the inflated view, but that's not what I want here.
08.10.2020 Update:
I have written an article on Medium where I provide an example of how to switch between layouts on the fly depending on the screen state using ViewStub and DataBinding:
https://medium.com/#mxdiland/viewstub-databinding-handle-screen-states-easily-2f1c01098b87
Old accepted answer:
I also faced the problem to write #BindingAdapter for the ViewStub to control layout inflation using databinding instead of direct referencing to the ViewStub and calling inflate()
Along the way, I did some investigation and studied the following things:
ViewStub must have android:id attribute to avoid build errors like java.lang.IllegalStateException: target.id must not be null;
any custom attribute declared for ViewStub in an XML, databinding tries to set as a variable to the layout which will be inflated instead of the stub;
... that's why any binding adapter is written for ViewStub will never be used by databinding
there is only one but pretty tricky #BindingAdapter which works: androidx.databinding.adapters.ViewStubBindingAdapter and allows setting ViewStub.OnInflateListener trough XML attribute android:onInflate
the ViewStubBindingAdapter's first argument is ViewStubProxy not View or ViewStub!;
any different adapter written similarly does not work - databinding tries to set variable to the future layout instead of using the adapter
BUT it is allowed to override existing androidx.databinding.adapters.ViewStubBindingAdapter and implement some desired logic.
Because this adapter is one and the only option to interact with ViewStub using databinding I decided to override the adapter and use not for its intended purpose
The idea is to provide specific ViewStub.OnInflateListener which will be the listener itself and at the same time will be a signal that ViewStub.inflate() should be called:
class ViewStubInflationProvoker(
listener: ViewStub.OnInflateListener = ViewStub.OnInflateListener { _, _ -> }
) : ViewStub.OnInflateListener by listener {
companion object {
#JvmStatic
fun provideIf(clause: Boolean): ViewStubInflationProvoker? {
return if (clause) {
ViewStubInflationProvoker()
} else {
null
}
}
}
}
and overriding binding adapter:
#BindingAdapter("android:onInflate")
fun setOnInflateListener(
viewStubProxy: ViewStubProxy,
listener: ViewStub.OnInflateListener?
) {
viewStubProxy.setOnInflateListener(listener)
if (viewStubProxy.isInflated) {
viewStubProxy.root.visibility = View.GONE.takeIf { listener == null } ?: View.VISIBLE
return
}
if (listener is ViewStubInflationProvoker) {
viewStubProxy.viewStub?.inflate()
}
}
and XML part
...
<ViewStub
android:id="#+id/no_data_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="#layout/no_data"
android:onInflate="#{ViewStubInflationProvoker.provideIf(viewModel.state == State.Empty.INSTANCE)}"
app:viewModel="#{viewModel.noDataViewModel}"
/>
...
So now the inflation will happen only when the state is State.Empty and databinding will set viewModel variable to the inflated #layout/no_data layout.
Not really graceful but working solution.
Related
is this code possible or any alternatives?
on xml you have like this,
<Button....
onClick="#{() -> adapter.itemClick(message, [here, pass binding resource like #this or context])}"
>
<TextView...
id="#+id/tvName"
>
then in your adapter class, you have
fun itemClick(message: String, view: View) {
view.tvName.text = "some text"
}
I'm not sure if this is possible since it is my first time using databinding.
I'm working on an android project in kotlin and while trying to add an OnTouchListener to several buttons, I have run into a problem: it cannot be done from XML and I want to keep my backing code clean. After a bit of research, I found out that I could add the XML support by using a method with the #BindingAdapter annotation:
#BindingAdapter("onTouch")
fun Button.setXMLTouchListener(listener : View.OnTouchListener)
{
this.setOnTouchListener(listener);
}
to this method:
class MainActivity : AppCompatActivity()
{
...
...
fun goLeft(v : View, event : MotionEvent) : Boolean
{
// my code
}
}
and in the XML:
<layout
...>
<data>
<variable name="main_activity" type="my.path.to.MainActivity" />
</data>
<androidx.constraintLayout.widget.ConstraintLayout
...>
<Button
...
app:onTouch="#{main_activity.goLeft}" />
...
</androidx.constraintLayout.widget.ConstraintLayout>
</layout>
and enabled data binding in the build.gradle:
apply plugin: 'kotlin.kapt'
and
android {
...
dataBinding {
enabled = true
}
}
This obviously didn't work, these are the solutions I have tried:
move the #Bindingadapter function from companion object to top level, so it's compiled static
try the app:onTouch contents as "main_activity.goLeft" (seen in a tutorial), "main_activity.goLeft()" (original try), and "main_activity::goLeft" (suggested by the compiler as the first is deprecated)
add logging to the click event to ensure the button receives events at all
change the value of the annotation to "app:onTouch" to be absolutely sure it's in the right xml namespace
move the touch listener function to a class that is non-activity and implements View.OnTouchListener (and renamed function accordingly)
After a bit of debugging, I also found out that the binding function doesn't run at all.
What could be the problem, and how can I solve it?
first write your data binding adapter like this.
#BindingAdapter("app:onTouch")
fun setXMLTouchListener(btn : Button , listener : View.OnTouchListener)
{
btn.setOnTouchListener(listener)
}
then chenge the goLeft() fun to it
val goLeftListener = View.OnTouchListener { v, event ->
Log.d("goLeftListener " , "it Worked !")
return#goLeftListener true
}
and don't forget to set activity on your binding object in onCreate fun
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: mainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.main_activity = this
}
and for the last step write onTouch attribute of Button in your xml layout like this
<Button
.
.
app:onTouch="#{main_activity.goLeftListener}"
.
/>
One Activity, two Fragments that share a common ViewModel. I have verified that the view model reference is the same in each fragment.
In the layout XML for fragment one, there is a TextInputLayout. Fragment two updates the view model with a boolean value. The text input layout is observing this value and should call a BindingAdapter when the value is changed.
The binding adapter fires when the fragments are instantiated and their layouts are inflated, so I know that the view is observing this value. However, later on, when fragment two updates the value, the view in fragment one does not trigger the binding adapter.
This is in onCreateView() of fragment one:
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
and this is the view that is observing that view model:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/reg_auth_code_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:errorState="#{registrationViewModel.registrationData.authorizationError}"
bind:errorMessage="#{#string/invalid_auth_code}">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/reg_auth_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{registrationViewModel.registrationData.authCode}"
android:hint="#string/enter_auth_code"
android:maxLines="1"
android:inputType="text"
android:imeOptions="actionDone"
app:autoSizeTextType="uniform"/>
</com.google.android.material.textfield.TextInputLayout>
As for fragment two, same code in onCreateView():
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
When a button is clicked, fragment two fires an activity in the view model:
private fun attemptNavigationToUserData() {
viewModelScope.launch {
isAuthorized = runBlocking { useCase.isAuthorized() }
registrationData.value?.authorizationError = !isAuthorized
}
}
And finally, here is the BindingAdapter:
#BindingAdapter("errorState", "errorMessage")
fun setErrorState(
textInputLayout: TextInputLayout?,
errorState: Boolean,
errorMessage: String) {
textInputLayout?.let {
it.isErrorEnabled = errorState
if (errorState) it.error = errorMessage
}
}
This all seems to be set up correctly, AFAIK. As I mentioned, the binding adapter fires when the views are initially inflated, but never again.
Why isn't my XML observing the view model? Or, why isn't the binding adapter firing upon update??
Thanks for any help.
The answer is most likely that you don't set the lifecycleOwner for your fragment binding object.
For further information see https://stackoverflow.com/a/56011798/1894338
Look at my answer here https://stackoverflow.com/a/66488334/9747826
setLifeCyclerOwner and setting the viewModel are the key.
You authorizationError should be a LiveData<>.So that the BindingAdapter mothod will called automatically when the LiveData's value(errorState) is updated.
If you want BindingAdapter method receive the automatic update, you should use LiveData in the dataBinding expression.
Such as:
subTitleText="#{removableItemsViewModel.removableItemsInfo}"
and the BindingAdapter method and the LiveData:
val removableItemsInfo: LiveData<Pair<Int, Long>>
-----------------------------------------------------
#BindingAdapter("subTitleText")
fun setSubTitleText(textView: TextView, pair: Pair<Int, Long>){
}
also,don't forget to invoke binding.setLifecycleOwner()
How can an activity access the instance of the view, after it's been set using SetContentView? I need this because the activity uses a custom view which includes logic and I need this view to sent events to the activity, through a custom event listener that the activity needs to set in the view.
I'm programming with android studio in kotlin.
I previously had all the UI control logic in the activity so I was fine, but I am factoring some UI code in a Custom View to re-use it in several activities.
Here is the initialization of the activity
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.custom_view)
// Here need to access the view instance
*xxxxxxx*.setCustomViewListener(new CustomView.MyCustomViewListener() {
#Override
public void onCancelled() {
// Code to handle cancellation from the view controls
}
});)
}
}
Here is the view layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:id="#+id/button_do"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="Do" />
<com.kotlin.app.views.CustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/view_custom" />
</FrameLayout>
Here is the custom view class CustomView.kt
class CustomView : FrameLayout, View.OnClickListener {
constructor(context: Context) : super(context)
init {
}
interface CustomViewListener {
fun onCancelled()
}
private var listener: CustomViewListener? = null
fun setCustomViewListener(listener: CustomerViewListener) {
this.listener = listener
}
Any idea please?
In Kotlin, it's more common to use Kotlin synthetic:
view_custom.setCustomViewListener(...)
Note: you seem to have written your listener implementation in Java, not Kotlin. Since your interface is defined in Kotlin you need something like this:
view_custom.setCustomViewListener(object : CustomView.MyCustomViewListener {
override fun onCancelled() {
...
}
})
SAM interfaces in Kotlin
Personally I like to use lambdas. Unfortunately you cannot use a lambda as a Kotlin SAM interface. You could however use a typealias instead of an interface:
typealias MyCustomerViewListener = () -> Void
Then you could use this instead:
view_custom.setCustomViewListener {
// listener code
}
How can an activity access the instance of the view, after it's been set using SetContentView?
Step #1: Add an android:id attribute to your root <FrameLayout> element.
Step #2: In onCreate() of your activity, after the setContentView() call, call findViewById() to retrieve the FrameLayout based on the ID that you assigned it in Step #1.
the activity uses a custom view which includes logic and I need this view to sent events to the activity, through a custom event listener that the activity needs to set in the view
You could also just call findViewById() and provide the ID of the custom view (findViewById(R.id.custom_view)).
Note that this covered by pretty much any book on Android app development.
I want to create a custom onClickListener to use for databinding. This custom click listener prevents the user from spamming the button and triggering the event twice(like showing two dialogues at the same time). I made a custom listener below that works in normal code, but I don't know how to implement it for databinding like the android:onClick="" in xml.
abstract class OneClickListener(var delay: Long) : View.OnClickListener {
private var hasClicked: Boolean = true
constructor() : this(1000)
override fun onClick(it: View) {
if (!hasClicked) {
return
} else {
hasClicked = false
onClicked(it)
GlobalScope.launch {
delay(delay)
hasClicked = true
}
}
}
abstract fun onClicked(it: View)
}
Is it possible to use this listner in databinding like for example
app:OneClickListener="#{viewModel::MyMethod}" in XML? and if yes, could you please tell me how?
Using data binding you can specify which listener to call when an event is fired just by calling it in a lambda. For example, let's say you have a method in your viewmodel, called myOnClick(). You can use it with data binding this way:
android:onClick="#{() -> viewModel.myOnClick()}"
Defining a custom binding adapter called OneClickListener is something different and it would not be called when the click event is fired, unless you use a trick: registering a click listener inside the custom binding adapter. This means you would have to call a method that register an other method: not really the cleanest way to add a listener.