Урок 14. Навигация по условию в андроид приложении. Android Conditional Navigation & Firebase Authentication

Продолжаем серию уроков по разработке android-приложений в Android Studio на языке Kotlin. Пришло время познакомиться с Android Conditional Navigation и Firebase Authentication.

На прошлом уроке мы научились работать с библиотекой Navigation Architecture Component, которая позволяет пользователям перемещаться между различными экранами и расположениями контента в андроид-приложении. Мы интегрировали навигацию в проект, добавили пункты назначения и переходы между ними.

На этом уроке

Нам предстоит более сложная и интересная задача. Мы создадим приложение, которое выполняет навигацию по некому условию, в зависимости от которого пользователь будет видеть тот или иной экран приложения. Таким образом, если условие выполняется, то переход будет происходить в один пункт назначения, а если не выполняется – в другой.

В приложении будет три экрана – домашний экран с кнопкой входа в личный кабинет, экран аутентификации и экран личного кабинета. При первом входе в приложение по нажатию кнопки личного кабинета на главном экране пользователь будет попадать на экран аутентификации. Но, если пользователь уже залогинен, то по нажатию этой же самой кнопки он будет попадать на экран личного кабинета, где в приветствии будет видеть свое имя.

Для аутентификации пользователей воспользуемся сервисом Firebase Authentication, который позволяет логиниться по email и паролю, а также поддерживает много других способов аутентификации.

Кроме Firebase, также будем использовать в проекте актуальные библиотеки из набора Android JetPack: Data Binding, View Model, Live Data, которые мы рассматривали на прошлых уроках. Таким образом, мы будем использовать правильный подход для построения архитектуры приложения с точки зрения быстродействия, стабильности и корректной обработки изменений конфигурации.

Создаем проект

Откройте среду разработки Android Studio и создайте новый проект с использованием шаблона Empty Activity.

Откройте среду разработки Android Studio и создайте новый проект с использованием шаблона Empty Activity.

Строковые ресурсы

В папке res/values откройте файл strings.xml и добавьте туда такие строки:

<string name="personal_account_text">Личный кабинет</string>
<string name="login_button_text">Вход</string>
<string name="logout_button_text">Выход</string>
<string name="login_prompt">Выполните вход</string>
<string name="welcome_message">Привет, %1$s!</string>
<string name="login_unsuccessful_msg">Вход не выполнен</string>
<string name="login_successful_msg">Вход успешно выполнен</string>

Эти строки будут служить текстом надписей для кнопок и полей.
Обратите внимание на строку «welcome_message» – она содержит символы %1$s. Это выражение позволяет в данном случае заменить его в коде первым переданным параметром (строковой переменной). Мы будем использовать это в коде класса LoginFragment для подстановки в строку приветствия имени пользователя.

Пункты назначения и NavGraph

Добавление библиотеки Android Navigation в проект

Для поддержки пользовательской навигации нужно добавить в проект библиотеки androidx.navigation:navigation-fragment-ktx и androidx.navigation:navigation-ui-ktx актуальных версий.

Для этого добавьте в файл build.gradle модуля app в секцию dependecies:

implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'

Создание графа навигации

Далее перейдите в папку res и создайте в ней папку navigation.  Внутри папки navigation создайте Navigation Resource File с именем nav_graph.xml и корневым элементом <navigation>.

Добавление фрагментов – пунктов назначения

Добавьте новые пункты назначения. Для этого:

  1. В окне редактора дизайна нажмите кнопку «New destination»
  2. Выберите «Create new destination»
  3. Далее в окне добавления фрагмента выберите Fragment (Blank):

Далее в окне добавления фрагмента выберите Fragment (Blank):

Создайте таким образом три фрагмента:

  • MainFragment
  • LoginFragment
  • AccountFragment

У вас в окне дизайна редактора ресурсов должно добавиться три фрагмента:

У вас в окне дизайна редактора ресурсов должно добавиться три фрагмента:

А в файле nav_graph.xml должен появиться такой код:

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
    app:startDestination="@id/mainFragment">

    <fragment
        android:id="@+id/mainFragment"
        android:name="info.fandroid.androidconditionalnavigation.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" />
    <fragment
        android:id="@+id/accountFragment"
        android:name="info.fandroid.androidconditionalnavigation.AccountFragment"
        android:label="fragment_setting"
        tools:layout="@layout/fragment_account" />
    <fragment
        android:id="@+id/loginFragment"
        android:name="info.fandroid.androidconditionalnavigation.LoginFragment"
        android:label="fragment_login"
        tools:layout="@layout/fragment_login" />
