Android Studio 2020.3.1 Canary 15 supports databin...
# android
m
Android Studio 2020.3.1 Canary 15 supports databinding with StateFlow<T> in viewmodels. Does it support two-way databinding when the view property is also a StateFlow<T>? If now, why?
g
It supports, but only if it MutableStateFlow, same as with LiveData and MutableLiveData
m
It's asking me to provide BindingAdapters...
Copy code
class RingViewModeSelector @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.ringViewModelSelectorStyle
) : LinearLayout(context, attrs, defStyleAttr), DefaultLifecycleObserver {
    val binding = RingViewModeSelectorBinding.inflate(layoutInflater, this)

    val modeStateFlow = MutableStateFlow(RingDisplayMode.Progress)
    var mode by modeStateFlow

    init {
        loadAttributes(attrs, defStyleAttr)

        binding.dotsIndicator.initDots(RingDisplayMode.values().size)

        binding.root.setOnTouchListener(object : OnSwipeTouchListener(context) {
            override fun onSwipeLeft() {
                modeStateFlow.cycleForward()
            }

            override fun onSwipeRight() {
                modeStateFlow.cycleBackward()
            }
        })
    }
    private fun loadAttributes(attrs: AttributeSet?, defStyleAttr: Int) {
        context.useStyledAttributes(
            attrs,
            R.styleable.RingViewModeSelector, defStyleAttr,
            R.style.RingViewModeSelector
        ) { array ->
        }
    }

    fun registerLifecycleOwner(lifecycle: Lifecycle){
        lifecycle.addObserver(this)
    }

    override fun onCreate(owner: LifecycleOwner) {
        super.onCreate(owner)

        owner.lifecycleScope.launch {
            modeStateFlow.collect {
                updateDotSelection(it)
                listeners.forEach { listener -> listener.onChange() }
            }
        }
    }

    val listeners = mutableListOf<InverseBindingListener>()

    private fun updateDotSelection(mode: RingDisplayMode) {
        binding.dotsIndicator.setDotSelection(mode.ordinal)
    }
}

@BindingAdapter("ringDisplayMode")
fun <T> setRingDisplayMode(view: RingViewModeSelector, data: RingDisplayMode) {
    view.mode = data
}

@InverseBindingAdapter(attribute="ringDisplayMode")
fun getRingDisplayMode(view: RingViewModeSelector) : RingDisplayMode =
    view.mode

@BindingAdapter("ringDisplayModeAttrChanged")
fun setListeners(
    view: RingViewModeSelector,
    attrChange: InverseBindingListener
) {
    view.listeners.add(attrChange)
}
The application does not compile if I don't provide all that.
I get
Copy code
Cannot find a getter for <x.y.z.RingViewModeSelector app:ringDisplayMode> that accepts parameter type 'x.y.z.RingDisplayMode'
I attempted to bind with both
Copy code
app:modeStateFlow="@={triadPlaybackAppModel.ringDisplayModeStateFlow}"
and
Copy code
app:ringDisplayMode="@={triadPlaybackAppModel.ringDisplayModeStateFlow}"
with
Copy code
@BindingAdapter("ringDisplayMode")
fun <T> setRingDisplayMode(view: RingViewModeSelector, data: StateFlow<RingDisplayMode>) {
    view.mode = data.value
}

@InverseBindingAdapter(attribute="ringDisplayMode")
fun getRingDisplayMode2(view: RingViewModeSelector) : StateFlow<RingDisplayMode> =
    view.modeStateFlow
