Ao vivo 18/11 · Turma 318 nov às 19:00 MSK — aula prática gratuita de agentes de IA · Turma 3 ao vivo começa 23/11Aula 18/11 →Turma 3 (23/11)
LançamentoAnálise · No radarHabr AI→ original

Desenvolvedor cria cliente KMP para Ollama: dois bugs de serialização e streaming

Um desenvolvedor mostrou como conectar um aplicativo móvel em Kotlin Multiplatform a um servidor Ollama local: UI compartilhada em Compose, Koin para dependências, Ktor para rede. O servidor é aberto para os celulares da rede com uma única variável, OLLAMA_HOST=0.0.0.0. A maior parte do tempo foi gasta em dois bugs discretos: o campo stream que some e uma resposta que chega inteira em vez de em fluxo. Ambos se resolvem com duas linhas, se você souber onde procurar.

Processado por IA de Habr AI; editado por Hamidun News
Desenvolvedor cria cliente KMP para Ollama: dois bugs de serialização e streaming
Fonte: Habr AI. Colagem: Hamidun News.
Ouvir artigo
Ficha técnica e números-chave
ParâmetroDados
Fonte originalHabr AI ↗
Editor-chefe
Dado-chave #1encodeDefaults = true devolve stream=false à requisição JSON
Dado-chave #2Para o fluxo é preciso preparePost().execute, e não um post() comum
Para HTTPusesCleartextTraffic no Android, NSAllowsArbitraryLoads no iOS

Rodar um modelo local num notebook hoje é simples. Mais difícil é criar um bom cliente móvel para ele, que funcione tanto no Android quanto no iPhone e não obrigue a escrever a UI duas vezes. O autor de um artigo no Habr percorreu esse caminho com Kotlin Multiplatform e descreveu com honestidade onde tropeçou. Para quem pretende fazer o mesmo, isso é mais útil do que qualquer teoria.

O servidor: três comandos e uma variável

O Ollama é instalado via brew ou por um script de instalação no Ubuntu. Por padrão o servidor escuta apenas em localhost, então o emulador e um celular real na mesma rede não conseguem alcançá-lo. Para abrir o acesso, o servidor é iniciado com OLLAMA_HOST=0.0.0.0:11434. Depois o modelo é baixado com ollama pull e, nos Macs com chips Apple, o autor usa builds MLX, por exemplo gemma4:12b-mlx. O comando ollama ps mostra que o modelo está inteiramente na memória da GPU (na saída, PROCESSOR 100% GPU).

O servidor pode ser verificado com um curl comum em /api/generate. Se a requisição tiver stream: false, a resposta chega em um único JSON.

O cliente: Compose, Koin, Ktor

O projeto é um app KMP com UI compartilhada: Koin cuida das dependências e do ViewModel, Ktor da rede e Navigation3 da navegação. O autor escolheu não o startKoin global, mas o KoinApplication composable. O contêiner é criado dentro da função App(), de modo que o @Preview no Android Studio não entra em conflito com o estado global, e o código Swift gerado pela IDE não precisa ser tocado. O preço dessa decisão: parte da API fica indisponível, incluindo by inject(), by viewModel() e androidContext(). Como dividir corretamente em módulos um aplicativo grande com essa abordagem, o autor ainda não descobriu.

Xcode e macOS continuam sendo necessários para compilar para iOS. O KMP livra da UI em Swift, mas não das ferramentas da Apple.

Duas armadilhas que custaram horas

A primeira armadilha está ligada ao serializador. O Kotlinx.Serialization, por padrão, não escreve no JSON campos com valores padrão. Se você tem stream = false, o campo simplesmente some da requisição, e sem ele o Ollama ativa o modo de streaming. O resultado: você espera uma resposta e recebe um punhado de linhas JSON. A correção é a configuração Json { encodeDefaults = true }. O Network Inspector do Android Studio ajudou a achar a causa.

