Android Kotlin Calling ViewModel function from View with Parameters - android

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

Related

Click Handling on Views in Clean Architecture

I have to implement click listener using binding and ViewModel as per clean architecture.
I have two button to select language like English and Chinese.
LanguageActivity.kt
#AndroidEntryPoint
class LanguageActivity : PBActivity(R.layout.activity_language) {
private val mViewModel:LanguageViewModel by viewModels()
private val mBinding:ActivityLanguageBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.apply {
lifecycleOwner = this#LanguageActivity
viewModel = mViewModel
}
collectFlow(mViewModel.uiState){
Toast.makeText(this, it.name, Toast.LENGTH_SHORT).show()
when(it){
LanguageSelected.ENGLISH -> {
}
LanguageSelected.CHINESE -> {
}
LanguageSelected.NONE -> {
}
}
}
}
}
enum class LanguageSelected{
ENGLISH,CHINESE,NONE
}
LanguageViewModel.kt
#HiltViewModel
class LanguageViewModel #Inject constructor(
private val pbPrefs: PBPrefs
): ViewModel(){
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LanguageSelected.NONE)
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LanguageSelected> = _uiState
fun englishSelected()= viewModelScope.launch {
_uiState.value = LanguageSelected.ENGLISH
pbPrefs.setLanguageSelect()
}
fun urduSelected() = viewModelScope.launch {
_uiState.value = LanguageSelected.URDU
pbPrefs.setLanguageSelect()
}
}
activity_language.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>
<variable
name="viewModel"
type="uk.co.planetbeyond.telenorbluecollar.ui.language.LanguageViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.language.LanguageActivity">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/center_vertical_gl"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<Button
android:id="#+id/englishTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="English "
android:onClick="#{() -> viewModel.englishSelected()}"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/urduTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Urdu"
android:onClick="#{() -> viewModel.urduSelected()}"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I have to select language on click of a button. What's the best approach as per clean architecture to implement
I have created an interface LanguageSelected but for that I have to created multiple methods in viewModel, As languages increase I have to create more methods in viewmodel.
How could I make it short or I mean extensible?

Android Two-Way Data Binding with Double (Kotlin)

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.

Why doesn't android:text display latest LiveData value?

The following code is based the project.
I modified a few code.
The android:text="#{viewmodel.name}" displays the LiveData value of the name.
The fun onLike() will change LiveData value of the name.
I think android:text="#{viewmodel.name}" will display latest value "My new" after I click the button (android:id="#+id/like_button").
But in fact, android:text="#{viewmodel.name}" keep to display "Ada", why?
SimpleViewModelSolution.kt
class SimpleViewModelSolution : ViewModel() {
private var _name = MutableLiveData("Ada") // I modified from private val _name = MutableLiveData("Ada")
val name: LiveData<String> = _name
...
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
_name = MutableLiveData("My new") // I added
}
}
solution.xml
<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.android.databinding.basicsample.data.SimpleViewModelSolution"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{viewmodel.name}"
..."/>
<Button
android:id="#+id/like_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:onClick="#{() -> viewmodel.onLike()}"
android:text="#string/like"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Change your onLike() to this:
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
_name.value = "My new"
}
Also, you can declare _name as val instead of var

A Kotlin property is in looping when getting the value ($field)

When I bind the value in the 'name' property in xml, the getter seems to be in a loop and the value inside it, is concatenating in the screen until I stop the app.
1 - I don't know with sure yet if I need to use notifyPropertyChanged() or the anotations #set and #get;
2 - If I set the get without the concatenating string, it's works nicelly: get() = field;
3 - If I try to return the get value inside braces, the problem keeps to occour: get(){return "Field: $field"};
This is the model:
class ContactModel : BaseObservable(){
#set:Bindable
#get:Bindable
var name: String = ""
get() = "Field: $field"
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#set:Bindable
#get:Bindable
var email: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.email)
}
#set:Bindable
#get:Bindable
var phone: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.phone)
}
}
Here's the activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var contactModel: ContactModel = ContactModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
contactModel = ContactModel(/*"Rômulo", "romulocoviel#gmail.com", "(19):98421-0821"*/)
contactModel.name = "Rômulo"
contactModel.email = "romulocoviel#gmail.com"
contactModel.phone = "(19):98421-0821"
binding.contactModel = contactModel
binding.setLifecycleOwner(this)
}
fun changeSignatures(view: View) {
Log.e("TESTING", "Testando!" + contactModel.name)
val nameList: ArrayList<ContactModel> = ArrayList()
contactModel.name = "asdasd"
contactModel.email = "asdasda"
contactModel.phone = "asdasd"
}
}
And here's the XML that I have a button that changes the values when tapped and the binding views:
<data>
<variable
name="contactModel"
type="com.example.romulo.bindingmetricsconversor.ContactModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:text="#={contactModel.name}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvName" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<TextView
android:text="#={contactModel.email}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvEmail" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvName" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<TextView
android:text="#={contactModel.phone}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/tvPhone" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvEmail" app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"/>
<Button
android:text="Change"
android:layout_width="wrap_content"
android:layout_height="49dp"
android:id="#+id/btChange" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp" android:onClick="changeSignatures"/>
</android.support.constraint.ConstraintLayout>
The result in the screen always is:
Field: asdasd
Field:Field: asdasd
Field:Field: asdasd
Field:Field:Field: asdasd
Field:Field:Field:Field: asdasd
... to the infinite
Just for the sake of completeness:
it seems like whenever the text view is updated by the property change listener it detects a change in its own content and thus tries to save back to the observable, triggering a loop, since you're using two-way binding.
The problem can be solved by using one-way binding instead (#{}), as upon changing its text the text view would trigger its own listeners and attempt to modify the observable, sending it into an infinite recursion.

How to implement validation using ViewModel and Databinding?

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
}

Categories

Resources