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>
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
.
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);
}
}
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
.
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
}
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
}
}
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.
Cela fonctionnera
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
Je suggérerais, si vous souhaitez creuser plus profondément, vous pouvez utiliser des adaptateurs de liaison personnalisés. De cette façon, vous:
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
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}" />
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.
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) { ... }
}
...
<EditText Android:id="@+id/firstNameInput"
Android:text="@{model.user.firstName}"
... />
<EditText Android:id="@+id/lastNameInput"
Android:text="@{model.user.lastName}"
... />
<Button Android:onClick="onSaveButtonClick" ... />
...
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 .