I'm new to android development so this may be simple but I just cant see what is wrong.
I have a data class (which is actually a room entity if that matters) , for example :
#Entity(...)
data class MyDataClass (
...
var dataType:Int=0,
...
)
In viewmodel I have
val data = MutableLiveData<MyDataClass>()
In the viewmodel init block this is initialised
In my activity layout xml I have various views to allow editing of the data class
There is a spinner in the layout which edits the dataType :
<Spinner
...
android:selectedItemPosition="#={viewmodel.data.dataType}"
/>
So far it works.
I then have a view in the layout
<LinearLayout
...
app:hideIfZero="#{viewmodel.data.dataType}"
/>
where the hideIfZero binding adapter is
#BindingAdapter("app:hideIfZero")
fun hideIfZero(view: View, number:Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
When I run the app, I can edit all of my fields including the dataType via the spinner. However when I change the spinner value the visibility of the LinearLayout is not changed.
What am I missing ?
Do I need to somehow tell the activity to refresh the complete layout. Any examples I've found seem to imply this should happen automatically.
Related
I was trying to see when binding.invalidateAll() is needed to refresh the UI when the data changes.
I have 2 examples where I change the data, in one of them the UI data changes automatically and in the other, I need to use invalidateAll() to see the changes
First of all, below is the XML of the TextView I'm testing on:
...
<TextView
android:id="#+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{mainActivity.myName.name}" />
First Example
In the first example, if I change the data inside onCreate() or onResume() the data changes directly without the need for invalidateAll(), please find the code below:
private lateinit var binding: ActivityMainBinding
var myName = MyName(name = "Test")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.mainActivity = this;
myName.name = "Data is visible directly"
}
Here, once the UI is drawn to the user, I see the TextView's text directly set as "Data is visible directly" without the need to call binding.invalidateAll()
Second Example
In the second example, I only change the text when a button is clicked as in the below code:
...
fun onButtonClickedChangeData(view:View){
myName.name = "Data is visible only after invalidateAll is called"
binding.invalidateAll() // without it the text won't change
}
In the second example, I have to use binding.invalidateAll() for the text of the TextView to change, if I don't use invalidateAll() the text will not change.
My First Speculation (which seems to be wrong)
I first thought maybe binding.invalidateAll() is needed when we change the data while the activity is already running and the UI is already visible to the user, and that invalidateAll() won't be needed if the data changes before the UI is drawn yet to the user (giving that the UI is only visible to the user in onResumue() according to my knowledge).
But when I tried to update the data inside onResumue() the data changed directly as well without the need for binding.invalidateAll()
I have extensively searched for this issue but have not yet found a reasonable solution.
What I'm trying to do is simply set up an onClickListener using the data binding format in the layout:
android:onClick="#{() -> subjectsViewModel.onClickAdd()}"
where subjectsViewModel is a layout variable to which I pass the ViewModel in the fragment.
What I want is to pass the currently entered edit text data to onClickAdd() function.
The only solution I have found yet is to use two way data binding but I do not think it should be required for setting up such a basic fuctionality.
you can pass the view reference and get the text from TextView after casting it back to TextView from View. something like this.
android:onClick="#{(view) -> subjectsViewModel.onClickAdd(view)}"
and in your view model just create something like this.
fun onClickAdd(view: View){
val myText = (view as TextView).text ?: ""
//here use text
}
First give an id to your EditText. Lets say the id is edittext.
Then do it like this.
android:onClick="#{() -> subjectsViewModel.onClickAdd(edittext.getText().toString())}"
Let's assume i' m programming an app showing some kind of countdown.
// somewhere in my fragment:
fun getCountdown(): LiveData<Int> = viewModel.countdown
// 10 ... 9 ... 8 ... etc.
I now want to bind this LiveData to two different TextViews.
<TextView
android:id="#+id/countdownTextView"
android:text="#{fragment.getCountdown}" />
<TextView
android:id="#+id/hurryUpTextView"
android:text="#{fragment.getCountdown}" />
If i only had one of those two views, my BindingAdapter(s) would look like this:
// for the countdown-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
view.setText("$ Seconds remaining!")
}
// OR for the hurry-up-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
if(state.value < 3){
view.setText("Hurry up!")
} else {
view.setText("Chill, you have a lot of time")
}
}
Now, what's the most elegant way to use both TextViews/Adapters together?
Should i just create two LiveDatas in my fragment which map countdown to the appropriate string?
Can i somehow specify which adapter i'd like to use?
Should i (try to) write one adapter which internally differenciates between the two views?
Better suggestions?
To make code simple and readable, I'll advise you choose between next approaches:
Use one binding adapter and supply to it already prepared data - your logic particulary will be placed in View or ViewModel.
Use different binding adapter names and post raw data - all work will be placed in each adapter.
What is best suited for you depends on your needs, how common is format logic and , of course, personal opinion.
I am trying to learn data binding with Kotlin and I was able to implement it successfully for edit text and text views. After that I am trying to use it for Image Views. I am currently trying to give an option to users to choose their profile picture by clicking on the imageview. This code works properly but when I try to set the image to the view using data binding adapter , I get the following error.
Found data binding errors.
****/ data binding error ****msg:Cannot find the getter for attribute 'android:userImage' with value type java.lang.String on de.hdodenhof.circleimageview.CircleImageView. file:/home/parangat-pt-p10/AndroidStudioProjects/ReUsableAndroid/reusable_android/app/src/main/res/layout/activity_signup.xml loc:25:12 - 31:48 ****\ data binding error ****
Below is my code for the same.
Layout code of ImageView
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:userImage="#{signup.userImage}"
android:id="#+id/iv_user"
android:src="#drawable/profile"/>
Model class code
class Signup {
var userImage=""
var firstName=""
var lastName=""
var phoneNumber=""
var postCode=""
var country=""
var email=""
var password=""
var confirmPassword=""
var isAcceptTerms=false
#BindingAdapter("android:userImage")
fun loadImage(view: CircleImageView, imageUrl: String) {
userImage=imageUrl
Glide.with(view.context).load(imageUrl).into(view)
}
}
And this is what I am doing after user selected image
override fun onSingleImageSelected(uri: Uri?) {
signupBinding.signup?.loadImage(iv_user,uri.toString())
}
Since this is written in kotlin, therefore there is no need to define the getter and setter methods but the error states that no getter method found.
As suggested by Enzokie, I created the binding Adapter in separate file like below
#BindingAdapter("userImage")
fun loadImage(view: CircleImageView, imageUrl: String) {
Glide.with(view.context).load(imageUrl).into(view)
}
But I still have the same issue.
Correct Approach
Use either Observable / Live Data.
Make a binding adapter class individually and don't mess-up things in model.
Yes tutorials do that, because they are just teaching you.
Just make one common binding adapter (like android:src) for whole app.
No need to use custom namespace, until when you need it. So you can use android:src instead of android:userImage.
No need to use CircleImageView in BindingAdapter, make common adapter with ImageView because CircleImageView is child of ImageView.
Final code
If you need to manually change fields like signup.userImage = "someUrl" then use Bindable and notify, other wise no need of both.
If you use ObservableField instead of extending BaseObservable class, then you don't need to use Bindable and notify.
Signup.class
class Signup : BaseObservable() {
#get:Bindable
var userImage: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.userImage)
}
}
DataBindingAdapter.kt
// binding adapter for setting url/uri on ImageView
#BindingAdapter("android:src")
fun setImageUrl(view: ImageView, url: String) {
Glide.with(view.context).load(url).into(view)
}
layout.xml
<de.hdodenhof.circleimageview.CircleImageView
...
android:src="#{signup.userImage}"/>
Now you can set binding.signup.userImage = "Url", it will refract on UI automatically.
That's all!
Reason of Fail
When you use data binding, and you want UI automatic update after setting fields. then your model should be one of below :
Either extend BaseObservable
Or fields must be Observable fields
Or using LiveData
In your case, initially your URL is empty (""). Now when you set image after some time programmatically, then UI is not notified because you are not using any observing option like I said above.
Bit more info
The difference between both is, Live data is Android Lifecycle Aware (Activity/ Fragments/ Services).
LiveData is an observable data holder class. Unlike a regular
observable, LiveData is lifecycle-aware, meaning it respects the
lifecycle of other app components, such as activities, fragments, or
services. This awareness ensures LiveData only updates app component
observers that are in an active lifecycle state.
Use this
#BindingAdapter({"bind:userImage"})
Instead of this
#BindingAdapter("android:userImage")
And in CircleImageView
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
app:userImage="#{signup.userImage}"
android:id="#+id/iv_user"
android:src="#drawable/profile"/>
here is the good article for Loading images with data binding
try by removing android TAG
#BindingAdapter("userImage")
fun loadImage(view: CircleImageView, imageUrl: String) {
userImage=imageUrl
Glide.with(view.context).load(imageUrl).into(view)
}
AND
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
app:userImage="#{signup.userImage}"
android:id="#+id/iv_user"
android:src="#drawable/profile"/>
Alongwith the answer given by Khemraj make sure you have marked Binding Adapter as #JvmStatic and it should be added above #BindingAdapter annotation. I wasted a whole day as I had added jvmstatic after Binding Adapter annotation.
object ImageBindingAdapter {
#JvmStatic
#BindingAdapter("android:src")
fun setImage(imageView: ImageView, uri: String) {
Glide.with(imageView.context).load(uri).placeholder(R.drawable.no_image).error(R.drawable.no_image).centerCrop().into(imageView)
}}
I want to toggle the visibility of a TextView using LiveData. There have been a few other posts on setting the visibility with databinding, but these use Observables, whereas I want to leverage the (newer) LiveData. In particular, use a LiveData.
Using this documentation, and a few SO posts, I have already learned that you should correctly align your getter of your observable (LiveData) so that the return type matches the type expected by the setter for the View attribute you want to set. Specifically:
setVisibility() of View requires an int, whereas I have a LiveData member (so the getter in my ViewModel will also return this type)
converting this Boolean to View.VISIBLE and VIEW.GONE is possible using a ternary operator. I should also add safeUnbox() in my XML expression to make it a primitive boolean
Using these insights, in my ViewModel class, I have defined:
MutableLiveData<Boolean> textHintVisible;
After pressing a button, I set this value to False:
textHintVisible.postValue(false);
(note, I also tried with setValue())
Then, in my layout XML, I have included:
<TextView
android:visibility="#{(safeUnbox(viewModel.textHintVisible) ? View.VISIBLE : View.GONE)}"
/>
But still, my TextView is always visible. To debug, I have added an observer in my activity, and this confirms that my boolean is correctly toggled between true and false:
mHintsViewModel.getTextHintVisible().observe(this, new Observer<Boolean>() {
#Override
public void onChanged(#Nullable Boolean newInt) {
Log.i(TAG,"onChanged: "+newInt);
}
});
But my TextView stays visible all the time. What am I doing wrong? Is it impossible to use LiveData for this? Should I use an additional Converter? Or is my code in principle correct, but is this a bug in Android Studio? Any help is much appreciated.
One thing I have in mind is - have you set your binding to observe liveData? As per documentation you have to set the binding layout to observe lifecycle binding.setLifecycleOwner(this)