Why my data-binding annotation(#BindingAdapter("imageURL")) doesn't work? - android-databinding

I've written my custom recyclerView and have got a problem. But when I wrote an annotation to mark a setter for my imageView I got a compilation error.
Here are the source code and errors.
Error:
Cannot find a setter for <com.makeramen.roundedimageview.RoundedImageView app:imageURL> that accepts parameter type 'java.lang.String'
If a binding adapter provides the setter, check that the adapter is annotated correctly and that the parameter type matches.
Setter code:
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import java.lang.Exception
class BindingAdapters {
#BindingAdapter("imageURL")//compilation error
fun setImageURL(imageView: ImageView, URL: String?) {
imageView.alpha = 0f
try {
Picasso.get().load(URL).noFade().into(imageView, object : Callback {
override fun onSuccess() {
imageView.animate().setDuration(300).alpha(1f).start()
}
override fun onError(e: Exception) {
}
})
} catch (ignored: Exception) {
}
}
}
ImageView xml code:
<data>
<variable
name="eventShow"
type="course.ru.qsearcher.models.Event" />
</data>
........
<com.makeramen.roundedimageview.RoundedImageView
android:id="#+id/imageEvent"
android:layout_width="#dimen/_70sdp"
android:layout_height="#dimen/_100sdp"
android:layout_marginStart="#dimen/_10sdp"
app:imageURL="#{eventShow.imagePath}"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:riv_corner_radius="#dimen/_4sdp" />
Event is a data class with some fields like the title of an event, description, image URL, and so on.
сode
xml usage
url-field

Change android:imageURL to imageURL, the android: prefix is for built-in attribute like android:text.
And use it like imageURL or app:imageURL in your ImageView.

I didn't add the Kotlin plugin to my gradle file
apply plugin: "kotlin-kapt"
That solved my problem

Related

Invoke receiver function declared in interface

I'm trying to create an easy-to-use html generator for a personal project. I thought I would use extension functions to be able to generate an html programmatically using something like this:
html {
head {
title("Site title")
}
body {
div {
// stuff in div
}
}
}
For that I declared an interface:
fun interface TagBlock {
operator fun Tag.invoke()
}
Where Tag would be the class designating the specific tags, like html, body, div etc:
class Tag(val name: String)
I now tried to create a function which accepts the earlier mentioned interface and returns a tag:
fun html(block: TagBlock): Tag {
val html = Tag("html")
// invoke `block` with `html`
return html
}
I'm stuck on how to invoke the provided parameter block. The following all don't work:
block(html) // Unresolved reference
block.invoke(html) // unresolved reference
html.block() // Unresolved reference: block
Where am I doing something wrong?
The invoke() operator you're declaring has 2 receivers:
the dispatch receiver TagBlock
the explicit receiver Tag
You need to provide the dispatch receiver in the context of your call for it to work. You can do this with the library function with():
fun html(block: TagBlock): Tag {
val html = Tag("html")
with(block) {
html.invoke()
}
return html
}
This may or may not be the usage experience you were looking for, though.
A more idiomatic approach in Kotlin would be to just take a function type as input:
fun html(block: Tag.() -> Unit): Tag {
val html = Tag("html")
html.block()
return html
}

How to get a RecyclerView Item to update itself from a LiveData value in it's binding object using BindingAdapter?

I have a recyclerview.widget.ListAdapter to display some cards in a grid. When I click on a card, it comes to the front of the grid, flips, and when you see the front, you are supposed to be able to like it. I have an ImageView on the corner of the front of the card, that is supposed to toggle between liking or not, displaying a different drawable that I have already defined.
My ViewHolder looks like this:
class GameCardViewHolder(private val binding : ItemGameCardBinding) :
RecyclerView.ViewHolder(binding.root){
companion object {
fun from(parent: ViewGroup): GameCardViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemGameCardBinding.inflate(layoutInflater, parent, false)
return GameCardViewHolder(binding)
}
...
}
fun bind(productCard: ProductCard, listener: OnClickListener){
binding.productCard = productCard
binding.executePendingBindings()
...
}
}
My item_game_card.xml looks like this:
<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>
<variable
name="productCard"
type="company.app.model.ProductCard" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
...
<ImageView
android:id="#+id/gameItemLikeImageView"
android:layout_width="0dp"
android:layout_height="0dp"
...
app:imageDrawable="#{productCard.liked}"
android:onClick="#{() -> productCard.toggleLike()}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My BindingAdapters.kt:
#BindingAdapter("imageDrawable")
fun bindImageDrawable(imgView: ImageView, boolean: LiveData<Boolean>?) {
Timber.d("Binding liked: $boolean")
when(boolean?.value){
true -> imgView.setImageResource(R.drawable.ic_card_like)
false -> imgView.setImageResource(R.drawable.ic_card_dislike)
}
}
And my ProductCard.kt:
data class ProductCard(
val position: Int,
val product: Product
){
...
private val _liked = MutableLiveData(false)
val liked : LiveData<Boolean>
get() = _liked
fun toggleLikeProduct(){
Timber.d("Toggle like before: ${_liked.value}")
val oldValue = _liked.value!!
_liked.value = !oldValue
Timber.d("Toggle like after: ${_liked.value}")
}
}
When I click on the ImageView, the console prints out the before and after values every time correctly, so I know the value of the liked LiveData is actually changing, but the View is not updating accordingly, that is because the binding adapter is only being called once (in the binding.executePendingBindings() I guess, when the RecyclerView binds the ViewHolder, and not every single time the LiveData changes).
I want the BindingAdapter code to be run every time the liked LiveData changes.
When I do this with ViewModel and Fragment layout, every time a LiveData changes in the ViewModel, the View changes because the xml is referencing that LiveData, but I don't know why, when it comes to ProductCard and recyclerview item layout, even if the xml is referencing that LiveData, the View doesn't change, the xml is not actually binding to the LiveData, which is the whole point of having LiveData.
I have already acheived this behaviour with a very brute, unelegant sort of way, setting an onClickListener on that view, and forcing it to change it's resource drawable, on the OnBindViewHolder like this:
fun bind(productCard: ProductCard, listener: OnClickListener){
binding.productCard = productCard
binding.executePendingBindings()
binding.gameItemLikeImageView.setOnClickListener {
it?.let {
val card = binding.productCard
Timber.d("Tried changing liked status: ${card!!.liked}")
val old = card.liked
card.liked = !old
binding.productCard = card
val card2 = binding.productCard
Timber.d("Tried changing liked status: ${card2!!.liked}")
if (!old){
it.setBackgroundResource(R.drawable.ic_card_like)
}
else {
it.setBackgroundResource(R.drawable.ic_card_dislike)
}
}
}
...
}
But I would really like that the behaviour of each card be controlled by the state of the ProductCard object that it is bind to.
I have also tried to force call binding.executePendingBindings() every time I click, but also didn't work.
How can I make the item_game_card.xml to observe the changes in the LiveData?
app:imageDrawable won't accept any LiveData<Boolean> and I'd wonder why it isn't LiveData<Product>? Try to bind the initial value alike this:
app:imageDrawable="#{product.liked ? R.drawable_liked : R.drawable_neutral}"
For LiveData the LifecycleOwner needs to be set for the binding:
class ProductsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: ProductsBinding = DataBindingUtil.setContentView(this, R.layout.products)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
}
}
see the documentation.

