Using PushEngage Android SDK with Jetpack Compose
PushEngage Android SDK works with Jetpack Compose out of the box. This guide covers initialization, notification permissions, subscriber management, deep linking, and best practices for Compose-based apps.
Compose Compatibility
PushEngage Android SDK uses ComponentActivity as its base parameter type for activity-dependent methods. Since Compose activities extend ComponentActivity, the SDK works natively with Compose — no adapters or wrappers needed.
| SDK Method | Parameter Type | Compose Compatible |
|---|---|---|
requestNotificationPermission() | ComponentActivity | Yes |
subscribe() | No activity needed (or ComponentActivity with callback) | Yes |
| All other methods | Static (no activity needed) | Yes |
Setup
1. Add Dependencies
In your app-level build.gradle.kts:
plugins {
id("com.android.application")
id("com.google.gms.google-services")
id("org.jetbrains.kotlin.android")
}
dependencies {
// PushEngage SDK
implementation("com.github.awesomemotive:pushengage-android-sdk:<latest-version>")
implementation(platform("com.google.firebase:firebase-bom:<latest-version>"))
// Jetpack Compose (your existing Compose dependencies)
implementation("androidx.activity:activity-compose:<latest-version>")
implementation("androidx.compose.material3:material3:<latest-version>")
implementation("androidx.navigation:navigation-compose:<latest-version>")
}
In your settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
2. Initialize in Application Class
Initialize PushEngage in your Application class — this is the same regardless of whether you use Compose or traditional Views.
import android.app.Application
import com.pushengage.pushengage.PushEngage
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
PushEngage.Builder()
.addContext(this)
.setAppId("YOUR_APP_ID")
.build()
// Optional: enable logging during development
PushEngage.enableLogging(true)
}
}
3. Set Up Your Compose Activity
Use ComponentActivity (or AppCompatActivity — both work) as your main activity:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
AppContent()
}
}
}
}
Notification Permissions (Android 13+)
Starting with Android 13 (API 33), apps must request the POST_NOTIFICATIONS runtime permission. PushEngage SDK handles this for you — just pass your Compose activity.
Option A: Use PushEngage SDK's Built-In Permission Request (Recommended)
The simplest approach. PushEngage handles the permission dialog and automatically subscribes the user when granted.
import androidx.activity.ComponentActivity
import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.compose.ui.platform.LocalContext
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.Callbacks.PushEngagePermissionCallback
@Composable
fun NotificationPermissionScreen() {
val activity = LocalContext.current as ComponentActivity
var permissionGranted by remember { mutableStateOf(false) }
var permissionRequested by remember { mutableStateOf(false) }
if (!permissionRequested) {
Button(onClick = {
permissionRequested = true
PushEngage.requestNotificationPermission(activity,
object : PushEngagePermissionCallback {
override fun onPermissionResult(granted: Boolean, error: Error?) {
permissionGranted = granted
// SDK automatically calls subscribe() when granted
}
}
)
}) {
Text("Enable Push Notifications")
}
} else if (permissionGranted) {
Text("Notifications enabled!")
}
}
The SDK uses one of two mechanisms depending on your activity type:
- Plain
ComponentActivity(the default for Compose apps): the SDK launches a transparent helper activity to drive the system permission dialog. FragmentActivitysubclass (e.g.,AppCompatActivity): the SDK attaches a headless Fragment to your activity instead.
Either way, the user sees only the standard Android permission prompt — the mechanism is invisible.
Option B: Handle Permission Yourself, Then Subscribe
If you want more control over the permission UX — for example, showing a rationale screen before the system dialog — handle the permission in Compose and then call subscribe().
import android.Manifest
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.compose.ui.platform.LocalContext
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.Callbacks.PushEngageResponseCallback
@Composable
fun CustomPermissionFlow() {
val activity = LocalContext.current as ComponentActivity
var showRationale by remember { mutableStateOf(true) }
fun subscribeToPushEngage() {
PushEngage.subscribe(activity, object : PushEngageResponseCallback {
override fun onSuccess(responseObject: Any?) {
// Subscription successful
}
override fun onFailure(errorCode: Int?, errorMessage: String?) {
// Handle subscription failure
}
})
}
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// Permission granted — now subscribe with PushEngage
subscribeToPushEngage()
}
}
if (showRationale) {
// Your custom rationale screen
AlertDialog(
onDismissRequest = { showRationale = false },
title = { Text("Stay in the loop") },
text = { Text("Enable notifications to get updates on your orders, exclusive deals, and important alerts.") },
confirmButton = {
Button(onClick = {
showRationale = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
// Pre-Android 13: permission granted by default
subscribeToPushEngage()
}
}) {
Text("Enable")
}
},
dismissButton = {
TextButton(onClick = { showRationale = false }) {
Text("Not now")
}
}
)
}
}
Checking Permission Status
getNotificationPermissionStatus() is synchronous, so it is safe to call directly inside remember. Note that this is a point-in-time snapshot — it reads the current OS state once at composition time and does not update if the user later changes notification settings in System Settings.
@Composable
fun NotificationPermissionGate() {
// Reads permission status once at composition time.
// Re-read on resume if you need up-to-date state after returning from Settings.
val status = remember { PushEngage.getNotificationPermissionStatus() }
when (status) {
"granted" -> Text("Notifications are enabled")
"denied" -> Text("Notifications are disabled. Enable them in Settings.")
}
}
getNotificationPermissionStatus() returns only "granted" or "denied". There is no "not yet requested" state — on Android 12 and below, it always returns "granted" since the permission is granted at install time.
Subscriber Management in Compose
All subscriber methods are static and don't require an Activity reference, so they work directly from any Composable.
Get Subscriber ID
PushEngage callbacks fire on a background thread. Use suspendCancellableCoroutine to bridge the callback back into the LaunchedEffect coroutine, which already runs on Dispatchers.Main. This ensures the state update happens on the main thread without any manual dispatcher switching.
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.Callbacks.PushEngageResponseCallback
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@Composable
fun SubscriberInfo() {
var subscriberId by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
// suspendCancellableCoroutine bridges the callback back into this coroutine.
// The resume runs on whatever thread the callback fires on, but the
// coroutine resumes on Dispatchers.Main (LaunchedEffect's default).
subscriberId = suspendCancellableCoroutine { cont ->
PushEngage.getSubscriberId(object : PushEngageResponseCallback {
override fun onSuccess(responseObject: Any?) {
cont.resume(responseObject as? String)
}
override fun onFailure(errorCode: Int?, errorMessage: String?) {
cont.resume(null)
}
})
}
}
subscriberId?.let { id ->
Text("Subscriber: $id")
}
}
Add Segments
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.Callbacks.PushEngageResponseCallback
fun addUserToSegment(segmentName: String) {
PushEngage.addSegment(listOf(segmentName),
object : PushEngageResponseCallback {
override fun onSuccess(responseObject: Any?) {
// Segment added
}
override fun onFailure(errorCode: Int?, errorMessage: String?) {
// Handle error
}
}
)
}
Add Subscriber Attributes
Use addSubscriberAttributes to merge new attributes with existing ones, or setSubscriberAttributes to replace all attributes entirely.
import org.json.JSONObject
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.Callbacks.PushEngageResponseCallback
fun updateUserAttributes(name: String, plan: String) {
val attributes = JSONObject().apply {
put("name", name)
put("plan", plan)
}
PushEngage.addSubscriberAttributes(attributes,
object : PushEngageResponseCallback {
override fun onSuccess(responseObject: Any?) {
// Attributes updated
}
override fun onFailure(errorCode: Int?, errorMessage: String?) {
// Handle error
}
}
)
}
Deep Linking with Compose Navigation
When a user taps a PushEngage notification, the SDK opens your app via an Intent. In a Compose app, you need to bridge Intent data to Compose Navigation.
1. Define Deep Link Routes
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController)
}
composable(
route = "product/{productId}",
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://product/{productId}" }
)
) { backStackEntry ->
val productId = backStackEntry.arguments?.getString("productId")
ProductScreen(productId = productId)
}
composable(
route = "orders",
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://orders" }
)
) {
OrdersScreen()
}
}
}
2. Configure AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link intent filter for PushEngage notifications -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
3. Handle Intent Data in Your Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
AppNavigation()
}
}
}
}
When a notification with deep link URL myapp://product/12345 is tapped, Compose Navigation automatically routes to the ProductScreen composable with productId = "12345".
4. Set Deep Links in PushEngage Dashboard
In the PushEngage dashboard, when creating a notification campaign:
- Set the Deep Link field to your deep link URI (e.g.,
myapp://product/12345) - The SDK will open this URL when the notification is tapped
- Compose Navigation picks it up via the intent filter and routes accordingly
Goal & Event Tracking
Track conversion events from anywhere in your Compose UI:
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.model.request.Goal
import com.pushengage.pushengage.model.request.TriggerCampaign
// Track a purchase goal
fun trackPurchase(amount: Double) {
val goal = Goal(
name = "purchase_complete",
count = 1,
value = amount
)
PushEngage.sendGoal(goal)
}
// Send a trigger event
fun sendCartAbandonTrigger(cartValue: String) {
val trigger = TriggerCampaign(
campaignName = "cart_abandon",
eventName = "cart_abandoned",
data = mapOf("cart_value" to cartValue)
)
PushEngage.sendTriggerEvent(trigger)
}
Use these in a Composable:
@Composable
fun CheckoutScreen(cartTotal: Double) {
Button(onClick = {
// Process payment...
trackPurchase(cartTotal)
}) {
Text("Complete Purchase")
}
}
Price Drop & Inventory Alerts
For e-commerce apps, trigger price drop and inventory alerts:
import com.pushengage.pushengage.PushEngage
import com.pushengage.pushengage.model.request.TriggerAlert
fun addPriceDropAlert(productId: String, price: Double) {
val alert = TriggerAlert(
type = PushEngage.TriggerAlertType.priceDrop,
productId = productId,
link = "myapp://product/$productId",
price = price
)
PushEngage.addAlert(alert)
}
fun addInventoryAlert(productId: String) {
val alert = TriggerAlert(
type = PushEngage.TriggerAlertType.inventory,
productId = productId,
link = "myapp://product/$productId",
price = 0.0,
availability = PushEngage.TriggerAlertAvailabilityType.outOfStock
)
PushEngage.addAlert(alert)
}
Best Practices
Do
- Initialize in Application class, not in your Compose Activity. The SDK should be ready before any UI renders.
- Request permission after value demonstration. Show the user what notifications will help with before asking. Don't prompt on the very first screen.
- Use
LaunchedEffectfor one-time SDK calls (like getting subscriber ID) to avoid re-calling on recomposition. - Handle the Android version check — always check
Build.VERSION.SDK_INT >= TIRAMISUbefore requestingPOST_NOTIFICATIONS. - Use deep link URIs that map cleanly to your Compose Navigation routes.
Don't
- Don't call async SDK methods (those with callbacks) inside
rememberblocks — useLaunchedEffectfor one-time async calls and event handlers (likeonClick) for user-triggered actions. Synchronous methods likegetNotificationPermissionStatus()are fine insideremember. - Don't block the UI thread with SDK callbacks — all PushEngage callbacks already run asynchronously.
- Don't request permission in
LaunchedEffect— permission requests must be triggered by user action (button tap), not automatically on composition. - Don't forget to add
POST_NOTIFICATIONSto AndroidManifest.xml — Android 13+ silently denies the runtime permission request if this declaration is missing, regardless of what the SDK does.
Migration Notes
Coming from XML-based Activities
If you're converting an existing PushEngage integration from XML Views to Compose:
- No SDK changes needed — the same
PushEngageAPI works in both - Replace
AppCompatActivitywithComponentActivity(or keepAppCompatActivity— both work) - Move permission request logic into Composable event handlers
- Replace Intent-based navigation with Compose Navigation deep links
- Static SDK methods (
subscribe,addSegment, etc.) work identically
Coming from OneSignal
If migrating from OneSignal to PushEngage in a Compose app:
- Replace
OneSignal.initWithContext(this)withPushEngage.Builder().addContext(this).setAppId("...").build() - Replace
OneSignal.promptForPushNotifications()withPushEngage.requestNotificationPermission(activity, callback) - Replace OneSignal's
setNotificationOpenedHandlerwith PushEngage deep links via Compose Navigation - OneSignal's tag system maps to PushEngage segments and attributes