18

NavController can't pop programmatically the latest @Composable in the stack. I.e. popBackStack() doesn't work if it's a root page. So the application can be closed by tap on "Close" button view and only hardware Back key allows to leave application.

Example: Activity

class AppActivity : ComponentActivity() {
    override fun onCreate(state: Bundle?) {
        super.onCreate(state)
        setContent {
            val controller = rememberNavController()
            NavHost(controller, startDestination = HOME) {
                composable(HOME) { HomePage(controller) }
                ...
            }
        }
    }
}

HomePage.kt

@Composable
fun HomePage(controller: NavController) {
    Button(onClick = {
        controller.popBackStack()
    }) {
        Text("Exit")
    }
}

Question:

How to close the app in onClick handler if Compose Navigation is used.

Sergey Krivenkov
  • 579
  • 1
  • 6
  • 14
  • I think you are looking for this: https://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically In Compose you have to pass the activity through to the composable that will use it. – 2jan222 May 05 '21 at 13:10
  • We do not know how the framework uses and cashes the @Composable entities. I mean, we do not manage its lifecycle, so there is risk of the Context leak. – Sergey Krivenkov May 05 '21 at 13:20
  • Posted an issue/question to Google tracker https://issuetracker.google.com/issues/187134652 – Sergey Krivenkov May 05 '21 at 13:24

3 Answers3

59

You can use this:

@Composable
fun HomePage(controller: NavController) {
    val activity = (LocalContext.current as? Activity)
    Button(onClick = {
        activity?.finish()
    }) {
        Text("Exit")
    }
}
nglauber
  • 18,674
  • 6
  • 70
  • 75
  • 3
    After few months in production, I can say this solution works good. Thank you. – Sergey Krivenkov Sep 01 '21 at 18:02
  • 12
    Getting activity inside composable is not a good idea. It's better to flow up the event to ViewModel, subscribe to it in the activity and then call finish there – Dmytro Marchuk Sep 23 '21 at 09:05
  • 5
    ..or even better: call onClick = { onExitClick() } which is passed as a lambda to the function fun HomePage(onExitClick: () -> Unit) {} – Gleichmut Dec 24 '21 at 11:47
  • Casting local context to Activity is unsafe. Passing a lambda to compose and trigger back to the hosted activity seems to be the best solution. – Bao Le Feb 24 '22 at 07:34
  • 2
    I agree with you guys that pass a lambda might be a better option and of course this solution only works if you're following single activity model.. But here it goes an honest question: Do you guys see a scenario where the local context is not an Activity? – nglauber Feb 25 '22 at 12:40
  • @nglauber Yes, it might be a `Fragment`. – Minas Mina Jun 30 '22 at 12:03
  • 1
    @MinasMina a `Fragment` **is not** a `Context` (since it does not extends from `Context`) a `Fragment` **has** a `Context`. https://stackoverflow.com/questions/36162714/what-is-the-difference-between-is-a-relationship-and-has-a-relationship-in – nglauber Jun 30 '22 at 12:21
  • @nglauber You are correct. :) – Minas Mina Jun 30 '22 at 12:32
  • I have honestly never come into a situation where the composable context isn't an activity. They always start with an activity. Just don't cast it to a specific activity – Racka98 Jul 19 '22 at 08:35
  • you may get class cast exception – shubham chouhan Oct 28 '22 at 13:45
  • yes my app really closes when I use this approach but it closes due to java.lang.ClassCastException: android.view.ContextThemeWrapper cannot be cast to android.app.Activity, so it's better to use the approach from @DmytroMarchuk and yes I cast the context to a normal activity like this: (context as Activity).finish() – easy_breezy Apr 29 '23 at 00:31
4
@AndroidEntryPoint
class MainActivity:AppCompatActivity() {

  @ExperimentalAnimatedInsets
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, false)
    val activityKiller: () -> Unit = {
      this.finish()
    }

    setContent {
      MainAppEntry(activityKiller = activityKiller)
    }
  }
}

@Composable
fun MainAppEntry(activityKiller: () -> Unit) {
  val mainViewModel: MainViewModel = hiltViewModel<MainViewModel>()
  //mutableStateOf .......
  var isKillRequested = mainViewModel.mainActivityState.isActivityFinishRequested
  if (isKillRequested) {
    activityKiller()
    return
  }

  Column(Modifier.fillMaxSize()) {
    TextButton(onClick = {
      mainViewModel.smuaActivityState.requestActivityFinish()
    }) {
      Text(text = "Kill app")
    }
  }
}
Jared Burrows
  • 54,294
  • 25
  • 151
  • 185
WAMii
  • 51
  • 1
2

finishAffinity() closes your app and keeps it in the Recent apps screen

finishAndRemoveTask() closes your app and removes it from the Recent apps screen

wbk727
  • 8,017
  • 12
  • 61
  • 125