0
votes

I've implemented email and password authentication using ViewModel and LiveData. My code looks like this:

AuhtService.kt

fun createNewUser(email: String, password: String): MutableLiveData<AuthData> {
    val authenticatedUserLiveData = MutableLiveData<AuthData>()

    firebaseAuth.createUserWithEmailAndPassword(email, password)
        .addOnCompleteListener { task: Task<AuthResult> ->
            if (task.isSuccessful) {
                Log.d("AuthService", "Created new user! $email")
                authenticatedUserLiveData.value = AuthData("success", task.result?.user, null)

                task.result?.user?.let {
                    DatabaseService.getDbInstance().collection("users")
                        .document(it.uid).set(it).addOnCompleteListener { task: Task<Void> ->
                            Log.d("AuthService", "Added new user to database.")
                        }
                }
            } else {
                Log.d(
                    "AuthService",
                    "Failed to create new user! $email ${task.exception?.message}"
                )
                authenticatedUserLiveData.value =
                    AuthData("failure", null, task.exception?.message)
            }
        }


    return authenticatedUserLiveData
}

This class performs the firebase operations and returns livedata of type AuthData which is basically a wrapper around FirebaseUser and a couple of strings to carry error messages in case Authentication fails.

UserAuthViewModel.kt

class UserAuthViewModel : ViewModel() {
    private val authService = AuthService()

    enum class AuthState {
        AUTHENTICATED,
        UNAUTHENTICATED,
        INVALID_AUTH
    }

    val authState = MutableLiveData<AuthState>()
    val authErrorMsg = MutableLiveData<String>()
    var currentUser = MutableLiveData<FirebaseUser>()

    init {
        authErrorMsg.value = ""
        authState.value = AuthState.UNAUTHENTICATED
        currentUser = authService.getCurrentUser()
    }

    fun signInWithEmail(email: String, password: String) {
        val result = authService.loginWithEmail(email, password).value

        if (result?.status.equals("success")) {
            authState.value = AuthState.AUTHENTICATED
        } else {
            authState.value = AuthState.INVALID_AUTH
            authErrorMsg.value = result?.msg
        }

        currentUser.value = result?.data
    }

    fun createUserWithEmailAndPassword(email: String, password: String) {

        val result = authService.createNewUser(email, password).value

        if (result?.status.equals("success")) {
            authState.value = AuthState.AUTHENTICATED
        } else {
            authState.value = AuthState.INVALID_AUTH
            Log.d("AuthVM", result?.msg)
            authErrorMsg.value = result?.msg
        }

        currentUser.value = result?.data
    }
}

finally my RegisterFragment.kt

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

    userAuthViewModel.authState.observe(viewLifecycleOwner, Observer { authState ->
        if (authState == UserAuthViewModel.AuthState.AUTHENTICATED) {
            Toast.makeText(
                context,
                "User is ${userAuthViewModel.currentUser.value?.email}",
                Toast.LENGTH_SHORT
            ).show()
        }

        if(authState == UserAuthViewModel.AuthState.INVALID_AUTH) {
            Snackbar.make(view, "${userAuthViewModel.authErrorMsg.value}", Snackbar.LENGTH_SHORT).show()
        }
    })

    register_button.setOnClickListener {
        val email = registerEmail_editText.editText?.text
        val pwd = registerPassword_editText.editText?.text

        userAuthViewModel.createUserWithEmailAndPassword(email.toString(), pwd.toString())
    }

    goToLoginButton.setOnClickListener{
        findNavController().navigate(R.id.action_registerFragment_to_loginFragment)
    }
}

The above code is mainly inspired from google docs and MVVM arch. but it behaves very weirdly, first of all I get a snackbar at the start with no text, secondly the error messages are not displayed in the sncakbar when auth fails. What am I doing wrong.

Update: On further testing it seems that result?.msg in my UserAuthViewModel is null, but I'm passing error messages in my AuthService, quite weird.

Update 2: Its seems in either case the else of if (result?.status.equals("success")) is being executed, am I doing string comparison wrong?

Update 3: I figured it out but I'm not sure what is the proper way to solve this. The problem is that although AuthServie returns a MutableLiveData that contains null values initially which will be replaced by proper values later (when async calls from AuthService finish). In the ViewModel I do this to propagate new AuthData when user perfroms a sign up

val result = authService.createNewUser(email, password).value

Problem is that doing this passes the initial LiveData object in the AuthService with null values to the ViewModel, so the View updates with null values, I need a way to so that the View can observe actual values when they get updated from authservice's callbacks.

1
check this code is executing or not authState.value = AuthState.AUTHENTICATED. - Rahul sharma
Updated the question please check - Amol Borkar

1 Answers

0
votes

createNewUser() method is doing it's work asynchronously. Initially authenticatedUserLiveData is null. So you are getting null from viewmodel. You have to push a callback after onCompleteListener() called from firebaseAuth.createUserWithEmailAndPassword(). There are several ways to make this. Personally I use RxJava for async tasks. For better understanding you can make a interface will be called after success or error callback from firebase auth. Code will looks like this

callback interface can be:

interface AuthCallback {
    fun onUserCreationResult(authData: AuthData)
}

from AuthService

fun createNewUser(email: String, password: String, callback: AuthCallback){

firebaseAuth.createUserWithEmailAndPassword(email, password)
    .addOnCompleteListener { task: Task<AuthResult> ->
        if (task.isSuccessful) {
            Log.d("AuthService", "Created new user! $email")
            callback.onUserCreationResult(AuthData("success", task.result?.user, null))

            task.result?.user?.let {
                DatabaseService.getDbInstance().collection("users")
                    .document(it.uid).set(it).addOnCompleteListener { task: Task<Void> ->
                        Log.d("AuthService", "Added new user to database.")
                    }
            }
        } else {
            Log.d(
                "AuthService",
                "Failed to create new user! $email ${task.exception?.message}"
            )
            callback.onUserCreationResult(AuthData("failure", null, task.exception?.message))
        }
    }
}

And finally in viewmodel:

fun createUserWithEmailAndPassword(email: String, password: String) {

    authService.createNewUser(email, password, object: AuthCallback {
        override fun onUserCreationResult(authData: AuthData) {
            if (data?.status.equals("success")) {
                authState.value = AuthState.AUTHENTICATED
            } else {
                authState.value = AuthState.INVALID_AUTH
                Log.d("AuthVM", result?.msg)
                authErrorMsg.value = result?.msg
            }

            currentUser.value = authData.data
        }

    })

}

I hope everything will work fine.