Android DataBinding: #BindingAdapter in Kotlin does not recognize lambdas - android

This is my BindingAdapter:
#BindingAdapter(value = *arrayOf("bind:commentsAdapter", "bind:itemClick", "bind:avatarClick", "bind:scrolledUp"), requireAll = false)
fun initWithCommentsAdapter(recyclerView: RecyclerView, commentsAdapter: CommentsAdapter,
itemClick: (item: EntityCommentItem) -> Unit,
avatarClick: ((item: EntityCommentItem) -> Unit)?,
scrolledUp: (() -> Unit)?) {
//Some code here
}
initWithCommentsAdapter is a top level function
This is my layout (an essential part):
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="some.example.path.CommentsViewModel"/>
<variable
name="commentsAdapter"
type="some.example.path.CommentsAdapter"/>
</data>
<android.support.v7.widget.RecyclerView
...
bind:avatarClick="#{(item) -> viewModel.avatarClick(item)}"
bind:itemClick="#{viewModel::commentClick}"
bind:commentsAdapter="#{commentsAdapter}"
bind:isVisible="#{viewModel.commentsVisibility}"
bind:scrolledUp="#{() -> viewModel.scrolledUp()}"
/>
</layout>
When I assign lambda with kotlin method call in the layout, I have such error during building:
e: java.lang.IllegalStateException: failed to analyze:
java.lang.RuntimeException: Found data binding errors.
****/ data binding error ****msg:cannot find method avatarClick(java.lang.Object)
in class some.example.path.CommentsViewModel
****\ data binding error ****
or if I assign method by reference:
e: java.lang.IllegalStateException: failed to analyze:
java.lang.RuntimeException: Found data binding errors.
****/ data binding error ****msg:Listener class kotlin.jvm.functions.Function1
with method invoke did not match signature of any method viewModel::commentClick
file:C:\Android\Projects\...\fragment_comments.xml
loc:70:12 - 83:17
****\ data binding error ****
But I have such methods with proper type, not Object
Question
How can I assign Kotlin lambda for custom #BindingAdapter in Kotlin in the layout?
Edit
The relevant part of the viewModel:
class CommentsViewModel(model: CommentsModel): BaseObservable() {
//Some binded variables here
...
fun commentClick(item: EntityCommentItem) {
//Some code here
}
fun avatarClick(item: EntityCommentItem) {
//Some code here
}
fun scrolledUp() {
//Some code here
}
...
}
The variables binding works just fine

Short Answer
Instead of using Kotlin generic lambda types, use interfaces with a single method that matches both return type and parameters of your method reference (itemClick) or your listener (avatarClick).
You can also use abstract classes with a single abstract method, also with matching parameters and return type.
Explanation
Actually the Databinding docs never mention that the Kotlin lambda types work as Databinding listeners or method references, probably because under the hood these lambda types translate to Kotlin's Function1, Function2... which are generics, and thus some of their type information doesn't make it to the executable and therefore is not available at runtime.
Why your scrolledUp binding did work though? Because type () -> Unit has no need for generics. It could have worked even with Runnable.
Code
interface ItemClickInterface {
// method may have any name
fun doIt(item: EntityCommentItem)
}
#BindingAdapter(
value = ["commentsAdapter", "scrolledUp", "itemClick", "avatarClick"],
requireAll = false
)
fun initWithCommentsAdapter(
view: View,
commentsAdapter: CommentsAdapter,
scrolledUp: () -> Unit, // could have been Runnable!
itemClick: ItemClickInterface,
avatarClick: ItemClickInterface
) {
// Some code here
}

I ran into the same case, and what worked was having it declared as variable defining its type, that worked with the compiler
val avatarClick:(item: EntityCommentItem)->Unit = {}

Related

How to use Kotlin extension property of function type with Android data binding?

I defined a Kotlin extension property of function type:
#set:BindingAdapter("myListener")
var View.myListener: ((Boolean) -> Unit)?
get() {
// ...
}
set(value) {
// ...
}
I'm now trying to use that in an Android data binding call. First I defined a method against my ViewModel (which is a variable viewModel within my databinding).
#MainThread
fun notify(value: Boolean) {
if (value) {
// do stuff
}
}
Then tried to use that method in my binding layout XML:
<TextView
...
app:myListener="#{viewModel::notify}"
/>
But it gives a compiler error "Listener class 'kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit>' with method 'invoke' did not match signature of any method 'app:myListener'".
I also tried this way, with calling the method in an expression:
<TextView
...
app:myListener="#{(b) -> viewModel.notify(b)}"
/>
But it also gave an error, this time on startup: "cannot find method notify(java.lang.Object) in class com.example.ui.models.MyViewModel".
I've managed to use data binding before with Kotlin extension functions returning types other than function, and I've also used data binding with method reference to listeners that are defined via an interface. But I'm not sure how to combine the two.