Call extension function nested inside an object, from outside that object

How do I call an extension function that's nested inside an object from outside that object?
Here is an example:
package test
object Example {
fun String.exampleExtension() {
}
}
fun test(str: String) {
str.exampleExtension() // Doesn't work. How can I use that extension function?
}
As far as I can tell this isn't possible, but I'm not sure, so I figured I'd ask.
There are 2 ways you can do this.
The simplest is:
fun test(str: String) {
with (Example) {
str.exampleExtension()
}
}
The other thing you can do is manually import the extension function. As of Kotlin 1.3.41, the compiler doesn't offer this via code completion, and you have to manually request it.
import test.Example.exampleExtension
fun test(str: String) {
str.exampleExtension() // Works now because of the import
}
I created a Kotlin bug to try and fix this, and have the auto-complete suggest this as an import: https://youtrack.jetbrains.com/issue/KT-33221

Kotlin compiler: Data binding error, cannot find method

Mirgating from Java to Kotlin I try to use static function with Data Binding:
<data>
<import type="com.package.domain.tools.helper.StringValidator"/>
...
</data>
Then I call function hideNumber:
<com.hastee.pay.ui.view.Text
...
android:text='#{StringValidator.hideNumber(account.number)}'
app:layout_constraintRight_toRightOf="#+id/number"
app:layout_constraintTop_toBottomOf="#+id/number" />
Using databinding here causes error:
[kapt] An exception occurred:
android.databinding.tool.util.LoggedErrorException: Found data binding
errors.
****/ data binding error ****msg:cannot find method
hideNumber(java.lang.String) in class
com.package.domain.tools.helper.StringValidator....
Here's this object:
object StringValidator {
...
fun hideNumber(number: String): String {
return "****" + number.substring(number.length - 4)
}
}
How can I reach this function using Kotlin and Data Binding?
The data binding compiler is looking for a static method.
Since a named object alone is not enough to make all methods inside that object static, you need an additional #JvmStatic annotation on your hideNumber-method:
#JvmStatic
fun hideNumber(number: String): String {
return "****" + number.substring(number.length - 4)
}
see also: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#static-methods

Kotlin all-open compiler plugin doesn't work

I use Realm and it requires open keyword to it's model classes.
Following https://blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here/,
I tried to use all-open compiler plugin to remove the open keyword from Realm model classes.
First, I added all-open compiler plugin and set the package name of annotation
buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
}
}
apply plugin: "kotlin-allopen"
allOpen {
annotation("com.mycompany.myapp.annotation")
}
Second, I generated annotation
package com.mycompany.myapp.annotation
annotation class AllOpenAnnotation
Finally, I added the annotation to Realm model class
#AllOpenAnnotation
class Model {
var id: Int = -1,
var title: String = "",
var desc: String? = null
}: RealmObject()
But the error: cannot inherit from final Model error occurs.
Is there something that I did wrong?
You need to add the name of the annotation to the path in your config file:
allOpen {
annotation("com.mycompany.myapp.annotation.AllOpenAnnotation")
}