With Android data-binding framework, I understand that you can pass an object that extends baseObservable to the layout xml, use #Bindable on getters and do notifyPropertyChanged(BR.xxx) to have the related part re-evaluated.
What I don't understand is this: if you don't use most the stuff above and just call the getter directly in xml, when would it be evaluated?
Here's the code:
my_widget.xml
<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="someViewModel"
type="com.example.SomeViewModel" />
</data>
<androidx.cardview.widget.CardView>
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="#{someViewModel.getName() ? View.VISIBLE : View.GONE}" />
</androidx.cardview.widget.CardView>
</layout>
MyView.java
MyWidgetBinding binding = MyWidgetBinding.inflate(LayoutInflater.from(mContext), parent, false);
binding.setSomeViewModel(someViewModel);
Questions:
If someViewModel.name ever changes, does the testView's visibility refreshes?
When does someViewModel.getName() get evaluated or how often?
If the expression is more complicated, something like:
android:visibility="#{func(otherVariable, someViewModel.getName()) ? View.VISIBLE : View.GONE}", say otherVariable is another variable defined in data section above, if somehow otherVariable gets re-set, then someViewModel.getName() will get evaluated and testView will reflect the latest visibility value, correct?
Following up on question 3, if otherVariable is changed to otherVariable.a where a is a 'bindable' field and notifyPropertyChanged(BR.a) is called in otherVariable then someViewModel.getName() will also get re-evaluated and testView will reflect the latest visibility value, correct?
Additionally, if I re-set someViewModel by calling binding.setSomeViewModel() but pass in the SAME someViewModel instance, does it do anything? Will the expression get re-evaluated?
I suggest you to create custom binding adapter for mutable visibility and use LiveData to update the visibility.
Code :
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Boolean>) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null) {
visibility.observe(
parentActivity,
Observer { value -> if (value) view.visibility = View.VISIBLE
else view.visibility = View.GONE})
}
}
To get the parent activity create ActivityExtensions.kt file and add the following function in it:
fun View.getParentActivity(): AppCompatActivity?{
var context = this.context
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
return null
}
And in the ViewModel :
//Other code here...
val itemVisibility = MutableLiveData<Boolean>()
//Other logic here to init itemVisible
if(itemVisibile) itemVisibility.value = true else itemVisibility.value = false
And finaly the layoutItem :
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:mutableVisibility ="#{viewModel.itemVisibility}" />
If someViewModel.name ever changes, does the testView's visibility
refreshes?
it depends on the underlying technology you are using. LiveData? Yes BaseObservable you have to manually notify that the observed property changed.
When does someViewModel.getName() get evaluated or how often?
LiveData ? when you set/post a value. BaseObservable when you notify it
Same as point 2
If you change the underlying value and notify this change properly, it will get propagated accordingly. If you change the instance of the observed object it will not.
Related
I am trying to implement two MaterialCardViews that should act like a Radiogroup. So if I click one, the other should be unchecked. I am using viewModel, liveData and custom two-way data binding to save these values for later purpose (sending per email).
I had success writing the .xml and implementing the check logic, but I struggle implementing uncheck logic.
XML, short version for better visibility
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
ViewModel
class EmailViewModel #ViewModelInject constructor(
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Variable for Id = cardViewOne
val cardOptionOneChecked = MutableLiveData<Boolean>()
// Variable for Id = cardViewTwo
val cardOptionTwoChecked = MutableLiveData<Boolean>()
}
CardViewAdapter.kt
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
liveData.value = view.isChecked
}
}
#InverseBindingAdapter(attribute = "state_checked")
fun getStateChecked(view: MaterialCardView,): Boolean {
return view.isChecked
}
// I don't know what logic belongs here to make it work!
// Current approach just checks the current view and does nothing more. How can I save the last
// checked value?
#BindingAdapter("state_checkedAttrChanged")
fun setCheckedAttrListener(
view: MaterialCardView,
attrChange: InverseBindingListener,
) {
view.apply {
setOnClickListener { view.isChecked = true }
setOnCheckedChangeListener { card, isChecked ->
if (card.isChecked && card != view) {
card.isChecked = false
}
}
attrChange.onChange()
}
}
I appreciate every help, thank you very much!
P.S: If there is a better and easier way to achieve this e.g. telling the viewModel from the view to save isChecked, please inform me. MaterialCardView has implemented "isChecked" by default but no logic.
Okay, I've solved the Problem:
First, Change Binding Adapter
I actually don't saw any way to use two-way data binding to achieve the above written case. Here is the new Binding Adapter
// View = Clicked MaterialCard, liveData = value in viewModel
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
if (liveData.value != null) {
view.isChecked = liveData.value!!
}
}
}
Second, Change XML Layout, because we don't use two-way data binding anymore
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
Third, Change viewmodel
class EmailViewModel #ViewModelInject constructor(
#ApplicationContext context: Context,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val cardOptionOneChecked = MutableLiveData<Boolean>()
val cardOptionTwoChecked = MutableLiveData<Boolean>()
// Added
fun firstCardClicked() {
cardOneChecked.value = true
cardTwoChecked.value = false
}
fun secondCardClicked() {
cardOneChecked.value = false
cardTwoChecked.value = true
}
}
Fourth, add clickListener to XML or Fragment (here Fragment)
cardViewOne.setOnClickListener {
viewModel.firstCardClicked()
}
cardViewTwo.setOnClickListener {
viewModel.secondCardClicked()
}
If someone has any questions, just write it in the comments, I will help.
I come here help since I don't understand at all my my code isn't working.
To be quick, my goal is to "reload" a View that represents a list item. Since my list item can contain other list items in it's children, I want to inflate a new list item, and then transfer those children from the old one to the new one.
I get a "The specified child already has a parent. You must call removeView() on the child's parent first." error, but I do call a removeView on the child's parent (somehow it doesn't work) (see my code after)
Here is how my layout is designed (I'm removing some lines so it is more readable) :
<?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="mainListItem"
type="com.plg.lirs.data.LirsDataEntity" />
</data>
<LinearLayout android:id="#+id/main_list_item_global_layout">
<LinearLayout android:id="#+id/main_list_item_parent_layout"
app:mainListItemParentLayout="#{mainListItem}">
<!-- contains a bunch of views and stuff, nothing important here -->
</LinearLayout>
<LinearLayout android:id="#+id/main_list_item_children_layout"
android:animateLayoutChanges="true">
<!-- here are all the children i want to transfer, all the children here are inflated from this layout -->
</LinearLayout>
</LinearLayout>
</layout>
Now here my code to inflate this layout :
/* entity is just a logical class that contains my data
olderView is the old view representing the old list item */
private fun inflateItem(entity: LirsDataEntity, olderView: View? = null) : View {
val itemBinding = DataBindingUtil.inflate<MainListItemBinding>(inflater, R.layout.main_list_item, null, false, null)
// the itemBinding.root will be added into the parent's children layout later on, after this function
// + i've tried with true as attachToParent, doesn't change
/* HERE is the core of the problem. My goal is : if an olderView is provided, then transfer the children from the old one to the new one */
if(olderView != null) {
val olderChildrenLayout = olderView.findViewById<LinearLayout>(R.id.main_list_item_children_layout) // here is the LinearLayout that contains the children
val children = olderChildrenLayout.children
children.forEach {
olderChildrenLayout.removeView(it) // remove the children from the old parent
itemBinding.mainListItemChildrenLayout.addView(it) // add it to the new parent
// at this point i get the error
}
}
entity.ui.reset() // not important here
itemBinding.mainListItem = entity
/* some listeners are set here */
return itemBinding.root
}
Thanks for reading !
I find out what were wrong.
When calling removeView(), android tries to animate it, thus placing the child view into a variable containing the children that are currently being animated. Then, when trying to change the child view's parent (which we want to be null), it checks it the current view is being animated. As it's true, the parent doesn't change (for now at least, I don't know if it will be changed later on). That's why we can't call the addView().
The solution is to store the LayoutTransition class, then setting it to null, do the transfer, and then resetting it. It will not animate the children, but at least it will work.
Here is a little piece of code to make that work:
public class JavaUtils {
public static void transferChildren(#NotNull final ViewGroup depart, #NotNull final ViewGroup arrival) {
LayoutTransition transition = depart.getLayoutTransition();
depart.setLayoutTransition(null);
while(depart.getChildCount() > 0) {
View c = depart.getChildAt(0);
depart.removeViewAt(0);
arrival.addView(c);
}
depart.setLayoutTransition(transition);
}
}
And for Kotlin users :
fun ViewGroup.transferChildrenTo(arrival: ViewGroup) {
val transition: LayoutTransition = layoutTransition
layoutTransition = null
while (childCount > 0) {
val c: View = getChildAt(0)
removeViewAt(0)
arrival.addView(c)
}
layoutTransition = transition
}
I want to use the ternary operator to change the color of a custom view in terms of a Boolean.
Here is my custom view
class AddButton(context: Context, attrs: AttributeSet): RelativeLayout(context, attrs) {
private var imageView: AppCompatImageView
private var textView: TextView
init {
inflate(context, R.layout.add_button, this)
imageView = findViewById(R.id.add_button_icon)
textView = findViewById(R.id.add_button_text)
val attributes = context.obtainStyledAttributes(attrs, R.styleable.AddButton)
val iconTint = attributes.getResourceId(R.styleable.AddButton_iconColor, 0)
imageView.setImageDrawable(attributes.getDrawable(R.styleable.AddButton_icon))
textView.text = attributes.getString(R.styleable.AddButton_text)
setIconTint(iconTint)
attributes.recycle()
}
fun setIconTint(colorId: Int) {
imageView.setColorFilter(ContextCompat.getColor(context, colorId), android.graphics.PorterDuff.Mode.SRC_IN);
}
fun setText(text: String) {
textView.text = text
}
}
values/attr.xml :
<declare-styleable name="AddButton">
<attr name="icon" format="reference"/>
<attr name="iconColor" format="color"/>
<attr name="text" format="string"/>
</declare-styleable>
In the layout :
<com.my.package.ui.customview.AddButton
app:icon="#drawable/ic_add"
app:iconColor="#{selected ? #color/colorRed : #color/colorBlack}"
app:text="#{selected ? #string/selected : #string/not_selected}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
It is working as expected for the app:text but when i want to do it for the iconColor i have this error :
Cannot find a setter for <com.my.package.ui.customview.AddButton app:iconColor> that accepts parameter type 'int'
For now to solve the problem, i have to change the color in the code behind by listening when the selected boolean changes and then call the setIconTint of my AddButton view.
Is there a way to change the color directly in the layout file using the ternary operator ?
You are looking for a custom data binding adapter for your custom view. First, check out this for the complete document.
So, in short, you need to define a method named: setIconColor that accepts iconId and sets the icon color by the provided resource id.
But if you want to use your current method named setIconTint, you just need to annotate your class with:
#BindingMethods(value = [
BindingMethod(
type = AddButton::class,
attribute = "iconColor",
method = "setIconTint")])
After all, I again suggest you check other variants of binding adapters in the official document.
Say for instance I have the following variable in my data bound XML.
<layout ...>
<data>
<variable name="listener" type="com.xyz.Listener" />
<!-- and other variables -->
</data>
...
</layout>
I use this variable in every single one of my data-bound layouts, and I need to access it in almost every single one of my #BindingAdapter. For instance, my binding adapters mostly look like this.
#BindingAdapter("board")
fun setBoard(view: TextView, board: Board) {
view.setText(board.name)
view.setOnClickListener {
listener.onBoardClicked(board)
}
}
#BindingAdapter("topic")
fun setTopic(view: TextView, topic: Topic) {
view.setText(topic.name)
view.setOnClickListener {
listener.onTopicClicked(topic)
}
}
// and a few others like that
and I use them like this
<TextView ... app:board="#{board}" ... />
<TextView ... app:topic="#{topic}" ... />
What I need here is a way to access that listener variable declared in the data block to all of my binding adapters. Is there a way to do that without manually passing it as a second variable every single time?
// I know I can do this - looking for an alternative
#BindingAdapter({"board", "listener"})
fun setBoard(view: TextView, board: Board, listener: Listener) {
view.setText(board.name)
view.setOnClickListener {
listener.onBoardClicked(board)
}
}
I am using Kotlin here, but a solution in Java works just fine for me as well.
After doing some more research, I've just discovered the DataBindingComponent interface and it solves precisely the problem I was having. Apparently, if you make your binding adapters instance methods rather than static, the compiler will take the class you declared it in, and add it as a property of DataBindingComponent. So I made my binding adapters instance methods, and injected the variable I wanted via the constructor.
class Binding(val listener: Listener) {
#BindingAdapter("board")
fun setBoard(view: TextView, board: Board) {
view.setText(board.name)
view.setOnClickListener {
listener.onBoardClicked(board)
}
}
#BindingAdapter("topic")
fun setTopic(view: TextView, topic: Topic) {
view.setText(topic.name)
view.setOnClickListener {
listener.onTopicClicked(topic)
}
}
}
After building, the compiler generated the following interface
package android.databinding;
public interface DataBindingComponent {
com.my.package.Binding getBinding();
}
I then completed the cycle by making the binding class extend this interface and return itself
class Binding(val listener: Listener) : DataBindingComponent {
override fun getBinding(): Binding {
return this
}
// all the other stuff
}
This allows me to pass it as an argument when inflating views, and as such I no longer have to even declare listener as an XML variable. I can just declare the Binding instance
val bindingComponent = Binding(object : Listener {
// implement listener methods here
})
and pass it when inflating the layout
// now I can use it in activities
DataBindingUtil.setContentView<MyActivityBinding>(
this, R.layout.my_activity, bindingComponent)
// ...or in fragments
DataBindingUtil.inflate<MyFragmentBinding>(
inflater, R.layout.my_fragment, parent, false)
// ...or any other place DataBindingUtil allows
I just run into a problem, using android databinding library.
Here is the 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">
<data>
<import type="com.test.app.ObservableFieldWrapper"/>
<variable
name="org"
type="ObservableFieldWrapper"/>
</data>
<LinearLayout
android:id="#+id/headerListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.test.app.NSpinner
android:id="#+id/orgSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:org="#{org.getSilent ? org.content : "silent"}"/>
</LinearLayout>
Here is my NSpinner:
public class ObservableFieldWrapper{
private final ObservableBoolean silent;
private final ObservableField<String> content;
#BindingAdapter("org")
public static void setOrg(Spinner view, String org) {
assert org != null;
if (org.equals("silent")) {
Log.i("ObsWrapper", "SET ORG called via binding adapter but got denied, because of SILENCE");
} else {
Log.i("ObsWrapper", "SET ORG called via binding adapter NORMALLY");
view.setSelection(Cache.GetOrgIndexForSpinner(), true);
}
}
public ObservableFieldWrapper(String startValue) {
content = new ObservableField<>(startValue);
silent = new ObservableBoolean();
silent.set(false);
}
public void setContent(String newValue) {
silent.set(false);
content.set(newValue);
content.notifyChange();
}
public void setContentSilent(String newValue) {
silent.set(true);
content.set(newValue);
}
//Bunch of getters
}
And this call should invoke the static getter provided, by ObservableFieldWrapper class (assume, that all bindings were already set):
ObservableFieldWrapper someField = new ObservableFieldWrapper("someString");
someField.setContent("some other string");
Well, problem is... It invokes nothing. But if I change my xml part from
app:org="#{org.getSilent ? org.content : "silent"}"
to common
app:org="#{org.content}"
It starts working! I realy need this extra functionality with boolean, and I am really lost trying to find the issue.
Found a work around, where didn't use any logics in xml expressions, I just passed 2 parameters to my function and did all job there.
#Bindable ("{org, silent}")
Yet, the question remains unanswered.
As George Mount mentioned - it's important to remove any getters on observable fields, otherwise it won't work, I spent pretty much time with this issue and then mentioned that I have a getter, after removing it - everything started working.