Android Custom View Two Way binding not working with LiveData - android

I have a problem regarding livedata in custom views in android, I have a custom class with a text livedata that is two-way binded to the xml but the problem is that whenever the user writes any new value in the TextInputEditText the value of livedata is not changed.
class MaterialTextInput #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : MaterialCardView(context, attrs) {
val viewModel = MaterialTextInputViewModel()
init {
ViewMaterialTextInputBinding.inflate(
LayoutInflater.from(context), this, true
).apply {
viewModel = this#MaterialTextInput.viewModel
}
}
class MaterialTextInputViewModel : ViewModel() {
var text = MutableLiveData("")
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.app.utils.views.MaterialTextInput.MaterialTextInputViewModel" />
</data>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/cardContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="10dp"
app:strokeColor="#{viewModel.text.length() > 0 ? #color/color_4 : #color/white}"
app:strokeWidth="1dp">
<com.google.android.material.textfield.TextInputLayout
style="#style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email_address"
app:boxBackgroundColor="#color/white"
app:boxStrokeWidth="0dp"
app:boxStrokeWidthFocused="0dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:text="#={viewModel.text}" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

Related

DataBind attributes of custom component to ViewModel of a Fragment?

So I'm trying to create a custom component that consists of two labels and one TextInputLayout views. When I implement it in my fragment I want to bind visibility of one label, colour of TextInputLayout boxStroke and text inside TextInputLayout to viewModel MutableLiveData variables. But somehow I can't get it to work, can anybody help?
My custom component layout
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/custom_input_label"
style="#style/BasicText"
android:layout_marginStart="#dimen/margin_normal"
android:text="#string/first_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/custom_input_layout"
style="#style/TextInputLayoutStyle"
android:layout_marginStart="#dimen/margin_normal"
android:layout_marginTop="#dimen/margin_12dp"
android:layout_marginEnd="#dimen/margin_normal"
app:errorEnabled="true"
app:errorTextColor="#color/colorPrimary"
app:helperTextEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/custom_input_label">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/custom_edit_text"
style="#style/InputTextStyle" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="#+id/custom_input_error"
style="#style/ErrorText"
android:layout_marginStart="#dimen/margin_normal"
android:layout_marginTop="#dimen/margin_11dp"
android:visibility="visible"
android:text="#string/first_name_error"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/custom_input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>
My CustomView class
class CustomInputField #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleRes: Int = 0
) : ConstraintLayout(context, attrs, defStyleRes) {
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.custom_text_input, this)
}
}
My MutableLiveData inside ViewModel
private val _nameErrorVisibility = MutableLiveData(View.GONE)
val nameErrorVisibility: LiveData<Int> = _nameErrorVisibility
private val _nameBoxColor =
MutableLiveData(R.color.mtrl_textinput_default_box_stroke_color)
val nameBoxColor: LiveData<Int> = _nameBoxColor
AND BASICALLY THIS IS WHAT I WANT TO DO IN MY FRAGMENT LAYOUT
<ba.project.project.ui.base.view.CustomInputField
android:id="#+id/txtFirstName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxColor="#{context.getColor(viewModel.nameBoxColor)}"
app:errorText="#string/first_name_error"
app:errorVisibility="#{viewModel.nameErrorVisibility}"
app:inputText="#={viewModel.firstName}"
app:labelText="#string/first_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

how to set error on Editext using databinding

