How to get activity in compose
Asked Answered
O

10

63

Is there a way to get current activity in compose function?

@Composable
fun CameraPreviewScreen() {
    val context = ContextAmbient.current
    if (ActivityCompat.checkSelfPermission(
            context,
            Manifest.permission.CAMERA
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        ActivityCompat.requestPermissions(
            this, MainActivity.REQUIRED_PERMISSIONS, MainActivity.REQUEST_CODE_PERMISSIONS  // get activity for `this`
        )
        return
    }
}
Odie answered 4/11, 2020 at 6:55 Comment(0)
C
55

While the previous answer (which is ContextWrapper-aware) is indeed the correct one, I'd like to provide a more idiomatic implementation to copy-paste.

fun Context.getActivity(): ComponentActivity? = when (this) {
    is ComponentActivity -> this
    is ContextWrapper -> baseContext.getActivity()
    else -> null
}

As ContextWrappers can't possibly wrap each other significant number of times, recursion is fine here.

Chastitychasuble answered 17/7, 2021 at 18:17 Comment(4)
Just the cherry on the top in the form of two suggestions on this answer, which I really like: 1. mark it with tailrec 2. it might be written as a val extension: private tailrec fun Context.getActivity(): AppCompatActivity? = when (this) { is AppCompatActivity -> this is ContextWrapper -> baseContext.getActivity() else -> null } val Context.activity: AppCompatActivity? get() = getActivity()Laird
I feel really dumb but this wasn't working for me because my activity was Activity instead of AppCompatActivity. So make sure you have the right type.Selfreliance
is ContextWrapper -> baseContext.getActivity() is a recursive call.Argueta
In my case Context was of type android.app.ContextImpl, which was a dead end for me, so I ended up just passing the activity around.Khanna
F
47

New answer:

Accompanist library uses internally the findActivity method to get the activity. I've tweaked it to follow a similar naming and behavior as other Kotlin methods:

fun Context.getActivityOrNull(): Activity? {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    
    return null
}

Old answer that may crash:

You can get the activity from your composables casting the context (I haven't found a single case where the context wasn't the activity). However, as Jim mentioned, is not a good practice to do so.

val activity = LocalContext.current as Activity

Personally, I use it when I'm just playing around some code that requires the activity (permissions is a good example) but once I've got it working, I simply move it to the activity and use parameters/callback.

Edit: As mentioned in the comments, using this in production code can be dangerous, as it can crash because current is a context wrapper, my suggestion is mostly for testing code.

Frequentative answered 10/12, 2020 at 23:39 Comment(4)
This might crash. Context is not necessarily an Activity in this case, it can be a ContextWrapper.Knocker
Compose initializes ComposeView with activity(this) in setContent, gets context from it, and provides it as LocalContext. So LocalContext.current as Activity is safe.Jiggermast
It's not. Even if it is the case right now, nothing prevents them from changing it to a ContextWrapper later. The correct way to get the Activity from Context is demonstrated below by Rajeev ShettyKnocker
@JiSungbin Composables can be initialized outside of the Activity scope, such as Fragment. It isn't and shouldn't be safe to retrieve an activity like this.Myopic
T
25

To get the context

val context = LocalContext.current

Then get activity using the context. Create an extension function, and call this extension function with your context like context.getActivity().

fun Context.getActivity(): AppCompatActivity? {
  var currentContext = this
  while (currentContext is ContextWrapper) {
       if (currentContext is AppCompatActivity) {
            return currentContext
       }
       currentContext = currentContext.baseContext
  }
  return null
}
Tribalism answered 10/6, 2021 at 18:57 Comment(0)
S
17
  • I actually found this really cool extension function inside the accompanist library to do this:
internal fun Context.findActivity(): Activity {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    throw IllegalStateException("Permissions should be called in the context of an Activity")
}
  • which gets used inside a composable function like this:

@Composable
fun composableFunc(){
    
    val context = LocalContext.current
    val activity = context.findActivity()

}

Snafu answered 6/12, 2022 at 1:7 Comment(1)
i believe accompanist is deprecated and now merged into androidx -- with modifications. i could not find any instructions on migration, so i just recreated the functionality from the new content in androidx.Vaccine
J
12

Rather than casting the Context to an Activity, you can safely use it by creating a LocalActivity.

val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
    noLocalProvidedFor("LocalActivity")
}

