I am trying to implement sign-in hints in my Android app using Jetpack Compose, but this API requires an Activity to work.
fun showPhoneNumberHint(activity: Activity) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val intent = Auth.CredentialsApi.getHintPickerIntent(apiClient, hintRequest)
val requestCode = 12345
try {
startIntentSenderForResult(activity, intent.intentSender, requestCode, null, 0, 0, 0, null)
} catch (exception: SendIntentException) {
// Error handling
}
}
So I guess that I'll have to pass the Activity object all the way down to the Composable where it's needed, which doesn't seem very clean but it should work.
But now the result of the hint will be received in the Activity's onActivityResult() and I'm not sure what the right way is to get it back to the Composable where it's needed.
Is there some clean/standard/alternative way to do this? Preferably I'd just keep all of this logic contained inside the Composable.
I ended up using rememberLauncherForActivityResult in combination with the ActivityResultContracts.StartIntentSenderForResult() contract to listen for the result. This returns a launcher that can be used to start the intent.
Instead of Auth.CredentialsApi, which requires the deprecated GoogleApiClient, I'm now using the Credentials.getClient. For this I still needed an Activity which I got using LocalContext.current.
val phoneNumberHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
if (it.resultCode != RESULT_OK) {
return#rememberLauncherForActivityResult
}
val credential: Credential? = it.data?.getParcelableExtra(Credential.EXTRA_KEY)
val hintResult = credential?.id
if (hintResult !== null) {
phoneNumber = hintResult
}
}
val context = LocalContext.current
LaunchedEffect(Unit) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val phoneNumberHintIntent = Credentials.getClient(context)
.getHintPickerIntent(hintRequest)
phoneNumberHintLauncher.launch(
IntentSenderRequest.Builder(phoneNumberHintIntent)
.build()
)
}
Activity.onActivityResult() is deprecated and you shouldn't use it even without compose. You should use the Activity Result APIs introduced in AndroidX Activity and Fragment.
The Activity Result APIs provide a registerForActivityResult() API for registering the result callback. registerForActivityResult() takes an ActivityResultContract and an ActivityResultCallback and returns an ActivityResultLauncher which you’ll use to launch the other activity.
Example without compose:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
val selectButton = findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Pass in the mime type you'd like to allow the user to select
// as the input
getContent.launch("image/*")
}
}
In compose use rememberLauncherForActivityResult() instead of registerForActivityResult:
val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
result.value = it
}
Button(onClick = { launcher.launch() }) {
Text(text = "Take a picture")
}
result.value?.let { image ->
Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
The problem with the API you're trying to use is it requires the use of onActivityResult. So, you have no other option but to use it. Try opening an issue on github requesting to update their API.
Related
So I looked for a solution on StackOverflow and read the documentation to see if I was doing something wrong, but I could find out.
I have a list of activities that I fetch from Firestore and display in a LazyColumn. Every activity has a name of an image that I have to get its link from Firebase storage. After I get the link in my variable, I expect the view (AsyncImage) to recompose as the variable is of type StateFlow, but nothing happens.
In my repository file, the code for getting actitivities and the download URL of the image works fine.
In my viewModel, I have two functions to load the activities and the download URL:
viewModel((private val dataSource: ChadHubDigitDataSource): ViewModel() {
...
var activities: LiveData<ActivityResponse>? = null
private var _uri = MutableStateFlow<Uri>(Uri.parse(""))
val uri: StateFlow<Uri>
get() = _uri
fun loadActivities() {
activities = flow {
emit(dataSource.getActivities())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ActivityResponse()
)
}
fun loadImage(imageName: String) {
_uri.value = dataSource.getImage(imageName)
}
Bellow is the code in my compose function:
val activities = _eventsViewModel.activities?.collectAsState()?.value?.activity
LazyColumn {
if (activities != null)
{
items(activities) { item ->
Column(
...
) {
// Loading the URL of the image
_eventsViewModel.loadImage(item.img)
Image(
...
painter = rememberAsyncImagePainter(
ImageRequest.Builder(
LocalContext.current
)
.data(
data = _eventsViewModel.uri.collectAsState().value
)
.apply(
block = fun ImageRequest.Builder.() {
crossfade(false)
placeholder(R.drawable.placeholder)
error(R.drawable.placeholder)
}
).build()
),
...
)
...
}
When the activities are displayed, it's the error image that is displayed even after the value of the variable uri changed in the viewModel.
Do you know a better way I can do such functionality or why my code is not working?
I tried your ImageRequest.Builder block of code, my Firebase Image fetching implementation stopped working, only showing the placeholder and not fetching.
ImageRequest.Builder(context)
.data(url)
.apply (
block = fun ImageRequest.Builder.() {
crossfade(false)
placeholder(R.drawable.placeholder)
error(R.drawable.placeholder)
}
)
.build()
Can you try doing it this way?
ImageRequest.Builder(context)
.data(url)
.crossfade(false)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.build()
I'm trying to integrate One Tap Sign in with Google into my app which I'm building with Jetpack Compose. I'm using startIntentSenderForResult to launch an intent, but now the problem is that I'm unable to receive activity result from my composable function. I'm using rememberLauncherForActivityResult to get the result from an intent but still not getting anywhere. Any solutions?
LoginScreen
#Composable
fun LoginScreen() {
val activity = LocalContext.current as Activity
val activityResult = remember { mutableStateOf<ActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val oneTapClient = Identity.getSignInClient(activity)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
Log.d("LOG", "ActivityResult")
if (result.resultCode == Activity.RESULT_OK) {
activityResult.value = result
}
}
activityResult.value?.let { _ ->
Log.d("LOG", "ActivityResultValue")
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
GoogleButton(
onClick = {
signIn(
activity = activity
)
}
)
}
}
fun signIn(
activity: Activity
) {
val oneTapClient = Identity.getSignInClient(activity)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener(activity) { result ->
try {
startIntentSenderForResult(
activity, result.pendingIntent.intentSender, ONE_TAP_REQ_CODE,
null, 0, 0, 0, null
)
} catch (e: IntentSender.SendIntentException) {
Log.e("LOG", "Couldn't start One Tap UI: ${e.localizedMessage}")
}
}
.addOnFailureListener(activity) { e ->
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
You aren't actually calling launch on the launcher you create, so you would never get a result back there.
Instead of using the StartActivityForResult contract, you need to use the StartIntentSenderForResult contract - that's the one that takes an IntentSender like the one you get back from your beginSignIn method.
This means your code should look like:
#Composable
fun LoginScreen() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
// The user cancelled the login, was it due to an Exception?
if (result.data?.action == StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST) {
val exception: Exception? = result.data?.getSerializableExtra(StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION)
Log.e("LOG", "Couldn't start One Tap UI: ${e?.localizedMessage}")
}
return#rememberLauncherForActivityResult
}
val oneTapClient = Identity.getSignInClient(context)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Create a scope that is automatically cancelled
// if the user closes your app while async work is
// happening
val scope = rememberCoroutineScope()
GoogleButton(
onClick = {
scope.launch {
signIn(
context = context,
launcher = launcher
)
}
}
)
}
}
suspend fun signIn(
context: Context,
launcher: ActivityResultLauncher<IntentSenderRequest>
) {
val oneTapClient = Identity.getSignInClient(context)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
try {
// Use await() from https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services
// Instead of listeners that aren't cleaned up automatically
val result = oneTapClient.beginSignIn(signInRequest).await()
// Now construct the IntentSenderRequest the launcher requires
val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build()
launcher.launch(intentSenderRequest)
} catch (e: Exception) {
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
I am trying to implement sign-in hints in my Android app using Jetpack Compose, but this API requires an Activity to work.
fun showPhoneNumberHint(activity: Activity) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val intent = Auth.CredentialsApi.getHintPickerIntent(apiClient, hintRequest)
val requestCode = 12345
try {
startIntentSenderForResult(activity, intent.intentSender, requestCode, null, 0, 0, 0, null)
} catch (exception: SendIntentException) {
// Error handling
}
}
So I guess that I'll have to pass the Activity object all the way down to the Composable where it's needed, which doesn't seem very clean but it should work.
But now the result of the hint will be received in the Activity's onActivityResult() and I'm not sure what the right way is to get it back to the Composable where it's needed.
Is there some clean/standard/alternative way to do this? Preferably I'd just keep all of this logic contained inside the Composable.
I ended up using rememberLauncherForActivityResult in combination with the ActivityResultContracts.StartIntentSenderForResult() contract to listen for the result. This returns a launcher that can be used to start the intent.
Instead of Auth.CredentialsApi, which requires the deprecated GoogleApiClient, I'm now using the Credentials.getClient. For this I still needed an Activity which I got using LocalContext.current.
val phoneNumberHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
if (it.resultCode != RESULT_OK) {
return#rememberLauncherForActivityResult
}
val credential: Credential? = it.data?.getParcelableExtra(Credential.EXTRA_KEY)
val hintResult = credential?.id
if (hintResult !== null) {
phoneNumber = hintResult
}
}
val context = LocalContext.current
LaunchedEffect(Unit) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val phoneNumberHintIntent = Credentials.getClient(context)
.getHintPickerIntent(hintRequest)
phoneNumberHintLauncher.launch(
IntentSenderRequest.Builder(phoneNumberHintIntent)
.build()
)
}
Activity.onActivityResult() is deprecated and you shouldn't use it even without compose. You should use the Activity Result APIs introduced in AndroidX Activity and Fragment.
The Activity Result APIs provide a registerForActivityResult() API for registering the result callback. registerForActivityResult() takes an ActivityResultContract and an ActivityResultCallback and returns an ActivityResultLauncher which you’ll use to launch the other activity.
Example without compose:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
val selectButton = findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Pass in the mime type you'd like to allow the user to select
// as the input
getContent.launch("image/*")
}
}
In compose use rememberLauncherForActivityResult() instead of registerForActivityResult:
val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
result.value = it
}
Button(onClick = { launcher.launch() }) {
Text(text = "Take a picture")
}
result.value?.let { image ->
Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
The problem with the API you're trying to use is it requires the use of onActivityResult. So, you have no other option but to use it. Try opening an issue on github requesting to update their API.
I'm trying to integrate One Tap Sign in with Google into my app which I'm building with Jetpack Compose. I'm using startIntentSenderForResult to launch an intent, but now the problem is that I'm unable to receive activity result from my composable function. I'm using rememberLauncherForActivityResult to get the result from an intent but still not getting anywhere. Any solutions?
LoginScreen
#Composable
fun LoginScreen() {
val activity = LocalContext.current as Activity
val activityResult = remember { mutableStateOf<ActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val oneTapClient = Identity.getSignInClient(activity)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
Log.d("LOG", "ActivityResult")
if (result.resultCode == Activity.RESULT_OK) {
activityResult.value = result
}
}
activityResult.value?.let { _ ->
Log.d("LOG", "ActivityResultValue")
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
GoogleButton(
onClick = {
signIn(
activity = activity
)
}
)
}
}
fun signIn(
activity: Activity
) {
val oneTapClient = Identity.getSignInClient(activity)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener(activity) { result ->
try {
startIntentSenderForResult(
activity, result.pendingIntent.intentSender, ONE_TAP_REQ_CODE,
null, 0, 0, 0, null
)
} catch (e: IntentSender.SendIntentException) {
Log.e("LOG", "Couldn't start One Tap UI: ${e.localizedMessage}")
}
}
.addOnFailureListener(activity) { e ->
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
You aren't actually calling launch on the launcher you create, so you would never get a result back there.
Instead of using the StartActivityForResult contract, you need to use the StartIntentSenderForResult contract - that's the one that takes an IntentSender like the one you get back from your beginSignIn method.
This means your code should look like:
#Composable
fun LoginScreen() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
// The user cancelled the login, was it due to an Exception?
if (result.data?.action == StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST) {
val exception: Exception? = result.data?.getSerializableExtra(StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION)
Log.e("LOG", "Couldn't start One Tap UI: ${e?.localizedMessage}")
}
return#rememberLauncherForActivityResult
}
val oneTapClient = Identity.getSignInClient(context)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Create a scope that is automatically cancelled
// if the user closes your app while async work is
// happening
val scope = rememberCoroutineScope()
GoogleButton(
onClick = {
scope.launch {
signIn(
context = context,
launcher = launcher
)
}
}
)
}
}
suspend fun signIn(
context: Context,
launcher: ActivityResultLauncher<IntentSenderRequest>
) {
val oneTapClient = Identity.getSignInClient(context)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
try {
// Use await() from https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services
// Instead of listeners that aren't cleaned up automatically
val result = oneTapClient.beginSignIn(signInRequest).await()
// Now construct the IntentSenderRequest the launcher requires
val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build()
launcher.launch(intentSenderRequest)
} catch (e: Exception) {
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
I am trying to implement a CredentialRequest inside my composable, but I can't get the ResolvableApiException handling to work.
I have created an ActivityResultLauncher using rememberLauncherForActivityResult and I'm calling it from the OnCompleteListener to start the PendingIntent as you can see below.
I'd expect this to work, but for some reason I never receive the ActivityResult.
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
if (it.resultCode != RESULT_OK) {
return#rememberLauncherForActivityResult
}
// Handling ActivityResult here
}
val context = LocalContext.current
LaunchedEffect(Unit) {
val credentialsRequest = CredentialRequest.Builder()
.setAccountTypes("https://signin.example.com")
.build()
val credentialsClient = Credentials.getClient(context)
// Read the stored credential if user already signed in before
credentialsClient.request(credentialsRequest).addOnCompleteListener {
val result = try {
it.result?.credential?.id
} catch (exception: Exception) {
val resolvableException = exception.cause as? ResolvableApiException
if (resolvableException === null) {
// Exception not resolvable
return#addOnCompleteListener
}
// User must sign in first
launcher.launch(
IntentSenderRequest.Builder(resolvableException.resolution)
.build()
)
return#addOnCompleteListener
}
// Handling result here
}
}
I'm guessing that this might have to do with the launcher being outdated because of recomposition, but I'm not sure if that's even possible.
What do I have to do to receive the ActivityResult?