Skip to main content

Subscribing to web messages

Web apps presented through a Portal can publish messages to native mobile code using the publish/subscribe interface (pub/sub) available with Portals.

In this step, you will subscribe to messages sent from the web to dismiss the current view.

Exploring the problem

Web apps consolidated into the Jobsync superapp may consist of multiple views, and therefore need to manage navigation within their scope. Each web app designed for this training contains a header with a back button and when the web app has exhausted its navigation stack, it uses the Portals web library to communicate with Android code by sending a message to the navigate:back topic.

In the section below, you will use the Portals Android library to subscribe to the navigate:back topic and dismiss the view when a message is received.

Creating a subscriber

The pub/sub mechanism included in the Portals Android library relies on two parts that work together: PortalsPubSub and PortalsPlugin. PortalsPlugin is a Capacitor plugin (you will learn about those in the next step) added to a Portal by default that allows web apps to communicate with the native layer and vice-versa. PortalsPubSub is the class that manages an internal message bus.

info

Pub/sub is bi-directional; messages can be sent from native mobile code to web code as well.

Modify portals/WebAppScreen.kt to a subscribe for the navigate:back topic:

portals/WebAppScreen.kt

_39
package io.ionic.cs.portals.jobsync.portals
_39
_39
import androidx.compose.foundation.layout.Arrangement
_39
import androidx.compose.foundation.layout.Column
_39
import androidx.compose.foundation.layout.fillMaxSize
_39
import androidx.compose.foundation.layout.padding
_39
import androidx.compose.material3.Scaffold
_39
import androidx.compose.runtime.Composable
_39
import androidx.compose.ui.Alignment
_39
import androidx.compose.ui.Modifier
_39
import androidx.compose.ui.viewinterop.AndroidView
_39
import androidx.navigation.NavHostController
_39
import io.ionic.cs.portals.jobsync.util.CredentialsManager
_39
import io.ionic.portals.PortalBuilder
_39
import io.ionic.portals.PortalView
_39
import io.ionic.portals.PortalsPlugin
_39
_39
@Composable
_39
fun WebAppScreen(navController: NavHostController, metadata: WebAppMetadata) {
_39
Scaffold { innerPadding ->
_39
Column(
_39
Modifier.fillMaxSize().padding(innerPadding),
_39
verticalArrangement = Arrangement.Center,
_39
horizontalAlignment = Alignment.CenterHorizontally
_39
) {
_39
AndroidView(
_39
modifier = Modifier.fillMaxSize(),
_39
factory = { context ->
_39
val portal = PortalBuilder("debug")
_39
.setStartDir("portals/debug")
_39
.setInitialContext(CredentialsManager.credentials!!.toMap())
_39
.addPluginInstance(PortalsPlugin())
_39
.create()
_39
PortalView(context, portal)
_39
}
_39
)
_39
}
_39
}
_39
}

Add an instance of PortalsPlugin to the Portal.

portals/WebAppScreen.kt

_42
package io.ionic.cs.portals.jobsync.portals
_42
_42
import androidx.compose.foundation.layout.Arrangement
_42
import androidx.compose.foundation.layout.Column
_42
import androidx.compose.foundation.layout.fillMaxSize
_42
import androidx.compose.foundation.layout.padding
_42
import androidx.compose.material3.Scaffold
_42
import androidx.compose.runtime.Composable
_42
import androidx.compose.ui.Alignment
_42
import androidx.compose.ui.Modifier
_42
import androidx.compose.ui.viewinterop.AndroidView
_42
import androidx.navigation.NavHostController
_42
import io.ionic.cs.portals.jobsync.util.CredentialsManager
_42
import io.ionic.portals.PortalBuilder
_42
import io.ionic.portals.PortalView
_42
import io.ionic.portals.PortalsPlugin
_42
import io.ionic.portals.PortalsPubSub
_42
_42
@Composable
_42
fun WebAppScreen(navController: NavHostController, metadata: WebAppMetadata) {
_42
val pubSub = PortalsPubSub()
_42
_42
Scaffold { innerPadding ->
_42
Column(
_42
Modifier.fillMaxSize().padding(innerPadding),
_42
verticalArrangement = Arrangement.Center,
_42
horizontalAlignment = Alignment.CenterHorizontally
_42
) {
_42
AndroidView(
_42
modifier = Modifier.fillMaxSize(),
_42
factory = { context ->
_42
val portal = PortalBuilder("debug")
_42
.setStartDir("portals/debug")
_42
.setInitialContext(CredentialsManager.credentials!!.toMap())
_42
.addPluginInstance(PortalsPlugin(pubSub))
_42
.create()
_42
PortalView(context, portal)
_42
}
_42
)
_42
}
_42
}
_42
}