</navigation>

На этом создание графа навигации завершено. Обратите внимание – в отличие от прошлых уроков, мы не добавляем здесь никаких секций <action> и не задаем переходов между фрагментами. Переходы по пунктам назначения мы пропишем непосредственно в коде классов экранов в зависимости от условий.

А чтобы фрагменты из графа навигации отображались на экране, нужно добавить в макет главного активити activity_main.xml компонент fragment – хост навигации:

<?xml version="1.0" encoding="utf-8"?>
<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=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph"/>
</androidx.constraintlayout.widget.ConstraintLayout>

 

DataBinding

Подключение DataBinding в android-проект

На экранах нашего приложения будут кнопки и текстовые поля, нужно их добавить в разметку макетов экранов. Но, прежде чем перейти к редактированию макетов, обеспечим в нашем проекте поддержку Data Binding для связи и обмена информацией компонентов разметки с кодом классов экранов.

  1. Откройте файл сборки build.gradle модуля app
  2. В секции plugins {…} добавьте строку id ‘kotlin_kapt’
  3. В секции android {…} добавьте строку dataBinding { enabled = true }.
  4. Синхронизируйте файл сборки с Gradle.

Макеты разметки экранов

Теперь займемся файлами разметки макетов.

В папке res/layout откройте файл fragment_main.xml. Удалите текстовое поле добавьте кнопку.  Приведите файл к такому виду:

<?xml version="1.0" encoding="utf-8"?>

<layout
    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">

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainFragment">


    <Button
        android:id="@+id/account_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/personal_account_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Как видим, здесь корневой элемент – это <layout>, который обеспечивает поддержку макетом разметки функции Data Binding.  Все директивы xlmns, соответственно, переместились в корневой элемент. Также здесь добавлена кнопка входа в личный кабинет.

Аналогично меняем макеты остальных фрагментов. Макет fragment_login.xml:

<?xml version="1.0" encoding="utf-8"?>

<layout
    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">

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginFragment">

    <TextView
        android:id="@+id/tv_login_promt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/login_prompt"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/login_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/login_button_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Здесь кнопка аутентификации и текстовое поле с приглашением.

И макет fragment_account.xml:

<?xml version="1.0" encoding="utf-8"?>

<layout
    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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".AccountFragment">

        <TextView
            android:id="@+id/tv_welcome"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Your personal account" />

        <Button
            android:id="@+id/logout_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/logout_button_text"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Здесь текстовое поле, отображающее приветствие, и кнопка выхода из личного кабинета.

 

Firebase Authentication

Создание проекта в Firebase

Для работы с Firebase Authentication нужно создать новый проект в консоли разработчика на сайте https://console.firebase.google.com/

  1.  В консоли Firebase нажмите «Добавить проект».
  2. Выберите или введите имя проекта.
  3. Нажмите «Продолжить».
  4. Настройку Google Analytics можно пропустить.
  5. Нажмите «Создать проект», чтобы завершить настройку проекта FireBase.

Регистрация приложения в Firebase

Далее нужно выполнить регистрацию нашего приложения в проекте Firebase, чтобы привязать приложение к проекту и обеспечить их взаимодействие.

  1. Откройте созданный проект и Нажмите «Добавить приложение».
  2. Выберите платформу «Android»
  3. Откроется такое окно:Регистрация приложения в Firebase
  4. В поле «Название пакета Андроид» укажите имя пакета вашего приложения.
    Его можно скопировать в файле сборки gradle, ищите параметр applicationID в секции android { … defaultConfig { … } …}.
  5. По желанию укажите псевдоним для приложения.
  6. Далее укажите хеш сертификата для отладки – его можно получить в Android Studio на вкладке Gradle правой панели. Выберите пункт Tasks\android\signingReport и, через некоторое время, внизу в открывшейся вкладке Run скопируйте ключ SHA1:хеш сертификата для отладки – его можно получить в Android Studio на вкладке Gradle правой панели. Выберите пункт Tasks\android\signingReport и, через некоторое время, внизу в открывшейся вкладке Run скопируйте ключ SHA1
  7. Вставьте ключ в поле формы регистрации приложения на сайте Firebase.
  8. Нажмите кнопку «Зарегистрировать приложение».
  9. После успешной регистрации станет доступен файл конфигурации google-service.json. Его нужно скачать и сохранить в папке app вашего проекта:файл конфигурации google-service.json. Его нужно скачать и сохранить в папке app вашего проекта.
  10. Добавьте Firebase SDK в проект путем изменения файлов gradle вашего проекта по инструкции, которая откроется на следующем шаге:

