Files
coco 723ce1af5c a
2026-07-03 15:12:48 +08:00

87 KiB

Jetmagic - Navigation Manager

The Navigation Manager and Composition Resource Manager (CRM) are designed to work together. It isn't possible to use either of these on their own. Jetmagic's CRM is responsible for creating composables that are dependent on the current device configuration. The CRM uses "Composable Resources" to create "Composable Instances" which are rendered to the UI. During navigation, the Navigation Manager manages a stack of composable instances that essentially represent the different screens in the app that users navigate to and from.

Table of Contents


How to navigate forwards

To navigate to a screen, call navman.goto. At the very minimum, a composable resource id must be specified:

navman.goto(composableResId = ComposableResourceIDs.TestScreen)

This causes the a new screen to be created and placed on top of the navigation stack. The new screen is created using the composable resource id that is provided. When a new screen is created, it is provided with a randomly generated id. However, if you know that there will only be one instance of the screen and you want an easy way of referencing it, you can provide your own id:

navman.goto(composableInstanceId = "appSettings", composableResId = ComposableResourceIDs.Settings)

The composableInstanceId must be provided if you set the cacheComposable parameter to true.

It should be noted that you cannot navigate to a screen that already exists on the navigation stack by bringing it to the foreground. For example, if you navigate from the home screen to screen A, then to screen B and then to screen C, you cannot navigate to Screen A by causing it to move from its current location on the stack to the top of the stack. If you navigate to another screen of tye A, a new screen will be created of tye A and become the current screen. However, if you provide the same composable instance id for a new screen A and another screen A was created with the same id and stored in the cache, a new screen A will get created but it will use the cached composable instance for the new screen. This effectively makes it look like the previous screen A has now been brought to the top of the stack. But if you return back to the previous screens, you will eventually reach the previous screen A and it will be displayed. Caching allows you to essentially create a single instance of a screen.


How to navigate back

To return to a previous screen, call goBack:

navman.goBack()

Keep in mind that when you return to a previous screen and the current screen uses animations, it will get recomposed. You need to make sure that the composable instances on the current screen realize that the screen is being terminated and takes whatever action it needs to prevent code from executing that you normally only want to execute when the screen is being displayed. You may also want to do cleanup work in your composable instances before returning to the previous screen.

A composable instance can know that its screen is being terminated by inspecting the isTerminated property. If it is set to true, the screen has been terminated. The screen's root composable instance and each of its children composable instances will have this property set to true.

There may be cases where the user taps on the Back button to return to the previous screen but your screen may be in a state where you may first need a confirmation from the user before proceeding with the navigation. For instance, if the screen is a form that the user might fill in but accidentally doesn't fill in some mandatory field and then attempts to navigate to the previous screen, you may want to prevent the navigation and notify the user about the field that they need to fill in. In order to prevent the navigation, you need to intercept the navigation and decide whether to allow it. This is done by implementing the onNavigateBack callback function that is defined in the NavigationManagerHelper interface. This will only work however if Jetmagic manages the viewmodel for the composable instance that is associated with the screen where you want to intercept the navigation event. In the demo app, there is a test screen that lets you test out this feature. It has a button labeled "Return to previous screen". This is the code for the the viewmodel:

class PromptToGoBackScreenViewModel : ViewModel(), NavigationManagerHelper {

    private val _onDisplayDialog = MutableLiveData(false)
    val onDisplayDialog: LiveData<Boolean> = _onDisplayDialog

    fun onDialogResponse(confirmed: Boolean) {
        _onDisplayDialog.value = false

        if (confirmed) {
            navman.goBackImmediately()
        }
    }

    override fun onNavigateBack(): NavigateBackOptions {
        _onDisplayDialog.value = true
        return NavigateBackOptions.Cancel
    }
}

When the user taps on the Back button, the onNavigateBack gets called and returns NavigateBackOptions.Cancel. It also notifies the composable to display a dialog that lets the user decide whether to return to the previous screen or not. If the user chooses to return, navman.goBackImmediately is called. You need to call navman.goBackImmediately instead of navman.goBack because navman.goBackImmediately ignores whether the viewmodel has implemented the onNavigateBack callback . If you were to call navman.goBack, you would end up in a loop with onNavigateBack being called repeatedly.

If you have multiple viewmodels associated with a screen and more than one implements NavigationManagerHelper, only one of them should be used to handle onNavigateBack. The others should return null, which is the same thing as returning Cancel. If more than one onNavigateBack is called and more than one returns something other than Cancel or null, you will get unpredictable results. The recommended place to use NavigationManagerHelper is in the viewmodel associated with the screen handler and not in the children composable instances. If the children composable instances need to know when the screen is about to be terminated, they can inspect the isTerminated property of their composable instance.

NavigateBackOptions has a number of other options that you can make use of. Returning GoBackImmediatelyAndCacheScreen will cause the screen to navigate to the previous screen but will also cache the current screen for re-use. Returning GoBackImmediatelyAndRemoveCachedScreen will cause the screen to navigate to the previous screen but will remove the current screen from the cache if it exists in the cache. See the section below on caching a screen.


How to navigate to the home screen

To return to the home screen, call:

navman.gotoHomeScreen()

The Navigation Manager will pop all the screens off the stack until only the home screen is the only screen on the stack. Please note, that in Jetmagic, the term "Home Screen" refers to the first screen on the stack. If the app hasn't been started and a user launches the app through a deep link and the deep link results in only one screen being shown, then they are already at the home screen. If the deep link launches multiple screens in succession, then the home screen is the first of these screens to be launched.

The gotoHomeScreen function has a paramter called cleanupScreensOnReturningHome. By default this is set to true which means that all the screens on the stack will get recomposed. During recomposition, a composable instance can inspect its isTerminated property to determine if the screen is being closed and do any cleanup work that needs to be done. If you have a lot of screens on the navigation stack and none of them (except the current screen and the home screen) need to do any cleanup work, recomposing all the screens can have performance that is obvious to the user. You can avoid this performance hit by preventing these screens from being recomposed when you set cleanupScreensOnReturningHome to false. The current screen will be recomposed before it is removed and you can do any cleanup work if it's required.

Just like the goBack function, the gotoHomeScreen function will also inspect the presence of the NavigationManagerHelper interface and take the appropriate action based upon the value returned by onNavigateBack. However, this only applies to the current screen and not to any of the other previous screens on the stack.


How to get notified of navigation events

There are a few ways an app can get notified when a navigation event occurs. One way is to observe the onScreenChange:

navman.onScreenChange.observeAsState().value

This will be triggered when goto is called. It will also be triggered when gotoHomeScreen is called and the cleanupScreensOnReturningHome parameter is set to false. Another way to get notified is to subscribe to observeScreenChange:

navman.observeScreenChange { composableInstance ->

}

The composableInstance parameter will refer to the current screen after the navigation event has taken place.

This callback will get called when any of these functions are called: goto, gotoHomeScreen, gotoHomeScreenImmediately, goBack or goBackImmediately

It will not be called if you are on the home screen and call goBack or goBackImmediately, which will terminate the activity.


How to cache a screen

Navigation Manager has a separate cache from the navigation stack it uses for keeping track of screens. The cache is designed to store a root composable instance even when the screen it represents is not currently on the navigation stack.

Consider the case of an app that hosts a video conference. The user navigates to a screen where the video is hosted. During the video streaming, the user wants to return back to the previous screen and possibly view some information and then return back to the video conference. What the user does not want is that when they return to the previous screen that the current screen hosting the video terminates and ends the streaming. They want the streaming to continue even when the video is not actually showing. What they really want however is the camera and microphone continue operating without interruption allowing other participants in the conference to keep seeing and hearing the user while that user returns to the previous screen to obtain the information they are interested in. Upon returning to the video screen, the user expects the video to be displayed as though no interruption took place.

By default, returning to the previous screen will cause the current screen to be removed from the navigation stack, destroying its viewmodel (if the viewmodel was managed by the CRM). In order to prevent the root composable instance from being destroyed, it can be stored into the cache either when the user navigates to the screen or when navigating to the previous screen (or even when navigating to the home screen). For it to be cached, the composable instance must provide an id. When the user ever navigates to the screen using goto and provides the id and sets the cacheComposable to true , a search is first made to see if the composable instance exists in the cache with the id. If it does exist, the composable instance is retrieved from the cache and places it on top of the navigation stack making it the current screen. The screen factory is recomposed and the cached screen will be displayed.

