Android Jetpack Navigation: Handle Firebase Auth Signin via Shared MainActivity

April 29, 2020

When you implement Jetpack Navigation, you would have a single MainActivity with many fragments. You could implement shared code among all fragments in MainActivity, such as sign in using Firebase Authentication.

MainActivity

class MainActivity : AppCompatActivity() {

    companion object {
        const val REQUEST_SIGN_IN = 1
    }

    private val viewModel: MainViewModel by viewModels()

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_SIGN_IN) {
            val response = IdpResponse.fromResultIntent(data)
            val signInRequest = viewModel.requestSignInEvent.value?.peek()

            if (resultCode == RESULT_OK) {
                val user = FirebaseAuth.getInstance().currentUser
                if (user != null) {
                    // TODO: init UserPrivate isAnonymouse=false, save email, etc.
                    Timber.d("name=${user.displayName}, email=${user.email}, uid=${user.uid}")
                    viewModel.initUser(user)
                }

                signInRequest?.also { request ->
                    if (request.liveData != null) {
                        request.liveData.value = Event(Resource(data = request.requestCode))
                    }
                }
            }
            else {
                response?.also { response ->
                    // when the signin credential already signup for this app
                    // as existing user cannot be linked to existing anonymous user
                    if (response.error?.errorCode == ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT) {
                        Timber.w("REQUEST_SIGN_IN conflict detected")
                        val currentAnonymousUser = FirebaseAuth.getInstance().currentUser
                        // TODO: save anonymous data

                        val newCredential = response.credentialForLinking
                        if (newCredential != null) {
                            FirebaseAuth.getInstance().signInWithCredential(newCredential)
                                .addOnSuccessListener {
                                    val newUser = FirebaseAuth.getInstance().currentUser
                                    // TODO: migrate/merge anonymous data to existing user

                                    signInRequest?.also { request ->
                                        if (request.liveData != null) {
                                            request.liveData.value =
                                                Event(Resource(data = request.requestCode))
                                        }
                                    }
                                }
                        }
                    }
                    else {
                        // TODO: toast
                        Timber.e(response.error,"REQUEST_SIGN_IN error")

                        signInRequest?.also { request ->
                            if (request.liveData != null) {
                                request.liveData.value =
                                    Event(Resource(exception = response.error!!))
                            }
                        }
                    }
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)

        // init nav controller for jetpack navigation

        // listen to sign in request from fragment
        viewModel.requestSignInEvent.observe(this, Observer { event ->
            event.getIfPending()?.let {
                startActivityForResult(viewModel.getAuthIntent(), REQUEST_SIGN_IN)
            }
        })
    }

}

NOTE: Refer Event class.

MainViewModel

class Request(val requestCode: Int = 0, val liveData: MutableLiveData<Event<Resource<Int>>>? = null)

class MainViewModel : ViewModel() {
    internal val requestSignInEvent = MutableLiveData<Event<Request>>()

    fun getAuthIntent(): Intent {
        val currentUser = FirebaseAuth.getInstance().currentUser
        val providers = if (currentUser == null) {
            arrayListOf(
                AuthUI.IdpConfig.GoogleBuilder().build(),
                // uthUI.IdpConfig.EmailBuilder().build(),
                AuthUI.IdpConfig.AnonymousBuilder().build()
            )
        } else {
            arrayListOf(
                AuthUI.IdpConfig.GoogleBuilder().build()
            )
        }

        return AuthUI.getInstance()
            .createSignInIntentBuilder()
            .setIsSmartLockEnabled(false)
            .setAvailableProviders(providers)
            .enableAnonymousUsersAutoUpgrade()
            .build()
    }

    fun initUser(user: FirebaseUser) {
        // TODO: create or update user in database
    }
}

Fragment

class HomeFragment : Fragment() {
    protected val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance()  }
    protected val mainViewModel: MainViewModel by activityViewModels()

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        if (auth.currentUser == null) {

            val liveData = viewModel.signInEvent
            mainViewModel.requestSignInEvent.value = Event(Request(liveData = liveData))

            // callback upon successful signin
            liveData.observe(viewLifecycleOwner, Observer { event ->
                event.getIfPending()?.also { resource ->
                    if (resource.isSuccessful) {
                        init()
                    }
                    else {
                        Toast.makeText(requireContext(), resource.error().toString(), Toast.LENGTH_LONG).show()
                    }
                }
            })
        }
        else {
            init()
        }
    }

    private fun init() {
        // TODO: init data and UI
    }
}
class HomeViewModel : ViewModel() {
    internal val signInEvent = MutableLiveData<Event<Resource<Int>>>()
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.