All
StateFlow
involved are
MutableStateFlow
.
g
It’s incorrect usage imo
m
Could you tell me what's incorrect or what's the correct usage?
g
If you need 2 way binding, you shouldn’t use StateFlow as argument, it’s also applied for LiveData/ObservableField
so data: StateFlow<RingDisplayMode> -> data: RingDisplayMode
and
Copy code
fun getRingDisplayMode2(view: RingViewModeSelector) : StateFlow<RingDisplayMode>
should be
Copy code
fun getRingDisplayMode2(view: RingViewModeSelector) : RingDisplayMode
m
I attempted that. If I do that, I get asked:
Copy code
Could not find event 'ringDisplayModeAttrChanged' on View type 'x.y.z.RingViewModeSelector'
I have to provide all the boilerplate binding implementation.
If that's the way two-way databinding with StateFlow is implemented, then two-way databinding with StateFlow is not supported...
Look at the long code above. It works. It's lots of code for each property that I have to bind two-way.
g
then two-way databinding with StateFlow is not supported
Of course they are not supported, two-way bindings supported on MutableStateFlow
m
I'm using MutableStateFlow
Please look at the code above. It's all MutableStateFlow
g
I have to provide all the boilerplate binding implementation.
No, if you bind to property which is standard, but in your case it’s not standard as I see
Look at the long code above. It works. It’s lots of code for each property that I have to bind two-way.
Yes, you need this for every cuystom view property, it’s also true for all standard binding adapters
m
Of course I'm talking about non-standard properties: "Does it support two-way databinding when the view property is also a
[Mutable]StateFlow<T>
" What standard property in any standard view on Android is implemented as
MutableStateFlow<T>
?
g
What standard property
Like checked in Checkbox, text in TextView etc
And they do not require any support of MutableStateFlow
because they same way they do not support LiveData or ObservableField directly
it’s higher level API and all binding adapters work with all supported observable properties
m
TextView does not use MutableStateFlow. I bet no standard view use kotlin flows.
g
Oh, again, TextView and binding adapter for TextView text doesn’t know about MutableStateFlow, neither it knows about MutableLiveData See how it implemented: https://android.googlesource.com/platform/frameworks/data-binding/+/refs/heads/studio-mas[…]ndroidx/databinding/adapters/TextViewBindingAdapter.java
Maybe I do not understand what you trying to achieve, but what I see above is attempt to write 2-way binding for custom atribute
Cannot find a getter for <x.y.z.RingViewModeSelector app:ringDisplayMode> that accepts parameter type ‘x.y.z.RingDisplayMode’
I think you have this issue because you don’t have event in your InverseBindingAdapter
m
There is no specific support for two-way databindings on properties that are implemented as
MutableStateFlow<T>
. You have to write all the adapters and the notification by yourself, even if the property is observable. It's a shortcoming of the databinding system.
Thanks for trying to help me, Andrey.
The very signature of this adapter is flawed:
Copy code
@BindingAdapter("somethingAttrChanged")
fun setListeners(
    view: View,
    attrChange: InverseBindingListener
) {
     // Your implementation
}
What if you bind and unbind multiple listeners? There is no way to remove a listener, unless you implement the notification mechanism as "single listener only", in which case you can swap a listener for another, but can't server multiple listeners.
g
Same as with all other bindings, attrChange must be nullable, so it will be null on binding detach
m
I know that. In fact I wrote: There is no way to remove a listener, unless you implement the notification mechanism as "single listener only". If you implement setListeners to allow multiple listeners, you won't know which one you have to remove.
g
I know that
Your listener is non-nullable and it may crash on runtime If you have multiple listeners and want to unregister old one, there are also ways to do that, you can receive “old” version of param, see official doc, there is an example of
android:onLayoutChange
binding, which is exactly your caser: https://developer.android.com/topic/libraries/data-binding/binding-adapters#custom-logic
There is no specific support for two-way databindings on properties that are implemented as 
MutableStateFlow<T>
. You have to write all the adapters and the notification by yourself, even if the property is observable.
It’s a shortcoming of the databinding system.
I’m not quite sure that understand what you trying to achieve here.
the notification by yourself
Bindings (starting from AGP 7.0) subscribe on notifications on your StateFlow and automatically update UI. For two-way bindings you need adapter for every property, there is no shortcuts there. Maybe I just don’t understand your case, but from what I see above you need a standard 2-way binding adapter
Even if your View exposes listener as MutableStateFlow, you just should subscribe on it as to any other listener and unsusbscribe when it changed, but it’s not a part of InverseBinding, or Binding, it’s a part of event binding