Skip to content

Repository files navigation

MagicBell Android SDK

This is the official MagicBell SDK for Android.

This SDK offers:

  • Real-time updates
  • Low-level wrappers for the MagicBell API
  • Support for the Compose framework

It requires:

  • API 23+
  • Android Studio Arctic Fox

Quick Start

First, grab your API key from your MagicBell dashboard. Then, initialize the client and set the current user:

importcom.magicbell.sdk.MagicBellClient// Create the MagicBell client with your project's API keyval magicbell =MagicBellClient(
apiKey ="[MAGICBELL_API_KEY]",
context = applicationContext
)
// Set the MagicBell userval user = magicbell.connectUserEmail("richard@example.com")
// Create a store of notificationsval store = user.store.build()
// Fetch the first page of notifications. There is also a method without coroutine.
coroutineScope.launch {
store.fetch().fold(
onSuccess = { notificationList ->
},
onFailure = { error ->
}
)
}

This repo also contains a full blown example. To run the project:

  • Clone the repo
  • Open the root build.gradle in XCode
  • Run app from the Example directory

Table of Contents

Installation

Gradle

Add the dependency in your build.gradle file.

// MagicBell SDK
implementation 'com.magicbell:magicbell-sdk:3.0.0'// MagicBell Compose
implementation 'com.magicbell:magicbell-sdk-compose:3.0.0'

The MagicBell Client

The first step is to create a MagicBellClient instance. It will manage users and other functionality for you. The API key for your MagicBell project is required to initialize it.

val magicbell =MagicBellClient(
apiKey ="[MAGICBELL_API_KEY]",
context = applicationContext
)

You can provide additional options when initializing a client:

val magicbell =MagicBellClient(
apiKey ="[MAGICBELL_API_KEY]",
baseURL = defaultBaseUrl,
logLevel =LogLevel.NONE,
context = applicationContext,
magicBellScope = coroutineScope
) 
ParamDefault ValueDescription
apiKey-Your MagicBell's API key
logLevel.noneSet it to .debug to enable logs
context-Application Context
logLevelDispatchers(Main)Scope to run all the tasks.

Though the API key is meant to be published, you should not distribute the API secret. Rather, enable HMAC in your project and generate the user secret on your backend before distributing your app.

Integrating into your app

You should create the client instance as early as possible in your application and ensure that only one instance is used across your application.

importcom.magicbell.sdk.MagicBellClient// Store the instance at a place of your convenienceval magicbell =MagicBellClient("[MAGICBELL_API_KEY]")

We recommend to create the instance in your Application class or in your Dependency Injection graph as a Singleton.

User

Requests to the MagicBell API require that you identify the MagicBell user. This can be done by calling the connectUser(...) method on the MagicBellClient instance with the user's email or external ID:

// Identify the user by its emailval user = magicbell.connectUserEmail("richard@example.com")
// Identify the user by its external idval user = magicbell.connectUserExternalId("001")
// Identify the user by both, email and external idval user = magicbell.connectUserWith(email ="richard@example.com", externalId ="0001")

Each variant of connectUser supports a variant for passing an hmac parameter that should be send when HMAC Security was enabled for the project.

You can connect as many users as you need.

IMPORTANT:User instances are singletons. Therefore, calls to the connectUser method with the same arguments will yield the same user:

val userOne = magicbell.connectUserEmail("mary@example.com")
val userTwo = magicbell.connectUserEmail("mary@example.com")
assert(userOne === userTwo, "Both users reference to the same instance")

Multi-User Support

If your app supports multiple logins, you may want to display the status of notifications for all logged in users at the same time. The MagicBell SDK allows you to that.

You can call the connectUser(:) method with the r external ID of your logged in users as many times as you need.

val userOne = magicbell.connectUserEmail("richard@example.com")
val userTwo = magicbell.connectUserEmail("mary@example.com")
val userThree = magicbell.connectUserExternalId("001")

Logout a User

When the user is logged out from your application you want to:

  • Remove user's notifications from memory
  • Stop the real-time connection with the MagicBell API
  • Unregister the device from push notifications

This can be achieved with the disconnectUser method of the MagicBell client instance:

// Remove by email
magicbell.disconnectUserEmail("richard@example.com")
// Remove by external id
magicbell.disconnectUserExternalId("001")
// Remove by email and external id
magicbell.disconnectUserWith(email ="richard@example.com", externalId ="001")

Integrating into your app

The MagicBell User instances need to available across your app. Here you have some options:

  • extend your own user object
  • define a global attribute
  • use your own dependency injection graph