A segunda armadilha diz respeito ao próprio streaming. A resposta chegava inteira no final, e o autor suspeitou primeiro de buffering no OkHttp e até separou o código por plataformas com expect/actual. O problema era outro: em vez de post() é preciso usar preparePost() e ler o corpo linha a linha via execute e bodyAsChannel. Cada linha é um chunk JSON separado com os campos message, done e error. No cliente tudo isso vira um Flow de eventos: Token, Thinking, Done e Error. O cancelamento da corrotina fecha a conexão e interrompe a geração. Os tokens de raciocínio (Thinking) chegam separadamente, se o modelo os emitir.

Mais um detalhe: em /api/chat o papel é obrigatório em cada mensagem, enquanto /api/generate não tem papéis, apenas prompt.

O que considerar na prática

Se o servidor funciona por HTTP, no Android é necessário usesCleartextTraffic="true", e para iOS será preciso adicionar NSAllowsArbitraryLoads no Info.plist. Embora, no experimento do autor, o emulador de iPhone tenha deixado as requisições passarem mesmo sem isso. Em produção não se pode usar HTTP sem criptografia nem um servidor em 0.0.0.0 sem autorização: o texto não diz uma palavra sobre proteção, então este é um ambiente doméstico ou interno, e não uma arquitetura pronta.

O projeto ainda está cru: a interface precisa ser refinada e não há histórico de chats. O autor acrescenta que já escreveu um cliente para os modelos em nuvem da cloud.ru com imagens e áudio. A tarefa de renomear o projeto levou cerca de cinco minutos do modelo e aproximadamente 50 rublos, embora o preço dependa muito do modelo escolhido.

Conclusão para o desenvolvedor: se você faz um cliente móvel para o Ollama, ative encodeDefaults desde o início, use preparePost nos fluxos e não procure o problema no OkHttp. O código-fonte está no GitHub (app-z/OllameChatKmp) e pode ser usado como modelo inicial.

História

Perguntas frequentes sobre a notícia

O que se sabe sobre O servidor: três comandos e uma variável?

O Ollama é instalado via brew ou por um script de instalação no Ubuntu. Por padrão o servidor escuta apenas em localhost, então o emulador e um celular real na mesma rede não conseguem alcançá-lo. Para abrir o acesso, o servidor é iniciado com OLLAMA_HOST=0.0.0.0:11434. Depois o modelo é baixado com ollama pull e, nos Macs com chips Apple, o autor usa builds MLX, por exemplo gemma4:12b-mlx. O comando ollama ps mostra que o modelo está inteiramente na memória da GPU (na saída, PROCESSOR 100% GPU).

O que se sabe sobre O cliente: Compose, Koin, Ktor?

O projeto é um app KMP com UI compartilhada: Koin cuida das dependências e do ViewModel, Ktor da rede e Navigation3 da navegação. O autor escolheu não o startKoin global, mas o KoinApplication composable. O contêiner é criado dentro da função App(), de modo que o @Preview no Android Studio não entra em conflito com o estado global, e o código Swift gerado pela IDE não precisa ser tocado. O preço dessa decisão: parte da API fica indisponível, incluindo by inject(), by viewModel() e androidContext().

O que se sabe sobre Duas armadilhas que custaram horas?

A primeira armadilha está ligada ao serializador. O Kotlinx.Serialization, por padrão, não escreve no JSON campos com valores padrão. Se você tem stream = false, o campo simplesmente some da requisição, e sem ele o Ollama ativa o modo de streaming. O resultado: você espera uma resposta e recebe um punhado de linhas JSON. A correção é a configuração Json { encodeDefaults = true }. O Network Inspector do Android Studio ajudou a achar a causa. A segunda armadilha diz respeito ao próprio streaming.

ZK
Zhemal Khamidun — CPO da Alpina Digital, gerente de produto do AlpinaGPT. Cria produtos de IA desde 2019. O Hamidun News é um projeto pessoal que virou um agregador em produção com mais de 6.000 leitores.
✦ Unified AI Workspace · Nomad

Test Frontier LLMs & Autonomous Agents in One Workspace

Run Claude Opus 5.5, GPT-6, Gemini 3.1, and DeepSeek side-by-side in Nomad AI (nomadnet.ai) — unified model routing, deep research, and multimodal workflows under one subscription.

Try Nomad AI →Launch Nomad Agent
O que você acha?
Carregando comentários…