Kotlin databinding with extension methods

I'm trying to use Kotlin extension methods inside Android's databinding. For example; calling an onclick handler. So I've made this code:
posttest_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
<import type="android.view.View"/>
<import type="com.example.test.post.posttest.PostTestItemViewModelExtensionKt" />
<variable
name="viewModel"
type="com.example.test.post.posttest.PostTestItemViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:clickable="true"
android:onClick="#{(view) -> viewModel.clicked(view)}"
>
[...]
PostTestItemViewModel.kt
open class PostTestItemViewModel : ViewModel() {
val postTitle = MutableLiveData<String>()
val postBody = MutableLiveData<String>()
/**
* Binds the required properties/entities to this ViewModel
*/
fun bind(post: Post) {
postTitle.value = post.title
postBody.value = post.body
}
}
PostTestItemViewModelExtension.kt
fun PostTestItemViewModel.clicked(v: View) {
this.postTitle.value = "clicked"
}
So when I place the clicked method inside the viewmodel, it works perfectly the way it should be. However, when I create it as an extension method, I get the following error on compilation:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
cannot find method clicked(android.view.View) in class ...PostItemViewModel
I've tried different things already, such as changing the android:onclick tag to PostTestItemViewModelExtensionKt instead of viewModel. Unfortunately all the things don't seem to work. So it looks like the extension method is getting generated after the databinding takes place. Is there a way around this or am I still doing something wrong? Or is it just not possible to bind extension methods?
I'm using Kotlin version 1.2.71, gradle 3.2.0 and have the databinding { enabled = true } and kapt { generateStubs = true } added to my .gradle, and have the plugings kotlin-android, kotlin-android-extensions and kotlin-kapt defined.
Unfortunately you can't use extension methods as onClick callbacks.
Extension methods in Kotlin are created as Java static methods while the Android framework is expecting an instance method.
Note that in Android Studio you can decompile the Kotlin classes as Java to see the generated Java code.
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78

Differences between Normal and Single Expression function definitions

Assume an Android project in which I have this XML for two buttons:
<layout>
<data>
<variable name="viewModel" type="com.package.package.UploadPhotoViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:padding="10dp">
<com.zoosk.zoosk.ui.widgets.ProgressButton
android:id="#+id/progressButtonChooseFromLibrary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.choosePhotoText}"
android:onClick="#{viewModel::choosePhotoButtonClick}"
android:visibility="#{viewModel.choosePhotoVisibility}" />
<com.zoosk.zoosk.ui.widgets.ProgressButton
android:id="#+id/progressButtonTakePhoto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.takePhotoText}"
android:onClick="#{viewModel::takePhotoButtonClick}"
android:visibility="#{viewModel.takePhotoVisibility}" />
</LinearLayout>
</layout>
And an accompanying ViewModel:
class UploadPhotoViewModel(resources: Resources) {
var onChoosePhotoButtonClicked: ((View) -> Unit)? = null
var onTakePhotoButtonClicked: ((View) -> Unit)? = null
fun choosePhotoButtonClick(v: View) {
onChoosePhotoButtonClicked?.invoke(v)
}
fun takePhotoButtonClick(v: View) = onTakePhotoButtonClicked?.invoke(v)
}
Noting particularly the difference between how choosePhotoButtonClick and takePhotoButtonClick are declared.
When building this project, Android's databinding will work properly for choosePhotoButtonClick, but throws an error with takePhotoButtonClick, saying that there's no method that matches the expected reference signature. Assuming these methods are created the same way under the hood, this should not happen, alas there must be some difference.
What exactly is the difference to these two declaration syntaxes? The official Kotlin documentation doesn't mention anything functional, only that it can serve as an additional way to declare that method.
If you use curly braces without specifying a return type, Your function is implicitly retuning Unit (this is Kotlin for void), so your method:
fun choosePhotoButtonClick(v: View) {
onChoosePhotoButtonClicked?.invoke(v)
}
Has signature of:
fun choosePhotoButtonClick(v: View) : Unit
(IDE will hint that return type "Unit" is redundant and can be ommited).
However using equals sign to shorthand a function infers the return type of expression on right hand side, for example:
fun itemCount() = items.size
Will infer return type of Int, so its signature is:
fun itemCount() : Int
In your case of :
fun takePhotoButtonClick(v: View) = onTakePhotoButtonClicked?.invoke(v)
Function can either return a null when onTakePhotoButtonClicked is null or return value of that method (Unit), which means your takePhotoButtonClick signature is:
fun takePhotoButtonClick(v: View) : Unit?
(OP) direct solution to question:
fun takePhotoButtonClick(v: View) = onTakePhotoButtonClicked?.invoke(v) ?: Unit
Can be considered less readable than the curly brace version, but this implies a return type of Unit instead of Unit? and will be picked up correctly by databinding.