The id that is provided for caching must be globally unique throughout the app. Here is an example of caching a screen:

object ComposableResourceIDs {
    // Screens
    const val VideoStreamingScreen = "videoStreamingScreen"
    //...
}

object ComposableInstanceIDs {
    // Screens
    const val VideoConferenceScreen = "videoConferenceScreen"
    //...
}

navman.goto(
    composableInstanceId = ComposableInstanceIDs.VideoConferenceScreen, 
    composableResId = VideoConferenceScreen.TestScreen,
    cacheComposable = true)

At some point, you may decide to remove the composable instance from the cache. In the video conference scenario, when the streaming ends, there probaably is no need to keep the composable instance in the cache. To remove it from the cache, call removeFromCache:

navman.removeFromCache(ComposableInstanceIDs.VideoConferenceScreen)

You can also have it removed when you return to the previous screen or when you return directly to the home sceen if the composable instance's viewmodel implements the onNavigateBack callback from the NavigationManagerHelper interface. When onNavigateBack is called, return GoBackImmediatelyAndRemoveCachedScreen.


NavigationManager (Class)

The Navigation Manager is responsible for maintaining the current stack of screens that a user navigates to. Each time the user navigates forward to a new screen, the root composable that makes up the new screen is pushed on to the stack. If the user moves back to a previous screen, the current screen is popped off the stack. Animated transitions between screens is not the responsibility of the Navigation Manager but rather the ScreenFactory. The Navigation Manager informs the ScreenFactory of changes to the navigation stack and it is the ScreenFactory's task to initiate a compose/recompose of the screens that make up the stack.

Function / Property Description
addDeepLinks fun addDeepLinks(map: List<DeepLinkMap>, onMatchNotFound: (uri: URI, queryKeyValues: Map<String, String>) -> List<String>?)

Adds a deep link to the deep link stack.

map: One or more deep link configurations. Each configuration can have its own list of paths that will trigger a deep link.

onMatchNotFound: If a deep link is triggered but the path for the deep link cannot be found in any of the configurations, the onMatchNotFound callback will be called. The callback can return zero or more composable resource Ids for screens that will be displayed in succession. This can be used to display a screen similar to a "page not found" or take the user to an alternative screen.

The callback will be provided with the uri and query string key/values that triggered the deep link.
clearDeepLinks fun clearDeepLinks()

Clears the stack of deep links.
clearScreenCache fun clearScreenCache()

Removes all items from the navigation cache.
exitApp fun exitApp()

getComposableInstanceById fun getComposableInstanceById(id: String): ComposableInstance

Returns a composable instance for the specified id.

id: The id of the composable instance.
getCurrentScreenComposableInstance fun getCurrentScreenComposableInstance(): ComposableInstance?

getDeepLinkForComposableInstance fun getDeepLinkForComposableInstance(composableInstance: ComposableInstance): DeepLink?

Returns the current deep link if it is associated with the specified composable instance.

If multiple screens are setup to be launched in succession when a deep link is triggered, each of the screens will have a deep link associated with it. These deep links are stored in a deep link stack (a collection) with the first item in the stack being the first screen that will be displayed and the last screen displayed is the last item in the stack.

Whenever a deep link results in a screen navigation, the deepLink property of a composable instance will be set and it generally will remain set as long as the composable instance exists. The Navigation Manager however keeps its own copy of the deep link on stack and remove the deep link when a screen moves to the next deep link. Because a screen's composable instance can be recomposed the deep link associated with the composable instance will not exist as soon as the Navigation Manager navigates to the next deep link. But the screen where the deep link has been removed from may still need to know that a deep link was used to launch the screen in order to put the screen into the appropriate state. For this reason, the composable instance should not rely on the deep link returned by getDeepLinkForComposableInstance to determine the state of a deep link. After retrieving the deep link when calling getDeepLinkForComposableInstance, the composable instance should act upon it and treat it as a one-time execution. Thereafter, if the composable instance still needs to have access to the deep link, it should reference the copy in its own property.

