Adding Notification Badges to a Kotlin App

When building a Kotlin app, or any app for that matter, it’s pretty inevitable that it will contain push notifications. And with push notifications, it’s likely the app icon will need a badge. This is possible with the standard Android API, but unfortunately, the target SDK needs to be 26 or higher for it to work.

Typically, any new features added to the API include a support version for older targets, but that’s not the case here. Oddly, the notification badges feature will not work at all if the app target is lower than API 26. This is a problem because it’s very likely the target SDK on apps will be lower than 26 for some time.

Thankfully, there is a way to get notification badges, and it’s simpler than what’s provided in API 26. Shortcut Badger doesn’t work on all Android devices, but a large number are supported. It consists of three parts.

Notification Badge for the App Icon

First, we add mavenCentral to the project build.gradle.


repositories {
    mavenCentral()
}

Then we add the dependency to the module build.gradle.


dependencies {
    implementation "me.leolin:ShortcutBadger:1.1.22@aar"
}

Finally, we apply the badge count.


val badgeCount = 1
ShortcutBadger.applyCount(context, badgeCount);

This would likely be added to the code where you handle notifications. However, depending on your Kotlin app, it could also be used to track other things.

Notification Badge for Bottom Navigation Menu

Occasionally, you might be working on an app with a bottom navigation where you also need to display a badge. This is something that doesn’t currently exist as part of the menus, but you can do it programatically, as this Stack Overflow question and answer explains.

The badge background shape only needs a color and corner radius. This could be designed to suit the needs of the app, but an oval is sufficient.


<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#CA3A4C" />
    <corners android:radius="10dp" />
</shape>

For the badge itself, instead of using a FrameLayout, try a ConstraintLayout. This maintains a minimum width for a single digit and allows it to expand properly if more than a single digit is added.


<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/bottom_bar_badge"
    android:layout_height="20dp"
    android:layout_width="wrap_content"
    android:minWidth="20dp"
    android:background="@drawable/badge_background">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/bottom_bar_badge_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:layout_marginEnd="4dp"
        android:textColor="#FFFFFF"
        android:textSize="14sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Advanced Bottom Navigation View

Compared to the Stack Overflow solution, the AdvancedBottomNavigationView has the most changes. Because there will probably only be one BottomNavigationView in the app, we can use one badge style (and avoid using attributes). The menu view itself can be grabbed as the first child of the BottomNavigationView.

Now, the init can just call initBadges with the layout ID. That’s where the navigation badges will be added.


class AdvancedBottomNavigationView(context: Context, attrs: AttributeSet) : BottomNavigationView(context, attrs) {

    private companion object {
        const val BADGE_MIN_WIDTH_HEIGHT = 20
        const val BADGE_MARGIN_TOP = 5
        const val BADGE_MARGIN_LEFT = 15
    }

    private val menuView = children.first() as BottomNavigationMenuView

    init {
        initBadges(R.layout.badge)
    }

    fun setBadgeValue(@IdRes resId: Int, count: Int) {
        val menuItem = menuView.findViewById(resId) as BottomNavigationItemView

        val badge = menuItem.findViewById<ConstraintLayout>(R.id.bottom_bar_badge).parent as ViewGroup
        val badgeText = menuItem.findViewById(R.id.bottom_bar_badge_text) as TextView

        if (count > 0) {
            badgeText.text = count.toString()
            badge.visibility = View.VISIBLE
        } else {
            badge.visibility = View.GONE
        }
    }

    private fun initBadges(@LayoutRes badgeLayoutId: Int) {
        val width = Resources.getSystem().displayMetrics.widthPixels
        val totalItems = menuView.childCount
        val oneItemAreaWidth = width / totalItems
        val marginTop = BADGE_MARGIN_TOP.toPx()
        val marginLeft = BADGE_MARGIN_LEFT.toPx()

        for (i in 0 until totalItems) {
            val menuItem = menuView.getChildAt(i) as BottomNavigationItemView

            val container = ConstraintLayout(context)
            container.visibility = View.GONE

            val badge =  View.inflate(container.context, badgeLayoutId, null)
            container.addView(badge)

            val constraintSet = ConstraintSet()
            constraintSet.constrainWidth(badge.id, ConstraintSet.WRAP_CONTENT)
            constraintSet.constrainMinWidth(badge.id, BADGE_MIN_WIDTH_HEIGHT.toPx())
            constraintSet.constrainHeight(badge.id, BADGE_MIN_WIDTH_HEIGHT.toPx())
            constraintSet.connect(badge.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, oneItemAreaWidth / 2 + marginLeft)
            constraintSet.connect(badge.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, marginTop)
            constraintSet.applyTo(container)
            menuItem.addView(container)
        }
    }
}

One of the first differences you’ll note is that the screen width can be grabbed from the system display metrics. There’s also a method called toPx(), which is a small extension on Int.


fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()

The other main difference is that instead of LayoutParams, a ConstraintSet is used. On API versions 23 or lower, the badge margins will not be set when using LayoutParams. To remedy this, you can add the badge to another ConstraintLayout. The margins can then be set on the badge inside that layout, and the new layout will expand to fill the menu item. This effectively sets the margins as was needed but in a slightly roundabout way.

The last method, setBadgeValue, only needs to change slightly because of the additional ConstraintLayout.

Finally, you can create the new AdvancedBottomNavigationView in the activity layout as follows:


<com.medhub.mobile.utilities.AdvancedBottomNavigationView
    android:id="@+id/bottom_navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:itemHorizontalTranslationEnabled="false"
    app:itemIconTint="@color/menu_item_color"
    app:itemTextColor="@color/menu_item_color"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:menu="@menu/bottom_navigation_menu" />

Then set the notification badge for a specific MenuItem.


bottom_navigation.setBadgeValue(R.id.navigation_menu_item, 5)

And it’s done! I hope this helps with adding notification badges to future Kotlin apps.