Clean Architecture

Introducción

En esta parte vamos a crear una nueva funcionalidad para nuestro sistema de login y registro, esta funcion es la de recuperar contraseña por medio de un email, la idea es poder recibir un link en nuestar casilla de correo el cual nos sirva para recuperar la contraseña perdida u olvidada.

Password recover with firebase

El primer paso es crear nuestra layout y actividad que va a utilizar esta nueva funcion, en este caso creamos una nueva actividad llamada PasswordRecoverActivity.kt dentro de nuestro paquete auth - passwordrecover

activity_password_recover.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".presentation.auth.passwordrecover.view.PasswordRecoverActivity">

    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:ems="10"
            android:id="@+id/etxt_recover_pw" app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" android:layout_marginTop="340dp"
            app:layout_constraintTop_toTopOf="parent"/>
    <Button
            android:text="Send recover email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btn_recover_pw" android:layout_marginTop="40dp"
            app:layout_constraintTop_toBottomOf="@+id/etxt_recover_pw" app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" app:layout_constraintHorizontal_bias="0.498"/>
    <ProgressBar
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/progress_recover_pw"
            android:elevation="3dp"
            android:visibility="gone"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            android:layout_marginTop="40dp"
            app:layout_constraintTop_toBottomOf="@+id/etxt_recover_pw"
            app:layout_constraintHorizontal_bias="0.498"/>
    <TextView
            android:text="Password Recover"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:textStyle="bold"
            android:id="@+id/textView" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp"
            app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp" android:layout_marginTop="176dp"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_bias="0.498"/>
</androidx.constraintlayout.widget.ConstraintLayout>

El próximo paso es crear la interfaz que va a sostener los metodos de nuestra vista como de nuestro presenter, a esto lo llamaremos

PasswordRecoverContract.kt

package com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover

/**
 * Created by Gastón Saillén on 14 June 2019
 */
interface PasswordRecoverContract {

    interface PasswordRecoverView{
        fun showError(msgError:String?)
        fun showProgress()
        fun hideProgress()
        fun recoverPassword()
        fun navigateToLogin()
    }

    interface PasswordRecoverPresenter{
        fun attachView(passwordRecoverView:PasswordRecoverView)
        fun detachView()
        fun detachJob()
        fun isViewAttached():Boolean
        fun sendPasswordRecover(email:String)
    }
}

Estas interfaces las vamos a implementar tanto en nuestra vista como en nuestro presenter, en nuestra vista PasswordRecoverActivity implementamos la interfaz y sus metodos, adema extendemos BaseActivity() ya que como habiamos mensionado en tutoriales anteriores va a ser nuestro base para cada actividad que creemos y que cumpla con las condiciones que estan planteadas en el.

PasswordRecoverActivity.kt

package com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.view

import android.content.Intent
import android.os.Bundle
import android.view.View
import com.gaston.cleanfirestorelogin.R
import com.gaston.cleanfirestorelogin.base.BaseActivity
import com.gaston.cleanfirestorelogin.domain.interactor.auth.passwordrecoverinteractor.PasswordRecoverImpl
import com.gaston.cleanfirestorelogin.presentation.auth.login.view.LoginActivity
import com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.PasswordRecoverContract
import com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.presenter.PasswordRecoverPresenter
import kotlinx.android.synthetic.main.activity_password_recover.*

class PasswordRecoverActivity : BaseActivity(), PasswordRecoverContract.PasswordRecoverView {

    lateinit var presenter: PasswordRecoverPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        presenter = PasswordRecoverPresenter(PasswordRecoverImpl())
        presenter.attachView(this)
        btn_recover_pw.setOnClickListener {
            recoverPassword()
        }
    }

    override fun getLayout(): Int {
        return R.layout.activity_password_recover
    }

    override fun showError(msgError: String?) {
        toast(this, msgError)
    }

    override fun showProgress() {
        progress_recover_pw.visibility = View.VISIBLE
        btn_recover_pw.visibility = View.GONE
    }

    override fun hideProgress() {
        progress_recover_pw.visibility = View.GONE
        btn_recover_pw.visibility = View.VISIBLE
    }

    override fun recoverPassword() {
        val email:String = etxt_recover_pw.text.trim().toString()
        if(!email.isEmpty()) presenter.sendPasswordRecover(email) else toast(this,"E-mail is empty")
    }

    override fun navigateToLogin() {
        startActivity(Intent(this,LoginActivity::class.java))
        toast(this,"Recover E-mail sent")
        finish()
    }

    override fun onDestroy() {
        super.onDestroy()
        presenter.detachView()
        presenter.detachJob()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        presenter.detachView()
        presenter.detachJob()
    }
}

