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
generateContentStreampara 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.