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,
)
}
}