惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
G
Google Developers Blog
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Check Point Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
D
Docker
Jina AI
Jina AI
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
L
LINUX DO - 最新话题
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
MongoDB | Blog
MongoDB | Blog
Attack and Defense Labs
Attack and Defense Labs
Webroot Blog
Webroot Blog
A
About on SuperTechFans
Schneier on Security
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Microsoft Azure Blog
Microsoft Azure Blog
F
Fortinet All Blogs
IT之家
IT之家
The Last Watchdog
The Last Watchdog
腾讯CDC
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
B
Blog
Recorded Future
Recorded Future
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
SegmentFault 最新的问题
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
T
Threatpost
H
Heimdal Security Blog
Cloudbric
Cloudbric
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog

Inside Nutrient

A guide to the invisible work behind documents Introducing Nutrient Documents for Salesforce: Native document generation and signing Document AI vs. traditional OCR: Choosing between OCR, AI, and hybrid pipelines PDF SDK compliance and security evaluation checklist for enterprise teams (2026) Invariant Corp replaces paper processes with Nutrient Workflow and scales without limits What is process mapping? A complete guide Nutrient vs. Conga Composer for Salesforce document generation (2026) Document routing: How to automate document distribution The CTO’s AI playbook: Why accountability architecture beats orchestration Compliance workflow automation: Why built-in compliance is table stakes Workflow diagrams: Examples, symbols, and how to build one that actually runs Digital forms: Replace paper forms with automated workflows Approval workflow software: How to automate approvals Why document-centric automation is different The CEO’s AI playbook: Why decision architecture beats model selection Nutrient SDK product updates for Q1 2026 PDF redaction verification: How to prove sensitive data is permanently removed What is a VPAT? The complete guide to accessibility conformance reports What is PDF/UA? The accessible PDF standard explained Salesforce eSignatures: Generate, sign, and track documents in one flow Online document viewer: Options, tradeoffs, and how to embed one Document viewer for web apps: React, Vue, Angular (2026) Best document viewers in 2026: A buyer’s guide How to edit a PDF in Python: Add text, images, and annotations Nutrient advances Workflow platform with agentic AI for enterprise-grade speed and consistency in document-heavy operations How to create a Salesforce quote template from opportunity data The business case for accessibility: Five ways it drives enterprise value Python PDF library comparison (2026): 7 libraries for developers Why your AI agent hallucinates PDF table data PDF.js limitations: When to upgrade to a commercial PDF SDK How Subject scaled 5× with Nutrient’s PDF SDK without rebuilding its document layer I replaced our sales training with an AI coach that runs in Slack — here’s what broke Redirecting to: https://securitybuzz.com/cybersecurity-news/why-enterprise-permissions-are-ais-most-dangerous-inheritance/ Nutrient .NET SDK vs. iText Core: Complete comparison for .NET developers DocuVieware: Support’s most frequently asked setup questions Introducing Nutrient Workflow How to convert PDF to Word in C# (.NET) When email and spreadsheets stop working: Work order approval workflows for field teams on the move Compliance with confidence: Why document-centric automation is the foundation of your mission Nutrient expands AI Assistant, automating multistep document workflows inside any application What is document generation? A developer’s guide to PDF generation Document Converter data flow and how real-time watermarks skip the queue PDF/UA compliance guide: Requirements, standards, and best practices Computers still can’t understand you How Athena Intelligence built AI agents for regulated enterprises with Nutrient’s document infrastructure How to convert HTML to PDF (2026): 4 methods from browser print to SDK How to build a document extraction pipeline with Nutrient Vision API OCR vs. intelligent document processing: Choosing the right document extraction engine Beyond OCR: How document intelligence eliminates manual processing in regulated industries Nutrient vs. IronPDF: Complete comparison for .NET developers Nutrient vs. Aspose.PDF: Complete comparison for .NET developers Redirecting to: https://fortune.com/2026/02/19/openclaw-who-is-peter-steinberger-openai-sam-altman-anthropic-moltbook/ Lufthansa Systems uses Nutrient to deliver reliable, scalable PDF rendering for pilots worldwide Nutrient vs. Syncfusion: Complete comparison for .NET developers React’s useTransition: The hook you’re probably using wrong First City Monument Bank streamlines banking processes with Nutrient Workflow Redirecting to: https://www.sdcexec.com/warehousing/automation/article/22957364/nutrient-workflow-automation-the-missing-link-in-supply-chain-efficiency The complete guide to digital signatures: PAdES, CAdES, and XAdES explained Nutrient Python SDK: Production-grade document processing for Python Introducing agentic document editing for web applications with AI Assistant Nutrient vs. QuestPDF: Complete comparison for .NET developers How we fixed the GdPicture license expiration (and what to do if you’re affected) Red team security testing with agentic AI The future of healthcare document automation Best healthcare workflow software compared Nutrient SDK product updates for Q4 2025 How Harvey scaled legal document workflows 50 percent MoM without rebuilding infrastructure HIPAA-compliant document management in hospitals How we optimized rendering performance while handling thousands of annotations in React — Part 2 Automated PII removal with Nutrient API Redirecting to: https://www.devopsdigest.com/2026-low-code-no-code-predictions Redirecting to: https://www.kmworld.com/Articles/Editorial/ViewPoints/Leaders-predict-AI-to-continue-permeating-all-aspects-of-KM-in-2026-172594.aspx What are deep agents and how do they solve complex problems? Whipping up document magic: Your easy-bake recipe for Vue and Nutrient Web SDK 🧁 What I’ve learned about product iteration planning while building SDKs Passwordless document signing: Three-layer security guide New zip folder functionality streamlines file management in Document Automation Server The keyboard shortcuts playbook: Taking control of keyboard events in Nutrient Web SDK From experienced engineer to AI beginner: My unexpected journey AI-assisted manual testing: Handling Safari’s PDF rendering and UI quirks How to keep a 20-year-old SDK up to date How we optimized rendering performance while handling thousands of annotations in React — Part 1 Nutrient announces new executive hires to accelerate next phase of growth High performance UI using web workers Automate document conversion at scale with Python and Nutrient DCS From curiosity to PLG (and AI): My journey to understanding product-led growth Prost to progress: One year as Nutrient Pigeon usage at Nutrient: Bridging native SDKs to Flutter Modernizing CI build servers: How to migrate from Chef to Ansible Unix man pages: AI-friendly documentation since 1971 Consistent hashing for even load distribution Best AI redaction APIs: Complete comparison guide for 2025 Why AI document redaction matters for modern security From coding to coordinating: How AI transformed my workflow What is intelligent document processing (IDP)? A complete guide Enterprise PDF SDKs: Best PSPDFKit (now Nutrient) alternatives Nutrient SDK product updates for Q3 2025 GdPicture support best practices Redacting sensitive data with Nutrient AI redaction API How AI is transforming the customer experience at Nutrient: From instant answers to intelligent support
From Zero to AI: Building custom chat interfaces with Nutrient on Android
Akshay Sharma · 2025-07-30 · via Inside Nutrient

