Реализация экранов авторизации и регистрации с помощью Custom View и Firebase
Каждый из нас сталкивается с авторизацией и регистрацией в приложениях как пользователь и как разработчик. Но перед разработчиком стоит более важная задача, а именно реализовать 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,}\quot; } }
Данный класс мало отличается от предыдущего, за исключением позиций
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 в макете, позволяя получить введенный пароль во внешних частях кода.
Теперь можно наверстать разметку для всех экранов.
<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>
<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.
@HiltAndroidApp class App : Application()
@Component interface AppComponent { }
@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. Более подробно ознакомиться с кодом можно тут.