Databinding can't call static function with String.function() - android

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"/>

Related

How to set app:icon dynamically with data binding?

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
}

Why does my data-binding with EditText not work?

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)
}

Android two way data binding on custom View. Cannot find getter

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".

Kotlin annotation - Require a parameter is a Constant variable from specific class

I have a function filter here
fun filter(category: String) {
...
}
and a Class with many constant string
object Constants {
val CAT_SPORT = "CAT_SPORT"
val CAT_CAR = "CAT_CAR"
...
}
How to ensure the parameter category is a constant string from Constants (or throw warning)?
I am looking for something like #StringRes.
I know Enum may do the trick but prefer not to code refactor at this moment.
Using androidx.annotation you can do something like this:
object Constants {
#Retention(AnnotationRetention.SOURCE)
#StringDef(CAT_SPORT, CAT_CAR)
annotation class Category
const val CAT_SPORT = "CAT_SPORT"
const val CAT_CAR = "CAT_CAR"
}
fun filter(#Constants.Category category: String) {
...
}

Kotlin: Java Util Date to String for Databindings

I want to use the Date value of my Data class in view via Databinding.
If I use the toString() method on the Date field it works. But I want to customize the Date value.
So I created the Utils object with Method. This is the Util object
object DateUtils {
fun toSimpleString(date: Date) : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(date)
}
}
But if I want to use this method in the xml like this
<data>
<import type="de.mjkd.journeylogger.Utils.DateUtils"/>
<variable
name="journey"
type="de.mjkd.journeylogger.data.Journey"/>
</data>
...
android:text="#{DateUtils.toSimpleString(journey.date)}"
I get an error cannot find method toSimpleString(java.util.Date) in class ...
This is my Dataclass:
data class Journey(var title: String, var date: Date?, var destination: String)
Whats wrong with this code?
Using the reserved word object in kotlin, that you really doing is declare a single instance. the equivalent in java is something more or less like:
class DataUtils {
static DataUtils INSTANCE;
public String toSimpleString()...
}
then when you call it you do a DateUtils.INSTANCE.toSimpleString()
You should capable to use DateUtils.INSTANCE.toSimpleString() in your xml
In order to make toSimpleString accessible from static context, you have to flag the method with#JvmStatic
object DateUtils {
#JvmStatic
fun toSimpleString(date: Date) : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(date)
}
}
Using extension function(doc)
#file:JvmName("DateUtils")//Use this to change your class name in java, by default is <the file name>Kt (DateUtilsKt in your case)
fun Date.toSimpleString() : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(this)
}
Then you can use it directly in xml as you are already doing:
android:text="#{DateUtils.toSimpleString(journey.date)}"
Why don't you just use a top-level function which is static by default? A top-level function is not defined in any class.
fun main(args: Array<String>){
println(toSimpleString(Date()))
}
fun toSimpleString(date: Date?) = with(date ?: Date()) {
SimpleDateFormat("dd/MM/yyy").format(this)
}
Also, notice how Jouney's date is nullable in your example and your toSimpleString only accepts a non-nullable Date!
I changed it, so that it will return the string of the current date in case null is passed.
More easy way would be to make a getDateString in model class.
android:text="#{journey.dateString)}"
class Journey {
lateinit var date: Date
fun getDateString(){
return DataUtils.toSimpleString(date)
}
}
I like this way because I don't need to import any class in this case.

Categories

Resources