Table of contents

    From Zero to AI: Building custom chat interfaces with Nutrient on Android

    Want to add intelligent document interaction to your Android app? This step-by-step guide shows you how to integrate Nutrient AI Assistant with a completely custom user interface (UI) that matches your app’s design.

    Use the Nutrient SDK to embed responsive, assistant-ready PDF views into your Jetpack Compose UI.

    Instead of using the default AI Assistant interface, you’ll learn to build your own chat UI using Jetpack Compose, handle streaming responses, and create a seamless user experience. This post will walk you through a practical implementation using a real Android app as reference.

    What you’ll build

    • A custom chat interface with Material Design 3
    • Real-time streaming AI responses
    • Document-aware Q&A functionality
    • Clean architecture using the Repository pattern and MVI (Model-View-Intent) state management

    Prerequisites

    • Basic Android development experience
    • Familiarity with Jetpack Compose
    • Understanding of Kotlin coroutines

    Step 1 — Set up the AI Assistant backend

    First, you’ll need the Nutrient AI Assistant service running locally to handle document processing and AI responses.

    Quick setup with Docker

    git clone https://github.com/PSPDFKit/ai-assistant-demo

    cd ai-assistant-demo

    docker-compose up

    This starts the service on http://localhost:3000. For more details, refer to the Nutrient AI Assistant documentation.

    Keep this terminal open — you’ll need the service running throughout development.

    Step 2 — Set up your Android project

    Add Nutrient SDK dependencies

    In your settings.gradle.kts, add the Nutrient Maven repository:

    dependencyResolutionManagement {

    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

    repositories {

    google()

    mavenCentral()

    maven { url = uri("https://my.nutrient.io/maven") }

    }

    }

    In your app’s build.gradle.kts, add the Nutrient dependency:

    dependencies {

    implementation("io.nutrient:nutrient:LATEST_VERSION")

    }

    For more information on adding the Nutrient SDK to your app and latest version, refer to the Nutrient documentation.

    Reference implementation

    To see these concepts in action, check out StoryVoyage(opens in a new tab), which is a real-world book reading app that showcases AI Assistant integration. The app demonstrates how readers can ask questions about books they’re reading, getting instant answers about plot details, character analysis, and themes.

    git clone https://github.com/akshay2211/StoryVoyage.git

    cd StoryVoyage

    StoryVoyage(opens in a new tab) is a fitting example to reference, as it addresses a real user problem: making reading more interactive and educational.

    Step 3 — Build the custom chat interface

    Now comes the fun part — building your custom chat interface! This is where your app’s personality shines. Instead of using the default AI Assistant UI, you’ll create a completely custom experience that matches your brand.

    Step 3.1 — Define your data models

    Start by creating the data structures that will power your chat experience:

    enum class MessageStatus {

    SENDING, SENT, FAILED

    }

    // AI Assistant events for reactive programming.

    sealed class AiAssistantEvents {

    object Chat : AiAssistantEvents()

    object Success : AiAssistantEvents()

    object Loading : AiAssistantEvents()

    data class Error(val message: String) : AiAssistantEvents()

    }

    Step 3.2 — Create state management with the MVI pattern

    Implement clean state management using the MVI pattern:

    // UI State — represents what the user sees.

    data class AiAssistantState(

    val isLoading: Boolean = false,

    val messages: List<ChatMessage> = emptyList(),

    val inputText: String = "",

    val isRecording: Boolean = false,

    val currentDocumentId: String? = null,

    val error: String? = null

    )

    // User Intents — represents what the user wants to do.

    sealed interface AiAssistantIntent {

    data class SendMessage(val message: String) : AiAssistantIntent

    data class UpdateInputText(val text: String) : AiAssistantIntent

    }

    Step 3.3 — Build the main chat screen

    Create the main composable that brings everything together:

    @Composable

    fun AiAssistantScreen(

    viewModel: AiAssistantViewModel = koinViewModel(),

    modifier: Modifier = Modifier

    ) {

    val state by viewModel.state.collectAsState()

    Column(

    modifier = modifier

    .fillMaxSize()

    .background(MaterialTheme.colorScheme.background)

    ) {

    // Chat header with document info.

    ChatHeader(

    documentName = state.currentDocumentId ?: "Document",

    isOnline = !state.error.isNullOrEmpty().not()

    )

    // Messages list.

    MessagesList(

    messages = state.messages,

    isLoading = state.isLoading,

    modifier = Modifier.weight(1f)

    )

    // Input area.

    ChatInput(

    inputText = state.inputText,

    onTextChanged = { viewModel.processIntent(AiAssistantIntent.UpdateInputText(it)) },

    onSendMessage = { viewModel.processIntent(AiAssistantIntent.SendMessage(it)) },

    )

    }

    }

    Step 3.4 — Create the messages list component

    Build a smooth, performant messages list with proper animations:

    @Composable

    fun MessagesList(

    messages: List<ChatMessage>,

    isLoading: Boolean,

    modifier: Modifier = Modifier

    ) {

    val listState = rememberLazyListState()

    // Auto-scroll to the bottom when new messages arrive.

    LaunchedEffect(messages.size) {

    if (messages.isNotEmpty()) {

    listState.animateScrollToItem(0) // `reverseLayout` = `true`, so 0 is the bottom.

    }

    }

    LazyColumn(

    modifier = modifier.fillMaxSize(),

    state = listState,

    reverseLayout = true, // New messages appear at the bottom.

    contentPadding = PaddingValues(16.dp),

    verticalArrangement = Arrangement.spacedBy(8.dp)

    ) {

    // Loading indicator at the top.

    if (isLoading) {

    item {

    TypingIndicator()

    }

    }

    // Messages in reverse order (newest first in data, but appears at bottom).

    items(

    items = messages.reversed(),

    key = { it.id }

    ) { message ->

    MessageItem(

    message = message,

    modifier = Modifier.animateItemPlacement()

    )

    }

    }

    }

    Step 3.5 — Design smart chat blocks

    Create intelligent message bubbles that adapt to content type:

    @Composable

    fun MessageItem(

    message: ChatMessage,

    modifier: Modifier = Modifier

    ) {

    val isFromUser = message.isFromUser

    Row(

    modifier = modifier

    .fillMaxWidth()

    .padding(horizontal = 8.dp, vertical = 4.dp),

    horizontalArrangement = if (isFromUser) Arrangement.End else Arrangement.Start

    ) {

    if (!isFromUser) {

    // AI avatar.

    AsyncImage(

    model = R.drawable.ai_avatar,

    contentDescription = "AI Assistant",

    modifier = Modifier

    .size(32.dp)

    .clip(CircleShape)

    .align(Alignment.Bottom)

    )

    Spacer(modifier = Modifier.width(8.dp))

    }

    // Message bubble.

    Surface(

    shape = RoundedCornerShape(

    topStart = 16.dp,

    topEnd = 16.dp,

    bottomStart = if (isFromUser) 16.dp else 4.dp,

    bottomEnd = if (isFromUser) 4.dp else 16.dp

    ),

    color = if (isFromUser)

    MaterialTheme.colorScheme.primary

    else

    MaterialTheme.colorScheme.surfaceVariant,

    modifier = Modifier.widthIn(max = 280.dp)

    ) {

    Column(

    modifier = Modifier.padding(12.dp)

    ) {

    // Message content with Markdown support.

    if (message.isMarkdown && !isFromUser) {

    MarkdownText(

    markdown = message.content,

    color = MaterialTheme.colorScheme.onSurfaceVariant

    )

    } else {

    Text(

    text = message.content,

    color = if (isFromUser)

    MaterialTheme.colorScheme.onPrimary

    else

    MaterialTheme.colorScheme.onSurfaceVariant,

    style = MaterialTheme.typography.bodyMedium

    )

    }

    // Message status and timestamp.

    Row(

    modifier = Modifier

    .fillMaxWidth()

    .padding(top = 4.dp),

    horizontalArrangement = Arrangement.End,

    verticalAlignment = Alignment.CenterVertically

    ) {

    Text(

    text = formatTimestamp(message.timestamp),

    style = MaterialTheme.typography.labelSmall,

    color = if (isFromUser)

    MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f)

    else

    MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)

    )

    if (isFromUser) {

    Spacer(modifier = Modifier.width(4.dp))

    MessageStatusIcon(status = message.status)

    }

    }

    }

    }

    if (isFromUser) {

    Spacer(modifier = Modifier.width(8.dp))

    // User avatar.

    AsyncImage(

    model = R.drawable.user_avatar,

    contentDescription = "You",

    modifier = Modifier

    .size(32.dp)

    .clip(CircleShape)

    .align(Alignment.Bottom)

    )

    }

    }

    }

    Step 3.6 — Add polish with loading and error states

    Complete the UI with loading indicators and error handling:

    @Composable

    fun TypingIndicator() {

    Row(

    modifier = Modifier.padding(16.dp),

    verticalAlignment = Alignment.CenterVertically

    ) {

    // AI avatar.

    AsyncImage(

    model = R.drawable.ai_avatar,

    contentDescription = "AI Assistant",

    modifier = Modifier

    .size(32.dp)

    .clip(CircleShape)

    )

    Spacer(modifier = Modifier.width(8.dp))

    // Animated typing dots.

    Surface(

    shape = RoundedCornerShape(16.dp),

    color = MaterialTheme.colorScheme.surfaceVariant

    ) {

    Row(

    modifier = Modifier.padding(16.dp, 12.dp),

    horizontalArrangement = Arrangement.spacedBy(4.dp)

    ) {

    repeat(3) { index ->

    val infiniteTransition = rememberInfiniteTransition()

    val alpha by infiniteTransition.animateFloat(

    initialValue = 0.3f,

    targetValue = 1f,

    animationSpec = infiniteRepeatable(

    animation = tween(600),

    repeatMode = RepeatMode.Reverse

    )

    )

    Box(

    modifier = Modifier

    .size(8.dp)

    .background(

    MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha),

    CircleShape

    )

    )

    }

    }

    }

    }

    }

    Status icon for messages:

    @Composable

    fun MessageStatusIcon(status: MessageStatus) {

    val (icon, color) = when (status) {

    MessageStatus.SENDING -> Icons.Default.Schedule to MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f)

    MessageStatus.SENT -> Icons.Default.Done to MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f)

    MessageStatus.FAILED -> Icons.Default.Error to MaterialTheme.colorScheme.error

    }

    Icon(

    imageVector = icon,

    contentDescription = status.name,

    modifier = Modifier.size(16.dp),

    tint = color

    )

    }

    Pro tips for step 3

    • Use animateItemPlacement() for smooth message animations.
    • Implement proper state hoisting to keep components testable.
    • Consider implementing message retry functionality.
    • Use LaunchedEffect to handle side effects properly.

    Step 4 — Connect to the AI Assistant service (ViewModel and Repository)

    Now you’ll build the business logic layer that connects your UI to the Nutrient AI Assistant service. This step covers the ViewModel and Repository implementation.

    Step 4.1 — Create the Repository interface

    First, define the contract for AI Assistant operations:

    interface AiAssistantRepository {

    // Stream of responses from AI assistant.

    val responseStream: Flow<CompletionResponse?>

    // Initialize with document provider and identifiers.

    suspend fun initialize(

    pdfDocument: PdfDocument,

    dataProvider: DataProvider,

    documentIdentifiers: DocumentIdentifiers,

    isRefresh: Boolean = false

    ): Boolean

    // Send a message to AI assistant.

    suspend fun sendMessage(message: String, documentId: String)

    // Terminate AI assistant.

    suspend fun terminate()

    }

    Step 4.2 — Implement the data source

    Create the data source that handles direct communication with the AI Assistant service:

    class AiAssistantDataSourceImpl(

    private val context: Context

    ) : AiAssistantDataSource {

    var aiAssistant: AiAssistant? = null

    override val responseState: Flow<CompletionResponse?>

    get() = aiAssistant?.responseState ?: emptyFlow()

    override suspend fun initialize(

    pdfDocument: PdfDocument,

    dataProvider: DataProvider,

    documentIdentifiers: DocumentIdentifiers,

    isRefresh: Boolean

    ): Boolean {

    return try {

    val session = pdfDocument.title?.replace(titleRegex, "") ?: "default-session"

    val aiAssistantConfiguration = AiAssistantConfiguration(

    "http://localhost:4000",

    JwtGenerator.generateJwtToken(

    context,

    claims = mapOf(

    "document_ids" to listOf(documentIdentifiers.permanentId),

    "session_ids" to listOf(session),

    "request_limit" to mapOf("requests" to 30, "time_period_s" to 1000 * 60)

    )

    ),

    session

    )

    aiAssistant = standaloneAiAssistant(context, aiAssistantConfiguration)

    pdfDocument.setAiAssistant(aiAssistant!!)

    aiAssistant?.initialize(dataProvider, documentIdentifiers, isRefresh)

    true

    } catch (e: Exception) {

    Log.e("AiAssistant", "Failed to initialize AI Assistant with document provider", e)

    false

    }

    }

    override suspend fun emitMessage(message: String, documentId: String) {

    val id = aiAssistant?.identifiers?.permanentId ?: ""

    aiAssistant?.emitMessage(message, id)

    }

    override suspend fun terminate() {

    aiAssistant?.terminate()

    }

    }

    Step 4.3 — Build the ViewModel

    Create the ViewModel that manages UI state and coordinates use cases:

    class AiAssistantViewModel(

    private val repository: AiAssistantRepository

    ) : ViewModel() {

    private val _state = MutableStateFlow(AiAssistantState())

    val state: StateFlow<AiAssistantState> = _state.asStateFlow()

    init {

    observeAiResponses()

    }

    fun processIntent(intent: AiAssistantIntent) {

    when (intent) {

    is AiAssistantIntent.SendMessage -> {

    sendMessage(intent.message)

    }

    is AiAssistantIntent.UpdateInputText -> {

    _state.update { it.copy(inputText = intent.text) }

    }

    }

    }

    private fun observeAiResponses() {

    viewModelScope.launch {

    repository.responseStream.collect { response ->

    response?.let { handleAiResponse(it) }

    }

    }

    }

    private fun addMessage(message: CompletionResponse) {

    _state.update { currentState ->

    currentState.copy(

    messages = currentState.messages + message

    )

    }

    }

    fun sendMessage(message: String) {

    if (message.isBlank()) return

    viewModelScope.launch {

    // Clear input.

    _state.update { it.copy(inputText = "", isLoading = true) }

    try {

    // Get document ID from current conversation, or use a default.

    val documentId = _state.value.currentDocumentId ?: "default"

    // Send message to AI Assistant.

    repository.sendMessage(message, documentId)

    // The response will be handled by the flow collector in init.

    } catch (e: Exception) {

    _state.update { it.copy(isLoading = false, error = "Failed to send message: ${e.message}") }

    }

    }

    }

    private fun handleAiResponse(response: CompletionResponse) {

    viewModelScope.launch {

    when (response.state) {

    is AiAssistantEvents.Chat -> {

    _state.update { currentState ->

    // Check if this is a new message or if we need to append to an existing one.

    if (response.sender.isEmpty() && (currentState.messages.isEmpty() || !response.end)) {

    // This is a new message — add it to the list.

    currentState.copy(

    isLoading = false,

    messages = currentState.messages + response

    )

    } else if (response.end && response.sender == "AI") {

    // This is the final part of a message — mark it as complete.

    val updatedMessages = currentState.messages.toMutableList()

    if (updatedMessages.isNotEmpty()) {

    val lastIndex = updatedMessages.lastIndex

    updatedMessages[lastIndex] = updatedMessages[lastIndex].copy(end = true)

    }

    currentState.copy(

    isLoading = false,

    messages = updatedMessages

    )

    } else {

    // This is a continuation of the current message — append the content.

    val updatedMessages = currentState.messages.toMutableList()

    if (updatedMessages.isNotEmpty()) {

    val lastIndex = updatedMessages.lastIndex

    updatedMessages[lastIndex] = updatedMessages[lastIndex].copy(

    content = updatedMessages[lastIndex].content + response.content

    )

    }

    currentState.copy(

    isLoading = false,

    messages = updatedMessages

    )

    }

    }

    _state.update { it.copy(isLoading = false) }

    }

    is AiAssistantEvents.Success -> {

    if (response.content.isNullOrEmpty()) return@launch

    addMessage(response)

    _state.update { it.copy(isLoading = false) }

    }

    is AiAssistantEvents.Loading -> {

    addMessage(response)

    _state.update { it.copy(isLoading = true) }

    }

    }

    }

    }

    }

    Step 5 — Wire everything together with dependency injection

    Now you’ll connect all the pieces using Koin for dependency injection and see how everything works together.

    Step 5.1 — Define the Koin module

    Set up dependency injection to manage all your components:

    val aiAssistantModule = module {

    // Data sources.

    single<AiAssistantDataSource> {

    AiAssistantDataSource(get(), get())

    }

    // Repositories.

    single<AiAssistantRepository> {

    AiAssistantRepositoryImpl(get())

    }

    // `ViewModel`s.

    viewModel { AiAssistantViewModel(get()) }

    ...

    }

    Step 5.2 — Data flow summary

    Here’s how data flows through your complete architecture:

    1. User sends message → UI calls viewModel.processIntent(SendMessage)
    2. ViewModel processes intent → Calls repository.sendMessage()
    3. Data source makes API call → Sends HTTP request to the AI Assistant service
    4. AI service responds → Returns CompletionResponse
    5. Data source emits response → Updates responseState StateFlow
    6. ViewModel observes → Converts response to ChatMessage
    7. UI reactsStateFlow triggers recomposition with new message

    This creates a complete reactive flow from UI → business logic → network → AI service and back!

    Step 6 — Test your AI-powered app

    Run the complete integration

    1. Verify the AI Assistant service is running at http://localhost:3000.
    2. Generate a JWT private key and place it in app/src/main/assets/keys/jwt.pem.
    3. Sync your project and resolve any dependencies.
    4. Run the app on a device or emulator.

    Test the user experience

    1. Load a PDF document in your app.
    2. Open the AI Assistant chat interface.
    3. Ask questions and give commands like “What is this document about?” or “Summarize the key points.”
    4. Observe streaming responses appearing in real time.
    5. Test error scenarios by disconnecting the network.

    Conclusion

    You’ve now learned how to integrate Nutrient AI Assistant with a completely custom UI in your Android app. This step-by-step approach gives you full control over the user experience while leveraging powerful AI capabilities.

    Next steps

    1. Explore the complete implementation in the StoryVoyage(opens in a new tab) repository.
    2. Adapt the patterns to your specific use case and design requirements.
    3. Deploy the AI Assistant service to your production infrastructure.
    4. Gather user feedback and iterate on the AI experience.

    Nutrient’s SDK makes it easy to embed rich, assistant-driven PDF workflows in your Android app — combining Jetpack Compose flexibility with intelligent document handling.

    The combination of Nutrient’s powerful document processing and your custom UI creates endless possibilities for intelligent document interaction. Whether you’re building educational apps, productivity tools, or entertainment platforms, these patterns will help you create engaging AI-powered experiences.

    Explore related topics

    Try for free Ready to get started?

    Related SDK articles

    Explore more