Extend your own user object

This approach is useful if you have a user object across your app. MagicBell will guarantee the User instance for a given email/externalId is unique, and you only need to provide access to the instance. For example:

// Your own userdata classUser {
val name:Stringval email:String
}
/// Returns the logged in MagicBell userfun User.magicBell(): MagicBell.User {
return magicbell.connectUserEmail(email)
}

Define a global attribute

This is how you can define a nullable global variable that will represent your MagicBell user:

val magicbell =MagicBellClient("[MAGICBELL_API_KEY]")
var magicbellUser:MagicBell.User?=null

As soon as you perform a login, assign a value to this variable. Keep in mind, you will have to check the magicbellUser variable was actually set before accessing it in your code.

Use your own dependency injection graph

You can also inject the MagicBell User instance in your own graph and keep track on it using your preferred pattern.

NotificationStore

Obtaining a notification store

The NotificationStore class represents a collection of MagicBell notifications. You can create an instance of this class through the .build(...) method on the user store object.

For example:

val allNotifications = user.store.build()
val readNotifications = user.store.build(read =true)
val unreadNotifications = user.store.build(read =false)
val archivedNotifications = user.store.build(archived =true)
val billingNotifications = user.store.build(category ="billing")
val firstOrderNotifications = user.store.build(topic ="order:001")

These are the attributes of a notification store:

AttributesTypeDescription
totalCountIntThe total number of notifications
unreadCountIntThe number of unread notifications
unseenCountIntThe number of unseen notifications
hasNextPageBoolWhether there are more items or not when paginating forwards
countIntThe current number of notifications in the store
predicateStorePredicateThe predicate used to filter notifications

And these are the available methods:

MethodDescription
refreshResets the store and fetches the first page of notifications
fetchFetches the next page of notifications
get(index:)Subscript to access the notifications: store[index]
deleteDeletes a notification
deleteDeletes a notification
markAsReadMarks a notification as read
markAsUnreadMarks a notification as unread
archiveArchives a notification
unarchiveUnarchives a notification
markAllReadMarks all notifications as read
markAllUnseenMarks all notifications as seen

Most methods have two implementations:

  • Using suspended functions returning a Result object
  • Using lambdas returning onSucess or onFailure
// Delete notification. Lambdas
store.delete(
notification,
onCompletion = {
println("Notification deleted")
},
onFailure = {
print("Failed: ${error})")
}
)
// Read a notification
store.markAsRead(notification).fold(
onSuccess = {
println("Notification marked as read")
},
onFailure = {
println("Failed: $error")
}
)

These methods ensure the state of the store is consistent when a notification changes. For example, when a notification is read, stores with the predicate read: .unread, will remove that notification from themselves notifying all observers of the notification store.

Advanced filters

You can also create stores with more advanced filters. To do it, fetch a store using the .build(...) method with a StorePredicate.

val predicate =StorePredicate()
val notifications = user.store.build(predicate)

These are the available options:

ParamOptionsDefaultDescription
readtrue, false, nullnullFilter by the read state (null means unspecified)
seentrue, false, nullnullFilter by the seen state (null means unspecified)
archivedtrue, falsefalseFilter by the archived state
categoryStringnullFilter by category
topicStringnullFilter by topic

For example, use this predicate to fetch unread notifications of the "important" category:

val predicate =StorePredicate(read =true, category ="important")
val store = user.store.build(predicate)

Notification stores are singletons. Creating a store with the same predicate twice will yield the same instance.

Note: Once a store is fetched, it will be kept alive in memory so it can be updated in real-time. You can force the removal of a store using the .dispose method.

val predicate =StorePredicate()
user.store.dispose(predicate)

This is automatically done for you when you remove a user instance.

Observing changes

When either fetch or refresh is called, the store will notify the content observers with the newly added notifications (read about observers here).

// Obtaining a new notification store (first time)val store = user.store.build()
// First loadingval listNotifications = store.fetch().getOrElse {
// An error occurred 
}

To reset and fetch the store:

val listNotifications = store.refresh().getOrElse {
// An error occurred 
}

Accessing notifications

The NotificationStore is a list and has all list methods available. Therefore, notifications can be accessed as expected:

// forEach
store.forEach { notification ->println("Notification = $notification")
}
// for infor (notification in store) {
println("Notification = $notification")
}
// As an arrayval notifications = store.notifications

Enumeration is also available:

// forEach
store.forEachIndexed { index, notification ->println("Notification = $notification is in position $index")
}

Observing notification store changes

