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).