Файл build.gradle уровня проекта (<project>/build.gradle):

buildscript {
  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
  }
  dependencies {
    ...
    // Add this line
    classpath 'com.google.gms:google-services:4.3.4'
  }
}

allprojects {
  ...
  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
    ...
  }
}

Файл build.gradle уровня приложения (<project>/<app-module>/build.gradle):

apply plugin: 'com.android.application'
// Add this line
apply plugin: 'com.google.gms.google-services'

dependencies {
  // Import the Firebase BoM
  implementation platform('com.google.firebase:firebase-bom:26.3.0')

  // Add the dependencies for the desired Firebase products
  // https://firebase.google.com/docs/android/setup#available-libraries
}

 

  1. Синхронизируйте проект с Gradle.

Регистрация приложения завершена. Откройте проект, и вы увидите его в списке приложений.

Включение аутентификации Firebase в андроид-приложении

Для подключения аутентификации в консоли Firebase нужно выполнить следующие шаги:

  1. В панели вашего проекта слева перейдите в раздел «Authentification»
  2. Выберите вкладку «Sign-in method»
  3. В списке «Провайдеры авторизации» включите 2 вида авторизации –  Адрес электронной почты и пароль, и Google:3. В списке «Провайдеры авторизации» включите 2 вида авторизации - Адрес электронной почты и пароль, и Google.

При подключении авторизации от Google нужно также указать общедоступное название проекта, которое будут видеть пользователи, например, в письмах подтверждения авторизации, и email поддержки вашего проекта. Здесь я указал свой email.

  1. Вернитесь в Android Studio и в файле build.gradle модуля app добавьте библиотеки для работы с Firebase аутентификацией:
implementation 'com.google.firebase:firebase-auth:20.0.2'

implementation 'com.firebaseui:firebase-ui-auth:5.0.0'

 

LiveData

Теперь переходим непосредственно к написанию кода.

Реализуем в нашем приложении класс, который позволит подписываться на события аутентификации пользователей вашего приложения. Будем использовать класс LiveData, который является хранилищем данных и реализует паттерн Наблюдатель (Observer). Таким образом, можно подписаться на данные, хранящиеся в LiveData.

Создайте класс FirebaseUserLiveData, наследник LiveData с параметризированным типом <FirebaseUser>.

package info.fandroid.androidconditionalnavigation

import androidx.lifecycle.LiveData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser

class FirebaseUserLiveData : LiveData<FirebaseUser?>() {
    private val firebaseAuth = FirebaseAuth.getInstance()

    private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
        value = firebaseAuth.currentUser
    }

    override fun onActive() {
        firebaseAuth.addAuthStateListener(authStateListener)
    }

    override fun onInactive() {
        firebaseAuth.removeAuthStateListener(authStateListener)
    }
}

Класс FirebaseUser предоставляет информацию о зарегистрированных пользователях.

Инициализируем переменную для инстанса класса FirebaseAuth, который является точкой входа в Firebase Authentication SDK.

Также напишем слушатель состояния аутентификации пользователя, который будет получать текущего пользователя.

Переопределенные методы onActive и onInactive срабатывают при изменении количества активных подписчиков. Метод onActive будет вызван, когда у LiveData появится хотя бы один подписчик. А onInactive – когда не останется ни одного подписчика.  В первом мы будем подключать, а во втором – отключать слушатель для экономии ресурсов.

 

ViewModel

Для получения данных из LiveData будем использовать ViewModel. Этот класс обеспечит сохранность данных при изменениях конфигурации, например, повороте устройства.

Создайте класс LoginViewModel, унаследуйте его от класса ViewModel.

package info.fandroid.androidconditionalnavigation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.map

class LoginViewModel : ViewModel() {

    enum class AuthenticationState {
        AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
    }

    val authenticationState = FirebaseUserLiveData().map { user ->
        if (user != null) {
            AuthenticationState.AUTHENTICATED
        } else {
            AuthenticationState.UNAUTHENTICATED
        }
    }
}

