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);
}
Related
I'm not sure if I'm using CoroutineScope properly inside my Binding Adapter function:
#BindingAdapter("srcByFileName")
fun setImageViewResource(imageView: ImageView, fileName: String?) {
if(fileName == null) return
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
val bitmap = ImageStorageManager.getImageFromInternalStorage(imageView.context, fileName)
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
}
}
I need to move the fetching of Bitmap to a different thread hence the need of coroutine. I'm just wondering if this is the correct way of doing this.
If you're using Data Binding, then your logic would probably be in your ViewModel, so it's better to pass viewModelScope to the BindingAdapter function.
Just add a third parameter of type CoroutineScope which you can pass from xml layout through the predefined viewModel object.
And in ViewModel, you can just define your scope like this:
val scope: CoroutineScope
get() = viewModelScope
And in xml:
<data>
<variable
name="viewModel"
type="ViewModelType" />
</data>
...
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcByFileName=#{viewModel.src}
app:coroutineScope="#{viewModel.scope}" />
I have the following log error when I'm building an android app:
Binding adapter AK(android.widget.ImageView, java.lang.String) already exists for imageUrl! Overriding com.example.newsapp.utils.ImageUtils.Companion#loadImageFromUrl with com.example.newsapp.utils.ImageUtils#loadImageFromUrlwarning: Binding adapter AK(android.widget.ImageView, java.lang.String) already exists for imageUrl! Overriding com.example.newsapp.utils.ImageUtils.Companion#loadImageFromUrl with com.example.newsapp.utils.ImageUtils#loadImageFromUrl
The imageUrl attribute is:
class ImageUtils {
companion object {
#JvmStatic
#BindingAdapter("imageUrl")
fun ImageView.loadImageFromUrl(imageUrl: String?) {
Glide.with(this).load(imageUrl).into(this)
}
}
}
and the xml file contains the following:
<ImageView
android:id="#+id/articleImage"
android:layout_width="158dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:imageUrl="#{article.urlToImage}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:srcCompat="#tools:sample/avatars" />
For Kotlin
I think the issue is due to how the Java class is generated fro your Kotlin class. If you decompile the code you can see the issue as it will define the adapter twice, once in a class and once in a companion class.
2 solutions:
Method 1: Object declaration
package myApp
// import omitted
companion object ImageUtilsBindingAdapter {
#JvmStatic
#BindingAdapter("imageUrl")
fun ImageView.loadImageFromUrl(imageUrl: String?) {
Glide.with(this).load(imageUrl).into(this)
}
}
Method 2: Package function
package myApp;
// import omitted
#BindingAdapter("imageUrl")
fun ImageView.loadImageFromUrl(imageUrl: String?) {
Glide.with(this).load(imageUrl).into(this)
}
You can read about this more here.
https://medium.com/#hkhcheung/defining-android-binding-adapter-in-kotlin-b08e82116704
Credit to the Medium article author Herman Cheung.
It might sound strange but in my case issue caused because I forgot to supply data to the ViewModel using data binding.
actual function was
fun navigateToNextScreen(data: Data, index: Int) {
//navigation code here
}
The issue caused because of the below code
<androidx.cardview.widget.CardView
android:onClick="#{() ->viewModel.navigateToNextScreen(data)}"
/>
It was solved by passing all the required params like
<androidx.cardview.widget.CardView
android:onClick="#{() ->viewModel.navigateToNextScreen(data, index)}"
/>
So in case if you still face the issue after trying all the solutions on this post, I'll suggest you to check for your model classes and view model functions.
Building on #Vishal Naikawadi's answer, I found that this was caused for me by the binding adapter having multiple parameters and only one being used.
https://developer.android.com/topic/libraries/data-binding/binding-adapters#custom-logic
From the docs for databinding:
#BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
You can use the adapter in your layout as shown in the following example. Note that #drawable/venueError refers to a resource in your app. Surrounding the resource with #{} makes it a valid binding expression.
<ImageView app:imageUrl="#{venue.imageUrl}" app:error="#{#drawable/venueError}" />
The adapter is called if both imageUrl and error are used for an ImageView object and imageUrl is a string and error is a Drawable. If you want the adapter to be called when any of the attributes is set, you can set the optional requireAll flag of the adapter to false, as shown in the following example:
#BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
Conclusion:
Be sure to bind every parameter. I was only binding one of several which caused this same error.
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 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"/>
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.