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.
Related
I am trying to load image with databinding. But I never got over it. Where's my problem? Below is my code and layout construction.
MyItemViewModel.kt
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String) = Glide.with(view.context).load(url).into(view)
layout.xml
<data>
<variable
name="viewModel"
type="com.myapp.app.ui.activity.albumlist.AlbumItemViewModel"/>
</data>
<com.makeramen.roundedimageview.RoundedImageView
android:layout_width="60dp"
android:id="#+id/ivRoundedAlbum"
android:layout_marginStart="#dimen/unit_20_dp"
app:riv_corner_radius="8dp"
app:imageUrl="#{viewModel.data.cover}"
android:layout_height="60dp"/>
You need to make url parameter nullable, and guard against null like this:
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String?) {
if (!url.isNullOrEmpty()) {
.....
}
}
BindingAdapter methods should be static, so marking it #JvmStatic would help in this case.
But that will generate 'compile time error' that "methods can't be static inside class" and so it should be moved to companion object or named objects.
In your case, you're having method in class member level so moving it to companion object will help. So for MyItemViewModel.kt make companion object and move method there like below :
class MyItemViewModel{
//Some code
companion object {
#JvmStatic
#BindingAdapter("imageUrl")
fun loadImage(view: RoundedImageView, url: String) { // This methods should not have any return type, = declaration would make it return that object declaration.
Glide.with(view.context).load(url).into(view)
}
}
//Some other code
}
Note: Also remove method declaration with =. Binding methods should have return type Unit.
Edit: One can also use method Glide.with(view) as #hmac suggested in comment, but ...
Things to consider before using this Glide.with(view):
Your view should be attached before using it from Activity/Fragment. Best usecase for this method is Custom View/ViewGroup.
Consider layout hierarchy before using this method as too many nested/large hierarchy layouts are discouraged to use that method. It becomes inefficient for such layouts.
Also note that, if view is inside non-support fragment class or context is of non-support fragment than that can produce noisy log as documentation indicates, first migrate to support library (Now considered as AndroidX) instead before using this method!
This work fine for me
<ImageView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:layout_gravity="center"
bind:image="#{subcategory.image}"
bind:placeholder="#{#drawable/no_imge}"
android:layout_weight="1" />
#BindingAdapter("image","placeholder")
fun setImage(image: ImageView, url: String?, placeHolder: Drawable) {
if (!imageUrl.isNullOrEmpty()){
Glide.with(image.context).load(url).centerCrop()
.placeholder(R.drawable.no_imge)
.into(image)
}
else{
image.setImageDrawable(placeHolder)
}
}
It would be more convenient to create a binding adapter which accepts multiple optional attributes so you can customize the loading request. Here's an example of such adapter.
#BindingAdapter(
"srcUrl",
"circleCrop",
"placeholder",
requireAll = false // make the attributes optional
)
fun ImageView.bindSrcUrl(
url: String,
circleCrop: Boolean = false,
placeholder: Drawable?,
) = Glide.with(this).load(url).let { request ->
if (circleCrop) {
request.circleCrop()
}
if (placeholder != null) {
request.placeholder(placeholder)
}
request.into(this)
}
And you can use it like this:
<ImageView
...
app:srcUrl="#{someUrl}"
app:placeholder="#{#drawable/ic_placeholder}"
app:circleCrop="#{true}" />
You can also find an example in sources of the Owl - an official Android sample app on GitHub. See BindingAdapters.kt.
#JvmStatic
#BindingAdapter("glide")
fun glide(view: ShapeableImageView, url: String?) {
if (!url.isNullOrEmpty()) {
Glide.with(view).load(url).into(view)
}
}
I think the best practise should be to create a separate variable for imageUrl of type string in layout.xml. BindingAdapter should be in model class. Also, the BindingAdapter method should be static as pointed out in comments. You can do it by wrapping inside a companion object with #JvmStatic annotation. For more details check this
<variable
name="imageUrl"
type="String" />
use app:glideSrc like this
<ImageView
android:id="#+id/sender_profile_image_view"
android:layout_width="#dimen/email_sender_profile_image_size"
android:layout_height="#dimen/email_sender_profile_image_size"
android:contentDescription="#string/email_sender_profile_content_desc"
android:scaleType="centerCrop"
app:glideCircularCrop="#{true}"
app:glideSrc="#{email.sender.avatar}"
app:layout_constraintTop_toTopOf="#id/sender_text_view"
app:layout_constraintBottom_toBottomOf="#+id/recipient_text_view"
app:layout_constraintEnd_toEndOf="parent"
tools:src="#drawable/avatar_3" />
and in BindingAdapter
#BindingAdapter(
"glideSrc",
"glideCenterCrop",
"glideCircularCrop",
requireAll = false
)
fun ImageView.bindGlideSrc(
#DrawableRes drawableRes: Int?,
centerCrop: Boolean = false,
circularCrop: Boolean = false
) {
if (drawableRes == null) return
createGlideRequest(
context,
drawableRes,
centerCrop,
circularCrop
).into(this)
}
private fun createGlideRequest(
context: Context,
#DrawableRes src: Int,
centerCrop: Boolean,
circularCrop: Boolean
): RequestBuilder<Drawable> {
val req = Glide.with(context).load(src)
if (centerCrop) req.centerCrop()
if (circularCrop) req.circleCrop()
return req
}
Layout:
<data>
<import type="com.example.package.R"/>
</data>
<ImageView
android:id="#+id/logo"
android:contentDescription="#string/logo"
android:scaleType="fitCenter"
app:gif = "#{R.drawable.logo}" />
Data Binding Utils:
#BindingAdapter("gif")
fun ImageView.setGif(res: Int) {
Glide.with(this).load(res).into(this);
}
I have a Custom component in android with this layout.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
when using in another layout I find editText by this code.(Espresso)
val editText = onView(
allOf(withId(R.id.editText)
, isDescendantOfA(withId(R.id.mainLayout))
, isDescendantOfA(withId(R.id.mobileEdt))
)
)
I use this custom Component in all app and many layouts.
can I minify or convert to function in my app for doesn't write again and again?
Maybe I change the component layout, so I have to edit all withId in all test.
Your component has probably a class name. Let's say CustomEditText.
In that case you can implement a BoundedMatcher based custom matcher, which makes sure that it will only match view instances of your CustomEditText.
Simple implementation could look like this:
fun customEditWithId(idMatcher: Matcher<Int>): Matcher<View> {
return object : BoundedMatcher<View, CustomEditText>(CustomEditText::class.java!!) {
override fun describeTo(description: Description) {
description.appendText("with id: ")
idMatcher.describeTo(description)
}
override fun matchesSafely(textView: CustomEditText): Boolean {
return idMatcher.matches(textView.id)
}
}
}
then your assertion looks like this:
onView(customEditWithId(0)).perform(click());
it's obviously not a descendant of R.id.mobileEdt ...
val editText = onView(allOf(
withId(R.id.editText),
isDescendantOfA(withId(R.id.mainLayout))
))
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
I have a fairly simple layout with Databinding:
<data>
<variable
name="viewModel"
type="MyViewModel" />
<variable
name="navigator"
type="MyNavigator" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="#{() -> navigator.goto(viewModel.insurance, viewModel.title)}"
android:text="Click">
Here's the viewModel in Kotlin:
class MyViewModel {
val title = ObservableInt()
val insurance = ObservableField<Insurance?>()
}
And here's the Navigator in Kotlin:
class MyNavigator(private val activity: MyActivity) {
fun goto(insurance: Insurance?, #StringRes title: Int) {
if (insurance != null) {
val intent = OtherActivity.newIntent(activity, insurance,title)
activity.startActivity(intent)
} else {
Timber.w("goToClaimsQuestionsList but no insurance")
}
}
}
Expected:
When I click the button, the Navigator should receive the event, and launch another activity. However, insurance Insurance? is always null.
When I use this expression in the layout (using .get()):
android:onClick="#{() -> navigator.goto(viewModel.insurance.get(), viewModel.title.get())}"
all works as expected, but I receive a warning while building:
Warning:warning: Do not explicitly call 'get()' on ObservasbleFields in an expression. This support will be removed soon. 'viewModel.insurance.get()'
Is this a bug in the current Databinding implementation with Kotlin? Or is there another explanation of why I have to use ObservableField.get() ?
NOTE:
AndroidStudio 3.0 for Mac
buildToolsVersion: 27.0.3
android gradle plugin: 3.0.1
kotlinVersion: 1.2.21
It appears to be a bug not related to Kotlin.
But in your case one could argue, that it might be better not to use it from the the XML layer at all.
Instead you could create a variable with callbacks to encapsulate the navigation logic
class Callbacks(val vm: MyViewModel, val navigator: MyNavigator) {
fun buttonClicked() = navigator.goto(vm.insurance.get(), vm.title.get())
}
and call this function onClick
android:onClick="#{() -> callbacks.buttonClicked())}"
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 = {}