Объявим класс-перечисление AuthenticatioState для разных состояний пользователя – аутентифицирован, неаутентифицирован, и ошибка аутентификации.

Далее создадим переменную authenticationState, которая, используя объект ранее созданного нами класса FirebaseUserLiveData будет получать и хранить состояние аутентификации пользователя при любых изменениях этого состояния.

С помощью этой переменной классы фрагментов будут получать информацию о том, вошел ли пользователь в систему.

Чтобы маппинг корректно работал здесь, добавьте такие зависимостти в файл сборки build.gradle (app):

    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

 

MainFragment

В теле класса фрагмента MainFragment удалите ненужный код. Объявите переменные для LoginViewModel и binding.

package info.fandroid.androidconditionalnavigation

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import info.fandroid.androidconditionalnavigation.databinding.FragmentMainBinding

class MainFragment : Fragment() {

    private val viewModel by viewModels<LoginViewModel>()
    private lateinit var binding: FragmentMainBinding  

}

Переменную viewModel инициализируем через viewModels – делегированное свойство для доступа к ViewModel, по умолчанию привязанное к текущему фрагменту. Таким образом, при пересоздании фрагмента ViewModel не будет пересоздан.

В методе onCreateView при помощи DataBindingUtil привязываем файл макета разметки к этому фрагменту. Посредством переменной binding мы сможем обращаться ко всем элементам разметки экрана.

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {

        // Inflate the layout for this fragment
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)

        return binding.root
    }

Создаем функцию observeAuthenticationState, в которой подписываемся на данные о состоянии аутентификации пользователя. Метод observe добавляет наблюдателя в список наблюдателей в пределах жизненного цикла текущего фрагмента. Наблюдатель получает данные из LiveData и мы на основе этих данных строим цикл when. Здесь, если пользователь аутентифицирован, обрабатываем нажатие кнопки, по которому отправляем его на экран личного кабинета. Если пользователь не аутентифицирован, то по нажатию кнопки отправляем его на экран входа.

 

private fun observeAuthenticationState() {
        viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
            when (authenticationState) {
                LoginViewModel.AuthenticationState.AUTHENTICATED -> {

                    binding.accountButton.setOnClickListener {
                        findNavController().navigate(R.id.accountFragment)
                    }
                } else -> {
                    binding.accountButton.setOnClickListener {
                        findNavController().navigate(R.id.loginFragment)
                    }
                }
            }
        })
    }

Функцию observeAuthenticationState будем вызывать в переопределенном методе жизненного цикла onViewCreated, который вызывается непосредственно перед отображением фрагмента на экране.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        observeAuthenticationState()
    }

Если мы повернем экран, то активити и фрагмент в нем будет пересоздан. Но LiveData и ViewModel будут активны и сразу передадут актуальные данные подписанному наблюдателю.

 

LoginFragment

Переходим к классу LoginFragment, который будет отображать текст приглашения и кнопку входа.

class LoginFragment : Fragment() {

    companion object {
        const val TAG = "LoginFragment"
        const val SIGN_IN_RESULT_CODE = 1001
    }

}

Создадим вспомогательный объект и объявим в нем две константы. Первая – TAG – будет использоваться в качестве тега для логирования (подробнее об этом здесь https://www.fandroid.info/urok-12-logcat-logi-prilozheniya-isklyucheniya-exception-obrabotka-oshibok-v-kode-android-studio/).

Вторая константа – SIGN_IN_RESULT_CODE – для кода, который будет использоваться в вызове startActivityForResult. Её значение – просто произвольное число. Мы будем стартовать активити для аутентификации, передавая туда эту константу, с ожиданием результата. Результат будет приходить в наш текущий фрагмент, а идентифицировать его мы будем по этой самой константе.  Более подробно об этой технологии можно посмотреть здесь https://www.fandroid.info/urok-29-vyzov-vtorogo-activity-s-vozvrashheniem-dannyh-uroki-android-studio/

Далее объявим и инициализируем переменную viewModel, аналогично, как мы это делали в классе MainFragment.

private val viewModel by viewModels<LoginViewModel>()

Также объявим пременную navController для контроллера навигации, с ленивой инициализацией.

private lateinit var navController: NavController

Далее в методе onCreateView подключаем binding и инфлейтим макет разметки фрагмента. Через binding обращаемся к кнопке и назначаем ей слушатель. В слушатель пока ничего не пишем.

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    val binding = DataBindingUtil.inflate<FragmentLoginBinding>(
        inflater, R.layout.fragment_login, container, false
    )

    binding.loginButton.setOnClickListener {}

    return binding.root
}