I want to show error on the Edittext, if input is not correct. I am doing this on the click of the button inside my activity class. Right now I am not getting anything, Please show me what is the correct way to achieve 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">
<variable
name="activity" type="com.example.SigninActivity" />
</data>
<RelativeLayout
<EditText
android:id="#+id/ed_login"
android:layout_width="match_parent
android:layout_height="match_parent"
android:digits="0123456789"
app:errorText='#{activity.errorMsg != null ? activity.errorMsg : ""}'/>
Binding Adapter
#BindingAdapter("errorText")
fun setError(editText: EditText, str: String?) {
if(!str.isNullOrEmpty()) {
editText.
setError((HtmlCompat.fromHtml(
"<font color='red'>" + str + "</font>",
HtmlCompat.FROM_HTML_MODE_LEGACY)))
}
}
Activity class
var errorMsg: MutableLiveData<String> = MutableLiveData()
override fun onClick(view: View) {
val mobileNo = dataBinding.etLoginMobnum.text.toString()
if (!TextUtils.isEmpty(mobileNo) && mobileNo.length != 11) {
errorMsg.value = "Enter Valid Number"
}
to show an error on your EditText you may use TextInputEditText within TextInputLayout, like the following
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your number" />
</com.google.android.material.textfield.TextInputLayout>
with app:errorEnabled="true" in the TextInputLayout you can achieve what you want
Using it in code:
show error to the user
myBinding.textInputLayout.setError("Enter Valid Number")
to remove it
myBinding.textInputLayout.setError(null)
to use it with databinding you can do the following
<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="myData" type="com.example.CustomDataObject"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
android:errorText="#{myData.errorMsg}">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your number" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
</layout>
Binding Adapter
#BindingAdapter({"android:errorText"})
fun setError(tInputLayout: TextInputLayout, str: String) {
if (!str.isNullOrEmpty()) {
tInputLayout.setError("Enter Valid Number")
} else {
tInputLayout.setError(null)
}
}
In Activity
myBinding.setMyData(myDataObject)
note: in <data> scope in the XML you should declare your data objects with <variable> tag, not your activities.
make sure you are using material library in your gradle
implementation 'com.google.android.material:material:1.1.0'
Take a look here for more information about DataBinding

Android custom view support MutableLiveData two-way binding

I've been search the entire internet or couple of hours, no luck
I'm trying to figure out how to pass in the MutableLiveData to a custom view
Here is the live data in the ViewModel for my activity data binding
var perStops: MutableLiveData<Int> = MutableLiveData(10)
Here is how I use the custom view in my activity
<com.abc.ui.widgets.NumberStepper
app:amount="#={data.perStops}"
app:min="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"/>
This is the custom view for the number stepper
class NumberStepper(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) {
private val rootView = LayoutInflater.from(context)
val binding = NumberPickerCustomLayoutBinding.inflate(rootView,this,true)
init {
val customAttributes = context.obtainStyledAttributes(attrs, R.styleable.NumberStepper)
val minAmount = customAttributes.getInteger(R.styleable.NumberStepper_min,0)
val amount = customAttributes.getInteger(R.styleable.NumberStepper_amount,0)
binding.suffix = customAttributes.getString(R.styleable.NumberStepper_suffix)?:""
binding.data = amount
}}
Here is the XML layout for NumberPickerCustomLayoutBinding
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="Integer" />
<variable
name="suffix"
type="String" />
</data>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/white"
android:orientation="horizontal">
<ImageButton
android:id="#+id/decrement"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="-10dp"
android:background="#null"
android:padding="#dimen/odc_padding16"
android:src="#drawable/ic_minus" />
<TextView
android:id="#+id/display"
style="#style/TextAppearance.Secondary"
android:layout_width="45dp"
android:layout_height="match_parent"
android:background="#android:color/white"
android:gravity="center"
tools:text="1"
android:text='#{data.toString() + suffix}' />
<ImageButton
android:id="#+id/increment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="-10dp"
android:background="#null"
android:padding="#dimen/odc_padding16"
android:src="#drawable/ic_plus" />
</LinearLayout>

2-way databinding not work by liveData

I want use 2-way dataBinding by LiveData. but change editText value not update textView that show user.name.
what is wrong by my code? I use android studio 3.3 canary 3 and enable data binding v2 in gradle.properties by this code:
android.databinding.enableV2=true
model data class:
data class User(var name: String)
viewModel class:
class MainViewModel : ViewModel() {
val user = MutableLiveData<User>()
init {
user.value = User("ali")
}
}
mainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
activityMainBinding.setLifecycleOwner(this)
activityMainBinding.viewModel = viewModel
}
}
<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="viewModel"
type="com.example.hoseinkelidari.databindingsample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"
android:text="#={viewModel.user.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="#{viewModel.user.name}"
app:layout_constraintBottom_toTopOf="#+id/editText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Update:
I found solution at this link:
LiveData update on object field change
I set this change and it work:
class User : BaseObservable() {
#get:Bindable
var firstName: String? = null
set(name) {
field = firstName
notifyPropertyChanged(BR.firstName)
}
}
class MainViewModel : ViewModel() {
var user = CustomMutableLiveData<User>()
init {
user.value = User()
}
}
and add new
class CustomMutableLiveData<T : BaseObservable> : MutableLiveData<T>() {
internal var callback: Observable.OnPropertyChangedCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
//Trigger LiveData observer on change of any property in object
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
//listen to property changes
value!!.addOnPropertyChangedCallback(callback)
}
}
I think the problem is that you only change the value of user's name, not the value of LiveData, so textview is not changing according to it.
Take a look at some change in your code, and 2-way dataBinding is worked in this case.
Change the ViewModel
class MainViewModel : ViewModel() {
val user = MutableLiveData<String>()
init {
user.value = "Ali"
}
}
and change the layout
<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="viewModel"
type="com.example.hoseinkelidari.databindingsample.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"
android:text="#={viewModel.user}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="#{viewModel.user}"
app:layout_constraintBottom_toTopOf="#+id/editText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>

