Badges

Pulsate supports two kinds of unread indicators:

  • App-icon badges — the dot/number on your launcher icon, driven by Android's native notification badge mechanism.
  • In-app badges — unread counts you render inside your own UI (e.g. on an "Inbox" button).

App-icon badges

The SDK sets the badge count with NotificationCompat.setNumber() — the native Android API. Depending on the device and launcher this shows as a number, a dot, or nothing at all.

Badge visibility is a property of the SDK's notification channel, controlled by PulsateConfig.NOTIFICATION_CHANNEL_SHOW_BADGES (default true). Set it in Application.onCreate() before the first notification:

override fun onCreate() {
    super.onCreate()
    PulsateConfig.NOTIFICATION_CHANNEL_SHOW_BADGES = false
}

⚠️

Android creates a notification channel once and never updates it. If you change this flag while testing, fully uninstall the app to recreate the channel.

For launchers that don't honor the native API, combine the badge listener below with a third-party library such as ShortcutBadger.

In-app badges

Option 1 — setBadgeUpdateListener (real-time)

The SDK pushes badge updates to your listener on three events: leaving the Pulsate Inbox, session start, and receiving a push. Your callback can also modify the count by returning a different value:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        val pulsateManager = PulsateFactory.getInstance()
        pulsateManager.setBadgeUpdateListener(object : IPulsateBadgeUpdateListener {
            override fun onBadgeUpdate(badge: Int): Int {
                ShortcutBadger.applyCount(this@MyApp, badge)
                return badge // return the (possibly adjusted) count the SDK should use
            }
        })
    }
}

⚠️

onBadgeUpdate is not guaranteed to run on the main thread — post to your UI/state holder (e.g. LiveData.postValue, MutableStateFlow) rather than touching views directly.

Feeding the count into UI state via a ViewModel:

class MenuViewModel : ViewModel() {
    private val _badgeState = MutableLiveData(0)
    val badgeState: LiveData<Int> get() = _badgeState

    init {
        PulsateFactory.getInstance().setBadgeUpdateListener(
            object : IPulsateBadgeUpdateListener {
                override fun onBadgeUpdate(badge: Int): Int {
                    _badgeState.postValue(badge)
                    return badge
                }
            },
        )
    }
}
class MenuFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        menuViewModel.badgeState.observe(viewLifecycleOwner) { badge ->
            binding.inboxBtn.text = "INBOX $badge"
        }
    }
}

Option 2 — getFeedUnreadCount (on demand)

Query the unread count directly — for example after the user closes the Feed (onFeedClose) or after session start:

private fun updateBadge() {
    PulsateFactory.getInstance().getFeedUnreadCount(
        object : IPulsateValueListener<Int> {
            override fun onSuccess(value: Int) {
                binding.feedBtn.text = "Inbox ($value)"
            }
            override fun onError(e: Throwable) {
                e.printStackTrace()
            }
        },
    )
}

// Refresh when the user closes the Feed:
binding.feedBtn.setOnClickListener {
    PulsateFactory.getInstance().showFeed(object : IPulsateFeedListener {
        override fun onFeedClose() = updateBadge()
    })
}

ℹ️

getFeedUnreadCount is debounced — requests within 30 seconds of the previous one return the cached value. For real-time updates prefer Option 1; the two combine well.