Создаем функцию launchSignInFlow() для запроса аутентификации. Создаем переменную providers для списка провайдеров аутентификации. Как вы помните, в настройках проекта в консоли Firebase мы выбрали два провайдера аутентификации – по email и паролю, и Google.  Соответственно, вызываем здесь два билдера через Firebase классы AuthUI.IdpConfig  для каждого провайдера.

private fun launchSignInFlow() {

    val providers = arrayListOf(
        AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build()
    )

    startActivityForResult(
        AuthUI.getInstance().createSignInIntentBuilder().setAvailableProviders(
            providers
        ).build(), SIGN_IN_RESULT_CODE
    )
}

Далее в методе startActivityForResult получаем экземпляр класса AuthUI, с помощью которого создаем экран подписки с выбором провайдеров из созданного нами ранее списка. В конце передаем константу SIGN_IN_RESULT_CODE. Вызов функции launchSignInFlow() будем выполнять по нажатию кнопки входа, пропишем ее в слушателе.

binding.loginButton.setOnClickListener { launchSignInFlow() }

Таким образом, методом startActivityForResult мы запускаем активити с выбором способов авторизации. Такой способ запуска гарантирует возврат какого-то результата. Результатом будет ответ от Firebase о том, аутентифицирован пользователь или нет.

Получать и обрабатывать результат от Firebase будем в текущем фрагменте. Для этого нужно переопределить метод onActivityResult, который принимает такие параметры:

  • requestCode – это наша константа, которую мы передали в startActivityForResult;
  • resultCode – это код результата, успешно или нет;
  • data – Intent с данными.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == SIGN_IN_RESULT_CODE) {
        val response = IdpResponse.fromResultIntent(data)
        if (resultCode == Activity.RESULT_OK) {
            // Successfully signed in user.
            i(
                TAG,
                "Successfully signed in user " +
                        "${FirebaseAuth.getInstance().currentUser?.displayName}!"
            )
        } else {
                Log.i(TAG, "Sign in unsuccessful ${response?.error?.errorCode}")
        }
    }
}

Проверяем, что requestCode соответствует коду нашей константы, и извлекаем данные из полученного интента методом IdpResponce.fromResultIntent. Далее проверяем –  если результат успешен, то пишем в логи сообщение об успешной аутентификации пользователя с добавлением имени пользователя, полученном через класс FirebaseAuth. В противном случае отображаем в логах ошибку, извлеченную из интента.

Выполнять навигацию мы будем в другом преопределенном методе – onViewCreated. Как вы помните, он вызывается системой перед отрисовкой фрагмента на экране.

 

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    navController = findNavController()
    requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
        navController.popBackStack(R.id.mainFragment, false)
    }

    viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
        when (authenticationState) {
            LoginViewModel.AuthenticationState.AUTHENTICATED -> {
                navController.popBackStack()
                navController.navigate(R.id.accountFragment)
            }
            LoginViewModel.AuthenticationState.INVALID_AUTHENTICATION -> Snackbar.make(
                view, requireActivity().getString(R.string.login_unsuccessful_msg),
                Snackbar.LENGTH_LONG
            ).show()
            else -> Log.e(
                TAG,
                "Authentication state that doesn't require any UI change $authenticationState"
            )
        }
    })
}

Инициализируем контроллер навигации.

Нам нужно переопределить поведение системной кнопки «Назад», чтобы при ее нажатии пользователь всегда возвращался на главный экран с кнопкой «Личный кабинет», а не уходил в бесконечную карусель экранов.  Реализуем это через onBackPressedDispatcher. В его функцию addCallback передаем текущий фрагмент и вызываем контроллер навигации с передачей в его бэк стек фрагмента MainFragment. Таким образом, нажатие кнопки «Назад» будет вести на MainFragment. Подробнее об этой технологии смотрите здесь: https://www.fandroid.info/urok-25-task-i-backstack-aktiviti-android-prilozhenij-uroki-android-studio/

Дальнейший код похож на код получения состояния пользователя в главном фрагменте. Мы подписываемся на состояние аутентификации пользователя. И если пользователь залогинился – вызываем метод контроллера навигации popBackStack(), который удалит текущий экран из стека, чтобы пользователь потом переходил на главный экран при нажатии кнопки «Назад». Затем отправляем пользователя на экран личного кабинета, в AccountFragment.

