I am implementing two way data binding on custom View. I followed the official android developers but still can't make it work. I have a knob that controlls integer value inside the value property.
class ControlKnob(context: Context, attributeSet : android.util.AttributeSet) : RelativeLayout(context, attributeSet), IUIControl {
companion object {
#JvmStatic
#BindingAdapter("value")
fun setValue(knob : ControlKnob, value : Int) {
if(knob.value != value) {
knob.value = value
}
}
#JvmStatic
#InverseBindingAdapter(attribute = "value")
fun getValue(knob : ControlKnob) : Int {
return knob.value
}
#JvmStatic
#BindingAdapter("app:valueAttrChanged")
fun setListeners( knob : ControlKnob, attrChange : InverseBindingListener) {
knob.setOnProgressChangedListener {
attrChange.onChange()
}
}
}
var value : Int = -1
set(value) {
field = value
valueView.text = stringConverter.invoke(value)
}
....
....
}
Inside layout i use it like this:
<cz.abc.def.package.controls.ControlKnob
android:id="#+id/knob"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_row="0"
android:layout_column="0"
app:value="#={viewModel.value}"
app:label="Knob" />
And my view model:
#Bindable
fun getValue() : Int {
return someValue
}
fun setValue(value : Int) {
someValue = value
}
But still i can't compile it. I get
Cannot find a getter for cz.abc.def.package.controls.ControlKnob app:value that accepts parameter type 'int'
If a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.
What could be the cause of this ?
I figured it out. It turned out that it is not problem with the code. I was missing the apply plugin: 'kotlin-kapt' in the gradle build file. After i added this line into build.gradle in the module it worked.
Maybe your listener binding adapter is wrong? Per the documentation, the event listener BindingAdapter value should be "android:valueAttrChanged" and you have "app:valueAttrChanged".
Related
class SomeDetector : Detector(), SourceCodeScanner {
override fun getApplicableConstructorTypes(): List<String>? {
return listOf(PARENT_CLASS)
}
override fun visitConstructor(context: JavaContext, node: UCallExpression, constructor: PsiMethod) {
// blabla...
}
}
Ok, I've even succeeded in applying lint to individual constructors for each class.
However, there are hundreds of classes I want to validate, and they all inherit a common interface.
So I want to verify the constructors of all classes inheriting the interface I specified.
The class I want to verify has an android dependency, so libraries like reflections cannot be used directly in the lint module, which is a java-library.
Can you help me to meet my requirements?
I gave your problem a go. I have checked if the value in annotation and the argument used are equal. You can tweak the code to suit your requirement. Here is a sample Detector class wherein I have provided explanations using comments. You can improve on it.
class InvalidConstructorCallDetector : Detector(), Detector.UastScanner {
// Check for call expressions
override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
// Check if call is constructor call and if the class referenced inherits your interface
if (node.isConstructorCall() &&
context.doesInherit(node, "com.example.android.YourInterface")
) {
val constructor = node.resolve()!!
// Get the first parameter. You may use a loop and check for all parameter or whatever you require
val param = constructor.parameterList.parameters[0]
// Get your annotation
val paramAnnotation =
param.getAnnotation("com.example.android.YourAnnotation")!!
// Get the value you specified in the annotation for the constructor declaration
val attributeValue = paramAnnotation.findAttributeValue("value")!!.text.toInt()
// Get the argument used for first parameter. Again, you can have this in a loop and use index
val argumentValue = node.getArgumentForParameter(0)!!.evaluate().toString().toInt()
if (attributeValue != argumentValue) // Checking for equality. Perform whatever check you want
{
context.report(
ISSUE,
node,
context.getNameLocation(node),
"Argument value($argumentValue) is invalid. Valid argument: $attributeValue"
)
}
}
}
}
// Check to see if class referenced by constructor call implements interface
private fun JavaContext.doesInherit(node: UCallExpression, typeName: String): Boolean {
for (type in node.classReference!!.getExpressionType()!!.superTypes) {
if (evaluator.typeMatches(type, typeName)) return true
}
return false
}
companion object {
val ISSUE = Issue.create(
"InvalidConstructorCall",
"Invalid arguments in constructor call",
"Only values defined in annotation in constructor declaration are allowed",
Category.CORRECTNESS,
10,
Severity.ERROR,
Implementation(
InvalidConstructorCallDetector::class.java,
EnumSet.of(Scope.JAVA_FILE)
)
)
}
}
I have this XML:
<Button
android:id="#+id/btn_default"
app:icon="#{model.actionBarData.myDynamicIcon}" />
And I have this method and LiveData in my model's actionBarData to set the icon programmatically:
private var _myDynamicIcon = MutableLiveData<Int>()
val myDynamicIcon: LiveData<Int>
get() = _myDynamicIcon
// Called by some logic in my app
fun setMyDynamicIcon() {
_myDynamicIcon.value = when (status) {
status.STATUS1 -> R.drawable.icon1
status.STATUS2 -> R.drawable.icon2
status.STATUS3 -> R.drawable.icon3
}
}
I want the icon to change when setMyDynamicIcon is called. However I get error:
Cannot find a setter for <android.widget.Button app:icon> that accepts parameter type 'androidx.lifecycle.LiveData<java.lang.Integer>'
I also tried storing a Drawable object in myDynamicIcon, this did not work either (same error but with Drawable type).
How can I set the app:icon via data binding?
You can use Binding Adapters, you just need to change your setMyDynamicIcon() implementation a little bit(i.e., make it a binding adapter method), other code is pretty much copy/paste from the provided link and it'll work fine.
Thx #generatedAcc.x09218. Final code using Binding Adapters:
XML:
<Button
android:id="#+id/btn_default"
app:dynamicIcon="#{model.actionBarData.status}" />
Adapter:
#BindingAdapter("dynamicIcon")
fun View.setDynamicIcon(status: Status?) {
status?.let {
val iconResource = when(status) {
Status.STATUS1 -> R.drawable.ic_1
Status.STATUS2 -> R.drawable.ic_2
Status.STATUS3 -> R.drawable.ic_3
}
(this as MaterialButton).setIconResource(iconResource)
}
}
LiveData:
private var _status = MutableLiveData<Status>()
val status: LiveData<Status>
get() = _status
fun setStatus(status: Status) {
_status.value = status
}
The icon changes on setStatus call.
There is universal BindingAdapter
#BindingAdapter("dynamicIcon")
fun setDynamicIcon(button: MaterialButton, #DrawableRes resourceId: Int) =
button.setIconResource(resourceId)
example use
app:dynamicIcon="#{viewModel.iconDepositButton(item)}"
BONUS
#BindingAdapter("dynamicIconGravity")
fun setIconGravity(button: MaterialButton, #MaterialButton.IconGravity iconGravity: Int) {
button.iconGravity = iconGravity
}
I have this class which I use for Room:
data class Piece(
#ColumnInfo(name = "title")
var title: String
)
Then I have a form class, which simply holds string values which I want to be shown in EditText.
class CreateEditPieceForm {
var title: String = ""
}
My ViewModel holds instances of these classes:
class EditPieceViewModel(...) : AndroidViewModel(application) {
val piece : LiveData<Piece?> = database.getMyPiece() // valid Piece with title set
val form = CreateEditPieceForm()
}
In my fragment I observe the piece:
viewModel.piece.observe(this, Observer {piece ->
piece?.let {
viewModel.updateInputValues(piece)
}
})
updateInputValues function in the ViewModel simply sets values in the form:
fun updateInputValues(piece: Piece) {
Log.d("mylog", "value: " + piece.title) // logs correct value
form.title = piece.title // setting this does not change EditText
}
And finally, in my layout, I try to use data binding to show the text from form.title in EditText:
<data>
<variable
name="viewModel"
type="com.example.tutorial.createedit.CreateEditPieceViewModel" />
</data>
<!-- ... -->
<EditText
android:id="#+id/input_title"
<!-- ... -->
android:inputType="text"
android:text="#={viewModel.form.title}" />
When I open the Screen with this fragment, EditText is empty. I know that the query for piece title is correct, because I log it before I set EditText's text attribute.
When I type something in the empty field, value of viewModel.form.title is being set with that value.
Why does it not set right at the beginning?
Databinding is not to be confused with
View binding.
Like in the guide, make EditPieceViewModel implement Observable and make form.title #Bindable.
Now it works. I still do not fully understand the Databinding library, so any advice to improve the code is welcomed!
I changed my ViewModel to implement Observable and added some methods to it:
// Make sure to import the correct Observable interface
import androidx.databinding.Observable
// ...
class EditPieceViewModel(...) : AndroidViewModel(application), Observable {
// ...
// New getter and setter methods for my title field:
#Bindable
fun getTitle() : String {
return form.title
}
fun setTitle(value: String) {
if(form.title != value) {
form.title = value
notifyPropertyChanged(BR.title) // This line is important for EditText' value to update
}
}
// New methods added for Observable:
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the #Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* #param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
In layout xml I changed title to observe viewModel.title instead of viewModel.form.title:
<EditText
// ...
android:text="#={viewModel.title}" />
Finally in updateInputValues I call my custom setter:
fun updateInputValues(piece: Piece) {
setTitle(piece.title)
}
I have a function which formats some text
fun String.formatTo(): String {
if (this.isNotEmpty()) {
val value = this.toDouble()
return "%.02f".format(value)
}
return ""
}
And I want to apply this fun to my textView, using databinding, so I called in textView android:text="#{viewModel.text.formatTo()}", importing class in data of my layout
<data>
<import type="com.project.utils.extensions.ExtKt"/>
<variable
name="viewModel"
type="com.project.ViewModel" />
</data>
But I've got an error throw building:
Found data binding errors.
****/ data binding error ****msg:cannot find method formatTo() in class java.lang.String
What is a problem?
Create an object named ExtKt (or anything you want) and define your extension function in it and annotate it with #JvmStatic like below
#JvmStatic
fun String.formatTo(): String {
if (this.isNotEmpty()) {
val value = this.toDouble()
return "%.02f".format(value)
}
return ""
}
Update
android:text="#{ExtKt.formatTo()}"
Databinding is still Java modules, so some features of kotlin like extension functions can't be used there. The only thing you can do here - create specific function in your ViewModel class.
class ViewModel {
val text: String
...
fun getDisplayText(): String = text.formatTo()
}
May be you want to use calculated properties.
val displayText: String get() = text.formatTo()
Anyway, your xml call will look like following:
android:text="#{viewModel.displayText}"
Consider use MediatorLiveData:
class ViewModel(
val list: MutableLiveData<List<String>> = MutableLiveData<List<String>>()
) {
val listStr = MediatorLiveData<String>()
init {
listStr .addSource(list, Observer {
listStr .postValue(ViewModel.joinList(it))
})
}
companion object {
#JvmStatic fun joinList(list: List<String>): String {
return list.joinToString(separator = ", ")
}
}
}
And than in the xml:
<TextView
android:id="#+id/items"
android:text="#{viewModel.listStr}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
I'm a newbie for android data binding.
I want to bind multiple SeekBars to a collection of float, such as
SeekBar1.progress <---> modelArray[0]
SeekBar2.progress <---> modelArray[1]
...
Since the progress of SeekBar is a Int type, I think it would be better to use Converter, and below is the converter code:
import android.databinding.InverseMethod
import android.util.Log
import kotlin.math.roundToInt
class Converter {
#InverseMethod("progressToFloat")
fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
fun progressToFloat(value: Int): Float {
return value.toFloat()/100.0f
}
}
and the model structure looks like:
class Model {
val params by lazy {
ObservableArrayMap<Int, Float>()
}
//....
}
and my xml is following:
<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 class="MyBinding">
<variable name="Converter" type="com.mypackage.model.Converter"></variable>
<variable
name="Converter"
type="com.mypackage.model.Converter"></variable>
<variable
name="model"
type="com.mypackage.model.Model"></variable>
</data>
...
<SeekBar
android:id="#+id/seekBar"
android:layout_width="100dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:progress="#={Converter.floatToProgress(model.params[0])}"/>
The problem is, every time I build it, it shows :
Found data binding errors.
****/ data binding error ****
msg:The expression converter.floatToProgress(modelParams0) cannot be inverted:
There is no inverse for method floatToProgress, you must add an #InverseMethod
annotation to the method to indicate which method should be used when using
it in two-way binding expressions
I already refer to lots of website, includes following:
https://medium.com/androiddevelopers/android-data-binding-inverse-functions-95aab4b11873
https://developer.android.com/topic/libraries/data-binding/two-way
But I still cannot find out what problem is. Could anyone give me some suggestion?
My development environment
macOS High Sierra,
Android Studio 3.2.1, with compileSdkVersion 28 and gradle 3.2.1
Update:
I also try to write Converter as following :
object Converter { // change to object
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int { // Add static
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float { // Add static
return value.toFloat()/100.0f
}
}
It still does not work.
Solution
Ok, I found the problem: My project does not use Kapt.
After add it to build.gradle, all works fine.
apply plugin: 'kotlin-kapt'
Also, I update my converter body as following:
object Converter {
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float {
val result = value.toFloat()/100.0f
Log.i("MODEL", "convert $value to $result(Float)")
return result
}
}
If the #JVMStatic removed, the project can compile successfully, but the binding does not work.
Thanks everyone who give my suggestion. I think I just ask a silly question.
Refer to:Databinding annotation processor kapt warning
Solution
Ok, I found the problem: My project does not use Kapt. After add it to build.gradle, all works fine.
apply plugin: 'kotlin-kapt'
Also, I update my converter body as following:
object Converter {
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float {
val result = value.toFloat()/100.0f
Log.i("MODEL", "convert $value to $result(Float)")
return result
}
}
If the #JVMStatic removed, the project can compile successfully, but the binding does not work.
Thanks everyone who give my suggestion. I think I just ask a silly question.
Refer to:Databinding annotation processor kapt warning
In my case the problem was that I was trying to make a custom TwoWay DataBinding Adapter
but my field in the ViewModel class was LiveData instead of MutableLiveData