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
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.
- 12 de nov. de 2026n8n lança o Nod, um agente open source que roda no seu computador
- 12 de nov. de 2026Claude Code e Codex limitam a engenharia reversa: a OpenAI baniu contas por engano
- 12 de nov. de 2026Engenheiros de QA da SpaceWeb deixaram a IA escrever autotestes: script funcional em 50 minutos
- 12 de nov. de 2026MiniMax abre os pesos do H3-Image: imagens 4K em 10–16 segundos e 16 referências
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.
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.
O essencial da IA — uma vez por semana
Sete histórias que realmente importaram, escolhidas a dedo. Sem ruído nem releases.
Pronto! Verifique seu e-mail para a confirmação.