Skip to main content
GemAI is built on a set of reusable base components that enforce architectural patterns and reduce boilerplate code. Understanding these components is essential for extending the application.

Base classes

All major components in GemAI inherit from base classes that provide common functionality:

BaseViewModel

The foundation for all ViewModels, implementing Unidirectional Data Flow (UDF) pattern:
BaseViewModel.kt
interface UIState        // Represents the state of the UI
interface UIEvent        // One-time events (navigation, alerts, toasts)
interface UIAction       // User actions (clicks, inputs)

abstract class BaseViewModel<State : UIState, Event : UIEvent, Action : UIAction>() : ViewModel() {
    abstract fun initialState(): State
    protected abstract fun onActionEvent(action: Action)

    // State management
    private val _uiState = MutableStateFlow(initialState)
    val uiState: StateFlow<State> = _uiState.asStateFlow()
    
    protected val currentState: State
        get() = uiState.value

    protected fun update(updatedState: State.() -> State) = _uiState.update(updatedState)

    // One-time event handling
    private val _uiEventFlow = Channel<Event>(capacity = Channel.BUFFERED)
    val uiEvent = _uiEventFlow.receiveAsFlow()
    
    protected fun sendOneTimeUIEvent(event: Event, delayMillis: Long? = null) {
        launch {
            delayMillis?.let { delay(it) }
            _uiEventFlow.send(event)
        }
    }

    // Public action handler
    fun onAction(action: Action) {
        onActionEvent(action)
    }
}
Define your state, events, and actions, then extend BaseViewModel:
Example ViewModel
data class ChatUIState(
    val prompt: String,
    val chat: List<Message>,
    val isLoading: Boolean,
    val chatId: Long?
) : UIState

sealed class ChatUIEvent : UIEvent {
    data class ShowError(val message: String) : ChatUIEvent()
}

sealed class ChatUIAction : UIAction {
    data class SetPrompt(val prompt: String) : ChatUIAction()
    object Submit : ChatUIAction()
}

@HiltViewModel
class ChatViewModel @Inject constructor(
    private val sendMessageUseCase: SendMessageUseCase
) : BaseViewModel<ChatUIState, ChatUIEvent, ChatUIAction>() {
    
    override fun initialState(): ChatUIState {
        return ChatUIState(
            prompt = "",
            chat = emptyList(),
            isLoading = false,
            chatId = null
        )
    }

    override fun onActionEvent(action: ChatUIAction) {
        when (action) {
            is ChatUIAction.SetPrompt -> update { copy(prompt = action.prompt) }
            ChatUIAction.Submit -> submit()
        }
    }

    private fun submit() {
        update { copy(isLoading = true) }
        viewModelScope.launch {
            sendMessageUseCase.perform(currentState.prompt)
                .onSuccess { update { copy(isLoading = false) } }
        }
    }
}

BaseUseCase

Defines the contract for all business logic operations:
BaseUseCase.kt
interface BaseUseCase<in Params, out Result> {
    // Execute without parameters
    suspend fun perform(): Result = 
        throw NotImplementedError("BaseUseCase perform() not implemented")

    // Execute with parameters
    suspend fun perform(params: Params): Result? =
        throw NotImplementedError("BaseUseCase perform(params) not implemented")

    // Execute as a stream
    fun performStreaming(params: Params): Flow<Result> =
        throw NotImplementedError("BaseUseCase performStreaming() not implemented")

    fun performStreaming(): Flow<Result> = 
        throw NotImplementedError("BaseUseCase performStreaming() not implemented")
}
Use cases encapsulate single business operations. Override only the methods you need: perform(), perform(params), or performStreaming().

BaseDao

Provides common CRUD operations for all Room DAOs:
BaseDao.kt
@Dao
interface BaseDao<T> {
    @Insert(onConflict = OnConflictStrategy.IGNORE) 
    suspend fun insert(entity: T): Long

    @Insert(onConflict = OnConflictStrategy.IGNORE) 
    suspend fun insertAll(entities: List<T>): List<Long>

    @Upsert suspend fun upsert(entity: T): Long
    @Upsert suspend fun upsertAll(entities: List<T>)

    @Update suspend fun update(entity: T)
    @Update suspend fun updateAll(entities: List<T>)

    @Delete suspend fun delete(entity: T)
    @Delete suspend fun deleteAll(entities: List<T>)
}
All DAOs extend BaseDao<T> to inherit standard operations, then add custom queries specific to their entity.

BaseMapper

Defines bidirectional mapping between entities and domain models:
BaseMapper.kt
interface BaseMapper<Entity, Domain> {
    fun mapToDomain(entity: Entity): Domain
    fun mapToEntity(domain: Domain): Entity
}

