How to set Double back press Exit in Jetpack Compose? - kotlin

There are some tutorials on YouTube about how to set double back press exit in XML android, but most of them are in JAVA and None of them are in Jetpack Compose.
So how can we set that Double back press in Jetpack Compose?
I mean that thing that ask us in a Toast to press back again if we are sure to Exit. Thanks for help

This sample shows Toast on first touch and waits for 2 seconds to touch again to exit app otherwise goes back to Idle state.
sealed class BackPress {
object Idle : BackPress()
object InitialTouch : BackPress()
}
#Composable
private fun BackPressSample() {
var showToast by remember { mutableStateOf(false) }
var backPressState by remember { mutableStateOf<BackPress>(BackPress.Idle) }
val context = LocalContext.current
if(showToast){
Toast.makeText(context, "Press again to exit", Toast.LENGTH_SHORT).show()
showToast= false
}
LaunchedEffect(key1 = backPressState) {
if (backPressState == BackPress.InitialTouch) {
delay(2000)
backPressState = BackPress.Idle
}
}
BackHandler(backPressState == BackPress.Idle) {
backPressState = BackPress.InitialTouch
showToast = true
}
}

Related

Kotlin app using camera does not return picture if camera is never launched before

I've stuck for a while with annoying issue with emulator's camera in my app. (The same is repro on a real device)
The camera is used from a dialog fragment to take a picture that later will be uploaded to remote host.
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
makeImageWithCamera()
} else {
dismiss()
}
}
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { isSuccess ->
if (isSuccess) {
path?.let { path ->
viewModel.setPhoto(path = path)
}
}
dismiss()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeFlow(viewModel.action) { dismiss() }
with(binding) {
buttonCamera.setOnClickListener { useCamera() }
buttonCancel.setOnClickListener { dismiss() }
}
}
private fun useCamera() {
val isGranted = ContextCompat.checkSelfPermission(requireContext(), PERM_CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (!isGranted) {
cameraPermissionLauncher.launch(PERM_CAMERA)
} else {
makeImageWithCamera()
}
}
private fun makeImageWithCamera() {
try {
val photoFileName = "photo${id}"
val photosDir = requireContext()
.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val photoFile = File.createTempFile(photoFileName, ".jpg", photosDir)
val photoUri: Uri = FileProvider.getUriForFile(
requireContext(),
"app.provider",
photoFile
)
path = photoFile.absolutePath
cameraLauncher.launch(photoUri)
} catch (err : IOException) {
Log.e(TAG, err.stackTraceToString())
}
}
The issue happens only in situation when camera itself is never launched on a device:
Inside my app I press camera button
Receive a request for camera usage permission from my app and accept it.
Camera app is starting and requests its own permission to access geolocation (it wants to add location-based tags to photos) - at this point it is irrelevant wether to accept or deny it.
Camera enters the scene. Now if I press shot button it makes a photo and stays in the scene allowing to make infinite amount of photos, as if I've launched Camera app in regular way.
If I use back button to return to my app - nothing have change at all, dialog is just dismisses.
If I launch the same second time, or just launch Camera app itself and accept/deny permissions request from it, all is fixed. Camera takes exactly one shot and allows me to accept it, after that it comes back to my app and processes received photo.
So while for the user experience this behaviour can be just annoying, it with a high probability makes the Firebase robotests to fail on this point.
What could be the possible root of a problem?

Rapid clicks navigation problem with bottom sheet in Jetpack Compose

I have a button from which on click I navigate to bottom sheet, the problem come when I click this button multiple times very fast, I am using accompanist library for navigation, as you see below, here is my ModalBottomSheetLayout and Scaffold
val navController = rememberAnimatedNavController()
val bottomSheetNavigator = rememberFullScreenBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator
) {
Scaffold(
scaffoldState = rememberScaffoldState(snackbarHostState = snackState),
content = { paddingValues ->
AnimatedNavHost(
navController = navController,
startDestination = "bottomsheet",
) {
bottomSheet("bottomsheet") {
CancelOrderBottomSheetUi()
}
}
},
bottomBar = {
BuildBottomBar(navController)
}
)
}
And this is my bottom sheet navigator
fun rememberFullScreenBottomSheetNavigator(
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
skipHalfExpanded: Boolean = true,
): BottomSheetNavigator {
val sheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden,
animationSpec
)
if (skipHalfExpanded) {
LaunchedEffect(sheetState) {
snapshotFlow { sheetState.isAnimationRunning }
.collect {
with(sheetState) {
val isOpening =
currentValue == ModalBottomSheetValue.Hidden && targetValue == ModalBottomSheetValue.HalfExpanded
val isClosing =
currentValue == ModalBottomSheetValue.Expanded && targetValue == ModalBottomSheetValue.HalfExpanded
when {
isOpening -> animateTo(ModalBottomSheetValue.Expanded)
isClosing -> animateTo(ModalBottomSheetValue.Hidden)
}
}
}
}
}
return remember(sheetState) {
BottomSheetNavigator(sheetState = sheetState)
}
}
I have one screen where there is button, and onClick of this button I want to navigate from screen 1(where the button is clicked) to screen 2(where bottom sheet is) both of this screens have different viewModels.
This is where I click cancel button
onCancel = {
viewModel.cancelButton()
}
and in VM
fun cancelButton() {
viewModelScope.launch {
navigationDispatcher.navigateTo("bottomsheet")
}
}
So the problem is just when I spam button very fast the bottom sheet appears almost on full size and then it collapse again, if I do not spam it, it appears like normal bottom sheet and I cannot click the button on background again, this bug happens only if I spam it.
PS: I tried with button bouncer, with some delays with if cycle and so on, but noting of this worked, I think I am missing something essential for navigation itself.

