{"id":41879559,"url":"https://github.com/magicbell/magicbell-android","last_synced_at":"2026-01-25T13:02:07.053Z","repository":{"id":43455526,"uuid":"428339858","full_name":"magicbell/magicbell-android","owner":"magicbell","description":"Official MagicBell Android SDK","archived":false,"fork":false,"pushed_at":"2024-10-14T09:50:18.000Z","size":396,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-10-22T19:52:44.955Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/magicbell.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-15T16:28:34.000Z","updated_at":"2024-10-14T09:50:22.000Z","dependencies_parsed_at":"2024-05-08T08:42:47.020Z","dependency_job_id":"ab7a0b29-ee1c-4f0b-9d49-22acbbd3c4b1","html_url":"https://github.com/magicbell/magicbell-android","commit_stats":null,"previous_names":["magicbell/magicbell-android","magicbell-io/magicbell-android"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/magicbell/magicbell-android","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/magicbell%2Fmagicbell-android","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/magicbell%2Fmagicbell-android/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/magicbell%2Fmagicbell-android/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/magicbell%2Fmagicbell-android/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/magicbell","download_url":"https://codeload.github.com/magicbell/magicbell-android/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/magicbell%2Fmagicbell-android/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28753411,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T10:25:12.305Z","status":"ssl_error","status_checked_at":"2026-01-25T10:25:11.933Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-25T13:02:06.219Z","updated_at":"2026-01-25T13:02:07.046Z","avatar_url":"https://github.com/magicbell.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MagicBell Android SDK\n\nThis is the official [MagicBell](https://magicbell.com) SDK for Android.\n\nThis SDK offers:\n\n- Real-time updates\n- Low-level wrappers for the MagicBell API\n- Support for the [Compose framework](https://developer.android.com/jetpack/compose)\n\nIt requires:\n\n- API 23+\n- Android Studio Arctic Fox\n\n## Quick Start\n\nFirst, grab your API key from your [MagicBell dashboard](https://app.magicbell.com). Then, initialize the client and set the current user:\n\n```kotlin\nimport com.magicbell.sdk.MagicBellClient\n\n// Create the MagicBell client with your project's API key\nval magicbell = MagicBellClient(\n  apiKey = \"[MAGICBELL_API_KEY]\",\n  context = applicationContext\n)\n\n// Set the MagicBell user\nval user = magicbell.connectUserEmail(\"richard@example.com\")\n\n// Create a store of notifications\nval store = user.store.build()\n\n// Fetch the first page of notifications. There is also a method without coroutine.\ncoroutineScope.launch {\n  store.fetch().fold(\n    onSuccess = { notificationList -\u003e\n\n    },\n    onFailure = { error -\u003e\n\n    }\n  )\n}\n```\n\nThis repo also contains a full blown example. To run the project:\n\n- Clone the repo\n- Open the root `build.gradle` in XCode\n- Run `app` from the `Example` directory\n\n## Table of Contents\n\n- [Installation](#installation)\n- [The MagicBell client](#the-magicbell-client)\n- [User](#user)\n    - [Multi-User Support](#multi-user-support)\n    - [Logout a user](#logout-a-user)\n    - [Integrating into your app](#integrating-into-your-app)\n- [NotificationStore](#notificationstore)\n    - [Obtaining a NotificationStore](#obtaining-a-notification-store)\n    - [Observing NotificationStore changes](#observing-notification-store-changes)\n    - [Notification Store adapter](#notification-store-adapter)\n- [Notification Preferences](#notification-preferences)\n- [Push Notification Support](#push-notifications)\n- [Contributing](#contributing)\n\n## Installation\n\n### Gradle\n\nAdd the dependency in your build.gradle file.\n```groovy\n  // MagicBell SDK\n  implementation 'com.magicbell:magicbell-sdk:3.0.0'\n  // MagicBell Compose\n  implementation 'com.magicbell:magicbell-sdk-compose:3.0.0'\n```\n\n## The MagicBell Client\n\nThe 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\nrequired to initialize it.\n\n```kotlin\nval magicbell = MagicBellClient(\n  apiKey = \"[MAGICBELL_API_KEY]\",\n  context = applicationContext\n)\n```\n\nYou can provide additional options when initializing a client:\n\n```kotlin\nval magicbell = MagicBellClient(\n  apiKey = \"[MAGICBELL_API_KEY]\",\n  baseURL = defaultBaseUrl,\n  logLevel = LogLevel.NONE,\n  context = applicationContext,\n  magicBellScope = coroutineScope\n) \n```\n\n| Param        | Default Value | Description                                                                                  |\n| ------------ | ------------- | -------------------------------------------------------------------------------------------- |\n| `apiKey`     | -             | Your MagicBell's API key                                                                     |\n| `logLevel`   | `.none`       | Set it to `.debug` to enable logs                                                            |\n| `context`    | -             | Application Context                                                                                 |\n| `logLevel`   | `Dispatchers(Main)` | Scope to run all the tasks.                                              |\n\nThough 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\nbackend before distributing your app.\n\n### Integrating into your app\n\nYou should create the client instance as early as possible in your application and ensure that only one instance is used across your application.\n\n```kotlin\nimport com.magicbell.sdk.MagicBellClient\n\n// Store the instance at a place of your convenience\nval magicbell = MagicBellClient(\"[MAGICBELL_API_KEY]\")\n```\n\nWe recommend to create the instance in your Application class or in your Dependency Injection graph as a Singleton.\n\n## User\n\nRequests to the MagicBell API require that you **identify the MagicBell user**. This can be done by calling the\n`connectUser(...)` method on the `MagicBellClient` instance with the user's email or external ID:\n\n```kotlin\n// Identify the user by its email\nval user = magicbell.connectUserEmail(\"richard@example.com\")\n\n// Identify the user by its external id\nval user = magicbell.connectUserExternalId(\"001\")\n\n// Identify the user by both, email and external id\nval user = magicbell.connectUserWith(email = \"richard@example.com\", externalId = \"0001\")\n```\n\nEach variant of `connectUser` supports a variant for passing an `hmac` parameter that should be send when HMAC Security was enabled for the project.\n\nYou can connect as [many users as you need](#multi-user-support).\n\n**IMPORTANT:** `User` instances are singletons. Therefore, calls to the `connectUser` method with the same arguments will yield the same user:\n\n```kotlin\nval userOne = magicbell.connectUserEmail(\"mary@example.com\")\nval userTwo = magicbell.connectUserEmail(\"mary@example.com\")\n\nassert(userOne === userTwo, \"Both users reference to the same instance\")\n```\n\n### Multi-User Support\n\nIf 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\nto that.\n\nYou can call the `connectUser(:)` method with the r external ID of your logged in users as many times as you need.\n\n```kotlin\nval userOne = magicbell.connectUserEmail(\"richard@example.com\")\nval userTwo = magicbell.connectUserEmail(\"mary@example.com\")\nval userThree = magicbell.connectUserExternalId(\"001\")\n```\n\n### Logout a User\n\nWhen the user is logged out from your application you want to:\n\n- Remove user's notifications from memory\n- Stop the real-time connection with the MagicBell API\n- Unregister the device from push notifications\n\nThis can be achieved with the `disconnectUser` method of the `MagicBell` client instance:\n\n```kotlin\n// Remove by email\nmagicbell.disconnectUserEmail(\"richard@example.com\")\n\n// Remove by external id\nmagicbell.disconnectUserExternalId(\"001\")\n\n// Remove by email and external id\nmagicbell.disconnectUserWith(email = \"richard@example.com\", externalId = \"001\")\n```\n\n### Integrating into your app\n\nThe MagicBell `User` instances need to available across your app. Here you have some options:\n\n- extend your own user object\n- define a global attribute\n- use your own dependency injection graph\n\n#### Extend your own user object\n\nThis 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\nonly need to provide access to the instance. For example:\n\n```kotlin\n\n// Your own user\ndata class User {\n  val name: String\n  val email: String\n}\n\n/// Returns the logged in MagicBell user\nfun User.magicBell(): MagicBell.User {\n  return magicbell.connectUserEmail(email)\n}\n```\n\n#### Define a global attribute\n\nThis is how you can define a nullable global variable that will represent your MagicBell user:\n\n```kotlin\nval magicbell = MagicBellClient(\"[MAGICBELL_API_KEY]\")\nvar magicbellUser: MagicBell.User? = null\n```\n\nAs soon as you perform a login, assign a value to this variable. Keep in mind, you will have to check the\n`magicbellUser` variable was actually set before accessing it in your code.\n\n#### Use your own dependency injection graph\n\nYou can also inject the MagicBell `User` instance in your own graph and keep track on it using your preferred pattern.\n\n## NotificationStore\n\n### Obtaining a notification store\n\nThe `NotificationStore` class represents a collection of [MagicBell](https://magicbell.com) notifications. You can create an instance of this class through\nthe `.build(...)` method on the user store object.\n\nFor example:\n\n```kotlin\nval allNotifications = user.store.build()\n\nval readNotifications = user.store.build(read = true)\n\nval unreadNotifications = user.store.build(read = false)\n\nval archivedNotifications = user.store.build(archived = true)\n\nval billingNotifications = user.store.build(category = \"billing\")\n\nval firstOrderNotifications = user.store.build(topic = \"order:001\")\n```\n\nThese are the attributes of a notification store:\n\n| Attributes    | Type             | Description                                                  |\n| ------------- | ---------------- | ------------------------------------------------------------ |\n| `totalCount`  | `Int`            | The total number of notifications                            |\n| `unreadCount` | `Int`            | The number of unread notifications                           |\n| `unseenCount` | `Int`            | The number of unseen notifications                           |\n| `hasNextPage` | `Bool`           | Whether there are more items or not when paginating forwards |\n| `count`       | `Int`            | The current number of notifications in the store             |\n| `predicate`   | `StorePredicate` | The predicate used to filter notifications                   |\n\nAnd these are the available methods:\n\n| Method              | Description                                                  |\n| ------------------- | ------------------------------------------------------------ |\n| `refresh`           | Resets the store and fetches the first page of notifications |\n| `fetch`             | Fetches the next page of notifications                       |\n| `get(index:)`       | Subscript to access the notifications: `store[index]`        |\n| `delete`            | Deletes a notification                                       |\n| `delete`            | Deletes a notification                                       |\n| `markAsRead`        | Marks a notification as read                                 |\n| `markAsUnread`      | Marks a notification as unread                               |\n| `archive`           | Archives a notification                                      |\n| `unarchive`         | Unarchives a notification                                    |\n| `markAllRead`       | Marks all notifications as read                              |\n| `markAllUnseen`     | Marks all notifications as seen                              |\n\nMost methods have two implementations:\n\n- Using suspended functions returning a `Result` object\n- Using lambdas returning `onSucess` or `onFailure`\n\n```kotlin\n// Delete notification. Lambdas\nstore.delete(\n  notification,\n  onCompletion = {\n    println(\"Notification deleted\")\n  },\n  onFailure = {\n    print(\"Failed: ${error})\")\n  }\n)\n\n// Read a notification\nstore.markAsRead(notification).fold(\n  onSuccess = {\n    println(\"Notification marked as read\")\n  },\n  onFailure = {\n    println(\"Failed: $error\")\n\n  }\n)\n```\n\nThese methods ensure the state of the store is consistent when a notification changes. For example, when a notification is read, stores with the\npredicate `read: .unread`, will remove that notification from themselves notifying all observers of the notification store.\n\n### Advanced filters\n\nYou can also create stores with more advanced filters. To do it, fetch a store using the `.build(...)` method with a\n`StorePredicate`.\n\n```kotlin\nval predicate = StorePredicate()\nval notifications = user.store.build(predicate)\n```\n\nThese are the available options:\n\n| Param        | Options                            | Default        | Description                    |\n| ------------ | ---------------------------------- | -------------- | ------------------------------ |\n| `read`       | `true`, `false`, `null`            | `null`         | Filter by the `read` state (`null` means unspecified) |\n| `seen`       | `true`, `false`, `null`            | `null`         | Filter by the `seen` state (`null` means unspecified) |\n| `archived`   | `true`, `false`                    | `false`        | Filter by the `archived` state |\n| `category`   | `String`                           | `null`         | Filter by category             |\n| `topic`      | `String`                           | `null`         | Filter by topic                |\n\nFor example, use this predicate to fetch unread notifications of the `\"important\"` category:\n\n```kotlin\nval predicate = StorePredicate(read = true, category = \"important\")\nval store = user.store.build(predicate)\n```\n\nNotification stores are singletons. Creating a store with the same predicate twice will yield the same instance.\n\n**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`\nmethod.\n\n```kotlin\nval predicate = StorePredicate()\nuser.store.dispose(predicate)\n```\n\nThis is automatically done for you when you [remove a user instance](#logout-a-user).\n\n### Observing changes\n\nWhen either `fetch` or `refresh` is called, the store will notify the content observers with the newly added notifications (read about\nobservers [here](#observing-notification-store-changes)).\n\n```kotlin\n// Obtaining a new notification store (first time)\nval store = user.store.build()\n\n// First loading\nval listNotifications = store.fetch().getOrElse {\n  // An error occurred \n}\n```\n\nTo reset and fetch the store:\n\n```kotlin\n    val listNotifications = store.refresh().getOrElse {\n  // An error occurred \n}\n```\n\n### Accessing notifications\n\nThe `NotificationStore` is a list and has all list methods available. Therefore, notifications can be accessed as expected:\n\n```kotlin\n// forEach\nstore.forEach { notification -\u003e\n  println(\"Notification = $notification\")\n}\n\n// for in\nfor (notification in store) {\n  println(\"Notification = $notification\")\n}\n\n// As an array\nval notifications = store.notifications\n```\n\nEnumeration is also available:\n\n```kotlin\n// forEach\nstore.forEachIndexed { index, notification -\u003e\n  println(\"Notification = $notification is in position $index\")\n}\n```\n\n### Observing notification store changes\n\n#### Kotlin flow\n\n`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\nContent event returns:\n\n```Kotlin\nsealed class NotificationStoreContentEvent {\n  object Reloaded : NotificationStoreContentEvent()\n  class Inserted(val indexes: List\u003cInt\u003e) : NotificationStoreContentEvent()\n  class Changed(val indexes: List\u003cInt\u003e) : NotificationStoreContentEvent()\n  class Deleted(val indexes: List\u003cInt\u003e) : NotificationStoreContentEvent()\n  class HasNextPageChanged(val hasNextPage: Boolean) : NotificationStoreContentEvent()\n}\n```\n\nFor Count events returns:\n\n```Kotlin\nsealed class NotificationStoreCountEvent {\n  class TotalCountChanged(val count: Int) : NotificationStoreCountEvent()\n  class UnreadCountChanged(val count: Int) : NotificationStoreCountEvent()\n  class UnseenCountChanged(val count: Int) : NotificationStoreCountEvent()\n}\n```\n\nExample. Subscribe to the flows:\n\n```Kotlin\nyourScope.launch {\n  store.contentFlow.onEach { contentEvent -\u003e\n    // when(contentEvent)\n    println(\"Content $it)\n  }.launchIn(this)\n\n  store.countFlow.onEach { countEvent -\u003e\n    // when(countEvent)\n    print(\"Count $it\")\n  }.launchIn(this)\n}\n```\n\n#### Classic Observer Approach\n\nInstances of `NotificationStore` are automatically updated when new notifications arrive, or a notification's state changes (marked read, archived, etc.)\n\nTo observe changes on a notification store, your observers must implement the following protocols:\n\n```kotlin\n// Get notified when the list of notifications of a notification store changes\ninterface NotificationStoreContentObserver {\n  fun onStoreReloaded()\n  fun onNotificationsChanged(indexes: List\u003cInt\u003e)\n  fun onNotificationsDeleted(indexes: List\u003cInt\u003e)\n  fun onStoreHasNextPageChanged(hasNextPage: Boolean)\n}\n\n// Get notified when the counters of a notification store change\ninterface NotificationStoreCountObserver {\n  fun onTotalCountChanged(count: Int)\n  fun onUnreadCountChanged(count: Int)\n  fun onUnseenCountChanged(count: Int)\n}\n```\n\nTo observe changes, implement these protocols (or one of them), and register as an observer to a notification store.\n\n```kotlin\nval store = user.store.build()\nval observer = myObserverClassInstance\n\nstore.addContentObserver(observer)\nstore.addCountObserver(observer)\n```\n\n#### Compose Approach\n\nUse the class `NotificationStoreViewModel` to create a reactive object compatible with Compose and capable of publishing changes on the main attributes of a\n`NotificaitonStore`.\n\nThis object must be created and retained by the user whenever it is needed.\n\n| Attribute       | Type                        | Description                                        |\n| --------------- | --------------------------- | -------------------------------------------------- |\n| `totalCount`    | `State Int`            | The total count                                    |\n| `unreadCount`   | `State Int`            | The unread count                                   |\n| `unseenCount`   | `State Int`            | The unseen count                                   |\n| `hasNextPage`   | `State Bool`           | Bool indicating if there is more content to fetch. |\n| `notifications` | `State [Notification]` | The array of notifications.                        |\n\n### Notification Store adapter\n\nThe `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\nlist very easy and with animations.\n\n```kotlin\nclass NotificationsAdapter(\n  var store: NotificationStore,\n  private val notificationClick: (Notification, Int) -\u003e Unit,\n) : RecyclerView.Adapter\u003cNotificationsAdapter.ViewHolder\u003e()\n```\n\nAnother option would be to have your own list of notifications and modify it every time that the user does an action.\n\n## Notification Preferences\n\nYou can fetch and set notification preferences for MagicBell channels and categories.\n\n```kotlin\nclass NotificationPreferences(\n  val categories: List\u003cCategory\u003e\n)\n\nclass Category(\n  val slug: String,\n  val label: String,\n  val channels: List\u003cChannel\u003e\n)\n\nclass Channel(\n  val slug: String,\n  val label: String,\n  val enabled: Boolean\n)\n```\n\nTo fetch notification preferences, use the `fetch` method as follows:\n\n```kotlin\nuser.preferences.fetch().fold(onSuccess = { notificationPreferences -\u003e\n  println(preferences)\n}, onFailure = {\n  // An error occurred\n})\n```\n\nTo update the preferences, use either `update`.\n\n```kotlin\n// Updating all preferences at once.\n// Only preference for the included categories will be changed\nuser.preferences.update().getOrElse { }\n```\n\nTo update a single channel you can use the provided convenience function `updateChannel`.\n\n```kotlin\nuser.preferences.updateChannel(\"new_comment\", \"in_app\", true).getOrElse { }\n```\n\n## Push Notifications\n\nYou 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\nnotification SDK:\n\n```kotlin\n// FCM Example\nFirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -\u003e\n  if (!task.isSuccessful) {\n    Log.w(TAG, \"Fetching FCM registration token failed\", task.exception)\n    return@OnCompleteListener\n  }\n\n  // Get new FCM registration token\n  val token = task.result\n\n  // Log and toast\n  magicbell.setDeviceToken(token)\n})\n```\n\nMagicBell will keep that device token stored temporarily in memory and send it as soon as new users are declared via\n`MagicBellClient.connectUser`.\n\nWhe a user is disconnected (`MagicBellClient.disconnectUser`), the device token is automatically unregistered for that user.\n\n## Contributing\n\nWe welcome contributions of any kind. To do so, clone the repo and open `build.gradle` with Android Studio Arctic Fox or above.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmagicbell%2Fmagicbell-android","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmagicbell%2Fmagicbell-android","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmagicbell%2Fmagicbell-android/lists"}