Explorando IA Multimodal: Criando um Assistente de Visão com Jetpack Compose e Gemini API

A Evolução das Interfaces Móveis com IA Multimodal

No cenário atual do desenvolvimento Android, a expectativa dos usuários transcende a simples exibição de dados. Espera-se que os aplicativos sejam capazes de interpretar o mundo real — entendendo imagens, contetxos e nuances de linguagem natural. Tradicionalmente, implementar o reconhecimento de objetos ou a análise contextual de imagens exigia modelos pesados e lógica complexa. Com a chegada da IA multimodal, como o Gemini da Google, essa barreira foi drasticamente reduzida.

Por que unir Gemini API e Jetpack Compoce?

  • Gemini API: Oferece modelos capazes de processar entradas mistas (texto e imagem) de forma nativa. O modelo gemini-1.5-flash, por exemplo, é otimizado para velocidade e eficiência em dispositivos móveis.
  • Jetpack Compose: Como um toolkit declarativo, o Compose facilita a sincronização da interface com os estados da IA. Quando o modelo retorna uma resposta, a UI reage instantaneamente, simplificando a gestão de estados complexos.

O objetivo deste guia é demonstrar a construção de um assistente inteligente que permite ao usuário carregar uma imagem, fazer uma pergunta sobre ela e receber uma análise detalhada gerada pela IA.

Configuração do Ambiente e Dependências

Antes de codificar, é necessário configurar o acesso à API. Recomenda-se armazenar a chave de API no arquivo local.properties para evitar a exposição em repositórios versionados.

// build.gradle.kts (Module: app)
android {
    // ... configurações padrão
    buildFeatures {
        compose = true
        buildConfig = true
    }
}

dependencies {
    // SDK do Google Generative AI
    implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
    
    // Coil para carregamento de imagens
    implementation("io.coil-kt:coil-compose:2.7.0")
    
    // Lifecycle e ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
}

Implementando a Lógica de Negócio com ViewModel

O VisionViewModel gerenciará o estado da aplicação, incluindo a URI da imagem selecionada, a pergunta do usuário e a resposta processada pelo Gemini.

class VisionViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(VisionUiState())
    val uiState: StateFlow<VisionUiState> = _uiState.asStateFlow()

    private var generativeModel: GenerativeModel? = null

    init {
        val key = BuildConfig.GEMINI_API_KEY
        if (key.isNotEmpty()) {
            generativeModel = GenerativeModel(
                modelName = "gemini-1.5-flash",
                apiKey = key
            )
        }
    }

    fun onImageSelected(uri: Uri?) {
        _uiState.update { it.copy(selectedUri = uri, statusMessage = "Imagem carregada. O que deseja saber?") }
    }

    fun onQueryChanged(query: String) {
        _uiState.update { it.copy(userPrompt = query) }
    }

    fun analyzeImage(context: Context, bitmap: Bitmap?) {
        val model = generativeModel ?: return
        val prompt = _uiState.value.userPrompt

        if (bitmap == null || prompt.isBlank()) return

        viewModelScope.launch {
            _uiState.update { it.copy(isProcessing = true, analysisResult = "Analisando...") }
            
            try {
                val inputContent = content {
                    image(bitmap)
                    text(prompt)
                }
                
                val response = model.generateContent(inputContent)
                _uiState.update { it.copy(analysisResult = response.text ?: "Não foi possível obter uma resposta.") }
            } catch (e: Exception) {
                _uiState.update { it.copy(analysisResult = "Erro na requisição: ${e.localizedMessage}") }
            } finally {
                _uiState.update { it.copy(isProcessing = false) }
            }
        }
    }
}

data class VisionUiState(
    val selectedUri: Uri? = null,
    val userPrompt: String = "",
    val analysisResult: String = "Aguardando interação...",
    val statusMessage: String = "Selecione uma imagem para começar",
    val isProcessing: Boolean = false
)

Construindo a Interface Declarativa

A interface deve ser intuitiva. Utilizaremos o ActivityResultLauncher para selecionar fotos da galeria e componentes do Material Design 3 para estruturar o layout.

@Composable
fun VisionExplorerScreen(viewModel: VisionViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsState()
    val context = LocalContext.current
    
    val galleryLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent()
    ) { uri -> viewModel.onImageSelected(uri) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp)
            .verticalScroll(rememberScrollState()),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Assistente de Visão IA",
            style = MaterialTheme.typography.headlineMedium,
            color = MaterialTheme.colorScheme.primary
        )

        Spacer(modifier = Modifier.height(20.dp))

        // Área de Preview da Imagem
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .height(250.dp)
                .clickable { galleryLauncher.launch("image/*") },
            shape = RoundedCornerShape(12.dp),
            color = MaterialTheme.colorScheme.surfaceVariant
        ) {
            if (state.selectedUri != null) {
                AsyncImage(
                    model = state.selectedUri,
                    contentDescription = "Preview",
                    modifier = Modifier.fillMaxSize(),
                    contentScale = ContentScale.Fit
                )
            } else {
                Box(contentAlignment = Alignment.Center) {
                    Text("Toque para selecionar uma imagem")
                }
            }
        }

        Spacer(modifier = Modifier.height(20.dp))

        OutlinedTextField(
            value = state.userPrompt,
            onValueChange = { viewModel.onQueryChanged(it) },
            label = { Text("O que há nesta imagem?") },
            modifier = Modifier.fillMaxWidth(),
            enabled = !state.isProcessing
        )

        Spacer(modifier = Modifier.height(15.dp))

        Button(
            onClick = {
                val bitmap = state.selectedUri?.let { uriToBitmap(context, it) }
                viewModel.analyzeImage(context, bitmap)
            },
            modifier = Modifier.fillMaxWidth(),
            enabled = state.selectedUri != null && state.userPrompt.isNotBlank() && !state.isProcessing
        ) {
            if (state.isProcessing) {
                CircularProgressIndicator(size = 20.dp, color = Color.White)
            } else {
                Text("Enviar para Gemini")
            }
        }

        Spacer(modifier = Modifier.height(25.dp))

        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
        ) {
            Text(
                text = state.analysisResult,
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

Conversão de Imagem para Bitmap

O SDK do Gemini exige que as imagens sejam enviadas como Bitmap. Abaixo, uma função utilitária para converter URIs de forma compatível com diferentes versões do Android:

fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
    return try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            val source = ImageDecoder.createSource(context.contentResolver, uri)
            ImageDecoder.decodeBitmap(source)
        } else {
            MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
        }
    } catch (e: Exception) {
        null
    }
}

Possbiilidades de Expansão

Uma vez estabelecida a base da aplicação multimodal, as possibilidades de evolução são vastas:

  • Function Calling: Permitir que a IA execute funções locais, como adicionar itens a um calendário ou buscar preços baseados no que ela "vê".
  • Processamento de Vídeo: Utilizar o Gemini para resumir ou identificar eventos em pequenos clipes de vídeo capturados pelo usuário.
  • Otimização de Prompt: Implementar técnicas de Chain-of-Thought no prompt enviado para obter respostas mais lógicas e detalhadas.
  • Streaming de Resposta: Utilizar generateContentStream para exibir a resposta da IA em tempo real, palavra por palavra, melhorando a percepção de performance.

A integração entre Jetpack Compose e Gemini API não apenas torna o desenvolvimento mais ágil, mas também democratiza o acesso a recursos de visão computacional de ponta para desenvolvedores mobile.

Tags: android Jetpack Compose Gemini API Kotlin Multimodal AI

Publicado em 6-22 04:36