Android startActivityForResult in Fragment Propagate onActivityResult to Both Activity and Result

October 22, 2019
Management of Sign In Request in Activity

Call startActivityForResult in Fragment

When you call Fragment.startActivityForResult, the result is returned in Fragment.onActivityResult (although Activity.onActivityResult is called, but the requestCode is obfuscated).

You can call activity?.startActivityForResult(intent, MainActivity.REQUEST_SIGN_IN), where the result is returned in Activity.onActivityResult, but the Fragment is not notified.

class TestFragment : Fragment() {
    compainion object {
        private const val REQUEST_SIGN_IN = 1
    }

    private fun requestSignIn() {
        val intent = ...

        // activity?.startActivityForResult(intent, MainActivity.REQUEST_SIGN_IN)
        startActivityForResult(intent, REQUEST_SIGN_IN)
    }
}

Scenario: Management of Sign In Request in Activity

I have multiple fragments which might make a sign in request (startActivityForResult(intent, REQUEST_SIGN_IN)), where I wanted a central code (Activity) to handle signin (handle error, save to database, update UI/state, etc.) and notify the fragment of the successful sign in so that it can run some fragment specific code as well.

Solution 1: Fragment call handleSignIn

I like this solution because it is simple, but fragment must call the handleSignIn function manually.

  • Shared code with Activity via ViewModel
class TestFragment : Fragment() {
    compainion object {
        private const val REQUEST_SIGN_IN = 1
    }

    private val viewModel by viewModels<TestViewModel>()
    private val mainViewModel by activityViewModels<MainViewModel>()

    private fun requestSignIn() {
        startActivityForResult(mainViewModel.getAuthIntent(), REQUEST_SIGN_IN)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_SIGN_IN) {
            // optional coroutines call, depending on handleSignIn
            lifecycleScope.launch(Dispatchers.Default) {

                if (mainViewModel.handleSignIn(requestCode, resultCode, data)) {
                    // if signin successful
                    val auth = FirebaseAuth.getInstance()
                    Timber.d("uid=${auth.currentUser?.uid}")
                }

            }
        }
    }
}

NOTE: For viewModels and activityViewModels, refer to Get ViewModel In Fragment via KTX.

class MainViewModel : ViewModel() {

    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()
            .setAvailableProviders(providers)
            .enableAnonymousUsersAutoUpgrade()
            .build()
    }

    suspend fun handleSignIn(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
        val response = IdpResponse.fromResultIntent(data)

        val auth = FirebaseAuth.getInstance()

        if (resultCode == Activity.RESULT_OK) {
            val user = auth.currentUser
            if (user != null) {
                Timber.d("name=${user.displayName}, email=${user.email}, uid=${user.uid}")
                // TODO: init user
            }

            return true
        }
        else {
            if (response != null) {
                // 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) {
                        val authResult = auth.signInWithCredential(newCredential).await()
                        val newUser = auth.currentUser
                        // TODO: migrate/merge anonymous data to existing user

                        return true
                    }
                }
                else {
                    // TODO: toast
                    Timber.e("REQUEST_SIGN_IN error", response.error)

                    return false
                }
            }

            Timber.w("IdpResponse.fromResultIntent(data) == null")
            return false
        }
    }
}

NOTE: Refer Firebase Auth and Firebase Auth Upgrade From Anonymous Account.

Solution 2: Use LiveData to handle communication between Activity and Fragment

This solution is more complex and involved more plumbling

  • Fragment make sign in request via LiveData to Activity
  • Activity handle the sign in request and notify Fragment via LiveData
  • Fragment listen to sign in request result via LiveData (not using onActivityResult)

NOTE: There might be ways to make it simpler, but I didn’t explore further.

class TestFragment : Fragment() {
    compainion object {
        private const val REQUEST_SIGN_IN = 1
    }

    private val viewModel by viewModels<TestViewModel>()
    private val mainViewModel by activityViewModels<MainViewModel>()

    private fun requestSignIn() {
         mainViewModel.requestSignInEvent.value = Event(Request(requestCode = REQUEST_SIGN_IN, liveData = viewModel.signInEvent))
    }

    private fun init() {
        viewModel.signInEvent.observe(this, Observer { event ->
            event.getIfPending()?.also { res ->
                if (res.isSuccessful) {
                    when(res.data()) {
                        REQUEST_SIGN_IN -> {
                            val auth = FirebaseAuth.getInstance()
                            Timber.d("uid=${auth.currentUser?.uid}")
                        }
                    }
                }
            }
        })
    }
}
class TestViewModel : ViewModel() {
    internal val signInEvent = MutableLiveData<Event<Resource<Int>>>()
}

Activity

class MainActivity : AppCompatActivity() {
    companion object {
        const val REQUEST_SIGN_IN = 1
    }

    private val viewModel by viewModels<MainViewModel>()

    private fun init() {
        viewModel.requestSignInEvent.observe(this, Observer { event ->
            event.getIfPending()?.let {
                startActivityForResult(getAuthIntent(), REQUEST_SIGN_IN)
            }
        })
    }

    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 == Activity.RESULT_OK) {
                val user = FirebaseAuth.getInstance().currentUser
                if (user != null) {
                    Timber.d("name=${user.displayName}, email=${user.email}, uid=${user.uid}")
                    // TODO: init user
                }

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

                        signInRequest?.also { request ->
                            request.liveData.value = Event(Resource(exception = response.error!!))
                        }
                    }
                }
                else {
                    // should not reach here
                }
            }
        }
    }

    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()
            .setAvailableProviders(providers)
            .enableAnonymousUsersAutoUpgrade()
            .build()
    }
}
class MainViewModel : ViewModel() {
    internal val requestSignInEvent = MutableLiveData<Event<Request>>()
}

NOTE: Refer to Resource (return data or exception) and Event (handle UI event).

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.