Create an instance of the PortalsPubSub class and pass it into the PortalsPlugin constructor.

portals/WebAppScreen.kt

_50
package io.ionic.cs.portals.jobsync.portals
_50
_50
import androidx.compose.foundation.layout.Arrangement
_50
import androidx.compose.foundation.layout.Column
_50
import androidx.compose.foundation.layout.fillMaxSize
_50
import androidx.compose.foundation.layout.padding
_50
import androidx.compose.material3.Scaffold
_50
import androidx.compose.runtime.Composable
_50
import androidx.compose.ui.Alignment
_50
import androidx.compose.ui.Modifier
_50
import androidx.compose.ui.viewinterop.AndroidView
_50
import androidx.navigation.NavHostController
_50
import io.ionic.cs.portals.jobsync.util.CredentialsManager
_50
import io.ionic.portals.PortalBuilder
_50
import io.ionic.portals.PortalView
_50
import io.ionic.portals.PortalsPlugin
_50
import io.ionic.portals.PortalsPubSub
_50
import kotlinx.coroutines.CoroutineScope
_50
import kotlinx.coroutines.Dispatchers
_50
import kotlinx.coroutines.launch
_50
_50
@Composable
_50
fun WebAppScreen(navController: NavHostController, metadata: WebAppMetadata) {
_50
val pubSub = PortalsPubSub()
_50
pubSub.subscribe("navigate:back") {
_50
CoroutineScope(Dispatchers.Main).launch {
_50
navController.popBackStack()
_50
}
_50
}
_50
_50
Scaffold { innerPadding ->
_50
Column(
_50
Modifier.fillMaxSize().padding(innerPadding),
_50
verticalArrangement = Arrangement.Center,
_50
horizontalAlignment = Alignment.CenterHorizontally
_50
) {
_50
AndroidView(
_50
modifier = Modifier.fillMaxSize(),
_50
factory = { context ->
_50
val portal = PortalBuilder("debug")
_50
.setStartDir("portals/debug")
_50
.setInitialContext(CredentialsManager.credentials!!.toMap())
_50
.addPluginInstance(PortalsPlugin(pubSub))
_50
.create()
_50
PortalView(context, portal)
_50
}
_50
)
_50
}
_50
}
_50
}

Subscribe to the navigate:back topic, and pop the nav stack when the topic receives a message.

portals/WebAppScreen.kt

