Showing permissions rationale in dialog using accompanist permissions - android

I am struggling with figure out how to close a dialog launched to explain denied permissions.
Using accompanist to ask for permissions:
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
locationPermissionState.launchPermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
})
In the same composable I launch a dialog depending on denied permissions:
when {
locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// Dialog to explain to users the permission
)
}
!locationPermissionState.status.isGranted && !locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// dialog to tell user they need to go to settings to enable
)
}
}
I am stuck figuring out how to close on the dialog when the user click an OK button.
I have tried to use another state that survives recomposition:
val openDialog by remember { mutableStateOf(false) }
....
// if permission state denied
openDialog = true
....
// then in dialog ok
openDialog = false
However when doing that and changing the state of openDialog the function is recomposed. Which just means when I check the permissions state again its still the same and my dialog opens again.

For a general solution handling location permissions requests in Compose you need to keep track of performed permissions requests. Android's permissions request system works in an iterative fashion, and at certain points in this iteration there is no state change to observe and to act upon besides the iteration count. The current Accompanist 0.24.12-rc has a permissions request callback that you can use to do this. Then you can structure your declarative Compose code to take action based on previously observed and saved iteration counts and the current iteration count. (And you could try to abstract it further, creating dedicated states based on iteration count values and differences, but that's not really necessary to make it work; my hope would be that someone will add this in Accompanist at some point.)
For example:
val locationPermissions: List<String> = listOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)
#Stable
interface LocationPermissionsState : MultiplePermissionsState {
/**
* Supplies a well-defined measure of time/progression
*/
val requestCount: UInt
}
#Composable
fun rememberLocationPermissionsState(): LocationPermissionsState {
val requestCountState: MutableState<UInt> = remember { mutableStateOf(0u) }
val multiplePermissionsState: MultiplePermissionsState =
rememberMultiplePermissionsState(locationPermissions) {
if (it.isEmpty()) {
// BUG in accompanist-permissions library upon configuration change
return#rememberMultiplePermissionsState
}
requestCountState.value++
}
return object : LocationPermissionsState, MultiplePermissionsState by multiplePermissionsState {
override val requestCount: UInt by requestCountState
}
}
You can see this solution with a little more context at https://github.com/google/accompanist/issues/819

Related

How to handle callbacks in Jetpack compose?