Kotlin flow

NotificationStore exposes two flows with Content changes and Count changes. You can subscribe both of them to receive all the changes in the store. For Content event returns:

sealedclassNotificationStoreContentEvent {
object Reloaded : NotificationStoreContentEvent()
classInserted(valindexes:List<Int>) : NotificationStoreContentEvent()
classChanged(valindexes:List<Int>) : NotificationStoreContentEvent()
classDeleted(valindexes:List<Int>) : NotificationStoreContentEvent()
classHasNextPageChanged(valhasNextPage:Boolean) : NotificationStoreContentEvent()
}

For Count events returns:

sealedclassNotificationStoreCountEvent {
classTotalCountChanged(valcount:Int) : NotificationStoreCountEvent()
classUnreadCountChanged(valcount:Int) : NotificationStoreCountEvent()
classUnseenCountChanged(valcount:Int) : NotificationStoreCountEvent()
}

Example. Subscribe to the flows:

yourScope.launch {
store.contentFlow.onEach { contentEvent ->// when(contentEvent)println("Content $it) }.launchIn(this) store.countFlow.onEach { countEvent -> // when(countEvent) print("Count $it") }.launchIn(this)}

Classic Observer Approach

Instances of NotificationStore are automatically updated when new notifications arrive, or a notification's state changes (marked read, archived, etc.)

To observe changes on a notification store, your observers must implement the following protocols:

// Get notified when the list of notifications of a notification store changesinterfaceNotificationStoreContentObserver {
funonStoreReloaded()
funonNotificationsChanged(indexes:List<Int>)
funonNotificationsDeleted(indexes:List<Int>)
funonStoreHasNextPageChanged(hasNextPage:Boolean)
}
// Get notified when the counters of a notification store changeinterfaceNotificationStoreCountObserver {
funonTotalCountChanged(count:Int)
funonUnreadCountChanged(count:Int)
funonUnseenCountChanged(count:Int)
}

To observe changes, implement these protocols (or one of them), and register as an observer to a notification store.

val store = user.store.build()
val observer = myObserverClassInstance
store.addContentObserver(observer)
store.addCountObserver(observer)

Compose Approach

Use the class NotificationStoreViewModel to create a reactive object compatible with Compose and capable of publishing changes on the main attributes of a NotificaitonStore.

This object must be created and retained by the user whenever it is needed.

AttributeTypeDescription
totalCountState IntThe total count
unreadCountState IntThe unread count
unseenCountState IntThe unseen count
hasNextPageState BoolBool indicating if there is more content to fetch.
notificationsState [Notification]The array of notifications.

Notification Store adapter

The Notification Store is a list also and we recommend to use it in your RecyclerView adapters. Thanks to the observers you can refresh your notification list very easy and with animations.

classNotificationsAdapter(
varstore:NotificationStore,
privatevalnotificationClick: (Notification, Int) ->Unit,
) : RecyclerView.Adapter<NotificationsAdapter.ViewHolder>()

Another option would be to have your own list of notifications and modify it every time that the user does an action.

Notification Preferences

You can fetch and set notification preferences for MagicBell channels and categories.

classNotificationPreferences(
valcategories:List<Category>
)
classCategory(
valslug:String,
vallabel:String,
valchannels:List<Channel>
)
classChannel(
valslug:String,
vallabel:String,
valenabled:Boolean
)

To fetch notification preferences, use the fetch method as follows:

user.preferences.fetch().fold(onSuccess = { notificationPreferences ->println(preferences)
}, onFailure = {
// An error occurred
})

To update the preferences, use either update.

// Updating all preferences at once.// Only preference for the included categories will be changed
user.preferences.update().getOrElse { }

To update a single channel you can use the provided convenience function updateChannel.

user.preferences.updateChannel("new_comment", "in_app", true).getOrElse { }

Push Notifications

You can register the device token with MagicBell for mobile push notifications. To do it, set the device token as soon as it is provided by FCM or your notification SDK:

// FCM ExampleFirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
// Get new FCM registration tokenval token = task.result
// Log and toast
magicbell.setDeviceToken(token)
})

MagicBell will keep that device token stored temporarily in memory and send it as soon as new users are declared via MagicBellClient.connectUser.

Whe a user is disconnected (MagicBellClient.disconnectUser), the device token is automatically unregistered for that user.

Contributing

We welcome contributions of any kind. To do so, clone the repo and open build.gradle with Android Studio Arctic Fox or above.

About

Official MagicBell Android SDK

Resources

Stars

1 star

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages