Integrate the Genuin Android SDK
Add Genuin embeds, placements, single sign-on, deep links, and push notifications to an Android app with SDK 2.1.5.
The Genuin Android SDK renders your brand's communities, groups, and video feeds inside your own app. You add one or more of four modules, initialize the SDK with your API key, and drop a view or fragment where the content goes. Everything the view shows, from which community to the carousel or grid layout, is configured in Brand Control Center and identified by an embed ID or placement ID.
| Module | Artifact | Size | Adds | Requires |
|---|---|---|---|---|
| Core | com.begenuin:core:2.1.5 | 3.4 MB | Embeds, placements, sign-in, deep links, push notification handling, the video player | — |
| UI | com.begenuin:ui:2.1.5 | 4.5 MB | Comments, search, notifications screen, community and group detail screens, deep-link destinations | Core |
| Camera | com.begenuin:camera:2.1.5 | 48 MB | Recording and uploading video posts | Core, UI |
| AI | com.begenuin:ai:2.1.5 | 6.5 MB | AI-assisted creation of communities and groups | Core, UI, Camera |
Sizes are the published AAR files; the Camera module is large because it bundles the video-processing toolchain. Add only the modules you use, and keep them on the same version.
Requirements
| Minimum | |
|---|---|
| Android | API 24 (Android 7.0). Every module declares minSdkVersion 24. |
| Android Gradle Plugin | 8.6.0 |
| Language | Kotlin or Java. The SDK ships with Kotlin 1.9.22 and exposes Java-callable methods through GenuinSDK.INSTANCE. |
| Orientation | Portrait |
| Form factor | Phones; the SDK isn't laid out for tablets or TV |
| UI toolkit | Views, or Jetpack Compose through AndroidView |
You also need an API key for your brand and at least one embed ID or placement ID, both from Brand Control Center. The key is the same one the Web and iOS SDKs use.
Add the SDK
Add the repositories
The SDK is published to Maven Central. Core also pulls in the Pangle ad SDK, served from ByteDance's repository, and two libraries hosted on JitPack, so declare all three in your project-level settings:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { setUrl("https://jitpack.io") }
maven { setUrl("https://artifact.bytedance.com/repository/pangle") }
}
}allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://artifact.bytedance.com/repository/pangle" }
}
}Without the Pangle repository, Gradle fails to resolve com.pangle.global:pag-sdk even if you never show an ad.
Add the dependencies
Declare one version and apply it to every module you include. Modules depend on each other in a fixed chain (ai → camera → ui → core), so an app that uses the AI module declares all four:
val genuinVersion = "2.1.5"
implementation("com.begenuin:core:$genuinVersion")
implementation("com.begenuin:ui:$genuinVersion") // comments, search, notifications, detail screens
implementation("com.begenuin:camera:$genuinVersion") // recording and upload
implementation("com.begenuin:ai:$genuinVersion") // AI-assisted community and group creationdef genuinVersion = "2.1.5"
implementation "com.begenuin:core:$genuinVersion"
implementation "com.begenuin:ui:$genuinVersion"
implementation "com.begenuin:camera:$genuinVersion"
implementation "com.begenuin:ai:$genuinVersion"Mixing versions across modules fails at runtime, not at build time, so pin them to one variable.
The SDK brings its own video stack (androidx.media3 1.7.1 with ExoPlayer, HLS, and IMA), Glide, Retrofit, Lottie, and the RudderStack analytics client. If your app already uses any of these, Gradle resolves to the higher version.
Declare permissions
Core declares INTERNET, ACCESS_NETWORK_STATE, VIBRATE, and WAKE_LOCK in its own manifest, and Android merges them into yours. Add the rest in your AndroidManifest.xml only for the features you use:
| Permission | Add it for | Module |
|---|---|---|
| android.permission.ACCESS_WIFI_STATE | Network-aware playback | Core |
| android.permission.READ_CONTACTS | Letting users add contacts as community and group members | UI |
| android.permission.POST_NOTIFICATIONS | Push notifications on Android 13 and later | UI |
| android.permission.CAMERA, android.permission.RECORD_AUDIO, plus <uses-feature android:name="android.hardware.camera" android:required="false" /> | Recording video posts | Camera |
| android.permission.READ_MEDIA_VIDEO (API 33+) and android.permission.READ_EXTERNAL_STORAGE with android:maxSdkVersion="32" | Uploading videos from the gallery | Camera |
| android.permission.ACCESS_COARSE_LOCATION, android.permission.ACCESS_FINE_LOCATION | AI-created communities tailored to the user's location | AI |
| com.google.android.gms.permission.AD_ID | Monetization. See Enable monetization. | Core |
Initialize the SDK
Call initSDK once, in your Application subclass, before any Genuin view is created:
class YourApplication : Application() {
override fun onCreate() {
super.onCreate()
GenuinSDK.initSDK(this, "<<YOUR_API_KEY>>") // production
// GenuinSDK.initSDK(this, "<<YOUR_API_KEY>>", genuinEnv = GenuinEnv.QA) // QA environment
}
}genuinEnv defaults to GenuinEnv.PROD. Use GenuinEnv.QA only with a key issued for the QA environment; keys aren't shared between environments. Calling initSDK from an Activity works too, but views created before the call fail to load.
Note
Don't ship a build that points at QA. The two environments have separate brands, users, and content.
Replace the loading animation
The SDK shows a Lottie animation while content loads. To use your own, add a Lottie file named exactly loader_mix.json to res/raw/; a resource with that name overrides the SDK's.
Show an embed
An embed is a feed of videos from a community, group, or your whole brand. It's configured in Brand Control Center and rendered in one of four layouts: carousel, grid, feed, or dynamic. The layout comes from the embed's configuration, so the same code shows whatever the embed is set to.
For a carousel or grid inside your own screen, use GenuinEmbedView. For a full-screen feed or a standard wall that takes over a screen, use FeedEmbedFragment.
Add a GenuinEmbedView
Give the view a fixed height. A container with wrap_content height renders nothing.
<LinearLayout
android:id="@+id/llCarouselContainer"
android:layout_width="match_parent"
android:layout_height="300dp">
<com.begenuin.core.ui.customview.embed.GenuinEmbedView
android:id="@+id/genuinEmbedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:interTileSpacing="12dp"
app:leftInset="12dp"
app:topInset="12dp"
app:rightInset="12dp"
app:bottomInset="12dp"
app:tileCornerRadius="12dp" />
</LinearLayout>Then configure and load it. The setters return the view, so they chain:
val params = hashMapOf<String, Any>( // optional: pre-fills sign-up for SSO users
"name" to "John Doe",
"email" to "john.doe@example.com",
"nickname" to "john",
"mobile" to "{{PHONE_NUMBER}}"
)
val contextualParams = hashMapOf<String, Any>( // optional: tailors the feed to the screen and viewer
"page_context" to "{{PAGE_CONTEXT}}",
"geo" to JSONObject().put("lat", 40.7128).put("long", -74.0060).toString()
)
genuinEmbedView
.setEmbedParams(
embedId = "{{EMBED_ID}}",
uniqueId = "{{UNIQUE_ID}}", // optional
interactionDeepLink = "{{DEEP_LINK}}", // optional
isDirectDeepLinkEnabled = false,
isShowProfileEnabled = false
)
.setActivity(this)
.setSSOToken("{{SSO_TOKEN}}") // optional: signs the viewer in
.setParams(params)
.setContextualParams(contextualParams)
.setInterTileSpacing(24) // pixels
.setInsets(left = 24, top = 24, right = 24, bottom = 24) // pixels
.setTileCornerRadius(24) // pixels
.load()To create the view in code instead of XML, construct GenuinEmbedView(context), add it to a container with a fixed height, and configure it the same way.
#### Embed parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| embedId | String | Required | The embed to load, from Brand Control Center. |
| uniqueId | String | none | Set a different value for each instance when you show the same embed on several screens, or more than once on one screen. |
| interactionDeepLink | String | none | A URL to open instead of the SDK's own flow when the viewer interacts in the full-screen view. The URL is validated; an invalid URL makes the redirect fail silently. |
| isDirectDeepLinkEnabled | Boolean | false | When true, interactions open the specific video in your white-labelled app and interactionDeepLink is ignored. |
| isShowProfileEnabled | Boolean | false | When true, signed-in viewers see their profile picture in the full-screen view, with access to account settings and sign-out. |
#### User parameters (setParams)
Pass these only together with an SSO token. They pre-fill the viewer's Genuin profile when their account is created.
| Key | Type | Description |
|---|---|---|
| name | String | Display name |
| String | Email address | |
| mobile | String | Phone number |
| nickname | String | Used as the viewer's nickname if it's free; otherwise the SDK generates one |
| profile_image | String | URL of a profile image to show in the SDK |
#### Contextual parameters (setContextualParams)
| Key | Type | Description |
|---|---|---|
| page_context | String | What the current screen is about. The feed is ranked toward it. |
| geo | JSON string | {"lat": <double>, "long": <double>} to rank by location |
Placements accept a longer list. See Contextual parameters for placements.
#### Layout setters
| Setter | XML attribute | Default |
|---|---|---|
| setInterTileSpacing(px) | app:interTileSpacing | 8 dp |
| setInsets(left, top, right, bottom) | app:leftInset, app:topInset, app:rightInset, app:bottomInset | left 16 dp, top 8 dp, right 16 dp, bottom 0 dp |
| setTileCornerRadius(px) | app:tileCornerRadius | 8 dp |
XML attributes take dp. The programmatic setters take pixels, so convert with your display density.
Corner radius, insets, and tile spacing on a carousel embed.
Control playback with the lifecycle
Videos autoplay. Pause them when the screen leaves the foreground and release the player when it's destroyed, or playback continues in the background:
override fun onResume() { super.onResume(); genuinEmbedView.play() }
override fun onPause() { genuinEmbedView.pause(); super.onPause() }
override fun onDestroy() { genuinEmbedView.release(); super.onDestroy() }Load asynchronously
load() shows the view immediately with a shimmer while data arrives. To keep the view hidden until the feed is ready, use loadAsync:
genuinEmbedView.loadAsync { success ->
genuinEmbedView.visibility = if (success) View.VISIBLE else View.GONE
}success is false when the embed doesn't exist, the key is wrong, or there's no network.
Show a full-screen feed or standard wall
Both use FeedEmbedFragment; the embed's configuration decides which layout appears. Add a FrameLayout that fills the screen and insert the fragment:
import com.begenuin.core.ui.fragment.FeedEmbedFragment
val fragment = FeedEmbedFragment.newInstance(
embedId = "{{EMBED_ID}}",
uniqueId = "{{UNIQUE_ID}}", // optional
interactionDeepLink = "{{DEEP_LINK}}", // optional
isDirectDeepLinkEnabled = false,
ssoToken = "{{SSO_TOKEN}}", // optional
params = params, // optional
contextualParams = contextualParams, // optional
isShowProfileEnabled = false
)
supportFragmentManager.beginTransaction()
.add(R.id.frameLayout, fragment)
.addToBackStack("GenuinFeed")
.commit()The parameters mean the same as on GenuinEmbedView. The fragment manages its own playback lifecycle.
Use Jetpack Compose
Wrap the view in AndroidView. The view needs an Activity, so resolve it from LocalContext:
@Composable
fun GenuinCarousel(embedId: String, modifier: Modifier = Modifier) {
val activity = LocalContext.current as? Activity
?: throw IllegalStateException("GenuinEmbedView needs an Activity context")
AndroidView(
modifier = modifier, // give it a height, e.g. Modifier.fillMaxWidth().height(400.dp)
factory = { context ->
GenuinEmbedView(context)
.setEmbedParams(embedId)
.setActivity(activity)
.load()
}
)
}For a full-screen feed in Compose, host FeedEmbedFragment in a FragmentContainerView inside AndroidView, and add the fragment only when findFragmentById returns null so recomposition doesn't add it twice.
Show a placement
A placement is an embed whose content is chosen at render time from the context you pass. That context is the page, the viewer's location and segment, and the communities or users to draw from. It needs a placement ID and a style ID, both from Brand Control Center. It renders through GenuinPlacementView, which has the same setters and lifecycle as GenuinEmbedView.
genuinPlacementView
.setPlacementParams(
placementId = "{{PLACEMENT_ID}}",
styleId = "{{STYLE_ID}}",
uniqueId = "{{UNIQUE_ID}}", // optional
interactionDeepLink = "{{DEEP_LINK}}", // optional
isDirectDeepLinkEnabled = false
)
.setActivity(this)
.setSSOToken("{{SSO_TOKEN}}") // optional
.setParams(params) // optional
.setContextualParams(contextualParams)
.load()The XML element is com.begenuin.core.ui.customview.embed.GenuinPlacementView, with the same layout attributes as the embed view.
Contextual parameters for placements
Every key is optional. JSON values are passed as strings (JSONObject.toString()); lists as JSONArray.
| Key | Type | Description |
|---|---|---|
| page_context | String | What the current screen is about |
| previous_page_context | String | What the previous screen was about |
| user_context | String | Anything else you know about the viewer's intent |
| url | String | The web URL this screen corresponds to |
| time | Number | A Unix timestamp to rank against scheduled content |
| geo | JSON | lat, long (doubles), and radius_limit |
| place | JSON | country (code), state, city, zipcode |
| user_segments | JSON | age, min_age, max_age, segment, gender (M, F, or O), race |
| user_interest | JSON array of strings | Interests to rank toward |
| brands_ids | JSON array | Restrict to these brands |
| community_ids | JSON array | Restrict to these communities |
| loop_ids | JSON array | Restrict to these groups |
| posted_by_user_ids | JSON array | Restrict to posts by these users |
The _ids keys scope the feed; the others only change its order. Which signals a placement honours is set per placement under Grow > Placement > Implement in Brand Control Center.
Sign users in
Viewers can use embeds without an account. To let them post, comment, or follow as themselves, sign them in with single sign-on. Your server issues an SSO token for the user, and the SDK creates or matches their Genuin account from it. Genuin's own login screens never appear.
Sign in with an SSO token
Call ssoLogin once your app has authenticated the user. Pass the same user parameters you'd pass to an embed; they're used the first time the account is created.
GenuinSDK.ssoLogin(
context = this,
ssoToken = "{{SSO_TOKEN}}",
params = params // optional
) { success ->
// success is false when the token is invalid or expired
}// Java
GenuinSDK.INSTANCE.ssoLogin(context, "{{SSO_TOKEN}}", params, success -> {
return Unit.INSTANCE;
});You can also set the token on an individual view with setSSOToken. ssoLogin signs the user in for every view in the app.
Sign the user out of the SDK when they sign out of your app:
GenuinSDK.ssoLogout(context = this) { success -> }Let the SDK ask your app to sign in
If the viewer isn't signed in and taps an action that needs an account, the SDK calls back into your app instead of showing its own login. Register the callback once, run your login flow in it, and finish with ssoLogin:
GenuinSDK.registerInterface(object : GenuinInterface {
override fun onLogin(activity: Activity) {
// Start your app's sign-in flow. When it completes:
// GenuinSDK.ssoLogin(activity, "{{SSO_TOKEN}}")
}
})Without a registered interface, the SDK uses its own sign-in screens.
For how tokens are issued and validated, see Choose how members sign in to the Genuin SDK and How Auto Login works.
Handle deep links
Links to Genuin content, such as a shared video or an invitation, use your white-labelled community domain. To open them in your app instead of the browser:
- White-label your community so links use your domain.
- Set up Android App Links for that domain in your app.
- Send your Genuin contact your package name and the SHA-256 fingerprint of every certificate that signs your app, including the Google Play app signing key. Genuin serves
assetlinks.jsonon your white-labelled domain from your brand's configuration, so you don't host it. Handle Genuin deep links in your app shows how to check it. - When your app receives a link, hand it to the SDK. You can do this as soon as the intent arrives, or after your home screen has loaded if you want it to sit behind your own UI:
GenuinSDK.handleDeepLink(context = this, deeplinkData = intent)Android has no check method like iOS's willHandleDeepLink. Pass only links on your Genuin domain, because the SDK drops a link it doesn't recognize instead of opening the browser.
Intercept link-outs
Videos can carry link-outs to product pages. When a link-out points at a URL your app handles, the SDK gives you the intent instead of opening the browser:
GenuinSDK.registerLinkOutInterceptor(object : GenuinLinkOutInterceptor {
override fun onLinkOutIntercept(intent: Intent, activity: Activity) {
// Route the intent inside your app
}
})Handle push notifications
Genuin sends notifications for replies, mentions, and community activity through Firebase Cloud Messaging (FCM). Your app owns the FCM integration, and the SDK registers the token and builds the notifications it recognizes. Genuin also needs your Firebase project's credentials to send anything; Set up push notifications in the Genuin SDK covers that step, the payload, and every notification type.
- Create the app in the Firebase console, add
google-services.jsonto your project, and add the FCM client SDK. - Declare
POST_NOTIFICATIONSand request it at runtime on Android 13 and later. - Register the FCM token with the SDK, on first launch and whenever it changes:
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) GenuinSDK.registerFCMToken(this, task.result)
}- In your
FirebaseMessagingService, let the SDK handle its own messages while the app is in the foreground, and re-register on token refresh:
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
if (GenuinSDK.willHandleForegroundNotification(data)) {
GenuinSDK.handleForegroundNotifications(
context = this,
data = data,
message = remoteMessage.notification?.body ?: "",
title = remoteMessage.notification?.title,
smallNotificationIcon = R.drawable.ic_notification
)
} else {
// Your app's other notifications
}
}
override fun onNewToken(token: String) {
GenuinSDK.registerFCMToken(this, token)
}
}- When the user taps a notification while the app is in the background or closed, Android delivers the payload to your launcher activity. Pass it on there:
if (GenuinSDK.willHandleNotification(intent)) {
GenuinSDK.handleBackgroundNotifications(context = this, intent = intent)
}willHandleForegroundNotification and willHandleNotification return false for notifications that aren't the SDK's, so your own handling is unaffected.
Listen to SDK callbacks
Register these on GenuinSDK to react to what happens inside Genuin views:
| Callback | Interface | Fires when |
|---|---|---|
| registerEventCallback | GenuinEventCallback.onEventCallback(name, properties) | The SDK records an analytics event, for forwarding to your own analytics |
| registerPlayerCallback | GenuinPlayerCallback (onPlay, onPause, onError, onAudioStateChanged, onFullScreenOpen, onFullScreenClose, …) | Playback or full-screen state changes, for pausing your own media or adjusting your UI |
| registerInteractionCallback / unregisterInteractionCallback | GenuinInteractionCallback.onInteraction(data, activity, continue) | The viewer interacts with a post; call continue to let the SDK proceed |
| registerInterface | GenuinInterface.onLogin(activity) | The viewer needs to sign in. See Sign users in. |
| registerLinkOutInterceptor | GenuinLinkOutInterceptor.onLinkOutIntercept(intent, activity) | A link-out targets a URL your app handles |
The SDK requests audio focus when a video plays with sound. Call GenuinSDK.abandonAudioFocus() if your app needs it back, and GenuinSDK.releasePlayerCache() to free the player's disk cache.
Enable monetization
If your brand runs ads in embeds, Google Play requires the app to declare its use of the advertising ID. Add the permission:
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />Then declare it in Google Play Console: open the app, go to Policy and programs > App content, and find Advertising ID. Click Manage, answer Yes to using the advertising ID, complete the questionnaire, and submit. Builds without the declaration are rejected at review.
Limitations
- One version for all modules:
core,ui,camera, andaimust be the same version. Mismatches fail at runtime. - Minimum API 24: Forcing a lower
minSdkwithtools:overrideLibrarylets the app build, but SDK features fail on devices below API 24. - Fixed-height containers: Embed and placement views render nothing inside a
wrap_contentheight. - Activity context: Views need an
Activity, set withsetActivity; an application context doesn't work. - Portrait only: The SDK doesn't lay out for landscape, tablets, or TV.
- Playback follows your lifecycle: Call
play,pause, andreleasefrom the host screen, or audio continues in the background. - Deep links need the domain setup:
handleDeepLinkonly resolves links on a white-labelled domain with a validassetlinks.json. - Camera module size: Adding
cameragrows the app by about 48 MB before compression.