Если аутентификация пользователя по какой-либо причине не удалась, отображаем снекбар с сообщением о неудаче. Если не то, и не другое – отображаем в логах сообщение, что состояние пользователя не изменилось.

AccountFragment

Осталось реализовать последний фрагмент AccountFragment. Он отображает текст приветствия с именем пользователя и кнопку выхода из аккаунта. Его реализация похожа на главный фрагмент MainFragment.

package info.fandroid.androidconditionalnavigation

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.firebase.ui.auth.AuthUI
import com.google.firebase.auth.FirebaseAuth
import info.fandroid.androidconditionalnavigation.databinding.FragmentAccountBinding

class AccountFragment : Fragment() {

    // Get a reference to the ViewModel scoped to this Fragment
    private val viewModel by viewModels<LoginViewModel>()
    private lateinit var binding: FragmentAccountBinding


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {

        // Inflate the layout for this fragment
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_account, container, false)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        observeAuthenticationState()
    }

    private fun observeAuthenticationState() {
        viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
            if (authenticationState == LoginViewModel.AuthenticationState.AUTHENTICATED) {

                val hello = String.format(
                    resources.getString(
                        R.string.welcome_message,
                        FirebaseAuth.getInstance().currentUser?.displayName))

                binding.tvWelcome.text = hello

                binding.logoutButton.setOnClickListener {
                    AuthUI.getInstance().signOut(requireContext())
                    findNavController().popBackStack()

                }
            }
        })
    }
}

Здесь в функции observeAutenticationState, где мы подписываемся на состояние аутентификации пользователя, мы создаем строковую переменную hello для приветствия, куда включаем полученное посредством класса FirebaseAuth имя текущего залогиненного пользователя. Мы используем строку с регулярным выражением, которую мы создавали в начале урока:

<string name="welcome_message">Привет, %1$s!</string>

Ее мы передаем функции String.format вместе с именем пользователя, и имя подставляется вместо выражения %1$s.

Затем через binding обращаемся к текстовому полю и помещаем туда форматированный текст переменной hello.

А по нажатию кнопки вызываем метод signOut класса AuthUI, таким образом пользователь выходит из аккаунта и далее отправляем его на главный экран вызовом функции popBackStack(), которая удаляет текущий фрагмент из стека. Таким образом, происходит переход к предыдущему фрагменту в стеке. А поскольку это был LoginFragment и мы тоже его удалили из стека аналогичным образом, то пользователь попадет на экран, который точно в cтеке есть – MainFragment.

Вызов функции observeAutenticationState() пропишем в переопределенном методе onViewCreated.

 

Запуск и тестирование приложения

С кодом мы закончили, переходим к тестированию. Запускаем приложение на эмуляторе.Запускаем приложение на эмуляторе

Нажимаем кнопку Личного кабинета. Поскольку мы еще не аутентифицированы, открывается экран входа в учетную запись.экран входа в учетную запись

После нажатия кнопки входа отображается список провайдеров аутентификации.список провайдеров аутентификации

Если выбрать способ с адресом электронной почты и паролем, то далее откроется окно для ввода email и пароля.окно для ввода email и пароля

А если выбрать Google, то откроется стандартный диалог выбора Google аккаунта из аутентифицированных на устройстве.

стандартный диалог выбора Google аккаунта

После аутентификации попадаем на экран Личного кабинета. Видим приветствие с собственным именем и кнопку выхода из аккаунта.После аутентификации попадаем на экран Личного кабинета

Системная кнопка «Назад» ведет нас на главный экран. Если мы снова нажмем кнопку Личного кабинета, то попадем непосредственно на его экран, поскольку уже аутентифицированы.

Кнопка выхода из аккаунта отправляет нас на главный экран, и теперь нужно снова залогиниться на экране входа, чтобы попасть в личный кабинет.

Исходный код

На этом урок закончен. Надеемся, он был вам полезен. Отзывы, пожелания и вопросы пишите в комментариях.

Исходный код проекта из урока можно скачать по ссылке.
До встречи на следующем уроке!

 

Урок 15. Передача данных между экранами — пунктами назначения. Android Navigation. Bundle vs Safe Args

 

 

 

 

 

Додати коментар