// Example implementation
object MessageMapper : BaseMapper<MessageEntity, Message> {
    override fun mapToDomain(entity: MessageEntity): Message {
        return with(entity) {
            Message(
                id = id,
                conversationId = conversationId,
                timestamp = timestamp,
                content = content,
                participant = participant,
                status = status,
            )
        }
    }

    override fun mapToEntity(domain: Message): MessageEntity {
        return with(domain) {
            MessageEntity(
                id = id,
                conversationId = conversationId,
                timestamp = timestamp,
                content = content,
                participant = participant,
                status = status,
            )
        }
    }
}

// Extension functions for convenience
fun Message.toEntity() = MessageMapper.mapToEntity(this)
fun MessageEntity.toDomain() = MessageMapper.mapToDomain(this)

AI model components

GemAI uses Google’s Generative AI SDK with custom abstractions:

BaseAIModel

Provides common functionality for all AI models:
BaseAIModel.kt
abstract class BaseAIModel(private val datastoreRepository: DatastoreRepository) {
    protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + exceptionHandler)
    
    protected var modelName: String = AIModel.GEMINI_1_5_FLASH.modelName
        private set
    
    protected var apiKey: String = "AIzaSyAy5BO5bOFOSLtAExAEBz1irLyAIsfFfoI"
        private set
    
    protected val modelBuilder = ModelBuilder.Builder()
    protected val geminiAIModel: GenerativeModel by lazy { getGenerativeModel() }

    init {
        coroutineScope.launch {
            val deferredModelName = async { datastoreRepository.getModel().modelName }
            val deferredApiKey = async { datastoreRepository.getApiKey() }
            modelName = deferredModelName.await()
            deferredApiKey.await()?.let { apiKey = it }
        }
    }

    protected open fun getGenerativeModel(): GenerativeModel {
        return modelBuilder.setApiKey(apiKey).setModel(modelName).build()
    }

    suspend fun testKey(key: String): Boolean {
        return modelBuilder.testKey(key)
    }
}

GemAIModel

Handles chat conversations with conversation history management:
GemAIModel.kt
class GemAIModel @Inject constructor(
    datastoreRepository: DatastoreRepository, 
    private val messageDao: MessageDao
) : BaseAIModel(datastoreRepository) {
    private val chatLock = Mutex()
    private var chat: Chat? = null
    private var activeChatId: Long? = null

    suspend fun sendMessage(conversationId: Long, content: String): Flow<GenerateContentResponse> {
        require(conversationId > 0) { "Invalid conversation ID" }
        require(content.isNotBlank()) { "Message content cannot be blank" }
        
        val activeChat = chatLock.withLock {
            if (conversationId != activeChatId || chat == null) {
                initializeChat(conversationId)
            }
            chat ?: throw IllegalStateException("Chat not initialized")
        }

        return activeChat.sendMessageStream(content)
    }

    private suspend fun initializeChat(conversationId: Long) {
        val history = try {
            messageDao.getMessages(conversationId)
                .map { content(role = it.participant.role) { text(it.content) } }
        } catch (e: Exception) {
            emptyList()
        }
        setChat(geminiAIModel.startChat(history), conversationId)
    }
}
GemAIModel maintains chat state per conversation, automatically loading message history when switching between chats.

SystemAIModel

Handles system-level AI tasks like title generation and prompt suggestions:
SystemAIModel.kt (excerpt)
class SystemAIModel @Inject constructor(
    datastoreRepository: DatastoreRepository
) : BaseAIModel(datastoreRepository) {

    suspend fun generateChatTitle(prompt: String): String? {
        return getGenerativeModel()
            .generateContent(
                """
                System Instruction :$updateChatTitleInstruction,
                Prompt: $prompt
            """.trimIndent()
            )
            .text
    }

    suspend fun generateInitialPrompts(prompt: String): List<PromptDto> {
        val model = baseModel
            .setConfig {
                generationConfig {
                    temperature = 0.7f
                    topP = 0.75f
                    topK = 40
                    responseMimeType = "application/json"
                }
            }
            .build()

        return try {
            model.generateContent(
                "System Instruction : $generateInitialPromptsInstruction, Prompt: $prompt"
            ).text?.let { Json.decodeFromString<List<PromptDto>>(it) } ?: emptyList()
        } catch (e: Exception) {
            emptyList()
        }
    }
}

Component relationships

Here’s how these components work together:

Next steps

Data layer

Learn about Room database and repository implementations

Extending GemAI

Add new features using these base components

Build docs developers (and LLMs) love