Skip to main content
Each component’s ViewModel lives in chatuikit-core and manages data fetching, state transitions, real-time listeners, and list operations via StateFlow. The same ViewModel is shared by both Kotlin XML Views and Jetpack Compose modules.

Creating and Providing a ViewModel

By default, each component creates its own ViewModel internally. To customize behavior, create the ViewModel externally using the factory and pass it to the component.
// 1. Create the factory (optionally with a custom repository)
val factory = CometChatConversationsViewModelFactory()

// 2. Create the ViewModel using ViewModelProvider
val viewModel = ViewModelProvider(this, factory)
    .get(CometChatConversationsViewModel::class.java)

// 3. Configure the ViewModel before passing it
viewModel.setConversationsRequestBuilder(
    ConversationsRequest.ConversationsRequestBuilder()
        .setLimit(20)
        .withTags(true)
)

// 4. Pass it to the component
val conversations = findViewById<CometChatConversations>(R.id.conversations)
conversations.setViewModel(viewModel)
This pattern applies to all components — CometChatUsers, CometChatGroups, CometChatMessageList, CometChatCallLogs, etc. Each has a corresponding factory class.

State Observation

ViewModels expose state via Kotlin StateFlow (not LiveData). The key state flows are:
StateFlowTypeDescription
uiStateStateFlow<UIState>Current screen state: Loading, Empty, Error(exception), Content(list)
conversationsStateFlow<List<Conversation>>The current list of conversations
typingIndicatorsStateFlow<Map<String, TypingIndicator>>Active typing indicators by conversation ID
deleteStateStateFlow<DeleteState>Delete operation state: Idle, InProgress, Success, Failure(exception)
playSoundEventSharedFlow<Boolean>Emits when a message sound should play
scrollToTopEventSharedFlow<Unit>Emits when the list should scroll to top

UIState

sealed class UIState {
    object Loading : UIState()
    object Empty : UIState()
    data class Error(val exception: CometChatException) : UIState()
    data class Content(val conversations: List<Conversation>) : UIState()
}

Observing State

// Collect in a lifecycle-aware scope
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            when (state) {
                is UIState.Loading -> { /* show loading */ }
                is UIState.Empty -> { /* show empty */ }
                is UIState.Error -> { /* show error: state.exception */ }
                is UIState.Content -> { /* data ready: state.conversations */ }
            }
        }
    }
}

Configuring Data Fetching

Pass a custom ConversationsRequestBuilder to control what data the component fetches. You can set it during ViewModel creation or directly on the component:
// Option 1: Set on the component directly
conversations.setConversationsRequestBuilder(
    ConversationsRequest.ConversationsRequestBuilder()
        .setLimit(20)
        .setTags(listOf("important", "pinned"))
        .withTags(true)
)

// Option 2: Set on the ViewModel after creation
viewModel.setConversationsRequestBuilder(
    ConversationsRequest.ConversationsRequestBuilder()
        .setLimit(20)
)
Pass the builder object, not the result of .build(). The component calls .build() internally.

ListOperations API

All list-based ViewModels implement ListOperations<T>, giving you a consistent API to manipulate list data:
// Add items
viewModel.addItem(conversation)
viewModel.addItems(conversations)

// Remove items
viewModel.removeItem(conversation)
viewModel.removeItemAt(0)

// Update items
viewModel.updateItem(updatedConversation) { 
    it.conversationId == updatedConversation.conversationId 
}

// Move to top
viewModel.moveItemToTop(importantConversation)

// Query
val items = viewModel.getItems()
val count = viewModel.getItemCount()
val first = viewModel.getItemAt(0)

// Clear
viewModel.clearItems()

Batch Operations

Perform multiple operations in a single emission — critical for performance with rapid updates:
viewModel.batch {
    add(newConversation1)
    add(newConversation2)
    remove(oldConversation)
    moveToTop(pinnedConversation)
}
// Only one StateFlow emission for all four operations

ViewModel Methods

MethodDescription
fetchConversations()Fetch the next page (pagination)
refreshList()Clear and re-fetch from the server (silent refresh)
deleteConversation(conversation)Delete a conversation via the SDK
resetDeleteState()Reset delete state to Idle
setConversationsRequestBuilder(builder)Set a custom request builder for filtering
setDisableReceipt(disable)Disable read receipts
setDisableSoundForMessages(disable)Disable message sounds
setCustomSoundForMessage(rawRes)Set a custom sound resource

Providing a Custom ViewModel

Subclass the ViewModel to override behavior, then inject it:
class MyConversationsViewModel(
    getConversationsUseCase: GetConversationsUseCase,
    deleteConversationUseCase: DeleteConversationUseCase,
    refreshConversationsUseCase: RefreshConversationsUseCase
) : CometChatConversationsViewModel(
    getConversationsUseCase,
    deleteConversationUseCase,
    refreshConversationsUseCase
) {
    // Override list operations to add custom behavior
    override fun addItem(item: Conversation) {
        // Example: filter out blocked users before adding
        val blockedUser = item.conversationWith as? User
        if (blockedUser?.isBlockedByMe == true) return
        super.addItem(item)
    }

    override fun moveItemToTop(item: Conversation) {
        // Example: log when a conversation is moved to top
        Log.d("MyVM", "Moving ${item.conversationId} to top")
        super.moveItemToTop(item)
    }
}
Create it via the factory with a custom repository if needed:
val factory = CometChatConversationsViewModelFactory(
    repository = MyCustomRepository()
)
val viewModel = ViewModelProvider(this, factory)
    .get(CometChatConversationsViewModel::class.java)

conversations.setViewModel(viewModel)

Lifecycle Callbacks

Intercept data loading events on the View:
conversations.setOnLoad { list ->
    Log.d("Conversations", "Loaded ${list.size} conversations")
}

conversations.setOnEmpty {
    Log.d("Conversations", "No conversations found")
}

conversations.setOnError { exception ->
    Log.e("Conversations", "Error: ${exception.message}")
}