Kotlin Property: "Type parameter of a property must be used in its receiver type"

I have the following simple Kotlin extension functions:
// Get the views of ViewGroup
inline val ViewGroup.views: List<View>
get() = (0..childCount - 1).map { getChildAt(it) }
// Get the views of ViewGroup of given type
inline fun <reified T : View> ViewGroup.getViewsOfType() : List<T> {
return this.views.filterIsInstance<T>()
}
This code compiles and works fine. But, I want the function getViewsOfType to be a property, just like the views. Android Studio even suggests it. I let AS do the refactoring and it generates this code:
inline val <reified T : View> ViewGroup.viewsOfType: List<T>
get() = this.views.filterIsInstance<T>()
But this code doesn't compile. It causes error: "Type parameter of a property must be used in its receiver type"
What is the issue here? Searching for help on this error doesn't seem to lead to an answer.
The error means that you can only have a generic type parameter for an extension property if you're using said type in the receiver type - the type that you're extending.
For example, you could have an extension that extends T:
val <T: View> T.propName: Unit
get() = Unit
Or one that extends a type that uses T as a parameter:
val <T: View> List<T>.propName: Unit
get() = Unit
As for why this is, I think the reason is that a property can't have a generic type parameter like a function can. While we can call a function with a generic type parameter...
val buttons = viewGroup.getViewsOfType<Button>()
... I don't believe a similar syntax exists for properties:
val buttons = viewGroup.viewsOfType<Button> // ??

Mocktito ArgumentCaptor for Kotlin lambda with arguments

I am trying to test this on Kotlin:
verify(myInterface).doSomething(argumentCaptor.capture())
capture.value.invoke(0L)
Where doSomething is:
doSomething((Long) -> Unit)
How can I create an ArgumentCaptor for this? Right now I am doing this
inline fun <reified T : Any> argumentCaptor() = ArgumentCaptor.forClass(T::class.java)!!
val captor = argumentCaptor<(Long) -> Unit>()
verify(mainApiInterface!!).downloadUserProfilePicture(captor.capture())
captor.value.invoke(0L)
But I am getting java.lang.IllegalStateException: captor.capture() must not be null
I also tried integrating mockito-kotlin but I get a PowerMockito error:
No instance field named "reported" could be found in the class hierarchy of org.mockito.internal.MockitoCore.
Using mockito-kotlin like this seems to work:
val myService = mock<MyInterface>()
myService.doSomething {
println(it)
}
verify(myService).doSomething(capture { function ->
function.invoke(123)
})
Edit: removed unnecessary argumentCaptor<(Long) -> Unit>().apply {} - it wasn't used
as with kotlin 1.3.72 and com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0 the following works fine for me:
create an argument captor via val captor = argumentCaptor<() -> Unit>() and call captor.capture() on it.
There's also a variant for nullable captors with nullableArgumentCaptor()
The following unit-test captures the lambda of type () -> Unit that is given to diff.open(). To capture it at runtime it then uses captor.capture()
// given
val onClose = argumentCaptor<() -> Unit>()
// when
diff.open(file, serialized) { onDiffClosed(clusterResource, documentBeforeDiff) }
// then
verify(diff).open(any(), any(), onClose.capture())
The nhaarman wrapper for mockito creates a wrapper KArgumentCaptor for the mockito class ArgumentCaptor. The nhaarman wrapper fixes your error by creating an instance instead of null as in mockito.

Categories

Resources