数据绑定和:输入字段验证和操作; Activity/Fragment 导航
Data Binding and: Input field validation and manipulation; Activity/Fragment Navigation
我目前正在学习数据绑定以及随之而来的所有新事物。目前我正在为如何正确实施事情而苦苦挣扎,所以寻求一些帮助。
在我申请的这个特定部分,我们将讨论我遇到最多问题的地方:SignIn/SignUp 表格;
以前,我的代码会很简单:
- 我在我的 ViewModel 和存储库中做事
- 正在 Fragment 或 Activity
中观察 LiveData
- 根据 LiveData 状态,UI 发生变化。
每个人都很开心,同时保持简单。
此刻,我正在尝试转移到数据绑定,虽然我已经设法了解了很多,但有些事情我不太确定。
当前代码和具体问题如下:
SignInViewModel:
class SignInViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
val signInForm = SignInForm()
private val _signInState = MutableLiveData<SignInState>()
val signInState: LiveData<SignInState>
get() = _signInState
fun userSignIn() {
_signInState.value = SignInState.Loading
Firebase.auth.signInWithEmailAndPassword(signInForm.email!!.value!!, signInForm.password!!.value!!)
.addOnCompleteListener {
_signInState.value =
if (it.isSuccessful)
SignInState.SignedIn
else
SignInState.Error(it.exception!!.localizedMessage!!)
}
}
// E-Mail
val emailValidationResponse = MediatorLiveData<String?>().apply {
addSource(signInForm.email as LiveData<String>) {
value = emailValidation()
}
}
val passwordValidationResponse = MediatorLiveData<String?>().apply {
addSource(signInForm.password as LiveData<String>) {
value = passwordValidation()
}
}
private fun emailValidation(): String? {
return when {
signInForm.email?.value.isNullOrEmpty() -> {
context.getString(R.string.error_message_field_is_empty)
}
!Patterns.EMAIL_ADDRESS.matcher(signInForm.email?.value!!).matches() -> {
context.getString(R.string.error_message_invalid_email)
}
else -> null
}
}
private fun passwordValidation(): String? {
return when {
signInForm.password?.value.isNullOrEmpty() -> {
context.getString(R.string.error_message_field_is_empty)
}
signInForm.password?.value!!.length < 8 -> {
context.getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
}
else -> null
}
}
SignInForm.kt
data class SignInForm(
override val email: MutableLiveData<String>? = MutableLiveData(),
val password: MutableLiveData<String>? = MutableLiveData()
) : Form()
SignInState.kt
sealed class SignInState {
object Loading : SignInState()
object SignedIn : SignInState()
data class Error(val errorMessage: String) : SignInState()
}
SignInFragment
class SignInFragment : Fragment() {
private val signInViewModel: SignInViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentSignInBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = signInViewModel
signInViewModel.signInState.observe(viewLifecycleOwner, { state ->
when (state) {
is SignInState.SignedIn -> {
proceedToProfileScreen(requireActivity())
}
}
})
binding.signInInputEditTextEmail.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
binding.signInInputEditTextPassword.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
binding.signInButtonGoToSignUp.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
}
binding.signInButtonGoToForgotPassword.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_forgotPasswordFragment)
}
return binding.root
}
}
fragment_sign_in.xml
<layout> ....
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_email"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
setError="@{viewModel.emailValidationResponse}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_email"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_in_input_edit_text_email"
android:layout_width="match_parent"
android:layout_height="55dp"
android:inputType="text"
android:text="@={viewModel.signInForm.email}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
setError="@{viewModel.passwordValidationResponse}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_in_input_edit_text_password"
android:layout_width="match_parent"
android:layout_height="55dp"
android:inputType="textPassword"
android:text="@={viewModel.signInForm.password}" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_submit"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="@{() -> viewModel.userSignIn()}"
android:text="@string/sign_in_button_submit"
android:textColor="@android:color/black" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_go_to_sign_up"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sign_in_button_go_to_sign_up"
android:textColor="@android:color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_go_to_forgot_password"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sign_in_button_go_to_forgot_password"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
... </layout>
问题:
- 这是使用数据绑定与 UI 一起工作的正确方法吗?
- 有没有更好的方法使用导航组件在具有数据绑定的片段之间导航
- enable/disable 基于电子邮件和密码输入的登录按钮有什么好的解决方案?自定义绑定适配器或 ViewModel 中的其他变量?
这是我最终使用的:
[1] 输入表单验证 + [3] Enabling/Disabling 按钮。
SignInForm.kt
data class SignInForm(
val email: MutableLiveData<String> = MutableLiveData(),
val password: MutableLiveData<String> = MutableLiveData()
) {
val emailError = MediatorLiveData<String?>().apply {
value = ""
addSource(email) {
value = validateEmail(it)
}
}
val passwordError = MediatorLiveData<String?>().apply {
value = ""
addSource(password) {
value = validatePassword(it)
}
}
}
InputValidators.kt
fun validateEmail(email: String?): String? {
return when {
email.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() -> TheContext.applicationContext().getString(R.string.error_message_invalid_email)
else -> null
}
}
fun validatePassword(password: String?): String? {
return when {
password.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
password.length < 8 -> TheContext.applicationContext().getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
else -> null
}
}
fragment_sign_in.xml
<layout> ...
<data>
<variable
name="viewModel"
type="my.test.movieexpert.loginscreen.viewmodel.SignInViewModel" />
</data>
... <com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_email"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
inputValidation="@{viewModel.signInForm.emailError}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_email"
app:errorEnabled="true"> ...
... <com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
inputValidation="@{viewModel.signInForm.passwordError}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText=""> ...
... <androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_submit"
emailError="@{viewModel.signInForm.emailError}"
passwordError="@{viewModel.signInForm.passwordError}"
setButtonState="@{viewModel.signInState}"
android:layout_width="match_parent"
android:layout_height="55dp"
android:background="@color/colorPrimaryDark"
android:onClick="@{() -> viewModel.userSignIn()}"
android:text="@string/sign_in_button_submit"
android:textColor="@android:color/white"
android:textSize="17sp" /> ...
... </layout>
[2] 导航保持不变:
SignInFragment
binding.signInButtonGoToSignUp.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
}
我目前正在学习数据绑定以及随之而来的所有新事物。目前我正在为如何正确实施事情而苦苦挣扎,所以寻求一些帮助。
在我申请的这个特定部分,我们将讨论我遇到最多问题的地方:SignIn/SignUp 表格;
以前,我的代码会很简单:
- 我在我的 ViewModel 和存储库中做事
- 正在 Fragment 或 Activity 中观察 LiveData
- 根据 LiveData 状态,UI 发生变化。
每个人都很开心,同时保持简单。
此刻,我正在尝试转移到数据绑定,虽然我已经设法了解了很多,但有些事情我不太确定。
当前代码和具体问题如下:
SignInViewModel:
class SignInViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
val signInForm = SignInForm()
private val _signInState = MutableLiveData<SignInState>()
val signInState: LiveData<SignInState>
get() = _signInState
fun userSignIn() {
_signInState.value = SignInState.Loading
Firebase.auth.signInWithEmailAndPassword(signInForm.email!!.value!!, signInForm.password!!.value!!)
.addOnCompleteListener {
_signInState.value =
if (it.isSuccessful)
SignInState.SignedIn
else
SignInState.Error(it.exception!!.localizedMessage!!)
}
}
// E-Mail
val emailValidationResponse = MediatorLiveData<String?>().apply {
addSource(signInForm.email as LiveData<String>) {
value = emailValidation()
}
}
val passwordValidationResponse = MediatorLiveData<String?>().apply {
addSource(signInForm.password as LiveData<String>) {
value = passwordValidation()
}
}
private fun emailValidation(): String? {
return when {
signInForm.email?.value.isNullOrEmpty() -> {
context.getString(R.string.error_message_field_is_empty)
}
!Patterns.EMAIL_ADDRESS.matcher(signInForm.email?.value!!).matches() -> {
context.getString(R.string.error_message_invalid_email)
}
else -> null
}
}
private fun passwordValidation(): String? {
return when {
signInForm.password?.value.isNullOrEmpty() -> {
context.getString(R.string.error_message_field_is_empty)
}
signInForm.password?.value!!.length < 8 -> {
context.getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
}
else -> null
}
}
SignInForm.kt
data class SignInForm(
override val email: MutableLiveData<String>? = MutableLiveData(),
val password: MutableLiveData<String>? = MutableLiveData()
) : Form()
SignInState.kt
sealed class SignInState {
object Loading : SignInState()
object SignedIn : SignInState()
data class Error(val errorMessage: String) : SignInState()
}
SignInFragment
class SignInFragment : Fragment() {
private val signInViewModel: SignInViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentSignInBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = signInViewModel
signInViewModel.signInState.observe(viewLifecycleOwner, { state ->
when (state) {
is SignInState.SignedIn -> {
proceedToProfileScreen(requireActivity())
}
}
})
binding.signInInputEditTextEmail.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
binding.signInInputEditTextPassword.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hideKeyboard() }
binding.signInButtonGoToSignUp.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
}
binding.signInButtonGoToForgotPassword.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_forgotPasswordFragment)
}
return binding.root
}
}
fragment_sign_in.xml
<layout> ....
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_email"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
setError="@{viewModel.emailValidationResponse}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_email"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_in_input_edit_text_email"
android:layout_width="match_parent"
android:layout_height="55dp"
android:inputType="text"
android:text="@={viewModel.signInForm.email}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
setError="@{viewModel.passwordValidationResponse}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_in_input_edit_text_password"
android:layout_width="match_parent"
android:layout_height="55dp"
android:inputType="textPassword"
android:text="@={viewModel.signInForm.password}" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_submit"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="@{() -> viewModel.userSignIn()}"
android:text="@string/sign_in_button_submit"
android:textColor="@android:color/black" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_go_to_sign_up"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sign_in_button_go_to_sign_up"
android:textColor="@android:color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_go_to_forgot_password"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sign_in_button_go_to_forgot_password"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
... </layout>
问题:
- 这是使用数据绑定与 UI 一起工作的正确方法吗?
- 有没有更好的方法使用导航组件在具有数据绑定的片段之间导航
- enable/disable 基于电子邮件和密码输入的登录按钮有什么好的解决方案?自定义绑定适配器或 ViewModel 中的其他变量?
这是我最终使用的:
[1] 输入表单验证 + [3] Enabling/Disabling 按钮。
SignInForm.kt
data class SignInForm(
val email: MutableLiveData<String> = MutableLiveData(),
val password: MutableLiveData<String> = MutableLiveData()
) {
val emailError = MediatorLiveData<String?>().apply {
value = ""
addSource(email) {
value = validateEmail(it)
}
}
val passwordError = MediatorLiveData<String?>().apply {
value = ""
addSource(password) {
value = validatePassword(it)
}
}
}
InputValidators.kt
fun validateEmail(email: String?): String? {
return when {
email.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() -> TheContext.applicationContext().getString(R.string.error_message_invalid_email)
else -> null
}
}
fun validatePassword(password: String?): String? {
return when {
password.isNullOrEmpty() -> TheContext.applicationContext().getString(R.string.error_message_field_is_empty)
password.length < 8 -> TheContext.applicationContext().getString(R.string.error_message_password_is_too_short, USER_PASSWORD_MIN_CHARACTERS)
else -> null
}
}
fragment_sign_in.xml
<layout> ...
<data>
<variable
name="viewModel"
type="my.test.movieexpert.loginscreen.viewmodel.SignInViewModel" />
</data>
... <com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_email"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
inputValidation="@{viewModel.signInForm.emailError}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_email"
app:errorEnabled="true"> ...
... <com.google.android.material.textfield.TextInputLayout
android:id="@+id/sign_in_input_layout_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
inputValidation="@{viewModel.signInForm.passwordError}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/input_hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText=""> ...
... <androidx.appcompat.widget.AppCompatButton
android:id="@+id/sign_in_button_submit"
emailError="@{viewModel.signInForm.emailError}"
passwordError="@{viewModel.signInForm.passwordError}"
setButtonState="@{viewModel.signInState}"
android:layout_width="match_parent"
android:layout_height="55dp"
android:background="@color/colorPrimaryDark"
android:onClick="@{() -> viewModel.userSignIn()}"
android:text="@string/sign_in_button_submit"
android:textColor="@android:color/white"
android:textSize="17sp" /> ...
... </layout>
[2] 导航保持不变:
SignInFragment
binding.signInButtonGoToSignUp.setOnClickListener {
this.findNavController().navigate(R.id.action_signInFragment_to_signUpFragment)
}