Why can't I cancel a Flow using Job.cancel() when I start it for two times?

In the App, I click "Start" button to display new information every 100ms, then click "Stop" button to stop display new information. it's work as my expection.
But If I click "Start" button for two times, then I click "Stop" button to hope to stop display new information, but the new information keep displaying, why? It seems that myJob.cancel() doesn't work.
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("Hello")
private lateinit var myJob: Job
private fun soundDbFlow(period: Long = 100) = flow {
while (true) {
emit(soundDb())
delay(period)
}
}
fun calCurrentAsynNew() {
myJob = viewModelScope.launch(Dispatchers.IO) {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
fun cancelJob(){
myJob.cancel()
}
...
}
#Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}
When you call handleMeter.calCurrentAsynNew() it starts a new coroutine and assigns the returned Job to myJob. When you click Start second time, it again starts a new coroutine and updates the value of myJob to this new Job instance but this doesn't cancel the previous coroutine, it's still running.
If you want to cancel previous coroutine when calCurrentAsynNew() is called, you will have to manually cancel it using myJob.cancel()
fun calCurrentAsynNew() {
if(::myJob.isInitialized)
myJob.cancel()
myJob = viewModelScope.launch {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
Instead of using lateinit var myJob: Job you could have also used var myJob: Job? = null and then to cancel it, use myJob?.cancel()
Dispatchers.IO is also not required in this case.

Compose for Desktop LazyRow/LazyColumn not scrolling with mouse click

For some reason LazyColumns do not scroll with a mouse click and move gesture. It only works with the mouse wheel so far. For LazyRows it is also not possible to scroll with the mouse wheel. It seems that lazy row is useless for Compose for desktop.
Are there any possiblities to enable a click and move gesture on LazyRow and LazyColum. And if not is it at least possible to enable to scroll through a LazyRow with the mouse wheel?
I used this minimal reproducible example to test the scrolling
#Composable
#Preview
fun App() {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme {
LazyRow(modifier = Modifier.fillMaxSize()) {
repeat(100) {
item {
Text("Test Test Test Test $it ")
}
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
This is the intended behavior.
All scrollable components (including LazyColumn) work (for now) only with mouse wheel scroll events on the desktop.The scrollable components should not respond to mouse drag/move events.
Here's a basic example of how you can add drag support to your components:
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
state = scrollState,
modifier = Modifier
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
coroutineScope.launch {
scrollState.scrollBy(-delta)
}
},
)
) {
items(100) {
Text("Test Test Test Test $it")
}
}

Avoid fragment recreation when opening from notification navigation component

I want when I click on a notification to open a fragment and not recreate it. I am using navigation component and using NavDeepLinkBuilder
val pendingIntent = NavDeepLinkBuilder(this)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.workouts_graph)
.setDestination(R.id.workoutFragment)
.createPendingIntent()
My case is I have a fragment and when you exit the app, there is a notification which when you click on it, it should return you to that same fragment. Problem is every time i click on it it's creating this fragment again, I don't want to be recreated.
I had the same issue. Looks like there is not an option to use the NavDeepLinkBuilder without clearing the stack according to the documentation
I'm not sure the exact nature of your action, but I'll make two assumptions:
You pass the destination id to your MainActivity to navigate.
Your MainActivity is using ViewBinding and has a NavHostFragment
You will have to create the pending intent like:
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("destination", R.id.workoutFragment)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
And in your MainActivity, you can handle both cases (app was already open, app was not already open)
override fun onStart() {
super.onStart()
// called when application was not open
intent?.let { processIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// called when application was open
intent?.let { processIntent(it) }
}
private fun processIntent(intent: Intent) {
intent.extras?.getInt("destination")?.let {
intent.removeExtra("destination")
binding.navHostFragment.findNavController().navigate(it)
}
}