web-dev-qa-db-fra.com

Comment implémenter la validation en utilisant ViewModel et Databinding?

Quelle est la meilleure approche pour valider les données de formulaire à l'aide de ViewModel et Databinding?

J'ai une activité d'inscription simple qui relie la mise en page de liaison et 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 avec 4 ObservableFields et signUp() méthode qui devrait valider les données avant de les soumettre au serveur.

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!!)
    }
}

Je suppose que nous pouvons ajouter quatre booléens ObservableFields pour chaque entrée, et dans signUp() nous pouvons vérifier les entrées et changer l'état de booléen ObservableField qui produira une erreur apparente sur la mise en page

val isNameError: ObservableField<Boolean> = ObservableField()


fun signUp() {

        if (name.value == null || name.value!!.length < 2 ) {
            isNameError.set(true)
        }

        auth.createUser(email.value!!, password.value!!)
    }

Mais je ne suis pas sûr que ViewModel devrait être responsable de la validation et de l'affichage d'une erreur pour un utilisateur et nous aurons un code passe-partout

<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>
9
Maxim Drobonog

Il peut y avoir plusieurs façons de mettre cela en œuvre. Je vous dis deux solutions, les deux fonctionnent bien, vous pouvez utiliser celle que vous trouvez appropriée pour vous.

J'utilise extends BaseObservable car je trouve cela aussi simple que de convertir tous les champs en Observers. Vous pouvez également utiliser ObservableFields.

Solution 1 (utilisation de BindingAdapter personnalisé)

en 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 (utilisation de afterTextChanged personnalisé)

en 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);
    }
}

Mise à jour

Vous pouvez également remplacer

Android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}"

avec

Android:afterTextChanged="@{handler::passwordValidator}"

Parce que les paramètres sont identiques à Android:afterTextChanged et passwordValidator.

5
Khemraj

Oui, vous pouvez utiliser votre logique de validation à partir de ViewModel, car vous avez vos variables observables à partir de ViewModel et votre xml dérive également des données de la classe ViewModel également.

Donc, Solution:

  • Vous pouvez créer @BindingAdapter dans ViewModel et liez votre bouton avec lui. Vérifiez votre validation là-bas et faites également d'autres choses.

  • Vous pouvez créer Listener, et l'implémenter sur ViewModel pour recevoir les clics du bouton et lier cet écouteur à xml.

  • Vous pouvez utiliser Liaison de données bidirectionnelle également (Attention aux boucles infinies) .

    //Let's say it's your binding adapter from ViewModel
    fun signUp() {
       if (check validation logic) {
          // Produce errors
       }
       // Further successful stuffs
    }
    
0
Jeel Vankhede

Cette approche utilise TextInputLayouts, un adaptateur de liaison personnalisé, et crée une énumération pour les erreurs de formulaire. Le résultat, je pense, se lit bien dans le xml et conserve toute la logique de validation à l'intérieur du ViewModel.

Le 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,
   }

}

Le BindingAdapter:

@BindingAdapter("app:errorText")
fun setErrorMessage(view: TextInputLayout, errorMessage: String) {
    view.error = errorMessage
}

Le 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 -->

Et puis, le ViewModel peut être appelé à partir d'une activité/d'un fragment comme ci-dessous:

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
  }


}
0
Vin Norman

Ce que vous avez en tête est vrai, en fait. Le viewmodel ne devrait rien savoir du système Android et ne fonctionnera qu'avec du Java/kotlin pur. Ainsi, faire ce à quoi vous pensez est correct. ViewModel ne devrait pas connaître le Android car toutes les interactions entre les vues doivent être gérées sur la vue. Mais, leurs propriétés peuvent être limitées à la vue.

TL; DR

Cela fonctionnera

fun signUp() {

    if (name.value == null || name.value!!.length < 2 ) {
        isNameError.set(true)
    }

    auth.createUser(email.value!!, password.value!!)
}


Suggestion

Je suggérerais, si vous souhaitez creuser plus profondément, vous pouvez utiliser des adaptateurs de liaison personnalisés. De cette façon, vous:

  • n'a pas besoin de variables supplémentaires pour votre modèle de vue
  • avoir un modèle de vue plus propre car la gestion des erreurs se fait sur l'adaptateur de liaison personnalisé
  • serait plus facile à lire sur vos vues XML car vous pourriez y définir les validations dont vous avez besoin

Je laisserai votre imagination voler sur la façon dont vous pourriez faire en sorte que l'adaptateur de reliure personnalisé n'ait que les validations. Pour l'instant, il est préférable de comprendre les bases des adaptateurs de liaison personnalisés.

Acclamations

0
Jian Astrero

J'ai écrit un bibliothèque pour valider les champs pouvant être liés d'un objet observable.

Configurez votre modèle observable:

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)
    }

}

Instancier et ajouter des règles

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")
}

}

Et configurez votre fichier xml:

<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}" />
0
Thalis Vilela

J'ai trouvé plusieurs articles sur l'implémentation de la validation de formulaire en termes de composants de l'architecture Android, et la plupart des solutions sont les suivantes: "ajoutez une propriété à votre modèle pour chaque champ que vous modifiez, et ajoutez une propriété supplémentaire pour chaque erreur de champ ". Dans le résultat, vous obtiendrez un joli code passe-partout, mais pour quoi faire? La bibliothèque ViewModel est destinée à rendre notre vie plus facile, pas à la compliquer.

Si la validation nécessite un accès à la vue, pour afficher les erreurs, les toasts, etc., pourquoi ne pas laisser la logique dans la vue? Le ViewModel charge les données et les fournit à la vue, qui valide les données et les renvoie au ViewModel. La vue enregistre l'état des composants comme EditText par défaut, le composant devrait juste avoir des identifiants. Vous ne perdrez pas l'état enregistré lors d'une recréation d'activité/fragment malgré le fait que les composants sont liés, car l'état enregistré a une priorité plus élevée.

ViewModel.kt

data class User(
    val id: String,
    val firstName: String,
    val lastName: String
)

class MainVM : ViewModel() {
    val user: LiveData<User> = ...
    fun save(firstName: CharSequence, lastName: CharSequence) { ... }
}

ActivityMain.xml

...
  <EditText Android:id="@+id/firstNameInput"
            Android:text="@{model.user.firstName}"
            ... />

  <EditText Android:id="@+id/lastNameInput"
            Android:text="@{model.user.lastName}"
            ... />

  <Button Android:onClick="onSaveButtonClick" ... />
...

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var model: MainVM
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        model = ViewModelProviders.of(this)[MainVM::class.Java]

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.model = model
    }

    fun onSaveButtonClick(button: View) {
        val firstName = binding.firstNameInput.text.trim()
        if (firstName.isBlank()) {
            binding.firstNameInput.error = getString(R.string.first_name_is_blank)
            return
        }

        val lastName = binding.lastNameInput.text.trim()
        if (lastName.isBlank()) {
            binding.lastNameInput.error = getString(R.string.last_name_is_blank)
            return
        }

        model.saveUser(firstName, lastName)
    }
}

Vous pouvez trouver l'exemple complet ici .

0
Valeriy Katkov