Skip to main content
Version: 5.21.0

Jetpack Compose integration

ChatCenterUI is based on Fragment/Activity and has no native Compose API. You can embed it into a Compose graph via AndroidFragment (requires androidx.fragment:fragment-compose:1.8+) or AndroidViewBinding + FragmentContainerView.

ChatCenterUI holder

Create a singleton ChatCenterUI in Application.onCreate() or via DI. The constructor is ChatCenterUI(appContext, logger?); init()/initAsync() is called exactly once per process — full reference in Initialization. The chatConfig is assembled from ChatTransportConfig + ChatNetworkConfig (see Quick start → Step 1).

import android.app.Application
import edna.chatcenter.ui.visual.core.ChatCenterUI

class MyApp : Application() {
lateinit var chatCenterUI: ChatCenterUI
private set

override fun onCreate() {
super.onCreate()
chatCenterUI = ChatCenterUI(applicationContext).apply {
init("YOUR_PROVIDER_UID", "YOUR_APP_MARKER", chatConfig)
}
}
}

Access from a Composable — via the rememberChatCenterUI() helper (definition below).

Helper for the Application variant:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import edna.chatcenter.ui.visual.core.ChatCenterUI

@Composable
fun rememberChatCenterUI(): ChatCenterUI {
val app = LocalContext.current.applicationContext
check(app is MyApp) {
"rememberChatCenterUI(): applicationContext = ${app.javaClass.name}, expected MyApp. " +
"In @Preview, wrap the call with `if (!LocalInspectionMode.current) { ... }` " +
"or substitute via CompositionLocalProvider."
}
return remember { app.chatCenterUI }
}

With asynchronous initialization (initAsync(...)), the onInitComplete callback arrives on the main thread, and it is safe to call authorize(...) from it. Until ready, keep the Composable using the SDK in a "loading" state — for example, via a readiness StateFlow<Boolean> and collectAsStateWithLifecycle(). The full signature is in Initialization → initAsync().

Embedding ChatFragment in Compose

Dependency androidx.fragment:fragment-compose:1.8+

AndroidFragment is shipped in a separate library and is not included in the SDK dependencies. Add:

dependencies {
implementation "androidx.fragment:fragment-compose:1.8.0"
}
import androidx.compose.ui.Modifier
import androidx.fragment.compose.AndroidFragment
import edna.chatcenter.ui.visual.fragments.ChatFragment

@Composable
fun ChatComposable(modifier: Modifier = Modifier) {
AndroidFragment<ChatFragment>(modifier = modifier)
}
AndroidFragment always opens the chat with OpenWay.DEFAULT

The fragment is created via the no-arg constructor, bypassing the ChatFragment.newInstance(@OpenWay from) factory — the OpenWay parameter remains at its default value. The argument key inside the SDK is private, so you cannot pass OpenWay.FROM_PUSH through AndroidFragment(arguments = …). For push-driven openings, use ChatActivity (see below) + a custom PendingIntentCreator in ChatConfigConfiguring the push notification tap action.

An alternative for older Compose versions (1.7-) is AndroidViewBinding with FragmentContainerView:

import androidx.compose.ui.viewinterop.AndroidViewBinding
import com.example.app.databinding.ChatContainerBinding

@Composable
fun ChatComposable(modifier: Modifier = Modifier) {
AndroidViewBinding(ChatContainerBinding::inflate, modifier = modifier) {
// in the layout: <androidx.fragment.app.FragmentContainerView
// android:name="edna.chatcenter.ui.visual.fragments.ChatFragment"
// android:id="@+id/chat_fragment"/>
}
}
ProGuard / R8 for the FragmentContainerView variant

FragmentContainerView instantiates ChatFragment reflectively via android:name. Add a rule that preserves the no-arg constructor:

-keep class edna.chatcenter.ui.visual.fragments.ChatFragment {
public <init>();
}

For AndroidFragment<ChatFragment>() without XML, the rule is not needed — R8 sees the direct class reference.

Rotation and recreation

AndroidFragment<ChatFragment> re-creates ChatFragment via the no-arg constructor on rotation/config change. SDK behavior in split-screen / multi-window / picture-in-picture and in nested FragmentManagers (nested NavHost) is not covered by targeted testing — see Known limitations.

chatCenterUI.getChatFragment() is unreliable after rotation

The SDK stores the reference as a WeakReference and updates it only through the ChatFragment.newInstance() factory method. On a system re-creation (rotation, restore), newInstance() is not called — getChatFragment() returns either null or a stale reference to a detached instance (depending on GC timing). In a Compose scenario, obtain the instance via FragmentManager:

import android.content.Context
import android.content.ContextWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import edna.chatcenter.ui.visual.fragments.ChatFragment

private tailrec fun Context.findFragmentActivity(): FragmentActivity? = when (this) {
is FragmentActivity -> this
is ContextWrapper -> baseContext.findFragmentActivity()
else -> null
}

@Composable
fun rememberChatFragment(): State<ChatFragment?> {
val context = LocalContext.current
// LocalContext.current may be a ContextThemeWrapper (under MaterialTheme),
// not a FragmentActivity directly — walk the baseContext chain.
val activity = remember(context) {
context.findFragmentActivity()
?: error("rememberChatFragment(): host context is not FragmentActivity (${context.javaClass.name})")
}
val state = remember { mutableStateOf<ChatFragment?>(null) }

DisposableEffect(activity) {
val fm = activity.supportFragmentManager
state.value = fm.fragments.filterIsInstance<ChatFragment>().firstOrNull()

val cb = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, ctx: Context) {
if (f is ChatFragment) state.value = f
}

override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
if (f is ChatFragment) state.value = null
}
}
// recursive = true — captures fragments in nested FragmentManagers (NavHost)
fm.registerFragmentLifecycleCallbacks(cb, /* recursive = */ true)
onDispose { fm.unregisterFragmentLifecycleCallbacks(cb) }
}

return state
}

Full-screen chat with BackHandler

ChatFragment has internal navigation (search, image viewing). When embedding it in Compose Navigation, add a BackHandler to call chatFragment.onBackPressed() first, and only close the screen if it returns true. The snippet below is a ready end-to-end Composable: picks up the fragment via rememberChatFragment(), correct back-handling, auto-recovery after rotation:

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.compose.AndroidFragment
import edna.chatcenter.ui.visual.fragments.ChatFragment

@Composable
fun ChatScreen(onClose: () -> Unit) {
val chatFragment by rememberChatFragment()

BackHandler {
// onBackPressed():
// true — close the screen (the SDK did nothing itself)
// false — the SDK handled the press (closed search/preview)
// null — the fragment is not attached (between rotation/recreation) — treat as "close"
val shouldClose = chatFragment?.onBackPressed() ?: true
if (shouldClose) onClose()
}

AndroidFragment<ChatFragment>(modifier = Modifier.fillMaxSize())
}

Opening ChatActivity from a Composable

If the chat must open as a separate screen, launch the built-in ChatActivity with a standard Intent:

import android.content.Intent
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import edna.chatcenter.ui.visual.activities.ChatActivity

@Composable
fun OpenChatButton() {
val context = LocalContext.current
Button(onClick = {
context.startActivity(Intent(context, ChatActivity::class.java))
}) {
Text("Open chat")
}
}

To get the already opened instance, use chatCenterUI.getChatActivity(): ChatActivity?. Unlike getChatFragment(), it is resilient to system re-creation: the internal reference is updated in ChatActivity.onCreate(...).

Themes: ChatTheme vs MaterialTheme

ChatFragment styles its views via the SDK's runtime mechanism (ChatTheme is a Kotlin class instance that the SDK reads during rendering). This is not an XML android:theme and not a Compose MaterialTheme. When embedded in a Compose graph wrapped in MaterialTheme, Compose colors and typography are not inherited into the SDK views — override SDK tokens explicitly via ChatComponents/ChatFlows (see Design system → Themes). The opposite is also true: SDK tokens do not leak into the surrounding Compose graph.