A composable instance should not depend on its own deepLink property to know whether to navigate to the next deep link. It must rely upon the value returned by calling getDeepLinkForComposableInstance. If the value returns null, it means that there is no deep link to navigate to.

Whenever a call to getDeepLinkForComposableInstance returns a non-nullable value, it means that its screen was created as a result of a deep link being triggered.

composableInstance: If the first deep link on the stack is associated with the specified composable instance, the deep link is returned, otherwise null is returned. If the composable instance is a child of a root composable instance and if the parent is associated with the first item on the deep link stack, the deep link is returned for the parent. In other words, you can pass in either a root composable instance or a child composable instance and as long as either is associated with the current deep link, it will be returned.

returns: If the specified composable instance is associated with the current deep link, the current deep link will be returned, otherwise null will be returned.
getRootComposableByIndex fun getRootComposableByIndex(index: Int): ComposableInstance

Returns a root composable instance for the specified index.

index: The first item in the navigation stack starts with zero. Use the totalScreensDisplayed property to get the last index.
getRootComposableInstanceById fun getRootComposableInstanceById(id: String): ComposableInstance

Returns a root composable instance for the specified id.

id: The id of the composable instance.
goBack fun goBack(): Boolean

Navigates back to the previous screen.

If the current screen's root composable instancece mplements onNavigateBack in its viewmodel (and the viewmodel is part of the composable instance), a call will be made to it. If onNavigateBack returns GoBackImmediately, GoBackImmediatelyAndCacheScreen or GoBackImmediatelyAndRemoveCachedScreen, the navigation manager will navigate to the previous screen (if one exists) If navigateBackImmediately returns Cancel, no navigation is made to the previous screen. If the current screen needs to perform clean up work or prompt the user about something prior to navigating to the previous screen, the current screen can then call the goBackImmediately function when it is ready to return to the previous screen.

Once navigation to the previous screen is allowed, the current screen is removed from the navigation stack. If the current screen is the home screen, then hitting the Back button should cause the app to exit.

Returns true if the Navigation Manager can navigate to a previous screen. It will return false if the current screen is the Home screen, meaning that there are no more screens that it could go back to.
goBackImmediately fun goBackImmediately()

Navigates back to the previous screen immediately.

No check is made to see if the current screen's composable instance implements onNavigateBack in its viewmodel. The current screen is removed from the navigation stack. If the current screen is the home screen, then hitting the Back button should cause the app to exit.
goto fun goto(composableInstanceId: String? = null, composableResId: String, p: Any? = null, deepLink: DeepLink? = null, cacheComposable: Boolean = false)

Navigates to a new screen.

composableInstanceId: If specified, a check will be made to see if a composable instance exists in the navigation cache with this id. If it does exist, a new screen is added to the stack but uses the cached composable instance instead of creating a new one. Make sure that this id is unique throughout your app. If this parameter is null, a new id will be created for the new screen.

composableResId: The id of the composable resource  that will be used to select which resource will be used to create a composable instance. The id that you provide must be the same id that is provided by the resourceId parameter of a ComposableResource added with addComposableResources.

If a viewmodel is specified by the composable resource, it will be created at this point and associated with the composable instance.

p: Any parameter data that needs to be passed to the composable instance.

cacheComposable: If set to true, the composable instance will also be placed permanently into a cache and remain there until explicitly removed by calling removeFromCache.
gotoDeepLink fun gotoDeepLink(url: String)

Causes one or more screens to be launched for the specified url.

url: The url that will launch the screens.
gotoHomeScreen fun gotoHomeScreen(cleanupScreensOnReturningHome: Boolean = true, p: Any? = null)

Navigates to the home screen.

All screens are removed from the navigation stack except the home screen (and the hidden placeholder screen) added after it.

Using the Back button at this point normally should cause the app to exit.

Before navigating to the home screen, a call to onNavigateBack on the current screen will be made if the current screen implements this interface function. The value returned by onNavigateBack will determine whether the navigation to the home screen will be canceled or whether to proceed and cache the current screen or remove it from the cache (if it was previously stored in the cache).

