ViewModelとDatabindingを使用してフォームデータを検証する最良の方法は何ですか?
バインディングレイアウトと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
およびsignUp()
メソッド。サーバーにデータを送信する前にデータを検証する必要があります。
_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!!)
}
}
_
各入力に4つのブール値ObservableFieldsを追加でき、signUp()
で入力をチェックし、レイアウトに表示エラーを生成するブール値ObservableFieldの状態を変更できると思います
_val isNameError: ObservableField<Boolean> = ObservableField()
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
_
しかし、ViewModel
が検証とユーザーへのエラーの表示を担当するかどうかはわかりません。定型コードがあります。
_<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>
_
これを実装するには多くの方法があります。私はあなたに2つの解決策を伝えていますが、どちらもうまくいき、あなたがあなたに適していると思うものを使うことができます。
私が使う extends BaseObservable
すべてのフィールドをObservers
に変換するよりも簡単だと思うからです。 ObservableFields
も使用できます。
BindingAdapter
を使用)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
を使用)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);
}
}
更新
交換することもできます
Android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}"
と
Android:afterTextChanged="@{handler::passwordValidator}"
パラメーターはAndroid:afterTextChanged
およびpasswordValidator
。
はい、ViewModel
から検証ロジックを使用できます。これは、ViewModel
&xmlもViewModel
クラスからデータを派生しています。
だから、解決策:
ViewModelで@BindingAdapter
を作成し、それをクリックしてボタンをバインドできます。そこで検証を確認し、他の作業も行います。
Listener
を作成し、それをViewModel
に実装して受け取ることができますボタンをクリックして、そのリスナーをxml
にバインドします。
双方向データバインディング を使用することもできます(ただし、無限ループに注意してください)。
//Let's say it's your binding adapter from ViewModel
fun signUp() {
if (check validation logic) {
// Produce errors
}
// Further successful stuffs
}
このアプローチでは、カスタムバインディングアダプターであるTextInputLayoutsを使用し、フォームエラーの列挙型を作成します。私が思う結果はxmlをうまく読み、すべての検証ロジックをViewModel内に保持します。
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,
}
}
BindingAdapter:
@BindingAdapter("app:errorText")
fun setErrorMessage(view: TextInputLayout, errorMessage: String) {
view.error = errorMessage
}
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 -->
そして、次に示すように、アクティビティ/フラグメントからViewModelを呼び出すことができます。
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
}
}
あなたが念頭に置いていることは、実際には正しいです。ビューモデルはAndroidシステムについて何も認識してはならず、純粋なJava/kotlinでのみ動作します。したがって、あなたが考えていることをするのは正しいことです。すべてのビューインタラクションはビューで処理される必要があるため、ViewModelはAndroidシステムについて認識してはなりません。ただし、それらのプロパティはビューにバインドできます。
これは動作します
fun signUp() {
if (name.value == null || name.value!!.length < 2 ) {
isNameError.set(true)
}
auth.createUser(email.value!!, password.value!!)
}
より深く掘り下げたい場合は、カスタムバインディングアダプターを使用することをお勧めします。この方法で:
カスタムバインディングアダプターに検証のみを持たせる方法について想像してみましょう。今のところ、カスタムバインディングアダプターの基本を理解することをお勧めします。
乾杯
Observableオブジェクトのバインド可能フィールドを検証するための library を作成しました。
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)
}
}
ルールをインスタンス化して追加する
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")
}
}
そして、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}" />
Androidアーキテクチャコンポーネントに関するフォーム検証の実装に関するいくつかの投稿を見つけました。ほとんどのソリューションは次のとおりです。「編集しているすべてのフィールドのモデルにプロパティを追加し、結果として、かなり定型的なコードを取得しますが、何のためですか?ViewModelライブラリは、複雑にするのではなく、ライブを簡単にすることを目的としています。
検証にエラー、トーストなどを表示するためにビューへのアクセスが必要な場合、なぜビューにロジックを残さないのですか? ViewModel
はデータをロードし、ビューに提供します。ビューはデータを検証し、ViewModel
に返します。ビューは、デフォルトでEditText
などのコンポーネントの状態を保存します。コンポーネントにはIDが必要です。保存された状態の方が優先度が高いため、コンポーネントがバインドされているにもかかわらず、アクティビティ/フラグメントの再作成で保存された状態を失うことはありません。
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)
}
}
完全な例 here があります。