_51
package io.ionic.cs.portals.jobsync.portals
_51
_51
import androidx.compose.foundation.layout.Arrangement
_51
import androidx.compose.foundation.layout.Column
_51
import androidx.compose.foundation.layout.fillMaxSize
_51
import androidx.compose.foundation.layout.padding
_51
import androidx.compose.material3.Scaffold
_51
import androidx.compose.runtime.Composable
_51
import androidx.compose.ui.Alignment
_51
import androidx.compose.ui.Modifier
_51
import androidx.compose.ui.viewinterop.AndroidView
_51
import androidx.navigation.NavHostController
_51
import io.ionic.cs.portals.jobsync.util.CredentialsManager
_51
import io.ionic.portals.PortalBuilder
_51
import io.ionic.portals.PortalView
_51
import io.ionic.portals.PortalsPlugin
_51
import io.ionic.portals.PortalsPubSub
_51
import kotlinx.coroutines.CoroutineScope
_51
import kotlinx.coroutines.Dispatchers
_51
import kotlinx.coroutines.launch
_51
_51
@Composable
_51
fun WebAppScreen(navController: NavHostController, metadata: WebAppMetadata) {
_51
val pubSub = PortalsPubSub()
_51
pubSub.subscribe("navigate:back") {
_51
CoroutineScope(Dispatchers.Main).launch {
_51
navController.popBackStack()
_51
}
_51
pubSub.unsubscribe("navigate:back", it.subscriptionRef)
_51
}
_51
_51
Scaffold { innerPadding ->
_51
Column(
_51
Modifier.fillMaxSize().padding(innerPadding),
_51
verticalArrangement = Arrangement.Center,
_51
horizontalAlignment = Alignment.CenterHorizontally
_51
) {
_51
AndroidView(
_51
modifier = Modifier.fillMaxSize(),
_51
factory = { context ->
_51
val portal = PortalBuilder("debug")
_51
.setStartDir("portals/debug")
_51
.setInitialContext(CredentialsManager.credentials!!.toMap())
_51
.addPluginInstance(PortalsPlugin(pubSub))
_51
.create()
_51
PortalView(context, portal)
_51
}
_51
)
_51
}
_51
}
_51
}

Clean up the subscription once it's no longer needed by unsubscribing to the navigate:back topic.

Add an instance of PortalsPlugin to the Portal.

Create an instance of the PortalsPubSub class and pass it into the PortalsPlugin constructor.

Subscribe to the navigate:back topic, and pop the nav stack when the topic receives a message.

Clean up the subscription once it's no longer needed by unsubscribing to the navigate:back topic.

portals/WebAppScreen.kt

_39
package io.ionic.cs.portals.jobsync.portals
_39
_39
import androidx.compose.foundation.layout.Arrangement
_39
import androidx.compose.foundation.layout.Column
_39
import androidx.compose.foundation.layout.fillMaxSize
_39
import androidx.compose.foundation.layout.padding
_39
import androidx.compose.material3.Scaffold
_39
import androidx.compose.runtime.Composable
_39
import androidx.compose.ui.Alignment
_39
import androidx.compose.ui.Modifier
_39
import androidx.compose.ui.viewinterop.AndroidView
_39
import androidx.navigation.NavHostController
_39
import io.ionic.cs.portals.jobsync.util.CredentialsManager
_39
import io.ionic.portals.PortalBuilder
_39
import io.ionic.portals.PortalView
_39
import io.ionic.portals.PortalsPlugin
_39
_39
@Composable
_39
fun WebAppScreen(navController: NavHostController, metadata: WebAppMetadata) {
_39
Scaffold { innerPadding ->
_39
Column(
_39
Modifier.fillMaxSize().padding(innerPadding),
_39
verticalArrangement = Arrangement.Center,
_39
horizontalAlignment = Alignment.CenterHorizontally
_39
) {
_39
AndroidView(
_39
modifier = Modifier.fillMaxSize(),
_39
factory = { context ->
_39
val portal = PortalBuilder("debug")
_39
.setStartDir("portals/debug")
_39
.setInitialContext(CredentialsManager.credentials!!.toMap())
_39
.addPluginInstance(PortalsPlugin())
_39
.create()
_39
PortalView(context, portal)
_39
}
_39
)
_39
}
_39
}
_39
}

info

Additional ways to create subscribers can be found at this link.

Testing the subscription

Build and run the Jobsync app and navigate to one of the features in the dashboard view. Switch from the 'Initial Context' tab to the 'Publish/Subscribe' tab.

Here, you can test Portal's pub/sub mechanism by entering navigate:back into the 'Topic:' input field under the 'Publish' heading. Press the 'Publish' button, and the view should dismiss, navigating back to the dashboard view.

What's next

The pub/sub mechanism available within the Portals library is ideal for simple use cases, such as allowing a web app presented through a Portal to request native navigation. However, it is not suitable for more complex use cases. In the next step of this module, you will learn about Capacitor plugins, which also communicate bi-directionally, but in a more structured manner suitable for complex use cases.