cleanupScreensOnReturningHome: If set to false, none of the previous screens that are currently on the navigation stack will get recomposed. Only the current screen will be recomposed. This means that none of the previous screens will have a chance to perform any cleanup work. Returning home as quickly as possible without recomposing any of the previous screens results in a better response. If this parameter is set to true, then every previous screen will get recomposed and if there are a lot of screens with a lot of cleanup work that each needs to perform, the user might see an obvious delay until they are back to the home screen. If none of your screens need to do any cleanup, or if you can ensure that all the screens currently on the navigation stack (except the current screen) don't require any cleanup, consider setting this parameter to false for a better UI response.

p: Any parameter data that needs to be passed to the home screen.
gotoHomeScreenImmediately fun gotoHomeScreenImmediately(cleanupScreensOnReturningHome: Boolean = true, p: Any? = null)

Navigates to the home screen immediately.

No check is made to see if the current screen implements onNavigateBack.

cleanupScreensOnReturningHome: See the documentation for the cleanupScreensOnReturningHome parameter of goto.

p: Any parameter data that needs to be passed to the home screen.
gotoNextDeepLink fun gotoNextDeepLink(composableInstance: ComposableInstance, p: Any? = null)

navStackCount navStackCount: Int

Indicates the total number of screens on the stack. This includes the hidden placeholder screen that always appears last in the stack. The stack can include hidden screens which can be created using deep links. A deep link can be setup to render multiple screens in succession and optionally hide all but the last of these screens.
observeScreenChange fun observeScreenChange(callback: (composableInstance: ComposableInstance) -> Unit)

An observer that clients can register with to get notified whenever the screen changes.

callback: When a screen change occurs, the composable instanceof the current screen will be sent to the subscriber.
onExitApp onExitApp: (() -> Unit)? = null

A callback that can be set that will get called when the user is on the home screen and then hits the Back button. In your callback, you can do something like prompt the user if they want to exit the app. If the user wants to exit, then call exitApp. If onExitApp is not set, hitting the Back button while on the home screen will cause the app to exit.
onScreenChange onScreenChange: LiveData<Int>

Used to notify the screen factory that it needs to recompose the screens. The value returned is a random number between 0 and one million. Using a randomly generated value forces LiveData to update and notify any observers.
removeFromCache fun removeFromCache(composableId: String)

Removes a composable instance from the navigation cache.

composableId: The id of the composable instance to remove.
totalScreensDisplayed totalScreensDisplayed: Int

Indicates the total number of screens on the stack excluding the hidden placeholder screen that always appears last in the stack. As with navStackCount, this count can include hidden screens.


NavigationManagerHelper (Interface)

You can use these callback functions to allow the Navigation Manager to interact with your viewmodels. You should only implement this on viewmodels that are managed by the CRM for root composable instances. Do not implement this on any children composable instances as the Navigation Manager will only execute the callback in the root composable instances.

Function / Property Description
onNavigateBack fun onNavigateBack(): NavigateBackOptions

When the goBack function is called, the Navigation Manager will first call onNavigateBack. The value returned by onNavigateBack will determine whether to proceed with the navigation or not. See the  NavigateBackOptions below for the values you can return.


NavigateBackOptions (Enum)

When the user navigates back to a previous screen, the Navigation Manager will check if the NavigationManagerHelper interface is defined in the root composable of the current screen and call the onNavigateBack callback. Depending on the value returned by this callback, the Navigation Manager will know how to correctly handle returning to the previous screen.

Option Description
GoBackImmediately The Navigation Manager navigates to the previous screen.
GoBackImmediatelyAndCacheScreen The Navigation Manager caches the current screen and then returns to the previous screen.
GoBackImmediatelyAndRemoveCachedScreen The Navigation Manager removes the screen from its cache and returns the previous screen.
Cancel The Navigation Manager will cancel navigating to the previous screen. This is useful in scenarios where you may want to delay returning to the previous screen for reasons such as performing clean up tasks or prompting the user if they really want to return to the previous screen.