I am migrating my multiple activity app to single activity app.
In the activity I am observing a live data from view model. When the observable triggers, I start a payment activity from a third party SDK as shown below.
onCreate() {
viewmodel.orderCreation.observe {
thirdpartysdk.startPaymentWithThisOrder(context)
}
}
onActivityResult() {
// use payment result
}
As I will be using a Composable now,
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
val orderCreation by viewmodel.orderCreation.observeAsState()
// How to use order creation once here to call onOrderCreated here only once as composable is called again and again
}
Here's my suggestion:
In your viewmodel, create a function to reset your orderCreation. And another field + function to store the payment result.
Something like:
fun resetOrderCreation() {
_orderCreation.value = null
}
fun paymentResult(value: SomeType) {
_paymentResult.value = value
}
Now, in your composable, you can do the following:
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
// 1
val orderCreation by viewmodel.orderCreation.observeAsState()
var paymentResult by viewmodel.paymentResult.observeAsState()
// 2
val launcher = rememberLauncherForActivityResult(
PaymentActivityResultContract()
) { result ->
viewModel.paymentResult(result)
}
...
// 3
LaunchedEffect(orderCreation) {
if (orderCreation != null) {
launcher.launch()
viewModel.resetOrderCreation()
}
}
// 4
if (paymentStatus != null) {
// Show some UI showing the payment status
}
}
Explaining the code:
I'm assuming that you're using LiveData. But I really suggest you move to StateFlow instead. See more here.
You will probably need to write a ActivityResultContact to your third party lib. I wrote a post about (it's in Portuguese, but I think you can get the idea translating it to English).
As soon the orderCreation has changed, the LaunchedEffect block will run, then you can start the third party activity using launcher.launch() (the parameters for this call are defined in your ActivityResultContract).
Finally, when the payment status changed, you can show something different to the user.

Android Permissions Helper Functions

I have an activity that requires camera permission.
this activity can be called from several user configurable places in the app.
The rationale dialog and permission dialog themselves should be shown before the activity opens.
right now I am trying to handle these dialogs in some kind of extension function.
fun handlePermissions(context: Context, required_permissions: Array<String>, activity: FragmentActivity?, fragment: Fragment?): Boolean {
var isGranted = allPermissionsGranted(context, required_permissions)
if (!isGranted) {
//null here is where I used to pass my listener which was the calling fragment previously that implemented an interface
val dialog = DialogPermissionFragment(null, DialogPermissionFragment.PermissionType.QR)
activity?.supportFragmentManager?.let { dialog.show(it, "") }
//get result from dialog? how?
//if accepted launch actual permission request
fragment?.registerForActivityResult(ActivityResultContracts.RequestPermission()) { success ->
isGranted = success
}?.launch(android.Manifest.permission.CAMERA)
}
return isGranted
}
But I am having trouble to get the dialog results back from the rationale/explanation dialog.
My research until now brought me to maybe using a higher order function, to pass a function variable to the dialog fragment that returns a Boolean value to the helper function. But I am absolutely unsure if thats the right thing.
I thought using my own code would be cleaner and less overhead, could I achieve this easier when using a framework like eazy-permissions? (is Dexter still recommendable since its no longer under development)
is that even a viable thing I am trying to achieve here?
One approach that I've implemented and seems viable to use is this:
Class PermissionsHelper
class PermissionsHelper(private val activity: Activity) {
fun requestPermissionsFromDevice(
arrayPermissions: Array<String>, permissionsResultInterface: PermissionsResultInterface
) {
setPermissionResultInterface(permissionsResultInterface)
getMyPermissionRequestActivity().launch(arrayPermissions)
}
fun checkPermissionsFromDevice(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
Class PermissionsResultInterface to the helper class be able to communicate with the activity.
interface PermissionsResultInterface {
fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>)
}
Usage with this approach to remove all files from app directory:
private fun clearFiles(firstCall: Boolean = false) {
if (verifyStoragePermissions(firstCall)) {
val dir = File(getExternalFilesDir(null).toString())
removeFileOrDirectory(dir)
Toast.makeText(
applicationContext,
resources.getString(R.string.done),
Toast.LENGTH_SHORT
).show()
}
}
private fun verifyStoragePermissions(firstCall: Boolean = false): Boolean {
val arrayListPermissions = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
for (permission in arrayListPermissions) {
if (!PermissionsHelper(this).checkPermissionsFromDevice(permission)) {
if (firstCall) PermissionsHelper(this)
.requestPermissionsFromDevice(arrayListPermissions, this)
else PermissionsDialogs(this).showPermissionsErrorDialog()
return false
}
}
return true
}
override fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>) {
clearFiles()
}
With this approach you are able to call the permissions helper and using the result interface, after each of the answers from user, decide wether you still need to make a call for permissions or show a dialog to him.
If you need any help don't hesitate to contact me.

Why the view keeps flashing when using jetpack navigation with Compose?

I have a login scren and when the login is successful and the view model updates the mutable state variable, my expectation is that a new composable function is called to show a new screen and the login one is removed. The problem is that when the new screen (aka Screen.AccountsScreen) is shown, its content keeps flashing/redrawing and same thing happen with the login form which never gets destroyed (I know this because the log message 'Recomponing...' gets printed endless). I assume this happens because the isLoginSuccessful state is always true. It seems I need an event that can be consumed only once, is this correct? If so, how can I do that?
LoginViewModel.kt
#HiltViewModel
class LoginViewModel #Inject constructor() : ViewModel() {
var isLoginSuccessful by mutableStateOf(false)
var errorMessage by mutableStateOf("")
fun onLoginClick(email: String, password:String) {
errorMessage = ""
if (credentialsValid(email, password)) {
isLoginSuccessful = true
} else {
errorMessage = "Email or password invalid"
isLoginSuccessful = false
}
}
}
LoginScreen.kt
#Composable
fun loginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel()
) {
println("Recomponing...")
// Here gos the code for the login form
if (viewModel.isLoginSuccessful) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
}
Composite navigation recomposes both disappearing and appearing views during transition. This is the expected behavior.
You're calling navigate on each recomposition. Your problem lays in these lines:
if (viewModel.isLoginSuccessful) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
You shouldn't change state directly from view builders. In this case LaunchedEffect should be used:
if (viewModel.isLoginSuccessful) {
LaunchedEffect(Unit) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
}
Check out more in side effects documentation.
For me, I see flicker because the activity background is white, but I am on dark mode.
Change your app theme to daynight, try adding
implementation 'com.google.android.material:material:1.5.0'
and change your theme to
<style name="Theme.MyStockApp" parent="Theme.Material3.DayNight.NoActionBar" />

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

Lifecycle Aware Component that needs Permission

Let's say I have a component that needs to be initialized and destroyed depending on the lifecycle of the activity. However, this component needs to be granted permissions from the user first. What is the best way to do that?
Do I have to subscribe to the same observer in two different positions or there is a better way to do it without code duplication?
You can implement life cycle aware class encapsulates permission sensitive work:
class MyLifecycleAware {
private var blObject: Any? = null
/**
* Manually call this method when permission granted
*/
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun init() = withPermission {
// code will be invoked only if permission was granted
blObject = TODO("Initialize business logic")
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun destroy() {
blObject?.destroy()
blObject = null
}
/**
* Wrap any permission sensitive actions with this check
*/
private inline fun withPermission(action: () -> Unit) {
val permissionGranted = TODO("Check permission granted")
if (permissionGranted)
action()
}
}

Categories

Resources