Ahora nos queda implementa el presenter con el cual esta actividad se va a comunicar para poder realizar distintas acciones, en este caso , una de ellas es la mas importante, y es la de recuperar contraseña.

Solo deberemos pasarle un E-mail al metodo sendPasswordRecover(email) , este E-mail es el que recogemos de nuestro edittext

PasswordRecoverPresenter.kt

package com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.presenter

import com.gaston.cleanfirestorelogin.domain.interactor.auth.passwordrecoverinteractor.PasswordRecover
import com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.PasswordRecoverContract
import com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.exception.PasswordRecoverException
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

/**
 * Created by Gastón Saillén on 14 June 2019
 */
class PasswordRecoverPresenter(passwordRecoverInteractor: PasswordRecover) :
    PasswordRecoverContract.PasswordRecoverPresenter, CoroutineScope {

    var view: PasswordRecoverContract.PasswordRecoverView? = null
    var passwordRecoverInteractor: PasswordRecover? = null

    var job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    init {
        this.passwordRecoverInteractor = passwordRecoverInteractor
    }

    override fun attachView(passwordRecoverView: PasswordRecoverContract.PasswordRecoverView) {
        this.view = passwordRecoverView
    }

    override fun detachView() {
        view = null
    }

    override fun detachJob() {
        coroutineContext.cancel()
    }

    override fun isViewAttached(): Boolean {
        return view != null
    }

    override fun sendPasswordRecover(email: String) {

        launch {
            try {
                view?.showProgress()
                passwordRecoverInteractor?.sendPasswordResetEmail(email)
                view?.hideProgress()
                view?.navigateToLogin()
            } catch (e:PasswordRecoverException){
                view?.hideProgress()
                view?.showError(e.message)
            }
        }

    }
}

Vamos a necesitar implementar una excepcion personalizada para poder retornar el error en caso de que el email no exista o que ocurra algun error en la red a la hora de tratar de recuperar la contraseña.

Para eso implementamos una nueva clase llamada

PasswordRecoverException.kt

package com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.exception

/**
 * Created by Gastón Saillén on 14 June 2019
 */
class PasswordRecoverException(message:String): Exception(message)

Por último vamos a implementar nuestro interactor (caso de uso) que nos va a servir para hacer la logica que envia el email y recibe dos responses, una exitosa y otra de error.

Primero creamos la interfaz de este interactor

 

PasswordRecover.kt

package com.gaston.cleanfirestorelogin.domain.interactor.auth.passwordrecoverinteractor

/**
 * Created by Gastón Saillén on 14 June 2019
 */
interface PasswordRecover {

    suspend fun sendPasswordResetEmail(email: String)

}

y por ultimo implementamos esa interfaz con su logica

PasswordRecoverImpl.kt

package com.gaston.cleanfirestorelogin.domain.interactor.auth.passwordrecoverinteractor

import com.gaston.cleanfirestorelogin.presentation.auth.passwordrecover.exception.PasswordRecoverException
import com.google.firebase.auth.FirebaseAuth
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * Created by Gastón Saillén on 14 June 2019
 */
class PasswordRecoverImpl: PasswordRecover {

    override suspend fun sendPasswordResetEmail(email: String): Unit = suspendCancellableCoroutine { continuation ->
        FirebaseAuth.getInstance().sendPasswordResetEmail(email).addOnCompleteListener {
            if(it.isSuccessful){
               continuation.resume(Unit)
            }else{
                continuation.resumeWithException(PasswordRecoverException(it.exception?.message !!))
            }
        }
    }
}

Para despejar dudas y ver la implementación concreta de este código podes seguir el siguiente video

Links útiles

Link GitHub del código completo: https://github.com/gastsail/CleanFirestoreLogin