Docs
grovs.io Start for free

In-app messages

Show in-app messages from the Grovs dashboard in your Android app, hold them back on some screens, and react to taps and answers.

Requires Android SDK 3.1.0

In-app messages are dashboard campaigns: a banner, modal, full-screen cover, system alert, question or your own HTML. The SDK downloads live campaigns, picks one on the device for each app open, screen view or event, and draws it in its own window above the current activity. To create campaigns, see In-app messages.

No code is required. Once the SDK is configured, campaigns show. Use the calls below to keep messages off some screens, pause them during a flow, or react to what the user did.

How messages reach the device

  • Sync. Once the SDK authenticates, each time the app comes to the foreground, and at least every 15 minutes while it's in use. When the event batch reports that campaigns changed, the device syncs within 10 seconds. Dashboard changes apply at the next sync.
  • Triggers. App open (once per session), a screen view (automatic, trackScreenView or trackNavigation), or a custom event from track, optionally filtered by its properties.
  • Decision. Schedule, conditions and frequency caps are checked on the device. If several campaigns match one event, the highest priority shows.
  • One at a time. While a message is on screen, the highest priority match waits. It shows once the screen is free, if it still qualifies.
  • Offline. Synced messages still show. After 48 hours without a sync, nothing shows until a sync succeeds. After 14 days, saved campaigns are deleted.

Messages show only while the SDK is enabled and authenticated, the app is in the foreground and an activity is visible. A message that can't be drawn (no visible activity, say) calls onInAppMessageFailed and is tried again at its next trigger. setSDK(false) closes the message on screen.

React to messages

Pass a GrovsInAppListener to Grovs.setInAppListener. Every method has a default, so override only what you need. Callbacks run on the main thread.

Kotlin
import io.grovs.Grovs
import io.grovs.GrovsInAppListener
 
class MedlyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Grovs.configure(this, "your-api-key", useTestEnvironment = false)
 
        Grovs.setInAppListener(object : GrovsInAppListener {
            // Return false to stop this message. It isn't counted as seen, and nothing else shows for this event.
            override fun shouldShowInAppMessage(campaignId: String): Boolean = !CheckoutFlow.isActive
 
            override fun onInAppMessageShown(campaignId: String) {}
 
            // buttonId is null for a tap on the message itself. payload is set for a key-value action.
            override fun onInAppMessageClicked(campaignId: String, buttonId: String?, payload: Map<String, Any?>?) {
                (payload?.get("screen") as? String)?.let { Router.open(it) }
            }
 
            // reason: close_button, backdrop, swipe, app or orientation
            override fun onInAppMessageDismissed(campaignId: String, reason: String) {}
 
            // Called once an answered question has closed, in place of the dismissal.
            override fun onInAppMessageAnswered(campaignId: String, value: Any, comment: String?) {}
 
            override fun onInAppMessageFailed(campaignId: String, message: String) {}
        })
    }
}

The SDK holds the listener strongly. Call Grovs.setInAppListener(null) to release it.

From Java, extend GrovsInAppListenerAdapter. Kotlin interface defaults aren't Java defaults on the SDK's JVM target:

Java
Grovs.Companion.setInAppListener(new GrovsInAppListenerAdapter() {
    @Override
    public void onInAppMessageShown(@NonNull String campaignId) {
        Log.d("Grovs", "Shown " + campaignId);
    }
});
MethodWhen it's called
shouldShowInAppMessage(campaignId)Before a message shows. Return false to stop it.
onInAppMessageShown(campaignId)The message is on screen and counted as viewed.
onInAppMessageClicked(campaignId, buttonId, payload)A button or the message was tapped. payload carries the pairs of a key-value action.
onInAppMessageDismissed(campaignId, reason)The message closed without a click. reason is close_button (close button, back gesture, or the alert's Close), backdrop, swipe, app (closed by the SDK or your app) or orientation (device turned to a disallowed orientation; not a dashboard dismissal).
onInAppMessageAnswered(campaignId, value, comment)A question was answered and the message closed. value is an Int (1 to 5) for stars, or the chosen option's ID (String) for a choice. comment is null unless one was sent.
onInAppMessageFailed(campaignId, message)The message couldn't be drawn. It isn't counted and is tried again at its next trigger.

Button actions

Each button, and the message itself, has one action. The SDK performs it, reports the click and calls onInAppMessageClicked.

ActionWhat the SDK does
CloseCloses the message.
Open URLOpens the URL with an ACTION_VIEW intent.
Key-valueCloses the message and passes the pairs to onInAppMessageClicked as payload. Route in your own code.
Request push permissionShows the system prompt on Android 13 and later. If the user declined for good, or notifications are off for the app, opens your app's notification settings. Skips people who already allowed notifications. Needs POST_NOTIFICATIONS in your manifest on Android 13 and later; without it, campaigns with this action aren't shown.
Request app ratingStarts the Google Play In-App Review flow. Play decides whether the dialog appears. Without the Play review library, campaigns with this action aren't shown.
Open storeOpens your app's Play Store page if the project has one configured. Otherwise campaigns with this action aren't shown.

Hold messages back

Kotlin
// Pause during checkout or a video. The highest priority message waits.
Grovs.pauseInAppMessages()
 
// Show messages again, starting with the one that waited, if it still qualifies.
Grovs.resumeInAppMessages()
 
// Drop waiting and delayed messages and stay paused. Pass true to also close the one on screen.
Grovs.discardInAppMessages(dismissCurrent = true)
 
// No message shows on these screens; messages wait until the user leaves them.
Grovs.setInAppExcludedScreens(listOf("CheckoutActivity", "PaymentFragment"))

discardInAppMessages() with no argument uses dismissCurrent = false, from Kotlin or Java.

Excluded screens match tracked screen names: the name passed to trackScreenView, or the one automatic tracking reported (the class's simple name, or its alias from setScreenAliases). Case is ignored. These calls, and the listener, work before configure and survive a second configure call.

Questions

A message can ask for a star rating (1 to 5) or one choice from a list. One tap answers.

  • A comment box (up to 500 characters, with Send and Skip) and a thank-you text, shown for 2 seconds, can follow.
  • A high enough star rating can be followed by the Play review dialog, if your app has the Play review library.
  • An answered campaign isn't shown again on that device.

The answer is reported on tap, so it counts even if the app closes during the comment. onInAppMessageAnswered runs once the message closes. The answer is also an event (in_app_answered) that can trigger another campaign.

App ratings

Star questions and the "Rate the app" action use Google Play In-App Review. The SDK doesn't bring it in. Add it to your app to enable them:

Groovy
implementation("com.google.android.play:review-ktx:2.0.2")

Without it, campaigns with a "Rate the app" action are skipped and the review step after a high star rating is left out. Play shows the review dialog only for apps installed from Play, and limits how often it appears.

Custom HTML messages

The HTML template shows your page in a WebView. The SDK injects window.grovs before your scripts run:

MemberDescription
grovs.close()Closes the message (reported as close_button).
grovs.track(name, properties)Tracks a custom event with campaign_id added. The message stays open. The first call counts as the message's click. Names are 1 to 100 characters and can't be reserved names.
grovs.open(url)Opens a link and closes the message. Only works within 2 seconds of a tap inside the message.
grovs.setAttribute(key, value)Sets a user attribute. value is a string, number, boolean or null.
grovs.requestPushPermission()Shows the push permission prompt. Resolves "granted", "denied" or "unsupported". Never opens the settings.
grovs.user, grovs.eventRead-only copies of the user's attributes and the triggering event's properties.
grovs.capabilities.pushPermission"granted", "denied", "prompt" or "unsupported" (no POST_NOTIFICATIONS declared on Android 13 and later).

Or use data attributes, no script needed:

HTML
<button data-grovs-action="close">Not now</button>
<button data-grovs-action="open" data-grovs-url="https://medly.sqd.link/refill">Refill now</button>
<button data-grovs-action="track" data-grovs-event="refill_interest">Tell me more</button>

Plain <a href> links go through grovs.open. A #fragment link scrolls the page.

Personalization. Write {{user.first_name}} or {{event.plan}} in text and in alt, title, value, placeholder, aria-* and data-* attributes. Add a fallback with {{user.first_name | default: "there"}}. If a field has no value and no fallback, the message isn't shown.

Links. javascript:, vbscript:, data:, blob:, file:, about:, intent: and plain http: links are refused. Use https:, mailto:, tel:, sms: or your app's own scheme.

Layout. Full screen, modal, or a top or bottom banner. The SDK sizes the message to the page's reported height and keeps it hidden until the page is ready. A page that isn't ready within 5 seconds isn't shown.

WebView. The page runs with JavaScript on, DOM storage, file access and third-party cookies off, and no navigation away from it. An HTML message whose script loops forever keeps the app's shared WebView renderer busy after the message closes.

Test messages

Send test on a dashboard campaign sends it to every device of one person, matched by Grovs.identifier. The device shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. A test waits up to 10 minutes on the server. To see it sooner, send the app to the background and open it again.

A test message isn't counted or reported. Events it tracks and attributes it sets are logged, not sent. If its pictures fail to load after 10 tries, the test is dropped with a log line.

Orientation, activities and appearance

A campaign targets portrait, landscape or both. It shows only in that orientation and closes if the device turns, with reason orientation.

The message follows the visible activity. If the activity stops, the window goes with it and the same message, at the same step, is drawn over the next activity that resumes.

Cards without their own background follow the app's light or dark theme. The SDK's labels (grovs_in_app_close, grovs_in_app_open, grovs_in_app_continue, grovs_in_app_send, grovs_in_app_skip, grovs_in_app_stars) are English string resources. Override them in your app's res/values-* folders to translate them.

Debugging

Kotlin
Grovs.setDebug(LogLevel.INFO)
Grovs.setInAppDebugLogging(true)

setInAppDebugLogging(true) writes to Logcat at INFO under the tag GrovsInApp, whatever the SDK log level. For each event it logs the campaigns considered and why each was skipped: outside its schedule, failed condition, frequency cap reached, lower priority, already answered, pictures not loaded yet, a personalization field with no value, an unsafe link, not synced for 48 hours, or unsupported by this SDK version.

Reserved event names

The SDK reports message views, clicks, dismissals, answers and comments as events. track and grovs.track in HTML messages refuse these names:

in_app_viewed, in_app_clicked, in_app_dismissed, in_app_answered, in_app_commented, in_app_held, push_open, push_delivered, push_held, journey_entered, journey_held, journey_path, plus the system names view, open, install, reinstall, app_open, time_spent, reactivation, user_referred, custom and screen_view.

See also

Last updated 2026-10-26
Was this page helpful?