I have the standard login form which has 2 edit text (one for email and other for password) and a button for log-in.
I want to send email and password to the server when the user clicks on login button.
activity_main.xml
<?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 class="ActivityMainBinding">
<variable
name="login"
type="com.example.itstym.reminder.loginViewModel" />
<variable
name="handler"
type="com.example.itstym.reminder.LoginHandler" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.itstym.reminder.MainActivity">
<EditText
app:error="#{login.errorEmail}"
android:text="#{login.userEmailAddress}"
android:hint="Email Address"
android:id="#+id/email_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:ems="10"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="24dp"/>
<EditText
app:error="#{login.errorPassword}"
android:text="#{login.userPassword}"
android:hint="Password"
android:id="#+id/user_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="0dp"
android:ems="10"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="#+id/email_address"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="#+id/email_address"/>
<Button
android:onClick="#{() -> handler.onLoginButtonClicked(login)}"
android:id="#+id/submit_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:text="Sign In"
app:layout_constraintEnd_toEndOf="#+id/user_password"
app:layout_constraintStart_toStartOf="#+id/user_password"
app:layout_constraintTop_toBottomOf="#+id/user_password"/>
</android.support.constraint.ConstraintLayout>
</layout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding=DataBindingUtil.setContentView<com.example.itstym.reminder.databinding.ActivityMainBinding>(this#MainActivity,R.layout.activity_main)
Log.i("Binding class ",binding.javaClass.simpleName.toString())
Log.i("type ",binding.javaClass.toString())
val login:loginViewModel=loginViewModel()
binding.login=login
val handler= LoginHandler()
binding.handler=handler
}
}
loginViewModel.kt
class loginViewModel(): BaseObservable() {
#Bindable
var userEmailAddress:String= String()
set(userEmailAddress){
field=userEmailAddress
notifyPropertyChanged(R.id.email_address)
/*to check Email for validation on every character inserted by user*/
notifyPropertyChanged(BR.errorEmail)
}
get() {
return field
}
#Bindable
var userPassword:String = String()
set(userPassword){
field=userPassword
notifyPropertyChanged(R.id.user_password)
notifyPropertyChanged(BR.errorPassword)
}
get() {
return field
}
}
//some code removed for readablitly purpose
LoginHandler.kt
class LoginHandler() {
fun onLoginButtonClicked(userInfo: loginViewModel){
Log.i("Button Clicked ","yup")
Log.i("Email is ",userInfo.userEmailAddress)
Log.i("Password is ",userInfo.userPassword)
}
}
Problem: I am not able to print the email address and password inside the written in onLoginButtonClicked(). Why??
Any lead will helpfull.
Before downvoting the question, add the comment that will helps in improving the question.
In your activity_main.xml file, you're only using One-Way Data Binding. If you want to use Two-Way Data Binding, you must use the equal sign after the at sign (#=) for example:
One-way Data Binding (Only update the view when the property change)
android:text="#{login.userEmailAddress}"
android:text="#{login.userPassword}"
Two-way Data Binding (Update the view when the property change and vice-versa)
android:text="#={login.userEmailAddress}"
android:text="#={login.userPassword}"
There is a great talk about Data Binding by Kevin Pelgrim if you want to go deeper with the topic.
Extra Resources:
Episode 057: Data Binding with GDE Lisa Wray - Fragmented Podcast
Episode 35: Data Bound - Android Developer Backstage
Hope it helps!
Related
I read a few posts to convert from String to Integer and tried to do the same to convert string to double using two-way binding but was unable to do it.
SampleViewModel.kt
class SampleViewModel: ViewModel() {
val weight = MutableLiveData<Double>()
fun validateFields() {
Log.i("SAMPLE_VIEW_MODEL", "validateFields: ${weight.value}")
}
}
TypeConverters.kt
object TypeConverters {
#InverseMethod("stringToDouble")
#JvmStatic
fun doubleToString(value: Double?): String {
return value?.toString() ?: ""
}
#JvmStatic
fun stringToDouble(value: String): Double? {
if (TextUtils.isEmpty(value)) {
return null
}
return value.toDoubleOrNull()
}
}
fragment_sample.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.sampleapp.SampleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/til_weight"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/_10sdp"
android:hint="#string/hint_weight"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="#+id/tilName">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/tiet_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={TypeConverters.doubleToString(viewModel.weight)}"/>
</com.google.android.material.textfield.TextInputLayout>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The example above does not let me enter the proper value on UI like "56.78". I tried to follow this post but doesn't work for me. Another way is to take string value and then convert it into double and vice versa accordingly. I would like to know which is the correct way.
In your TextInputEditText, update this line
android:text="#={String.valueOf(viewModel.weight)}"
See Results
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/tiet_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={String.valueOf(viewModel.weight)}"/>
I have to implement OTP validation on my application, I just want to get the mobile number from the previous activity and set it to the textview on the next activity once I clicked the Next Button intent from Activity 1 to Activity 2.
Here is how it looks like in view:
I would like to put the mobile number below the please enter the otp that has sent to..
First try: I called the ObjectSingleton.mobileNum since it carries the mobile # then I used mobileNum.setText(ObjectSingleton.mobileNum) but it ddint work.
My codes in 1st Activity where intent is happening to go to the next Activity;
class MobileNumberActivity : AppCompatActivity(), OtpInterface.MobileNumberViews {
lateinit var presenterMobileNumber:MobileNumberPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mobile_number)
presenterMobileNumber = MobileNumberPresenter()
presenterMobileNumber.mobileViews = this
close_icon2.setOnClickListener {
finish()
}
nxtBtn.setOnClickListener {
if (inputText.text.isNullOrEmpty()) {
inputText.error = "Please enter valid mobile number."
} else if (inputText.text.toString().length != 10) {
inputText.error = "Please enter valid mobile number."
} else{
val mobile = inputText.text.toString()
presenterMobileNumber.otpMobile(mobile)
}
}
}
override fun ifFailed(msg: String) {
errorMsg.setText(msg)
}
override fun ifSuccess(res: OtpData) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
startActivity(intent)
}
Here are the codes of the 2nd Acitivy where I want to change the text of mobile number display
class OtpValidationActivity : AppCompatActivity(), OtpValidateInterface.OtpValidateViews {
lateinit var otpPresenter: OtpPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_otp_verification)
otpPresenter = OtpPresenter()
otpPresenter.otpValidationViews = this
verifyOtpBtn.setOnClickListener {
if (otp_input.text.isNullOrEmpty()) {
otp_input.error = "Please enter OTP."
} else{
val otpNumber = otp_input.text.toString()
otpPresenter.otpValidate(ObjectSingleton.mobileNum,otpNumber)
}
}
}
override fun validateFailed(res: String) {
resend.setText("Failed")
}
override fun validateSuccess(msg: OtpValidationData) {
resend.setText("Success")
}
For your REFERENCE here's the XML file of the said PICTURE above where should I put the Mobile number.
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/rectangle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/linearLayout6"
android:layout_width="372dp"
android:layout_height="wrap_content"
android:background="#drawable/otpbg"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.487"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.041">
<LinearLayout
android:id="#+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_30sdp"
android:text="OTP VERIFICATION"
android:textColor="#color/reply_black_800"
android:textSize="#dimen/_22sdp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</TextView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please enter the OTP that has sent to">
</TextView>
<TextView
android:id="#+id/mobileNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_22sdp"
android:layout_marginBottom="#dimen/_30sdp"
android:text="#string/_63"
android:textColor="#color/colorPrimary"
android:textSize="#dimen/_19sdp"
android:textStyle="bold">
</TextView>
</LinearLayout>
<com.mukesh.OtpView
android:id="#+id/otp_input"
android:layout_width="267dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:layout_marginLeft="#dimen/_10sdp"
android:layout_marginRight="#dimen/_10sdp"
android:inputType="number"
android:textAppearance="?attr/textAppearanceHeadline5"
android:textColor="#android:color/black"
app:OtpItemCount="6"
app:OtpItemWidth="#dimen/_30sdp"
app:OtpLineColor="#color/reply_black_800"
app:OtpViewType="line" />
<TextView
android:id="#+id/resend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_19sdp"
android:gravity="center"
android:textSize="#dimen/_14sdp"
android:text="#string/didn_t_recieve_the_code_resend_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/otp_view"
app:layout_constraintVertical_bias="0.039"
android:paddingBottom="#dimen/_30sdp"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/verifyOtpBtn"
android:layout_width="378dp"
android:layout_height="42dp"
android:background="#drawable/radius_btn"
android:backgroundTint="#color/colorPrimary"
android:text="Next"
android:textAllCaps="false"
android:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.484"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/linearLayout6"
app:layout_constraintVertical_bias="0.117" />
</androidx.constraintlayout.widget.ConstraintLayout>
I am new to Kotlin/Android Develepment I hope someone help me with this.
#JUniorDEV
UPDATE: Imma share to you my MobileNumberPresenter codes:
var mobileViews:OtpInterface.MobileNumberViews? = null
override fun otpMobile(mobile: String) {
Fuel.post("https://api.staging.riderko.com/riderko_be/public/api/riderSendRegisterOtp", listOf(
"mobile" to mobile
)).timeout(5000)
.header("Accept", "application/json")
.responseObject<OtpResponse>{request, response, result ->
when (result) {
is Result.Failure -> {
mobileViews?.ifFailed(result.error.response.statusCode.toString())
}
is Result.Success -> {
val (bytes, error) = result
if (bytes != null) {
val status = bytes.success
if (status){
mobileViews?.ifSuccess(bytes.data)
}else{
mobileViews?.ifFailed(bytes.message)
}
}
}
}
}
}
in OtpInterface.MobileNumberViews
add second parameter as string to ifSuccess
in MobileNumberPresenter
change mobileViews?.ifSuccess(bytes.data) to mobileViews?.ifSuccess(bytes.data, mobile)
and you ifSuccess method will be changed to
override fun ifSuccess(res: OtpData, mobileNum: String) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
intent.putExtra("MOBILE_NUMBER",mobileNum)
startActivity(intent)
}
And in OtpValidationActivity
val mobileNum = arguments?.getString("MOBILE_NUMBER")
Update the code of 1st activity to
override fun ifSuccess(res: OtpData) {
errorMsg.setText("Sent!")
var intent = Intent(this, OtpValidationActivity::class.java)
startActivity(intent)
intent.putExtra("MobileNum",mobile)
}
Now in the second activity after setContentView(R.layout.activity_otp_verification)
add this line:
val MobNo = intent.getExtra("MobileNum")
mobileNum.text = MobNo
Hope this will work
Here in the MobileNumberActivity inside the ifSuccess() before you do startActivity(intent) do intent.putExtra("IDENTIFER",mobileNum)
Now in the 2nd activity simply do val mobNO = intent.getStringExtra("IDENTIFER")
and now assign the mobNo to the desired textView.
This helps pass data between activities.
I was able to solve this simply by calling ObjectSingleton.mobNum assigned to a variable and displayed it into view I just learnt that once objectsingleton has been assigned to, the value will be carried over to any part of your app, this value will be removed automatically when you close your app.
I have a ViewModel class defined as follows:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
}
That is bound to the following layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="it.kfi.lorikeetmobile.extras.Converter" alias="Converter"/
<variable
name="viewModel"
type="it.kfi.lorikeetmobile.stock.models.StockLoadTaskModel" />
<variable
name="view"
type="it.kfi.lorikeetmobile.stock.ui.movements.StockLoadTaskFragment
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_item_code"
android:text="#={viewModel.itemCode}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={Converter.doubleToString(d)}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_note"
android:lines="3"
android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls"
android:gravity="top"
android:inputType="textMultiLine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/hint_et_note"
android:text="#={viewModel.selectedItem.detail.note}"/>
</com.google.android.material.textfield.TextInputLayout>
...
</LinearLayout>
And I have also the following Converter object:
object Converter {
#JvmStatic
#InverseMethod("stringToDouble")
fun doubleToString(value: Double?): String? {
if (value == null) {
return null
}
return DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).format(value)
}
#JvmStatic
fun stringToDouble(value: String?): Double? {
if (value == null) {
return null
}
val v = DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).parse(value)
return v.toDouble()
}
}
If I set: android:text="#={Converter.doubleToString(d)}" (two-way databinding), in the EditText with id et_quantity I get the following error:
...error: cannot find symbol
If I change it into a one-way databinding like: android:text="#{Converter.doubleToString(d)}", it works. It looks like the binding manager is not able to recognize the inverse method.
Can anybody help me? Thank you.
Why the error happens?
When you define two-way data binding like you have in your example android:text="#={Converter.doubleToString(d)}" the question is: what function/object will receive data that you get back passed from EditText as user types data in? Should data be passed to Converter.doubleToString or maybe some other static function of Converter? Maybe to the result of Converter.doubleToString(d) or to d variable?
You must be precise.
You expect it is d, the compiler expects it is the result of Converter.doubleToString(d). Actually, neither will work.
Another issue is that EditText does operate with characters. It knows nothing about double, int, float, byte, short, boolean or anything else that is not a string.
It means that in order to implement two-way data binding your source:
must return value of type String;
must be assignable.
How to fix the issue?
Android architecture components introduce us with ObservableField class. There are ready to use ObservableBoolean, ObservableChar, ObservableFloat and a few others. If you open the link from the previous sentence you should see all of the classes Observable... on the left pane.
There is no ObservableString but ObservableField accepts a generic type. So you can define a variable that is a part of data binding to be ObservableField<String>("defaultValueHere").
So what you should have is:
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable = ObservableField<String>(d.toString())
}
The dataBindingVariable will always return you the contents of an EditText you bound it to. You can get that value and safely convert to double.
class StockLoadTaskModel : ViewModel() {
....
....
var d: Double = 10.0
var dataBindingVariable =
object: ObservableField<String>(d.toString()) {
override fun set(value: String?) {
super.set(value)
// a value has been set
d = value.toDoubleOrNull() ?: d
}
}
}
Layout declaration will look like that for input field:
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/et_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={viewModel.dataBindingVariable}"
android:hint="#string/quantity" />
</com.google.android.material.textfield.TextInputLayout>
And there will be no need for object Converter.
There is another way of doing two-way data binding I'm not talking about here because it was already answered. Here it is.
I am building an Android app.
I have a layout that contains a button 'saveButton'. When the user clicks the button, the onClick should call a function in my ViewModel, onSave(). This function requires 2 parameters: the text contents of 2 EditText views that are also present in the same layout.
Basically, the user has edited the name and/or the synopsis and now wants to have the ViewModel update the object's data in the database.
(Part of) my UI (.xml fragment 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"
tools:context=".ui.CreateEditRelationFragment">
<data>
<variable
name="createEditRelationViewModel"
type="be.pjvandamme.farfiled.viewmodels.CreateEditRelationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/saveButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/saveText"
android:onClick="#{() -> createEditRelationViewModel.onSave(relationNameEditText.getEditText().getText().toString(), relationSynopsisEditText.getEditText().getText().toString())}" />
<Button
android:id="#+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="#string/cancelText"
android:onClick="#{() -> createEditRelationViewModel.onCancel()}" />
<EditText
android:id="#+id/relationSynopsisEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine" />
<EditText
android:id="#+id/relationNameEditText"
android:layout_width="#dimen/relationNameEditWidth"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
(Part of) the ViewModel:
class CreateEditRelationViewModel (
private val relationKey: Long?,
val database: RelationDao,
application: Application
): AndroidViewModel(application){
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var relation = MutableLiveData<Relation?>()
private var _navigateToRelationDetail = MutableLiveData<Boolean>()
val navigateToRelationDetail: LiveData<Boolean>
get() = _navigateToRelationDetail
fun onSave(name: String, synopsis: String){
Timber.i("Got name: " + name + " and synopsis: " + synopsis)
if(relationKey == null){
uiScope.launch{
relation.value = Relation(0, name, synopsis, false)
insert(relation.value!!)
}
}
else{
uiScope.launch{
relation.value?.name = name
relation.value?.synopsis = synopsis
update(relation.value)
}
}
_navigateToRelationDetail.value = true
}
private suspend fun insert(newRelation: Relation){
withContext(Dispatchers.IO){
database.insert(newRelation)
}
}
private suspend fun update(relation: Relation?){
if(relation != null) {
withContext(Dispatchers.IO) {
database.update(relation)
}
}
}
}
I would want this thing to compile so that the onSave() function is called and the contents of the EditTexts passed as parameters.
I cannot manage to pass the text contents of these EditTexts. The compiler throws this error:
[databinding] {"msg":"cannot find method getEditText() in class android.widget.EditText","file":"D:\\ etc.
It does the same thing when I try to access using the .text property directly.
Does anyone know what I'm doing wrong? I'm tearing my hair out.
getEditText() method doesn't exist and you can remove it.
instead of;
relationNameEditText.getEditText().getText().toString()
you can do
relationNameEditText.getText().toString()`
i.e.
https://github.com/dgngulcan/droid-feed/blob/e0d0d5f4af07c5375d42b74e42c55b793319a937/app/src/main/res/layout/fragment_newsletter.xml#L120
What is the best approach to validate form data using ViewModel and Databinding?
I have a simple Sign-Up activity that links binding layout and ViewModel
class StartActivity : AppCompatActivity() {
private lateinit var binding: StartActivityBinding
private lateinit var viewModel: SignUpViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, SignUpViewModelFactory(AuthFirebase()))
.get(SignUpViewModel::class.java);
binding = DataBindingUtil.setContentView(this, R.layout.start_activity)
binding.viewModel = viewModel;
signUpButton.setOnClickListener {
}
}
}
ViewModel with 4 ObservableFields and signUp() method that should validate data before submitting data to the server.
class SignUpViewModel(val auth: Auth) : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
val email: MutableLiveData<String> = MutableLiveData()
val password: MutableLiveData<String> = MutableLiveData()
val passwordConfirm: MutableLiveData<String> = MutableLiveData()
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
}
I guess we can add four boolean ObservableFields for each input, and in signUp() we can check inputs and change state of boolean ObservableField that will produce an appearing error on layout
val isNameError: ObservableField<Boolean> = ObservableField()
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
But I am not sure that ViewModel should be responsible for validation and showing an error for a user and we will have boilerplate code
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.maximdrobonoh.fitnessx.SignUpViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorGreyDark"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="#+id/appTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="#string/app_title"
android:textColor="#color/colorWhite"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/screenTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/appTitle">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="#string/sign"
android:textColor="#color/colorWhite"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/up"
android:textColor="#color/colorWhite"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:id="#+id/form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/screenTitle">
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_name"
android:inputType="textPersonName"
android:text="#={viewModel.name}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_email"
android:inputType="textEmailAddress"
android:text="#={viewModel.email}"
/>
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_password"
android:inputType="textPassword"
android:text="#={viewModel.password}" />
<android.support.v7.widget.AppCompatEditText
style="#style/SignUp.InputBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/sign_up_confirm_password"
android:inputType="textPassword"
android:text="#={viewModel.passwordConfirm}" />
<Button
android:id="#+id/signUpButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="#drawable/button_gradient"
android:text="#string/sign_up_next_btn"
android:textAllCaps="true"
android:textColor="#color/colorBlack" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</layout>
There can be many ways to implement this. I am telling you two solutions, both works well, you can use which you find suitable for you.
I use extends BaseObservable because I find that easy than converting all fields to Observers. You can use ObservableFields too.
Solution 1 (Using custom BindingAdapter)
In xml
<variable
name="model"
type="sample.data.Model"/>
<EditText
passwordValidator="#{model.password}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={model.password}"/>
Model.java
public class Model extends BaseObservable {
private String password;
#Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
DataBindingAdapter.java
public class DataBindingAdapter {
#BindingAdapter("passwordValidator")
public static void passwordValidator(EditText editText, String password) {
// ignore infinite loops
int minimumLength = 5;
if (TextUtils.isEmpty(password)) {
editText.setError(null);
return;
}
if (editText.getText().toString().length() < minimumLength) {
editText.setError("Password must be minimum " + minimumLength + " length");
} else editText.setError(null);
}
}
Solution 2 (Using custom afterTextChanged)
In xml
<variable
name="model"
type="com.innovanathinklabs.sample.data.Model"/>
<variable
name="handler"
type="sample.activities.MainActivityHandler"/>
<EditText
android:id="#+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
android:text="#={model.password}"/>
MainActivityHandler.java
public class MainActivityHandler {
ActivityMainBinding binding;
public void setBinding(ActivityMainBinding binding) {
this.binding = binding;
}
public void passwordValidator(Editable editable) {
if (binding.etPassword == null) return;
int minimumLength = 5;
if (!TextUtils.isEmpty(editable.toString()) && editable.length() < minimumLength) {
binding.etPassword.setError("Password must be minimum " + minimumLength + " length");
} else {
binding.etPassword.setError(null);
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setModel(new Model());
MainActivityHandler handler = new MainActivityHandler();
handler.setBinding(binding);
binding.setHandler(handler);
}
}
Update
You can also replace
android:afterTextChanged="#{(edible)->handler.passwordValidator(edible)}"
with
android:afterTextChanged="#{handler::passwordValidator}"
Because parameter are same of android:afterTextChanged and passwordValidator.
This approach uses TextInputLayouts, a custom binding adapter, and creates an enum for form errors. The result I think reads nicely in the xml, and keeps all validation logic inside the ViewModel.
The ViewModel:
class SignUpViewModel() : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
// the rest of your fields as normal
val formErrors = ObservableArrayList<FormErrors>()
fun isFormValid(): Boolean {
formErrors.clear()
if (name.value?.isNullOrEmpty()) {
formErrors.add(FormErrors.MISSING_NAME)
}
// all the other validation you require
return formErrors.isEmpty()
}
fun signUp() {
auth.createUser(email.value!!, password.value!!)
}
enum class FormErrors {
MISSING_NAME,
INVALID_EMAIL,
INVALID_PASSWORD,
PASSWORDS_NOT_MATCHING,
}
}
The BindingAdapter:
#BindingAdapter("app:errorText")
fun setErrorMessage(view: TextInputLayout, errorMessage: String) {
view.error = errorMessage
}
The XML:
<layout>
<data>
<import type="com.example.SignUpViewModel.FormErrors" />
<variable
name="viewModel"
type="com.example.SignUpViewModel" />
</data>
<!-- The rest of your layout file etc. -->
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/text_input_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorText='#{viewModel.formErrors.contains(FormErrors.MISSING_NAME) ? "Required" : ""}'>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Name"
android:text="#={viewModel.name}"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Any other fields as above format -->
And then, the ViewModel can be called from activity/fragment as below:
class YourActivity: AppCompatActivity() {
val viewModel: SignUpViewModel
// rest of class
fun onFormSubmit() {
if (viewModel.isFormValid()) {
viewModel.signUp()
// the rest of your logic to proceed to next screen etc.
}
// no need for else block if form invalid, as ViewModel, Observables
// and databinding will take care of the UI
}
}
I've written a library for validating bindable fields of an Observable object.
Setup your Observable model:
class RegisterUser:BaseObservable(){
#Bindable
var name:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#Bindable
var email:String?=""
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
}
Instantiate and add rules
class RegisterViewModel : ViewModel() {
var user:LiveData<RegisterUser> = MutableLiveData<RegisterUser>().also {
it.value = RegisterUser()
}
var validator = ObservableValidator(user.value!!, BR::class.java).apply {
addRule("name", ValidationFlags.FIELD_REQUIRED, "Enter your name")
addRule("email", ValidationFlags.FIELD_REQUIRED, "Enter your email")
addRule("email", ValidationFlags.FIELD_EMAIL, "Enter a valid email")
addRule("age", ValidationFlags.FIELD_REQUIRED, "Enter your age (Underage or too old?)")
addRule("age", ValidationFlags.FIELD_MIN, "You can't be underage!", limit = 18)
addRule("age", ValidationFlags.FIELD_MAX, "You sure you're still alive?", limit = 100)
addRule("password", ValidationFlags.FIELD_REQUIRED, "Enter your password")
addRule("passwordConfirmation", ValidationFlags.FIELD_REQUIRED, "Enter password confirmation")
addRule("passwordConfirmation", ValidationFlags.FIELD_MATCH, "Passwords don't match", "password")
}
}
And setup your xml file:
<com.google.android.material.textfield.TextInputLayout
style="#style/textFieldOutlined"
error='#{viewModel.validator.getValidation("email")}'
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/email"
style="#style/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your email"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:text="#={viewModel.user.email}" />
What you have in mind is right, actually. The viewmodel should not know anything about the android system and will only work with pure java/kotlin. Thus, doing what you are thinking of is right. ViewModel's shouldn't know about the android system as all view interactions should be handled on the View. But, their properties can be bounded to the view.
TL;DR
This will work
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
Suggestion
I would suggest, if you would like to dig in deeper, you could use Custom Binding Adapters. This way you:
won't need additional variables to your view model
have a cleaner view model since the error handling is on the custom binding adapter
would easier read on your XML views as you could define there the validations you need
I'll let your imagination fly on how you could make the custom binding adapter only have the validations. For now, it's better to understand the basics of custom binding adapters.
Cheers
Yes, you can use your validation logic from ViewModel, because you're having your observable variables from ViewModel & your xml is also deriving data from ViewModel class also.
So, Solution :
You can create #BindingAdapter in ViewModel and bind your
button click with it. Check your validation there and do some other
stuffs also.
You can create Listener, and implement it on ViewModel to receive clicks from button and bind that listener to xml.
You can use Two-Way data binding also (Be aware of infinite loops though).
//Let's say it's your binding adapter from ViewModel
fun signUp() {
if (check validation logic) {
// Produce errors
}
// Further successful stuffs
}