Skip to content

Navigating to Activities and other apps

It is also possible navigate to an Activity both inside and outside of the app. Similar to other screens it is required to define a route for Activities. Instead of NavRoute the route class needs to extend either InternalActivtyRoute for Activity instances in the current app or ExternalActivityRoute if it is part of a different app.

Setup

Navigating to an Activity is not directly possible with HostNavigator. Instead an ActivityNavigator is needed. This can just be a simple class that extends ActivityNavigator or DestinationNavigator. The latter combines the functionality of HostNavigator and ActivityNavigator so that one class can be used for all navigation related actions of a screen.

From the Composable it is then required to call NavigationSetup(navigator) which will do the required setup to make the Activity related navigation actions work.

It’s recommended to make ActivityNavigator/DestinationNavigator classes specific to one screen where they are needed instead of having a global instance.

Internal Activities

This is example shows the route for a SettingsActivity:

@Parcelize
data class SettingsActivityRoute(
    val id: String,
) : InternalActivityRoute() {
    override fun buildIntent(context: Context) = Intent("com.example.SETTINGS")
}

An instance of the route will automatically be added to the Intent which is why it’s not needed to manually put the id into it. SettingsActivity can then obtain an instance of the route that was used to navigate to it by calling Activity.getRoute() or Activity.requireRoute() and access the argument through it.

To avoid having to reference a specific Activity class it’s possible to use custom Intent actions. Khonshu will automatically make sure that the created Intent is always routed to the current app and can not be hijacked.

Other apps

Activities in other apps can be targeted with ExternalActivityRoute. The only differences to InternalActivityRoute is that the route itself won’t be added to the Intent and that the Intent isn’t limited to the current app.

A very simple route would just be an object without any parameters:

@Parcelize
data object SystemLocationSettings : ExternalActivityRoute {
    override fun buildIntent(context: Context) = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
}

For opening an external Activity that requires arguments, those arguments can be part of the route and then be used in buildIntent to fill in the extras:

@Parcelize
class ShareRoute(
    private val title: String,
    private val message: String
) : ExternalActivityRoute {
    override fun buildIntent(context: Context) = Intent(Intent.ACTION_CHOOSER)
        .putExtra(Intent.EXTRA_TITLE, title)
        .putExtra(Intent.EXTRA_INTENT, Intent().apply {
            action = Intent.ACTION_SEND
            type = "text/plain"
            putExtra(Intent.EXTRA_TEXT, message)
        })
}

// route with a data uri extra
@Parcelize
class BrowserRoute(
    private val uri: Uri,
) : ExternalActivityRoute {
    override fun buildIntent(context: Context) = Intent(Intent.ACTION_VIEW).setData(uri)
}

Activity results

External SDKs and the framework often provide an Activity that is supposed to be used with startActivityForResult. AndroidX already introduced ActivityResultContract to simplify handling this and ActivityNavigator uses it to also enable starting Activities from outside the UI layer and receiving results there.

To use the API registerForActivityResult needs to be called with an instance of the wanted ActivityResultContract. This needs to happen before NavigationSetup is called for the navigator, so it needs to be called during the construction of the navigator. The method returns an ActivityResultRequest object that can be then used for two things. It can be passed to navigateForResult(request) to launch the contract. It also has a results property that returns a Flow<O>, where O is the contract’s output type, to make it possible to receive the returned results.

This is an example navigator that allow navigating to the camera or the system file picker to take or pick an image:

class MyNavigator : ActivityNavigator() {
    val cameraImageRequest = registerForActivityResult(ActivityResultContracts.TakePicture())
    val galleryImageRequest = registerForActivityResult(ActivityResultContracts.GetContent())


    fun takePicture(uri: Uri) {
        // the uri here is the parameter that the TakePicture contract expects
        navigateForResult(cameraImageRequest, uri)
    }

    fun pickPicture() {
        navigateForResult(galleryImageRequest, "image/*")
    }
}

In the example above cameraImageRequest.results returns a Flow<Boolean> and galleryImageRequest.results a Flow<Uri?> which can both be collected to handle the results.

Requesting permissions

The Activity result APIs can already be used with ActivityResultContracts.RequestPermission or ActivityResultContracts.RequestMultiplePermissions to also handle requesting Android runtime permission requests. HostNavigator provides a slightly higher level API for this.

To use this call registerForPermissionResult, which should be done during the construction of the navigator or shortly after. This can then be passed to requestPermissions with one or more permission to request to launch the request. Results can be collected through the Flow<Map<String, PermissionResult>> that is returned by the results property of request.

The PermissionResult is the main advantage of using this API instead for the Activity result APIs. Instead of being a simple Boolean for granted/denied it is a sealed class with Granted and Denied where Denied has an extra shouldShowRationale property. After it receives the result from the contract, the library will internally use Activity.shouldShowRequestPermissionRationale(permission) to make it possible to handle denials more granularly without needing a reference to an Activity.

An example usage can look like this:

class MyNavigator : ActivtyNavigator() {
    // use permissionRequest.results somewhere to handle results
    val permissionRequest = registerForPermissionsResult()

    fun requestContactsPermission(uri: Uri) {
        requestPermissions(permissionRequest, Manifest.permission.CAMERA)
    }

    fun requestLocationPermissions() {
        requestPermissions(
            permissionRequest,
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_FINE_LOCATION,
        )
    }
}