MVVM notify View about loading state

I’m using LiveData by Google now and they recomend to use a MVVM patter design. For some of my requests I use RxJava2, and listen for responses in SubscribeWith(…).
For example, when I press a button to send some data to the remote data source, I’m showing some loading animation and want to hide it onComplete() event (inside subscribeWith(…)). The problem is that I don’t have an access to the View from ModelView. How it’s possible to let the View know that loading animation should be hidden?
My current idea is to create in interface inside ViewModel and implement it in View. But it ruins the concept of View and ViewModel separation.
Well you can use liveData for this :D
At your ViewModel class you can create a live data object like this
MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
and for example make a function called downloadFinished and call it in the onComplete
for your remote code
private void downloadFinished() {
isLoading.setValue(true);
}
At your activity that use the view model you observe the value of the loading and hide the progress or what ever you want
TestViewModel viewModel = ViewModelProviders.of(this).get(TestViewModel.class);
viewModel.isLoading.observe(this, new Observer<Boolean>() {
#Override
public void onChanged(#Nullable Boolean isLoading) {
if (isLoading != null) {
if (isLoading) {
// hide your progress bar
}
}
}
});
you can use DataBinding for that too
make a separate layout to reuse it everywhere
laoding_state_xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="#+id/progressBar2"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
then include it inside your desired layout
<?xml version="1.0" encoding="utf-8"?>
<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>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.blogspot.soyamr.notforgotagain.view.signin.SignInViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.signin.SignInFragment">
<include
android:id="#+id/include"
layout="#layout/toolbar_application"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/signInButtonView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="68dp"
android:layout_marginEnd="16dp"
android:onClick="#{() -> viewModel.logIn()}"
android:text="#string/sign_in"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/passwordTextInputLayout" />
<TextView
android:id="#+id/noAccountTextview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="4dp"
android:text="#string/no_account"
android:textColor="#android:color/black"
app:layout_constraintEnd_toStartOf="#+id/createAccountTextView"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/signInButtonView" />
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/emailTextInputLayout"
style="#style/myTextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="80dp"
android:layout_marginEnd="16dp"
app:errorEnabled="true"
app:errorText="#{viewModel.emailErrorMessage}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/include">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email"
android:inputType="textEmailAddress"
android:text="#={viewModel.emailText}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/passwordTextInputLayout"
style="#style/myTextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:errorText="#{viewModel.passwordErrorMessage}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/emailTextInputLayout">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/password"
android:inputType="textPassword"
android:text="#={viewModel.passwordText}" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="#+id/createAccountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:text="#string/create_one"
android:textColor="#color/textBlue"
app:layout_constraintBottom_toBottomOf="#+id/noAccountTextview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/noAccountTextview"
app:layout_constraintTop_toTopOf="#+id/noAccountTextview" />
<!-- **here is the important include**-->
<include
android:id="#+id/here_must_be_id_or_no_databinding"
android:visibility="#{viewModel.isLoading ? View.VISIBLE : View.GONE}"
layout="#layout/loading_state" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
i included the whole XML for clarification.
then in your view model add this
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
// Override ViewModelProvider.NewInstanceFactory to create the ViewModel (VM).
class SignInViewModelFactory(private val repository: NoteRepository) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SignInViewModel(repository) as T
}
class SignInViewModel(val repository: NoteRepository) : ViewModel() {
private val _isLoading = MutableLiveData(true)
val emailText = MutableLiveData("")
val passwordText = MutableLiveData("")
val isLoading: LiveData<Boolean> = _isLoading
fun logIn() {
//start loading, this will make the view start loading directly
_isLoading.value = true
if (isValidInput()) {
val res = repository.logIn(LoginUser(emailText.value!!, passwordText.value!!))
}//remove loading view
_isLoading.value = false
}
//code ..
}
notice that you are observing isLoading variable inside the XML so whenever its value is changed the view will observe the change and start act on it.

Categories

Resources