Click Handling on Views in Clean Architecture - android

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?

Related

How to pass multiple views as parameters in BindingAdapter in Data binding android?

I have one relative layout and one ImageView. I want to set visibility based on Image loading like if image loads successfully then imageview is visible and if some error occurs relative layout is visible. How can I manage this scenario in data binding using BindingAdapter ?
Your question is not clear so I don't know if it will help you.
These are steps to implement
1: Create parameters in BindingAdapter.kt
#BindingAdapter("showLoading")
fun View.showLoading(loading: Boolean) {
if (loading) {
visible()
} else {
gone()
}
}
#BindingAdapter("showError")
fun View.showError(error: Boolean) {
if (error) {
visible()
} else {
gone()
}
}
fun View.gone() {
this.visibility = View.GONE
}
fun View.visible() {
this.visibility = View.VISIBLE
}
2: Use parameters [app:showLoading="#{viewModel.showLoading}"] and [app:showError="#{viewModel.showError}"] created in file 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">
<data>
<variable
name="viewModel"
type="com.xxx.xxx.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EAEAE2"
android:orientation="vertical">
<Button
android:id="#+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ShowLoading" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:showLoading="#{viewModel.showLoading}" />
<RelativeLayout
android:id="#+id/viewError"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#color/colorPrimary"
app:showError="#{viewModel.showError}" />
</LinearLayout>
</layout>
3: Create showError and showLoading variables in Viewmodel.
And assign the viewModel variable to the binding.
class MainViewModel: ViewModel() {
val showError: MutableLiveData<Boolean> = MutableLiveData()
val showLoading: MutableLiveData<Boolean> = MutableLiveData()
init {
showError.postValue(false)
showLoading.postValue(true)
}
}
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
binding.viewModel = viewModel
initViews()
}
private fun initViews() {
binding.btnAdd.setOnClickListener {
viewModel.showError.postValue(true)
viewModel.showLoading.postValue(false)
}
}
}

Android binding adapter doesn't change after property changed

I write a code for a simple dice roll app but there is a problem in updating the image of an image view that bound with a binding adapter
I can't extend BaseObservable class because I already extended the ViewModel class. I need that for keeping data after the status changed. Also, I can't observe the live data inside my binding adapter because the function is static
do you know any other way?
This is my xml code
<?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="com.example.diceroller.MainActivityViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="#+id/imgDice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:imageRes="#{viewModel.drawableResource}" />
<Button
android:id="#+id/btnRoll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="Roll the dice"
android:onClick="#{() -> viewModel.onRollButtonClicked()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
and this is my kotlin code
class MainActivityViewModel : ViewModel() , Observable {
#Bindable
var drawableResource : MutableLiveData<Int> = MutableLiveData<Int>()
init {
drawableResource.value = R.drawable.dice_1
}
fun onRollButtonClicked(){
drawableResource.value = when(Random.nextInt(1,7)){
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
}
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
override fun removeOnPropertyChangedCallback(callback:
Observable.OnPropertyChangedCallback?) {
}
}
#BindingAdapter("android:imageRes")
fun loadImage(view : View , imageRes : Int){
(view as ImageView).setImageResource(imageRes)
}

Button onCLick does not work in MVVM Android

I have a MVVM setup for my project and I have a simple layout with an EditText and a button and when I click the button I want to show Text which is in the EditText.
For the button when i add. this code android:onClick="#={() -> addProductViewModel.addProduct()}" it gives me an error A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
and when I remove that like app builds okay.
Error is not so clear and I am not sure how to fix it.
here is my code
add_product.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" >
<data class=".AddProductBinding">
<variable
name="addProductViewModel"
type="com.rao.iremind.AddProductViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/etProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:text="#={addProductViewModel.inputProductName}"
android:hint="Product name"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/btn_add_product"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"
android:text="Add product"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/etProductName" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
AddProductViewModel
class AddProductViewModel (
private val repository: ProductRepository,
private val context: Context
): ViewModel(), Observable {
#Bindable
val inputProductName = MutableLiveData<String>()
fun addProduct() {
//inputProductName.value
Toast.makeText(context, " add product ", Toast.LENGTH_LONG).show()
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
}
AddProductViewModelFactory
class AddProductViewModelFactory (
private val repository: ProductRepository,
private val context: Context
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AddProductViewModel::class.java)) {
return AddProductViewModel(repository, context) as T
}
throw IllegalArgumentException("Unknown View Model class")
}
}
Your help and suggestions are much appreciated
Thanks
R

How to access a button in included layout using data binding

I am using databinding in my application but button is not working. where am i making mistake in this code. i tried many solution but no luck. but if make button in activity_login.xml then button click works. i think i am not able to pass the view model to the included view.
Here is my code
activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name ="loginViewModel"
type="com.innowi.checoutrestaurantdashboard.view.main.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/login_constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/login_background_burgerstack"
android:isScrollContainer="false"
android:paddingEnd="32dp"
android:paddingStart="32dp"
tools:context=".view.main.LoginActivity">
<include
android:id="#+id/login_layout"
layout="#layout/layout_login"
android:layout_width="0dp"
android:layout_height="0dp"
bind:loginViewModel="#{loginViewModel}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
layout_login.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">
<data>
<variable
name ="loginViewModel"
type="com.innowi.checoutrestaurantdashboard.view.main.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
style="#style/Login"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="false"
android:padding="32dp">
<Button
android:id="#+id/login_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="#drawable/button_login_drawable"
android:enabled="false"
android:gravity="center"
android:paddingBottom="16dp"
android:paddingTop="16dp"
android:text="#string/button_sign_in"
android:textColor="#color/colorWhite"
android:textSize="24sp"
android:onClick="#{loginViewModel::onLoginButtonClick}"
app:layout_constraintLeft_toLeftOf="#+id/login_username"
app:layout_constraintRight_toRightOf="#+id/login_username"
app:layout_constraintTop_toBottomOf="#+id/login_password" />
.
.
.
.
</android.support.constraint.ConstraintLayout>
</layout>
MainViewModel.kt
class MainViewModel #Inject constructor(
private val loginRepository: LoginRepository
) : BaseViewModel() {
fun onLoginButtonClick(view : View){
performLogin()
}
.
.
.
}
LoginActivity.kt
class LoginActivity : AppBaseActivity() {
private lateinit var loginViewBinding: ActivityLoginBinding
private lateinit var viewModel: MainViewModel
private val TAG = LoginActivity::class.simpleName
override fun initViewModel(viewModelProvider: ViewModelProvider): BaseViewModel? {
viewModel = viewModelProvider.get(MainViewModel::class.java)
return viewModel
}
override fun render(state: ViewState) {
when (state) {
is LoadingState -> {
// loading
val loading = state.loading
Log.d(TAG,"Loading State")
}
is DefaultState -> {
// render Data
val data = state.data
Log.d(TAG,"Data State")
}
is ErrorState -> {
// show error
val error = state.error
Log.d(TAG,"Loading State")
}
}
}
override fun setContentView() {
loginViewBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginViewBinding.loginViewModel = viewModel
loginViewBinding.executePendingBindings()
}
}

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