Привет, читатели Хабр!
Каждый из нас сталкивается с авторизацией и регистрацией в приложениях как пользователь и как разработчик. Но перед разработчиком стоит более важная задача, а именно реализовать View таким образом, чтобы данные, которые введет пользователь, были корректно обработаны и переданы на сервер, что если пользователь введет вместо своего email просто набор символов, или напишет пароль из одной цифры? В нормальных приложениях это недопустимо! В этой статье я хочу продемонстрировать демо приложение, где будет представлен способ обработки данных полей с использованием Custom View и авторизацией в firebase.
Структура приложения
Данное демо приложение содержит активити и три фрагмента. Первый фрагмент – экран авторизации, второй фрагмент – экран регистрации и еще один фрагмент на который попадает пользователь, если он успешно прошел валидацию на одном из предыдущих фрагментов. Аутентификация, как я писал ранее, происходит в firebase.
Реализация приложения
Поскольку Custom View будет расширять функционал полей для ввода текста, чтобы не писать один и тот же код несколько раз, можно сделать класс заготовку, в котором будет описана основная логика и от которого будут наследоваться последующие классы, таким классом будет CustomInputLayout.
abstract class CustomInputLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int = 0
) : TextInputLayout(context, attrs, defStyleAttr), Validation {
protected abstract val errorMessageId: Int
private val textWatcher = RegistrationTextWatcher { error = "" }
open fun text() = editText?.text.toString()
override fun onAttachedToWindow() {
super.onAttachedToWindow()
editText?.addTextChangedListener(textWatcher)
}
override fun isValid(): Boolean {
val isValid = innerIsValid()
error = if (isValid) "" else context.getString(errorMessageId)
return isValid
}
protected abstract fun innerIsValid(): Boolean
}
interface Validation {
fun isValid(): Boolean
}
В данном коде определен абстрактный класс CustomInputLayout, который является наследником класса TextInputLayout из библиотеки Android Material Design. CustomInputLayout также реализует интерфейс Validation.
Конструктор класса принимает параметры context, attrs и defStyleAttr, где context представляет контекст приложения, attrs содержит атрибуты, указанные в разметке, а defStyleAttr - стиль, который будет применен к макету.
В классе определены следующие члены:
errorMessageId - абстрактное свойство, обозначающее идентификатор строки ресурса сообщения об ошибке.
textWatcher - экземпляр класса RegistrationTextWatcher для отслеживания изменений текста в поле ввода.
text() - возвращает строку текста из поля ввода.
onAttachedToWindow() - переопределенный метод, вызывающий родительскую реализацию и добавляющий textWatcher к полю ввода.
isValid() - переопределенный метод интерфейса Validation, возвращающий true, если введенное значение считается допустимым, и false - в противном случае.
innerIsValid() - абстрактный метод, определяющий валидацию значения в классах-наследниках.
Так же нужно создать класс RegistrationTextWatcher, экземпляр которого был создан в CustomInputLayout.
class RegistrationTextWatcher(private val onTextChanged: () -> Unit) : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChanged.invoke()
}
override fun afterTextChanged(s: Editable?) {
}
}
Класс RegistrationTextWatcher принимает в конструкторе функцию onTextChanged, которая будет вызываться при изменении текста. Это высокоуровневая функция, которая не принимает аргументов и ничего не возвращает (Unit).
Далее класс переопределяет три метода интерфейса TextWatcher:
beforeTextChanged - этот метод вызывается перед изменением текста. В данном коде метод не содержит какой-либо реализации и оставлен пустым.
onTextChanged - этот метод вызывается во время изменения текста. В данной реализации метода вызывается функция onTextChanged.invoke(), что приводит к выполнению функции onTextChanged, переданной в конструкторе класса. Таким образом, при изменении текста будет вызываться переданная функция.
afterTextChanged - этот метод вызывается после изменения текста.
Таким образом, класс RegistrationTextWatcher позволяет отслеживать изменения текста в поле и вызывать заданную функцию onTextChanged, когда происходят эти изменения.
После того как был создан класс, от которого будут наследоваться классы для обработки полей Email и Password, можно приступать к их реализации.
class MailInput @JvmOverloads constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int = 0
) : CustomInputLayout(context, attrs, defStyleAttr) {
override val errorMessageId = R.string.login_error
override fun innerIsValid(): Boolean {
return Patterns.EMAIL_ADDRESS.matcher(text()).matches()
}
}
В данном коде определен класс MailInput, который наследуется от созданного ранее класса CustomInputLayout. Этот класс будет обрабатывать поле для электронной почты.
Конструктор класса MailInput принимает следующие параметры:
context: Context - контекст приложения.
attrs: AttributeSet - набор атрибутов, определенных в XML-разметке для этого пользовательского элемента ввода.
defStyleAttr: Int - атрибут стиля по умолчанию.
@JvmOverloads - это аннотация, используемая для генерации перегруженных конструкторов с параметрами по умолчанию, которые могут быть использованы из Java-кода.
Класс MailInput переопределяет два метода:
innerIsValid() - этот метод проверяет, является ли введенный текст в поле электронной почты валидным.
В данной реализации используется паттерн который доступен в Java Patterns.EMAIL_ADDRESS.matcher(text()).matches(), чтобы проверить, соответствует ли введенный текст стандартному формату электронной почты. Если текст соответствует формату, то метод возвращает true, в противном случае - false.
errorMessageId - устанавливает идентификатор R.string.login_error. Это идентификатор используется для отображения сообщения об ошибке, которое будет показано пользователю, если введенный текст не является валидным адресом электронной почты.
class PasswordInput @JvmOverloads constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int = 0
) : CustomInputLayout(context, attrs, defStyleAttr) {
override val errorMessageId: Int = R.string.password_error
override fun innerIsValid(): Boolean {
return text().matches(Regex(PASSWORD_PATTERN))
}
companion object {
private const val PASSWORD_PATTERN =
"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@\$%^&*-]).{8,}\$"
}
}
Данный класс мало отличается от предыдущего, за исключением позиций
errorMessageId – в котором переопределяется и устанавливается идентификатор строки ресурса R.string.password_error.
А также PASSWORD_PATTERN, поскольку он не доступен как EMAIL_ADDRESS, был использован сторонний паттерн.
Данное регулярное выражение будет соответствовать строкам, которые:
Содержат хотя бы одну заглавную букву [A-Z].
Содержат хотя бы одну строчную букву [a-z].
Содержат хотя бы одну цифру [0-9].
Содержат хотя бы один специальный символ [#?!@\$%^&*-].
Имеют длину не менее 8 символов .{8,}.
В фрагменте с регистрацией, в отличии от фрагмента с авторизацией, кроме поля с email будут два поля password где будет проходить сравнение паролей.
class PasswordLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), Validation {
private val binding = PasswordLayoutBinding.inflate(LayoutInflater.from(context), this)
init {
orientation = VERTICAL
listOf(binding.passwordEditText, binding.passwordRepeatEditText).forEach {
it.addTextChangedListener(RegistrationTextWatcher {
binding.errorText.text = ""
})
}
}
private val errorMessageId: Int = R.string.password_error_same
override fun isValid(): Boolean {
with(binding) {
val isPasswordsEquals = passwordLayout.text() == passwordRepeatLayout.text()
errorText.text = if (isPasswordsEquals) "" else context.getString(errorMessageId)
val isPasswordsValid = listOf(passwordLayout, passwordRepeatLayout).map { it.isValid() }
return isPasswordsValid.all { it } && isPasswordsEquals
}
}
fun text(): String {
return binding.passwordLayout.text()
}
}
Класс PasswordLayout определяет переменную binding, которая инфлейтит макет PasswordLayoutBinding с использованием LayoutInflater и привязывается к текущему экземпляру LinearLayout.
Затем в блоке init устанавливается вертикальная ориентация для макета, и для каждого из полей ввода пароля (passwordEditText и passwordRepeatEditText) добавляется TextChangedListener, реализованный как экземпляр RegistrationTextWatcher. Этот слушатель отслеживает изменения в полях ввода и обновляет текст ошибки (errorText) в зависимости от того, совпадают ли значения паролей.
Переменная errorMessageId хранит R.string.password_error_same, которая используется для отображения текста ошибки, если значения полей ввода пароля не совпадают.
Метод isValid() реализует интерфейс Validation и выполняет проверки валидности пароля и сравнивает значения полей ввода пароля. В данной реализации метод сравнивает значения passwordLayout и passwordRepeatLayout в макете и устанавливает текст ошибки errorText в зависимости от результата сравнения. Затем метод проводит валидацию каждого из полей ввода пароля и возвращает значение true, если все поля валидны и значения паролей совпадают, и false в противном случае.
Метод text() возвращает текст из поля ввода пароля passwordLayout в макете, позволяя получить введенный пароль во внешних частях кода.
Теперь можно наверстать разметку для всех экранов.
Разметка MainActivity.
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".core.ui.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
Разметка AuthorizationFragment.
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.ui.authorization.AuthorizationFragment">
<ru.anb.passwordapp.features.ui.input.PasswordInput
android:id="@+id/auth_password"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="32dp"
android:hint="@string/password"
app:endIconDrawable="@drawable/eye"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/auth_mail">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</ru.anb.passwordapp.features.ui.input.PasswordInput>
<ru.anb.passwordapp.features.ui.input.MailInput
android:id="@+id/auth_mail"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="64dp"
android:hint="@string/email"
app:endIconDrawable="@drawable/clear"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/mail_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</ru.anb.passwordapp.features.ui.input.MailInput>
<Button
android:id="@+id/sign_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/sign_in"
app:layout_constraintEnd_toStartOf="@+id/navigate_to_sign_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/auth_password" />
<Button
android:id="@+id/navigate_to_sign_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/sign_up"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/sign_in"
app:layout_constraintTop_toBottomOf="@+id/auth_password" />
<FrameLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Разметка PasswordLayout.
<merge 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout">
<ru.anb.passwordapp.features.ui.input.PasswordInput
android:id="@+id/password_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:hint="@string/password"
app:endIconDrawable="@drawable/eye"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</ru.anb.passwordapp.features.ui.input.PasswordInput>
<ru.anb.passwordapp.features.ui.input.PasswordInput
android:id="@+id/password_repeat_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:hint="@string/check_password"
app:endIconDrawable="@drawable/eye"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_repeat_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</ru.anb.passwordapp.features.ui.input.PasswordInput>
<TextView
android:id="@+id/error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/red" />
</merge>
Разметка RegistrationFragment.
<FrameLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".features.auth.ui.RegisterFragment">
<ru.anb.passwordapp.features.ui.input.MailInput
android:id="@+id/sign_up_email"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="64dp"
android:hint="@string/email"
android:inputType="text"
app:endIconDrawable="@drawable/clear"
app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/register_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</ru.anb.passwordapp.features.ui.input.MailInput>
<ru.anb.passwordapp.features.ui.input.PasswordLayout
android:id="@+id/sign_up_password_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/start_sign_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/sign_up" />
</LinearLayout>
<FrameLayout
android:id="@+id/progress_bar_registration"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>
Как можно заметить, вместо обычного TextInputLayout используются созданные Custom View. Так же, в разметке, стоит обратить внимание на такие строки как app:endIconMode="password_toggle" которые позволяют скрывать и показывать что вводит пользователь в полях password, а так же строки app:endIconMode="clear_text" которые позволяют стереть то, что было ранее написано в поле email.
Навигация между фрагментами будет осуществляться с помощью Navigation Components. Так же, в приложении, для внедрения зависимостей будет использован Hilt.
Для этого понадобятся:
класс App (нужно указать его в манифесте)
@HiltAndroidApp
class App : Application()
интерфейс AppComponent
@Component
interface AppComponent {
}
класс AuthModule - в этом модуле подключается firebase.
@InstallIn(SingletonComponent::class)
@Module
class AuthModule {
@Provides
@Singleton
fun provideFirebaseAuth(): FirebaseAuth {
return Firebase.auth
}
}
абстрактный класс Module, с абстрактным методом bindAuthRepository(), в котором осуществляется привязка реализации AuthRepositoryImpl с интерфейсом AuthRepository (их реализация будет написана ниже).
@InstallIn(SingletonComponent::class)
@Module
abstract class Module {
@Binds
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
}
Что касается самого firebase, опущу описание его подключения, в самом firebase все очень подробно описано, добавлю лишь то что для работы с аутентификацией понадобится зависимость implementation "com.google.firebase:firebase-auth-ktx" а также в самом firebase нужно указать, что аутентификация, будет проходить с помощью email и password.
Далее, нужен abstract class User.
abstract class User {
abstract val email: String
abstract val id: String
class Base(override val email: String, override val id: String) : User()
object Empty : User() {
override val email = "Empty"
override val id = "Empty_id"
}
}
Данный код позволяет создавать объекты User с разными реализациями и значениями email и id, включая "базовые" пользователи Base и "пустого" пользователя Empty.
Теперь понадобится интерфейс AuthRepositiry, в нем описаны методы для авторизации и регистрации.
interface AuthRepository {
suspend fun signInWithEmailAndPassword(email: String, password: String): AuthResult
suspend fun signUpWithEmailAndPassword(email: String, password: String): AuthResult
}
Также нужен класс AuthResult, в нем будут описаны различные результаты обработки ответов от firebase.
sealed class AuthResult {
class Success(val user: User) : AuthResult()
class Error(val e: Exception) : AuthResult()
object Loading : AuthResult()
}
После этого, нужно создать класс AuthRepositiryImpl, которому будет имплементирован интерфейс AuthRepositiry, где будут реализованы его методы.
class AuthRepositoryImpl @Inject constructor(private val auth: FirebaseAuth) : AuthRepository {
override suspend fun signInWithEmailAndPassword(email: String, password: String): AuthResult {
return try {
val user = auth.signInWithEmailAndPassword(email, password).await().user!!
AuthResult.Success(User.Base(user.email ?: " ", user.uid))
} catch (e: Exception) {
AuthResult.Error(e)
}
}
override suspend fun signUpWithEmailAndPassword(email: String, password: String): AuthResult {
return try {
val user = auth.createUserWithEmailAndPassword(email, password).await().user!!
AuthResult.Success(User.Base(user.email ?: " ", user.uid))
} catch (e: Exception) {
AuthResult.Error(e)
}
}
}
В конструкторе класса AuthRepositoryImpl используется внедрение зависимостей с помощью аннотации @Inject и параметра auth типа FirebaseAuth. Это позволяет получить экземпляр FirebaseAuth из Hilt и использовать его внутри класса.
signInWithEmailAndPassword - функция, которая выполняет аутентификацию пользователя с использованием электронной почты и пароля. Она вызывает метод signInWithEmailAndPassword из FirebaseAuth для выполнения фактической операции аутентификации. Если операция завершилась успешно, создается объект User.Base с электронной почтой и идентификатором пользователя, и возвращается AuthResult.Success. Если произошла ошибка, возвращается AuthResult.Error
signUpWithEmailAndPassword – функция устроена аналогично.
Для того чтобы не писать один и тот же код по несколько раз, можно создать класс BaseFragment и класс BaseViewModel , в которых будет описана общая функциональность.
abstract class BaseFragment<B : ViewBinding> : Fragment() {
protected abstract val bindingInflater: (LayoutInflater, ViewGroup?) -> B
private var _binding: B? = null
protected val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = bindingInflater.invoke(inflater, container)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
BaseFragment - абстрактный класс, параметризованный типом B, который наследуется от класса Fragment.
bindingInflater - абстрактное свойство, которое должно быть переопределено в подклассах BaseFragment. Оно представляет лямбда-выражение, принимающее LayoutInflater и ViewGroup?, и возвращающее B (тип, унаследованный от ViewBinding).
_binding - приватное свойство, представляющее привязку к представлению (binding) фрагмента. Оно инициализируется значением null при создании фрагмента.
binding - защищенное свойство, которое обеспечивает доступ к экземпляру привязки к представлению (binding).
onCreateView - переопределенный метод из класса Fragment, который вызывается при создании представления фрагмента.
onDestroyView - переопределенный метод из класса Fragment, который вызывается при уничтожении фрагмента. В этом методе привязка к представлению (_binding) устанавливается в null для освобождения ресурсов.
abstract class BaseViewModel : ViewModel() {
abstract val sendRequest: suspend (String, String) -> AuthResult
private val _authState = MutableLiveData<AuthResult>()
val authState: LiveData<AuthResult> get() = _authState
fun sendCredentials(email: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
_authState.postValue(AuthResult.Loading)
val result = sendRequest.invoke(email, password)
_authState.postValue(result)
}
}
}
BaseViewModel - абстрактный класс, наследующийся от ViewModel. Он предоставляет базовую функциональность для управления состоянием аутентификации (или других операций) в архитектуре MVVM.
sendRequest - абстрактная функция, которая должна быть переопределена в подклассах BaseViewModel. В данном случае, эта функция принимает две строки (String) - адрес электронной почты и пароль, и должна возвращать объект типа AuthResult. Это позволяет использовать подклассам BaseViewModel собственную логику для отправки запросов на сервер аутентификации.
_authState - приватное свойство типа MutableLiveData<AuthResult>, которое представляет внутреннее состояние аутентификации. Оно инициализируется экземпляром MutableLiveData, использующим тип AuthResult.
authState - открытое свойство типа LiveData<AuthResult>, которое предоставляет доступ к текущему состоянию аутентификации через authState.value. Отслеживание этого свойства позволяет обновлять пользовательский интерфейс в соответствии с изменениями состояния аутентификации.
sendCredentials - функция, которая вызывается для отправки данных на сервер (firebase). Она запускается в viewModelScope с исполнителем Dispatchers.IO, чтобы выполняться в фоновом потоке. Внутри функции, сначала устанавливается состояние AuthResult.Loading, затем вызывается функция sendRequest с передачей адреса электронной почты и пароля, и результат устанавливается в _authState.
Теперь можно создать фрагменты и вью модели для всех View.
@HiltViewModel
class AuthorizationViewModel @Inject constructor(private val authRepository: AuthRepository) :
BaseViewModel() {
override val sendRequest: suspend (String, String) -> AuthResult =
{ email, password -> authRepository.signInWithEmailAndPassword(email, password) }
}
AuthorizationViewModel - класс, представляющий ViewModel. Он наследуется от BaseViewModel.
@HiltViewModel - аннотация, которая обозначает, что этот класс является ViewModel, и его зависимости должны быть внедрены с помощью Hilt.
authRepository - зависимость типа AuthRepository, которая внедряется в конструктор AuthorizationViewModel с использованием аннотации @Inject, через механизм внедрения зависимостей Hilt.
sendRequest - переопределенное свойство из BaseViewModel, которое представляет функцию (String, String) -> AuthResult. В данном случае, оно устанавливается как лямбда-выражение, где email и password передаются в authRepository.signInWithEmailAndPassword для выполнения операции аутентификации. authRepository.signInWithEmailAndPassword возвращает объект AuthResult.
@HiltViewModel
class RegistrationViewModel @Inject constructor(private val authRepository: AuthRepository) :
BaseViewModel() {
override val sendRequest: suspend (String, String) -> AuthResult =
{ email, password -> authRepository.signUpWithEmailAndPassword(email, password) }
}
RegistrationViewModelустроена аналогично с AuthorizationViewModel, за исключением того что в sendRequest email и password передаются в signUpWithEmailAndPassword.
@AndroidEntryPoint
class AuthorizationFragment : BaseFragment<FragmentAuthorizationBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?) -> FragmentAuthorizationBinding =
{ inflater, container ->
FragmentAuthorizationBinding.inflate(inflater, container, false)
}
private val viewModel: AuthorizationViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val inputList = listOf(
binding.authMail,
binding.authPassword
)
viewModel.authState.observe(viewLifecycleOwner) {
when (it) {
AuthResult.Loading -> binding.progressBar.visibility = View.VISIBLE
is AuthResult.Error -> {
binding.progressBar.visibility = View.GONE
Toast.makeText(requireContext(), it.e.message.toString(), Toast.LENGTH_LONG)
.show()
}
is AuthResult.Success -> {
findNavController().navigate(R.id.action_authorizationFragment_to_homeFragment)
}
}
}
binding.signIn.setOnClickListener {
val allValidation = inputList.map { it.isValid() }
if (allValidation.all { it }) {
viewModel.sendCredentials(
email = binding.authMail.text(),
password = binding.authPassword.text()
)
}
}
binding.navigateToSignUp.setOnClickListener {
findNavController().navigate(R.id.action_authorizationFragment_to_registrationFragment)
}
}
}
AuthorizationFragment - класс фрагмента, наследуется от BaseFragment<FragmentAuthorizationBinding>.
@AndroidEntryPoint - аннотация, которая обозначает, что этот класс является Android-компонентом, который должен быть внедрен с помощью Hilt.
bindingInflater - переопределенное свойство из BaseFragment, которое представляет лямбда-выражение для создания привязки к представлению (binding). В данном случае, оно использует FragmentAuthorizationBinding.inflate для создания привязки FragmentAuthorizationBinding на основе разметки.
viewModel - экземпляр AuthorizationViewModel.
val inputList = listOf(binding.authMail, binding.authPassword) - создается список inputList, который содержит ссылки на представления для ввода адреса электронной почты и пароля.
viewModel.authState.observe(viewLifecycleOwner) { authResult -> ... } - устанавливается наблюдатель (observe) на свойство authState из viewModel. Когда состояние аутентификации меняется, код внутри лямбда-выражения будет выполняться. Внутри лямбда-выражения определена логика для обработки каждого возможного значения authResult:
Если значение authResult является AuthResult.Loading, то прогресс-бар (binding.progressBar) делается видимым.
Если значение authResult является AuthResult.Error, то прогресс-бар скрывается, и отображается уведомление (Toast) с сообщением об ошибке из объекта authResult.e.
Если значение authResult является AuthResult.Success, то происходит переход к другому фрагменту с помощью findNavController().navigate(R.id.action_authorizationFragment_to_homeFragment).
binding.signIn.setOnClickListener { ... } - устанавливается слушатель для кнопки signIn. Когда кнопка нажимается, выполняется код внутри лямбда-выражения. Внутри лямбда-выражения происходит валидация полей ввода (inputList), и если все поля ввода прошли валидацию (allValidation.all { it }), вызывается метод viewModel.sendCredentials, который отправляет учетные данные (email и password) на сервер (firebase) для аутентификации.
binding.navigateToSignUp.setOnClickListener { ... } - устанавливается слушатель для кнопки navigateToSignUp. Когда кнопка нажимается, выполняется код внутри лямбда-выражения. Внутри лямбда-выражения происходит переход к другому фрагменту с помощью findNavController().navigate(R.id.action_authorizationFragment_to_registrationFragment).
@AndroidEntryPoint
class RegistrationFragment : BaseFragment<FragmentRegistrationBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?) -> FragmentRegistrationBinding =
{ inflater, container ->
FragmentRegistrationBinding.inflate(inflater, container, false)
}
private val viewModel: RegistrationViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val inputList = listOf(
binding.signUpEmail,
binding.signUpPasswordLayout
)
viewModel.authState.observe(viewLifecycleOwner) {
when (it) {
AuthResult.Loading -> binding.progressBarRegistration.visibility = View.VISIBLE
is AuthResult.Error -> {
binding.progressBarRegistration.visibility = View.GONE
Toast.makeText(requireContext(), it.e.message.toString(), Toast.LENGTH_LONG)
.show()
}
is AuthResult.Success -> {
findNavController().navigate(R.id.action_registrationFragment_to_homeFragment)
}
}
}
binding.startSignUp.setOnClickListener {
val allValidation = inputList.map { it.isValid() }
if (allValidation.all { it }) {
viewModel.sendCredentials(
email = binding.signUpEmail.text(),
password = binding.signUpPasswordLayout.text()
)
}
}
}
}
RegistrationFragment устроен аналогично, за исключением некоторых деталей в onViewCreated
Теперь можно запустить приложение и посмотреть, что получилось.
Итог
Данное приложение получилось расширяемым, его легко можно интегрировать в свой проект, можно заменить firebase на свой сервер, добавить новые поля для ввода данных и написать свою логику их обработки, на основании уже написанного кода.
Если вы хотите запустить приложение на своем устройстве, то вам понадобится свой google-services.json. Более подробно ознакомиться с кодом можно тут.