private fun noLocalProvidedFor(name: String): Nothing {
    error("CompositionLocal $name not present")
}

Usage:

CompositionLocalProvider(LocalActivity provides this) {
   val activity = LocalActivity.current
   // your content
} 
Jiggermast answered 21/3, 2022 at 5:47 Comment(3)
Wow this is really nice. I presume I don't have to worry about this Activity reference being held past its own lifecycle?Ocher
@JayBobzin Sorry for the late reply. Compose is tracking the lifecycle of the activity used in setContent under the hood. If the activity is destroyed, compose removes all data from the Composable and the CompositionLocals.Jiggermast
Not good solution as in previews you are not able to get it and they fail.Kimball
G
6

This extention function allows you to specify activity you want to get:

inline fun <reified Activity : ComponentActivity> Context.getActivity(): Activity? {
    return when (this) {
        is Activity -> this
        else -> {
            var context = this
            while (context is ContextWrapper) {
                context = context.baseContext
                if (context is Activity) return context
            }
            null
        }
    }
}

Example:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { HomeScreen() }
    }
}

@Composable
fun HomeScreen() {
    val activity = LocalContext.current.getActivity<MainActivity>()
}
Gestapo answered 4/6, 2022 at 15:51 Comment(0)
S
3

Getting an activity from within a Composable function is considered a bad practice, as your composables should not be tightly coupled with the rest of your app. Among other things, a tight coupling will prevent you from unit-testing your composable and generally make reuse harder.

Looking at your code, it looks like you are requesting permissions from within the composable. Again, this is not something you want to be doing inside your composable, as composable functions can run as often as every frame, which means you would keep calling that function every frame.

Instead, setup your camera permissions in your activity, and pass down (via parameters) any information that is needed by your composable in order to render pixels.

Solitude answered 4/11, 2020 at 16:36 Comment(3)
Thanks for the advise, but this doesn't answer the question!Tranche
While you're right this doesn't answer the questionDuluth
when using MVVM and LiveData, an alternative to needing to gett the Context in the Composable is to observe your view state changes in your activity (as well as the composable). Change the pixels in the composable, and call the system methods in the activity.Potemkin
B
3

For requesting runtime permission in Jetpack Compose use Accompanist library: https://github.com/google/accompanist/tree/main/permissions

Usage example from docs:

@Composable
private fun FeatureThatRequiresCameraPermission(
    navigateToSettingsScreen: () -> Unit
) {
    // Track if the user doesn't want to see the rationale any more.
    var doNotShowRationale by rememberSaveable { mutableStateOf(false) }

    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    PermissionRequired(
        permissionState = cameraPermissionState,
        permissionNotGrantedContent = {
            if (doNotShowRationale) {
                Text("Feature not available")
            } else {
                Column {
                    Text("The camera is important for this app. Please grant the permission.")
                    Spacer(modifier = Modifier.height(8.dp))
                    Row {
                        Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
                            Text("Ok!")
                        }
                        Spacer(Modifier.width(8.dp))
                        Button(onClick = { doNotShowRationale = true }) {
                            Text("Nope")
                        }
                    }
                }
            }
        },
        permissionNotAvailableContent = {
            Column {
                Text(
                    "Camera permission denied. See this FAQ with information about why we " +
                        "need this permission. Please, grant us access on the Settings screen."
                )
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = navigateToSettingsScreen) {
                    Text("Open Settings")
                }
            }
        }
    ) {
        Text("Camera permission Granted")
    }
}

Also, if you check the source, you will find out, that Google uses same workaround as provided by Rajeev answer, so Jim's answer about bad practice is somewhat disputable.

Burgoo answered 28/9, 2021 at 12:42 Comment(0)
P
1

For Compose, these could be written as tailrec functions to find a ComponentActivity:

tailrec fun Context.findActivity(): ComponentActivity? = when (this) {
    is ComponentActivity -> {
        this
    }

    is ContextWrapper -> {
        baseContext.findActivity()
    }

    else -> {
        null
    }
}
Perspicuous answered 6/11, 2023 at 5:24 Comment(0)
C
0

Below is a slight modification to @Jeffset answer since Compose activities are based off of ComponentActivity and not AppCompatActivity.

fun Context.getActivity(): ComponentActivity? = when (this) {
    is ComponentActivity -> this
    is ContextWrapper -> baseContext.findActivity()
    else -> null
}
Chinchin answered 4/5, 2022 at 18:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.