The Android documentation does a great job of describing how one can create a binding class using a layout xml file. But I have a couple of questions.
Is there a way to create a data binding class for a custom view that is instantiated programmatically? For example, lets say I have two custom view classes and I want to bind the same view model object to them programmatically without using any xml. The classes are as follows:
class MyViewModel {
}
class MyCustomView extends View {
}
class MyAnotherCustomView extends MyCustomView {
}
Now lets say I instantiate MyCustomView/MyAnotherCustomView using:
MyCustomView customView = new MyCustomView(context);
How do I go about using data binding in this case? Is this possible using the official Android data binding framework? If not, what other frameworks/libraries are available or recommended to achieve this?
My second question is a follow up of the first question. Lets say it is not possible to achieve what I want in my first question. Then, I will have to define a my_custom_view.xml file. This will look something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<com.example.name.MyCustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{user.firstName}"/>
</layout>
Now, if I want to use MyAnotherCustomView which is a subclass of MyCustomView keeping the binding logic the same, will I have to create a new xml file my_another_custom_view.xml just to replace MyCustomView with MyAnotherCustomView to define the same binding?
The answer to the first question is "No." Android data binding requires the XML to generate the binding classes.
In your second question, you offer a solution that will work. If you go that route, one way to do it is to use the ViewDataBinding base class setters to set your variables. I can imagine a method like this:
public void addCustomView(LayoutInflater inflater, ViewGroup container, User user) {
ViewDataBinding binding = DataBindingUtil.inflate(inflater,
this.layoutId, container, true);
binding.setVariable(BR.user, user);
}
Here, I've assumed the selection of which custom View is determined by a field layoutId. Each possible layout will have to define a user variable of type User.
I don't know the particulars of your use, but if you want to dynamically choose which custom view to load, you could use a ViewStub. You could also do the same thing with just visibility if you don't have any tremendous overhead in loading your custom Views.
<?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="user" type="com.example.User"/>
<variable name="viewChoice" type="int"/>
</data>
<FrameLayout ...>
<!-- All of your outer layout, which may include binding
to the user variable -->
<ViewStub android:layout="#layout/myCustomView1"
app:user="#{user}"
android:visiblity="#{viewChoice == 1} ? View.VISIBLE : View.GONE"/>
<ViewStub android:layout="#layout/myCustomView2"
app:user="#{user}"
android:visiblity="#{viewChoice == 2} ? View.VISIBLE : View.GONE"/>
</FrameLayout>
</layout>
Related
I have parent layout with databinding. I want to add child layout depends on logic.
I expected to need similar code:
<include
android:id="#+id/layout_id"
layout="#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}"
app:model="#{model}" />
But I get error:
android.databinding.tool.processing.ScopedException: [databinding] {"msg":"included value (#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}) must start with #layout/.","file":"x.xml","pos":[]}
Is it possible to include child layout dynamically through "if-else" condition? If yes what is the best way? Any samples are welcome!
Is it possible to include child layout dynamically through "if-else" condition?
So far, this is not possible, because the layout attribute must point to a certain layout before the data-binding works:
(Data Binding) As its name implies, it binds data to a layout, so there must be a layout first, in order to bind data to it.
In other words, the layout="#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}" doesn't work because there should be an existing layout before examining model.isOk, and also the layout attribute expects a layout resource after the # symbol.
So, we need to do that programmatically instead of the XML layout. And this an be done by replacing the <include> with ViewStub which is a sort of deferring layout inflation to be done in the activity.
So before inflating the ViewStub layout, you can have a chance to check whether the model.isOk in the activity before deciding which layout can be inflated on the ViewStub.
And hence based on the model.isOk value, we can set the layout with binding.myLayoutStub.myViewStub?.layoutResource = R.layout.layout_container_1
Eventually we can inflate the new ViewStub layout using binding.myLayoutStub.myViewStub?.inflate() method.
Using the ViewStub, will make you have a couple of binding object, one for the activity (or fragment), and the other for the inflated layout of the ViewStub.
Here is an example like yours:
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="com.example.android.databindingexample.ActivityViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="#+id/layout_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And we've layout_container_1 & layout_container_2 that only differ in the the textview value:
layout_container_1.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/purple_200">
<TextView
android:id="#+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Layout 1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModel:
class ActivityViewModel : ViewModel() {
var isOk = true
}
Then decide which layout in the activity based on the model.isOk whenever you inflate the ViewStub:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// This is of Any type as it will be casted later to the proper DataBinding generated class when the ViewStub layout inflated
private lateinit var stubBinding: Any
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val model = ViewModelProvider(this).get(ActivityViewModel::class.java)
binding.model = model
// Listener to ViewStub inflation so that we can change its layout
binding.layoutStub.setOnInflateListener { _, inflated ->
stubBinding =
(if (model.isOk) DataBindingUtil.bind<LayoutContainer1Binding>(
inflated
)
else DataBindingUtil.bind<LayoutContainer2Binding>(
inflated
))!!
// Changing the text of the inflated layout in the ViewStub using the ViewStubBinding
if (model.isOk)
(stubBinding as (LayoutContainer1Binding)).textview.text = "This is Layout container 1"
else
(stubBinding as (LayoutContainer2Binding)).textview.text = "This is Layout container 2"
}
// Inflating the ViewStub
if (!binding.layoutStub.isInflated) {
binding.layoutStub.viewStub?.layoutResource = if (model.isOk)
R.layout.layout_container_1 else R.layout.layout_container_2
binding.layoutStub.viewStub?.inflate()
}
}
}
Preview:
This is impossible with the current implementation of <include> tag. By "implementation" I mean how it is actually used by the SDK.
<include> tag is parsed in the process of layout inflation which means layout attribute of <include> tag must have a layout resource ID defined before any of your code logic can be executed. While data-binding is what you can use after a layout was successfully inflated.
I am trying to go from DataBinding to ViewBinding. And stumbled upon a problem, what to do with .xml files that use data and variable tags. I am using them to interact with the ViewModel object.
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="app.myapp.android.main.viewmodel.MainViewModel" />
</data>
I would be grateful for any help!
You can't use viewmodel in xml ,if you are moving from data binding to view binding. You can only use viewbinding to eliminate findViewById().If you want viewmodel in recycleview adapter or somewhere you can send viewmodel instance from activity to adapter through adapter constructor.
Data and variable tags are the feature of Data Binding, not View Binding.
Suppose I have Activity A with activity_a.xml. and I am setting the binding variable of my ViewModel VMA. I have another Activity B now I want to use the same activity_a.xml but with different ViewModel VMB. Now the problem is I can use the same variable name "ViewModel" for both ViewModels (VMA and VMB). And If I am using different-different variable name then How I will use them in same view property eg: android:text = "#{viewModel.text}"
Let's see only TextView case there can be multiple other cases where I would need ViewModel in XML
activity_a.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.example.VMA" />
<!--
<variable
name="viewModel" I can't use same variable name as ViewModel
type="com.example.VMB" /> -->
</data>
<TextView>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{viewModel.text}"
</TextView>
</layout>
Now I want to use the same layout with different ViewModel type VMB in XML for Activity B.
You have a strange logic in your program. I can't understand why you need use different ViewModels with one layout?. If you are using same layout it's supposed that you viewmodel structure is the same, so you can just change data in your ViewModelA and using it with your ActivityB.
I would like to change the border color of my cardview based on the content.Is it possible to reach somehow the xml file from the recyclerview adapter and change the color?
xml file from the recyclerview adapter and change the color?
No. XML is read only. And you do not need to touch it, but use i.f. findViewById(), find your card view, and use its methods to alter the color.
This is possible by using the Databinding library.
Let's say that your content is a User and you need to change the color of your CardView if he/she is an adult or a child. So you can pass the object in your Activity or Fragment like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
And then, add the data tag and the variable in your XML file:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<android.support.v7.widget.CardView
android:background="#{user.isAdult ? #color/yellow : #color/gray }"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.firstName}"/>
</android.support.v7.widget.CardView>
</layout>
If you want to know more, check the documentation : https://developer.android.com/topic/libraries/data-binding/index.html?hl=pt-br
I'm wondering if it's possible to use DataBinding to do conditionally show a layout based on a methods boolean response. Here's what I'm trying to do
XML Layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="View"
type="android.view.View"/>
<variable
name="App"
type="com.app.JTApp"/>
</data>
<include layout="#layout/view_checkout_total_cardview"
android:visibility="#{App.isLandscape() ? View.GONE : View.VISIBLE}" />
</layout>
JTApp class:
public class JTApp {
public boolean isLandscape() {
Timber.d("putty-- isLandscape: --------------------------");
return getResources().getBoolean(R.bool.is_landscape);
}
…
}
Currently this doesn't work. Am I missing something or is this not possible? I'm coming from the web where this is possible with frameworks like Angular.
Yes, using a conditional statement within XML is possible. I am not too familiar with data binding library, but a similar functionality is used in the documentation:
Zero or more import elements may be used inside the data element.
These allow easy reference to classes inside your layout file, just
like in Java.
<data>
<import type="android.view.View"/>
</data>
Now, View may be used within your binding expression:
<TextView
android:text="#{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{user.isAdult ? View.VISIBLE : View.GONE}"/>
I believe the only issue with your code is that you are using the View as a variable instead of as an import in your <data> element.
You can do this way easier with resource-modifiers:
have one layout in layout-land that does not contain the cardview
have another layout in layout that does contain the cardview
so you will get this effect and in landscape it is not even inflated and then set invisible