Not Getting result from rememberLauncherForActivityResult() in jetpack compose - android

I'm calling StartIntentSenderForResult() but it doesn't get called.
val authResult = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
Log.d("appDebug", "called!!!") // not get called
}
oneTapClient.beginSignIn(signUpRequest)
.addOnSuccessListener(activity) { result ->
try {
// Calling here for result
authResult.launch(
IntentSenderRequest
.Builder(result.pendingIntent.intentSender)
.build()
)
} catch (e: IntentSender.SendIntentException) {
Log.d("appDebug", "CATCH : ${e.localizedMessage}")
}
}
.addOnFailureListener(activity) { e ->
Log.d("appDebug", "FAILED : ${e.localizedMessage}")
}

If someone having same issue then just use this composable instead of rememberLauncherForActivityResult().
Thanks to #RĂ³bert Nagy
Ref: https://stackoverflow.com/a/65323208/15301088
I removed some deprecated codes from original post now it works fine for me.
#Composable
fun <I, O> registerForActivityResult(
contract: ActivityResultContract<I, O>,
onResult: (O) -> Unit
): ActivityResultLauncher<I> {
val owner = LocalContext.current as ActivityResultRegistryOwner
val activityResultRegistry = owner.activityResultRegistry
// Tracking current onResult listener
val currentOnResult = rememberUpdatedState(onResult)
// Only need to be unique and consistent across configuration changes.
val key = remember { UUID.randomUUID().toString() }
DisposableEffect(activityResultRegistry, key, contract) {
onDispose {
realLauncher.unregister()
}
}
return realLauncher
}
for e.g.
val registerActivityResult = registerForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
// handle your response
}
// just call launch and pass the contract
registerActivityResult.launch(/*Your Contract*/)

Related

Stop collecting Flow in ViewModel when app in background

Need to collect flow in ViewModel and after some data modification, the UI is updated using _batteryProfileState.
Inside compose I'm collecting states like this
val batteryProfile by viewModel.batteryProfileState.collectAsStateWithLifecycle()
batteryProfile.voltage
In ViewModel:
private val _batteryProfileState = MutableStateFlow(BatteryProfileState())
val batteryProfileState = _batteryProfileState.asStateFlow()
private fun getBatteryProfileData() {
viewModelScope.launch {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
}
The problem is when I put my app in the background the _batteryProfile(Unit).collect does not stop collecting while in UI batteryProfile.voltage stop updating UI which is correct behavior as I have used collectAsStateWithLifecycle() for UI.
But I have no idea how to achieve the same behavior for ViewModel.
In ViewModel I have used stateIn operator and access data like below everything working fine now:
val batteryProfileState = _batteryProfile(Unit).map { result ->
when(result) {
is Result.Success -> {
BatteryProfileState(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit.unit)
?: _context.getString(R.string.msg_unknown),
)
}
is Result.Error -> {
BatteryProfileState(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}.stateIn(viewModelScope, WhileViewSubscribed, BatteryProfileState())
collecting data in composing will be the same
Explanation: WhileViewSubscribed Stops updating data while the app is in the background for more than 5 seconds.
val WhileViewSubscribed = SharingStarted.WhileSubscribed(5000)
You can try to define getBatteryProfileData() as suspend fun:
suspend fun getBatteryProfileData() {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
And than in your composable define scope:
scope = rememberCoroutineScope()
scope.launch {
yourviewmodel.getBatteryProfileData()
}
And I think you can move suspend fun getBatteryProfileData() out of ViewModel class...

Android Kotlin Coroutines - Call Api Every X Seconds on Background

I've build clean architectured app (with mvvm, use cases, compose). I've a CoinListViewModel for list all crypto coins by using CoinPaprika API. It is like;
#HiltViewModel
class CoinListViewModel #Inject constructor(
private val getCoinsUseCase: GetCoinsUseCase
) : ViewModel() {
private val _state = mutableStateOf(CoinListState())
val state: State<CoinListState> = _state
init {
getCoins()
}
private fun getCoins() {
getCoinsUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = CoinListState(coins = result.data ?: emptyList())
}
is Resource.Error -> {
_state.value = CoinListState(
error = result.message ?: "An unexpected error occured"
)
}
is Resource.Loading -> {
_state.value = CoinListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
And this viewmodel is used in my CoinListScreen like;
#Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) { coin ->
CoinListItem(
coin = coin,
onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${coin.id}")
}
)
}
}
if(state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if(state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
And that is my GetCoinsUseCase:
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
try {
emit(Resource.Loading<List<Coin>>())
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
}
}
I have 2 questions;
How can I make this API call every 3 seconds ?
How can I continue to do API call on background in phone ?
You could add a loop with a delay in your use case:
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
while (currentCoroutineContext().isActive) {
try {
emit(Resource.Loading<List<Coin>>())
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
// Wait until next request
delay(3000)
}
}
}
You could also move the loop and delay call around depending on the precise functionality you are looking for. For example, to stop once there is an error you could do
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
operator fun invoke(): Flow<Resource<List<Coin>>> = flow {
emit(Resource.Loading<List<Coin>>())
try {
while (currentCoroutineContext().isActive) {
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.Success<List<Coin>>(coins))
// Wait until next request
delay(3000)
}
} catch(e: HttpException) {
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "An unexpected error occured"))
} catch(e: IOException){
emit(Resource.Error<List<Coin>>("Couldn't reach to server. Check your internet connection."))
}
}
}
This should already naturally "run in the background" as long as your app is running and you are not cancelling the coroutine manually when going in the background.
The way to repeat a task in Jetpack Compose is
LaunchedEffect(Unit) {
while(true) {
vm.someMethod()
delay(3000)
}
}
The someMethod is the method that fetches the coin data.

#Composable invocations can only happen from the context of a #Composable function(Google SignIn) [duplicate]

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())
}
}

One Tap Sign in - Activity Result with Jetpack Compose

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())
}
}

How to handle ResolvableApiException in Jetpack Compose?

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?

Categories

Resources