View Binding got released as part of Android Jetpack
Docs: https://developer.android.com/topic/libraries/view-binding
My question is, how to use view binding with custom views. Google documentation has only show-cased Activity and fragment.
I tried this, but nothing was shown.
LayoutInflater inflater = LayoutInflater.from(getContext());
And then, I used this one, but again, no luck.
LayoutInflater inflater = (LayoutInflater)
getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
I guess maybe I don't target the correct layout inflater for my view but not sure.
Just inform the root, and whether you want to attach to it
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this)
}
or
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this, true)
}
which inflate method to use will depend on the root layout type in xml.
To use the view binding, you need to use the generated binding class not the LayoutInflater, for example, if the layout name is result_profile.xml then you need to use ResultProfileBinding as:
class CustomView #kotlin.jvm.JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private lateinit var binding: ResultProfileBinding
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context))
addView(binding.root)
}
}
Auto generated class : result_profile.xml -> ResultProfileBinding(name of layout, appended with Binding )
Inflate the binding
ResultProfileBinding.inflate(LayoutInflater.from(context))
Use addView to add the view in the hierarchy as:
addView(binding.root)
Note: If you are extending from ConstraintLayout(is the parent class) then use constraint set
You can initialize the view binding property right away
private val binding = CustomViewBinding.inflate(LayoutInflater.from(context), this)
If you are trying to use View Binding with the root view, this is working for me:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private lateinit var binding: CustomViewBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = CustomViewBinding.bind(this)
}
}
This is the simplest kotlin answer I can think of. It's a custom view that just wraps a single TextView and provides an update(s:String) function to update the text.
<!-- view_stub.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="#+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</layout>
// StubView.kt
class StubView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context,attrs,defStyleAttr) {
val binding = ViewStubBinding.inflate(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.also { addView(it.root) }
fun update(updatedText: String) {
binding.myTextView.text = updatedText
}
}
The two things I like about this answer are:
binding is a val instead of a var. I try to limit the number of vars as much as possible.
The addView is closely associated with the val binding using the also {} scope function instead of an init {} clause, making the instantiation of the View feel much more declarative.
One could argue that the addView() is really a side effect and should be in the init {} section so that it is separate from the declaration of the binding val. I would argue the opposite -- declaring a val then feeding it to a section of code that needs it does not feel like a side effect to me.
You can use DataBindingUtil
binding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.your_layout_id,
this,
true
)
Related
I have a custom view that is initially not visible. However this is determined with a binding adapter. My question as simple as it sounds, how do I write a test to check if the view is visible when the binding adapter method is called?
For example, this is my custom view:
class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
fun setVisibile(visible: Boolean) {
this.visibility = if (visible) VISIBLE else GONE
}
}
And this is the binding adapter method :
#BindingAdapter(“visible)
#JvmStatic
fun setVisible(myView: MyView, someObject: SomeObject?) {
// Some checking on someObject
// ....
myView.setVisible(someObject.someCriteria())
}
I want to implement a custom view within some Fragments, and this view doesn't have any dependency on activity, this way I can share it as a common UI component to others.
But I have a problem when trying to use FragmentManager to manage those Fragments inside the custom view, how can I get a FragmentManager instance? Generally, we can get it from Activity, but in my case, what should I do would be more reasonable?
If your custom view is inflated by a fragment you have access to getParentFragmentManager there is more info here
But I have a problem when trying to use FragmentManager to manage those Fragments inside the custom view, how can I get a FragmentManager instance? Generally, we can get it from Activity, but in my case, what should I do would be more reasonable?
The best way to do it is to define an interface in the view to describe what events it has, then the fragment that hosts this view will implement this interface and actually handle it.
fun View.onClick(clickListener: (View) -> Unit) {
setOnClickListener(clickListener)
}
class MyView: FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
interface Listener {
fun onSomeEvent(someData: SomeData)
}
var listener: Listener? = null
override fun onFinishInflate() {
super.onFinishInflate()
val binding = MyViewBinding.bind(this)
with(binding) {
someButton.onClick {
listener?.onSomeEvent(someData)
}
}
}
}
And then
class MyFragment: Fragment(R.layout.my_fragment), MyView.Listener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = MyFragmentBinding.bind(view)
with(binding) {
myView.listener = this#MyFragment
}
}
}
I'm trying to use 2-way databinding on a custom view that contains a SeekBar. The layout is rather simple, but I need to reuse it across the project, hence wrapping it into a custom view/component
<androidx.constraintlayout.widget.ConstraintLayout ... />
<TextView .../>
<TextView .../>
<SeekBar
android:id="#+id/ds_seekbar"
android:layout....
android:max="9"
android:min="0"
android:progress="0"
</androidx.constraintlayout.widget.ConstraintLayout>
The backing code looks like so (reduced)
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
init {
LayoutInflater.from(context).inflate(R.layout.custom_view, this, true)
ds_description.setOnClickListener(this)
}
override fun onClick(view: View) {
//onClick implementation
}
}
I can do the binding in the ViewModel for the layout where this custom view is going to be used, with a BindingAdapter there with custom attribute (ex. app:seekbar), but the custom view would be used multiple times and I'd prefer to have the a lot of the logic that is required into the view and have a "lighter" handling in the ViewModel.
I read Android 2-Way DataBinding With Custom View and Custom Attr and a bunch of other articles which seem to be a little different but oon the same topic, however no matter how I wrote the getter and setters I always run into the kapt exception that it cannot find the getter/setter.
Either I'm not annotating properly the methods or they have wrong signatures.
Ideally I want to have something like:
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener, SeekBar.OnProgressChangedListener {
... ds_seekbar.setOnProgressChangedListener(this)
And then in the main layout have the app:progress (or even better if someone can show how it's done android:progress) on the custom view for binding when passing my object.
Okay after more and more headscratching, here's what I've come with, that seems to work. Whether this is the proper way or how performant/reliable is - I'm not sure
#InverseBindingMethods(InverseBindingMethod(type = CustomView::class, attribute = "progress", event = "progressAttrChanged"))
CustomView #JvmOverloads constructor(...
private var progress = 0
private var mInverseBindingListener: InverseBindingListener? = null
cv_seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
progress = i + 1
if (mInverseBindingListener != null) {
mInverseBindingListener!!.onChange()
cv_indicator.text = progress.toString()
}
}...
})
fun getProgress(): Int {
return progress
}
fun setProgress(p: Int) {
if (progress != p) {
progress = p
}
}
fun setProgressAttrChanged(inverseBindingListener: InverseBindingListener?) {
if (inverseBindingListener != null) {
mInverseBindingListener = inverseBindingListener
}
}
Then the XML is
<com.xxx.CustomView
android:id="#+id/xxx"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:progress="#={viewModel.dataobject.value}"
....
/>
I have a Custom Linear layout and an xml file in with 3 checkboxes. The Custom linear layout looks roughly like this:
class AdvancedBox : LinearLayout, DefineExchangesDialog.DefineExchangesDialogListener {
private lateinit var mBinding: AdvancedBoxBinding
private lateinit var viewModel: GlobalConfigViewModel
constructor(c: Context) : super(c) {
initLayout()
}
constructor(c: Context, a: AttributeSet) : super(c, a) {
initLayout()
}
private fun initLayout() {
val inflater = LayoutInflater.from(context)
mBinding = AdvancedBoxBinding.inflate(inflater)
}
override fun getRootView(): View {
return mBinding.root
}
fun setViewModel(viewModel: GlobalConfigViewModel){
mBinding.viewModel = viewModel
}
override fun onFinishInflate() {
super.onFinishInflate()
//cbVolumeChange is an id defined in advanced_box.xml
cbVolumeChange.setOnClickListener(this::onShowAdvanced)
cbExchanges.setOnClickListener(this::onShowAdvanced)
cbPriceFilter.setOnClickListener(this::onShowAdvanced)
}
The problem is that setting an onClickListener on any of the checkboxes in onFinishInflate() will cause a NullPointerException. I thought overriding the getRootView() would be the solution but that did not work. Something that would work is the following
val root = mBinding.root
root.cbVolumeChange ...
But that is not something I would like to do. So what would be the right way to use the Android Kotlin extension with databinding in my case?
The issue is, that the inflated view is not attached to the layout. You should use AdvancedBoxBinding.inflate(inflater, this, true) for it.
After that onFinishInflate() doesn't matter for you, as it's only relevant for inflating the view and the hierarchy from XML not from code as you're doing.
If we are building a custom View, for example, something like this:
class FrameLayoutNormal: FrameLayout{
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
textView{
lparams(...)
}
}
we can't define lparams, because the compiler doesn't know who the parent is. If we wrap the textView inside a FrameLayout it works, and you scan specify a layout parameter. But in a custom view, the parent is itself. So how can we make the children be aware of that so we can use the extension?
Is there any way to get it working, besides extending from: _FrameLayout ?`
An old question, but since it is common ...
Applying the answer from https://github.com/Kotlin/anko/issues/267
I think you might want something like this:
class FrameLayoutNormal: AnkoComponent<Context> {
override fun createView(ui: AnkoContext<Context>): View {
return with(ui) {
frameLayout {
textView("Hello") {
}.lparams()
}
}
}
}
inline fun ViewManager.frameLayoutNormal(theme: Int = 0) = frameLayoutNormal(theme) {}
inline fun ViewManager.frameLayoutNormal(theme: Int = 0, init: View.(frameLayoutNormal: FrameLayoutNormal) -> Unit): View {
val fln = FrameLayoutNormal()
return ankoView({ fln.createView(AnkoContext.create(it))}, theme, {init(fln)})
}
This allows the component to be used in the ANKO DSL. One downside to this approach is that the custom component is a View, not a ViewGroup, and thus can not have additional children added outside of its definition. It is challenging/laborious to make a custom component which is a ViewGroup that can be used in ANKO DSL (if I understand correctly).